android: a playlist, out of the phone
The desktop has had Export since playlists v2 and the phone had no door onto it for one honest reason: `tauri-plugin-dialog` does open Android's create-document sheet, but it answers with a `content://` URI and `playlist_export` wrote with `std::fs::write`, which cannot take one. The storage plugin grew a `create_document` whose Kotlin writes the bytes through the ContentResolver — a plugin function called from Rust, not a command, so the file never passes through the WebView and nothing was added to the ACL — and `playlist_export` now builds the file above the platform fork with the same `playlist_formats` writers, so the phone rebuilds no format and the two exports cannot drift. The door is the playlist's own sheet, on the page and on the shelf card, with the desktop's five profiles in the desktop's order; a dismissed sheet says nothing, because nothing happened. The mock walk drives it three ways — a file written where the sheet pointed, a sheet dismissed, a refusal read back as a sentence — and the phone walk opens the real sheet and dismisses it with BACK, writing nothing to the phone. The Kotlin is unbuilt: nothing in this tree compiles it, though it is written against tauri-android's own `PluginHandle`, `Invoke` and `JSObject` rather than against a guess at them.
This commit is contained in:
1 parent
95c2bfeeba
commit
04c87e0db1
9 files changed
+390
-72
No files matched your search
@@ -13,7 +13,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, artistKey, picture, heroFromPhoto, heroClear } from "./catalog.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 { pls, bulk, bulkable, runBulk, coverFor, queueable, entryOf, playFrom, shufflePlay, togglePin, renamePlaylist, describePlaylist, duplicatePlaylist, deletePlaylist, removeItems, addEntries, createPlaylist, moveItem, coverFromPhoto, coverClear, EXPORTS, exportPlaylist } 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, tracksOf, loadLibrary, refreshPlaylists } from "./lib.svelte.js";
|
||||
@@ -475,6 +475,8 @@
|
||||
{@render row("download", "Download all tracks", bulkable(rows) + " not in the library yet, by your download rules", act(() => { void runBulk(p, rows, "download"); }))}
|
||||
{/if}
|
||||
<div class="sheet-hair"></div>
|
||||
{@render row("export", "Export…", p.track_count + " track" + (p.track_count === 1 ? "" : "s") + ", as a playlist file", () => openHalf("plexport", { playlist: p }))}
|
||||
<div class="sheet-hair"></div>
|
||||
{@render row("trash", "Delete playlist", "", () => confirmDelete(p))}
|
||||
</div>
|
||||
{:else if kind === "pltrack" && shown?.arg?.item}
|
||||
@@ -812,6 +814,15 @@
|
||||
<div class="setup-foot"><button class="btn btn-fill wide" onclick={() => void finishSetup()}>Start listening</button></div>
|
||||
<div class="sec-note" style="padding-bottom:16px">Nothing above is required. You → Set up this phone again opens it back up.</div>
|
||||
</div>
|
||||
{:else if kind === "plexport" && shown?.arg?.playlist}
|
||||
{@const p = shown.arg.playlist}
|
||||
<div class="sheet-title">Export <span class="t-sub">· {p.name}</span></div>
|
||||
<div class="sheet-rows">
|
||||
{#each EXPORTS as [format, label, m] (format)}
|
||||
{@render row("export", label, m, act(() => exportPlaylist(p, format).catch(oops)))}
|
||||
{/each}
|
||||
</div>
|
||||
<div class="sec-note" style="padding-bottom:16px">The file goes wherever the sheet points it — Downloads, Drive, anywhere the phone offers. A row with no file of its own keeps its name: M3U8 leaves a comment where the location would be, XSPF, CSV and the backup carry the row without one, and PLS holds only real files — and the locations that are there are paths on this phone.</div>
|
||||
{:else if kind === "sleep"}
|
||||
<div class="sheet-title">Sleep timer</div>
|
||||
<div class="sheet-sec">Pause after</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { resolveSources, songCandidates, simplifyTitle, cachedStreamSourceFor, n
|
||||
import { slsk } from "./slsk.svelte.js";
|
||||
import { downloadsAllowed } from "./network.svelte.js";
|
||||
import { note } from "./notice.svelte.js";
|
||||
import { basename } from "./util.js";
|
||||
|
||||
export const pls = $state({
|
||||
/** "id:revision" → data URL, or null once the backend said there is none */
|
||||
@@ -201,6 +202,29 @@ export async function coverClear(p) {
|
||||
note(`Back to the mosaic for ${p.name}`);
|
||||
}
|
||||
|
||||
/* ── export: the desktop's five profiles, in the desktop's order and its
|
||||
own words. The file itself is built in Rust by the writers the desktop
|
||||
uses (playlist_formats), so nothing here knows what an M3U8 looks like —
|
||||
two exports that drift are worse than one that is missing. What comes
|
||||
back is only where it went: a path on the desktop, the document's name
|
||||
on the phone, and null when the sheet was dismissed. ── */
|
||||
export const EXPORTS = [
|
||||
["m3u8", "M3U8", "one line a track — most players read it"],
|
||||
["xspf", "XSPF", "an open format, titles and times carried"],
|
||||
["pls", "PLS", "older players want this one"],
|
||||
["csv", "CSV", "a row a track, for a spreadsheet"],
|
||||
["json", "Backup (JSON)", "everything about it, this app's own"],
|
||||
];
|
||||
/** Export `p` as `format` — the system's own sheet says where it goes.
|
||||
* False, and nothing said, when that sheet was dismissed: nothing
|
||||
* happened, so there is nothing to report. */
|
||||
export async function exportPlaylist(p, format) {
|
||||
const where = await api.playlistExport(p.id, format);
|
||||
if (!where) return false;
|
||||
note(`Exported as ${basename(where)}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ── 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