Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions app/src/main/assets/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ function _md5(str) {
).join('');
}

// ── MD5 self-check (verifies implementation against RFC 1321 vector) ─────────
// ── MD5 self-test (verifies implementation against RFC 1321 vector) ─────────
// MD5("abc") must equal "900150983cd24fb0d6963f7d28e17f72"
let _md5SelfTestPassed = null;
function _md5SelfTest() {
Expand All @@ -378,7 +378,7 @@ function _md5SelfTest() {
const actual = _md5('abc');
_md5SelfTestPassed = (actual === expected);
if (!_md5SelfTestPassed) {
console.error('[Auth] MD5 self-check FAILED — got:', actual, ' expected:', expected);
console.error('[Auth] MD5 self-test FAILED — got:', actual, ' expected:', expected);
}
return _md5SelfTestPassed;
}
Expand Down Expand Up @@ -1044,7 +1044,6 @@ async function lfmCall(params) {
if (!state.apiKey) throw new Error('No API key set. Go to Settings.');
const url = new URL(LASTFM_BASE);
const p = { ...params, api_key: state.apiKey, format: 'json' };
if (state.sessionKey && !p.sk) p.sk = state.sessionKey;
Object.entries(p).forEach(([k, v]) => url.searchParams.set(k, v));
return _cachedFetch(url.toString());
}
Expand Down Expand Up @@ -1081,7 +1080,7 @@ function _lfmFriendlyError(code, rawMsg) {
*
* Flow:
* 1. Normalise key + secret (strip all non-hex chars, force lowercase)
* 2. Run MD5 self-check to catch any implementation regression
* 2. Run MD5 self-test to catch any implementation regression
* 3. Build params (NO format / callback at this point)
* 4. Compute api_sig = _lfmSig(params, secret)
* 5. Add format=json AFTER signing
Expand All @@ -1103,7 +1102,7 @@ async function lfmCallSigned(params) {
if (keyNorm.length !== 32) console.warn('[Auth] API key is', keyNorm.length, 'chars — expected 32');
if (secNorm.length !== 32) console.warn('[Auth] API secret is', secNorm.length, 'chars — expected 32');

// ── Step 2: MD5 self-check ─────────────────────────────────────────────────
// ── Step 2: MD5 self-test ─────────────────────────────────────────────────
if (!_md5SelfTest()) {
throw new Error('Internal MD5 error — authentication cannot proceed. Please report this bug.');
}
Expand Down Expand Up @@ -2425,4 +2424,4 @@ function _lpFallbackCopy(text) {
showToast('Copied', 'success');
} catch { showToast('Could not copy', 'error'); }
}
function sanitizeFilename(name) { return name.replace(/[^a-z0-9\-_\s]/gi,'').replace(/\s+/g,'_').substring(0,60)||'playlist'; }
function sanitizeFilename(name) { return name.replace(/[^a-z0-9\-_\s]/gi,'').replace(/\s+/g,'_').substring(0,60)||'playlist'; }
24 changes: 23 additions & 1 deletion app/src/main/assets/genres/genres.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,25 @@ const _GD_NO_ART = '2a96cbd8b46e442fc41c2b86b821562f';

// ── Screen init ───────────────────────────────────────────────
async function screen_genres() {
// Register hardware-back handler:
// 1st press closes the detail sheet if open; 2nd press lets nav.js pop the stack.
window._lwScreenBackHandlers['genres'] = function () {
// Close active dropdown first
if (_activeGDDropdown) {
_activeGDDropdown.classList.add('track-dropdown-leaving');
setTimeout(() => { if (_activeGDDropdown) { _activeGDDropdown.remove(); _activeGDDropdown = null; } }, 160);
return true;
}
// Close genre detail overlay if open
const overlay = document.getElementById('genreDetailOverlay');
if (overlay && overlay.classList.contains('open')) {
_closeGenreDetail();
return true;
}
// Nothing intercepted — let nav.js handle the back (pop history)
return false;
};

if (!state.username || !state.apiKey) {
_genresShowError('Enter your username and API key in Settings first.', 'No credentials found');
return;
Expand Down Expand Up @@ -714,6 +733,9 @@ function _openGDDropdown(btn, trackName, artistName) {
if (typeof showToast === 'function') showToast('Cover art not available', 'error');
}
}},
{ icon: 'delete', label: 'Delete Scrobble', fn: async () => {
await _lfmDeleteScrobble(trackName, artistName, null);
}},
];

const menu = document.createElement('div');
Expand Down Expand Up @@ -1223,7 +1245,7 @@ function _genresShowError(sub, title) {

// ── Tiny helpers ──────────────────────────────────────────────
function _esc(str) {
return String(str || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
return String(str || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'&quot;');
}
function _escAttr(str) {
return String(str || '').replace(/'/g,"\\'").replace(/"/g,'\\"');
Expand Down
17 changes: 17 additions & 0 deletions app/src/main/assets/home/home.js
Original file line number Diff line number Diff line change
Expand Up @@ -1246,6 +1246,23 @@ function _openTrackDropdown(btn, trackName, artistName) {
showToast('Cover art not available', 'error');
}
}},
{ icon: 'delete', label: 'Delete Scrobble', action: () => {
if (!state.sessionKey) { showToast('Sign in to delete scrobbles', 'error'); return; }
showModal(
'Delete Scrobble?',
`Remove \u201c${trackName}\u201d by ${artistName} from your Last.fm history?`,
async () => {
const ok = await _lfmDeleteScrobble(trackName, artistName, tsMs);
if (ok) {
const idx = _homeAllTracks.findIndex(
t => t.name === trackName && t.artist === artistName &&
(!tsMs || t._timestamp === tsMs)
);
if (idx !== -1) { _homeAllTracks.splice(idx, 1); _renderList(); }
}
}
);
}},
];

const menu = document.createElement('div');
Expand Down
10 changes: 9 additions & 1 deletion app/src/main/assets/nav.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,16 @@ async function navigateTo(page, opts) {

// 3. Record history — skipped for back-navigation so we don't re-push
// the page we just popped. Also skip on the very first load (no currentPage).
// Root tabs (home / generator / playlist) don't push other root tabs onto
// the back stack — switching between them always exits on back press.
const _ROOT_TABS = new Set(['home', 'generator', 'playlist']);
if (state.currentPage && !(opts && opts.isBack)) {
_navHistory.push(state.currentPage);
if (_ROOT_TABS.has(page) && _ROOT_TABS.has(state.currentPage)) {
// Tab → tab transition: clear the stack so back exits the app
_navHistory.length = 0;
} else {
_navHistory.push(state.currentPage);
}
}

// 3. Hide current screen
Expand Down
18 changes: 18 additions & 0 deletions app/src/main/assets/playlist/playlist.js
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,24 @@ function _plOpenTrackMenu(btn, trackName, artistName, plId) {
}
}
},
{
icon: 'delete', label: 'Delete Scrobble',
fn: () => {
if (!state.sessionKey) { showToast('Sign in to delete scrobbles', 'error'); return; }
showModal(
'Delete Scrobble?',
`Remove \u201c${trackName}\u201d by ${artistName} from your Last.fm history?`,
async () => {
const ok = await _lfmDeleteScrobble(trackName, artistName, null);
if (ok) {
document.querySelectorAll(
`.pl-track-row[data-lp-name="${CSS.escape(trackName)}"][data-lp-artist="${CSS.escape(artistName)}"]`
).forEach(row => row.remove());
}
}
);
}
},
];

const menu = document.createElement('div');
Expand Down
22 changes: 19 additions & 3 deletions app/src/main/assets/search/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,22 @@ function _showSearchDropdown(item, anchorBtn) {
});
}

// "Delete Scrobble" — tracks only
if (isTrack) {
menuItems.push({
icon: 'delete',
label: 'Delete Scrobble',
fn() {
if (!state.sessionKey) { showToast('Sign in to delete scrobbles', 'error'); return; }
showModal(
'Delete Scrobble?',
`Remove \u201c${item._track}\u201d by ${item._artist} from your Last.fm history?`,
async () => { await _lfmDeleteScrobble(item._track, item._artist, null); }
);
}
});
}

// ── Build DOM ─────────────────────────────────────────────
const menuEl = document.createElement('div');
menuEl.className = 'track-dropdown-menu';
Expand Down Expand Up @@ -746,8 +762,8 @@ function _showState(s) {
function _esc(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '&quot;');
}
11 changes: 11 additions & 0 deletions app/src/main/assets/settings/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,17 @@
</div>
</div>

<!-- ListenBrainz Artwork Fallback toggle -->
<div class="settings-toggle-row settings-toggle-row--bordered" onclick="toggleLbzArtwork()" style="cursor:pointer">
<div>
<div class="settings-toggle-label">ListenBrainz Artwork</div>
<div class="settings-hint">Use Cover Art Archive as final artwork fallback</div>
</div>
<div class="toggle-wrap">
<div class="toggle" id="lbzArtworkToggle"></div>
</div>
</div>

<!-- Accent color picker — Material You Monet palette cards -->
<div class="settings-divider"></div>
<div class="settings-toggle-label" style="margin-bottom:14px">Theme</div>
Expand Down
47 changes: 24 additions & 23 deletions app/src/main/assets/settings/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,13 @@ const _CW_EDGE_PAD = 16; // inset from canvas edge to wheel rim (CSS px)
// SCREEN ENTRY POINT
// ═════════════════════════════════════════════════════════════
window.screen_settings = function () {
document.body.classList.add('settings-open');
_injectBackButton();
// Register hardware-back handler so the Android back button
// and any navigation pop returns via the history stack.
// No top back button is injected and the bottom nav stays visible.
window._lwScreenBackHandlers['settings'] = function () {
_goBack();
return true;
};

const elUser = document.getElementById('settingsUsername');
const elKey = document.getElementById('settingsApiKey');
Expand All @@ -50,30 +55,18 @@ window.screen_settings = function () {
};

// ─────────────────────────────────────────────────────────────
// Back button
// Back navigation
// ─────────────────────────────────────────────────────────────
function _injectBackButton() {
if (document.getElementById('settingsBackBtn')) return;
const topbarLeft = document.querySelector('.topbar-left');
if (!topbarLeft) return;
const logo = topbarLeft.querySelector('svg');
if (logo) logo.style.display = 'none';
const btn = document.createElement('button');
btn.id = 'settingsBackBtn';
btn.className = 'settings-back-btn';
btn.setAttribute('aria-label', 'Back');
btn.innerHTML = '<span class="material-symbols-rounded">arrow_back</span>';
btn.onclick = _goBack;
topbarLeft.insertBefore(btn, topbarLeft.firstChild);
}

function _goBack() {
const logo = document.querySelector('.topbar-left svg');
if (logo) logo.style.display = '';
const btn = document.getElementById('settingsBackBtn');
if (btn) btn.remove();
document.body.classList.remove('settings-open');
navigateTo('home');
delete window._lwScreenBackHandlers['settings'];
// Pop nav history stack if possible, otherwise fall back to home
if (typeof _navHistory !== 'undefined' && _navHistory.length > 0) {
const prev = _navHistory.pop();
navigateTo(prev, { isBack: true });
} else {
navigateTo('home', { isBack: true });
}
}

// ─────────────────────────────────────────────────────────────
Expand All @@ -83,6 +76,7 @@ function _refreshToggles() {
_setToggle('amoledToggle', document.body.classList.contains('amoled-mode'));
_setToggle('dynamicThemeToggle', state.accentMode === 'dynamic');
_setToggle('itunesArtworkToggle', localStorage.getItem('lw_use_itunes') !== '0');
_setToggle('lbzArtworkToggle', localStorage.getItem('lw_use_lbz') !== '0');
}

function _setToggle(id, active) {
Expand Down Expand Up @@ -179,6 +173,13 @@ function toggleItunesArtwork() {
_setToggle('itunesArtworkToggle', next);
}

function toggleLbzArtwork() {
const wasOn = localStorage.getItem('lw_use_lbz') !== '0';
const next = !wasOn;
localStorage.setItem('lw_use_lbz', next ? '1' : '0');
_setToggle('lbzArtworkToggle', next);
}

function setPaletteAccent(hue) {
const hex = _settHslToHex(hue, 0.65, 0.52);
const hexL = _settHslToHex(hue, 0.65, 0.72);
Expand Down
Loading