Update sender.html
Browse files- sender.html +231 -3
sender.html
CHANGED
|
@@ -865,7 +865,7 @@
|
|
| 865 |
<hr>
|
| 866 |
|
| 867 |
<h3>プレイヤーの使い方</h3>
|
| 868 |
-
<p
|
| 869 |
|
| 870 |
<hr>
|
| 871 |
|
|
@@ -878,7 +878,6 @@
|
|
| 878 |
|
| 879 |
<h3>高度な設定</h3>
|
| 880 |
<ul>
|
| 881 |
-
<li>消画モード:画面を消すことができます。</li>
|
| 882 |
<li>全体音量係数:全体の音量を上げたり下げたりできます。10が最大です。</li>
|
| 883 |
<li>再生速度:再生速度を変更できます。</li>
|
| 884 |
<li>テンポ:テンポを入力すると、そのテンポになるように再生速度を自動調整します。</li>
|
|
@@ -908,6 +907,18 @@
|
|
| 908 |
|
| 909 |
</script>
|
| 910 |
<div class="audio-controls">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 911 |
<h2>音声コントロール</h2>
|
| 912 |
<span>各パートの音の大きさを設定できます。自分のパートの音量を1、それ以外のパートの音量を0.3くらいにすると他のパートのタイミングを確認しながら練習できます。<br>
|
| 913 |
※「ピアノ」と「全体」は他のパートと音がずれるため、組み合わせて使用しないでください。</span>
|
|
@@ -1053,7 +1064,224 @@
|
|
| 1053 |
<br>楽譜に記載された「♪=」の横のテンポ(時の旅人は約92、地球星歌は約66)を基準に計算します。</span>
|
| 1054 |
</div>
|
| 1055 |
</div>
|
| 1056 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1057 |
<script>
|
| 1058 |
document.addEventListener('DOMContentLoaded', function() {
|
| 1059 |
// 同期管理用の変数
|
|
|
|
| 865 |
<hr>
|
| 866 |
|
| 867 |
<h3>プレイヤーの使い方</h3>
|
| 868 |
+
<p>「▶」や「⏸」ボタンで再生や一時停止ができます。「↺」で最初から再生できます。音量スライダーで音量を変更できます。スライダーで再生速度も変更できます。</p>
|
| 869 |
|
| 870 |
<hr>
|
| 871 |
|
|
|
|
| 878 |
|
| 879 |
<h3>高度な設定</h3>
|
| 880 |
<ul>
|
|
|
|
| 881 |
<li>全体音量係数:全体の音量を上げたり下げたりできます。10が最大です。</li>
|
| 882 |
<li>再生速度:再生速度を変更できます。</li>
|
| 883 |
<li>テンポ:テンポを入力すると、そのテンポになるように再生速度を自動調整します。</li>
|
|
|
|
| 907 |
|
| 908 |
</script>
|
| 909 |
<div class="audio-controls">
|
| 910 |
+
<details>
|
| 911 |
+
<summary style="font-size: 1.2em;">オフライン設定</summary>
|
| 912 |
+
<div class="swsettings" id="swsettings">
|
| 913 |
+
<a>下のボタンを押すと、サービスワーカーでページをオフラインで使用できるようにします。<br>
|
| 914 |
+
ブラウザの仕様により、オフラインで開く際に<b>最初のみ二回再読み込みボタンを押さなければいけません。</b><br>
|
| 915 |
+
不安定な機能で、ページの更新などができなくなるバグが発生する可能性があります。</a>
|
| 916 |
+
<br>
|
| 917 |
+
<button class="combine-button" id="sw-register-btn" disabled>登録を開始</button>
|
| 918 |
+
<div class="combine-status" id="sw-status">
|
| 919 |
+
</div>
|
| 920 |
+
</div>
|
| 921 |
+
</details><br>
|
| 922 |
<h2>音声コントロール</h2>
|
| 923 |
<span>各パートの音の大きさを設定できます。自分のパートの音量を1、それ以外のパートの音量を0.3くらいにすると他のパートのタイミングを確認しながら練習できます。<br>
|
| 924 |
※「ピアノ」と「全体」は他のパートと音がずれるため、組み合わせて使用しないでください。</span>
|
|
|
|
| 1064 |
<br>楽譜に記載された「♪=」の横のテンポ(時の旅人は約92、地球星歌は約66)を基準に計算します。</span>
|
| 1065 |
</div>
|
| 1066 |
</div>
|
| 1067 |
+
<script>
|
| 1068 |
+
window.addEventListener('load', async () => {
|
| 1069 |
+
const statusElem = document.getElementById('sw-status');
|
| 1070 |
+
const registerBtn = document.getElementById('sw-register-btn');
|
| 1071 |
+
const UPDATE_URL = '/update.txt';
|
| 1072 |
+
const LS_SW_VERSION = 'sw_version';
|
| 1073 |
+
const LS_LAST_SEEN = 'last_seen_update_txt';
|
| 1074 |
+
const CHECK_INTERVAL_MS = 30000;
|
| 1075 |
+
const SKIP_WAITING_TIMEOUT_MS = 8000;
|
| 1076 |
+
|
| 1077 |
+
const log = (v) => { console.log(v); try { if(statusElem) statusElem.textContent = String(v); } catch (e) {} };
|
| 1078 |
+
const err = (m, e) => { console.error(m, e); try { if(statusElem) statusElem.textContent = m + (e?.message ? (' — ' + e.message) : ''); } catch (_) {} };
|
| 1079 |
+
|
| 1080 |
+
if(!('serviceWorker' in navigator)) {
|
| 1081 |
+
err('このブラウザは Service Worker に対応していません。');
|
| 1082 |
+
if(registerBtn) registerBtn.disabled = true;
|
| 1083 |
+
return;
|
| 1084 |
+
}
|
| 1085 |
+
if(registerBtn) registerBtn.disabled = false;
|
| 1086 |
+
|
| 1087 |
+
// CSS
|
| 1088 |
+
if(!document.getElementById('sw-update-style')) {
|
| 1089 |
+
const style = document.createElement('style');
|
| 1090 |
+
style.id = 'sw-update-style';
|
| 1091 |
+
style.textContent = `.sw-popup{position:fixed;top:20px;right:20px;background:#fff3cd;color:#856304;border:1px solid #ffeeba;border-radius:8px;padding:16px;z-index:10000;width:320px;font-family:sans-serif}
|
| 1092 |
+
.sw-popup .close{position:absolute;top:6px;right:8px;background:none;border:none;font-weight:bold;cursor:pointer}
|
| 1093 |
+
.sw-popup .primary{display:block;margin-top:12px;padding:8px;border:none;background:#ffc107;border-radius:4px;width:100%;cursor:pointer}
|
| 1094 |
+
.sw-popup .secondary{display:block;margin-top:8px;padding:6px;border:1px solid #ddd;background:#fff;border-radius:4px;width:100%;cursor:pointer}`;
|
| 1095 |
+
document.head.appendChild(style);
|
| 1096 |
+
}
|
| 1097 |
+
|
| 1098 |
+
// popup queue
|
| 1099 |
+
const queue = [];
|
| 1100 |
+
let showing = false;
|
| 1101 |
+
|
| 1102 |
+
function enqueue(type, payload = {}) { queue.push({ type, payload });
|
| 1103 |
+
showNext(); }
|
| 1104 |
+
|
| 1105 |
+
function showNext() { if(showing || !queue.length) return; const it = queue.shift();
|
| 1106 |
+
showing = true;
|
| 1107 |
+
show(it.type, it.payload).finally(() => { showing = false;
|
| 1108 |
+
setTimeout(showNext, 200); }); }
|
| 1109 |
+
|
| 1110 |
+
function show(type, payload) {
|
| 1111 |
+
return new Promise(resolve => {
|
| 1112 |
+
const pop = document.createElement('div');
|
| 1113 |
+
pop.className = 'sw-popup';
|
| 1114 |
+
pop.setAttribute('data-type', type);
|
| 1115 |
+
let title = '更新',
|
| 1116 |
+
body = '更新があります。';
|
| 1117 |
+
if(type === 'reload') { title = 'ページ更新が必要です';
|
| 1118 |
+
body = 'ページが新しくなりました。再読み込みしてください。'; } else if(type === 'sw-update') { title = 'Service Worker の更新';
|
| 1119 |
+
body = '新しい Service Worker が利用可能です。反映するには更新してください。'; }
|
| 1120 |
+
pop.innerHTML = `<button class="close" aria-label="閉じる">×</button><strong>${title}</strong><p style="margin:10px 0 0">${body}</p><div><button class="primary">${payload.primaryText|| (type==='sw-update'?'サービスワーカーを更新する':'再読み込み')}</button><button class="secondary">${payload.secondaryText||'後で'}</button></div>`;
|
| 1121 |
+
document.body.appendChild(pop);
|
| 1122 |
+
pop.querySelector('.close').addEventListener('click', () => { pop.remove();
|
| 1123 |
+
resolve(); });
|
| 1124 |
+
pop.querySelector('.secondary').addEventListener('click', () => { pop.remove();
|
| 1125 |
+
resolve(); });
|
| 1126 |
+
|
| 1127 |
+
const primary = pop.querySelector('.primary');
|
| 1128 |
+
|
| 1129 |
+
if(type === 'reload') {
|
| 1130 |
+
primary.addEventListener('click', () => location.reload());
|
| 1131 |
+
} else if(type === 'sw-update') {
|
| 1132 |
+
primary.addEventListener('click', async () => {
|
| 1133 |
+
primary.disabled = true;
|
| 1134 |
+
try {
|
| 1135 |
+
log('Service Worker 更新処理を開始します...');
|
| 1136 |
+
const reg = await navigator.serviceWorker.getRegistration();
|
| 1137 |
+
// まず、サーバに問い合わせて sw.js を更新取得してもらう(reg.update)
|
| 1138 |
+
if(reg) {
|
| 1139 |
+
try { await reg.update();
|
| 1140 |
+
log('reg.update() を呼び出しました。'); } catch (e) { console.warn('reg.update() エラー', e); }
|
| 1141 |
+
} else {
|
| 1142 |
+
log('registration が見つかりません。');
|
| 1143 |
+
}
|
| 1144 |
+
|
| 1145 |
+
// waiting または installing を監視して installed になったら skipWaiting を送る
|
| 1146 |
+
let handled = false;
|
| 1147 |
+
const attemptHandle = async () => {
|
| 1148 |
+
const r = await navigator.serviceWorker.getRegistration();
|
| 1149 |
+
if(!r) return;
|
| 1150 |
+
if(r.waiting) {
|
| 1151 |
+
log('waiting を発見: skipWaiting を送信します。');
|
| 1152 |
+
r.waiting.postMessage({ type: 'SKIP_WAITING' });
|
| 1153 |
+
handled = true;
|
| 1154 |
+
// controllerchange を待つ
|
| 1155 |
+
navigator.serviceWorker.addEventListener('controllerchange', function oncc() {
|
| 1156 |
+
navigator.serviceWorker.removeEventListener('controllerchange', oncc);
|
| 1157 |
+
(async () => {
|
| 1158 |
+
// activated されてから最新の update.txt を保存
|
| 1159 |
+
const latest = await fetchVersion();
|
| 1160 |
+
if(latest) {
|
| 1161 |
+
localStorage.setItem(LS_SW_VERSION, latest);
|
| 1162 |
+
log('sw_version を保存しました: ' + latest);
|
| 1163 |
+
}
|
| 1164 |
+
setTimeout(() => location.reload(), 400);
|
| 1165 |
+
})();
|
| 1166 |
+
});
|
| 1167 |
+
return;
|
| 1168 |
+
}
|
| 1169 |
+
if(r.installing) {
|
| 1170 |
+
log('installing を監視します...');
|
| 1171 |
+
r.installing.addEventListener('statechange', function onsc() {
|
| 1172 |
+
if(r.installing.state === 'installed') {
|
| 1173 |
+
r.installing.removeEventListener('statechange', onsc);
|
| 1174 |
+
if(r.waiting) {
|
| 1175 |
+
r.waiting.postMessage({ type: 'SKIP_WAITING' });
|
| 1176 |
+
}
|
| 1177 |
+
}
|
| 1178 |
+
});
|
| 1179 |
+
return;
|
| 1180 |
+
}
|
| 1181 |
+
};
|
| 1182 |
+
|
| 1183 |
+
await attemptHandle();
|
| 1184 |
+
|
| 1185 |
+
// タイムアウトしてもフォールバック
|
| 1186 |
+
setTimeout(async () => {
|
| 1187 |
+
if(!handled) {
|
| 1188 |
+
log('waiting の応答がありません。フォールバックで update.txt を保存してリロードします。');
|
| 1189 |
+
const latest = await fetchVersion();
|
| 1190 |
+
if(latest) {
|
| 1191 |
+
localStorage.setItem(LS_SW_VERSION, latest);
|
| 1192 |
+
log('sw_version を保存しました(フォールバック): ' + latest);
|
| 1193 |
+
}
|
| 1194 |
+
location.reload();
|
| 1195 |
+
}
|
| 1196 |
+
}, SKIP_WAITING_TIMEOUT_MS);
|
| 1197 |
+
|
| 1198 |
+
} catch (e) {
|
| 1199 |
+
err('sw-update 処理でエラー', e);
|
| 1200 |
+
const latest = await fetchVersion();
|
| 1201 |
+
if(latest) localStorage.setItem(LS_SW_VERSION, latest);
|
| 1202 |
+
location.reload();
|
| 1203 |
+
}
|
| 1204 |
+
});
|
| 1205 |
+
} else {
|
| 1206 |
+
primary.addEventListener('click', () => { pop.remove();
|
| 1207 |
+
resolve(); });
|
| 1208 |
+
}
|
| 1209 |
+
});
|
| 1210 |
+
}
|
| 1211 |
+
|
| 1212 |
+
// fetch update.txt
|
| 1213 |
+
async function fetchVersion() {
|
| 1214 |
+
try {
|
| 1215 |
+
const r = await fetch(UPDATE_URL + '?t=' + Date.now(), { cache: 'no-store' });
|
| 1216 |
+
if(r.ok) return (await r.text()).trim();
|
| 1217 |
+
} catch (e) { console.warn('update.txt fetch failed', e); }
|
| 1218 |
+
return null;
|
| 1219 |
+
}
|
| 1220 |
+
|
| 1221 |
+
// メインロジック
|
| 1222 |
+
let lastSeen = localStorage.getItem(LS_LAST_SEEN) || null;
|
| 1223 |
+
|
| 1224 |
+
async function check() {
|
| 1225 |
+
const latest = await fetchVersion();
|
| 1226 |
+
if(!latest) return;
|
| 1227 |
+
// ページの「最後に見たupdate.txt」と違えば reload を提示
|
| 1228 |
+
if(lastSeen && latest !== lastSeen) {
|
| 1229 |
+
enqueue('reload');
|
| 1230 |
+
}
|
| 1231 |
+
lastSeen = latest;
|
| 1232 |
+
localStorage.setItem(LS_LAST_SEEN, latest);
|
| 1233 |
+
|
| 1234 |
+
// sw_version と最新が違えば sw-update を提示(controller に依存しない)
|
| 1235 |
+
const swv = localStorage.getItem(LS_SW_VERSION);
|
| 1236 |
+
if(swv && swv !== latest) {
|
| 1237 |
+
enqueue('sw-update');
|
| 1238 |
+
}
|
| 1239 |
+
}
|
| 1240 |
+
|
| 1241 |
+
// 初回
|
| 1242 |
+
(async () => {
|
| 1243 |
+
const latest = await fetchVersion();
|
| 1244 |
+
if(latest) {
|
| 1245 |
+
if(!lastSeen) {
|
| 1246 |
+
lastSeen = latest;
|
| 1247 |
+
localStorage.setItem(LS_LAST_SEEN, latest);
|
| 1248 |
+
}
|
| 1249 |
+
const swv = localStorage.getItem(LS_SW_VERSION);
|
| 1250 |
+
if(swv && swv !== latest) enqueue('sw-update');
|
| 1251 |
+
}
|
| 1252 |
+
setInterval(check, CHECK_INTERVAL_MS);
|
| 1253 |
+
})();
|
| 1254 |
+
|
| 1255 |
+
// register ボタンを押したら sw を登録して sw_version を保存
|
| 1256 |
+
if(registerBtn) {
|
| 1257 |
+
registerBtn.addEventListener('click', async () => {
|
| 1258 |
+
registerBtn.disabled = true;
|
| 1259 |
+
try {
|
| 1260 |
+
log('既存の Service Worker を unregister...');
|
| 1261 |
+
const regs = await navigator.serviceWorker.getRegistrations();
|
| 1262 |
+
for(const r of regs) await r.unregister();
|
| 1263 |
+
log('registering...');
|
| 1264 |
+
await navigator.serviceWorker.register('/sw.js');
|
| 1265 |
+
const newv = await fetchVersion();
|
| 1266 |
+
if(newv) {
|
| 1267 |
+
localStorage.setItem(LS_SW_VERSION, newv);
|
| 1268 |
+
log('sw_version を保存: ' + newv);
|
| 1269 |
+
} else {
|
| 1270 |
+
log('update.txt を取得できませんでし��(sw_version 保存不可)。');
|
| 1271 |
+
}
|
| 1272 |
+
registerBtn.textContent = 'ページをリロード';
|
| 1273 |
+
registerBtn.disabled = false;
|
| 1274 |
+
registerBtn.onclick = () => location.reload();
|
| 1275 |
+
} catch (e) {
|
| 1276 |
+
err('登録失敗', e);
|
| 1277 |
+
registerBtn.disabled = false;
|
| 1278 |
+
}
|
| 1279 |
+
});
|
| 1280 |
+
}
|
| 1281 |
+
|
| 1282 |
+
});
|
| 1283 |
+
|
| 1284 |
+
</script>
|
| 1285 |
<script>
|
| 1286 |
document.addEventListener('DOMContentLoaded', function() {
|
| 1287 |
// 同期管理用の変数
|