Created
March 8, 2024 17:25
alphaTab-issue-1400 - change the version on line 4 between 933 and 938
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<script src="https://cdn.jsdelivr.net/npm/@coderline/alphatab@1.3.0-alpha.938/dist/alphaTab.js"></script> | |
<!-- <script src="https://cdn.jsdelivr.net/npm/@coderline/alphatab@1.3.0-alpha.933/dist/alphaTab.js"></script> --> | |
<meta charset="utf-8" /> | |
<title>Tab Portal</title> | |
<style> | |
:root { | |
--main-bg-color: #5d9150; | |
--main-bg-color-dark: #548348; | |
--main-text-color: #fff; | |
--main-link-color: #00d9ef; | |
--brighter1: #ffffff40; | |
--brighterb: #ffffffb0; | |
--darker1: #00000040; | |
--dark-text-color: #606060; | |
--accent-bg-color: #e6e7b5; | |
--page-bg-color: #f9f9f5; | |
--page-text-color: #000; | |
--page-link-color: #002ae6; | |
} | |
body { | |
background-color: #bbbbbb; | |
margin: 0; | |
} | |
.tp-main, .tp-main *, .tp-main *::after, .tp-main *::before { | |
box-sizing: border-box; | |
} | |
.tp-main { | |
display: flex; | |
width: 100%; | |
max-width: 1800px; | |
margin: auto; | |
box-shadow: 0px 0px 0px 1px #00000062; | |
background-color: #f3f3f3; | |
font-family: sans-serif; | |
} | |
.tp-main a, .tp-main a:visited { | |
text-decoration: none; | |
font-weight: 500; | |
color: var(--page-link-color); | |
} | |
.tp-main a:hover { | |
text-decoration: underline; | |
} | |
.tp-sidebar { | |
flex: 0 0 222px; | |
background-color: var(--main-bg-color-dark); | |
color: var(--main-text-color); | |
display: flex; | |
flex-direction: column; | |
} | |
.tp-sidebar>* { | |
padding: 10px; | |
} | |
.tp-sidebar .tp-header { | |
background-color: var(--accent-bg-color); | |
color: var(--dark-text-color); | |
} | |
.tp-sidebar .tp-logo { | |
font-size: 1.8em; | |
font-weight: bold; | |
margin-bottom: .5em; | |
border-bottom: 1px solid var(--darker1); | |
} | |
.tp-sidebar .tp-section-link { | |
font-size: 1.6em; | |
} | |
.tp-sidebar .tp-section-link:not(:last-child) { | |
margin-bottom: .25em; | |
} | |
.tp-sidebar .tp-menu { | |
flex-grow: 1; | |
} | |
.tp-sidebar .tp-menu .tp-help { | |
background-color: var(--brighterb); | |
color: var(--page-text-color); | |
padding: 4px; | |
border-radius: 4px; | |
} | |
.tp-sidebar .tp-menu .tp-help:not(:first-child) { | |
margin-top: 10px; | |
} | |
.tp-sidebar .tp-menu .tp-help .tp-help-title { | |
font-weight: bold; | |
border-bottom: 1px solid var(--darker1); | |
margin-bottom: .2em; | |
} | |
.tp-sidebar .tp-credits { | |
font-size: 60%; | |
} | |
.tp-content { | |
flex: 1; | |
display: flex; | |
height: 100vh; | |
} | |
.tp-content>* { | |
flex: 1; | |
} | |
.tp-editor-container { | |
display: flex; | |
flex-direction: column; | |
} | |
.tp-editor { | |
flex: 4 1; | |
display: flex; | |
} | |
.tp-editor.hidden { | |
display: none; | |
} | |
#tp-text-editor { | |
flex: 1; | |
height: 100%; | |
width: 100%; | |
background-color: #333; | |
color: #eee; | |
outline: none; | |
font-size: 16px; | |
font-family: Consolas, 'Courier New', monospace; | |
display: block; | |
resize: none; | |
border: none; | |
padding: 6px; | |
padding-bottom: calc((100vh - 48px) * 0.3); | |
overflow-wrap: normal; | |
} | |
.tp-editor-menu { | |
flex: 0 0 70px; | |
background-color: var(--main-bg-color); | |
color: var(--main-text-color); | |
display: flex; | |
flex-direction: column-reverse; | |
} | |
.tp-editor-menu>*{ | |
height: 56px; | |
text-align: center; | |
padding: 4px; | |
} | |
.tp-editor-menu a.btn { | |
font-size: 16px; | |
color: var(--main-text-color, #fff); | |
cursor: pointer; | |
text-decoration: none; | |
} | |
.tp-editor-menu a.btn.disabled { | |
color: #888; | |
} | |
.tp-editor-bar { | |
flex: 0 0 19px; | |
background-color: var(--main-bg-color); | |
color: var(--main-text-color); | |
font-size: 14px; | |
text-align: center; | |
cursor: pointer; | |
} | |
.tp-viewer { | |
flex: 6 1 auto; | |
display: flex; | |
flex-direction: column; | |
} | |
.tp-viewer>* { | |
flex: 1; | |
} | |
.tp-explorer { | |
padding: 20px; | |
} | |
</style> | |
<style> | |
</style> | |
<link rel="apple-touch-icon" sizes="180x180" href="/static/icon/apple-touch-icon.png"> | |
<link rel="icon" type="image/png" sizes="32x32" href="/static/icon/favicon-32x32.png"> | |
<link rel="icon" type="image/png" sizes="16x16" href="/static/icon/favicon-16x16.png"> | |
<link rel="manifest" href="/static/icon/site.webmanifest"> | |
<script async src="https://kit.fontawesome.com/067013981a.js" crossorigin="anonymous"></script> | |
<style>.at-wrap, .at-wrap *, .at-wrap *::after, .at-wrap *::before { | |
box-sizing: border-box; | |
} | |
.at-wrap { | |
font-family: sans-serif; | |
font-size: 12px; | |
background-color: var(--page-bg-color, #f9f9f5); | |
display: flex; | |
flex-direction: column; | |
position: relative; | |
} | |
.at-content { | |
position: relative; | |
flex: 1 1 auto; | |
} | |
/** Sidebar **/ | |
.at-sidebar { | |
position: absolute; | |
top: 0; | |
left: 0; | |
bottom: 0; | |
max-width: 70px; | |
width: auto; | |
display: flex; | |
align-content: stretch; | |
z-index: 1001; | |
overflow: hidden; | |
border-right: 1px solid rgba(0, 0, 0, 0.12); | |
background: #f7f7f7; | |
} | |
.at-sidebar:hover { | |
max-width: 400px; | |
transition: max-width 0.2s; | |
overflow-y: auto; | |
} | |
.at-viewport { | |
overflow-y: auto; | |
position: absolute; | |
top: 0; | |
/* left: 70px; */ | |
left: 0; | |
right: 0; | |
bottom: 0; | |
/* padding-right: 20px; */ | |
} | |
/** Overlay **/ | |
.at-overlay { | |
/** Fill Parent */ | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
z-index: 1002; | |
/* Blurry dark shade */ | |
backdrop-filter: blur(3px); | |
background: rgba(0, 0, 0, 0.5); | |
/* center content */ | |
display: flex; | |
justify-content: center; | |
align-items: flex-start; | |
} | |
.at-overlay#at-overlay-error { | |
box-shadow: inset 0px 0px 8px 2px #9d0000; | |
} | |
.at-overlay-content { | |
/* white box with drop-shadow */ | |
margin-top: 20px; | |
background: #fff; | |
box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.3); | |
padding: 10px; | |
} | |
/** Track selector **/ | |
.at-track { | |
display: flex; | |
position: relative; | |
padding: 5px; | |
transition: background 0.2s; | |
cursor: pointer; | |
} | |
.at-track:hover { | |
background: rgba(0, 0, 0, 0.1); | |
} | |
.at-track>.at-track-icon, .at-track>.at-track-details { | |
display: flex; | |
flex-direction: column; | |
justify-content: center; | |
} | |
.at-track>.at-track-icon { | |
flex-shrink: 0; | |
font-size: 32px; | |
opacity: 0.5; | |
transition: opacity 0.2s; | |
width: 64px; | |
height: 64px; | |
margin-right: 5px; | |
align-items: center; | |
} | |
.at-track-name { | |
font-weight: bold; | |
margin-bottom: 5px; | |
} | |
.at-track:hover>.at-track-icon { | |
opacity: 0.8; | |
} | |
.at-track.active { | |
background: rgba(0, 0, 0, 0.03); | |
} | |
.at-track.active>.at-track-icon { | |
color: #4972a1; | |
opacity: 1; | |
} | |
.at-track>.at-track-name { | |
font-weight: 500; | |
} | |
/** Footer **/ | |
.at-controls { | |
flex: 0 0 auto; | |
display: flex; | |
justify-content: space-between; | |
background: var(--main-bg-color, rgb(67, 109, 157)); | |
color: var(--main-text-color, #fff); | |
} | |
.at-controls>div { | |
display: flex; | |
justify-content: flex-start; | |
align-content: center; | |
align-items: center; | |
} | |
.at-controls>div>* { | |
display: flex; | |
text-align: center; | |
align-items: center; | |
justify-content: center; | |
padding: 4px; | |
} | |
.at-controls a.btn { | |
width: 44px; | |
height: 48px; | |
font-size: 16px; | |
cursor: pointer; | |
color: var(--main-text-color, #fff); | |
text-decoration: none; | |
} | |
.at-controls a.btn.at-stepper { | |
width: 25px; | |
} | |
.at-controls a.btn.iconHelper { | |
justify-content: right; | |
width: 28px; | |
} | |
.at-controls a.btn.disabled { | |
cursor: progress; | |
opacity: 0.5; | |
} | |
.at-controls a.active { | |
background: var(--brighter1, rgb(85, 136, 199)); | |
text-decoration: none; | |
} | |
/* .at-controls .btn i { | |
vertical-align: top; | |
} */ | |
.at-controls select { | |
-moz-appearance: none; | |
-webkit-appearance: none; | |
appearance: none; | |
border: none; | |
height: 40px; | |
background: var(--main-bg-color, rgb(67, 109, 157)); | |
padding: 4px 4px; | |
color: var(--main-text-color, #fff); | |
font-size: 16px; | |
text-align-last: left; | |
text-align: left; | |
-moz-text-align-last: left; | |
cursor: pointer; | |
} | |
.at-controls input[type=range] { | |
-webkit-appearance: none; | |
width: 100%; | |
max-width: 80px; | |
/* outline: none; */ | |
height: 4px; | |
} | |
.at-controls input[type=range]::-webkit-slider-thumb { | |
-webkit-appearance: none; | |
height: 14px; | |
width: 14px; | |
border-radius: 42%; | |
background: #ffffff; | |
cursor: pointer; | |
margin-top: -1px; | |
border: 1px solid #666; | |
} | |
.at-song-info { | |
height: 48px; | |
overflow-y: auto; | |
} | |
.at-song-title { | |
font-weight: bold; | |
} | |
.at-song-album { | |
font-size: 90%; | |
} | |
.fs14 { | |
font-size: 14px; | |
} | |
.at-logo { | |
background: var(--accent-bg-color, #e4e4e4); | |
color: var(--dark-text-color, #838383); | |
font-size: 14px; | |
height: 100%; | |
} | |
.at-logo img { | |
height: 30px; | |
} | |
.at-cursor-bar { | |
/* Defines the color of the bar background when a bar is played */ | |
background: rgba(255, 242, 0, 0.2); | |
} | |
.at-selection div { | |
/* Defines the color of the selection background */ | |
background: rgba(64, 64, 255, 0.1); | |
} | |
.at-cursor-beat { | |
/* Defines the beat cursor */ | |
background: rgba(73, 161, 51, 0.55); | |
width: 2px; | |
transform: translateX(2px); | |
} | |
.at-highlight * { | |
/* Defines the color of the music symbols when they are being played (svg) */ | |
fill: #5d9150; | |
stroke: #5d9150; | |
}</style> | |
</head> | |
<body> | |
<div class="tp-main"> | |
<div class="tp-sidebar"> | |
<div class="tp-header"> | |
<div class="tp-logo">Tab Portal</div> | |
<div class="tp-section-link"><a href="/">Editor</a></div> | |
<div class="tp-section-link"><a href="/tabs">Explore Tabs</a></div> | |
</div> | |
<div class="tp-menu"> | |
<div class="tp-help"> | |
<div class="tp-help-title">Editor: How to use</div> | |
Write <a href="https://www.alphatab.net/docs/alphaTex/introduction/" target="_blank">alphaTex</a> notation in the text editor | |
and press <b>Ctrl+Enter</b> to render it. Press <b>Enter</b> to re-focus on the editor. | |
</div> | |
<div class="tp-help"> | |
<div class="tp-help-title">Player: How to use</div> | |
Press <b>Space</b> to Play/Pause. | |
<b>M</b> to mute. | |
<b>Backspace</b> to reset. | |
</div> | |
<div class="tp-help"> | |
<div class="tp-help-title">BPM counter</div> | |
<button id="tp-bpm-btn">Click</button> | |
<span id="tp-bpm-result"></span> | |
</div> | |
</div> | |
<div class="tp-credits"> | |
alphaTab-issue-1400 - | |
<a target="_blank" href="https://github.com/jonaro00/TabPortal">GitHub</a> | |
</div> | |
</div> | |
<div class="tp-content"> | |
<div class="tp-editor-container"> | |
<div class="tp-editor"> | |
<textarea id="tp-text-editor" spellcheck="false">3.3 3.3</textarea> | |
<div class="tp-editor-menu"> | |
<a class="btn" id="tp-render"> | |
<i class="fas fa-file-download"></i> | |
<br>Render | |
</a> | |
<a class="btn disabled" id="tp-save"> | |
<i class="fas fa-save"></i> | |
<br>Save | |
</a> | |
<a class="btn disabled" id="tp-save-new"> | |
<i class="fas fa-save"></i> | |
<br>Save As | |
</a> | |
<div>API Password: <input id="tp-api-password" type="password"></div> | |
<div>Name: <input id="tp-name" type="text" value="{{ name }}"></div> | |
</div> | |
</div> | |
<div class="tp-editor-bar"> | |
{% if readonly %} | |
<i class="fas fa-chevron-down"></i> | |
Source | |
<i class="fas fa-chevron-down"></i> | |
{% else %} | |
<i class="fas fa-chevron-up"></i> | |
Editor | |
<i class="fas fa-chevron-up"></i> | |
{% endif %} | |
</div> | |
<div class="tp-viewer"> | |
<div class="at-wrap" data-file=""> | |
<div class="at-content"> | |
<div id="at-overlay-loading" class="at-overlay"> | |
<div class="at-overlay-content"> | |
Music sheet is loading | |
</div> | |
</div> | |
<div id="at-overlay-error" class="at-overlay" style="display:none"> | |
<div id="at-overlay-error-text" class="at-overlay-content"> | |
Error loading music sheet | |
</div> | |
</div> | |
<!-- <div class="at-sidebar"> | |
<div class="at-sidebar-content"> | |
<div class="at-track-list"></div> | |
</div> | |
</div> --> | |
<div class="at-viewport"> | |
<div class="at-main"></div> | |
</div> | |
</div> | |
<div class="at-controls"> | |
<div class="at-controls-left"> | |
<a class="btn at-player-reset disabled" title="Reset [Backspace]"> | |
<i class="fas fa-step-backward"></i> | |
</a> | |
<a class="btn at-player-play-pause disabled" title="Play/Pause [Space]"> | |
<i class="fas fa-play"></i> | |
</a> | |
<a class="btn at-player-step-backward at-stepper disabled" title="Step backward (100 MIDI ticks)"> | |
<i class="fas fa-caret-left"></i> | |
</a> | |
<a class="btn at-player-step-forward at-stepper disabled" title="Step forward (100 MIDI ticks)"> | |
<i class="fas fa-caret-right"></i> | |
</a> | |
<div class="at-song-position">00:00 / 00:00</div> | |
<a class="btn iconHelper at-mute" title="Mute [M]"> | |
<i class="fas fa-volume-up"></i> | |
</a> | |
<div class="at-volume"> | |
<input type="range" title="Volume"/> | |
</div> | |
<div class="at-speed"> | |
<i class="fas fa-angle-double-right fs14"></i> | |
<select title="Speed"> | |
<option value="0.3">0.3x</option> | |
<option value="0.5">0.5x</option> | |
<option value="0.7">0.7x</option> | |
<option value="0.8">0.8x</option> | |
<option value="0.9">0.9x</option> | |
<option value="1" selected>1x</option> | |
<option value="1.1">1.1x</option> | |
<option value="1.2">1.2x</option> | |
<option value="1.3">1.3x</option> | |
<option value="1.5">1.5x</option> | |
<option value="2" >2x</option> | |
</select> | |
</div> | |
<a class="btn toggle at-count-in" title="Count-in"> | |
<i class="fas fa-hourglass-half"></i> | |
</a> | |
<a class="btn at-metronome" title="Metronome"> | |
<i class="fas fa-tachometer-alt"></i> | |
</a> | |
<a class="btn at-loop" title="Loop"> | |
<i class="fas fa-retweet"></i> | |
</a> | |
</div> | |
<div class="at-song-info"> | |
<div> | |
<div> | |
<span class="at-song-artist"></span> - <span class="at-song-title"></span><br><span class="at-song-album"></span> | |
</div> | |
</div> | |
</div> | |
<div class="at-controls-right"> | |
<a class="btn at-print" title="Print"> | |
<i class="fas fa-print"></i> | |
</a> | |
<a class="btn at-show-notation active" title="Show music notation"> | |
<i class="fas fa-music"></i> | |
</a> | |
<a class="btn at-layout active" title="Page/Horizontal layout"> | |
<i class="fas fa-file-alt"></i> | |
</a> | |
<div class="at-zoom"> | |
<i class="fas fa-search fs14"></i> | |
<select title="Zoom"> | |
<option value="0.25">25%</option> | |
<option value="0.5" >50%</option> | |
<option value="0.75">75%</option> | |
<option value="0.9" >90%</option> | |
<option value="1" selected>100%</option> | |
<option value="1.1" >110%</option> | |
<option value="1.25">125%</option> | |
<option value="1.5" >150%</option> | |
<option value="2">200%</option> | |
</select> | |
</div> | |
<div class="at-logo">powered by<a href="https://www.alphatab.net/" target="_blank"><img src="/static/alphatab.png"></a></div> | |
</div> | |
</div> | |
</div> | |
<!-- <template id="at-track-template"> | |
<div class="at-track"> | |
<div class="at-track-icon"> | |
<i class="fas fa-guitar"></i> | |
</div> | |
<div class="at-track-details"> | |
<div class="at-track-name"></div> | |
</div> | |
</div> | |
</template> --> | |
<script> | |
const MAX_VOLUME = 40; | |
const STEP_LENTGH = 100; | |
const Settings = { | |
defaults: { | |
muted: false, | |
volume: Math.round(MAX_VOLUME * 0.4), | |
speed: 1.0, | |
countIn: 0, | |
metronome: 0, | |
loop: false, | |
showNotation: true, | |
layout: alphaTab.LayoutMode.Page, | |
zoom: 1.0, | |
}, | |
values: {}, | |
load() { | |
this.values = {...this.defaults, ...JSON.parse(localStorage.getItem("tp-settings") || "{}")}; | |
}, | |
save() { | |
localStorage.setItem("tp-settings", JSON.stringify(this.values)); | |
}, | |
getApiSettings() { | |
return { | |
core: { | |
file: wrapper.getAttribute("data-file") || null, | |
}, | |
player: { | |
enablePlayer: true, | |
soundFont: "https://cdn.jsdelivr.net/npm/@coderline/alphatab@latest/dist/soundfont/sonivox.sf2", | |
scrollElement: viewport, | |
scrollOffsetX: -6, | |
scrollOffsetY: -10, | |
slide: { | |
shiftSlideDurationRatio: 0.2, | |
}, | |
}, | |
display: { | |
stretchForce: 0.75, | |
staveProfile: this.values.showNotation ? 'default' : 'tab', | |
layoutMode: this.values.layout, | |
scale: this.values.zoom / 100, | |
}, | |
notation: { | |
rhythmMode: this.values.showNotation ? 'hidden' : 'showwithbars', | |
}, | |
} | |
}, | |
} | |
Settings.load(); | |
// load elements | |
const wrapper = document.querySelector(".at-wrap"); | |
const main = wrapper.querySelector(".at-main"); | |
const viewport = wrapper.querySelector('.at-viewport'); | |
// initialize alphatab | |
const at = new alphaTab.AlphaTabApi(main, Settings.getApiSettings()); | |
// overlay logic | |
const overlay = wrapper.querySelector("#at-overlay-loading"); | |
const overlayError = wrapper.querySelector("#at-overlay-error"); | |
const overlayErrorText = wrapper.querySelector("#at-overlay-error-text"); | |
// const overlayContent = wrapper.querySelector(".at-overlay-content"); | |
at.renderStarted.on(() => { | |
overlay.style.display = "flex"; | |
}); | |
at.renderFinished.on(() => { | |
overlay.style.display = "none"; | |
overlayError.style.display = "none"; | |
}); | |
// // track selector | |
// function createTrackItem(track) { | |
// const trackItem = document | |
// .querySelector("#at-track-template") | |
// .content.cloneNode(true).firstElementChild; | |
// trackItem.querySelector(".at-track-name").innerText = track.name; | |
// trackItem.track = track; | |
// trackItem.onclick = (e) => { | |
// e.stopPropagation(); | |
// api.renderTracks([track]); | |
// }; | |
// return trackItem; | |
// } | |
// const trackList = wrapper.querySelector(".at-track-list"); | |
// api.scoreLoaded.on((score) => { | |
// // clear items | |
// trackList.innerHTML = ""; | |
// // generate a track item for all tracks of the score | |
// score.tracks.forEach((track) => { | |
// trackList.appendChild(createTrackItem(track)); | |
// }); | |
// }); | |
// api.renderStarted.on(() => { | |
// // collect tracks being rendered | |
// const tracks = new Map(); | |
// api.tracks.forEach((t) => { | |
// tracks.set(t.index, t); | |
// }); | |
// // mark the item as active or not | |
// const trackItems = trackList.querySelectorAll(".at-track"); | |
// trackItems.forEach((trackItem) => { | |
// if (tracks.has(trackItem.track.index)) { | |
// trackItem.classList.add("active"); | |
// } else { | |
// trackItem.classList.remove("active"); | |
// } | |
// }); | |
// }); | |
// main player controls | |
const playPause = wrapper.querySelector(".at-controls .at-player-play-pause"); | |
const reset = wrapper.querySelector(".at-controls .at-player-reset"); | |
const stepBackward = wrapper.querySelector(".at-controls .at-player-step-backward"); | |
const stepForward = wrapper.querySelector(".at-controls .at-player-step-forward"); | |
playPause.onclick = (e) => { | |
if (e.target.classList.contains("disabled")) return; | |
at.playPause(); | |
}; | |
reset.onclick = (e) => { | |
if (e.target.classList.contains("disabled")) return; | |
at.stop(); | |
viewport.scrollTo(0, 0); | |
}; | |
stepBackward.onclick = (e) => { | |
if (e.target.classList.contains("disabled")) return; | |
at.tickPosition -= STEP_LENTGH; | |
}; | |
stepForward.onclick = (e) => { | |
if (e.target.classList.contains("disabled")) return; | |
at.tickPosition += STEP_LENTGH; | |
}; | |
at.playerReady.on(() => { | |
playPause.classList.remove("disabled"); | |
reset.classList.remove("disabled"); | |
stepBackward.classList.remove("disabled"); | |
stepForward.classList.remove("disabled"); | |
}); | |
at.playerStateChanged.on((e) => { | |
const icon = playPause.querySelector("i.fas"); | |
const playing = e.state === alphaTab.synth.PlayerState.Playing; | |
icon.classList.toggle("fa-play", !playing); | |
icon.classList.toggle("fa-pause", playing); | |
}); | |
// song position | |
function formatDuration(milliseconds) { | |
let seconds = milliseconds / 1000; | |
const minutes = (seconds / 60) | 0; | |
seconds = (seconds - minutes * 60) | 0; | |
return ( | |
String(minutes).padStart(2, "0") + | |
":" + | |
String(seconds).padStart(2, "0") | |
); | |
} | |
const songPosition = wrapper.querySelector(".at-song-position"); | |
let previousTime = -1; | |
at.playerPositionChanged.on((e) => { | |
// reduce number of UI updates to second changes. | |
const currentSeconds = (e.currentTime / 1000) | 0; | |
if (currentSeconds == previousTime) { | |
return; | |
} | |
songPosition.innerText = formatDuration(e.currentTime) + " / " + formatDuration(e.endTime); | |
}); | |
const mute = wrapper.querySelector(".at-controls .at-mute"); | |
const muteIcon = wrapper.querySelector(".at-controls .at-mute i"); | |
mute.onclick = () => { | |
Settings.values.muted = !Settings.values.muted; | |
setMuted(Settings.values.muted); | |
Settings.save(); | |
}; | |
function setMuted(b) { | |
muteIcon.classList.toggle("fa-volume-mute", b); | |
muteIcon.classList.toggle("fa-volume-up", !b); | |
at.changeTrackMute(at.score.tracks, b); | |
} | |
const volume = wrapper.querySelector(".at-controls .at-volume input"); | |
volume.max = MAX_VOLUME; | |
volume.oninput = () => { // when moving slider | |
setVolume(parseInt(volume.value)); | |
}; | |
volume.onchange = () => { // when releasing slider | |
Settings.values.volume = volume.value; | |
Settings.save(); | |
} | |
function setVolume(v) { | |
at.masterVolume = v / MAX_VOLUME; | |
} | |
const speed = wrapper.querySelector(".at-controls .at-speed select"); | |
speed.onchange = () => { | |
setSpeed(parseFloat(speed.value)); | |
Settings.values.speed = speed.value; | |
Settings.save(); | |
}; | |
function setSpeed(f) { | |
at.playbackSpeed = f; | |
} | |
const countIn = wrapper.querySelector('.at-controls .at-count-in'); | |
countIn.onclick = () => { | |
countIn.classList.toggle('active'); | |
const counting = countIn.classList.contains('active'); | |
setCountIn(counting); | |
Settings.values.countIn = counting; | |
Settings.save(); | |
}; | |
function setCountIn(b) { | |
at.countInVolume = b ? 1 : 0; | |
} | |
const metronome = wrapper.querySelector(".at-controls .at-metronome"); | |
metronome.onclick = () => { | |
metronome.classList.toggle('active'); | |
const counting = metronome.classList.contains('active'); | |
setMetronome(counting); | |
Settings.values.metronome = counting; | |
Settings.save(); | |
}; | |
function setMetronome(b) { | |
at.metronomeVolume = b ? 1 : 0; | |
} | |
const loop = wrapper.querySelector(".at-controls .at-loop"); | |
loop.onclick = () => { | |
loop.classList.toggle('active'); | |
const looping = loop.classList.contains('active'); | |
setLoop(looping); | |
Settings.values.loop = looping; | |
Settings.save(); | |
}; | |
function setLoop(b) { | |
at.isLooping = b; | |
} | |
at.scoreLoaded.on((score) => { | |
wrapper.querySelector(".at-song-title").innerText = score.title; | |
wrapper.querySelector(".at-song-artist").innerText = score.artist; | |
wrapper.querySelector(".at-song-album").innerText = score.album ? `(${score.album})` : ''; | |
// set Document title | |
document.title = score.title || score.artist || score.album ? `${score.title} - ${score.artist} | ${score.album}` : "Tab Portal"; | |
}); | |
wrapper.querySelector(".at-controls .at-print").onclick = () => { | |
at.print(); | |
}; | |
const showNotation = wrapper.querySelector(".at-controls .at-show-notation"); | |
showNotation.onclick = () => { | |
showNotation.classList.toggle("active"); | |
if(showNotation.classList.contains("active")){ | |
at.settings.display.staveProfile = 'default'; | |
at.settings.notation.rhythmMode = 'hidden'; | |
Settings.values.showNotation = true; | |
} else { | |
at.settings.display.staveProfile = 'tab'; | |
at.settings.notation.rhythmMode = 'showwithbars'; | |
Settings.values.showNotation = false; | |
} | |
Settings.save(); | |
at.updateSettings(); | |
at.render(); | |
}; | |
const layout = wrapper.querySelector(".at-controls .at-layout"); | |
layout.onclick = () => { | |
layout.classList.toggle("active"); | |
at.settings.display.layoutMode = layout.classList.contains("active") | |
? alphaTab.LayoutMode.Page | |
: alphaTab.LayoutMode.Horizontal; | |
Settings.values.layout = at.settings.display.layoutMode; | |
Settings.save(); | |
at.updateSettings(); | |
at.render(); | |
}; | |
const zoom = wrapper.querySelector(".at-controls .at-zoom select"); | |
zoom.onchange = () => { | |
setZoom(parseFloat(zoom.value)); | |
Settings.values.zoom = zoom.value; | |
Settings.save(); | |
}; | |
function setZoom(f) { | |
at.settings.display.scale = f; | |
at.updateSettings(); | |
at.render(); | |
} | |
viewport.onclick = () => { | |
document.activeElement.blur(); | |
} | |
// keyboard | |
document.addEventListener("keydown", (event) => { | |
if (event.defaultPrevented) { | |
return; // Do nothing if event already handled | |
} | |
let bodyFocused = document.activeElement == document.body; | |
switch(event.key) { | |
case " ": | |
if (!bodyFocused && !event.ctrlKey) return; | |
playPause.click(); | |
event.preventDefault(); // Consume the event so it doesn't get handled twice | |
break; | |
case "Backspace": | |
if (!bodyFocused) return; | |
reset.click() | |
event.preventDefault(); | |
break; | |
case "m": | |
if (!bodyFocused) return; | |
mute.click() | |
event.preventDefault(); | |
break; | |
} | |
}, true); | |
// load settings | |
document.addEventListener('DOMContentLoaded', () => { | |
setMuted(Settings.values.muted); | |
volume.value = Settings.values.volume; | |
setVolume(Settings.values.volume); | |
speed.value = Settings.values.speed; | |
setSpeed(parseFloat(Settings.values.speed)); | |
Settings.values.countIn && countIn.classList.toggle("active"); | |
setCountIn(Settings.values.countIn); | |
Settings.values.metronome && metronome.classList.toggle("active"); | |
setMetronome(Settings.values.metronome); | |
Settings.values.loop && loop.classList.toggle("active"); | |
setLoop(Settings.values.loop); | |
Settings.values.showNotation || showNotation.classList.toggle("active"); | |
Settings.values.layout === alphaTab.LayoutMode.Page || layout.classList.toggle("active"); | |
zoom.value = Settings.values.zoom; | |
setZoom(parseFloat(Settings.values.zoom)); | |
}); | |
</script> | |
</div> | |
</div> | |
<script> | |
const cont = document.querySelector(".tp-editor-container") | |
const editor = cont.querySelector(".tp-editor") | |
const editorTextArea = cont.querySelector("#tp-text-editor") | |
const editorEditBtn = cont.querySelector("#tp-edit") | |
const editorRenderBtn = cont.querySelector("#tp-render") | |
const editorSaveBtn = cont.querySelector("#tp-save") | |
const editorSaveNewBtn = cont.querySelector("#tp-save-new") | |
const editorBar = cont.querySelector(".tp-editor-bar") | |
const editorBarIcons = cont.querySelectorAll(".tp-editor-bar i") | |
const editorName = cont.querySelector("#tp-name"); | |
const editorApiPassword = cont.querySelector("#tp-api-password"); | |
const menuBpmBtn = document.querySelector("#tp-bpm-btn"); | |
const menuBpmSpan = document.querySelector("#tp-bpm-result"); | |
function toggleEditor() { | |
editor.classList.toggle("hidden"); | |
editorBarIcons.forEach((e) => { | |
e.classList.toggle("fa-chevron-up"); | |
e.classList.toggle("fa-chevron-down"); | |
}); | |
} | |
editorBar.onclick = toggleEditor; | |
editorEditBtn && (editorEditBtn.onclick = () => { | |
window.location.assign('?edit=true') | |
}); | |
editorRenderBtn && (editorRenderBtn.onclick = () => { | |
render(); | |
}); | |
tabId = window.location.pathname.match(/\/tabs\/(\w+)/)?.[1]; | |
editorSaveBtn && (editorSaveBtn.onclick = () => { | |
if (editorSaveBtn.classList.contains("disabled")) return; | |
fetch( | |
`/api/tabs/${tabId}`, | |
{ | |
method: "PUT", | |
headers: new Headers({ | |
"Content-Type": "application/json", | |
"X-Tp-Pass": editorApiPassword?.value || "", | |
}), | |
body: JSON.stringify({ | |
name: editorName?.value || "", | |
tex: editorTextArea.value, | |
}), | |
}, | |
).then(res => { | |
if (res.ok) { | |
edits_made = false; | |
updateEditorState(); | |
} | |
}); | |
}); | |
editorSaveNewBtn && (editorSaveNewBtn.onclick = () => { | |
if (editorSaveNewBtn.classList.contains("disabled")) return; | |
fetch( | |
"/api/tabs", | |
{ | |
method: "POST", | |
headers: { | |
"Content-Type": "application/json", | |
"X-Tp-Pass": editorApiPassword?.value || "", | |
}, | |
body: JSON.stringify({ | |
name: editorName?.value || "", | |
tex: editorTextArea.value, | |
}), | |
}, | |
).then(res => { | |
if (res.ok) { | |
edits_made = false; | |
updateEditorState(); | |
} | |
}); | |
}); | |
function render() { | |
let pos = at.tickPosition; | |
at.tex(editorTextArea.value); | |
at.tickPosition = pos; | |
editorTextArea.blur(); | |
viewport.focus(); | |
} | |
at.error.on((error) => { | |
overlayErrorText.innerText = error.message; | |
overlayError.style.display = "flex"; | |
}) | |
// Warn user when leaving unsaved changes. | |
let edits_made = false; | |
editorTextArea.oninput = () => { | |
edits_made = true; | |
updateEditorState(); | |
} | |
editorName && (editorName.oninput = () => { | |
edits_made = true; | |
updateEditorState(); | |
}); | |
function updateEditorState() { | |
if (tabId) editorSaveBtn.classList.toggle("disabled", !edits_made); | |
editorSaveNewBtn.classList.toggle("disabled", !edits_made); | |
} | |
window.addEventListener("beforeunload", (e) => { | |
if (!edits_made) return undefined; | |
e.preventDefault(); | |
return e.returnValue = `Changes made will be lost.`; | |
}); | |
// keyboard | |
document.addEventListener("keydown", (event) => { | |
if (event.defaultPrevented) { | |
return; // Do nothing if event already handled | |
} | |
switch(event.key) { | |
case "Enter": | |
if (event.ctrlKey) { // Ctrl+Enter renders the alphaTex | |
render(); | |
event.preventDefault(); // Consume the event so it doesn't get handled twice | |
} | |
else if (document.activeElement == document.body) { // Enter focuses textarea | |
if(editor.classList.contains("hidden")){ | |
toggleEditor(); | |
} | |
editorTextArea.focus(); | |
event.preventDefault(); | |
} | |
break; | |
case "s" | "S": | |
if (event.ctrlKey) { | |
if (event.shiftKey) { | |
// Ctrl+Shift+S | |
editorSaveNewBtn && editorSaveNewBtn.click(); | |
} else { | |
// Ctrl+S | |
editorSaveBtn && editorSaveBtn.click(); | |
} | |
event.preventDefault(); // Consume the event so it doesn't get handled twice | |
} | |
break; | |
} | |
}, true); | |
const BPM_RESET_MS = 2000; | |
let firstClick = -BPM_RESET_MS; | |
let lastClick = -BPM_RESET_MS; | |
let clickCount = 0; | |
menuBpmBtn && menuBpmSpan && (menuBpmBtn.onclick = (e) => { | |
let t = e.timeStamp; | |
if (t - lastClick > BPM_RESET_MS) { | |
clickCount = 1; | |
firstClick = lastClick = t; | |
menuBpmSpan.innerHTML = "-"; | |
return; | |
} | |
clickCount++; | |
let int = (t - firstClick) / (clickCount - 1); | |
let ps = 1000 / int; | |
let bpm = 60 * ps; | |
console.log(bpm); | |
menuBpmSpan.innerHTML = Math.floor(bpm + 0.5); | |
lastClick = t; | |
}); | |
editorTextArea.value = editorTextArea.value || `\\title "" | |
\\artist "" | |
\\album "" | |
\\instrument 33 | |
\\tuning g2 d2 a1 e1 | |
\\tempo 120 | |
. | |
\\clef F4 | |
\\ks C | |
3.3 3.3 3.3 3.3 | 2.3 2.3 2.3 2.3`; | |
at.tex(editorTextArea.value); | |
</script> | |
</div> | |
</div> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment