#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]
|
## <a id="unreleased"></a>[Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- handle launch error to report and export DB
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- DB post-upgrade sanitization
|
||||||
- upgraded Flutter to stable v3.29.2
|
- upgraded Flutter to stable v3.29.2
|
||||||
|
|
||||||
## <a id="v1.12.6"></a>[v1.12.6] - 2025-03-11
|
## <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() }
|
"requestMediaFileAccess" -> ioScope.launch { requestMediaFileAccess() }
|
||||||
"createFile" -> ioScope.launch { createFile() }
|
"createFile" -> ioScope.launch { createFile() }
|
||||||
"openFile" -> ioScope.launch { openFile() }
|
"openFile" -> ioScope.launch { openFile() }
|
||||||
|
"copyFile" -> ioScope.launch { copyFile() }
|
||||||
"edit" -> edit()
|
"edit" -> edit()
|
||||||
"pickCollectionFilters" -> pickCollectionFilters()
|
"pickCollectionFilters" -> pickCollectionFilters()
|
||||||
else -> endOfStream()
|
else -> endOfStream()
|
||||||
|
@ -181,6 +182,49 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
|
||||||
safeStartActivityForStorageAccessResult(intent, MainActivity.OPEN_FILE_REQUEST, ::onGranted, ::onDenied)
|
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() {
|
private fun edit() {
|
||||||
val uri = args["uri"] as String?
|
val uri = args["uri"] as String?
|
||||||
val mimeType = args["mimeType"] as String? // optional
|
val mimeType = args["mimeType"] as String? // optional
|
||||||
|
|
|
@ -12,6 +12,8 @@ import 'package:aves/model/viewer/video_playback.dart';
|
||||||
abstract class LocalMediaDb {
|
abstract class LocalMediaDb {
|
||||||
int get nextId;
|
int get nextId;
|
||||||
|
|
||||||
|
Future<String> get path;
|
||||||
|
|
||||||
Future<void> init();
|
Future<void> init();
|
||||||
|
|
||||||
Future<int> dbFileSize();
|
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/covers.dart';
|
||||||
import 'package:aves/model/db/db.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/db/db_sqflite_upgrade.dart';
|
||||||
import 'package:aves/model/dynamic_albums.dart';
|
import 'package:aves/model/dynamic_albums.dart';
|
||||||
import 'package:aves/model/entry/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
@ -20,18 +21,19 @@ import 'package:sqflite/sqflite.dart';
|
||||||
class SqfliteLocalMediaDb implements LocalMediaDb {
|
class SqfliteLocalMediaDb implements LocalMediaDb {
|
||||||
late Database _db;
|
late Database _db;
|
||||||
|
|
||||||
|
@override
|
||||||
Future<String> get path async => pContext.join(await getDatabasesPath(), 'metadata.db');
|
Future<String> get path async => pContext.join(await getDatabasesPath(), 'metadata.db');
|
||||||
|
|
||||||
static const entryTable = 'entry';
|
static const entryTable = SqfliteLocalMediaDbSchema.entryTable;
|
||||||
static const dateTakenTable = 'dateTaken';
|
static const dateTakenTable = SqfliteLocalMediaDbSchema.dateTakenTable;
|
||||||
static const metadataTable = 'metadata';
|
static const metadataTable = SqfliteLocalMediaDbSchema.metadataTable;
|
||||||
static const addressTable = 'address';
|
static const addressTable = SqfliteLocalMediaDbSchema.addressTable;
|
||||||
static const favouriteTable = 'favourites';
|
static const favouriteTable = SqfliteLocalMediaDbSchema.favouriteTable;
|
||||||
static const coverTable = 'covers';
|
static const coverTable = SqfliteLocalMediaDbSchema.coverTable;
|
||||||
static const dynamicAlbumTable = 'dynamicAlbums';
|
static const dynamicAlbumTable = SqfliteLocalMediaDbSchema.dynamicAlbumTable;
|
||||||
static const vaultTable = 'vaults';
|
static const vaultTable = SqfliteLocalMediaDbSchema.vaultTable;
|
||||||
static const trashTable = 'trash';
|
static const trashTable = SqfliteLocalMediaDbSchema.trashTable;
|
||||||
static const videoPlaybackTable = 'videoPlayback';
|
static const videoPlaybackTable = SqfliteLocalMediaDbSchema.videoPlaybackTable;
|
||||||
|
|
||||||
static const _entryInsertSliceMaxCount = 10000; // number of entries
|
static const _entryInsertSliceMaxCount = 10000; // number of entries
|
||||||
static const _queryCursorBufferSize = 1000; // number of rows
|
static const _queryCursorBufferSize = 1000; // number of rows
|
||||||
|
@ -44,78 +46,7 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
_db = await openDatabase(
|
_db = await openDatabase(
|
||||||
await path,
|
await path,
|
||||||
onCreate: (db, version) async {
|
onCreate: (db, version) => SqfliteLocalMediaDbSchema.createLatestVersion(db),
|
||||||
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'
|
|
||||||
')');
|
|
||||||
},
|
|
||||||
onUpgrade: LocalMediaDbUpgrader.upgradeDb,
|
onUpgrade: LocalMediaDbUpgrader.upgradeDb,
|
||||||
version: 15,
|
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 'dart:ui';
|
||||||
|
|
||||||
import 'package:aves/model/covers.dart';
|
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:aves/model/filters/filters.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:sqflite/sqflite.dart';
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
|
||||||
class LocalMediaDbUpgrader {
|
class LocalMediaDbUpgrader {
|
||||||
static const entryTable = SqfliteLocalMediaDb.entryTable;
|
static const entryTable = SqfliteLocalMediaDbSchema.entryTable;
|
||||||
static const dateTakenTable = SqfliteLocalMediaDb.dateTakenTable;
|
static const dateTakenTable = SqfliteLocalMediaDbSchema.dateTakenTable;
|
||||||
static const metadataTable = SqfliteLocalMediaDb.metadataTable;
|
static const metadataTable = SqfliteLocalMediaDbSchema.metadataTable;
|
||||||
static const addressTable = SqfliteLocalMediaDb.addressTable;
|
static const addressTable = SqfliteLocalMediaDbSchema.addressTable;
|
||||||
static const favouriteTable = SqfliteLocalMediaDb.favouriteTable;
|
static const favouriteTable = SqfliteLocalMediaDbSchema.favouriteTable;
|
||||||
static const coverTable = SqfliteLocalMediaDb.coverTable;
|
static const coverTable = SqfliteLocalMediaDbSchema.coverTable;
|
||||||
static const dynamicAlbumTable = SqfliteLocalMediaDb.dynamicAlbumTable;
|
static const dynamicAlbumTable = SqfliteLocalMediaDbSchema.dynamicAlbumTable;
|
||||||
static const vaultTable = SqfliteLocalMediaDb.vaultTable;
|
static const vaultTable = SqfliteLocalMediaDbSchema.vaultTable;
|
||||||
static const trashTable = SqfliteLocalMediaDb.trashTable;
|
static const trashTable = SqfliteLocalMediaDbSchema.trashTable;
|
||||||
static const videoPlaybackTable = SqfliteLocalMediaDb.videoPlaybackTable;
|
static const videoPlaybackTable = SqfliteLocalMediaDbSchema.videoPlaybackTable;
|
||||||
|
|
||||||
// warning: "ALTER TABLE ... RENAME COLUMN ..." is not supported
|
// warning: "ALTER TABLE ... RENAME COLUMN ..." is not supported
|
||||||
// on SQLite <3.25.0, bundled on older Android devices
|
// on SQLite <3.25.0, bundled on older Android devices
|
||||||
|
@ -55,55 +56,68 @@ class LocalMediaDbUpgrader {
|
||||||
}
|
}
|
||||||
oldVersion++;
|
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 {
|
static Future<void> _upgradeFrom1(Database db) async {
|
||||||
debugPrint('upgrading DB from v1');
|
debugPrint('upgrading DB from v1');
|
||||||
|
|
||||||
// rename column 'orientationDegrees' to 'sourceRotationDegrees'
|
// rename column 'orientationDegrees' to 'sourceRotationDegrees'
|
||||||
await db.transaction((txn) async {
|
const newEntryTable = '${entryTable}TEMP';
|
||||||
const newEntryTable = '${entryTable}TEMP';
|
await db.execute('CREATE TABLE $newEntryTable ('
|
||||||
await db.execute('CREATE TABLE $newEntryTable('
|
'contentId INTEGER PRIMARY KEY'
|
||||||
'contentId INTEGER PRIMARY KEY'
|
', uri TEXT'
|
||||||
', uri TEXT'
|
', path TEXT'
|
||||||
', path TEXT'
|
', sourceMimeType TEXT'
|
||||||
', sourceMimeType TEXT'
|
', width INTEGER'
|
||||||
', width INTEGER'
|
', height INTEGER'
|
||||||
', height INTEGER'
|
', sourceRotationDegrees INTEGER'
|
||||||
', sourceRotationDegrees INTEGER'
|
', sizeBytes INTEGER'
|
||||||
', sizeBytes INTEGER'
|
', title TEXT'
|
||||||
', title TEXT'
|
', dateModifiedSecs INTEGER'
|
||||||
', dateModifiedSecs INTEGER'
|
', sourceDateTakenMillis INTEGER'
|
||||||
', sourceDateTakenMillis INTEGER'
|
', durationMillis INTEGER'
|
||||||
', durationMillis INTEGER'
|
')');
|
||||||
')');
|
await db.rawInsert('INSERT INTO $newEntryTable (contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis)'
|
||||||
await db.rawInsert('INSERT INTO $newEntryTable(contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis)'
|
' SELECT contentId,uri,path,sourceMimeType,width,height,orientationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis'
|
||||||
' SELECT contentId,uri,path,sourceMimeType,width,height,orientationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis'
|
' FROM $entryTable;');
|
||||||
' FROM $entryTable;');
|
await db.execute('DROP TABLE $entryTable;');
|
||||||
await db.execute('DROP TABLE $entryTable;');
|
await db.execute('ALTER TABLE $newEntryTable RENAME TO $entryTable;');
|
||||||
await db.execute('ALTER TABLE $newEntryTable RENAME TO $entryTable;');
|
|
||||||
});
|
|
||||||
|
|
||||||
// rename column 'videoRotation' to 'rotationDegrees'
|
// rename column 'videoRotation' to 'rotationDegrees'
|
||||||
await db.transaction((txn) async {
|
const newMetadataTable = '${metadataTable}TEMP';
|
||||||
const newMetadataTable = '${metadataTable}TEMP';
|
await db.execute('CREATE TABLE $newMetadataTable ('
|
||||||
await db.execute('CREATE TABLE $newMetadataTable('
|
'contentId INTEGER PRIMARY KEY'
|
||||||
'contentId INTEGER PRIMARY KEY'
|
', mimeType TEXT'
|
||||||
', mimeType TEXT'
|
', dateMillis INTEGER'
|
||||||
', dateMillis INTEGER'
|
', isAnimated INTEGER'
|
||||||
', isAnimated INTEGER'
|
', rotationDegrees INTEGER'
|
||||||
', rotationDegrees INTEGER'
|
', xmpSubjects TEXT'
|
||||||
', xmpSubjects TEXT'
|
', xmpTitleDescription TEXT'
|
||||||
', xmpTitleDescription TEXT'
|
', latitude REAL'
|
||||||
', latitude REAL'
|
', longitude REAL'
|
||||||
', longitude REAL'
|
')');
|
||||||
')');
|
await db.rawInsert('INSERT INTO $newMetadataTable (contentId,mimeType,dateMillis,isAnimated,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude)'
|
||||||
await db.rawInsert('INSERT INTO $newMetadataTable(contentId,mimeType,dateMillis,isAnimated,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude)'
|
' SELECT contentId,mimeType,dateMillis,isAnimated,videoRotation,xmpSubjects,xmpTitleDescription,latitude,longitude'
|
||||||
' SELECT contentId,mimeType,dateMillis,isAnimated,videoRotation,xmpSubjects,xmpTitleDescription,latitude,longitude'
|
' FROM $metadataTable;');
|
||||||
' FROM $metadataTable;');
|
await db.rawInsert('UPDATE $newMetadataTable SET rotationDegrees = NULL WHERE rotationDegrees = 0;');
|
||||||
await db.rawInsert('UPDATE $newMetadataTable SET rotationDegrees = NULL WHERE rotationDegrees = 0;');
|
await db.execute('DROP TABLE $metadataTable;');
|
||||||
await db.execute('DROP TABLE $metadataTable;');
|
await db.execute('ALTER TABLE $newMetadataTable RENAME TO $metadataTable;');
|
||||||
await db.execute('ALTER TABLE $newMetadataTable RENAME TO $metadataTable;');
|
|
||||||
});
|
|
||||||
|
|
||||||
// new column 'isFlipped'
|
// new column 'isFlipped'
|
||||||
await db.execute('ALTER TABLE $metadataTable ADD COLUMN isFlipped INTEGER;');
|
await db.execute('ALTER TABLE $metadataTable ADD COLUMN isFlipped INTEGER;');
|
||||||
|
@ -111,31 +125,30 @@ class LocalMediaDbUpgrader {
|
||||||
|
|
||||||
static Future<void> _upgradeFrom2(Database db) async {
|
static Future<void> _upgradeFrom2(Database db) async {
|
||||||
debugPrint('upgrading DB from v2');
|
debugPrint('upgrading DB from v2');
|
||||||
|
|
||||||
// merge columns 'isAnimated' and 'isFlipped' into 'flags'
|
// merge columns 'isAnimated' and 'isFlipped' into 'flags'
|
||||||
await db.transaction((txn) async {
|
const newMetadataTable = '${metadataTable}TEMP';
|
||||||
const newMetadataTable = '${metadataTable}TEMP';
|
await db.execute('CREATE TABLE $newMetadataTable ('
|
||||||
await db.execute('CREATE TABLE $newMetadataTable('
|
'contentId INTEGER PRIMARY KEY'
|
||||||
'contentId INTEGER PRIMARY KEY'
|
', mimeType TEXT'
|
||||||
', mimeType TEXT'
|
', dateMillis INTEGER'
|
||||||
', dateMillis INTEGER'
|
', flags INTEGER'
|
||||||
', flags INTEGER'
|
', rotationDegrees INTEGER'
|
||||||
', rotationDegrees INTEGER'
|
', xmpSubjects TEXT'
|
||||||
', xmpSubjects TEXT'
|
', xmpTitleDescription TEXT'
|
||||||
', xmpTitleDescription TEXT'
|
', latitude REAL'
|
||||||
', latitude REAL'
|
', longitude REAL'
|
||||||
', longitude REAL'
|
')');
|
||||||
')');
|
await db.rawInsert('INSERT INTO $newMetadataTable (contentId,mimeType,dateMillis,flags,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude)'
|
||||||
await db.rawInsert('INSERT INTO $newMetadataTable(contentId,mimeType,dateMillis,flags,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude)'
|
' SELECT contentId,mimeType,dateMillis,ifnull(isAnimated,0)+ifnull(isFlipped,0)*2,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude'
|
||||||
' SELECT contentId,mimeType,dateMillis,ifnull(isAnimated,0)+ifnull(isFlipped,0)*2,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude'
|
' FROM $metadataTable;');
|
||||||
' FROM $metadataTable;');
|
await db.execute('DROP TABLE $metadataTable;');
|
||||||
await db.execute('DROP TABLE $metadataTable;');
|
await db.execute('ALTER TABLE $newMetadataTable RENAME TO $metadataTable;');
|
||||||
await db.execute('ALTER TABLE $newMetadataTable RENAME TO $metadataTable;');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> _upgradeFrom3(Database db) async {
|
static Future<void> _upgradeFrom3(Database db) async {
|
||||||
debugPrint('upgrading DB from v3');
|
debugPrint('upgrading DB from v3');
|
||||||
await db.execute('CREATE TABLE $coverTable('
|
await db.execute('CREATE TABLE $coverTable ('
|
||||||
'filter TEXT PRIMARY KEY'
|
'filter TEXT PRIMARY KEY'
|
||||||
', contentId INTEGER'
|
', contentId INTEGER'
|
||||||
')');
|
')');
|
||||||
|
@ -143,7 +156,7 @@ class LocalMediaDbUpgrader {
|
||||||
|
|
||||||
static Future<void> _upgradeFrom4(Database db) async {
|
static Future<void> _upgradeFrom4(Database db) async {
|
||||||
debugPrint('upgrading DB from v4');
|
debugPrint('upgrading DB from v4');
|
||||||
await db.execute('CREATE TABLE $videoPlaybackTable('
|
await db.execute('CREATE TABLE $videoPlaybackTable ('
|
||||||
'contentId INTEGER PRIMARY KEY'
|
'contentId INTEGER PRIMARY KEY'
|
||||||
', resumeTimeMillis INTEGER'
|
', resumeTimeMillis INTEGER'
|
||||||
')');
|
')');
|
||||||
|
@ -160,7 +173,7 @@ class LocalMediaDbUpgrader {
|
||||||
// new column `trashed`
|
// new column `trashed`
|
||||||
await db.transaction((txn) async {
|
await db.transaction((txn) async {
|
||||||
const newEntryTable = '${entryTable}TEMP';
|
const newEntryTable = '${entryTable}TEMP';
|
||||||
await db.execute('CREATE TABLE $newEntryTable('
|
await db.execute('CREATE TABLE $newEntryTable ('
|
||||||
'id INTEGER PRIMARY KEY'
|
'id INTEGER PRIMARY KEY'
|
||||||
', contentId INTEGER'
|
', contentId INTEGER'
|
||||||
', uri TEXT'
|
', uri TEXT'
|
||||||
|
@ -176,7 +189,7 @@ class LocalMediaDbUpgrader {
|
||||||
', durationMillis INTEGER'
|
', durationMillis INTEGER'
|
||||||
', trashed INTEGER DEFAULT 0'
|
', trashed INTEGER DEFAULT 0'
|
||||||
')');
|
')');
|
||||||
await db.rawInsert('INSERT INTO $newEntryTable(id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis)'
|
await db.rawInsert('INSERT INTO $newEntryTable (id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis)'
|
||||||
' SELECT contentId,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis'
|
' SELECT contentId,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis'
|
||||||
' FROM $entryTable;');
|
' FROM $entryTable;');
|
||||||
await db.execute('DROP TABLE $entryTable;');
|
await db.execute('DROP TABLE $entryTable;');
|
||||||
|
@ -186,11 +199,11 @@ class LocalMediaDbUpgrader {
|
||||||
// rename column `contentId` to `id`
|
// rename column `contentId` to `id`
|
||||||
await db.transaction((txn) async {
|
await db.transaction((txn) async {
|
||||||
const newDateTakenTable = '${dateTakenTable}TEMP';
|
const newDateTakenTable = '${dateTakenTable}TEMP';
|
||||||
await db.execute('CREATE TABLE $newDateTakenTable('
|
await db.execute('CREATE TABLE $newDateTakenTable ('
|
||||||
'id INTEGER PRIMARY KEY'
|
'id INTEGER PRIMARY KEY'
|
||||||
', dateMillis INTEGER'
|
', dateMillis INTEGER'
|
||||||
')');
|
')');
|
||||||
await db.rawInsert('INSERT INTO $newDateTakenTable(id,dateMillis)'
|
await db.rawInsert('INSERT INTO $newDateTakenTable (id,dateMillis)'
|
||||||
' SELECT contentId,dateMillis'
|
' SELECT contentId,dateMillis'
|
||||||
' FROM $dateTakenTable;');
|
' FROM $dateTakenTable;');
|
||||||
await db.execute('DROP TABLE $dateTakenTable;');
|
await db.execute('DROP TABLE $dateTakenTable;');
|
||||||
|
@ -200,7 +213,7 @@ class LocalMediaDbUpgrader {
|
||||||
// rename column `contentId` to `id`
|
// rename column `contentId` to `id`
|
||||||
await db.transaction((txn) async {
|
await db.transaction((txn) async {
|
||||||
const newMetadataTable = '${metadataTable}TEMP';
|
const newMetadataTable = '${metadataTable}TEMP';
|
||||||
await db.execute('CREATE TABLE $newMetadataTable('
|
await db.execute('CREATE TABLE $newMetadataTable ('
|
||||||
'id INTEGER PRIMARY KEY'
|
'id INTEGER PRIMARY KEY'
|
||||||
', mimeType TEXT'
|
', mimeType TEXT'
|
||||||
', dateMillis INTEGER'
|
', dateMillis INTEGER'
|
||||||
|
@ -212,7 +225,7 @@ class LocalMediaDbUpgrader {
|
||||||
', longitude REAL'
|
', longitude REAL'
|
||||||
', rating INTEGER'
|
', rating INTEGER'
|
||||||
')');
|
')');
|
||||||
await db.rawInsert('INSERT INTO $newMetadataTable(id,mimeType,dateMillis,flags,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude,rating)'
|
await db.rawInsert('INSERT INTO $newMetadataTable (id,mimeType,dateMillis,flags,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude,rating)'
|
||||||
' SELECT contentId,mimeType,dateMillis,flags,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude,rating'
|
' SELECT contentId,mimeType,dateMillis,flags,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude,rating'
|
||||||
' FROM $metadataTable;');
|
' FROM $metadataTable;');
|
||||||
await db.execute('DROP TABLE $metadataTable;');
|
await db.execute('DROP TABLE $metadataTable;');
|
||||||
|
@ -222,7 +235,7 @@ class LocalMediaDbUpgrader {
|
||||||
// rename column `contentId` to `id`
|
// rename column `contentId` to `id`
|
||||||
await db.transaction((txn) async {
|
await db.transaction((txn) async {
|
||||||
const newAddressTable = '${addressTable}TEMP';
|
const newAddressTable = '${addressTable}TEMP';
|
||||||
await db.execute('CREATE TABLE $newAddressTable('
|
await db.execute('CREATE TABLE $newAddressTable ('
|
||||||
'id INTEGER PRIMARY KEY'
|
'id INTEGER PRIMARY KEY'
|
||||||
', addressLine TEXT'
|
', addressLine TEXT'
|
||||||
', countryCode TEXT'
|
', countryCode TEXT'
|
||||||
|
@ -230,7 +243,7 @@ class LocalMediaDbUpgrader {
|
||||||
', adminArea TEXT'
|
', adminArea TEXT'
|
||||||
', locality TEXT'
|
', locality TEXT'
|
||||||
')');
|
')');
|
||||||
await db.rawInsert('INSERT INTO $newAddressTable(id,addressLine,countryCode,countryName,adminArea,locality)'
|
await db.rawInsert('INSERT INTO $newAddressTable (id,addressLine,countryCode,countryName,adminArea,locality)'
|
||||||
' SELECT contentId,addressLine,countryCode,countryName,adminArea,locality'
|
' SELECT contentId,addressLine,countryCode,countryName,adminArea,locality'
|
||||||
' FROM $addressTable;');
|
' FROM $addressTable;');
|
||||||
await db.execute('DROP TABLE $addressTable;');
|
await db.execute('DROP TABLE $addressTable;');
|
||||||
|
@ -240,11 +253,11 @@ class LocalMediaDbUpgrader {
|
||||||
// rename column `contentId` to `id`
|
// rename column `contentId` to `id`
|
||||||
await db.transaction((txn) async {
|
await db.transaction((txn) async {
|
||||||
const newVideoPlaybackTable = '${videoPlaybackTable}TEMP';
|
const newVideoPlaybackTable = '${videoPlaybackTable}TEMP';
|
||||||
await db.execute('CREATE TABLE $newVideoPlaybackTable('
|
await db.execute('CREATE TABLE $newVideoPlaybackTable ('
|
||||||
'id INTEGER PRIMARY KEY'
|
'id INTEGER PRIMARY KEY'
|
||||||
', resumeTimeMillis INTEGER'
|
', resumeTimeMillis INTEGER'
|
||||||
')');
|
')');
|
||||||
await db.rawInsert('INSERT INTO $newVideoPlaybackTable(id,resumeTimeMillis)'
|
await db.rawInsert('INSERT INTO $newVideoPlaybackTable (id,resumeTimeMillis)'
|
||||||
' SELECT contentId,resumeTimeMillis'
|
' SELECT contentId,resumeTimeMillis'
|
||||||
' FROM $videoPlaybackTable;');
|
' FROM $videoPlaybackTable;');
|
||||||
await db.execute('DROP TABLE $videoPlaybackTable;');
|
await db.execute('DROP TABLE $videoPlaybackTable;');
|
||||||
|
@ -255,10 +268,10 @@ class LocalMediaDbUpgrader {
|
||||||
// remove column `path`
|
// remove column `path`
|
||||||
await db.transaction((txn) async {
|
await db.transaction((txn) async {
|
||||||
const newFavouriteTable = '${favouriteTable}TEMP';
|
const newFavouriteTable = '${favouriteTable}TEMP';
|
||||||
await db.execute('CREATE TABLE $newFavouriteTable('
|
await db.execute('CREATE TABLE $newFavouriteTable ('
|
||||||
'id INTEGER PRIMARY KEY'
|
'id INTEGER PRIMARY KEY'
|
||||||
')');
|
')');
|
||||||
await db.rawInsert('INSERT INTO $newFavouriteTable(id)'
|
await db.rawInsert('INSERT INTO $newFavouriteTable (id)'
|
||||||
' SELECT contentId'
|
' SELECT contentId'
|
||||||
' FROM $favouriteTable;');
|
' FROM $favouriteTable;');
|
||||||
await db.execute('DROP TABLE $favouriteTable;');
|
await db.execute('DROP TABLE $favouriteTable;');
|
||||||
|
@ -268,11 +281,11 @@ class LocalMediaDbUpgrader {
|
||||||
// rename column `contentId` to `entryId`
|
// rename column `contentId` to `entryId`
|
||||||
await db.transaction((txn) async {
|
await db.transaction((txn) async {
|
||||||
const newCoverTable = '${coverTable}TEMP';
|
const newCoverTable = '${coverTable}TEMP';
|
||||||
await db.execute('CREATE TABLE $newCoverTable('
|
await db.execute('CREATE TABLE $newCoverTable ('
|
||||||
'filter TEXT PRIMARY KEY'
|
'filter TEXT PRIMARY KEY'
|
||||||
', entryId INTEGER'
|
', entryId INTEGER'
|
||||||
')');
|
')');
|
||||||
await db.rawInsert('INSERT INTO $newCoverTable(filter,entryId)'
|
await db.rawInsert('INSERT INTO $newCoverTable (filter,entryId)'
|
||||||
' SELECT filter,contentId'
|
' SELECT filter,contentId'
|
||||||
' FROM $coverTable;');
|
' FROM $coverTable;');
|
||||||
await db.execute('DROP TABLE $coverTable;');
|
await db.execute('DROP TABLE $coverTable;');
|
||||||
|
@ -280,7 +293,7 @@ class LocalMediaDbUpgrader {
|
||||||
});
|
});
|
||||||
|
|
||||||
// new table
|
// new table
|
||||||
await db.execute('CREATE TABLE $trashTable('
|
await db.execute('CREATE TABLE $trashTable ('
|
||||||
'id INTEGER PRIMARY KEY'
|
'id INTEGER PRIMARY KEY'
|
||||||
', path TEXT'
|
', path TEXT'
|
||||||
', dateMillis INTEGER'
|
', dateMillis INTEGER'
|
||||||
|
@ -299,7 +312,7 @@ class LocalMediaDbUpgrader {
|
||||||
// new column `dateAddedSecs`
|
// new column `dateAddedSecs`
|
||||||
await db.transaction((txn) async {
|
await db.transaction((txn) async {
|
||||||
const newEntryTable = '${entryTable}TEMP';
|
const newEntryTable = '${entryTable}TEMP';
|
||||||
await db.execute('CREATE TABLE $newEntryTable('
|
await db.execute('CREATE TABLE $newEntryTable ('
|
||||||
'id INTEGER PRIMARY KEY'
|
'id INTEGER PRIMARY KEY'
|
||||||
', contentId INTEGER'
|
', contentId INTEGER'
|
||||||
', uri TEXT'
|
', uri TEXT'
|
||||||
|
@ -316,7 +329,7 @@ class LocalMediaDbUpgrader {
|
||||||
', durationMillis INTEGER'
|
', durationMillis INTEGER'
|
||||||
', trashed INTEGER DEFAULT 0'
|
', trashed INTEGER DEFAULT 0'
|
||||||
')');
|
')');
|
||||||
await db.rawInsert('INSERT INTO $newEntryTable(id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis,trashed)'
|
await db.rawInsert('INSERT INTO $newEntryTable (id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis,trashed)'
|
||||||
' SELECT id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis,trashed'
|
' SELECT id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateModifiedSecs,sourceDateTakenMillis,durationMillis,trashed'
|
||||||
' FROM $entryTable;');
|
' FROM $entryTable;');
|
||||||
await db.execute('DROP TABLE $entryTable;');
|
await db.execute('DROP TABLE $entryTable;');
|
||||||
|
@ -326,7 +339,7 @@ class LocalMediaDbUpgrader {
|
||||||
// rename column `xmpTitleDescription` to `xmpTitle`
|
// rename column `xmpTitleDescription` to `xmpTitle`
|
||||||
await db.transaction((txn) async {
|
await db.transaction((txn) async {
|
||||||
const newMetadataTable = '${metadataTable}TEMP';
|
const newMetadataTable = '${metadataTable}TEMP';
|
||||||
await db.execute('CREATE TABLE $newMetadataTable('
|
await db.execute('CREATE TABLE $newMetadataTable ('
|
||||||
'id INTEGER PRIMARY KEY'
|
'id INTEGER PRIMARY KEY'
|
||||||
', mimeType TEXT'
|
', mimeType TEXT'
|
||||||
', dateMillis INTEGER'
|
', dateMillis INTEGER'
|
||||||
|
@ -338,7 +351,7 @@ class LocalMediaDbUpgrader {
|
||||||
', longitude REAL'
|
', longitude REAL'
|
||||||
', rating INTEGER'
|
', rating INTEGER'
|
||||||
')');
|
')');
|
||||||
await db.rawInsert('INSERT INTO $newMetadataTable(id,mimeType,dateMillis,flags,rotationDegrees,xmpSubjects,xmpTitle,latitude,longitude,rating)'
|
await db.rawInsert('INSERT INTO $newMetadataTable (id,mimeType,dateMillis,flags,rotationDegrees,xmpSubjects,xmpTitle,latitude,longitude,rating)'
|
||||||
' SELECT id,mimeType,dateMillis,flags,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude,rating'
|
' SELECT id,mimeType,dateMillis,flags,rotationDegrees,xmpSubjects,xmpTitleDescription,latitude,longitude,rating'
|
||||||
' FROM $metadataTable;');
|
' FROM $metadataTable;');
|
||||||
await db.execute('DROP TABLE $metadataTable;');
|
await db.execute('DROP TABLE $metadataTable;');
|
||||||
|
@ -383,7 +396,7 @@ class LocalMediaDbUpgrader {
|
||||||
|
|
||||||
await db.execute('ALTER TABLE $entryTable ADD COLUMN origin INTEGER DEFAULT 0;');
|
await db.execute('ALTER TABLE $entryTable ADD COLUMN origin INTEGER DEFAULT 0;');
|
||||||
|
|
||||||
await db.execute('CREATE TABLE $vaultTable('
|
await db.execute('CREATE TABLE $vaultTable ('
|
||||||
'name TEXT PRIMARY KEY'
|
'name TEXT PRIMARY KEY'
|
||||||
', autoLock INTEGER'
|
', autoLock INTEGER'
|
||||||
', useBin INTEGER'
|
', useBin INTEGER'
|
||||||
|
@ -394,7 +407,7 @@ class LocalMediaDbUpgrader {
|
||||||
static Future<void> _upgradeFrom11(Database db) async {
|
static Future<void> _upgradeFrom11(Database db) async {
|
||||||
debugPrint('upgrading DB from v11');
|
debugPrint('upgrading DB from v11');
|
||||||
|
|
||||||
await db.execute('CREATE TABLE $dynamicAlbumTable('
|
await db.execute('CREATE TABLE $dynamicAlbumTable ('
|
||||||
'name TEXT PRIMARY KEY'
|
'name TEXT PRIMARY KEY'
|
||||||
', filter TEXT'
|
', filter TEXT'
|
||||||
')');
|
')');
|
||||||
|
@ -423,40 +436,38 @@ class LocalMediaDbUpgrader {
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert `color` column type from value number to JSON string
|
// convert `color` column type from value number to JSON string
|
||||||
await db.transaction((txn) async {
|
const newCoverTable = '${coverTable}TEMP';
|
||||||
const newCoverTable = '${coverTable}TEMP';
|
await db.execute('CREATE TABLE $newCoverTable ('
|
||||||
await db.execute('CREATE TABLE $newCoverTable('
|
'filter TEXT PRIMARY KEY'
|
||||||
'filter TEXT PRIMARY KEY'
|
', entryId INTEGER'
|
||||||
', entryId INTEGER'
|
', packageName TEXT'
|
||||||
', packageName TEXT'
|
', color TEXT'
|
||||||
', color TEXT'
|
')');
|
||||||
')');
|
|
||||||
|
|
||||||
// insert covers with `string` color value
|
// insert covers with `string` color value
|
||||||
if (rows.isNotEmpty) {
|
if (rows.isNotEmpty) {
|
||||||
final batch = db.batch();
|
final batch = db.batch();
|
||||||
rows.forEach((row) {
|
rows.forEach((row) {
|
||||||
batch.insert(
|
batch.insert(
|
||||||
newCoverTable,
|
newCoverTable,
|
||||||
row.toMap(),
|
row.toMap(),
|
||||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
await batch.commit(noResult: true);
|
await batch.commit(noResult: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.execute('DROP TABLE $coverTable;');
|
await db.execute('DROP TABLE $coverTable;');
|
||||||
await db.execute('ALTER TABLE $newCoverTable RENAME TO $coverTable;');
|
await db.execute('ALTER TABLE $newCoverTable RENAME TO $coverTable;');
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> _upgradeFrom13(Database db) async {
|
static Future<void> _upgradeFrom13(Database db) async {
|
||||||
debugPrint('upgrading DB from v13');
|
debugPrint('upgrading DB from v13');
|
||||||
|
|
||||||
// rename column 'dateModifiedSecs' to 'dateModifiedMillis'
|
if (db.tableExists(entryTable)) {
|
||||||
await db.transaction((txn) async {
|
// rename column 'dateModifiedSecs' to 'dateModifiedMillis'
|
||||||
const newEntryTable = '${entryTable}TEMP';
|
const newEntryTable = '${entryTable}TEMP';
|
||||||
await db.execute('CREATE TABLE $newEntryTable('
|
await db.execute('CREATE TABLE $newEntryTable ('
|
||||||
'id INTEGER PRIMARY KEY'
|
'id INTEGER PRIMARY KEY'
|
||||||
', contentId INTEGER'
|
', contentId INTEGER'
|
||||||
', uri TEXT'
|
', uri TEXT'
|
||||||
|
@ -474,30 +485,24 @@ class LocalMediaDbUpgrader {
|
||||||
', trashed INTEGER DEFAULT 0'
|
', trashed INTEGER DEFAULT 0'
|
||||||
', origin INTEGER DEFAULT 0'
|
', origin INTEGER DEFAULT 0'
|
||||||
')');
|
')');
|
||||||
await db.rawInsert('INSERT INTO $newEntryTable(id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateAddedSecs,dateModifiedMillis,sourceDateTakenMillis,durationMillis,trashed,origin)'
|
await db.rawInsert('INSERT INTO $newEntryTable (id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateAddedSecs,dateModifiedMillis,sourceDateTakenMillis,durationMillis,trashed,origin)'
|
||||||
' SELECT id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateAddedSecs,dateModifiedSecs*1000,sourceDateTakenMillis,durationMillis,trashed,origin'
|
' SELECT id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateAddedSecs,dateModifiedSecs*1000,sourceDateTakenMillis,durationMillis,trashed,origin'
|
||||||
' FROM $entryTable;');
|
' FROM $entryTable;');
|
||||||
await db.execute('DROP TABLE $entryTable;');
|
await db.execute('DROP TABLE $entryTable;');
|
||||||
await db.execute('ALTER TABLE $newEntryTable RENAME TO $entryTable;');
|
await db.execute('ALTER TABLE $newEntryTable RENAME TO $entryTable;');
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> _upgradeFrom14(Database db) async {
|
static Future<void> _upgradeFrom14(Database db) async {
|
||||||
debugPrint('upgrading DB from v14');
|
debugPrint('upgrading DB from v14');
|
||||||
|
|
||||||
// no schema changes, but v1.12.4 may have corrupted the DB, so we sanitize it
|
// no schema changes, but v1.12.4 may have corrupted the DB,
|
||||||
|
// so we clear rebuildable tables
|
||||||
// clear rebuildable tables
|
final tables = [dateTakenTable, metadataTable, addressTable, trashTable, videoPlaybackTable];
|
||||||
await db.delete(dateTakenTable, where: '1');
|
await Future.forEach(tables, (table) async {
|
||||||
await db.delete(metadataTable, where: '1');
|
if (db.tableExists(table)) {
|
||||||
await db.delete(addressTable, where: '1');
|
await db.delete(table, 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]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,7 @@ class MimeTypes {
|
||||||
|
|
||||||
static const json = 'application/json';
|
static const json = 'application/json';
|
||||||
static const plainText = 'text/plain';
|
static const plainText = 'text/plain';
|
||||||
|
static const sqlite3 = 'application/vnd.sqlite3';
|
||||||
|
|
||||||
// JB2, JPC, JPX?
|
// JB2, JPC, JPX?
|
||||||
static const octetStream = 'application/octet-stream';
|
static const octetStream = 'application/octet-stream';
|
||||||
|
|
|
@ -53,6 +53,9 @@ abstract class StorageService {
|
||||||
Future<bool?> createFile(String name, String mimeType, Uint8List bytes);
|
Future<bool?> createFile(String name, String mimeType, Uint8List bytes);
|
||||||
|
|
||||||
Future<Uint8List> openFile([String? mimeType]);
|
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 {
|
class PlatformStorageService implements StorageService {
|
||||||
|
@ -369,4 +372,29 @@ class PlatformStorageService implements StorageService {
|
||||||
}
|
}
|
||||||
return Uint8List(0);
|
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();
|
State<BugReport> createState() => _BugReportState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BugReportState extends State<BugReport> with FeedbackMixin {
|
class _BugReportState extends State<BugReport> {
|
||||||
late Future<String> _infoLoader;
|
|
||||||
bool _showInstructions = false;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = context.l10n;
|
|
||||||
final animationDuration = context.select<DurationsData, Duration>((v) => v.expansionTileAnimation);
|
final animationDuration = context.select<DurationsData, Duration>((v) => v.expansionTileAnimation);
|
||||||
return ExpansionPanelList(
|
return ExpansionPanelList(
|
||||||
expansionCallback: (index, isExpanded) {
|
expansionCallback: (index, isExpanded) {
|
||||||
|
@ -66,53 +56,10 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
alignment: AlignmentDirectional.centerStart,
|
alignment: AlignmentDirectional.centerStart,
|
||||||
child: Text(l10n.aboutBugSectionTitle, style: AStyles.knownTitleText),
|
child: Text(context.l10n.aboutBugSectionTitle, style: AStyles.knownTitleText),
|
||||||
),
|
|
||||||
),
|
|
||||||
body: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_buildStep(1, l10n.aboutBugSaveLogInstruction, l10n.saveTooltip, _saveLogs),
|
|
||||||
_buildStep(2, l10n.aboutBugCopyInfoInstruction, l10n.aboutBugCopyInfoButton, _copySystemInfo),
|
|
||||||
FutureBuilder<String>(
|
|
||||||
future: _infoLoader,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
final info = snapshot.data;
|
|
||||||
if (info == null) return const SizedBox();
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Themes.secondLayerColor(context),
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
width: AvesBorder.curvedBorderWidth(context),
|
|
||||||
),
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
|
||||||
),
|
|
||||||
constraints: const BoxConstraints(maxHeight: 100),
|
|
||||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
// to show a scroll bar, we would need to provide a scroll controller
|
|
||||||
// to both the `Scrollable` and the `Scrollbar`, but
|
|
||||||
// as of Flutter v3.0.0, `SelectableText` does not allow passing the `scrollController`
|
|
||||||
child: SelectableText(
|
|
||||||
info,
|
|
||||||
textDirection: ui.TextDirection.ltr,
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
_buildStep(3, l10n.aboutBugReportInstruction, l10n.aboutBugReportButton, _goToGithub),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
body: const BugReportContent(),
|
||||||
isExpanded: _showInstructions,
|
isExpanded: _showInstructions,
|
||||||
canTapOnHeader: true,
|
canTapOnHeader: true,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
|
@ -120,6 +67,73 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
children: [
|
||||||
|
_buildStep(1, l10n.aboutBugSaveLogInstruction, l10n.saveTooltip, _saveLogs),
|
||||||
|
_buildStep(2, l10n.aboutBugCopyInfoInstruction, l10n.aboutBugCopyInfoButton, _copySystemInfo),
|
||||||
|
FutureBuilder<String>(
|
||||||
|
future: _infoLoader,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final info = snapshot.data;
|
||||||
|
if (info == null) return const SizedBox();
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Themes.secondLayerColor(context),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: AvesBorder.curvedBorderWidth(context),
|
||||||
|
),
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
|
),
|
||||||
|
constraints: const BoxConstraints(maxHeight: 100),
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
// to show a scroll bar, we would need to provide a scroll controller
|
||||||
|
// to both the `Scrollable` and the `Scrollbar`, but
|
||||||
|
// as of Flutter v3.0.0, `SelectableText` does not allow passing the `scrollController`
|
||||||
|
child: SelectableText(
|
||||||
|
info,
|
||||||
|
textDirection: ui.TextDirection.ltr,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_buildStep(3, l10n.aboutBugReportInstruction, l10n.aboutBugReportButton, _goToGithub),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildStep(int step, String text, String buttonText, VoidCallback onPressed) {
|
Widget _buildStep(int step, String text, String buttonText, VoidCallback onPressed) {
|
||||||
final isMonochrome = settings.themeColorMode == AvesThemeColorMode.monochrome;
|
final isMonochrome = settings.themeColorMode == AvesThemeColorMode.monochrome;
|
||||||
|
|
|
@ -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/media_query_data_provider.dart';
|
||||||
import 'package:aves/widgets/common/providers/viewer_entry_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/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_page_transitions.dart';
|
||||||
import 'package:aves/widgets/navigation/tv_rail.dart';
|
import 'package:aves/widgets/navigation/tv_rail.dart';
|
||||||
import 'package:aves/widgets/welcome_page.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/app_inventory.dart';
|
||||||
import 'package:aves/model/entry/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/entry/extensions/catalog.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/covered/stored_album.dart';
|
||||||
import 'package:aves/model/filters/filters.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/enums/home_page.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_lens.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/explorer/explorer_page.dart';
|
||||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||||
import 'package:aves/widgets/filter_grids/tags_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/map/map_page.dart';
|
||||||
import 'package:aves/widgets/search/search_delegate.dart';
|
import 'package:aves/widgets/search/search_delegate.dart';
|
||||||
import 'package:aves/widgets/settings/home_widget_settings_page.dart';
|
import 'package:aves/widgets/settings/home_widget_settings_page.dart';
|
||||||
|
@ -68,6 +69,7 @@ class _HomePageState extends State<HomePage> {
|
||||||
String? _initialExplorerPath;
|
String? _initialExplorerPath;
|
||||||
(LatLng, double?)? _initialLocationZoom;
|
(LatLng, double?)? _initialLocationZoom;
|
||||||
List<String>? _secureUris;
|
List<String>? _secureUris;
|
||||||
|
(Object, StackTrace)? _setupError;
|
||||||
|
|
||||||
static const allowedShortcutRoutes = [
|
static const allowedShortcutRoutes = [
|
||||||
AlbumListPage.routeName,
|
AlbumListPage.routeName,
|
||||||
|
@ -85,183 +87,195 @@ class _HomePageState extends State<HomePage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@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 {
|
Future<void> _setup() async {
|
||||||
final stopwatch = Stopwatch()..start();
|
try {
|
||||||
if (await windowService.isActivity()) {
|
final stopwatch = Stopwatch()..start();
|
||||||
// do not check whether permission was granted, because some app stores
|
if (await windowService.isActivity()) {
|
||||||
// hide in some countries apps that force quit on permission denial
|
// do not check whether permission was granted, because some app stores
|
||||||
await Permissions.mediaAccess.request();
|
// hide in some countries apps that force quit on permission denial
|
||||||
}
|
await Permissions.mediaAccess.request();
|
||||||
|
}
|
||||||
|
|
||||||
var appMode = AppMode.main;
|
var appMode = AppMode.main;
|
||||||
var error = false;
|
var error = false;
|
||||||
final intentData = widget.intentData ?? await IntentService.getIntentData();
|
final intentData = widget.intentData ?? await IntentService.getIntentData();
|
||||||
final intentAction = intentData[IntentDataKeys.action] as String?;
|
final intentAction = intentData[IntentDataKeys.action] as String?;
|
||||||
_initialFilters = null;
|
_initialFilters = null;
|
||||||
_initialExplorerPath = null;
|
_initialExplorerPath = null;
|
||||||
_secureUris = null;
|
_secureUris = null;
|
||||||
|
|
||||||
await availability.onNewIntent();
|
await availability.onNewIntent();
|
||||||
await androidFileUtils.init();
|
await androidFileUtils.init();
|
||||||
if (!{
|
if (!{
|
||||||
IntentActions.edit,
|
IntentActions.edit,
|
||||||
IntentActions.screenSaver,
|
IntentActions.screenSaver,
|
||||||
IntentActions.setWallpaper,
|
IntentActions.setWallpaper,
|
||||||
}.contains(intentAction) &&
|
}.contains(intentAction) &&
|
||||||
settings.isInstalledAppAccessAllowed) {
|
settings.isInstalledAppAccessAllowed) {
|
||||||
unawaited(appInventory.initAppNames());
|
unawaited(appInventory.initAppNames());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (intentData.values.nonNulls.isNotEmpty) {
|
if (intentData.values.nonNulls.isNotEmpty) {
|
||||||
await reportService.log('Intent data=$intentData');
|
await reportService.log('Intent data=$intentData');
|
||||||
var intentUri = intentData[IntentDataKeys.uri] as String?;
|
var intentUri = intentData[IntentDataKeys.uri] as String?;
|
||||||
final intentMimeType = intentData[IntentDataKeys.mimeType] as String?;
|
final intentMimeType = intentData[IntentDataKeys.mimeType] as String?;
|
||||||
|
|
||||||
switch (intentAction) {
|
switch (intentAction) {
|
||||||
case IntentActions.view:
|
case IntentActions.view:
|
||||||
appMode = AppMode.view;
|
appMode = AppMode.view;
|
||||||
_secureUris = (intentData[IntentDataKeys.secureUris] as List?)?.cast<String>();
|
_secureUris = (intentData[IntentDataKeys.secureUris] as List?)?.cast<String>();
|
||||||
case IntentActions.viewGeo:
|
case IntentActions.viewGeo:
|
||||||
error = true;
|
|
||||||
if (intentUri != null) {
|
|
||||||
final locationZoom = parseGeoUri(intentUri);
|
|
||||||
if (locationZoom != null) {
|
|
||||||
_initialRouteName = MapPage.routeName;
|
|
||||||
_initialLocationZoom = locationZoom;
|
|
||||||
error = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case IntentActions.edit:
|
|
||||||
appMode = AppMode.edit;
|
|
||||||
case IntentActions.setWallpaper:
|
|
||||||
appMode = AppMode.setWallpaper;
|
|
||||||
case IntentActions.pickItems:
|
|
||||||
// TODO TLAD apply pick mimetype(s)
|
|
||||||
// some apps define multiple types, separated by a space (maybe other signs too, like `,` `;`?)
|
|
||||||
final multiple = (intentData[IntentDataKeys.allowMultiple] as bool?) ?? false;
|
|
||||||
debugPrint('pick mimeType=$intentMimeType multiple=$multiple');
|
|
||||||
appMode = multiple ? AppMode.pickMultipleMediaExternal : AppMode.pickSingleMediaExternal;
|
|
||||||
case IntentActions.pickCollectionFilters:
|
|
||||||
appMode = AppMode.pickCollectionFiltersExternal;
|
|
||||||
case IntentActions.screenSaver:
|
|
||||||
appMode = AppMode.screenSaver;
|
|
||||||
_initialRouteName = ScreenSaverPage.routeName;
|
|
||||||
case IntentActions.screenSaverSettings:
|
|
||||||
_initialRouteName = ScreenSaverSettingsPage.routeName;
|
|
||||||
case IntentActions.search:
|
|
||||||
_initialRouteName = SearchPage.routeName;
|
|
||||||
_initialSearchQuery = intentData[IntentDataKeys.query] as String?;
|
|
||||||
case IntentActions.widgetSettings:
|
|
||||||
_initialRouteName = HomeWidgetSettingsPage.routeName;
|
|
||||||
_widgetId = (intentData[IntentDataKeys.widgetId] as int?) ?? 0;
|
|
||||||
case IntentActions.widgetOpen:
|
|
||||||
final widgetId = intentData[IntentDataKeys.widgetId] as int?;
|
|
||||||
if (widgetId == null) {
|
|
||||||
error = true;
|
error = true;
|
||||||
} else {
|
if (intentUri != null) {
|
||||||
// widget settings may be modified in a different process after channel setup
|
final locationZoom = parseGeoUri(intentUri);
|
||||||
await settings.reload();
|
if (locationZoom != null) {
|
||||||
final page = settings.getWidgetOpenPage(widgetId);
|
_initialRouteName = MapPage.routeName;
|
||||||
switch (page) {
|
_initialLocationZoom = locationZoom;
|
||||||
case WidgetOpenPage.collection:
|
error = false;
|
||||||
_initialFilters = settings.getWidgetCollectionFilters(widgetId);
|
}
|
||||||
case WidgetOpenPage.viewer:
|
|
||||||
appMode = AppMode.view;
|
|
||||||
intentUri = settings.getWidgetUri(widgetId);
|
|
||||||
case WidgetOpenPage.home:
|
|
||||||
case WidgetOpenPage.updateWidget:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
unawaited(WidgetService.update(widgetId));
|
break;
|
||||||
}
|
case IntentActions.edit:
|
||||||
default:
|
appMode = AppMode.edit;
|
||||||
// do not use 'route' as extra key, as the Flutter framework acts on it
|
case IntentActions.setWallpaper:
|
||||||
final extraRoute = intentData[IntentDataKeys.page] as String?;
|
appMode = AppMode.setWallpaper;
|
||||||
if (allowedShortcutRoutes.contains(extraRoute)) {
|
case IntentActions.pickItems:
|
||||||
_initialRouteName = extraRoute;
|
// TODO TLAD apply pick mimetype(s)
|
||||||
}
|
// some apps define multiple types, separated by a space (maybe other signs too, like `,` `;`?)
|
||||||
|
final multiple = (intentData[IntentDataKeys.allowMultiple] as bool?) ?? false;
|
||||||
|
debugPrint('pick mimeType=$intentMimeType multiple=$multiple');
|
||||||
|
appMode = multiple ? AppMode.pickMultipleMediaExternal : AppMode.pickSingleMediaExternal;
|
||||||
|
case IntentActions.pickCollectionFilters:
|
||||||
|
appMode = AppMode.pickCollectionFiltersExternal;
|
||||||
|
case IntentActions.screenSaver:
|
||||||
|
appMode = AppMode.screenSaver;
|
||||||
|
_initialRouteName = ScreenSaverPage.routeName;
|
||||||
|
case IntentActions.screenSaverSettings:
|
||||||
|
_initialRouteName = ScreenSaverSettingsPage.routeName;
|
||||||
|
case IntentActions.search:
|
||||||
|
_initialRouteName = SearchPage.routeName;
|
||||||
|
_initialSearchQuery = intentData[IntentDataKeys.query] as String?;
|
||||||
|
case IntentActions.widgetSettings:
|
||||||
|
_initialRouteName = HomeWidgetSettingsPage.routeName;
|
||||||
|
_widgetId = (intentData[IntentDataKeys.widgetId] as int?) ?? 0;
|
||||||
|
case IntentActions.widgetOpen:
|
||||||
|
final widgetId = intentData[IntentDataKeys.widgetId] as int?;
|
||||||
|
if (widgetId == null) {
|
||||||
|
error = true;
|
||||||
|
} else {
|
||||||
|
// widget settings may be modified in a different process after channel setup
|
||||||
|
await settings.reload();
|
||||||
|
final page = settings.getWidgetOpenPage(widgetId);
|
||||||
|
switch (page) {
|
||||||
|
case WidgetOpenPage.collection:
|
||||||
|
_initialFilters = settings.getWidgetCollectionFilters(widgetId);
|
||||||
|
case WidgetOpenPage.viewer:
|
||||||
|
appMode = AppMode.view;
|
||||||
|
intentUri = settings.getWidgetUri(widgetId);
|
||||||
|
case WidgetOpenPage.home:
|
||||||
|
case WidgetOpenPage.updateWidget:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
unawaited(WidgetService.update(widgetId));
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// do not use 'route' as extra key, as the Flutter framework acts on it
|
||||||
|
final extraRoute = intentData[IntentDataKeys.page] as String?;
|
||||||
|
if (allowedShortcutRoutes.contains(extraRoute)) {
|
||||||
|
_initialRouteName = extraRoute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_initialFilters == null) {
|
||||||
|
final extraFilters = (intentData[IntentDataKeys.filters] as List?)?.cast<String>();
|
||||||
|
_initialFilters = extraFilters?.map(CollectionFilter.fromJson).nonNulls.toSet();
|
||||||
|
}
|
||||||
|
_initialExplorerPath = intentData[IntentDataKeys.explorerPath] as String?;
|
||||||
|
|
||||||
|
switch (appMode) {
|
||||||
|
case AppMode.view:
|
||||||
|
case AppMode.edit:
|
||||||
|
case AppMode.setWallpaper:
|
||||||
|
if (intentUri != null) {
|
||||||
|
_viewerEntry = await _initViewerEntry(
|
||||||
|
uri: intentUri,
|
||||||
|
mimeType: intentMimeType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
error = _viewerEntry == null;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (_initialFilters == null) {
|
|
||||||
final extraFilters = (intentData[IntentDataKeys.filters] as List?)?.cast<String>();
|
if (error) {
|
||||||
_initialFilters = extraFilters?.map(CollectionFilter.fromJson).nonNulls.toSet();
|
debugPrint('Failed to init app mode=$appMode for intent data=$intentData. Fallback to main mode.');
|
||||||
|
appMode = AppMode.main;
|
||||||
}
|
}
|
||||||
_initialExplorerPath = intentData[IntentDataKeys.explorerPath] as String?;
|
|
||||||
|
context.read<ValueNotifier<AppMode>>().value = appMode;
|
||||||
|
unawaited(reportService.setCustomKey('app_mode', appMode.toString()));
|
||||||
|
|
||||||
switch (appMode) {
|
switch (appMode) {
|
||||||
|
case AppMode.main:
|
||||||
|
case AppMode.pickCollectionFiltersExternal:
|
||||||
|
case AppMode.pickSingleMediaExternal:
|
||||||
|
case AppMode.pickMultipleMediaExternal:
|
||||||
|
unawaited(GlobalSearch.registerCallback());
|
||||||
|
unawaited(AnalysisService.registerCallback());
|
||||||
|
final source = context.read<CollectionSource>();
|
||||||
|
if (source.loadedScope != CollectionSource.fullScope) {
|
||||||
|
await reportService.log('Initialize source to start app with mode=$appMode, loaded scope=${source.loadedScope}');
|
||||||
|
final loadTopEntriesFirst = settings.homePage == HomePageSetting.collection && settings.homeCustomCollection.isEmpty;
|
||||||
|
source.canAnalyze = true;
|
||||||
|
await source.init(scope: CollectionSource.fullScope, loadTopEntriesFirst: loadTopEntriesFirst);
|
||||||
|
}
|
||||||
|
case AppMode.screenSaver:
|
||||||
|
await reportService.log('Initialize source to start screen saver');
|
||||||
|
final source = context.read<CollectionSource>();
|
||||||
|
source.canAnalyze = false;
|
||||||
|
await source.init(scope: settings.screenSaverCollectionFilters);
|
||||||
case AppMode.view:
|
case AppMode.view:
|
||||||
|
if (_isViewerSourceable(_viewerEntry) && _secureUris == null) {
|
||||||
|
final directory = _viewerEntry?.directory;
|
||||||
|
if (directory != null) {
|
||||||
|
unawaited(AnalysisService.registerCallback());
|
||||||
|
await reportService.log('Initialize source to view item in directory $directory');
|
||||||
|
final source = context.read<CollectionSource>();
|
||||||
|
// analysis is necessary to display neighbour items when the initial item is a new one
|
||||||
|
source.canAnalyze = true;
|
||||||
|
await source.init(scope: {StoredAlbumFilter(directory, null)});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await _initViewerEssentials();
|
||||||
|
}
|
||||||
case AppMode.edit:
|
case AppMode.edit:
|
||||||
case AppMode.setWallpaper:
|
case AppMode.setWallpaper:
|
||||||
if (intentUri != null) {
|
await _initViewerEssentials();
|
||||||
_viewerEntry = await _initViewerEntry(
|
|
||||||
uri: intentUri,
|
|
||||||
mimeType: intentMimeType,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
error = _viewerEntry == null;
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debugPrint('Home setup complete in ${stopwatch.elapsed.inMilliseconds}ms');
|
||||||
|
|
||||||
|
// `pushReplacement` is not enough in some edge cases
|
||||||
|
// e.g. when opening the viewer in `view` mode should replace a viewer in `main` mode
|
||||||
|
unawaited(Navigator.maybeOf(context)?.pushAndRemoveUntil(
|
||||||
|
await _getRedirectRoute(appMode),
|
||||||
|
(route) => false,
|
||||||
|
));
|
||||||
|
} catch (error, stack) {
|
||||||
|
debugPrint('failed to setup app with error=$error\n$stack');
|
||||||
|
_setupError = (error, stack);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
|
||||||
debugPrint('Failed to init app mode=$appMode for intent data=$intentData. Fallback to main mode.');
|
|
||||||
appMode = AppMode.main;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.read<ValueNotifier<AppMode>>().value = appMode;
|
|
||||||
unawaited(reportService.setCustomKey('app_mode', appMode.toString()));
|
|
||||||
|
|
||||||
switch (appMode) {
|
|
||||||
case AppMode.main:
|
|
||||||
case AppMode.pickCollectionFiltersExternal:
|
|
||||||
case AppMode.pickSingleMediaExternal:
|
|
||||||
case AppMode.pickMultipleMediaExternal:
|
|
||||||
unawaited(GlobalSearch.registerCallback());
|
|
||||||
unawaited(AnalysisService.registerCallback());
|
|
||||||
final source = context.read<CollectionSource>();
|
|
||||||
if (source.loadedScope != CollectionSource.fullScope) {
|
|
||||||
await reportService.log('Initialize source to start app with mode=$appMode, loaded scope=${source.loadedScope}');
|
|
||||||
final loadTopEntriesFirst = settings.homePage == HomePageSetting.collection && settings.homeCustomCollection.isEmpty;
|
|
||||||
source.canAnalyze = true;
|
|
||||||
await source.init(scope: CollectionSource.fullScope, loadTopEntriesFirst: loadTopEntriesFirst);
|
|
||||||
}
|
|
||||||
case AppMode.screenSaver:
|
|
||||||
await reportService.log('Initialize source to start screen saver');
|
|
||||||
final source = context.read<CollectionSource>();
|
|
||||||
source.canAnalyze = false;
|
|
||||||
await source.init(scope: settings.screenSaverCollectionFilters);
|
|
||||||
case AppMode.view:
|
|
||||||
if (_isViewerSourceable(_viewerEntry) && _secureUris == null) {
|
|
||||||
final directory = _viewerEntry?.directory;
|
|
||||||
if (directory != null) {
|
|
||||||
unawaited(AnalysisService.registerCallback());
|
|
||||||
await reportService.log('Initialize source to view item in directory $directory');
|
|
||||||
final source = context.read<CollectionSource>();
|
|
||||||
// analysis is necessary to display neighbour items when the initial item is a new one
|
|
||||||
source.canAnalyze = true;
|
|
||||||
await source.init(scope: {StoredAlbumFilter(directory, null)});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await _initViewerEssentials();
|
|
||||||
}
|
|
||||||
case AppMode.edit:
|
|
||||||
case AppMode.setWallpaper:
|
|
||||||
await _initViewerEssentials();
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
debugPrint('Home setup complete in ${stopwatch.elapsed.inMilliseconds}ms');
|
|
||||||
|
|
||||||
// `pushReplacement` is not enough in some edge cases
|
|
||||||
// e.g. when opening the viewer in `view` mode should replace a viewer in `main` mode
|
|
||||||
unawaited(Navigator.maybeOf(context)?.pushAndRemoveUntil(
|
|
||||||
await _getRedirectRoute(appMode),
|
|
||||||
(route) => false,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initViewerEssentials() async {
|
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/countries_page.dart';
|
||||||
import 'package:aves/widgets/filter_grids/places_page.dart';
|
import 'package:aves/widgets/filter_grids/places_page.dart';
|
||||||
import 'package:aves/widgets/filter_grids/tags_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/collection_nav_tile.dart';
|
||||||
import 'package:aves/widgets/navigation/drawer/page_nav_tile.dart';
|
import 'package:aves/widgets/navigation/drawer/page_nav_tile.dart';
|
||||||
import 'package:aves/widgets/navigation/drawer/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/countries_page.dart';
|
||||||
import 'package:aves/widgets/filter_grids/places_page.dart';
|
import 'package:aves/widgets/filter_grids/places_page.dart';
|
||||||
import 'package:aves/widgets/filter_grids/tags_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:aves/widgets/settings/settings_page.dart';
|
||||||
import 'package:flutter/material.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/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_logo.dart';
|
import 'package:aves/widgets/common/identity/aves_logo.dart';
|
||||||
import 'package:aves/widgets/common/identity/buttons/outlined_button.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/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
Loading…
Reference in a new issue