#1476 launch error handling;

DB: table existence check in v13+ upgrades
This commit is contained in:
Thibault Deckers 2025-03-16 17:17:45 +01:00
parent cf74e75d58
commit cb067aa1ac
16 changed files with 722 additions and 438 deletions

View file

@ -4,8 +4,13 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased]
### Added
- handle launch error to report and export DB
### Changed
- DB post-upgrade sanitization
- upgraded Flutter to stable v3.29.2
## <a id="v1.12.6"></a>[v1.12.6] - 2025-03-11

View file

@ -49,6 +49,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
"requestMediaFileAccess" -> ioScope.launch { requestMediaFileAccess() }
"createFile" -> ioScope.launch { createFile() }
"openFile" -> ioScope.launch { openFile() }
"copyFile" -> ioScope.launch { copyFile() }
"edit" -> edit()
"pickCollectionFilters" -> pickCollectionFilters()
else -> endOfStream()
@ -181,6 +182,49 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
safeStartActivityForStorageAccessResult(intent, MainActivity.OPEN_FILE_REQUEST, ::onGranted, ::onDenied)
}
private suspend fun copyFile() {
val name = args["name"] as String?
val mimeType = args["mimeType"] as String?
val sourceUri = (args["sourceUri"] as String?)?.toUri()
if (name == null || mimeType == null || sourceUri == null) {
error("copyFile-args", "missing arguments", null)
return
}
fun onGranted(uri: Uri) {
ioScope.launch {
try {
StorageUtils.openInputStream(activity, sourceUri)?.use { input ->
// truncate is necessary when overwriting a longer file
activity.contentResolver.openOutputStream(uri, "wt")?.use { output ->
val buffer = ByteArray(BUFFER_SIZE)
var len: Int
while (input.read(buffer).also { len = it } != -1) {
output.write(buffer, 0, len)
}
}
}
success(true)
} catch (e: Exception) {
error("copyFile-write", "failed to copy file from sourceUri=$sourceUri to uri=$uri", e.message)
}
endOfStream()
}
}
fun onDenied() {
success(null)
endOfStream()
}
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = mimeType
putExtra(Intent.EXTRA_TITLE, name)
}
safeStartActivityForStorageAccessResult(intent, MainActivity.CREATE_FILE_REQUEST, ::onGranted, ::onDenied)
}
private fun edit() {
val uri = args["uri"] as String?
val mimeType = args["mimeType"] as String? // optional

View file

@ -12,6 +12,8 @@ import 'package:aves/model/viewer/video_playback.dart';
abstract class LocalMediaDb {
int get nextId;
Future<String> get path;
Future<void> init();
Future<int> dbFileSize();

View file

@ -0,0 +1,15 @@
import 'package:sqflite/sqflite.dart';
extension ExtraDatabase on Database {
// check table existence
// proper way is to select from `sqlite_master` but this meta table may be missing on some devices
// so we rely on failure check instead
bool tableExists(String table) {
try {
query(table, limit: 1);
return true;
} catch (error) {
return false;
}
}
}

View file

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:aves/model/covers.dart';
import 'package:aves/model/db/db.dart';
import 'package:aves/model/db/db_sqflite_schema.dart';
import 'package:aves/model/db/db_sqflite_upgrade.dart';
import 'package:aves/model/dynamic_albums.dart';
import 'package:aves/model/entry/entry.dart';
@ -20,18 +21,19 @@ import 'package:sqflite/sqflite.dart';
class SqfliteLocalMediaDb implements LocalMediaDb {
late Database _db;
@override
Future<String> get path async => pContext.join(await getDatabasesPath(), 'metadata.db');
static const entryTable = 'entry';
static const dateTakenTable = 'dateTaken';
static const metadataTable = 'metadata';
static const addressTable = 'address';
static const favouriteTable = 'favourites';
static const coverTable = 'covers';
static const dynamicAlbumTable = 'dynamicAlbums';
static const vaultTable = 'vaults';
static const trashTable = 'trash';
static const videoPlaybackTable = 'videoPlayback';
static const entryTable = SqfliteLocalMediaDbSchema.entryTable;
static const dateTakenTable = SqfliteLocalMediaDbSchema.dateTakenTable;
static const metadataTable = SqfliteLocalMediaDbSchema.metadataTable;
static const addressTable = SqfliteLocalMediaDbSchema.addressTable;
static const favouriteTable = SqfliteLocalMediaDbSchema.favouriteTable;
static const coverTable = SqfliteLocalMediaDbSchema.coverTable;
static const dynamicAlbumTable = SqfliteLocalMediaDbSchema.dynamicAlbumTable;
static const vaultTable = SqfliteLocalMediaDbSchema.vaultTable;
static const trashTable = SqfliteLocalMediaDbSchema.trashTable;
static const videoPlaybackTable = SqfliteLocalMediaDbSchema.videoPlaybackTable;
static const _entryInsertSliceMaxCount = 10000; // number of entries
static const _queryCursorBufferSize = 1000; // number of rows
@ -44,78 +46,7 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
Future<void> init() async {
_db = await openDatabase(
await path,
onCreate: (db, version) async {
await db.execute('CREATE TABLE $entryTable('
'id INTEGER PRIMARY KEY'
', contentId INTEGER'
', uri TEXT'
', path TEXT'
', sourceMimeType TEXT'
', width INTEGER'
', height INTEGER'
', sourceRotationDegrees INTEGER'
', sizeBytes INTEGER'
', title TEXT'
', dateAddedSecs INTEGER DEFAULT (strftime(\'%s\',\'now\'))'
', dateModifiedMillis INTEGER'
', sourceDateTakenMillis INTEGER'
', durationMillis INTEGER'
', trashed INTEGER DEFAULT 0'
', origin INTEGER DEFAULT 0'
')');
await db.execute('CREATE TABLE $dateTakenTable('
'id INTEGER PRIMARY KEY'
', dateMillis INTEGER'
')');
await db.execute('CREATE TABLE $metadataTable('
'id INTEGER PRIMARY KEY'
', mimeType TEXT'
', dateMillis INTEGER'
', flags INTEGER'
', rotationDegrees INTEGER'
', xmpSubjects TEXT'
', xmpTitle TEXT'
', latitude REAL'
', longitude REAL'
', rating INTEGER'
')');
await db.execute('CREATE TABLE $addressTable('
'id INTEGER PRIMARY KEY'
', addressLine TEXT'
', countryCode TEXT'
', countryName TEXT'
', adminArea TEXT'
', locality TEXT'
')');
await db.execute('CREATE TABLE $favouriteTable('
'id INTEGER PRIMARY KEY'
')');
await db.execute('CREATE TABLE $coverTable('
'filter TEXT PRIMARY KEY'
', entryId INTEGER'
', packageName TEXT'
', color TEXT'
')');
await db.execute('CREATE TABLE $dynamicAlbumTable('
'name TEXT PRIMARY KEY'
', filter TEXT'
')');
await db.execute('CREATE TABLE $vaultTable('
'name TEXT PRIMARY KEY'
', autoLock INTEGER'
', useBin INTEGER'
', lockType TEXT'
')');
await db.execute('CREATE TABLE $trashTable('
'id INTEGER PRIMARY KEY'
', path TEXT'
', dateMillis INTEGER'
')');
await db.execute('CREATE TABLE $videoPlaybackTable('
'id INTEGER PRIMARY KEY'
', resumeTimeMillis INTEGER'
')');
},
onCreate: (db, version) => SqfliteLocalMediaDbSchema.createLatestVersion(db),
onUpgrade: LocalMediaDbUpgrader.upgradeDb,
version: 15,
);

View file

@ -0,0 +1,118 @@
import 'package:sqflite/sqflite.dart';
class SqfliteLocalMediaDbSchema {
static const entryTable = 'entry';
static const dateTakenTable = 'dateTaken';
static const metadataTable = 'metadata';
static const addressTable = 'address';
static const favouriteTable = 'favourites';
static const coverTable = 'covers';
static const dynamicAlbumTable = 'dynamicAlbums';
static const vaultTable = 'vaults';
static const trashTable = 'trash';
static const videoPlaybackTable = 'videoPlayback';
static const allTables = [
entryTable,
dateTakenTable,
metadataTable,
addressTable,
favouriteTable,
coverTable,
dynamicAlbumTable,
vaultTable,
trashTable,
videoPlaybackTable,
];
static Future<void> createLatestVersion(Database db) async {
await Future.forEach(allTables, (table) => createTable(db, table));
}
static Future<void> createTable(Database db, String table) {
switch (table) {
case entryTable:
return db.execute('CREATE TABLE $entryTable('
'id INTEGER PRIMARY KEY'
', contentId INTEGER'
', uri TEXT'
', path TEXT'
', sourceMimeType TEXT'
', width INTEGER'
', height INTEGER'
', sourceRotationDegrees INTEGER'
', sizeBytes INTEGER'
', title TEXT'
', dateAddedSecs INTEGER DEFAULT (strftime(\'%s\',\'now\'))'
', dateModifiedMillis INTEGER'
', sourceDateTakenMillis INTEGER'
', durationMillis INTEGER'
', trashed INTEGER DEFAULT 0'
', origin INTEGER DEFAULT 0'
')');
case dateTakenTable:
return db.execute('CREATE TABLE $dateTakenTable('
'id INTEGER PRIMARY KEY'
', dateMillis INTEGER'
')');
case metadataTable:
return db.execute('CREATE TABLE $metadataTable('
'id INTEGER PRIMARY KEY'
', mimeType TEXT'
', dateMillis INTEGER'
', flags INTEGER'
', rotationDegrees INTEGER'
', xmpSubjects TEXT'
', xmpTitle TEXT'
', latitude REAL'
', longitude REAL'
', rating INTEGER'
')');
case addressTable:
return db.execute('CREATE TABLE $addressTable('
'id INTEGER PRIMARY KEY'
', addressLine TEXT'
', countryCode TEXT'
', countryName TEXT'
', adminArea TEXT'
', locality TEXT'
')');
case favouriteTable:
return db.execute('CREATE TABLE $favouriteTable('
'id INTEGER PRIMARY KEY'
')');
case coverTable:
return db.execute('CREATE TABLE $coverTable('
'filter TEXT PRIMARY KEY'
', entryId INTEGER'
', packageName TEXT'
', color TEXT'
')');
case dynamicAlbumTable:
return db.execute('CREATE TABLE $dynamicAlbumTable('
'name TEXT PRIMARY KEY'
', filter TEXT'
')');
case vaultTable:
return db.execute('CREATE TABLE $vaultTable('
'name TEXT PRIMARY KEY'
', autoLock INTEGER'
', useBin INTEGER'
', lockType TEXT'
')');
case trashTable:
return db.execute('CREATE TABLE $trashTable('
'id INTEGER PRIMARY KEY'
', path TEXT'
', dateMillis INTEGER'
')');
case videoPlaybackTable:
return db.execute('CREATE TABLE $videoPlaybackTable('
'id INTEGER PRIMARY KEY'
', resumeTimeMillis INTEGER'
')');
default:
throw Exception('unknown table=$table');
}
}
}

View file

@ -1,22 +1,23 @@
import 'dart:ui';
import 'package:aves/model/covers.dart';
import 'package:aves/model/db/db_sqflite.dart';
import 'package:aves/model/db/db_extension.dart';
import 'package:aves/model/db/db_sqflite_schema.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:flutter/foundation.dart';
import 'package:sqflite/sqflite.dart';
class LocalMediaDbUpgrader {
static const entryTable = SqfliteLocalMediaDb.entryTable;
static const dateTakenTable = SqfliteLocalMediaDb.dateTakenTable;
static const metadataTable = SqfliteLocalMediaDb.metadataTable;
static const addressTable = SqfliteLocalMediaDb.addressTable;
static const favouriteTable = SqfliteLocalMediaDb.favouriteTable;
static const coverTable = SqfliteLocalMediaDb.coverTable;
static const dynamicAlbumTable = SqfliteLocalMediaDb.dynamicAlbumTable;
static const vaultTable = SqfliteLocalMediaDb.vaultTable;
static const trashTable = SqfliteLocalMediaDb.trashTable;
static const videoPlaybackTable = SqfliteLocalMediaDb.videoPlaybackTable;
static const entryTable = SqfliteLocalMediaDbSchema.entryTable;
static const dateTakenTable = SqfliteLocalMediaDbSchema.dateTakenTable;
static const metadataTable = SqfliteLocalMediaDbSchema.metadataTable;
static const addressTable = SqfliteLocalMediaDbSchema.addressTable;
static const favouriteTable = SqfliteLocalMediaDbSchema.favouriteTable;
static const coverTable = SqfliteLocalMediaDbSchema.coverTable;
static const dynamicAlbumTable = SqfliteLocalMediaDbSchema.dynamicAlbumTable;
static const vaultTable = SqfliteLocalMediaDbSchema.vaultTable;
static const trashTable = SqfliteLocalMediaDbSchema.trashTable;
static const videoPlaybackTable = SqfliteLocalMediaDbSchema.videoPlaybackTable;
// warning: "ALTER TABLE ... RENAME COLUMN ..." is not supported
// on SQLite <3.25.0, bundled on older Android devices
@ -55,12 +56,28 @@ class LocalMediaDbUpgrader {
}
oldVersion++;
}
await _sanitize(db);
}
static Future<void> _sanitize(Database db) async {
// ensure all tables exist
await Future.forEach(SqfliteLocalMediaDbSchema.allTables, (table) async {
if (!db.tableExists(table)) {
await SqfliteLocalMediaDbSchema.createTable(db, table);
}
});
// remove rows referencing future entry IDs
final maxIdRows = await db.rawQuery('SELECT MAX(id) AS maxId FROM $entryTable');
final lastId = (maxIdRows.firstOrNull?['maxId'] as int?) ?? 0;
await db.delete(favouriteTable, where: 'id > ?', whereArgs: [lastId]);
await db.delete(coverTable, where: 'entryId > ?', whereArgs: [lastId]);
}
static Future<void> _upgradeFrom1(Database db) async {
debugPrint('upgrading DB from v1');
// rename column 'orientationDegrees' to 'sourceRotationDegrees'
await db.transaction((txn) async {
const newEntryTable = '${entryTable}TEMP';
await db.execute('CREATE TABLE $newEntryTable ('
'contentId INTEGER PRIMARY KEY'
@ -81,10 +98,8 @@ class LocalMediaDbUpgrader {
' FROM $entryTable;');
await db.execute('DROP TABLE $entryTable;');
await db.execute('ALTER TABLE $newEntryTable RENAME TO $entryTable;');
});
// rename column 'videoRotation' to 'rotationDegrees'
await db.transaction((txn) async {
const newMetadataTable = '${metadataTable}TEMP';
await db.execute('CREATE TABLE $newMetadataTable ('
'contentId INTEGER PRIMARY KEY'
@ -103,7 +118,6 @@ class LocalMediaDbUpgrader {
await db.rawInsert('UPDATE $newMetadataTable SET rotationDegrees = NULL WHERE rotationDegrees = 0;');
await db.execute('DROP TABLE $metadataTable;');
await db.execute('ALTER TABLE $newMetadataTable RENAME TO $metadataTable;');
});
// new column 'isFlipped'
await db.execute('ALTER TABLE $metadataTable ADD COLUMN isFlipped INTEGER;');
@ -111,8 +125,8 @@ class LocalMediaDbUpgrader {
static Future<void> _upgradeFrom2(Database db) async {
debugPrint('upgrading DB from v2');
// merge columns 'isAnimated' and 'isFlipped' into 'flags'
await db.transaction((txn) async {
const newMetadataTable = '${metadataTable}TEMP';
await db.execute('CREATE TABLE $newMetadataTable ('
'contentId INTEGER PRIMARY KEY'
@ -130,7 +144,6 @@ class LocalMediaDbUpgrader {
' FROM $metadataTable;');
await db.execute('DROP TABLE $metadataTable;');
await db.execute('ALTER TABLE $newMetadataTable RENAME TO $metadataTable;');
});
}
static Future<void> _upgradeFrom3(Database db) async {
@ -423,7 +436,6 @@ class LocalMediaDbUpgrader {
}
// convert `color` column type from value number to JSON string
await db.transaction((txn) async {
const newCoverTable = '${coverTable}TEMP';
await db.execute('CREATE TABLE $newCoverTable ('
'filter TEXT PRIMARY KEY'
@ -447,14 +459,13 @@ class LocalMediaDbUpgrader {
await db.execute('DROP TABLE $coverTable;');
await db.execute('ALTER TABLE $newCoverTable RENAME TO $coverTable;');
});
}
static Future<void> _upgradeFrom13(Database db) async {
debugPrint('upgrading DB from v13');
if (db.tableExists(entryTable)) {
// rename column 'dateModifiedSecs' to 'dateModifiedMillis'
await db.transaction((txn) async {
const newEntryTable = '${entryTable}TEMP';
await db.execute('CREATE TABLE $newEntryTable ('
'id INTEGER PRIMARY KEY'
@ -479,25 +490,19 @@ class LocalMediaDbUpgrader {
' FROM $entryTable;');
await db.execute('DROP TABLE $entryTable;');
await db.execute('ALTER TABLE $newEntryTable RENAME TO $entryTable;');
});
}
}
static Future<void> _upgradeFrom14(Database db) async {
debugPrint('upgrading DB from v14');
// no schema changes, but v1.12.4 may have corrupted the DB, so we sanitize it
// clear rebuildable tables
await db.delete(dateTakenTable, where: '1');
await db.delete(metadataTable, where: '1');
await db.delete(addressTable, where: '1');
await db.delete(trashTable, where: '1');
await db.delete(videoPlaybackTable, where: '1');
// remove rows referencing future entry IDs
final maxIdRows = await db.rawQuery('SELECT MAX(id) AS maxId FROM $entryTable');
final lastId = (maxIdRows.firstOrNull?['maxId'] as int?) ?? 0;
await db.delete(favouriteTable, where: 'id > ?', whereArgs: [lastId]);
await db.delete(coverTable, where: 'entryId > ?', whereArgs: [lastId]);
// no schema changes, but v1.12.4 may have corrupted the DB,
// so we clear rebuildable tables
final tables = [dateTakenTable, metadataTable, addressTable, trashTable, videoPlaybackTable];
await Future.forEach(tables, (table) async {
if (db.tableExists(table)) {
await db.delete(table, where: '1');
}
});
}
}

View file

@ -75,6 +75,7 @@ class MimeTypes {
static const json = 'application/json';
static const plainText = 'text/plain';
static const sqlite3 = 'application/vnd.sqlite3';
// JB2, JPC, JPX?
static const octetStream = 'application/octet-stream';

View file

@ -53,6 +53,9 @@ abstract class StorageService {
Future<bool?> createFile(String name, String mimeType, Uint8List bytes);
Future<Uint8List> openFile([String? mimeType]);
// return whether operation succeeded (`null` if user cancelled)
Future<bool?> copyFile(String name, String mimeType, String sourceUri);
}
class PlatformStorageService implements StorageService {
@ -369,4 +372,29 @@ class PlatformStorageService implements StorageService {
}
return Uint8List(0);
}
@override
Future<bool?> copyFile(String name, String mimeType, String sourceUri) async {
try {
final opCompleter = Completer<bool?>();
_stream.receiveBroadcastStream(<String, dynamic>{
'op': 'copyFile',
'name': name,
'mimeType': mimeType,
'sourceUri': sourceUri,
}).listen(
(data) => opCompleter.complete(data as bool?),
onError: opCompleter.completeError,
onDone: () {
if (!opCompleter.isCompleted) opCompleter.complete(false);
},
cancelOnError: true,
);
// `await` here, so that `completeError` will be caught below
return await opCompleter.future;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return false;
}
}

View file

@ -36,21 +36,11 @@ class BugReport extends StatefulWidget {
State<BugReport> createState() => _BugReportState();
}
class _BugReportState extends State<BugReport> with FeedbackMixin {
late Future<String> _infoLoader;
class _BugReportState extends State<BugReport> {
bool _showInstructions = false;
static const bugReportUrl = '${AppReference.avesGithub}/issues/new?labels=type%3Abug&template=bug_report.md';
@override
void initState() {
super.initState();
_infoLoader = _getInfo(context);
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final animationDuration = context.select<DurationsData, Duration>((v) => v.expansionTileAnimation);
return ExpansionPanelList(
expansionCallback: (index, isExpanded) {
@ -66,10 +56,40 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: AlignmentDirectional.centerStart,
child: Text(l10n.aboutBugSectionTitle, style: AStyles.knownTitleText),
child: Text(context.l10n.aboutBugSectionTitle, style: AStyles.knownTitleText),
),
),
body: Padding(
body: const BugReportContent(),
isExpanded: _showInstructions,
canTapOnHeader: true,
backgroundColor: Colors.transparent,
),
],
);
}
}
class BugReportContent extends StatefulWidget {
const BugReportContent({super.key});
@override
State<BugReportContent> createState() => _BugReportContentState();
}
class _BugReportContentState extends State<BugReportContent> with FeedbackMixin {
late Future<String> _infoLoader;
static const bugReportUrl = '${AppReference.avesGithub}/issues/new?labels=type%3Abug&template=bug_report.md';
@override
void initState() {
super.initState();
_infoLoader = _getInfo(context);
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -109,15 +129,9 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
},
),
_buildStep(3, l10n.aboutBugReportInstruction, l10n.aboutBugReportButton, _goToGithub),
const SizedBox(height: 16),
const SizedBox(height: 8),
],
),
),
isExpanded: _showInstructions,
canTapOnHeader: true,
backgroundColor: Colors.transparent,
),
],
);
}

View file

@ -36,7 +36,7 @@ import 'package:aves/widgets/common/providers/highlight_info_provider.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/providers/viewer_entry_provider.dart';
import 'package:aves/widgets/dialogs/entry_editors/edit_location_dialog.dart';
import 'package:aves/widgets/home_page.dart';
import 'package:aves/widgets/home/home_page.dart';
import 'package:aves/widgets/navigation/tv_page_transitions.dart';
import 'package:aves/widgets/navigation/tv_rail.dart';
import 'package:aves/widgets/welcome_page.dart';

View file

@ -0,0 +1,107 @@
import 'package:aves/ref/locales.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/about/bug_report.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
import 'package:aves/widgets/common/identity/buttons/outlined_button.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class HomeError extends StatefulWidget {
final Object error;
final StackTrace stack;
const HomeError({
super.key,
required this.error,
required this.stack,
});
@override
State<HomeError> createState() => _HomeErrorState();
}
class _HomeErrorState extends State<HomeError> with FeedbackMixin {
final ValueNotifier<String?> _expandedNotifier = ValueNotifier(null);
@override
void dispose() {
_expandedNotifier.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return SafeArea(
bottom: false,
child: CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 8),
sliver: SliverList(
delegate: SliverChildListDelegate(
[
AvesExpansionTile(
title: 'Error',
expandedNotifier: _expandedNotifier,
showHighlight: false,
children: [
Padding(
padding: const EdgeInsets.all(8),
child: SelectableText(
'${widget.error}:\n${widget.stack}',
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
AvesExpansionTile(
title: l10n.aboutBugSectionTitle,
expandedNotifier: _expandedNotifier,
showHighlight: false,
children: const [BugReportContent()],
),
AvesExpansionTile(
title: l10n.aboutDataUsageDatabase,
expandedNotifier: _expandedNotifier,
showHighlight: false,
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: AvesOutlinedButton(
label: l10n.settingsActionExport,
onPressed: () async {
final sourcePath = await localMediaDb.path;
final success = await storageService.copyFile(
'aves-database-${DateFormat('yyyyMMdd_HHmmss', asciiLocale).format(DateTime.now())}.db',
MimeTypes.sqlite3,
Uri.file(sourcePath).toString(),
);
if (success != null) {
if (success) {
showFeedback(context, FeedbackType.info, context.l10n.genericSuccessFeedback);
} else {
showFeedback(context, FeedbackType.warn, context.l10n.genericFailureFeedback);
}
}
},
),
),
],
),
],
),
],
),
),
),
],
),
);
}
}

View file

@ -7,9 +7,9 @@ import 'package:aves/model/app/permissions.dart';
import 'package:aves/model/app_inventory.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/catalog.dart';
import 'package:aves/model/filters/covered/location.dart';
import 'package:aves/model/filters/covered/stored_album.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/covered/location.dart';
import 'package:aves/model/settings/enums/home_page.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
@ -31,6 +31,7 @@ import 'package:aves/widgets/editor/entry_editor_page.dart';
import 'package:aves/widgets/explorer/explorer_page.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/tags_page.dart';
import 'package:aves/widgets/home/home_error.dart';
import 'package:aves/widgets/map/map_page.dart';
import 'package:aves/widgets/search/search_delegate.dart';
import 'package:aves/widgets/settings/home_widget_settings_page.dart';
@ -68,6 +69,7 @@ class _HomePageState extends State<HomePage> {
String? _initialExplorerPath;
(LatLng, double?)? _initialLocationZoom;
List<String>? _secureUris;
(Object, StackTrace)? _setupError;
static const allowedShortcutRoutes = [
AlbumListPage.routeName,
@ -85,9 +87,17 @@ class _HomePageState extends State<HomePage> {
}
@override
Widget build(BuildContext context) => const AvesScaffold();
Widget build(BuildContext context) => AvesScaffold(
body: _setupError != null
? HomeError(
error: _setupError!.$1,
stack: _setupError!.$2,
)
: null,
);
Future<void> _setup() async {
try {
final stopwatch = Stopwatch()..start();
if (await windowService.isActivity()) {
// do not check whether permission was granted, because some app stores
@ -262,6 +272,10 @@ class _HomePageState extends State<HomePage> {
await _getRedirectRoute(appMode),
(route) => false,
));
} catch (error, stack) {
debugPrint('failed to setup app with error=$error\n$stack');
_setupError = (error, stack);
}
}
Future<void> _initViewerEssentials() async {

View file

@ -26,7 +26,7 @@ import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/countries_page.dart';
import 'package:aves/widgets/filter_grids/places_page.dart';
import 'package:aves/widgets/filter_grids/tags_page.dart';
import 'package:aves/widgets/home_page.dart';
import 'package:aves/widgets/home/home_page.dart';
import 'package:aves/widgets/navigation/drawer/collection_nav_tile.dart';
import 'package:aves/widgets/navigation/drawer/page_nav_tile.dart';
import 'package:aves/widgets/navigation/drawer/tile.dart';

View file

@ -12,7 +12,7 @@ import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/countries_page.dart';
import 'package:aves/widgets/filter_grids/places_page.dart';
import 'package:aves/widgets/filter_grids/tags_page.dart';
import 'package:aves/widgets/home_page.dart';
import 'package:aves/widgets/home/home_page.dart';
import 'package:aves/widgets/settings/settings_page.dart';
import 'package:flutter/material.dart';

View file

@ -11,7 +11,7 @@ import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_logo.dart';
import 'package:aves/widgets/common/identity/buttons/outlined_button.dart';
import 'package:aves/widgets/home_page.dart';
import 'package:aves/widgets/home/home_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';