Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc5b97ecf4 | ||
|
|
a2f7a7fceb |
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"VERSION": "v0.7.2",
|
||||
"VERSION_NUMBER": "0.7.2",
|
||||
"BUILD_DATE": "2026-04-19T17:37:32.473Z"
|
||||
"VERSION": "v0.7.4",
|
||||
"VERSION_NUMBER": "0.7.4",
|
||||
"BUILD_DATE": "2026-04-19T18:00:32.861Z"
|
||||
}
|
||||
|
||||
172
www/music.html
172
www/music.html
@@ -932,6 +932,15 @@
|
||||
min-width: 52px;
|
||||
}
|
||||
|
||||
#musicSaveToggleBtn {
|
||||
min-width: 52px;
|
||||
}
|
||||
|
||||
#musicSaveToggleBtn[data-enabled='true'] {
|
||||
border-color: var(--accent-color);
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
#musicProgressWrap {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
@@ -1217,6 +1226,7 @@
|
||||
|
||||
<div id="musicPlayerControls">
|
||||
<button id="musicPlaybackModeBtn" class="btn musicControlBtn" type="button" title="Playback mode">—</button>
|
||||
<button id="musicSaveToggleBtn" class="btn musicControlBtn" type="button" title="Auto-save now playing track to Blossom" aria-pressed="false">💾</button>
|
||||
<button id="musicPrevBtn" class="btn musicControlBtn" type="button">◀</button>
|
||||
<button id="musicPlayPauseBtn" class="btn musicControlBtn" type="button">▶</button>
|
||||
<button id="musicNextBtn" class="btn musicControlBtn" type="button">▶▶</button>
|
||||
@@ -1367,6 +1377,7 @@
|
||||
import { HamburgerMorphing } from "./hamburger_morphing/hamburger.mjs";
|
||||
import { initFooterRelayStatus, updateFooterRelayStatus, initSidenavRelaySection, updateSidenavRelaySection, setRelayActivityState } from './js/relay-ui.mjs';
|
||||
import { initBlossomSection, updateBlossomSection } from './js/blossom-ui.mjs';
|
||||
import { uploadToServer, getDefaultBlossomServer } from './js/blossom-api.mjs';
|
||||
|
||||
import { initAiSectionWithLocalConfig } from './js/ai-ui.mjs';
|
||||
import { GreyscaleAPI } from './js/greyscale-api.mjs';
|
||||
@@ -1475,6 +1486,7 @@ import { GreyscaleAPI } from './js/greyscale-api.mjs';
|
||||
queueClearBtn: document.getElementById('musicQueueClearBtn'),
|
||||
queueList: document.getElementById('musicQueueList'),
|
||||
playbackModeBtn: document.getElementById('musicPlaybackModeBtn'),
|
||||
saveToggleBtn: document.getElementById('musicSaveToggleBtn'),
|
||||
shareModal: document.getElementById('musicShareModal'),
|
||||
shareTarget: document.getElementById('musicShareTarget'),
|
||||
shareRendered: document.getElementById('musicShareRendered'),
|
||||
@@ -1531,6 +1543,7 @@ import { GreyscaleAPI } from './js/greyscale-api.mjs';
|
||||
};
|
||||
let isHandlingRoute = false;
|
||||
let playbackMode = 'normal';
|
||||
let autoSaveToBlossom = false;
|
||||
let currentPlayingTrackId = '';
|
||||
let pendingShareUrl = '';
|
||||
let pendingShareArtUrl = '';
|
||||
@@ -1617,6 +1630,21 @@ import { GreyscaleAPI } from './js/greyscale-api.mjs';
|
||||
return message.includes('authentication required');
|
||||
}
|
||||
|
||||
async function tryGetPubkeyWithoutPrompt({ attempts = 8, delayMs = 350 } = {}) {
|
||||
for (let i = 0; i < attempts; i += 1) {
|
||||
try {
|
||||
const pubkey = await getPubkey();
|
||||
if (pubkey) return pubkey;
|
||||
} catch {
|
||||
// Signer can attach slightly after initial script execution.
|
||||
}
|
||||
if (i < attempts - 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function isReadOnlyTargetMode() {
|
||||
return Boolean(targetPubkey && (!currentPubkey || targetPubkey !== currentPubkey));
|
||||
}
|
||||
@@ -1695,13 +1723,8 @@ import { GreyscaleAPI } from './js/greyscale-api.mjs';
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
currentPubkey = await getPubkey();
|
||||
isAuthenticated = Boolean(currentPubkey);
|
||||
} catch {
|
||||
currentPubkey = null;
|
||||
isAuthenticated = false;
|
||||
}
|
||||
currentPubkey = await tryGetPubkeyWithoutPrompt();
|
||||
isAuthenticated = Boolean(currentPubkey);
|
||||
}
|
||||
|
||||
async function initializeAuthenticatedPageFeatures() {
|
||||
@@ -1827,6 +1850,14 @@ import { GreyscaleAPI } from './js/greyscale-api.mjs';
|
||||
return `music-queue:${currentPubkey || 'anon'}`;
|
||||
}
|
||||
|
||||
function getAutoSaveBlossomStorageKey() {
|
||||
return `music-auto-save-blossom:${currentPubkey || 'anon'}`;
|
||||
}
|
||||
|
||||
function getSavedTrackBlossomMapStorageKey() {
|
||||
return `music-saved-track-blossom-map:${currentPubkey || 'anon'}`;
|
||||
}
|
||||
|
||||
function normalizeQueueTrack(track) {
|
||||
if (!track || typeof track !== 'object') return null;
|
||||
return {
|
||||
@@ -1954,6 +1985,64 @@ import { GreyscaleAPI } from './js/greyscale-api.mjs';
|
||||
}
|
||||
}
|
||||
|
||||
const savedTrackBlossomUrlById = new Map();
|
||||
const pendingTrackBlossomSaves = new Set();
|
||||
|
||||
function updateSaveToggleButton() {
|
||||
if (!musicEls.saveToggleBtn) return;
|
||||
musicEls.saveToggleBtn.dataset.enabled = autoSaveToBlossom ? 'true' : 'false';
|
||||
musicEls.saveToggleBtn.setAttribute('aria-pressed', autoSaveToBlossom ? 'true' : 'false');
|
||||
musicEls.saveToggleBtn.textContent = autoSaveToBlossom ? '💾✓' : '💾';
|
||||
musicEls.saveToggleBtn.title = autoSaveToBlossom
|
||||
? 'Auto-save now playing track to Blossom: ON'
|
||||
: 'Auto-save now playing track to Blossom: OFF';
|
||||
}
|
||||
|
||||
function loadAutoSaveBlossomSetting() {
|
||||
try {
|
||||
autoSaveToBlossom = localStorage.getItem(getAutoSaveBlossomStorageKey()) === 'true';
|
||||
} catch {
|
||||
autoSaveToBlossom = false;
|
||||
}
|
||||
updateSaveToggleButton();
|
||||
}
|
||||
|
||||
function persistAutoSaveBlossomSetting() {
|
||||
try {
|
||||
localStorage.setItem(getAutoSaveBlossomStorageKey(), autoSaveToBlossom ? 'true' : 'false');
|
||||
} catch {
|
||||
// Ignore storage write failures.
|
||||
}
|
||||
}
|
||||
|
||||
function loadSavedTrackBlossomMap() {
|
||||
savedTrackBlossomUrlById.clear();
|
||||
try {
|
||||
const raw = localStorage.getItem(getSavedTrackBlossomMapStorageKey());
|
||||
if (!raw) return;
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed || typeof parsed !== 'object') return;
|
||||
Object.entries(parsed).forEach(([trackId, url]) => {
|
||||
if (!trackId || typeof url !== 'string' || !url.trim()) return;
|
||||
savedTrackBlossomUrlById.set(String(trackId), url.trim());
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('[music] Could not load saved Blossom track map:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function persistSavedTrackBlossomMap() {
|
||||
try {
|
||||
const payload = {};
|
||||
savedTrackBlossomUrlById.forEach((url, trackId) => {
|
||||
payload[trackId] = url;
|
||||
});
|
||||
localStorage.setItem(getSavedTrackBlossomMapStorageKey(), JSON.stringify(payload));
|
||||
} catch {
|
||||
// Ignore storage write failures.
|
||||
}
|
||||
}
|
||||
|
||||
const mountedMusicMenus = {
|
||||
myPlaylists: [],
|
||||
friendPlaylists: [],
|
||||
@@ -3413,6 +3502,7 @@ import { GreyscaleAPI } from './js/greyscale-api.mjs';
|
||||
updatePlayingResultHighlight();
|
||||
saveQueueLocal();
|
||||
setMusicStatus(`Playing queue: ${track.title}`, 'ok');
|
||||
void maybeAutoSaveTrackToBlossom(track);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setMusicStatus(`Queue playback failed: ${error.message || 'Unknown error'}`, 'error');
|
||||
@@ -4368,6 +4458,65 @@ import { GreyscaleAPI } from './js/greyscale-api.mjs';
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTrackToBlossom(track, { suppressStatus = true } = {}) {
|
||||
const trackId = String(track?.id || '').trim();
|
||||
if (!trackId) return '';
|
||||
if (savedTrackBlossomUrlById.has(trackId)) {
|
||||
return savedTrackBlossomUrlById.get(trackId) || '';
|
||||
}
|
||||
if (pendingTrackBlossomSaves.has(trackId)) return '';
|
||||
|
||||
const targetServer = getDefaultBlossomServer();
|
||||
if (!targetServer) {
|
||||
if (!suppressStatus) setMusicStatus('No Blossom server configured.', 'error');
|
||||
return '';
|
||||
}
|
||||
|
||||
pendingTrackBlossomSaves.add(trackId);
|
||||
try {
|
||||
if (!suppressStatus) {
|
||||
setMusicStatus(`Saving to Blossom: ${track.title || 'Track'}...`, 'neutral');
|
||||
}
|
||||
|
||||
const { blob, ext } = await fetchDownloadBlobForTrack(track);
|
||||
const title = sanitizeFilenamePart(track.title || 'Unknown title') || 'Unknown title';
|
||||
const artist = sanitizeFilenamePart(track.artist || 'Unknown artist') || 'Unknown artist';
|
||||
const fileName = `${artist} - ${title}.${ext}`;
|
||||
const file = new File([blob], fileName, {
|
||||
type: blob.type || 'application/octet-stream',
|
||||
});
|
||||
|
||||
const result = await uploadToServer(file, targetServer);
|
||||
const sha256 = String(result?.sha256 || '').trim();
|
||||
if (!sha256) throw new Error('Upload returned no SHA-256 hash.');
|
||||
|
||||
const suffix = ext ? `.${ext.replace(/^\./, '')}` : '';
|
||||
const blossomUrl = `${String(result?.serverUrl || targetServer).replace(/\/$/, '')}/${sha256}${suffix}`;
|
||||
|
||||
savedTrackBlossomUrlById.set(trackId, blossomUrl);
|
||||
persistSavedTrackBlossomMap();
|
||||
|
||||
if (!suppressStatus) {
|
||||
setMusicStatus(`Saved to Blossom: ${track.title || 'Track'}`, 'ok');
|
||||
}
|
||||
|
||||
return blossomUrl;
|
||||
} catch (error) {
|
||||
console.error('[music] saveTrackToBlossom failed:', error);
|
||||
if (!suppressStatus) {
|
||||
setMusicStatus(`Save to Blossom failed: ${error.message || 'Unknown error'}`, 'error');
|
||||
}
|
||||
return '';
|
||||
} finally {
|
||||
pendingTrackBlossomSaves.delete(trackId);
|
||||
}
|
||||
}
|
||||
|
||||
async function maybeAutoSaveTrackToBlossom(track) {
|
||||
if (!autoSaveToBlossom) return;
|
||||
await saveTrackToBlossom(track, { suppressStatus: true });
|
||||
}
|
||||
|
||||
async function downloadMusicByIndex(index) {
|
||||
if (!Number.isInteger(index) || index < 0 || index >= musicTracks.length) return;
|
||||
const track = musicTracks[index];
|
||||
@@ -4956,9 +5105,16 @@ import { GreyscaleAPI } from './js/greyscale-api.mjs';
|
||||
updatePlayingResultHighlight();
|
||||
});
|
||||
musicEls.playbackModeBtn?.addEventListener('click', cyclePlaybackMode);
|
||||
musicEls.saveToggleBtn?.addEventListener('click', () => {
|
||||
autoSaveToBlossom = !autoSaveToBlossom;
|
||||
persistAutoSaveBlossomSetting();
|
||||
updateSaveToggleButton();
|
||||
setMusicStatus(`Auto-save to Blossom ${autoSaveToBlossom ? 'enabled' : 'disabled'}.`, 'ok');
|
||||
});
|
||||
setMusicPlayButtonState();
|
||||
updateMusicPlayerMeta(null);
|
||||
updatePlaybackModeButton();
|
||||
updateSaveToggleButton();
|
||||
|
||||
musicEls.queueClearBtn?.addEventListener('click', clearQueue);
|
||||
|
||||
@@ -5059,6 +5215,8 @@ import { GreyscaleAPI } from './js/greyscale-api.mjs';
|
||||
myPlaylists.clear();
|
||||
}
|
||||
loadQueueLocal();
|
||||
loadAutoSaveBlossomSetting();
|
||||
loadSavedTrackBlossomMap();
|
||||
updateViewerControls();
|
||||
renderMyPlaylists();
|
||||
renderFriendPlaylists();
|
||||
|
||||
Reference in New Issue
Block a user