Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2025-03-16 19:44:30 +01:00
commit 160544926c
30 changed files with 814 additions and 508 deletions

@ -1 +1 @@
Subproject commit 09de023485e95e6d1225c2baa44b8feb85e0d45f Subproject commit c23637390482d4cf9598c3ce3f2be31aa7332daf

View file

@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased] ## <a id="unreleased"></a>[Unreleased]
## <a id="v1.12.7"></a>[v1.12.7] - 2025-03-16
### 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 ## <a id="v1.12.6"></a>[v1.12.6] - 2025-03-11
### Fixed ### Fixed

1
android/.gitignore vendored
View file

@ -6,6 +6,7 @@ gradle-wrapper.jar
/local.properties /local.properties
GeneratedPluginRegistrant.java GeneratedPluginRegistrant.java
.cxx/ .cxx/
.kotlin/
# Remember to never publicly share your keystore. # Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore # See https://flutter.dev/to/reference-keystore

View file

@ -331,7 +331,7 @@
android:value="2" /> android:value="2" />
<!-- <!--
Screenshot driver scenario is not supported by Impeller: "Compressed screenshots not supported for Impeller". Screenshot driver scenario is not supported by Impeller: "Compressed screenshots not supported for Impeller".
As of Flutter v3.29.0, switching pages with alpha transition yields artifacts when Impeller is enabled. As of Flutter v3.29.2, switching pages with alpha transition yields artifacts when Impeller is enabled.
--> -->
<meta-data <meta-data
android:name="io.flutter.embedding.android.EnableImpeller" android:name="io.flutter.embedding.android.EnableImpeller"

View file

@ -541,14 +541,18 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// fallback to MP4 `loci` box for location // fallback to MP4 `loci` box for location
if (!metadataMap.contains(KEY_LATITUDE) || !metadataMap.contains(KEY_LONGITUDE)) { if (!metadataMap.contains(KEY_LATITUDE) || !metadataMap.contains(KEY_LONGITUDE)) {
Mp4ParserHelper.getUserDataBox(context, mimeType, uri)?.let { userDataBox -> try {
Path.getPath<LocationInformationBox>(userDataBox, LocationInformationBox.TYPE)?.let { locationBox -> Mp4ParserHelper.getUserDataBox(context, mimeType, uri)?.let { userDataBox ->
if (!locationBox.isParsed) { Path.getPath<LocationInformationBox>(userDataBox, LocationInformationBox.TYPE)?.let { locationBox ->
locationBox.parseDetails() if (!locationBox.isParsed) {
locationBox.parseDetails()
}
metadataMap[KEY_LATITUDE] = locationBox.latitude
metadataMap[KEY_LONGITUDE] = locationBox.longitude
} }
metadataMap[KEY_LATITUDE] = locationBox.latitude
metadataMap[KEY_LONGITUDE] = locationBox.longitude
} }
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get Location Information box by MP4 parser for mimeType=$mimeType uri=$uri", e)
} }
} }
} }

View file

@ -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

View file

@ -0,0 +1,4 @@
In v1.12.7:
- play more kinds of motion photos
- enjoy the app in Galician
Full changelog available on GitHub

View file

@ -0,0 +1,4 @@
In v1.12.7:
- play more kinds of motion photos
- enjoy the app in Galician
Full changelog available on GitHub

View file

@ -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();

View file

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

View file

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:aves/model/covers.dart'; import 'package:aves/model/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,
); );

View file

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

View file

@ -1,22 +1,23 @@
import 'dart:ui'; import '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]);
} }
} }

View file

@ -108,8 +108,9 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
return _fromMap(jsonMap); return _fromMap(jsonMap);
} }
debugPrint('failed to parse filter from json=$jsonString'); debugPrint('failed to parse filter from json=$jsonString');
} catch (error, stack) { } catch (error) {
debugPrint('failed to parse filter from json=$jsonString error=$error\n$stack'); // no need for stack
debugPrint('failed to parse filter from json=$jsonString error=$error');
} }
return null; return null;
} }

View file

@ -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';

View file

@ -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;
}
} }

View file

@ -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;

View file

@ -36,7 +36,7 @@ import 'package:aves/widgets/common/providers/highlight_info_provider.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/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';

View file

@ -626,7 +626,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
void _onQueryFocusRequest() => _queryBarFocusNode.requestFocus(); void _onQueryFocusRequest() => _queryBarFocusNode.requestFocus();
void _updateStatusBarHeight() { void _updateStatusBarHeight() {
if (!context.mounted) { if (!mounted) {
return; return;
} }
_statusBarHeight = MediaQuery.paddingOf(context).top; _statusBarHeight = MediaQuery.paddingOf(context).top;

View file

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

View file

@ -7,9 +7,9 @@ import 'package:aves/model/app/permissions.dart';
import 'package:aves/model/app_inventory.dart'; import 'package:aves/model/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 {

View file

@ -26,7 +26,7 @@ import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/countries_page.dart'; import 'package:aves/widgets/filter_grids/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';

View file

@ -12,7 +12,7 @@ import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/countries_page.dart'; import 'package:aves/widgets/filter_grids/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';

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/entry/extensions/props.dart';
@ -227,40 +228,40 @@ mixin EntryViewControllerMixin<T extends StatefulWidget> on State<T> {
static const _pipRatioMin = Rational(18, 43); static const _pipRatioMin = Rational(18, 43);
Future<void> updatePictureInPicture(BuildContext context) async { Future<void> updatePictureInPicture(BuildContext context) async {
if (context.mounted) { if (!device.supportPictureInPicture) return;
if (settings.videoBackgroundMode == VideoBackgroundMode.pip) {
final playingController = context.read<VideoConductor>().getPlayingController();
if (playingController != null) {
final entrySize = playingController.entry.displaySize;
final entryAspectRatio = entrySize.aspectRatio;
final Rational pipAspectRatio;
if (entryAspectRatio > _pipRatioMax.aspectRatio) {
pipAspectRatio = _pipRatioMax;
} else if (entryAspectRatio < _pipRatioMin.aspectRatio) {
pipAspectRatio = _pipRatioMin;
} else {
pipAspectRatio = Rational(entrySize.width.round(), entrySize.height.round());
}
final viewSize = MediaQuery.sizeOf(context) * MediaQuery.devicePixelRatioOf(context); if (context.mounted && settings.videoBackgroundMode == VideoBackgroundMode.pip) {
final fittedSize = applyBoxFit(BoxFit.contain, entrySize, viewSize).destination; final playingController = context.read<VideoConductor>().getPlayingController();
final sourceRectHint = Rectangle<int>( if (playingController != null) {
((viewSize.width - fittedSize.width) / 2).round(), final entrySize = playingController.entry.displaySize;
((viewSize.height - fittedSize.height) / 2).round(), final entryAspectRatio = entrySize.aspectRatio;
fittedSize.width.round(), final Rational pipAspectRatio;
fittedSize.height.round(), if (entryAspectRatio > _pipRatioMax.aspectRatio) {
); pipAspectRatio = _pipRatioMax;
} else if (entryAspectRatio < _pipRatioMin.aspectRatio) {
pipAspectRatio = _pipRatioMin;
} else {
pipAspectRatio = Rational(entrySize.width.round(), entrySize.height.round());
}
try { final viewSize = MediaQuery.sizeOf(context) * MediaQuery.devicePixelRatioOf(context);
final status = await Floating().enable(OnLeavePiP( final fittedSize = applyBoxFit(BoxFit.contain, entrySize, viewSize).destination;
aspectRatio: pipAspectRatio, final sourceRectHint = Rectangle<int>(
sourceRectHint: sourceRectHint, ((viewSize.width - fittedSize.width) / 2).round(),
)); ((viewSize.height - fittedSize.height) / 2).round(),
debugPrint('Enabled picture-in-picture with status=$status'); fittedSize.width.round(),
return; fittedSize.height.round(),
} on PlatformException catch (e, stack) { );
await reportService.recordError(e, stack);
} try {
final status = await Floating().enable(OnLeavePiP(
aspectRatio: pipAspectRatio,
sourceRectHint: sourceRectHint,
));
debugPrint('Enabled picture-in-picture with status=$status');
return;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
} }
} }
} }

View file

@ -14,7 +14,7 @@ extension ExtraSwipeAction on SwipeAction {
case SwipeAction.brightness: case SwipeAction.brightness:
return AvesApp.screenBrightness?.application ?? Future.value(1); return AvesApp.screenBrightness?.application ?? Future.value(1);
case SwipeAction.volume: case SwipeAction.volume:
return VolumeController().getVolume(); return VolumeController.instance.getVolume();
} }
} }
@ -23,7 +23,8 @@ extension ExtraSwipeAction on SwipeAction {
case SwipeAction.brightness: case SwipeAction.brightness:
await AvesApp.screenBrightness?.setApplicationScreenBrightness(value); await AvesApp.screenBrightness?.setApplicationScreenBrightness(value);
case SwipeAction.volume: case SwipeAction.volume:
VolumeController().setVolume(value, showSystemUI: false); VolumeController.instance.showSystemUI = false;
await VolumeController.instance.setVolume(value);
} }
} }
} }

View file

@ -11,7 +11,7 @@ import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/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';

View file

@ -37,10 +37,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: args name: args
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.6.0" version: "2.7.0"
async: async:
dependency: transitive dependency: transitive
description: description:
@ -567,10 +567,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: google_maps_flutter_web name: google_maps_flutter_web
sha256: a9fd5356d46f54744ced1ebedbbf212f3d2cb71e95d79b1d08690c1ec33dc584 sha256: bbeb93807a34bfeebdb7ace506bd2bc400a3915dc96736254fea721eb264caa0
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.10+1" version: "0.5.11"
gpx: gpx:
dependency: "direct main" dependency: "direct main"
description: description:
@ -791,8 +791,8 @@ packages:
dependency: "direct overridden" dependency: "direct overridden"
description: description:
path: media_kit path: media_kit
ref: "4d8c634c28d439384aab40b9d2edff83077f37c9" ref: d2145a50f68394096845915a28874341fbf5b3fe
resolved-ref: "4d8c634c28d439384aab40b9d2edff83077f37c9" resolved-ref: d2145a50f68394096845915a28874341fbf5b3fe
url: "https://github.com/media-kit/media-kit.git" url: "https://github.com/media-kit/media-kit.git"
source: git source: git
version: "1.1.11" version: "1.1.11"
@ -808,8 +808,8 @@ packages:
dependency: "direct overridden" dependency: "direct overridden"
description: description:
path: media_kit_video path: media_kit_video
ref: "4d8c634c28d439384aab40b9d2edff83077f37c9" ref: d2145a50f68394096845915a28874341fbf5b3fe
resolved-ref: "4d8c634c28d439384aab40b9d2edff83077f37c9" resolved-ref: d2145a50f68394096845915a28874341fbf5b3fe
url: "https://github.com/media-kit/media-kit.git" url: "https://github.com/media-kit/media-kit.git"
source: git source: git
version: "1.2.5" version: "1.2.5"
@ -898,10 +898,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: package_config name: package_config
sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.2.0"
package_info_plus: package_info_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1219,18 +1219,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: screen_brightness name: screen_brightness
sha256: "99b898dae860ebe55fc872d8e300c6eafff3ee4ccb09301b90adb3f241f29874" sha256: eca7bd9d2c3c688bcad14855361cab7097839400b6b4a56f62b7ae511c709958
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
screen_brightness_android: screen_brightness_android:
dependency: transitive dependency: transitive
description: description:
name: screen_brightness_android name: screen_brightness_android
sha256: ff9141bed547db02233e7dd88f990ab01973a0c8a8c04ddb855c7b072f33409a sha256: "6ba1b5812f66c64e9e4892be2d36ecd34210f4e0da8bdec6a2ea34f1aa42683e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "2.1.1"
screen_brightness_ios: screen_brightness_ios:
dependency: transitive dependency: transitive
description: description:
@ -1673,10 +1673,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: volume_controller name: volume_controller
sha256: c71d4c62631305df63b72da79089e078af2659649301807fa746088f365cb48e sha256: "30863a51338db47fe16f92902b1a6c4ee5e15c9287b46573d7c2eb6be1f197d2"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.8" version: "3.3.1"
wakelock_plus: wakelock_plus:
dependency: transitive dependency: transitive
description: description:
@ -1745,10 +1745,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: b89e6e24d1454e149ab20fbb225af58660f0c0bf4475544650700d8e2da54aef sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.11.0" version: "5.12.0"
win32_registry: win32_registry:
dependency: transitive dependency: transitive
description: description:
@ -1791,4 +1791,4 @@ packages:
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.7.0 <4.0.0" dart: ">=3.7.0 <4.0.0"
flutter: ">=3.29.1" flutter: ">=3.29.2"

View file

@ -7,13 +7,13 @@ repository: https://github.com/deckerst/aves
# - play changelog: /whatsnew/whatsnew-en-US # - play changelog: /whatsnew/whatsnew-en-US
# - izzy changelog: /fastlane/metadata/android/en-US/changelogs/XXX01.txt # - izzy changelog: /fastlane/metadata/android/en-US/changelogs/XXX01.txt
# - libre changelog: /fastlane/metadata/android/en-US/changelogs/XXX.txt # - libre changelog: /fastlane/metadata/android/en-US/changelogs/XXX.txt
version: 1.12.6+146 version: 1.12.7+147
publish_to: none publish_to: none
environment: environment:
# this project bundles Flutter SDK via `flutter_wrapper` # this project bundles Flutter SDK via `flutter_wrapper`
# cf https://github.com/passsy/flutter_wrapper # cf https://github.com/passsy/flutter_wrapper
flutter: 3.29.1 flutter: 3.29.2
sdk: ">=3.6.0 <4.0.0" # incoherent dartfmt from 3.7.0 sdk: ">=3.6.0 <4.0.0" # incoherent dartfmt from 3.7.0
workspace: workspace:
- plugins/aves_magnifier - plugins/aves_magnifier
@ -134,12 +134,12 @@ dependency_overrides:
media_kit: media_kit:
git: git:
url: https://github.com/media-kit/media-kit.git url: https://github.com/media-kit/media-kit.git
ref: 4d8c634c28d439384aab40b9d2edff83077f37c9 ref: d2145a50f68394096845915a28874341fbf5b3fe
path: media_kit path: media_kit
media_kit_video: media_kit_video:
git: git:
url: https://github.com/media-kit/media-kit.git url: https://github.com/media-kit/media-kit.git
ref: 4d8c634c28d439384aab40b9d2edff83077f37c9 ref: d2145a50f68394096845915a28874341fbf5b3fe
path: media_kit_video path: media_kit_video
dev_dependencies: dev_dependencies:

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,4 @@
In v1.12.6: In v1.12.7:
- play more kinds of motion photos - play more kinds of motion photos
- enjoy the app in Galician - enjoy the app in Galician
Full changelog available on GitHub Full changelog available on GitHub