fix(app): a recovery phrase's and codes' files name the instance

Saved from several apps or instances, they differed by " (1)" alone. They
name the instance now, without the app's word, as an operator's apps share
one account: halkyon.eu-recovery-phrase.txt, localhost-8280-recovery-codes.txt.

Whats-New: A saved recovery phrase or recovery codes file now names its instance.
Whats-New-De: Gespeicherte Wiederherstellungsphrasen und -codes nennen jetzt ihre Instanz im Dateinamen.

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-05 15:26:43 +01:00
commit 2535da15de
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:flutter/services.dart';
import 'package:halkyon_app/l10n/halkyon_app_localizations.dart';
import 'package:halkyon_app/src/app/app_frame.dart';
import 'package:halkyon_app/src/app/app_scope.dart';
import 'package:halkyon_app/src/password/model/account_authenticator.dart';
import 'package:halkyon_app/src/password/model/phrase_check.dart';
@ -328,26 +329,46 @@ class const SheetProblem(final String text, {super.key})
}
}
/// Save for [text], kept as [fileName]. Nothing secret is copied (the
/// reading of the fixes): no app can mark a copy as sensitive through
/// How the file of a secret names the instance called [name] (its host, with
/// the port if it has one), for the app called [product]: without the app's
/// word, as an operator's apps share one account (`learn.halkyon.eu`,
/// `todo.halkyon.eu`: `halkyon.eu`), and safe in a file's name
/// (`localhost:8280`: `localhost-8280`).
String instanceFileLabel(String name, {required String product}) {
final host = name.toLowerCase();
final prefix = '${product.toLowerCase()}.';
return (host.startsWith(prefix) ? host.substring(prefix.length) : host)
.replaceAll(RegExp('[^a-z0-9.-]+'), '-');
}
/// Save for [text], in a file named after the instance and [fileKind].
/// Nothing secret is copied (the reading of the fixes): no app can mark a copy as sensitive through
/// Flutter, so clipboard histories (Windows, KDE, Gboard, Samsung's) and
/// cloud clipboards would keep it, and on Android an app in the background
/// can't empty the clipboard again.
class const KeepTextButtons({
required final String text,
required final String fileName,
/// What the file holds; its name adds the instance's, whose account it
/// belongs to whichever app showed it (`halkyon.eu-recovery-phrase.txt`).
required final String fileKind,
super.key,
}) extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = HalkyonAppLocalizations.of(context);
final dependencies = AppScope.of(context);
final instance = instanceFileLabel(
dependencies.serverName,
product: AppFrameScope.of(context).product(context),
);
return Align(
alignment: AlignmentDirectional.centerStart,
child: OutlinedButton.icon(
icon: const Icon(LucideIcons.download),
label: Text(l10n.passwordSave),
onPressed: () => AppScope.of(context).fileSaver.save(
name: fileName,
onPressed: () => dependencies.fileSaver.save(
name: '$instance-$fileKind.txt',
bytes: utf8.encode('$text\n'),
mimeType: 'text/plain',
),
@ -426,7 +447,7 @@ class const RecoveryPhraseView({required final String phrase, super.key})
// Saved or written down, never copied ([KeepTextButtons]).
KeepTextButtons(
text: phrase,
fileName: 'halkyon-recovery-phrase.txt',
fileKind: 'recovery-phrase',
),
],
),
@ -683,7 +704,7 @@ class const RecoveryCodesView({required final List<String> codes, super.key})
const SizedBox(height: 12),
KeepTextButtons(
text: codes.join('\n'),
fileName: 'halkyon-recovery-codes.txt',
fileKind: 'recovery-codes',
),
],
),