#1476 launch error handling;
DB: table existence check in v13+ upgrades
This commit is contained in:
parent
cf74e75d58
commit
cb067aa1ac
16 changed files with 722 additions and 438 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
15
lib/model/db/db_extension.dart
Normal file
15
lib/model/db/db_extension.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
|
|
118
lib/model/db/db_sqflite_schema.dart
Normal file
118
lib/model/db/db_sqflite_schema.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
107
lib/widgets/home/home_error.dart
Normal file
107
lib/widgets/home/home_error.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 {
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Reference in a new issue