diff --git a/ANDROID.md b/ANDROID.md index 6218c2e..c773332 100644 --- a/ANDROID.md +++ b/ANDROID.md @@ -26,6 +26,7 @@ implements is `research/mobile-mockup/` (gitignored; see its README). | `src/mobile/Sheet.svelte`, `HalfSheet.svelte`, `SyncRoom.svelte`, `Lane.svelte`, `Cover.svelte`, `Release.svelte` | the player (art / lyrics / queue faces, container transform from the mini bar, drags), the bottom sheets (track, record, sort, the page overflow, the sleep timer), the tap-to-sync room, the waveform lane, the two card pieces (a long press on a card is its sheet) | | `src/mobile/screens/` | Home, Search (the library half: records and artists here, tracks through `library_track_search`), Library (albums / artists / tracks / playlists), Album, Artist (the library's shell at once, the catalog dissolving in: portrait, Popular, discography with what is here marked), CatalogAlbum (a catalog record, rows the library has play), Playlist, Community (sign in; rooms, chat, buddies, taste — the Soulseek side; the private layer waits), Room, Thread, You (Appearance switches, real) | | `src/mobile/prefs.svelte.js` | Appearance, kept in localStorage: ambience, accent from artwork, motion | +| `src/mobile/util.js` | what every screen shares and no screen should write twice: the formatters (`mmss` / `longDur` / `stamp` for a length in seconds, in words and in milliseconds; `ago` / `clock`; `fmtBytes` / `fmtCount`; `fmtOf` / `albumFmt`), the name and folder readers a peer's file needs, the `drag` action, and `devBuild` | | `src-tauri/tauri.android.conf.json` | merged over `tauri.conf.json` on Android: one window at `mobile.html`, no desktop resources, `minSdkVersion` 26 | | `src-tauri/gen/android/` | the generated Gradle project (`npx tauri android init`); `minSdk = 26`. Gitignored | | `src-tauri/android/build.sh`, `deploy.sh` | the detour build (below) and build → install → launch → screenshot | diff --git a/src/mobile/HalfSheet.svelte b/src/mobile/HalfSheet.svelte index 8f5e527..0ad0f9f 100644 --- a/src/mobile/HalfSheet.svelte +++ b/src/mobile/HalfSheet.svelte @@ -16,7 +16,7 @@ import { pls, bulk, bulkable, runBulk, coverFor, queueable, entryOf, playFrom, shufflePlay, togglePin, renamePlaylist, describePlaylist, duplicatePlaylist, deletePlaylist, removeItems, addEntries, createPlaylist, moveItem, coverFromPhoto, coverClear } from "./playlists.svelte.js"; import { DOORS, start as startImport, pickImportFolder, startFolder } from "./importer.svelte.js"; import { slsk, refreshRooms, disconnect, openThread, addBuddy, removeBuddy, leaveRoom } from "./slsk.svelte.js"; - import { lib, albumById, albumsBy, tracksOf, loadLibrary, refreshPlaylists, refreshRecents } from "./lib.svelte.js"; + import { lib, albumById, tracksOf, loadLibrary, refreshPlaylists } from "./lib.svelte.js"; import { lyr, nudge, searchCandidates, closeCandidates, chooseCandidate, forgetLyrics, pasteLyrics, setAutoSync, openTapSync, tapProgress, reduceVocals, backToOriginal } from "./lyrics.svelte.js"; import { art, wantArt } from "./art.svelte.js"; import { metaOf, display, realName, setMeta, clearMeta } from "./contacts.svelte.js"; diff --git a/src/mobile/Sheet.svelte b/src/mobile/Sheet.svelte index 2c6c7c1..853e12b 100644 --- a/src/mobile/Sheet.svelte +++ b/src/mobile/Sheet.svelte @@ -13,7 +13,6 @@ import Lane from "./Lane.svelte"; import Cover from "./Cover.svelte"; import { art } from "./art.svelte.js"; - import { albumsBy } from "./lib.svelte.js"; import { player, next, prev, toggle, seekFrac, seekSecs, playQueueAt, toggleShuffle, cycleRepeat } from "./player.svelte.js"; import { nav, closeSheet, morphSource, openHalf, openViewer, push } from "./nav.svelte.js"; import { lyr, loadLyrics } from "./lyrics.svelte.js"; diff --git a/src/mobile/player.svelte.js b/src/mobile/player.svelte.js index d471389..72a1010 100644 --- a/src/mobile/player.svelte.js +++ b/src/mobile/player.svelte.js @@ -724,7 +724,7 @@ function listenTick(pos) { const title = t.title; const artist = (t.artist || player.album?.artist || "").trim(); if (!title || !artist) return; - const key = artist + "" + title; + const key = artist + "\u0000" + title; if (session?.key !== key) { listenFlush(); scheduleRecents(1500); diff --git a/src/mobile/screens/Artist.svelte b/src/mobile/screens/Artist.svelte index f94d067..19d6ece 100644 --- a/src/mobile/screens/Artist.svelte +++ b/src/mobile/screens/Artist.svelte @@ -22,7 +22,7 @@ import { catalog, lookupArtist, artistKey, fans, yearOf, looseTitle, loadExtras, loadPopular, loadHeroSeed, loadOverview, picture } from "../catalog.svelte.js"; import { resolver, streamSong, streamCatalogAlbum } from "../resolve.svelte.js"; import { discoTabs, activeYears, latestOf, timelineOf, isUpcoming, SOCIAL_LABEL } from "../disco.js"; - import { shuffled, mmss, ago } from "../util.js"; + import { shuffled, mmss, ago, fmtCount } from "../util.js"; /** `seed`: the search's own row (a CatalogArtist), when the page was * opened from one — the identity is then known, no name search. */ @@ -109,7 +109,6 @@ const popular = $derived((showAllPopular ? popularAll : popularAll.slice(0, 5)).map((t) => ({ t, hit: libByTitle.get(looseTitle(t.title)) || null }))); const popPeak = $derived(popularAll.reduce((m, t) => Math.max(m, t.listeners || 0), 0)); const share = (t) => (popPeak ? Math.max(0.04, (t.listeners || 0) / popPeak) : 0); - const fmtCount = (n) => (!n ? "" : n >= 1e6 ? (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M" : n >= 1e3 ? Math.round(n / 1e3) + "k" : String(n)); /* ── the discography, filed ──────────────────────────────────────── */ /* the overview's raw first page holds the discography up until the full diff --git a/src/mobile/screens/CatalogAlbum.svelte b/src/mobile/screens/CatalogAlbum.svelte index e68b50c..867a151 100644 --- a/src/mobile/screens/CatalogAlbum.svelte +++ b/src/mobile/screens/CatalogAlbum.svelte @@ -16,7 +16,7 @@ import { resolver, streamSong, streamCatalogAlbum, saveCatalogAlbum } from "../resolve.svelte.js"; import { releaseFamilies, editionLabel, splitDescription } from "../../lib/editions.js"; import { say } from "../trouble.js"; - import { mmss, longDur } from "../util.js"; + import { mmss, longDur, fmtCount } from "../util.js"; let { id, artist = "", title = "", screenEl = null } = $props(); @@ -81,7 +81,6 @@ const peak = $derived(rows.reduce((m, t) => Math.max(m, listenersOf(t) || 0), 0)); const showCount = $derived(peak > 0 && rows.length >= 3); const hasUnreleased = $derived(rows.some((t) => t.unreleased)); - const fmtCount = (n) => (!n ? "" : n >= 1e6 ? (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M" : n >= 1e3 ? Math.round(n / 1e3) + "k" : String(n)); const fullDate = (d) => { const v = String(d || ""); if (v.length < 10) return yearOf(v); diff --git a/src/mobile/screens/Import.svelte b/src/mobile/screens/Import.svelte index 1cb50a3..3e6cd95 100644 --- a/src/mobile/screens/Import.svelte +++ b/src/mobile/screens/Import.svelte @@ -7,20 +7,13 @@ import Icon from "../../components/Icon.svelte"; import { importer, FORMAT_LABEL, kept, checkedFound, commit, cancel } from "../importer.svelte.js"; import { openHalf, back, replace } from "../nav.svelte.js"; - import { mmss } from "../util.js"; + import { mmss, stamp } from "../util.js"; const w = $derived(importer.draft); const rows = $derived(kept()); const n = $derived(rows.length); const found = $derived(checkedFound()); - function stamp(ms) { - const s = Math.floor((ms || 0) / 1000); - const h = Math.floor(s / 3600); - const m = Math.floor((s % 3600) / 60); - const sec = s % 60; - return (h ? h + ":" + String(m).padStart(2, "0") : String(m)) + ":" + String(sec).padStart(2, "0"); - } /* [tag class, label] — the desktop's evidence column */ function verdict(e) { if (e.resolution === "local") return ["ok", e.matched_by === "path" || e.matched_by === "file" ? "file" : e.matched_by === "relinked" ? "relinked" : "tags"]; diff --git a/src/mobile/screens/Library.svelte b/src/mobile/screens/Library.svelte index 4a41a54..b225109 100644 --- a/src/mobile/screens/Library.svelte +++ b/src/mobile/screens/Library.svelte @@ -13,7 +13,6 @@ import { push, go, patch, openHalf } from "../nav.svelte.js"; import { looseTitle, yearOf } from "../catalog.svelte.js"; import { isUpcoming } from "../disco.js"; - import { longDur } from "../util.js"; let { entry, screenEl = null } = $props(); diff --git a/src/mobile/screens/Playlist.svelte b/src/mobile/screens/Playlist.svelte index df5061c..52af637 100644 --- a/src/mobile/screens/Playlist.svelte +++ b/src/mobile/screens/Playlist.svelte @@ -15,7 +15,7 @@ import { openHalf } from "../nav.svelte.js"; import { pls, bulk, cancelBulk, dismissBulk, coverFor, isSetlist, queueable, unresolvedCount, missingCount, totalSecs, playFrom, shufflePlay, rowClick } from "../playlists.svelte.js"; import { say } from "../trouble.js"; - import { mmss, longDur } from "../util.js"; + import { mmss, longDur, stamp } from "../util.js"; let { id } = $props(); @@ -63,13 +63,6 @@ const playable = $derived(items.filter(queueable).length); const nowItemId = $derived(player.queue[player.qi]?.plItemId ?? null); - function stamp(ms) { - const s = Math.floor((ms || 0) / 1000); - const h = Math.floor(s / 3600); - const m = Math.floor((s % 3600) / 60); - const sec = s % 60; - return (h ? h + ":" + String(m).padStart(2, "0") : String(m)) + ":" + String(sec).padStart(2, "0"); - } function sheet(item) { openHalf("pltrack", { playlist: pl, items, item }); } function tap(item) { if (!rowClick(pl, items, item)) sheet(item); } diff --git a/src/mobile/screens/Room.svelte b/src/mobile/screens/Room.svelte index 949b341..9a4ab18 100644 --- a/src/mobile/screens/Room.svelte +++ b/src/mobile/screens/Room.svelte @@ -4,9 +4,9 @@ opens that person — and a composer that stays above the mini bar. */ import { tick } from "svelte"; import Icon from "../../components/Icon.svelte"; - import { slsk, sayRoom, leaveRoom, openThread, addBuddy } from "../slsk.svelte.js"; + import { slsk, sayRoom, openThread } from "../slsk.svelte.js"; import { display, realName } from "../contacts.svelte.js"; - import { push, back, openHalf } from "../nav.svelte.js"; + import { push, openHalf } from "../nav.svelte.js"; import { player } from "../player.svelte.js"; import { clock } from "../util.js"; diff --git a/src/mobile/screens/Search.svelte b/src/mobile/screens/Search.svelte index aa0aeec..9e9728b 100644 --- a/src/mobile/screens/Search.svelte +++ b/src/mobile/screens/Search.svelte @@ -22,7 +22,7 @@ import { catalog, artistKey, loadProfile, seedFor, lookupArtist, yearOf, fans, picture } from "../catalog.svelte.js"; import { latestOf } from "../disco.js"; import { say } from "../trouble.js"; - import { mmss, fmtBytes, titleFromName, folderMeta } from "../util.js"; + import { mmss, fmtBytes, titleFromName } from "../util.js"; let { entry, screenEl = null } = $props(); diff --git a/src/mobile/screens/Transfers.svelte b/src/mobile/screens/Transfers.svelte index d4a8119..b036a9b 100644 --- a/src/mobile/screens/Transfers.svelte +++ b/src/mobile/screens/Transfers.svelte @@ -4,7 +4,7 @@ cancels it, finished rows clear together. The desktop's hub list. */ import Icon from "../../components/Icon.svelte"; import Identicon from "../../components/Identicon.svelte"; - import { transfers, DONE, list, active, clearFinished, cancel, cancelAll, stateLine } from "../transfers.svelte.js"; + import { DONE, list, active, clearFinished, cancel, cancelAll, stateLine } from "../transfers.svelte.js"; import { fmtBytes } from "../util.js"; const rows = $derived(list()); diff --git a/src/mobile/shell.css b/src/mobile/shell.css index bdf9efc..3f88940 100644 --- a/src/mobile/shell.css +++ b/src/mobile/shell.css @@ -164,7 +164,6 @@ html[data-material="solid"] .gbar { background: rgba(24, 24, 24, 0.94); backdrop .iconbtn.sm { width: 38px; height: 38px; } .iconbtn.muted { color: var(--text-secondary); } .iconbtn.on { color: var(--accent-hover); } -.iconbtn.liked { color: var(--accent); } /* ── large page header (the un-scrolled state) ────────────────────────── */ .phead { padding: 14px var(--pad) 6px; position: relative; z-index: 2; } @@ -440,7 +439,6 @@ html[data-material="solid"] .cover-wrap { background: var(--bg-elevated); } /* ── cards / groups ───────────────────────────────────────────────────── */ .card { background: var(--surface); box-shadow: var(--ring); border-radius: var(--r-card); overflow: hidden; } -.cardpad { padding: 14px; } .group { margin: 0 var(--pad) 14px; border-radius: var(--r-card); overflow: hidden; background: var(--surface); box-shadow: var(--ring); } html[data-material="solid"] .card, html[data-material="solid"] .group { background: var(--bg-elevated); box-shadow: none; } .group .row { padding: 11px 14px; } @@ -1218,6 +1216,11 @@ html[data-material="solid"] .peer { background: var(--bg-elevated); box-shadow: .countline { display: flex; align-items: center; justify-content: space-between; padding: 0 var(--pad) 10px; } /* a hero bleeds up under the status bar; the scrim keeps the chrome legible */ .screen.has-hero { padding-top: 0; } +/* not a stray: `.lane` is width: 100%, and the mini bar's lane is ALSO + positioned with both left: 16px and right: 16px — an over-constrained + absolute box drops `right`, so without this it comes out 32px wider than + the strip it was cut for (395 where 363 was asked for, at 411), the bars + sit off-centre and the tail runs under the bar's own overflow: hidden. */ .mini .mini-lane { width: auto; } /* ── screen transitions ─────────────────────────────────────────────────── diff --git a/src/mobile/slsk.svelte.js b/src/mobile/slsk.svelte.js index 2e40d2a..dd20ec5 100644 --- a/src/mobile/slsk.svelte.js +++ b/src/mobile/slsk.svelte.js @@ -81,12 +81,6 @@ export async function disconnect() { slsk.joined = {}; } -export async function forgetAccount() { - try { await api.forgetCredentials(); } catch { /* fine */ } - slsk.savedUser = ""; - slsk.hasPassword = false; -} - let roomsTimer = null; export function refreshRooms() { slsk.roomsLate = false; diff --git a/src/mobile/util.js b/src/mobile/util.js index c5c103f..8e4d55b 100644 --- a/src/mobile/util.js +++ b/src/mobile/util.js @@ -15,6 +15,17 @@ export function longDur(secs) { return m + " min"; } +/** "4:31", "1:02:07" — a length or a position in MILLISECONDS, which is how + * a playlist row's cue and an import draft's track both carry one. Hours + * only when there are hours; `mmss` above is the seconds-in version. */ +export function stamp(ms) { + const s = Math.floor((ms || 0) / 1000); + const h = Math.floor(s / 3600); + const m = Math.floor((s % 3600) / 60); + const sec = s % 60; + return (h ? h + ":" + String(m).padStart(2, "0") : String(m)) + ":" + String(sec).padStart(2, "0"); +} + /** A relative day, against the wall clock. `ts` is unix seconds. */ export function ago(ts) { const d = Math.floor((Date.now() / 1000 - ts) / 86400); @@ -66,6 +77,10 @@ export function fmtBytes(n) { return Math.round(n / 1024) + " kB"; } +/** "1.2M", "430k" — a listener or play count beside a row, the desktop's + * short form. Nothing under a thousand is abbreviated. */ +export const fmtCount = (n) => (!n ? "" : n >= 1e6 ? (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M" : n >= 1e3 ? Math.round(n / 1e3) + "k" : String(n)); + export function basename(p) { if (!p) return ""; const i = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\")); @@ -93,9 +108,6 @@ export function fmtOf(t) { if (t.bitrate_kbps) return name + " " + t.bitrate_kbps; return name; } -export function isLossless(fmt) { - return /^(FLAC|WAV|AIFF?|APE|WV|ALAC|DSF|DFF)\b/.test(fmt || ""); -} /** The format most of an album's tracks share. */ export function albumFmt(tracks) { const count = new Map();