diff --git a/tools/pak-noter-i-portioner.py b/tools/pak-noter-i-portioner.py new file mode 100755 index 0000000..67ef828 --- /dev/null +++ b/tools/pak-noter-i-portioner.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Pakker en Apple Noter-eksport i portioner, der kan importeres. + +Eksporten af tolv hundrede noter fylder knap to hundrede megabyte, og næsten +alt sammen er billeder: teksten selv er under fire. Importen tager 64 MB ad +gangen, og tunnelen foran den tager mindre — så det skal deles. + +Delt efter hvad en note *vejer med sine billeder*, ikke efter antal. De fleste +noter har ingen, så de fylder én portion til sammen; de få med et fotoalbum i +får hver deres. En note og dens billeder ligger altid i samme portion, for +linkene i teksten er relative, og en note, hvis billeder kom med i næste +portion, ville pege på ingenting. + + ./pak-noter-i-portioner.py ~/Desktop/apple-noter [MB] +""" + +import os +import re +import sys +import zipfile + +# Under importens 64 MB og under det, en tunnel plejer at tage. Hellere en +# portion mere end en, der ryger på målstregen. +DEFAULT_LIMIT_MB = 40 + +LINK = re.compile(r"!\[[^\]]*\]\((vedhaeftninger/[^)]+)\)") + + +def main() -> int: + src = os.path.expanduser(sys.argv[1] if len(sys.argv) > 1 else "~/Desktop/apple-noter") + limit = int(float(sys.argv[2] if len(sys.argv) > 2 else DEFAULT_LIMIT_MB) * 1024 * 1024) + out_dir = src.rstrip("/") + "-portioner" + os.makedirs(out_dir, exist_ok=True) + + notes = sorted(f for f in os.listdir(src) if f.endswith(".md")) + if not notes: + print(f"Ingen .md-filer i {src}") + return 1 + + # Hvad hver note vejer, og hvad den trækker med sig. + weighed = [] + for name in notes: + path = os.path.join(src, name) + text = open(path, encoding="utf-8", errors="replace").read() + files = [] + size = os.path.getsize(path) + for rel in LINK.findall(text): + f = os.path.join(src, rel) + if os.path.exists(f): + files.append(rel) + size += os.path.getsize(f) + weighed.append((name, files, size)) + + # En note, der alene er større end grænsen, får sin egen portion frem for at + # blive sprunget over: den skal med, og en portion på 25 MB er stadig en, der + # kan sendes. + batches, current, current_size = [], [], 0 + for item in weighed: + if current and current_size + item[2] > limit: + batches.append(current) + current, current_size = [], 0 + current.append(item) + current_size += item[2] + if current: + batches.append(current) + + print(f"{len(notes)} noter → {len(batches)} portioner\n") + for i, batch in enumerate(batches, start=1): + zip_path = os.path.join(out_dir, f"noter-{i:02d}.zip") + seen = set() + with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as z: + for name, files, _ in batch: + z.write(os.path.join(src, name), name) + for rel in files: + if rel in seen: + continue + seen.add(rel) + z.write(os.path.join(src, rel), rel) + mb = os.path.getsize(zip_path) / 1024 / 1024 + print(f" {os.path.basename(zip_path)} {len(batch):4d} noter {len(seen):3d} bilag {mb:6.1f} MB") + + print(f"\nLigger i:\n {out_dir}") + print("\nImportér én ad gangen under Indstillinger → Data → Noter.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/web/src/lib/components/NoteEditor.svelte b/web/src/lib/components/NoteEditor.svelte index f509b09..0e94666 100644 --- a/web/src/lib/components/NoteEditor.svelte +++ b/web/src/lib/components/NoteEditor.svelte @@ -191,6 +191,9 @@ let suggestions = $state([]); let chosen = $state(0); + // Hvor forslagslisten skal stå, og wrappen den måles imod. + let menuAt = $state(null); + let wrap; /** * The word being typed after a #, if that is what is happening. @@ -210,6 +213,33 @@ return { node, start: selection.anchorOffset - match[1].length, term: match[1] }; } + /** + * Hvor `#` står, målt i wrappens koordinater. + * + * Målt på selve tegnet og ikke på markøren: et sammenfaldet område har ingen + * kasse på en tom linje, mens `#term` altid har en, fordi der står noget i den. + * + * Klemt inden for ruden, så en note skrevet helt ude i højre side ikke sender + * menuen ud over kanten, og vendt op over linjen, når der ikke er plads under. + */ + function tagPosition(partial) { + if (!wrap) return null; + const range = document.createRange(); + range.setStart(partial.node, Math.max(0, partial.start - 1)); + range.setEnd(partial.node, partial.start + partial.term.length); + const at = range.getBoundingClientRect(); + const box = wrap.getBoundingClientRect(); + if (!at.width && !at.height) return null; + + const MENU = { w: 210, h: 240 }; + const below = at.bottom - box.top + 6; + const flip = at.bottom + MENU.h > box.bottom && at.top - box.top > MENU.h; + return { + top: flip ? at.top - box.top - MENU.h - 6 : below, + left: Math.max(0, Math.min(at.left - box.left, box.width - MENU.w)) + }; + } + function readSuggestions() { const partial = partialTag(); if (!partial) { @@ -218,6 +248,12 @@ } const term = partial.term.toLowerCase(); + // Målt når menuen åbner, ikke ved hvert tastetryk. Den skal stå ved det `#`, + // man skriver — den stod før øverst i ruden, hvilket i en lang note er et + // helt andet sted end det, øjet er — men den må heller ikke rykke sig et par + // pixels for hvert bogstav, mens listen snævres ind. + if (!suggestions.length) menuAt = tagPosition(partial); + // Alle der passer, ikke de seks første. // // Loftet på seks var sat, dengang listen ikke kunne rulle, og det gjorde @@ -371,7 +407,7 @@ } -
+
@@ -475,10 +511,15 @@ >
{#if suggestions.length} - -