Merge branch 'develop'
This commit is contained in:
commit
160544926c
30 changed files with 814 additions and 508 deletions
2
.flutter
2
.flutter
|
@ -1 +1 @@
|
||||||
Subproject commit 09de023485e95e6d1225c2baa44b8feb85e0d45f
|
Subproject commit c23637390482d4cf9598c3ce3f2be31aa7332daf
|
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -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
1
android/.gitignore
vendored
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -541,6 +541,7 @@ 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)) {
|
||||||
|
try {
|
||||||
Mp4ParserHelper.getUserDataBox(context, mimeType, uri)?.let { userDataBox ->
|
Mp4ParserHelper.getUserDataBox(context, mimeType, uri)?.let { userDataBox ->
|
||||||
Path.getPath<LocationInformationBox>(userDataBox, LocationInformationBox.TYPE)?.let { locationBox ->
|
Path.getPath<LocationInformationBox>(userDataBox, LocationInformationBox.TYPE)?.let { locationBox ->
|
||||||
if (!locationBox.isParsed) {
|
if (!locationBox.isParsed) {
|
||||||
|
@ -550,6 +551,9 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
metadataMap[KEY_LONGITUDE] = locationBox.longitude
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
4
fastlane/metadata/android/en-US/changelogs/147.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/147.txt
Normal 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
|
4
fastlane/metadata/android/en-US/changelogs/14701.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/14701.txt
Normal 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
|
|
@ -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,14 +56,30 @@ 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'
|
||||||
|
@ -76,17 +93,15 @@ class LocalMediaDbUpgrader {
|
||||||
', 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'
|
||||||
|
@ -97,13 +112,12 @@ class LocalMediaDbUpgrader {
|
||||||
', 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,10 +125,10 @@ 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'
|
||||||
|
@ -125,17 +139,16 @@ class LocalMediaDbUpgrader {
|
||||||
', 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,9 +436,8 @@ 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'
|
||||||
|
@ -447,16 +459,15 @@ class LocalMediaDbUpgrader {
|
||||||
|
|
||||||
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');
|
||||||
|
|
||||||
|
if (db.tableExists(entryTable)) {
|
||||||
// rename column 'dateModifiedSecs' to 'dateModifiedMillis'
|
// rename column 'dateModifiedSecs' to 'dateModifiedMillis'
|
||||||
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'
|
||||||
|
@ -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]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,10 +56,40 @@ 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(
|
body: const BugReportContent(),
|
||||||
|
isExpanded: _showInstructions,
|
||||||
|
canTapOnHeader: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BugReportContent extends StatefulWidget {
|
||||||
|
const BugReportContent({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BugReportContent> createState() => _BugReportContentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BugReportContentState extends State<BugReportContent> with FeedbackMixin {
|
||||||
|
late Future<String> _infoLoader;
|
||||||
|
static const bugReportUrl = '${AppReference.avesGithub}/issues/new?labels=type%3Abug&template=bug_report.md';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_infoLoader = _getInfo(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
@ -109,15 +129,9 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_buildStep(3, l10n.aboutBugReportInstruction, l10n.aboutBugReportButton, _goToGithub),
|
_buildStep(3, l10n.aboutBugReportInstruction, l10n.aboutBugReportButton, _goToGithub),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
isExpanded: _showInstructions,
|
|
||||||
canTapOnHeader: true,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ import 'package:aves/widgets/common/providers/highlight_info_provider.dart';
|
||||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
import 'package:aves/widgets/common/providers/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';
|
||||||
|
|
|
@ -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;
|
||||||
|
|
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,9 +87,17 @@ 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 {
|
||||||
|
try {
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
if (await windowService.isActivity()) {
|
if (await windowService.isActivity()) {
|
||||||
// do not check whether permission was granted, because some app stores
|
// do not check whether permission was granted, because some app stores
|
||||||
|
@ -262,6 +272,10 @@ class _HomePageState extends State<HomePage> {
|
||||||
await _getRedirectRoute(appMode),
|
await _getRedirectRoute(appMode),
|
||||||
(route) => false,
|
(route) => false,
|
||||||
));
|
));
|
||||||
|
} catch (error, stack) {
|
||||||
|
debugPrint('failed to setup app with error=$error\n$stack');
|
||||||
|
_setupError = (error, stack);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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';
|
||||||
|
|
||||||
|
|
|
@ -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,8 +228,9 @@ 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) {
|
|
||||||
|
if (context.mounted && settings.videoBackgroundMode == VideoBackgroundMode.pip) {
|
||||||
final playingController = context.read<VideoConductor>().getPlayingController();
|
final playingController = context.read<VideoConductor>().getPlayingController();
|
||||||
if (playingController != null) {
|
if (playingController != null) {
|
||||||
final entrySize = playingController.entry.displaySize;
|
final entrySize = playingController.entry.displaySize;
|
||||||
|
@ -263,7 +265,6 @@ mixin EntryViewControllerMixin<T extends StatefulWidget> on State<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
debugPrint('Cancelling picture-in-picture');
|
debugPrint('Cancelling picture-in-picture');
|
||||||
await Floating().cancelOnLeavePiP();
|
await Floating().cancelOnLeavePiP();
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
38
pubspec.lock
38
pubspec.lock
|
@ -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"
|
||||||
|
|
|
@ -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
|
@ -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
|
Loading…
Reference in a new issue