|
146 | 146 | let controlStatusTimer: ReturnType<typeof setInterval> | undefined; |
147 | 147 | let thumbnailRetryTimer: ReturnType<typeof setInterval> | undefined; |
148 | 148 |
|
| 149 | + let wakeLock: any = null; |
| 150 | +
|
| 151 | + const requestWakeLock = async () => { |
| 152 | + if (typeof navigator === "undefined" || !("wakeLock" in navigator)) return; |
| 153 | + try { |
| 154 | + if (wakeLock) return; |
| 155 | + wakeLock = await (navigator as any).wakeLock.request("screen"); |
| 156 | + wakeLock.addEventListener("release", () => { |
| 157 | + wakeLock = null; |
| 158 | + }); |
| 159 | + } catch (err) { |
| 160 | + console.warn("Wake Lock request failed", err); |
| 161 | + } |
| 162 | + }; |
| 163 | +
|
| 164 | + const releaseWakeLock = async () => { |
| 165 | + if (wakeLock) { |
| 166 | + await wakeLock.release().catch(() => {}); |
| 167 | + wakeLock = null; |
| 168 | + } |
| 169 | + }; |
| 170 | +
|
| 171 | + const handleVisibilityChange = () => { |
| 172 | + if (document.visibilityState === "visible" && canControl) { |
| 173 | + void requestWakeLock(); |
| 174 | + } |
| 175 | + }; |
| 176 | +
|
149 | 177 | const THUMBNAIL_BATCH_SIZE = 16; |
150 | 178 | const THUMBNAIL_RETRY_DELAY_MS = 8000; |
151 | 179 | const THUMBNAIL_MAX_ATTEMPTS = 3; |
|
219 | 247 | } |
220 | 248 | } |
221 | 249 |
|
| 250 | + $: { |
| 251 | + if (canControl) { |
| 252 | + void requestWakeLock(); |
| 253 | + } else { |
| 254 | + void releaseWakeLock(); |
| 255 | + } |
| 256 | + } |
| 257 | +
|
222 | 258 | $: selectedGame = |
223 | 259 | selectedGameId === null |
224 | 260 | ? null |
|
542 | 578 | }; |
543 | 579 |
|
544 | 580 | const adoptActiveGame = (nextActiveGameId: number | null) => { |
| 581 | + const prevActiveGameId = activeGameId; |
545 | 582 | activeGameId = nextActiveGameId; |
546 | 583 | if (nextActiveGameId === null) { |
547 | 584 | return; |
|
555 | 592 | const shouldLoadMemo = selectedGameId !== target.id; |
556 | 593 | selectedGameId = target.id; |
557 | 594 | didSelectGameManually = false; |
558 | | - if (activeView === "detail") { |
| 595 | + if (prevActiveGameId !== nextActiveGameId) { |
559 | 596 | activeView = "controller"; |
560 | 597 | } |
561 | 598 |
|
|
915 | 952 |
|
916 | 953 | document.addEventListener("focusin", handleViewportReset); |
917 | 954 | document.addEventListener("focusout", handleViewportReset); |
| 955 | + document.addEventListener("visibilitychange", handleVisibilityChange); |
918 | 956 |
|
919 | 957 | void getAllCachedImages().then((cached) => { |
920 | 958 | cached.forEach(({ path, blob }) => { |
|
957 | 995 |
|
958 | 996 | document.removeEventListener("focusin", handleViewportReset); |
959 | 997 | document.removeEventListener("focusout", handleViewportReset); |
| 998 | + document.removeEventListener("visibilitychange", handleVisibilityChange); |
| 999 | + void releaseWakeLock(); |
960 | 1000 |
|
961 | 1001 | cleanupCallbacks.forEach((callback) => callback()); |
962 | 1002 | cleanupCallbacks = []; |
|
1062 | 1102 | <span class="i-material-symbols:chevron-right-rounded text-[22px]" /> |
1063 | 1103 | </button> |
1064 | 1104 | {/if} |
1065 | | - {#if lastActionText} |
1066 | | - <div class="toast-line">{lastActionText}</div> |
1067 | | - {/if} |
| 1105 | + |
1068 | 1106 | </section> |
1069 | 1107 |
|
1070 | 1108 | <section class="content detail-content" class:hidden={activeView !== 'detail'}> |
|
1124 | 1162 | {:else} |
1125 | 1163 | <section class="empty-state">ゲームを選択してください</section> |
1126 | 1164 | {/if} |
1127 | | - {#if lastActionText} |
1128 | | - <div class="toast-line">{lastActionText}</div> |
1129 | | - {/if} |
| 1165 | + |
1130 | 1166 | </section> |
1131 | 1167 |
|
1132 | 1168 | <section class="content connect-content" class:hidden={activeView !== 'connect'}> |
|
1177 | 1213 | <span class="i-material-symbols:chevron-right-rounded text-[22px]" /> |
1178 | 1214 | </button> |
1179 | 1215 | {/if} |
1180 | | - {#if lastActionText} |
1181 | | - <div class="toast-line">{lastActionText}</div> |
1182 | | - {/if} |
| 1216 | + |
1183 | 1217 | </section> |
1184 | 1218 |
|
1185 | 1219 | <section class="content controller-content" class:controller-mode={true} class:hidden={activeView !== 'controller'}> |
1186 | 1220 | {#if canControl && selectedGame} |
1187 | 1221 | <section class="controller-panel"> |
1188 | | - <button type="button" class="back-button" on:click={() => (activeView = "library")}> |
1189 | | - <span class="i-material-symbols:arrow-back-rounded text-[20px]" /> |
1190 | | - <span>一覧</span> |
| 1222 | + <button |
| 1223 | + type="button" |
| 1224 | + class="reconnect-small-button" |
| 1225 | + on:click={() => window.location.reload()} |
| 1226 | + > |
| 1227 | + <span class="i-material-symbols:refresh-rounded text-[15px]" /> |
| 1228 | + <span>再接続</span> |
1191 | 1229 | </button> |
1192 | 1230 | <div class="controller-kicker">Now Playing</div> |
1193 | 1231 | <div class="controller-game">{selectedGame.title}</div> |
|
1230 | 1268 | <span>文字消しスクショ</span> |
1231 | 1269 | </button> |
1232 | 1270 |
|
| 1271 | + <section class="memo-preview"> |
| 1272 | + <div class="section-head"> |
| 1273 | + <h2>メモ</h2> |
| 1274 | + <span class="subtle">編集</span> |
| 1275 | + </div> |
| 1276 | + <div class="detail-memo-editor"> |
| 1277 | + <textarea |
| 1278 | + bind:value={memoText} |
| 1279 | + placeholder="メモを入力" |
| 1280 | + /> |
| 1281 | + <button |
| 1282 | + type="button" |
| 1283 | + disabled={connectionState !== "connected" || selectedGameId === null} |
| 1284 | + on:click={syncMemo} |
| 1285 | + > |
| 1286 | + メモを同期 |
| 1287 | + </button> |
| 1288 | + </div> |
| 1289 | + </section> |
| 1290 | + |
1233 | 1291 | </section> |
1234 | 1292 | {:else} |
1235 | 1293 | <section class="empty-state controller-waiting"> |
|
1243 | 1301 | {/if} |
1244 | 1302 | </section> |
1245 | 1303 | {/if} |
1246 | | - {#if lastActionText} |
1247 | | - <div class="toast-line">{lastActionText}</div> |
1248 | | - {/if} |
| 1304 | + |
1249 | 1305 | </section> |
1250 | 1306 |
|
1251 | 1307 | <nav class="bottom-nav" aria-label="スマホ連携"> |
|
1281 | 1337 |
|
1282 | 1338 | <style> |
1283 | 1339 | .mobile-shell { |
1284 | | - height: 100vh; |
1285 | | - height: 100dvh; |
| 1340 | + position: fixed; |
| 1341 | + top: 0; |
| 1342 | + left: 0; |
| 1343 | + right: 0; |
| 1344 | + bottom: 0; |
| 1345 | + height: auto; |
1286 | 1346 | background: #151515; |
1287 | 1347 | color: white; |
1288 | 1348 | display: flex; |
|
1293 | 1353 | } |
1294 | 1354 |
|
1295 | 1355 | .topbar { |
1296 | | - min-height: 64px; |
1297 | | - padding: calc(14px + env(safe-area-inset-top, 0px)) 16px 12px; |
| 1356 | + min-height: 52px; |
| 1357 | + padding: calc(8px + env(safe-area-inset-top, 0px)) 16px 8px; |
1298 | 1358 | display: flex; |
1299 | 1359 | align-items: center; |
1300 | 1360 | justify-content: space-between; |
|
1624 | 1684 | font-weight: 700; |
1625 | 1685 | } |
1626 | 1686 |
|
| 1687 | + .reconnect-small-button { |
| 1688 | + display: inline-flex; |
| 1689 | + align-items: center; |
| 1690 | + gap: 4px; |
| 1691 | + border: 1px solid rgb(255 255 255 / 0.15); |
| 1692 | + border-radius: 4px; |
| 1693 | + background: rgb(255 255 255 / 0.05); |
| 1694 | + color: rgb(255 255 255 / 0.7); |
| 1695 | + padding: 3px 8px; |
| 1696 | + font-size: 11px; |
| 1697 | + font-weight: 800; |
| 1698 | + width: fit-content; |
| 1699 | + } |
| 1700 | +
|
1627 | 1701 | .detail-kicker, |
1628 | 1702 | .controller-kicker { |
1629 | 1703 | margin-top: 8px; |
|
0 commit comments