android: a playlist's cover from a photo on the phone
The storage plugin gains pick_image — the document picker limited to images, the picked content copied into the app's cache so the Rust side gets a plain path. playlist_cover_set(id, path) is the desktop pick's install step split out and shared (squared, the master cover, the row's revision moved). A playlist's sheet offers Choose cover photo and Remove custom cover; the mock walk exercises both.
This commit is contained in:
1 parent
d95c3f48d1
commit
b50577fc81
15 files changed
+188
-12
No files matched your search
@@ -274,6 +274,7 @@ export const playlistCover = (id, maxEdge = null) =>
|
||||
invoke("playlist_cover", { id, maxEdge });
|
||||
export const playlistCoverPick = (id) => invoke("playlist_cover_pick", { id });
|
||||
export const playlistCoverClear = (id) => invoke("playlist_cover_clear", { id });
|
||||
export const playlistCoverSet = (id, path) => invoke("playlist_cover_set", { id, path });
|
||||
// Import: probe opens a file dialog and stages a reviewed draft; text stages
|
||||
// a pasted tracklist; commit writes the accepted draft in one transaction.
|
||||
export const playlistImportProbe = () => invoke("playlist_import_probe");
|
||||
@@ -500,6 +501,8 @@ export const requestAllFilesAccess = () => invoke("plugin:storage|request_all_fi
|
||||
// The phone's share plugin (crates/tauri-plugin-share): what another app
|
||||
// shared to SoulStream, { text } — read once.
|
||||
export const pendingShare = () => invoke("plugin:share|pending_share");
|
||||
// A photo from the phone (the storage plugin): { path } in the app's cache.
|
||||
export const pickImage = () => invoke("plugin:storage|pick_image");
|
||||
// Download a found album copy into (Cache)\_audition\<rel>\ for the version
|
||||
// comparison. Called track 1 first, then the rest — `startIndex` keeps the
|
||||
// second batch's indices continuing, since index 0 means "track 1".
|
||||
|
||||
@@ -93,7 +93,7 @@ let plSeq = 3;
|
||||
let plItemSeq = 100;
|
||||
const PL_T0 = 1_725_000_000;
|
||||
const PL = [
|
||||
{ id: 1, name: "Late night drive", track_count: 0, description: "Slow, wide, mostly synths.", kind: "manual", pinned: true, cover_path: null, created_at: PL_T0, updated_at: PL_T0, revision: 1, duration_secs: 0, unresolved_count: 0 },
|
||||
{ id: 1, name: "Late night drive", track_count: 0, description: "Slow, wide, mostly synths.", kind: "manual", pinned: true, cover_path: "mock://cover1", created_at: PL_T0, updated_at: PL_T0, revision: 1, duration_secs: 0, unresolved_count: 0 },
|
||||
{ id: 2, name: "Boiler Room 2019", track_count: 0, description: null, kind: "setlist", pinned: false, cover_path: null, created_at: PL_T0 + 10, updated_at: PL_T0 + 10, revision: 1, duration_secs: 0, unresolved_count: 0 },
|
||||
{ id: 3, name: "Empty one", track_count: 0, description: null, kind: "manual", pinned: false, cover_path: null, created_at: PL_T0 + 20, updated_at: PL_T0 + 20, revision: 1, duration_secs: 0, unresolved_count: 0 },
|
||||
];
|
||||
@@ -135,7 +135,9 @@ function mockLyrics(title) {
|
||||
function mockPlaylists(cmd, args) {
|
||||
if (cmd === "list_playlists") return Promise.resolve(PL.map((p) => ({ ...p })));
|
||||
if (cmd === "playlist_items") return new Promise((r) => setTimeout(() => r((PL_ITEMS[Number(args?.id)] || []).map((x) => ({ ...x }))), 80));
|
||||
if (cmd === "playlist_cover") { const p = plOf(args); return Promise.resolve(p && (p.id === 1 || p.cover_path) ? svgArt(p.id === 1 ? 330 : 280, p.id === 1 ? "L" : "M") : null); }
|
||||
if (cmd === "playlist_cover") { const p = plOf(args); return Promise.resolve(p && p.cover_path ? svgArt(p.id === 1 ? 330 : 280, p.id === 1 ? "L" : "M") : null); }
|
||||
if (cmd === "playlist_cover_set") { const p = plOf(args); if (!p) return Promise.reject(new Error("no such playlist")); p.cover_path = String(args?.path || "mock://photo"); plTouch(p); return Promise.resolve(svgArt(300, "P")); }
|
||||
if (cmd === "playlist_cover_clear") { const p = plOf(args); if (p) p.cover_path = null; plTouch(p); return Promise.resolve(); }
|
||||
if (cmd === "create_playlist") {
|
||||
const p = { id: ++plSeq, name: args?.name || "New Playlist", track_count: 0, description: null, kind: args?.kind || "manual", pinned: false, cover_path: null, created_at: Math.floor(Date.now() / 1000), updated_at: 0, revision: 0, duration_secs: 0, unresolved_count: 0 };
|
||||
PL.push(p);
|
||||
@@ -516,6 +518,7 @@ function mockCatalog(cmd, args) {
|
||||
if (cmd === "plugin:storage|pick_folder") return Promise.resolve({ uri: "content://mock/tree/primary%3AMusic", path: "C:\\mock\\Music" });
|
||||
if (cmd === "plugin:storage|all_files_access") return Promise.resolve({ granted: true });
|
||||
if (cmd === "plugin:storage|request_all_files_access") return Promise.resolve();
|
||||
if (cmd === "plugin:storage|pick_image") return Promise.resolve({ path: "C:\\mock\\photo.jpg" });
|
||||
if (cmd === "plugin:share|pending_share") {
|
||||
/* the harness shares by setting window.__ssMock.share and waking the page */
|
||||
const t = (typeof window !== "undefined" && window.__ssMock?.share) || null;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
import { player, playNext, enqueue, playEntries, playQueueAt, removeFromQueue, moveToNext, moveInQueue, setSleep, sleepLabel } from "./player.svelte.js";
|
||||
import { streamSong, saveSong, streamCatalogAlbum, saveCatalogAlbum } from "./resolve.svelte.js";
|
||||
import { catalog, yearOf } from "./catalog.svelte.js";
|
||||
import { pls, bulk, bulkable, runBulk, coverFor, queueable, entryOf, playFrom, shufflePlay, togglePin, renamePlaylist, describePlaylist, duplicatePlaylist, deletePlaylist, removeItems, addEntries, createPlaylist, moveItem } from "./playlists.svelte.js";
|
||||
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 } 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";
|
||||
@@ -374,6 +374,11 @@
|
||||
{@render row("file-text", p.description ? "Edit description" : "Add a description", p.description || "", () => describePrompt(p))}
|
||||
{/if}
|
||||
{@render row("layers", "Duplicate", "a copy, “" + p.name + " copy”", act(() => duplicatePlaylist(p).then((id) => push("playlist", { id })).catch(oops)))}
|
||||
<div class="sheet-hair"></div>
|
||||
{@render row("image", "Choose cover photo…", "a picture from this phone", act(() => coverFromPhoto(p).catch(oops)))}
|
||||
{#if p.cover_path}
|
||||
{@render row("x", "Remove custom cover", "the mosaic comes back", act(() => coverClear(p).catch(oops)))}
|
||||
{/if}
|
||||
{#if kind === "playlist-more" && unres.length}
|
||||
{@render row("x", "Remove unresolved entries", unres.length + " that never matched a track", act(() => removeItems(p, rows, unres).catch(oops)))}
|
||||
{/if}
|
||||
|
||||
@@ -184,6 +184,22 @@ export async function deletePlaylist(p) {
|
||||
await refreshPlaylists();
|
||||
}
|
||||
|
||||
/* ── the cover: a photo from the phone, or the mosaic again ── */
|
||||
export async function coverFromPhoto(p) {
|
||||
let r;
|
||||
try { r = await api.pickImage(); } catch (e) { if (/cancelled/.test(String(e))) return false; throw e; }
|
||||
if (!r?.path) return false;
|
||||
await api.playlistCoverSet(p.id, r.path);
|
||||
await refreshPlaylists();
|
||||
note(`Cover set for ${p.name}`);
|
||||
return true;
|
||||
}
|
||||
export async function coverClear(p) {
|
||||
await api.playlistCoverClear(p.id);
|
||||
await refreshPlaylists();
|
||||
note(`Back to the mosaic for ${p.name}`);
|
||||
}
|
||||
|
||||
/* ── reorder: a row up, down or to the top; the complete id order goes
|
||||
in one call, as the desktop's drop does ── */
|
||||
export async function moveItem(p, items, item, where) {
|
||||
|
||||
Reference in new issue
Block a user