Android: write and read picked files through content URIs

On Android the file dialogs return content:// URIs, not paths. Exports
wrote them with std::fs, which failed after the system had already
created the document, leaving a 0 B file; deck, library-card and library
backup exports now write through tauri-plugin-fs, and a failed deck
export shows an error instead of failing silently.

Library-file and card-database imports read the pick through the fs
plugin too. A library file is told apart by its SQLite header rather
than its extension, and SQLite opens a local copy. The library-import
picker drops its filters on Android, where they hid .db and backup files.

Deck import suggests the file's display name rather than the URI's
opaque id, and the backup's "Saved to" message shows the file name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 22:48:36 -05:00
parent eeb426ab8f
commit ef4a0d740d
9 changed files with 173 additions and 36 deletions

View File

@@ -101,6 +101,7 @@ type DeckDialog =
| { kind: "duplicate"; deck: Deck }
| { kind: "import-name"; suggestedName: string; path: string }
| { kind: "import-result"; result: DeckImportResult }
| { kind: "export-failed"; message: string }
| null;
/** Name a new deck and, optionally, pick its format up front. */
@@ -1775,7 +1776,12 @@ export function DeckView({
defaultPath: `${d.name}.txt`,
filters: [{ name: "Decklist", extensions: ["txt"] }],
});
if (typeof path === "string") await api.exportDeck(d.id, path);
if (typeof path !== "string") return;
try {
await api.exportDeck(d.id, path);
} catch (err) {
setDialog({ kind: "export-failed", message: String(err) });
}
}
async function pickImportFile() {
@@ -1784,7 +1790,8 @@ export function DeckView({
filters: [{ name: "Decklist", extensions: ["txt"] }],
});
if (typeof path !== "string") return;
const base = path.split(/[/\\]/).pop() ?? "Imported deck";
// On Android the path is a content:// URI, so ask for the file's name.
const base = (await api.pickedFileName(path).catch(() => null)) || "Imported deck";
const suggestedName = base.replace(/\.txt$/i, "");
showDialog({ kind: "import-name", suggestedName, path });
}
@@ -1935,6 +1942,13 @@ export function DeckView({
onClose={() => setDialog(null)}
/>
)}
{dialog?.kind === "export-failed" && (
<InfoDialog
title="Couldn't export deck"
message={dialog.message}
onClose={() => setDialog(null)}
/>
)}
</>
);
}