79ad411d7e
The version was a hand-written constant that had to be kept in sync with `version:` in pubspec.yaml. It was not: the constant still said 1.1.0 while the shipped build was 1.1.2+22, so every abuse report carried a version that no build ever had — and reports could not be traced back to a build. AppInfo reads versionName / CFBundleShortVersionString, which Flutter derives from the pubspec, so the two cannot drift apart again. Reports now carry the build number as well; when the version is not known yet the field is omitted rather than guessed. Also adds the timestamp regression test that covers MessagingService.wireSentAt, which shipped in 1.1.2 without its test.
691 lines
24 KiB
Dart
691 lines
24 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:file_picker/file_picker.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:share_plus/share_plus.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
import 'package:sepot/core/app_info.dart';
|
|
import 'package:sepot/core/constants/app_constants.dart';
|
|
import 'package:sepot/core/l10n/error_messages.dart';
|
|
import 'package:sepot/core/l10n/l10n.dart';
|
|
import 'package:sepot/core/storage/secure_storage.dart';
|
|
import 'package:sepot/models/identity.dart';
|
|
import 'package:sepot/core/theme/app_theme.dart';
|
|
import 'package:sepot/providers/contacts_provider.dart';
|
|
import 'package:sepot/providers/conversations_provider.dart';
|
|
import 'package:sepot/providers/identity_provider.dart';
|
|
import 'package:sepot/providers/messaging_provider.dart';
|
|
import 'package:sepot/providers/storage_provider.dart';
|
|
import 'package:sepot/providers/settings_provider.dart';
|
|
import 'package:sepot/features/settings/biometrics_tile.dart';
|
|
import 'package:sepot/features/settings/language_settings_tile.dart';
|
|
import 'package:sepot/features/settings/push_settings_tile.dart';
|
|
import 'package:sepot/features/settings/tor_settings_tile.dart';
|
|
import 'package:sepot/widgets/sepot_logo.dart';
|
|
|
|
/// Nastavení aplikace
|
|
class SettingsScreen extends ConsumerWidget {
|
|
const SettingsScreen({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final identityAsync = ref.watch(identityProvider);
|
|
final l10n = context.l10n;
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(l10n.settingsTitle),
|
|
),
|
|
body: SingleChildScrollView(
|
|
child: Column(
|
|
children: [
|
|
// Profil
|
|
identityAsync.when(
|
|
data: (identity) {
|
|
if (identity == null) return const SizedBox.shrink();
|
|
return _buildProfileCard(context, identity);
|
|
},
|
|
loading: () => const SizedBox.shrink(),
|
|
error: (_, _) => const SizedBox.shrink(),
|
|
),
|
|
const SizedBox(height: 8),
|
|
// Sekce Obecné
|
|
_buildSectionHeader(l10n.settingsSectionGeneral),
|
|
const LanguageSettingsTile(),
|
|
const SizedBox(height: 8),
|
|
// Sekce Zabezpečení
|
|
_buildSectionHeader(l10n.settingsSectionSecurity),
|
|
_buildTile(
|
|
icon: Icons.pin_outlined,
|
|
title: l10n.settingsChangePinTitle,
|
|
subtitle: l10n.settingsChangePinSubtitle,
|
|
onTap: () => context.push('/change-pin'),
|
|
),
|
|
const BiometricsTile(),
|
|
_buildTile(
|
|
icon: Icons.timer_outlined,
|
|
title: l10n.settingsAutoLockTitle,
|
|
subtitle: _autoLockLabel(
|
|
l10n,
|
|
ref.watch(autoLockProvider).valueOrNull ??
|
|
AutoLockNotifier.vychozi,
|
|
),
|
|
onTap: () => _nastavAutoLock(context, ref),
|
|
),
|
|
_buildTile(
|
|
icon: Icons.password_outlined,
|
|
title: l10n.settingsDecoyPinTitle,
|
|
subtitle: ref.watch(hasDecoyPinProvider).valueOrNull == true
|
|
? l10n.settingsDecoyPinSetSubtitle
|
|
: l10n.settingsDecoyPinUnsetSubtitle,
|
|
trailing: ref.watch(hasDecoyPinProvider).valueOrNull == true
|
|
? TextButton(
|
|
onPressed: () => _disableDecoyPin(context, ref),
|
|
child: Text(l10n.commonCancel),
|
|
)
|
|
: null,
|
|
onTap: () {
|
|
if (ref.read(hasDecoyPinProvider).valueOrNull != true) {
|
|
_setDecoyPin(context, ref);
|
|
}
|
|
},
|
|
),
|
|
const SizedBox(height: 8),
|
|
// Sekce Notifikace
|
|
_buildSectionHeader(l10n.settingsSectionNotifications),
|
|
const PushSettingsTile(),
|
|
const SizedBox(height: 8),
|
|
// Sekce Připojení — Tor je přes Orbot (Android). Na iOS zatím
|
|
// není cesta k lokální SOCKS proxy, tak dlaždici skryjeme.
|
|
if (Platform.isAndroid) ...[
|
|
_buildSectionHeader(l10n.settingsSectionConnection),
|
|
const TorSettingsTile(),
|
|
const SizedBox(height: 8),
|
|
],
|
|
// Sekce Soukromí
|
|
// Mizící zprávy se nastavují per konverzaci (menu ⋮), globální
|
|
// dlaždice tu byla mrtvá — odebrána místo předstírání funkce.
|
|
_buildSectionHeader(l10n.settingsSectionPrivacy),
|
|
_buildTile(
|
|
icon: Icons.done_all,
|
|
title: l10n.settingsReadReceiptsTitle,
|
|
subtitle: l10n.settingsReadReceiptsSubtitle,
|
|
trailing: Switch(
|
|
value: ref.watch(readReceiptsProvider).valueOrNull ?? false,
|
|
onChanged: (v) =>
|
|
ref.read(readReceiptsProvider.notifier).set(v),
|
|
activeTrackColor: SepotColors.ember,
|
|
),
|
|
onTap: () {},
|
|
),
|
|
_buildTile(
|
|
icon: Icons.screenshot_monitor_outlined,
|
|
title: l10n.settingsScreenSecurityTitle,
|
|
subtitle: l10n.settingsScreenSecuritySubtitle,
|
|
trailing: Switch(
|
|
value: ref.watch(screenSecurityProvider).valueOrNull ?? false,
|
|
onChanged: (v) =>
|
|
ref.read(screenSecurityProvider.notifier).set(v),
|
|
activeTrackColor: SepotColors.ember,
|
|
),
|
|
onTap: () {},
|
|
),
|
|
const SizedBox(height: 8),
|
|
// Sekce Záloha
|
|
_buildSectionHeader(l10n.settingsSectionBackup),
|
|
_buildTile(
|
|
icon: Icons.key_outlined,
|
|
title: l10n.settingsSeedPhraseTitle,
|
|
subtitle: l10n.settingsSeedPhraseSubtitle,
|
|
onTap: () => _showSeedPhrase(context, ref),
|
|
),
|
|
_buildTile(
|
|
icon: Icons.backup_outlined,
|
|
title: l10n.settingsBackupExportTitle,
|
|
subtitle: l10n.settingsBackupExportSubtitle,
|
|
onTap: () => _exportBackup(context, ref),
|
|
),
|
|
_buildTile(
|
|
icon: Icons.restore_outlined,
|
|
title: l10n.settingsBackupImportTitle,
|
|
subtitle: l10n.settingsBackupImportSubtitle,
|
|
onTap: () => _importBackup(context, ref),
|
|
),
|
|
// „Web klient" byl slib funkce, která neexistuje (multi-device je
|
|
// vlna 3). Apple Guideline 2.1 takové dlaždice bere jako nedodělek.
|
|
const SizedBox(height: 8),
|
|
// Sekce Bezpečnost — Play UGC/CSAE policy, Apple Guideline 1.2.
|
|
// Nahlášení musí být dostupné i mimo konkrétní konverzaci.
|
|
_buildSectionHeader(l10n.settingsSectionSafety),
|
|
_buildTile(
|
|
icon: Icons.flag_outlined,
|
|
title: l10n.settingsReportAbuseTitle,
|
|
subtitle: l10n.settingsReportAbuseSubtitle,
|
|
onTap: () => context.push('/report'),
|
|
),
|
|
_buildTile(
|
|
icon: Icons.shield_outlined,
|
|
title: l10n.settingsChildSafetyTitle,
|
|
subtitle: l10n.settingsChildSafetySubtitle,
|
|
onTap: () => launchUrl(
|
|
Uri.parse(AppConstants.childSafetyUrl),
|
|
mode: LaunchMode.externalApplication,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
// Sekce O aplikaci
|
|
_buildSectionHeader(l10n.settingsSectionAbout),
|
|
_buildTile(
|
|
icon: Icons.info_outline,
|
|
title: l10n.commonAppName,
|
|
subtitle: l10n.settingsVersion(AppInfo.version),
|
|
onTap: () => _showAbout(context),
|
|
),
|
|
// Zdrojový kód je veřejný (AGPL-3.0). Detaily o licenci a odkaz
|
|
// na repozitář dává dlaždice níže.
|
|
_buildTile(
|
|
icon: Icons.gavel_outlined,
|
|
title: l10n.settingsLicenseTitle,
|
|
subtitle: l10n.settingsLicenseSubtitle,
|
|
onTap: () => _zobrazLicenci(context),
|
|
),
|
|
_buildTile(
|
|
icon: Icons.privacy_tip_outlined,
|
|
title: l10n.settingsPrivacyPolicyTitle,
|
|
subtitle: l10n.settingsPrivacyPolicySubtitle,
|
|
onTap: () => _otevri(AppConstants.privacyUrl),
|
|
),
|
|
_buildTile(
|
|
icon: Icons.description_outlined,
|
|
title: l10n.settingsTermsTitle,
|
|
subtitle: l10n.settingsTermsSubtitle,
|
|
onTap: () => _otevri(AppConstants.termsUrl),
|
|
),
|
|
_buildTile(
|
|
icon: Icons.delete_outline,
|
|
title: l10n.settingsDataDeletionTitle,
|
|
subtitle: l10n.settingsDataDeletionSubtitle,
|
|
onTap: () => _otevri(AppConstants.deletionUrl),
|
|
),
|
|
const SizedBox(height: 24),
|
|
// Smazat účet
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: OutlinedButton(
|
|
onPressed: () => _confirmDeleteAccount(context, ref),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: SepotColors.error,
|
|
side: const BorderSide(color: SepotColors.error),
|
|
),
|
|
child: Text(l10n.settingsDeleteAllDataButton),
|
|
),
|
|
),
|
|
const SizedBox(height: 48),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _otevri(String url) => launchUrl(
|
|
Uri.parse(url),
|
|
mode: LaunchMode.externalApplication,
|
|
);
|
|
|
|
/// Popisek intervalu auto-zámku pro daný počet sekund.
|
|
String _autoLockLabel(AppLocalizations l10n, int sekundy) {
|
|
switch (sekundy) {
|
|
case 0:
|
|
return l10n.settingsAutoLockNever;
|
|
case 60:
|
|
return l10n.settingsAutoLock1min;
|
|
case 900:
|
|
return l10n.settingsAutoLock15min;
|
|
default:
|
|
return l10n.settingsAutoLock5min;
|
|
}
|
|
}
|
|
|
|
/// AGPL-3.0 § 6 chce spolu s binárkou i nabídku zdrojového kódu.
|
|
/// Zdroj je veřejný (repozitář, F-Droid, APK); tenhle text na něj odkazuje.
|
|
void _zobrazLicenci(BuildContext context) {
|
|
showDialog<void>(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: Text(ctx.l10n.settingsLicenseTitle),
|
|
content: Text(
|
|
ctx.l10n.settingsLicenseBody(AppConstants.supportEmail),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx),
|
|
child: Text(ctx.l10n.commonClose),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _nastavAutoLock(BuildContext context, WidgetRef ref) async {
|
|
final ted = ref.read(autoLockProvider).valueOrNull ?? AutoLockNotifier.vychozi;
|
|
final vybrano = await showDialog<int>(
|
|
context: context,
|
|
builder: (ctx) => SimpleDialog(
|
|
title: Text(ctx.l10n.settingsAutoLockTitle),
|
|
children: [
|
|
for (final sekundy in AutoLockNotifier.moznostiSekundy)
|
|
ListTile(
|
|
onTap: () => Navigator.pop(ctx, sekundy),
|
|
leading: Icon(
|
|
sekundy == ted
|
|
? Icons.radio_button_checked
|
|
: Icons.radio_button_off,
|
|
color: sekundy == ted
|
|
? SepotColors.ember
|
|
: SepotColors.textMuted,
|
|
),
|
|
title: Text(_autoLockLabel(ctx.l10n, sekundy)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
if (vybrano != null && vybrano != ted) {
|
|
await ref.read(autoLockProvider.notifier).set(vybrano);
|
|
}
|
|
}
|
|
|
|
Widget _buildProfileCard(BuildContext context, LocalIdentity identity) {
|
|
return Container(
|
|
margin: const EdgeInsets.all(16),
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(16),
|
|
color: SepotColors.surface,
|
|
border: Border.all(color: SepotColors.border),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
CircleAvatar(
|
|
radius: 36,
|
|
backgroundColor: SepotColors.ember.withValues(alpha: 0.1),
|
|
child: Text(
|
|
identity.displayName.isEmpty
|
|
? '?'
|
|
: identity.displayName.characters.first.toUpperCase(),
|
|
style: const TextStyle(
|
|
fontFamily: kFontSans,
|
|
fontSize: 28,
|
|
fontWeight: FontWeight.w600,
|
|
color: SepotColors.ember,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
identity.displayName,
|
|
style: const TextStyle(
|
|
fontFamily: kFontSans,
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w600,
|
|
color: SepotColors.textPrimary,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
InkWell(
|
|
onTap: () {
|
|
Clipboard.setData(ClipboardData(text: identity.sepotId));
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(context.l10n.settingsIdCopied)),
|
|
);
|
|
},
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
identity.sepotId,
|
|
style: SepotTheme.idText,
|
|
),
|
|
const SizedBox(width: 4),
|
|
const Icon(Icons.copy, size: 14, color: SepotColors.ember),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
context.l10n.settingsFingerprint(identity.fingerprint.display),
|
|
style: SepotTheme.cryptoText.copyWith(fontSize: 11),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSectionHeader(String title) {
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
|
child: Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Text(
|
|
title,
|
|
style: const TextStyle(
|
|
fontFamily: kFontSans,
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
color: SepotColors.textMuted,
|
|
letterSpacing: 0.5,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTile({
|
|
required IconData icon,
|
|
required String title,
|
|
required String subtitle,
|
|
Widget? trailing,
|
|
required VoidCallback onTap,
|
|
}) {
|
|
return ListTile(
|
|
leading: Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(10),
|
|
color: SepotColors.surfaceVariant,
|
|
),
|
|
child: Icon(icon, size: 20, color: SepotColors.textSecondary),
|
|
),
|
|
title: Text(title, style: const TextStyle(fontSize: 15)),
|
|
subtitle: Text(
|
|
subtitle,
|
|
style: const TextStyle(fontSize: 13, color: SepotColors.textMuted),
|
|
),
|
|
trailing: trailing ?? const Icon(Icons.chevron_right, color: SepotColors.textMuted),
|
|
onTap: onTap,
|
|
);
|
|
}
|
|
|
|
Future<void> _exportBackup(BuildContext context, WidgetRef ref) async {
|
|
final l10n = context.l10n;
|
|
final identity = ref.read(identityProvider).valueOrNull;
|
|
if (identity == null) return;
|
|
try {
|
|
final bytes = await ref.read(backupServiceProvider).export(identity);
|
|
final dir = await getTemporaryDirectory();
|
|
final path = '${dir.path}/sepot-zaloha.sepotbackup';
|
|
await File(path).writeAsBytes(bytes);
|
|
await Share.shareXFiles(
|
|
[XFile(path, mimeType: 'application/octet-stream')],
|
|
subject: l10n.backupShareSubject,
|
|
);
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(l10n.backupExportFailed(describeError(l10n, e)))),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _importBackup(BuildContext context, WidgetRef ref) async {
|
|
final l10n = context.l10n;
|
|
final identity = ref.read(identityProvider).valueOrNull;
|
|
if (identity == null) return;
|
|
try {
|
|
final res = await FilePicker.pickFiles(withData: true);
|
|
final data = res?.files.firstOrNull?.bytes;
|
|
if (data == null) return;
|
|
final n = await ref.read(backupServiceProvider).import(identity, data);
|
|
ref.invalidate(conversationsProvider);
|
|
ref.invalidate(contactsProvider);
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(l10n.backupRestoredCount(n))),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(l10n.backupImportFailed(describeError(l10n, e)))),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _setDecoyPin(BuildContext context, WidgetRef ref) async {
|
|
final l10n = context.l10n;
|
|
final pinCtrl = TextEditingController();
|
|
final confirmCtrl = TextEditingController();
|
|
const numberField = TextInputType.number;
|
|
final pin = await showDialog<String>(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: Text(ctx.l10n.settingsDecoyPinTitle),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
ctx.l10n.settingsDecoyPinExplain,
|
|
style: const TextStyle(fontSize: 13),
|
|
),
|
|
const SizedBox(height: 16),
|
|
TextField(
|
|
controller: pinCtrl,
|
|
obscureText: true,
|
|
keyboardType: numberField,
|
|
maxLength: 6,
|
|
decoration: InputDecoration(
|
|
labelText: ctx.l10n.settingsDecoyPinTitle, counterText: ''),
|
|
),
|
|
TextField(
|
|
controller: confirmCtrl,
|
|
obscureText: true,
|
|
keyboardType: numberField,
|
|
maxLength: 6,
|
|
decoration: InputDecoration(
|
|
labelText: ctx.l10n.commonConfirm, counterText: ''),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx),
|
|
child: Text(ctx.l10n.commonCancel)),
|
|
TextButton(
|
|
onPressed: () {
|
|
if (pinCtrl.text.length == 6 &&
|
|
pinCtrl.text == confirmCtrl.text) {
|
|
Navigator.pop(ctx, pinCtrl.text);
|
|
}
|
|
},
|
|
child: Text(ctx.l10n.commonSet),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
if (pin == null) return;
|
|
try {
|
|
await ref.read(storageProvider).enableDecoyPin(pin);
|
|
ref.invalidate(hasDecoyPinProvider);
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(l10n.settingsDecoyPinEnabled)),
|
|
);
|
|
}
|
|
} on DecoyPinConflict {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(l10n.settingsDecoyPinConflict)),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(describeError(l10n, e))),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _disableDecoyPin(BuildContext context, WidgetRef ref) async {
|
|
final l10n = context.l10n;
|
|
await ref.read(storageProvider).disableDecoyPin();
|
|
ref.invalidate(hasDecoyPinProvider);
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(l10n.settingsDecoyPinDisabled)),
|
|
);
|
|
}
|
|
}
|
|
|
|
void _showSeedPhrase(BuildContext context, WidgetRef ref) async {
|
|
final l10n = context.l10n;
|
|
final storage = ref.read(storageProvider);
|
|
final seed = await storage.loadSeedPhrase();
|
|
|
|
if (seed == null) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(l10n.settingsSeedPhraseNotFound)),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!context.mounted) return;
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: Text(ctx.l10n.settingsSeedPhraseTitle),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
ctx.l10n.settingsSeedPhraseWarning,
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
color: SepotColors.warning,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: List.generate(seed.length, (i) {
|
|
return Chip(
|
|
label: Text(
|
|
ctx.l10n.settingsSeedWordChip(i + 1, seed[i]),
|
|
style: SepotTheme.cryptoText,
|
|
),
|
|
backgroundColor: SepotColors.surfaceVariant,
|
|
side: const BorderSide(color: SepotColors.border),
|
|
);
|
|
}),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.of(ctx).pop(),
|
|
child: Text(ctx.l10n.commonClose),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showAbout(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const SepotLogo(size: 64),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
ctx.l10n.settingsAboutTagline,
|
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
ctx.l10n.settingsVersion(AppInfo.version),
|
|
style: const TextStyle(color: SepotColors.textSecondary),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
ctx.l10n.settingsAboutBody,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
color: SepotColors.textSecondary,
|
|
height: 1.5,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.of(ctx).pop(),
|
|
child: Text(ctx.l10n.commonClose),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _confirmDeleteAccount(BuildContext context, WidgetRef ref) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: Text(ctx.l10n.settingsDeleteAccountTitle),
|
|
content: Text(
|
|
ctx.l10n.settingsDeleteAccountBody,
|
|
style: const TextStyle(color: SepotColors.textSecondary),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(ctx).pop(),
|
|
child: Text(ctx.l10n.commonCancel),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () async {
|
|
final messaging = ref.read(messagingServiceProvider);
|
|
// Serverová deregistrace PŘED lokálním wipe — podpis
|
|
// požadavku potřebuje identity klíč, který wipe zahodí.
|
|
final serverOk = await messaging.deleteRelayAccount();
|
|
// Messaging drží referenci na DB — zastavit PŘED smazáním
|
|
await messaging.stop();
|
|
await ref.read(storageProvider).deleteAll();
|
|
if (ctx.mounted) Navigator.of(ctx).pop();
|
|
if (context.mounted) {
|
|
if (!serverOk) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
context.l10n.settingsDeleteAccountServerOffline,
|
|
),
|
|
duration: const Duration(seconds: 8),
|
|
),
|
|
);
|
|
}
|
|
context.go('/onboarding');
|
|
}
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: SepotColors.error,
|
|
),
|
|
child: Text(ctx.l10n.settingsDeleteAllConfirm),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|