Een dating-app op basis van brieven, niet van gezichten!
Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.
import { Capacitor } from "@capacitor/core";
import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
import { Share } from "@capacitor/share";
import { log } from "./log";
/*
* De gegevensexport als bestand (AVG art. 15 en 20: inzage én overdraagbaarheid). Er is geen
* webversie van de app, dus "download hem maar in de browser" is geen antwoord — de knop moet het
* op de telefoon doen.
*
* Op Android/iOS kan dat niet met een blob-download: de webview van Capacitor kent geen
* downloadmanager, een <a download> doet daar eenvoudigweg niets. Wat er wél is, is de weg die het
* besturingssysteem zelf voor bestanden heeft: schrijf hem weg en geef hem aan het deelblad, waar
* "Opslaan in Bestanden", Drive of een mailtje aan jezelf tussen staat. Dat is dezelfde handeling
* als downloaden, alleen kiest de gebruiker de bestemming.
*
* Het bestand gaat naar de cachemap van de app: privéopslag waar geen andere app bij kan, en die
* het systeem zelf mag opruimen. Bij een volgende export ruimen we onze eigen oude bestanden op —
* een complete kopie van je brieven hoort niet te blijven rondslingeren omdat je één keer op de
* knop drukte. Meteen ná het delen wissen kan niet: de app die het bestand aanneemt leest het dan
* nog.
*
* In de browser (`npm run dev`) blijft het een gewone blob-download.
*/
const PREFIX = "liefdesbrieven-";
const naamVan = (exportedAt) => `${PREFIX}${new Date(exportedAt).toISOString().slice(0, 10)}.json`;
/* Eerdere exports uit de cachemap. Mislukt dit, dan is dat geen reden om het opslaan af te breken */
async function ruimOp() {
try {
const { files } = await Filesystem.readdir({ path: "", directory: Directory.Cache });
await Promise.all(
files
.filter((f) => (f.name ?? f).startsWith(PREFIX))
.map((f) =>
Filesystem.deleteFile({ path: f.name ?? f, directory: Directory.Cache }).catch((e) =>
log("✗ export opruimen", f.name ?? f, e?.message ?? String(e))
)
)
);
} catch (e) {
/* geen cachemap, of niet te lezen: dan valt er ook niets op te ruimen */
log("✗ export opruimen", e?.message ?? String(e));
}
}
/*
* Bewaar de export als bestand. Geeft { name, bytes, saved } terug, of { canceled: true } als de
* gebruiker het deelblad wegtikt. Gooit als het schrijven zelf mislukt (volle schijf, geen ruimte).
*
* `bytes` is de lengte van de JSON in tekens. Dat is niet exact het aantal bytes, maar de export
* bestaat vrijwel volledig uit ASCII (de foto's zitten er als base64 in), dus als maat voor "hoe
* groot is dit bestand" klopt het tot op een haar — en het scheelt een tweede kopie in het geheugen.
*/
export async function saveExport(data) {
const json = JSON.stringify(data, null, 2);
const name = naamVan(data.exportedAt);
const bytes = json.length;
if (!Capacitor.isNativePlatform()) {
const url = URL.createObjectURL(new Blob([json], { type: "application/json" }));
const a = Object.assign(document.createElement("a"), { href: url, download: name });
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
return { name, bytes, saved: true };
}
await ruimOp();
const { uri } = await Filesystem.writeFile({ path: name, directory: Directory.Cache, data: json, encoding: Encoding.UTF8 });
try {
await Share.share({ title: "Mijn gegevens (Liefdesbrieven)", files: [uri] });
} catch (e) {
/* Wegtikken is geen fout — de plugin meldt het wel als een afwijzing */
if (/cancel/i.test(e?.message ?? "")) return { name, bytes, canceled: true };
throw e;
}
return { name, bytes, saved: true };
}
/* 'MB' zodra het er een is; daaronder kB. Nederlandse komma, want dit staat in de app */
export const bestandsgrootte = (bytes) =>
bytes >= 1024 * 1024 ? `${(bytes / 1024 / 1024).toFixed(1).replace(".", ",")} MB` : `${Math.max(1, Math.round(bytes / 1024))} kB`;