feat(todo,app): files attached to tasks and comments

Halkyon's ADR 0043, D1, D3, D5 and D8: any file up to 100 MB, attached to
a task's description or to a comment, sealed as everything of Todo's.

Server: the attachment table, synced and sealed as the others, with the
sealed file's hash readable for the store; todoFiles hands halkyon_base
its rows, readable by their space's members, and takes sealed files
alone; the files no attachment names go daily. Its tests hold downloads
to the row's space, refuse a readable file and prune what no live row
names. Migration attachments: CREATE TABLE "attachment" with its indexes
and the foreign key to its space, cascading as every synced table's.

App: AttachmentRepository seals each file for its row, keeps it sealed
and opened in the person's folder, uploads it after each sync and fetches
it as it shows, both held back by Without files on mobile data but for a
file tapped. Under the description and a comment's text, a row of them:
pictures as thumbnails opening full-size, other files as chips opening in
the system's app, else saved; the paperclip beside the description and in the comment's
composer adds them, the composer holding them until it is sent. A task deleted takes its files
and its comments' along, a comment its own.

halkyon_app opens a file in the system's app (FileOpener): on Android
through its plugin and a FileProvider of its own class, sharing the
cache's halkyon_opened/ folder alone; on the desktop as a file: URI.

Whats-New: Attach files to a task's description or a comment with the paperclip: pictures show as thumbnails, other files open in their app.
Whats-New-De: Hänge mit der Büroklammer Dateien an die Beschreibung einer Aufgabe oder an einen Kommentar: Bilder erscheinen als Vorschau, andere Dateien öffnen sich in ihrer App.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-11 01:17:22 +01:00
commit 3ca4b4308d
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
5 changed files with 23300 additions and 62 deletions

View file

@ -439,5 +439,17 @@
"weatherPlaceHelp": "Die Tage zeigen die Vorhersage für diesen Ort, und Aufgaben, die vom Wetter abhängen, richten sich nach ihr. Der Standort deines Geräts wird dafür nie verwendet.",
"weatherPlaceRemove": "Ort entfernen",
"weatherOff": "Diese Instanz liefert keine Vorhersagen.",
"weatherBy": "Wetterdaten von Open-Meteo.com"
"weatherBy": "Wetterdaten von Open-Meteo.com",
"attachmentAdd": "Dateien anhängen",
"attachmentAnyFile": "Alle Dateien",
"attachmentTooLarge": "{name} ist größer als 100 MB.",
"attachmentNotAdded": "{name} wurde nicht angehängt.",
"attachmentDownload": "Herunterladen",
"attachmentUnavailable": "Die Datei lässt sich gerade nicht laden. Bist du online?",
"attachmentSave": "Kopie speichern",
"attachmentRemove": "Entfernen",
"attachmentRemoveTitle": "Diese Datei entfernen?",
"attachmentMenu": "Dateioptionen",
"attachmentSizeKb": "{size} KB",
"attachmentSizeMb": "{size} MB"
}

View file

@ -1440,5 +1440,29 @@
"weatherBy": "Weather data by Open-Meteo.com",
"@weatherBy": {
"description": "The credit Open-Meteo's licence asks for next to its data (D40)."
}
},
"attachmentAdd": "Attach files",
"@attachmentAdd": {"description": "The paperclip button adding files to a task's description or a comment."},
"attachmentAnyFile": "Any file",
"@attachmentAnyFile": {"description": "The file type offered in the dialog picking files to attach."},
"attachmentTooLarge": "{name} is larger than 100 MB.",
"@attachmentTooLarge": {"description": "Said when a file picked to attach is too large.", "placeholders": {"name": {"type": "String"}}},
"attachmentNotAdded": "{name} wasn't attached.",
"@attachmentNotAdded": {"description": "Said when a file couldn't be attached (no keys on this device, or it failed).", "placeholders": {"name": {"type": "String"}}},
"attachmentDownload": "Download",
"@attachmentDownload": {"description": "The tooltip of an attachment not on this device yet, fetched when tapped."},
"attachmentUnavailable": "This file can't be fetched right now. Are you online?",
"@attachmentUnavailable": {"description": "Said when an attachment couldn't be downloaded."},
"attachmentSave": "Save a copy",
"@attachmentSave": {"description": "The menu entry saving an attachment where the person chooses."},
"attachmentRemove": "Remove",
"@attachmentRemove": {"description": "The menu entry removing an attachment, and the button removing a file picked for a comment before it is sent."},
"attachmentRemoveTitle": "Remove this file?",
"@attachmentRemoveTitle": {"description": "The title of the question before an attachment is removed."},
"attachmentMenu": "File options",
"@attachmentMenu": {"description": "The tooltip of an attachment's menu."},
"attachmentSizeKb": "{size} KB",
"@attachmentSizeKb": {"description": "An attachment's size in kilobytes.", "placeholders": {"size": {"type": "int"}}},
"attachmentSizeMb": "{size} MB",
"@attachmentSizeMb": {"description": "An attachment's size in megabytes, with one decimal.", "placeholders": {"size": {"type": "String"}}}
}

View file

@ -2739,6 +2739,78 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Weather data by Open-Meteo.com'**
String get weatherBy;
/// The paperclip button adding files to a task's description or a comment.
///
/// In en, this message translates to:
/// **'Attach files'**
String get attachmentAdd;
/// The file type offered in the dialog picking files to attach.
///
/// In en, this message translates to:
/// **'Any file'**
String get attachmentAnyFile;
/// Said when a file picked to attach is too large.
///
/// In en, this message translates to:
/// **'{name} is larger than 100 MB.'**
String attachmentTooLarge(String name);
/// Said when a file couldn't be attached (no keys on this device, or it failed).
///
/// In en, this message translates to:
/// **'{name} wasn\'t attached.'**
String attachmentNotAdded(String name);
/// The tooltip of an attachment not on this device yet, fetched when tapped.
///
/// In en, this message translates to:
/// **'Download'**
String get attachmentDownload;
/// Said when an attachment couldn't be downloaded.
///
/// In en, this message translates to:
/// **'This file can\'t be fetched right now. Are you online?'**
String get attachmentUnavailable;
/// The menu entry saving an attachment where the person chooses.
///
/// In en, this message translates to:
/// **'Save a copy'**
String get attachmentSave;
/// The menu entry removing an attachment, and the button removing a file picked for a comment before it is sent.
///
/// In en, this message translates to:
/// **'Remove'**
String get attachmentRemove;
/// The title of the question before an attachment is removed.
///
/// In en, this message translates to:
/// **'Remove this file?'**
String get attachmentRemoveTitle;
/// The tooltip of an attachment's menu.
///
/// In en, this message translates to:
/// **'File options'**
String get attachmentMenu;
/// An attachment's size in kilobytes.
///
/// In en, this message translates to:
/// **'{size} KB'**
String attachmentSizeKb(int size);
/// An attachment's size in megabytes, with one decimal.
///
/// In en, this message translates to:
/// **'{size} MB'**
String attachmentSizeMb(String size);
}
class _AppLocalizationsDelegate

View file

@ -1739,4 +1739,49 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get weatherBy => 'Wetterdaten von Open-Meteo.com';
@override
String get attachmentAdd => 'Dateien anhängen';
@override
String get attachmentAnyFile => 'Alle Dateien';
@override
String attachmentTooLarge(String name) {
return '$name ist größer als 100 MB.';
}
@override
String attachmentNotAdded(String name) {
return '$name wurde nicht angehängt.';
}
@override
String get attachmentDownload => 'Herunterladen';
@override
String get attachmentUnavailable =>
'Die Datei lässt sich gerade nicht laden. Bist du online?';
@override
String get attachmentSave => 'Kopie speichern';
@override
String get attachmentRemove => 'Entfernen';
@override
String get attachmentRemoveTitle => 'Diese Datei entfernen?';
@override
String get attachmentMenu => 'Dateioptionen';
@override
String attachmentSizeKb(int size) {
return '$size KB';
}
@override
String attachmentSizeMb(String size) {
return '$size MB';
}
}

View file

@ -1735,6 +1735,51 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get weatherBy => 'Weather data by Open-Meteo.com';
@override
String get attachmentAdd => 'Attach files';
@override
String get attachmentAnyFile => 'Any file';
@override
String attachmentTooLarge(String name) {
return '$name is larger than 100 MB.';
}
@override
String attachmentNotAdded(String name) {
return '$name wasn\'t attached.';
}
@override
String get attachmentDownload => 'Download';
@override
String get attachmentUnavailable =>
'This file can\'t be fetched right now. Are you online?';
@override
String get attachmentSave => 'Save a copy';
@override
String get attachmentRemove => 'Remove';
@override
String get attachmentRemoveTitle => 'Remove this file?';
@override
String get attachmentMenu => 'File options';
@override
String attachmentSizeKb(int size) {
return '$size KB';
}
@override
String attachmentSizeMb(String size) {
return '$size MB';
}
}
/// The translations for English, as used in the United Kingdom (`en_GB`).