#179 allow custom app / color along cover item
This commit is contained in:
parent
7b24e25d71
commit
e980fae768
38 changed files with 976 additions and 255 deletions
|
@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Albums / Countries / Tags: allow custom app / color along cover item
|
||||||
- Info: improved GeoTIFF section
|
- Info: improved GeoTIFF section
|
||||||
- Cataloguing: locating from GeoTIFF metadata (requires rescan, limited to some projections)
|
- Cataloguing: locating from GeoTIFF metadata (requires rescan, limited to some projections)
|
||||||
- Info: action to overlay GeoTIFF on map (limited to some projections)
|
- Info: action to overlay GeoTIFF on map (limited to some projections)
|
||||||
|
|
|
@ -308,6 +308,7 @@
|
||||||
|
|
||||||
"setCoverDialogTitle": "Set Cover",
|
"setCoverDialogTitle": "Set Cover",
|
||||||
"setCoverDialogLatest": "Latest item",
|
"setCoverDialogLatest": "Latest item",
|
||||||
|
"setCoverDialogAuto": "Auto",
|
||||||
"setCoverDialogCustom": "Custom",
|
"setCoverDialogCustom": "Custom",
|
||||||
|
|
||||||
"hideFilterConfirmationDialogMessage": "Matching photos and videos will be hidden from your collection. You can show them again from the “Privacy” settings.\n\nAre you sure you want to hide them?",
|
"hideFilterConfirmationDialogMessage": "Matching photos and videos will be hidden from your collection. You can show them again from the “Privacy” settings.\n\nAre you sure you want to hide them?",
|
||||||
|
@ -399,6 +400,13 @@
|
||||||
"tileLayoutGrid": "Grid",
|
"tileLayoutGrid": "Grid",
|
||||||
"tileLayoutList": "List",
|
"tileLayoutList": "List",
|
||||||
|
|
||||||
|
"coverDialogTabCover": "Cover",
|
||||||
|
"coverDialogTabApp": "App",
|
||||||
|
"coverDialogTabColor": "Color",
|
||||||
|
|
||||||
|
"appPickDialogTitle": "Pick App",
|
||||||
|
"appPickDialogNone": "None",
|
||||||
|
|
||||||
"aboutPageTitle": "About",
|
"aboutPageTitle": "About",
|
||||||
"aboutLinkSources": "Sources",
|
"aboutLinkSources": "Sources",
|
||||||
"aboutLinkLicense": "License",
|
"aboutLinkLicense": "License",
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
|
@ -7,10 +9,22 @@ import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/painting.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
final Covers covers = Covers._private();
|
final Covers covers = Covers._private();
|
||||||
|
|
||||||
class Covers with ChangeNotifier {
|
class Covers {
|
||||||
|
final StreamController<Set<CollectionFilter>?> _entryChangeStreamController = StreamController.broadcast();
|
||||||
|
final StreamController<Set<CollectionFilter>?> _packageChangeStreamController = StreamController.broadcast();
|
||||||
|
final StreamController<Set<CollectionFilter>?> _colorChangeStreamController = StreamController.broadcast();
|
||||||
|
|
||||||
|
Stream<Set<CollectionFilter>?> get entryChangeStream => _entryChangeStreamController.stream;
|
||||||
|
|
||||||
|
Stream<Set<CollectionFilter>?> get packageChangeStream => _packageChangeStreamController.stream;
|
||||||
|
|
||||||
|
Stream<Set<CollectionFilter>?> get colorChangeStream => _colorChangeStreamController.stream;
|
||||||
|
|
||||||
Set<CoverRow> _rows = {};
|
Set<CoverRow> _rows = {};
|
||||||
|
|
||||||
Covers._private();
|
Covers._private();
|
||||||
|
@ -23,58 +37,88 @@ class Covers with ChangeNotifier {
|
||||||
|
|
||||||
Set<CoverRow> get all => Set.unmodifiable(_rows);
|
Set<CoverRow> get all => Set.unmodifiable(_rows);
|
||||||
|
|
||||||
int? coverEntryId(CollectionFilter filter) => _rows.firstWhereOrNull((row) => row.filter == filter)?.entryId;
|
Tuple3<int?, String?, Color?>? of(CollectionFilter filter) {
|
||||||
|
final row = _rows.firstWhereOrNull((row) => row.filter == filter);
|
||||||
|
return row != null ? Tuple3(row.entryId, row.packageName, row.color) : null;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> set(CollectionFilter filter, int? entryId) async {
|
Future<void> set({
|
||||||
|
required CollectionFilter filter,
|
||||||
|
required int? entryId,
|
||||||
|
required String? packageName,
|
||||||
|
required Color? color,
|
||||||
|
}) async {
|
||||||
// erase contextual properties from filters before saving them
|
// erase contextual properties from filters before saving them
|
||||||
if (filter is AlbumFilter) {
|
if (filter is AlbumFilter) {
|
||||||
filter = AlbumFilter(filter.album, null);
|
filter = AlbumFilter(filter.album, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
_rows.removeWhere((row) => row.filter == filter);
|
final oldRows = _rows.where((row) => row.filter == filter).toSet();
|
||||||
if (entryId == null) {
|
_rows.removeAll(oldRows);
|
||||||
|
final oldRow = oldRows.firstOrNull;
|
||||||
|
final oldEntry = oldRow?.entryId;
|
||||||
|
final oldPackage = oldRow?.packageName;
|
||||||
|
final oldColor = oldRow?.color;
|
||||||
|
|
||||||
|
if (entryId == null && packageName == null && color == null) {
|
||||||
await metadataDb.removeCovers({filter});
|
await metadataDb.removeCovers({filter});
|
||||||
} else {
|
} else {
|
||||||
final row = CoverRow(filter: filter, entryId: entryId);
|
final row = CoverRow(
|
||||||
|
filter: filter,
|
||||||
|
entryId: entryId,
|
||||||
|
packageName: packageName,
|
||||||
|
color: color,
|
||||||
|
);
|
||||||
_rows.add(row);
|
_rows.add(row);
|
||||||
await metadataDb.addCovers({row});
|
await metadataDb.addCovers({row});
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyListeners();
|
if (oldEntry != entryId) _entryChangeStreamController.add({filter});
|
||||||
|
if (oldPackage != packageName) _packageChangeStreamController.add({filter});
|
||||||
|
if (oldColor != color) _colorChangeStreamController.add({filter});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> moveEntry(AvesEntry entry, {required bool persist}) async {
|
Future<void> _removeEntryFromRows(Set<CoverRow> rows) {
|
||||||
|
return Future.forEach<CoverRow>(
|
||||||
|
rows,
|
||||||
|
(row) => set(
|
||||||
|
filter: row.filter,
|
||||||
|
entryId: null,
|
||||||
|
packageName: row.packageName,
|
||||||
|
color: row.color,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> moveEntry(AvesEntry entry) async {
|
||||||
final entryId = entry.id;
|
final entryId = entry.id;
|
||||||
final rows = _rows.where((row) => row.entryId == entryId).toSet();
|
await _removeEntryFromRows(_rows.where((row) => row.entryId == entryId && !row.filter.test(entry)).toSet());
|
||||||
for (final row in rows) {
|
|
||||||
final filter = row.filter;
|
|
||||||
if (!filter.test(entry)) {
|
|
||||||
_rows.remove(row);
|
|
||||||
if (persist) {
|
|
||||||
await metadataDb.removeCovers({filter});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> removeEntries(Set<AvesEntry> entries) => removeIds(entries.map((entry) => entry.id).toSet());
|
|
||||||
|
|
||||||
Future<void> removeIds(Set<int> entryIds) async {
|
Future<void> removeIds(Set<int> entryIds) async {
|
||||||
final removedRows = _rows.where((row) => entryIds.contains(row.entryId)).toSet();
|
await _removeEntryFromRows(_rows.where((row) => entryIds.contains(row.entryId)).toSet());
|
||||||
|
|
||||||
await metadataDb.removeCovers(removedRows.map((row) => row.filter).toSet());
|
|
||||||
_rows.removeAll(removedRows);
|
|
||||||
|
|
||||||
notifyListeners();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> clear() async {
|
Future<void> clear() async {
|
||||||
await metadataDb.clearCovers();
|
await metadataDb.clearCovers();
|
||||||
_rows.clear();
|
_rows.clear();
|
||||||
|
|
||||||
notifyListeners();
|
_entryChangeStreamController.add(null);
|
||||||
|
_packageChangeStreamController.add(null);
|
||||||
|
_colorChangeStreamController.add(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlbumType effectiveAlbumType(String albumPath) {
|
||||||
|
final filterPackage = of(AlbumFilter(albumPath, null))?.item2;
|
||||||
|
if (filterPackage != null) {
|
||||||
|
return filterPackage.isEmpty ? AlbumType.regular : AlbumType.app;
|
||||||
|
} else {
|
||||||
|
return androidFileUtils.getAlbumType(albumPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? effectiveAlbumPackage(String albumPath) {
|
||||||
|
final filterPackage = of(AlbumFilter(albumPath, null))?.item2;
|
||||||
|
return filterPackage ?? androidFileUtils.getAlbumAppPackageName(albumPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// import/export
|
// import/export
|
||||||
|
@ -85,16 +129,17 @@ class Covers with ChangeNotifier {
|
||||||
.map((row) {
|
.map((row) {
|
||||||
final entryId = row.entryId;
|
final entryId = row.entryId;
|
||||||
final path = visibleEntries.firstWhereOrNull((entry) => entryId == entry.id)?.path;
|
final path = visibleEntries.firstWhereOrNull((entry) => entryId == entry.id)?.path;
|
||||||
if (path == null) return null;
|
|
||||||
|
|
||||||
final volume = androidFileUtils.getStorageVolume(path)?.path;
|
final volume = androidFileUtils.getStorageVolume(path)?.path;
|
||||||
if (volume == null) return null;
|
final relativePath = volume != null ? path?.substring(volume.length) : null;
|
||||||
|
final packageName = row.packageName;
|
||||||
|
final colorValue = row.color?.value;
|
||||||
|
|
||||||
final relativePath = path.substring(volume.length);
|
|
||||||
return {
|
return {
|
||||||
'filter': row.filter.toJson(),
|
'filter': row.filter.toJson(),
|
||||||
'volume': volume,
|
if (volume != null) 'volume': volume,
|
||||||
'relativePath': relativePath,
|
if (relativePath != null) 'relativePath': relativePath,
|
||||||
|
if (packageName != null) 'packageName': packageName,
|
||||||
|
if (colorValue != null) 'color': colorValue,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.whereNotNull()
|
.whereNotNull()
|
||||||
|
@ -116,18 +161,27 @@ class Covers with ChangeNotifier {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final volume = row['volume'];
|
final volume = row['volume'] as String?;
|
||||||
final relativePath = row['relativePath'];
|
final relativePath = row['relativePath'] as String?;
|
||||||
if (volume is String && relativePath is String) {
|
final packageName = row['packageName'] as String?;
|
||||||
|
final colorValue = row['color'] as int?;
|
||||||
|
|
||||||
|
AvesEntry? entry;
|
||||||
|
if (volume != null && relativePath != null) {
|
||||||
final path = pContext.join(volume, relativePath);
|
final path = pContext.join(volume, relativePath);
|
||||||
final entry = visibleEntries.firstWhereOrNull((entry) => entry.path == path && filter.test(entry));
|
entry = visibleEntries.firstWhereOrNull((entry) => entry.path == path && filter.test(entry));
|
||||||
if (entry != null) {
|
if (entry == null) {
|
||||||
covers.set(filter, entry.id);
|
debugPrint('failed to import cover entry for path=$path, filter=$filter');
|
||||||
} else {
|
|
||||||
debugPrint('failed to import cover for path=$path, filter=$filter');
|
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
debugPrint('failed to import cover for volume=$volume, relativePath=$relativePath, filter=$filter');
|
|
||||||
|
if (entry != null || packageName != null || colorValue != null) {
|
||||||
|
covers.set(
|
||||||
|
filter: filter,
|
||||||
|
entryId: entry?.id,
|
||||||
|
packageName: packageName,
|
||||||
|
color: colorValue != null ? Color(colorValue) : null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -136,27 +190,38 @@ class Covers with ChangeNotifier {
|
||||||
@immutable
|
@immutable
|
||||||
class CoverRow extends Equatable {
|
class CoverRow extends Equatable {
|
||||||
final CollectionFilter filter;
|
final CollectionFilter filter;
|
||||||
final int entryId;
|
final int? entryId;
|
||||||
|
final String? packageName;
|
||||||
|
final Color? color;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [filter, entryId];
|
List<Object?> get props => [filter, entryId, packageName, color];
|
||||||
|
|
||||||
const CoverRow({
|
const CoverRow({
|
||||||
required this.filter,
|
required this.filter,
|
||||||
required this.entryId,
|
required this.entryId,
|
||||||
|
required this.packageName,
|
||||||
|
required this.color,
|
||||||
});
|
});
|
||||||
|
|
||||||
static CoverRow? fromMap(Map map) {
|
static CoverRow? fromMap(Map map) {
|
||||||
final filter = CollectionFilter.fromJson(map['filter']);
|
final filter = CollectionFilter.fromJson(map['filter']);
|
||||||
if (filter == null) return null;
|
if (filter == null) return null;
|
||||||
|
|
||||||
|
final colorValue = map['color'] as int?;
|
||||||
|
final color = colorValue != null ? Color(colorValue) : null;
|
||||||
return CoverRow(
|
return CoverRow(
|
||||||
filter: filter,
|
filter: filter,
|
||||||
entryId: map['entryId'],
|
entryId: map['entryId'] as int?,
|
||||||
|
packageName: map['packageName'] as String?,
|
||||||
|
color: color,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() => {
|
Map<String, dynamic> toMap() => {
|
||||||
'filter': filter.toJson(),
|
'filter': filter.toJson(),
|
||||||
'entryId': entryId,
|
'entryId': entryId,
|
||||||
|
'packageName': packageName,
|
||||||
|
'color': color?.value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,6 +85,8 @@ class SqfliteMetadataDb implements MetadataDb {
|
||||||
await db.execute('CREATE TABLE $coverTable('
|
await db.execute('CREATE TABLE $coverTable('
|
||||||
'filter TEXT PRIMARY KEY'
|
'filter TEXT PRIMARY KEY'
|
||||||
', entryId INTEGER'
|
', entryId INTEGER'
|
||||||
|
', packageName TEXT'
|
||||||
|
', color INTEGER'
|
||||||
')');
|
')');
|
||||||
await db.execute('CREATE TABLE $trashTable('
|
await db.execute('CREATE TABLE $trashTable('
|
||||||
'id INTEGER PRIMARY KEY'
|
'id INTEGER PRIMARY KEY'
|
||||||
|
@ -97,7 +99,7 @@ class SqfliteMetadataDb implements MetadataDb {
|
||||||
')');
|
')');
|
||||||
},
|
},
|
||||||
onUpgrade: MetadataDbUpgrader.upgradeDb,
|
onUpgrade: MetadataDbUpgrader.upgradeDb,
|
||||||
version: 7,
|
version: 8,
|
||||||
);
|
);
|
||||||
|
|
||||||
final maxIdRows = await _db.rawQuery('SELECT max(id) AS maxId FROM $entryTable');
|
final maxIdRows = await _db.rawQuery('SELECT max(id) AS maxId FROM $entryTable');
|
||||||
|
|
|
@ -35,6 +35,9 @@ class MetadataDbUpgrader {
|
||||||
case 6:
|
case 6:
|
||||||
await _upgradeFrom6(db);
|
await _upgradeFrom6(db);
|
||||||
break;
|
break;
|
||||||
|
case 7:
|
||||||
|
await _upgradeFrom7(db);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
oldVersion++;
|
oldVersion++;
|
||||||
}
|
}
|
||||||
|
@ -269,4 +272,10 @@ class MetadataDbUpgrader {
|
||||||
', dateMillis INTEGER'
|
', dateMillis INTEGER'
|
||||||
')');
|
')');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<void> _upgradeFrom7(Database db) async {
|
||||||
|
debugPrint('upgrading DB from v7');
|
||||||
|
await db.execute('ALTER TABLE $coverTable ADD COLUMN packageName TEXT;');
|
||||||
|
await db.execute('ALTER TABLE $coverTable ADD COLUMN color INTEGER;');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:aves/model/covers.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/theme/colors.dart';
|
import 'package:aves/theme/colors.dart';
|
||||||
|
@ -8,7 +9,7 @@ import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class AlbumFilter extends CollectionFilter {
|
class AlbumFilter extends CoveredCollectionFilter {
|
||||||
static const type = 'album';
|
static const type = 'album';
|
||||||
|
|
||||||
final String album;
|
final String album;
|
||||||
|
@ -53,10 +54,14 @@ class AlbumFilter extends CollectionFilter {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Color> color(BuildContext context) {
|
Future<Color> color(BuildContext context) {
|
||||||
final colors = context.watch<AvesColorsData>();
|
// custom color has precedence over others, even custom app color
|
||||||
|
final customColor = covers.of(this)?.item3;
|
||||||
|
if (customColor != null) return SynchronousFuture(customColor);
|
||||||
|
|
||||||
|
final colors = context.read<AvesColorsData>();
|
||||||
// do not use async/await and rely on `SynchronousFuture`
|
// do not use async/await and rely on `SynchronousFuture`
|
||||||
// to prevent rebuilding of the `FutureBuilder` listening on this future
|
// to prevent rebuilding of the `FutureBuilder` listening on this future
|
||||||
final albumType = androidFileUtils.getAlbumType(album);
|
final albumType = covers.effectiveAlbumType(album);
|
||||||
switch (albumType) {
|
switch (albumType) {
|
||||||
case AlbumType.regular:
|
case AlbumType.regular:
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -35,7 +35,7 @@ class FavouriteFilter extends CollectionFilter {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Color> color(BuildContext context) {
|
Future<Color> color(BuildContext context) {
|
||||||
final colors = context.watch<AvesColorsData>();
|
final colors = context.read<AvesColorsData>();
|
||||||
return SynchronousFuture(colors.favourite);
|
return SynchronousFuture(colors.favourite);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:aves/model/covers.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/coordinate.dart';
|
import 'package:aves/model/filters/coordinate.dart';
|
||||||
|
@ -95,7 +96,7 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
||||||
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => null;
|
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => null;
|
||||||
|
|
||||||
Future<Color> color(BuildContext context) {
|
Future<Color> color(BuildContext context) {
|
||||||
final colors = context.watch<AvesColorsData>();
|
final colors = context.read<AvesColorsData>();
|
||||||
return SynchronousFuture(colors.fromString(getLabel(context)));
|
return SynchronousFuture(colors.fromString(getLabel(context)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,6 +115,20 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
abstract class CoveredCollectionFilter extends CollectionFilter {
|
||||||
|
const CoveredCollectionFilter({bool not = false}) : super(not: not);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Color> color(BuildContext context) {
|
||||||
|
final customColor = covers.of(this)?.item3;
|
||||||
|
if (customColor != null) {
|
||||||
|
return SynchronousFuture(customColor);
|
||||||
|
}
|
||||||
|
return super.color(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class FilterGridItem<T extends CollectionFilter> with EquatableMixin {
|
class FilterGridItem<T extends CollectionFilter> with EquatableMixin {
|
||||||
final T filter;
|
final T filter;
|
||||||
|
|
|
@ -5,7 +5,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class LocationFilter extends CollectionFilter {
|
class LocationFilter extends CoveredCollectionFilter {
|
||||||
static const type = 'location';
|
static const type = 'location';
|
||||||
static const locationSeparator = ';';
|
static const locationSeparator = ';';
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,7 @@ class MimeFilter extends CollectionFilter {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Color> color(BuildContext context) {
|
Future<Color> color(BuildContext context) {
|
||||||
final colors = context.watch<AvesColorsData>();
|
final colors = context.read<AvesColorsData>();
|
||||||
switch (mime) {
|
switch (mime) {
|
||||||
case MimeTypes.anyImage:
|
case MimeTypes.anyImage:
|
||||||
return SynchronousFuture(colors.image);
|
return SynchronousFuture(colors.image);
|
||||||
|
|
|
@ -73,7 +73,7 @@ class QueryFilter extends CollectionFilter {
|
||||||
return super.color(context);
|
return super.color(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
final colors = context.watch<AvesColorsData>();
|
final colors = context.read<AvesColorsData>();
|
||||||
return SynchronousFuture(colors.neutral);
|
return SynchronousFuture(colors.neutral);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class TagFilter extends CollectionFilter {
|
class TagFilter extends CoveredCollectionFilter {
|
||||||
static const type = 'tag';
|
static const type = 'tag';
|
||||||
|
|
||||||
final String tag;
|
final String tag;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class TrashFilter extends CollectionFilter {
|
class TrashFilter extends CollectionFilter {
|
||||||
static const type = 'trash';
|
static const type = 'trash';
|
||||||
|
|
|
@ -101,7 +101,7 @@ class TypeFilter extends CollectionFilter {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Color> color(BuildContext context) {
|
Future<Color> color(BuildContext context) {
|
||||||
final colors = context.watch<AvesColorsData>();
|
final colors = context.read<AvesColorsData>();
|
||||||
switch (itemType) {
|
switch (itemType) {
|
||||||
case _animated:
|
case _animated:
|
||||||
return SynchronousFuture(colors.animated);
|
return SynchronousFuture(colors.animated);
|
||||||
|
|
|
@ -28,7 +28,7 @@ class Query extends ChangeNotifier {
|
||||||
|
|
||||||
void toggle() => enabled = !enabled;
|
void toggle() => enabled = !enabled;
|
||||||
|
|
||||||
final StreamController<bool> _enabledStreamController = StreamController<bool>.broadcast();
|
final StreamController<bool> _enabledStreamController = StreamController.broadcast();
|
||||||
|
|
||||||
Stream<bool> get enabledStream => _enabledStreamController.stream;
|
Stream<bool> get enabledStream => _enabledStreamController.stream;
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ final Settings settings = Settings._private();
|
||||||
|
|
||||||
class Settings extends ChangeNotifier {
|
class Settings extends ChangeNotifier {
|
||||||
final EventChannel _platformSettingsChangeChannel = const EventChannel('deckers.thibault/aves/settings_change');
|
final EventChannel _platformSettingsChangeChannel = const EventChannel('deckers.thibault/aves/settings_change');
|
||||||
final StreamController<SettingsChangedEvent> _updateStreamController = StreamController<SettingsChangedEvent>.broadcast();
|
final StreamController<SettingsChangedEvent> _updateStreamController = StreamController.broadcast();
|
||||||
|
|
||||||
Stream<SettingsChangedEvent> get updateStream => _updateStreamController.stream;
|
Stream<SettingsChangedEvent> get updateStream => _updateStreamController.stream;
|
||||||
|
|
||||||
|
|
|
@ -219,9 +219,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
||||||
entry.uri = 'file://${entry.trashDetails?.path}';
|
entry.uri = 'file://${entry.trashDetails?.path}';
|
||||||
}
|
}
|
||||||
|
|
||||||
await covers.moveEntry(entry, persist: persist);
|
|
||||||
|
|
||||||
if (persist) {
|
if (persist) {
|
||||||
|
await covers.moveEntry(entry);
|
||||||
final id = entry.id;
|
final id = entry.id;
|
||||||
await metadataDb.updateEntry(id, entry);
|
await metadataDb.updateEntry(id, entry);
|
||||||
await metadataDb.updateCatalogMetadata(id, entry.catalogMetadata);
|
await metadataDb.updateCatalogMetadata(id, entry.catalogMetadata);
|
||||||
|
@ -236,7 +235,15 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
||||||
|
|
||||||
final bookmark = settings.drawerAlbumBookmarks?.indexOf(sourceAlbum);
|
final bookmark = settings.drawerAlbumBookmarks?.indexOf(sourceAlbum);
|
||||||
final pinned = settings.pinnedFilters.contains(oldFilter);
|
final pinned = settings.pinnedFilters.contains(oldFilter);
|
||||||
await covers.set(newFilter, covers.coverEntryId(oldFilter));
|
|
||||||
|
final existingCover = covers.of(oldFilter);
|
||||||
|
await covers.set(
|
||||||
|
filter: newFilter,
|
||||||
|
entryId: existingCover?.item1,
|
||||||
|
packageName: existingCover?.item2,
|
||||||
|
color: existingCover?.item3,
|
||||||
|
);
|
||||||
|
|
||||||
renameNewAlbum(sourceAlbum, destinationAlbum);
|
renameNewAlbum(sourceAlbum, destinationAlbum);
|
||||||
await updateAfterMove(
|
await updateAfterMove(
|
||||||
todoEntries: entries,
|
todoEntries: entries,
|
||||||
|
@ -441,7 +448,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
||||||
}
|
}
|
||||||
|
|
||||||
AvesEntry? coverEntry(CollectionFilter filter) {
|
AvesEntry? coverEntry(CollectionFilter filter) {
|
||||||
final id = covers.coverEntryId(filter);
|
final id = covers.of(filter)?.item1;
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
final entry = visibleEntries.firstWhereOrNull((entry) => entry.id == id);
|
final entry = visibleEntries.firstWhereOrNull((entry) => entry.id == id);
|
||||||
if (entry != null) return entry;
|
if (entry != null) return entry;
|
||||||
|
|
|
@ -8,7 +8,7 @@ import 'package:tuple/tuple.dart';
|
||||||
final ServicePolicy servicePolicy = ServicePolicy._private();
|
final ServicePolicy servicePolicy = ServicePolicy._private();
|
||||||
|
|
||||||
class ServicePolicy {
|
class ServicePolicy {
|
||||||
final StreamController<QueueState> _queueStreamController = StreamController<QueueState>.broadcast();
|
final StreamController<QueueState> _queueStreamController = StreamController.broadcast();
|
||||||
final Map<Object, Tuple2<int, _Task>> _paused = {};
|
final Map<Object, Tuple2<int, _Task>> _paused = {};
|
||||||
final SplayTreeMap<int, LinkedHashMap<Object, _Task>> _queues = SplayTreeMap();
|
final SplayTreeMap<int, LinkedHashMap<Object, _Task>> _queues = SplayTreeMap();
|
||||||
final LinkedHashMap<Object, _Task> _runningQueue = LinkedHashMap();
|
final LinkedHashMap<Object, _Task> _runningQueue = LinkedHashMap();
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:aves/image_providers/app_icon_image_provider.dart';
|
import 'package:aves/image_providers/app_icon_image_provider.dart';
|
||||||
|
import 'package:aves/model/covers.dart';
|
||||||
import 'package:aves/model/settings/enums/enums.dart';
|
import 'package:aves/model/settings/enums/enums.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:palette_generator/palette_generator.dart';
|
import 'package:palette_generator/palette_generator.dart';
|
||||||
|
@ -55,7 +55,7 @@ abstract class AvesColorsData {
|
||||||
Future<Color>? appColor(String album) {
|
Future<Color>? appColor(String album) {
|
||||||
if (_appColors.containsKey(album)) return SynchronousFuture(_appColors[album]!);
|
if (_appColors.containsKey(album)) return SynchronousFuture(_appColors[album]!);
|
||||||
|
|
||||||
final packageName = androidFileUtils.getAlbumAppPackageName(album);
|
final packageName = covers.effectiveAlbumPackage(album);
|
||||||
if (packageName == null) return null;
|
if (packageName == null) return null;
|
||||||
|
|
||||||
return PaletteGenerator.fromImageProvider(
|
return PaletteGenerator.fromImageProvider(
|
||||||
|
@ -69,6 +69,8 @@ abstract class AvesColorsData {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clearAppColor(String album) => _appColors.remove(album);
|
||||||
|
|
||||||
static const Color _neutralOnDark = Colors.white;
|
static const Color _neutralOnDark = Colors.white;
|
||||||
static const Color _neutralOnLight = Color(0xAA000000);
|
static const Color _neutralOnLight = Color(0xAA000000);
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ class AIcons {
|
||||||
|
|
||||||
static const IconData accessibility = Icons.accessibility_new_outlined;
|
static const IconData accessibility = Icons.accessibility_new_outlined;
|
||||||
static const IconData android = Icons.android;
|
static const IconData android = Icons.android;
|
||||||
|
static const IconData app = Icons.apps_outlined;
|
||||||
static const IconData bin = Icons.delete_outlined;
|
static const IconData bin = Icons.delete_outlined;
|
||||||
static const IconData broken = Icons.broken_image_outlined;
|
static const IconData broken = Icons.broken_image_outlined;
|
||||||
static const IconData checked = Icons.done_outlined;
|
static const IconData checked = Icons.done_outlined;
|
||||||
|
@ -20,6 +21,7 @@ class AIcons {
|
||||||
static const IconData folder = Icons.folder_outlined;
|
static const IconData folder = Icons.folder_outlined;
|
||||||
static const IconData grid = Icons.grid_on_outlined;
|
static const IconData grid = Icons.grid_on_outlined;
|
||||||
static const IconData home = Icons.home_outlined;
|
static const IconData home = Icons.home_outlined;
|
||||||
|
static const IconData important = Icons.label_important_outline;
|
||||||
static const IconData language = Icons.translate_outlined;
|
static const IconData language = Icons.translate_outlined;
|
||||||
static const IconData location = Icons.place_outlined;
|
static const IconData location = Icons.place_outlined;
|
||||||
static const IconData locationUnlocated = Icons.location_off_outlined;
|
static const IconData locationUnlocated = Icons.location_off_outlined;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:aves/model/covers.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/model/source/section_keys.dart';
|
import 'package:aves/model/source/section_keys.dart';
|
||||||
|
@ -49,7 +50,7 @@ class AlbumSectionHeader extends StatelessWidget {
|
||||||
context: context,
|
context: context,
|
||||||
maxWidth: maxWidth,
|
maxWidth: maxWidth,
|
||||||
title: source.getAlbumDisplayName(context, directory),
|
title: source.getAlbumDisplayName(context, directory),
|
||||||
hasLeading: androidFileUtils.getAlbumType(directory) != AlbumType.regular,
|
hasLeading: covers.effectiveAlbumType(directory) != AlbumType.regular,
|
||||||
hasTrailing: androidFileUtils.isOnRemovableStorage(directory),
|
hasTrailing: androidFileUtils.isOnRemovableStorage(directory),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ class ColorListTile extends StatelessWidget {
|
||||||
final Color value;
|
final Color value;
|
||||||
final ValueSetter<Color> onChanged;
|
final ValueSetter<Color> onChanged;
|
||||||
|
|
||||||
static const radius = 16.0;
|
static const double radius = 16.0;
|
||||||
|
|
||||||
const ColorListTile({
|
const ColorListTile({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/app_mode.dart';
|
import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/actions/chip_actions.dart';
|
import 'package:aves/model/actions/chip_actions.dart';
|
||||||
|
import 'package:aves/model/covers.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/filters/location.dart';
|
import 'package:aves/model/filters/location.dart';
|
||||||
|
@ -114,6 +117,7 @@ class AvesFilterChip extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AvesFilterChipState extends State<AvesFilterChip> {
|
class _AvesFilterChipState extends State<AvesFilterChip> {
|
||||||
|
final List<StreamSubscription> _subscriptions = [];
|
||||||
late Future<Color> _colorFuture;
|
late Future<Color> _colorFuture;
|
||||||
late Color _outlineColor;
|
late Color _outlineColor;
|
||||||
late bool _tapped;
|
late bool _tapped;
|
||||||
|
@ -131,6 +135,14 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_tapped = false;
|
_tapped = false;
|
||||||
|
_subscriptions.add(covers.packageChangeStream.listen(_onCoverColorChange));
|
||||||
|
_subscriptions.add(covers.colorChangeStream.listen(_onCoverColorChange));
|
||||||
|
_subscriptions.add(settings.updateStream.where((event) => event.key == Settings.themeColorModeKey).listen((_) {
|
||||||
|
// delay so that contextual colors reflect the new settings
|
||||||
|
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||||
|
_onCoverColorChange(null);
|
||||||
|
});
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -148,6 +160,14 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_subscriptions
|
||||||
|
..forEach((sub) => sub.cancel())
|
||||||
|
..clear();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
void _initColorLoader() {
|
void _initColorLoader() {
|
||||||
// For app albums, `filter.color` yields a regular async `Future` the first time
|
// For app albums, `filter.color` yields a regular async `Future` the first time
|
||||||
// but it yields a `SynchronousFuture` when called again on a known album.
|
// but it yields a `SynchronousFuture` when called again on a known album.
|
||||||
|
@ -159,6 +179,13 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
||||||
_outlineColor = context.read<AvesColorsData>().neutral;
|
_outlineColor = context.read<AvesColorsData>().neutral;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onCoverColorChange(Set<CollectionFilter>? event) {
|
||||||
|
if (event == null || event.contains(filter)) {
|
||||||
|
_initColorLoader();
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final decoration = widget.decoration;
|
final decoration = widget.decoration;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/image_providers/app_icon_image_provider.dart';
|
import 'package:aves/image_providers/app_icon_image_provider.dart';
|
||||||
|
import 'package:aves/model/covers.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
|
@ -267,7 +268,8 @@ class IconUtils {
|
||||||
}) {
|
}) {
|
||||||
size ??= IconTheme.of(context).size;
|
size ??= IconTheme.of(context).size;
|
||||||
Widget buildIcon(IconData icon) => Icon(icon, size: size);
|
Widget buildIcon(IconData icon) => Icon(icon, size: size);
|
||||||
switch (androidFileUtils.getAlbumType(albumPath)) {
|
|
||||||
|
switch (covers.effectiveAlbumType(albumPath)) {
|
||||||
case AlbumType.camera:
|
case AlbumType.camera:
|
||||||
return buildIcon(AIcons.cameraAlbum);
|
return buildIcon(AIcons.cameraAlbum);
|
||||||
case AlbumType.screenshots:
|
case AlbumType.screenshots:
|
||||||
|
@ -278,14 +280,17 @@ class IconUtils {
|
||||||
case AlbumType.download:
|
case AlbumType.download:
|
||||||
return buildIcon(AIcons.downloadAlbum);
|
return buildIcon(AIcons.downloadAlbum);
|
||||||
case AlbumType.app:
|
case AlbumType.app:
|
||||||
return Image(
|
final package = covers.effectiveAlbumPackage(albumPath);
|
||||||
|
return package != null
|
||||||
|
? Image(
|
||||||
image: AppIconImage(
|
image: AppIconImage(
|
||||||
packageName: androidFileUtils.getAlbumAppPackageName(albumPath)!,
|
packageName: package,
|
||||||
size: size!,
|
size: size!,
|
||||||
),
|
),
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
);
|
)
|
||||||
|
: null;
|
||||||
case AlbumType.regular:
|
case AlbumType.regular:
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -39,7 +39,7 @@ class _DebugAndroidAppSectionState extends State<DebugAndroidAppSection> with Au
|
||||||
future: _loader,
|
future: _loader,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasError) return Text(snapshot.error.toString());
|
if (snapshot.hasError) return Text(snapshot.error.toString());
|
||||||
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
|
if (snapshot.connectionState != ConnectionState.done) return const SizedBox();
|
||||||
final packages = snapshot.data!.toList()..sort((a, b) => compareAsciiUpperCase(a.packageName, b.packageName));
|
final packages = snapshot.data!.toList()..sort((a, b) => compareAsciiUpperCase(a.packageName, b.packageName));
|
||||||
final enabledTheme = IconTheme.of(context);
|
final enabledTheme = IconTheme.of(context);
|
||||||
final disabledTheme = enabledTheme.merge(const IconThemeData(opacity: .2));
|
final disabledTheme = enabledTheme.merge(const IconThemeData(opacity: .2));
|
||||||
|
|
|
@ -38,7 +38,7 @@ class _AddShortcutDialogState extends State<AddShortcutDialog> {
|
||||||
if (_collection != null) {
|
if (_collection != null) {
|
||||||
final entries = _collection.sortedEntries;
|
final entries = _collection.sortedEntries;
|
||||||
if (entries.isNotEmpty) {
|
if (entries.isNotEmpty) {
|
||||||
final coverEntries = _collection.filters.map(covers.coverEntryId).whereNotNull().map((id) => entries.firstWhereOrNull((entry) => entry.id == id)).whereNotNull();
|
final coverEntries = _collection.filters.map((filter) => covers.of(filter)?.item1).whereNotNull().map((id) => entries.firstWhereOrNull((entry) => entry.id == id)).whereNotNull();
|
||||||
_coverEntry = coverEntries.firstOrNull ?? entries.first;
|
_coverEntry = coverEntries.firstOrNull ?? entries.first;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,7 +110,7 @@ class _AddShortcutDialogState extends State<AddShortcutDialog> {
|
||||||
final _collection = widget.collection;
|
final _collection = widget.collection;
|
||||||
if (_collection == null) return;
|
if (_collection == null) return;
|
||||||
|
|
||||||
final entry = await Navigator.push(
|
final entry = await Navigator.push<AvesEntry>(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
settings: const RouteSettings(name: ItemPickDialog.routeName),
|
settings: const RouteSettings(name: ItemPickDialog.routeName),
|
||||||
|
|
146
lib/widgets/dialogs/app_pick_dialog.dart
Normal file
146
lib/widgets/dialogs/app_pick_dialog.dart
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
import 'package:aves/image_providers/app_icon_image_provider.dart';
|
||||||
|
import 'package:aves/services/common/services.dart';
|
||||||
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
|
import 'package:aves/widgets/common/basic/query_bar.dart';
|
||||||
|
import 'package:aves/widgets/common/basic/reselectable_radio_list_tile.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AppPickDialog extends StatefulWidget {
|
||||||
|
static const routeName = '/app_pick';
|
||||||
|
|
||||||
|
final String? initialValue;
|
||||||
|
|
||||||
|
const AppPickDialog({
|
||||||
|
Key? key,
|
||||||
|
required this.initialValue,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppPickDialog> createState() => _AppPickDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppPickDialogState extends State<AppPickDialog> {
|
||||||
|
late String? _selectedValue;
|
||||||
|
late Future<Set<Package>> _loader;
|
||||||
|
final ValueNotifier<String> _queryNotifier = ValueNotifier('');
|
||||||
|
|
||||||
|
static const double iconSize = 32;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_selectedValue = widget.initialValue;
|
||||||
|
_loader = androidAppService.getPackages();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MediaQueryDataProvider(
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(context.l10n.appPickDialogTitle),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: FutureBuilder<Set<Package>>(
|
||||||
|
future: _loader,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasError) return Text(snapshot.error.toString());
|
||||||
|
if (snapshot.connectionState != ConnectionState.done) {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final allPackages = snapshot.data;
|
||||||
|
if (allPackages == null) return const SizedBox();
|
||||||
|
final packages = allPackages.where((package) => package.categoryLauncher).toList()..sort((a, b) => compareAsciiUpperCase(_displayName(a), _displayName(b)));
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
QueryBar(queryNotifier: _queryNotifier),
|
||||||
|
ValueListenableBuilder<String>(
|
||||||
|
valueListenable: _queryNotifier,
|
||||||
|
builder: (context, query, child) {
|
||||||
|
final visiblePackages = packages.where((package) {
|
||||||
|
return {
|
||||||
|
package.packageName,
|
||||||
|
package.currentLabel,
|
||||||
|
package.englishLabel,
|
||||||
|
...package.potentialDirs,
|
||||||
|
}.any((v) => v != null && v.toLowerCase().contains(query.toLowerCase()));
|
||||||
|
}).toList();
|
||||||
|
final showNoneOption = query.isEmpty;
|
||||||
|
final itemCount = visiblePackages.length + (showNoneOption ? 1 : 0);
|
||||||
|
return Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (showNoneOption) {
|
||||||
|
if (index == 0) {
|
||||||
|
return ReselectableRadioListTile<String?>(
|
||||||
|
value: '',
|
||||||
|
groupValue: _selectedValue,
|
||||||
|
onChanged: (v) => Navigator.pop(context, v),
|
||||||
|
reselectable: true,
|
||||||
|
title: Text(
|
||||||
|
context.l10n.appPickDialogNone,
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
index--;
|
||||||
|
}
|
||||||
|
|
||||||
|
final package = visiblePackages[index];
|
||||||
|
return ReselectableRadioListTile<String?>(
|
||||||
|
value: package.packageName,
|
||||||
|
groupValue: _selectedValue,
|
||||||
|
onChanged: (v) => Navigator.pop(context, v),
|
||||||
|
reselectable: true,
|
||||||
|
title: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
WidgetSpan(
|
||||||
|
alignment: PlaceholderAlignment.middle,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.only(end: 16),
|
||||||
|
child: Image(
|
||||||
|
image: AppIconImage(
|
||||||
|
packageName: package.packageName,
|
||||||
|
size: iconSize,
|
||||||
|
),
|
||||||
|
width: iconSize,
|
||||||
|
height: iconSize,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: _displayName(package),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: itemCount,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _displayName(Package package) => package.currentLabel ?? package.packageName;
|
||||||
|
}
|
|
@ -376,7 +376,7 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
||||||
final _collection = widget.collection;
|
final _collection = widget.collection;
|
||||||
if (_collection == null) return;
|
if (_collection == null) return;
|
||||||
|
|
||||||
final entry = await Navigator.push(
|
final entry = await Navigator.push<AvesEntry>(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
settings: const RouteSettings(name: ItemPickDialog.routeName),
|
settings: const RouteSettings(name: ItemPickDialog.routeName),
|
||||||
|
|
|
@ -1,24 +1,38 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:aves/image_providers/app_icon_image_provider.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
|
import 'package:aves/model/settings/enums/enums.dart';
|
||||||
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
|
import 'package:aves/theme/icons.dart';
|
||||||
|
import 'package:aves/widgets/common/basic/color_list_tile.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
import 'package:aves/widgets/common/fx/borders.dart';
|
||||||
|
import 'package:aves/widgets/dialogs/app_pick_dialog.dart';
|
||||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||||
import 'package:aves/widgets/dialogs/item_pick_dialog.dart';
|
import 'package:aves/widgets/dialogs/item_pick_dialog.dart';
|
||||||
import 'package:aves/widgets/dialogs/item_picker.dart';
|
import 'package:aves/widgets/dialogs/item_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
class CoverSelectionDialog extends StatefulWidget {
|
class CoverSelectionDialog extends StatefulWidget {
|
||||||
final CollectionFilter filter;
|
final CollectionFilter filter;
|
||||||
final AvesEntry? customEntry;
|
final AvesEntry? customEntry;
|
||||||
|
final String? customPackage;
|
||||||
|
final Color? customColor;
|
||||||
|
|
||||||
const CoverSelectionDialog({
|
const CoverSelectionDialog({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.filter,
|
required this.filter,
|
||||||
required this.customEntry,
|
required this.customEntry,
|
||||||
|
required this.customPackage,
|
||||||
|
required this.customColor,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -26,28 +40,170 @@ class CoverSelectionDialog extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
|
class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
|
||||||
late bool _isCustom;
|
late bool _isCustomEntry, _isCustomPackage, _isCustomColor;
|
||||||
AvesEntry? _customEntry;
|
AvesEntry? _customEntry;
|
||||||
|
String? _customPackage;
|
||||||
|
Color? _customColor;
|
||||||
|
|
||||||
CollectionFilter get filter => widget.filter;
|
CollectionFilter get filter => widget.filter;
|
||||||
|
|
||||||
|
bool get showAppTab => filter is AlbumFilter && settings.isInstalledAppAccessAllowed;
|
||||||
|
|
||||||
|
bool get showColorTab => settings.themeColorMode == AvesThemeColorMode.polychrome;
|
||||||
|
|
||||||
|
static const double itemPickerExtent = 46;
|
||||||
|
static const double appPickerExtent = 32;
|
||||||
|
static const double colorPickerRadius = 16;
|
||||||
|
|
||||||
|
double tabBarHeight(BuildContext context) => 64 * max(1, MediaQuery.textScaleFactorOf(context));
|
||||||
|
|
||||||
|
static const double tabIndicatorWeight = 2;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
_customEntry = widget.customEntry;
|
_customEntry = widget.customEntry;
|
||||||
_isCustom = _customEntry != null;
|
_isCustomEntry = _customEntry != null;
|
||||||
|
|
||||||
|
_customPackage = widget.customPackage;
|
||||||
|
_isCustomPackage = _customPackage != null;
|
||||||
|
|
||||||
|
_customColor = widget.customColor;
|
||||||
|
_isCustomColor = _customColor != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MediaQueryDataProvider(
|
|
||||||
child: Builder(
|
|
||||||
builder: (context) {
|
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
return AvesDialog(
|
final tabs = <Tuple2<Tab, Widget>>[
|
||||||
title: l10n.setCoverDialogTitle,
|
Tuple2(
|
||||||
scrollableContent: [
|
_buildTab(
|
||||||
...[false, true].map(
|
context,
|
||||||
|
const Key('tab-entry'),
|
||||||
|
AIcons.image,
|
||||||
|
l10n.coverDialogTabCover,
|
||||||
|
),
|
||||||
|
Column(children: _buildEntryOptions()),
|
||||||
|
),
|
||||||
|
if (showAppTab)
|
||||||
|
Tuple2(
|
||||||
|
_buildTab(
|
||||||
|
context,
|
||||||
|
const Key('tab-package'),
|
||||||
|
AIcons.app,
|
||||||
|
l10n.coverDialogTabApp,
|
||||||
|
),
|
||||||
|
Column(children: _buildAppOptions()),
|
||||||
|
),
|
||||||
|
if (showColorTab)
|
||||||
|
Tuple2(
|
||||||
|
_buildTab(
|
||||||
|
context,
|
||||||
|
const Key('tab-color'),
|
||||||
|
AIcons.opacity,
|
||||||
|
l10n.coverDialogTabColor,
|
||||||
|
),
|
||||||
|
Column(children: _buildColorOptions()),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
final contentWidget = DecoratedBox(
|
||||||
|
decoration: AvesDialog.contentDecoration(context),
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final availableBodyHeight = constraints.maxHeight - tabBarHeight(context) - tabIndicatorWeight;
|
||||||
|
final maxHeight = min(availableBodyHeight, tabBodyMaxHeight(context));
|
||||||
|
return DefaultTabController(
|
||||||
|
length: 1 + (showAppTab ? 1 : 0) + (showColorTab ? 1 : 0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Material(
|
||||||
|
borderRadius: const BorderRadius.vertical(
|
||||||
|
top: AvesDialog.cornerRadius,
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: TabBar(
|
||||||
|
indicatorWeight: tabIndicatorWeight,
|
||||||
|
tabs: tabs.map((t) => t.item1).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: maxHeight,
|
||||||
|
),
|
||||||
|
child: TabBarView(
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
children: tabs
|
||||||
|
.map((t) => SingleChildScrollView(
|
||||||
|
child: t.item2,
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const actionsPadding = EdgeInsets.symmetric(horizontal: 8);
|
||||||
|
const double actionsSpacing = 8.0;
|
||||||
|
final actionsWidget = Padding(
|
||||||
|
padding: actionsPadding.add(const EdgeInsets.all(actionsSpacing)),
|
||||||
|
child: OverflowBar(
|
||||||
|
alignment: MainAxisAlignment.end,
|
||||||
|
spacing: actionsSpacing,
|
||||||
|
overflowAlignment: OverflowBarAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
final entry = _isCustomEntry ? _customEntry : null;
|
||||||
|
final package = _isCustomPackage ? _customPackage : null;
|
||||||
|
final color = _isCustomColor ? _customColor : null;
|
||||||
|
return Navigator.pop(context, Tuple3<AvesEntry?, String?, Color?>(entry, package, color));
|
||||||
|
},
|
||||||
|
child: Text(l10n.applyButtonLabel),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget dialogChild = LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final availableBodyWidth = constraints.maxWidth;
|
||||||
|
final maxWidth = min(availableBodyWidth, tabBodyMaxWidth(context));
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: maxWidth,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Flexible(child: contentWidget),
|
||||||
|
actionsWidget,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return Dialog(
|
||||||
|
shape: AvesDialog.shape(context),
|
||||||
|
child: dialogChild,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildEntryOptions() {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return [false, true].map(
|
||||||
(isCustom) {
|
(isCustom) {
|
||||||
final title = Text(
|
final title = Text(
|
||||||
isCustom ? l10n.setCoverDialogCustom : l10n.setCoverDialogLatest,
|
isCustom ? l10n.setCoverDialogCustom : l10n.setCoverDialogLatest,
|
||||||
|
@ -57,14 +213,14 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
|
||||||
);
|
);
|
||||||
return RadioListTile<bool>(
|
return RadioListTile<bool>(
|
||||||
value: isCustom,
|
value: isCustom,
|
||||||
groupValue: _isCustom,
|
groupValue: _isCustomEntry,
|
||||||
onChanged: (v) {
|
onChanged: (v) {
|
||||||
if (v == null) return;
|
if (v == null) return;
|
||||||
if (v && _customEntry == null) {
|
if (v && _customEntry == null) {
|
||||||
_pickEntry();
|
_pickEntry();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_isCustom = v;
|
_isCustomEntry = v;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
title: isCustom
|
title: isCustom
|
||||||
|
@ -74,7 +230,7 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
if (_customEntry != null)
|
if (_customEntry != null)
|
||||||
ItemPicker(
|
ItemPicker(
|
||||||
extent: 46,
|
extent: itemPickerExtent,
|
||||||
entry: _customEntry!,
|
entry: _customEntry!,
|
||||||
onTap: _pickEntry,
|
onTap: _pickEntry,
|
||||||
),
|
),
|
||||||
|
@ -83,26 +239,116 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
|
||||||
: title,
|
: title,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildAppOptions() {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return [false, true].map(
|
||||||
|
(isCustom) {
|
||||||
|
final title = Text(
|
||||||
|
isCustom ? l10n.setCoverDialogCustom : l10n.setCoverDialogAuto,
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
maxLines: 1,
|
||||||
|
);
|
||||||
|
return RadioListTile<bool>(
|
||||||
|
value: isCustom,
|
||||||
|
groupValue: _isCustomPackage,
|
||||||
|
onChanged: (v) {
|
||||||
|
if (v == null) return;
|
||||||
|
if (v && _customPackage == null) {
|
||||||
|
_pickPackage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_isCustomPackage = v;
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
title: isCustom
|
||||||
|
? Row(
|
||||||
|
children: [
|
||||||
|
title,
|
||||||
|
const Spacer(),
|
||||||
|
if (_customPackage != null)
|
||||||
|
GestureDetector(
|
||||||
|
onTap: _pickPackage,
|
||||||
|
child: _customPackage!.isNotEmpty
|
||||||
|
? Image(
|
||||||
|
image: AppIconImage(
|
||||||
|
packageName: _customPackage!,
|
||||||
|
size: appPickerExtent,
|
||||||
|
),
|
||||||
|
width: appPickerExtent,
|
||||||
|
height: appPickerExtent,
|
||||||
|
)
|
||||||
|
: Container(
|
||||||
|
height: appPickerExtent,
|
||||||
|
width: appPickerExtent,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: AvesBorder.border(context),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(AIcons.clear),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
actions: [
|
)
|
||||||
TextButton(
|
: title,
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context, Tuple2<bool, AvesEntry?>(_isCustom, _customEntry)),
|
|
||||||
child: Text(l10n.applyButtonLabel),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildColorOptions() {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return [false, true].map(
|
||||||
|
(isCustom) {
|
||||||
|
final title = Text(
|
||||||
|
isCustom ? l10n.setCoverDialogCustom : l10n.setCoverDialogAuto,
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
maxLines: 1,
|
||||||
);
|
);
|
||||||
|
return RadioListTile<bool>(
|
||||||
|
value: isCustom,
|
||||||
|
groupValue: _isCustomColor,
|
||||||
|
onChanged: (v) {
|
||||||
|
if (v == null) return;
|
||||||
|
if (v && _customColor == null) {
|
||||||
|
_pickColor();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_isCustomColor = v;
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
title: isCustom
|
||||||
|
? Row(
|
||||||
|
children: [
|
||||||
|
title,
|
||||||
|
const Spacer(),
|
||||||
|
if (_customColor != null)
|
||||||
|
GestureDetector(
|
||||||
|
onTap: _pickColor,
|
||||||
|
child: Container(
|
||||||
|
height: colorPickerRadius * 2,
|
||||||
|
width: colorPickerRadius * 2,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _customColor,
|
||||||
|
border: AvesBorder.border(context),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: title,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pickEntry() async {
|
Future<void> _pickEntry() async {
|
||||||
final entry = await Navigator.push(
|
final entry = await Navigator.push<AvesEntry>(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
settings: const RouteSettings(name: ItemPickDialog.routeName),
|
settings: const RouteSettings(name: ItemPickDialog.routeName),
|
||||||
|
@ -117,8 +363,104 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
|
||||||
);
|
);
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
_customEntry = entry;
|
_customEntry = entry;
|
||||||
_isCustom = true;
|
_isCustomEntry = true;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _pickPackage() async {
|
||||||
|
final package = await Navigator.push<String>(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
settings: const RouteSettings(name: AppPickDialog.routeName),
|
||||||
|
builder: (context) => AppPickDialog(
|
||||||
|
initialValue: _customPackage,
|
||||||
|
),
|
||||||
|
fullscreenDialog: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (package != null) {
|
||||||
|
_customPackage = package;
|
||||||
|
_isCustomPackage = true;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickColor() async {
|
||||||
|
final color = await showDialog<Color>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ColorPickerDialog(
|
||||||
|
// avoid a pure material color as the default, so that
|
||||||
|
// picker controls are not on edge and palette panel is more stable
|
||||||
|
initialValue: _customColor ?? const Color(0xff3f51b5),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (color != null) {
|
||||||
|
_customColor = color;
|
||||||
|
_isCustomColor = true;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tabs
|
||||||
|
|
||||||
|
Tab _buildTab(
|
||||||
|
BuildContext context,
|
||||||
|
Key key,
|
||||||
|
IconData icon,
|
||||||
|
String text, {
|
||||||
|
Color? color,
|
||||||
|
}) {
|
||||||
|
// cannot use `IconTheme` over `TabBar` to change size,
|
||||||
|
// because `TabBar` does so internally
|
||||||
|
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
|
||||||
|
final iconSize = IconTheme.of(context).size! * textScaleFactor;
|
||||||
|
return Tab(
|
||||||
|
key: key,
|
||||||
|
height: tabBarHeight(context),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
size: iconSize,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(color: color),
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// based on `ListTile` height computation (one line, no subtitle, not dense)
|
||||||
|
double singleOptionTileHeight(BuildContext context) => 56.0 + Theme.of(context).visualDensity.baseSizeAdjustment.dy;
|
||||||
|
|
||||||
|
double tabBodyMaxWidth(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final _optionLines = {
|
||||||
|
l10n.setCoverDialogLatest,
|
||||||
|
l10n.setCoverDialogAuto,
|
||||||
|
l10n.setCoverDialogCustom,
|
||||||
|
}.fold('', (previousValue, element) => '$previousValue\n$element');
|
||||||
|
|
||||||
|
final para = RenderParagraph(
|
||||||
|
TextSpan(text: _optionLines, style: Theme.of(context).textTheme.subtitle1!),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
textScaleFactor: MediaQuery.textScaleFactorOf(context),
|
||||||
|
)..layout(const BoxConstraints(), parentUsesSize: true);
|
||||||
|
final textWidth = para.getMaxIntrinsicWidth(double.infinity);
|
||||||
|
|
||||||
|
// from `RadioListTile` layout
|
||||||
|
const contentPadding = 32;
|
||||||
|
const leadingWidth = kMinInteractiveDimension + 8;
|
||||||
|
return contentPadding + leadingWidth + textWidth + itemPickerExtent;
|
||||||
|
}
|
||||||
|
|
||||||
|
double tabBodyMaxHeight(BuildContext context) => 2 * singleOptionTileHeight(context);
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,8 @@ class _TileViewDialogState<S, G, L> extends State<TileViewDialog<S, G, L>> with
|
||||||
|
|
||||||
Map<L, String> get layoutOptions => widget.layoutOptions;
|
Map<L, String> get layoutOptions => widget.layoutOptions;
|
||||||
|
|
||||||
|
static const int groupTabIndex = 1;
|
||||||
|
|
||||||
double tabBarHeight(BuildContext context) => 64 * max(1, MediaQuery.textScaleFactorOf(context));
|
double tabBarHeight(BuildContext context) => 64 * max(1, MediaQuery.textScaleFactorOf(context));
|
||||||
|
|
||||||
static const double tabIndicatorWeight = 2;
|
static const double tabIndicatorWeight = 2;
|
||||||
|
@ -144,7 +146,6 @@ class _TileViewDialogState<S, G, L> extends State<TileViewDialog<S, G, L>> with
|
||||||
final maxHeight = min(availableBodyHeight, tabBodyMaxHeight(context));
|
final maxHeight = min(availableBodyHeight, tabBodyMaxHeight(context));
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
// crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
children: [
|
||||||
Material(
|
Material(
|
||||||
borderRadius: const BorderRadius.vertical(
|
borderRadius: const BorderRadius.vertical(
|
||||||
|
@ -225,6 +226,24 @@ class _TileViewDialogState<S, G, L> extends State<TileViewDialog<S, G, L>> with
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildRadioListTile<T>(T value, String title, T? Function() get, void Function(T value) set) {
|
||||||
|
return RadioListTile<T>(
|
||||||
|
// key is expected by test driver
|
||||||
|
key: Key(value.toString()),
|
||||||
|
value: value,
|
||||||
|
groupValue: get(),
|
||||||
|
onChanged: (v) => setState(() => set(v!)),
|
||||||
|
title: Text(
|
||||||
|
title,
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// tabs
|
||||||
|
|
||||||
Tab _buildTab(
|
Tab _buildTab(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Key key,
|
Key key,
|
||||||
|
@ -262,7 +281,7 @@ class _TileViewDialogState<S, G, L> extends State<TileViewDialog<S, G, L>> with
|
||||||
bool get canGroup => _selectedSort == EntrySortFactor.date || _selectedSort is ChipSortFactor;
|
bool get canGroup => _selectedSort == EntrySortFactor.date || _selectedSort is ChipSortFactor;
|
||||||
|
|
||||||
void _onTabChange() {
|
void _onTabChange() {
|
||||||
if (!canGroup && _tabController.index == 1) {
|
if (!canGroup && _tabController.index == groupTabIndex) {
|
||||||
_tabController.index = _tabController.previousIndex;
|
_tabController.index = _tabController.previousIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -291,20 +310,4 @@ class _TileViewDialogState<S, G, L> extends State<TileViewDialog<S, G, L>> with
|
||||||
layoutOptions,
|
layoutOptions,
|
||||||
].map((v) => v.length).fold(0, max) *
|
].map((v) => v.length).fold(0, max) *
|
||||||
singleOptionTileHeight(context);
|
singleOptionTileHeight(context);
|
||||||
|
|
||||||
Widget _buildRadioListTile<T>(T value, String title, T? Function() get, void Function(T value) set) {
|
|
||||||
return RadioListTile<T>(
|
|
||||||
// key is expected by test driver
|
|
||||||
key: Key(value.toString()),
|
|
||||||
value: value,
|
|
||||||
groupValue: get(),
|
|
||||||
onChanged: (v) => setState(() => set(v!)),
|
|
||||||
title: Text(
|
|
||||||
title,
|
|
||||||
softWrap: false,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:aves/model/covers.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
|
@ -38,7 +39,10 @@ class AlbumListPage extends StatelessWidget {
|
||||||
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final gridItems = getAlbumGridItems(context, source);
|
final gridItems = getAlbumGridItems(context, source);
|
||||||
return FilterNavigationPage<AlbumFilter>(
|
return StreamBuilder<Set<CollectionFilter>?>(
|
||||||
|
// to update sections by tier
|
||||||
|
stream: covers.packageChangeStream,
|
||||||
|
builder: (context, snapshot) => FilterNavigationPage<AlbumFilter>(
|
||||||
source: source,
|
source: source,
|
||||||
title: context.l10n.albumPageTitle,
|
title: context.l10n.albumPageTitle,
|
||||||
sortFactor: settings.albumSortFactor,
|
sortFactor: settings.albumSortFactor,
|
||||||
|
@ -50,6 +54,7 @@ class AlbumListPage extends StatelessWidget {
|
||||||
icon: AIcons.album,
|
icon: AIcons.album,
|
||||||
text: context.l10n.albumEmpty,
|
text: context.l10n.albumEmpty,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -89,7 +94,7 @@ class AlbumListPage extends StatelessWidget {
|
||||||
final appsKey = AlbumImportanceSectionKey.apps(context);
|
final appsKey = AlbumImportanceSectionKey.apps(context);
|
||||||
final regularKey = AlbumImportanceSectionKey.regular(context);
|
final regularKey = AlbumImportanceSectionKey.regular(context);
|
||||||
sections = groupBy<FilterGridItem<AlbumFilter>, ChipSectionKey>(unpinnedMapEntries, (kv) {
|
sections = groupBy<FilterGridItem<AlbumFilter>, ChipSectionKey>(unpinnedMapEntries, (kv) {
|
||||||
switch (androidFileUtils.getAlbumType(kv.filter.album)) {
|
switch (covers.effectiveAlbumType(kv.filter.album)) {
|
||||||
case AlbumType.regular:
|
case AlbumType.regular:
|
||||||
return regularKey;
|
return regularKey;
|
||||||
case AlbumType.app:
|
case AlbumType.app:
|
||||||
|
|
|
@ -2,12 +2,14 @@ import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/actions/chip_set_actions.dart';
|
import 'package:aves/model/actions/chip_set_actions.dart';
|
||||||
import 'package:aves/model/covers.dart';
|
import 'package:aves/model/covers.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/selection.dart';
|
import 'package:aves/model/selection.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';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/model/source/enums.dart';
|
import 'package:aves/model/source/enums.dart';
|
||||||
|
import 'package:aves/theme/colors.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
||||||
import 'package:aves/widgets/common/action_mixins/permission_aware.dart';
|
import 'package:aves/widgets/common/action_mixins/permission_aware.dart';
|
||||||
|
@ -277,19 +279,33 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setCover(BuildContext context, T filter) async {
|
void _setCover(BuildContext context, T filter) async {
|
||||||
final entryId = covers.coverEntryId(filter);
|
final existingCover = covers.of(filter);
|
||||||
final customEntry = context.read<CollectionSource>().visibleEntries.firstWhereOrNull((entry) => entry.id == entryId);
|
final entryId = existingCover?.item1;
|
||||||
final coverSelection = await showDialog<Tuple2<bool, AvesEntry?>>(
|
final customEntry = entryId != null ? context.read<CollectionSource>().visibleEntries.firstWhereOrNull((entry) => entry.id == entryId) : null;
|
||||||
|
final selectedCover = await showDialog<Tuple3<AvesEntry?, String?, Color?>>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => CoverSelectionDialog(
|
builder: (context) => CoverSelectionDialog(
|
||||||
filter: filter,
|
filter: filter,
|
||||||
customEntry: customEntry,
|
customEntry: customEntry,
|
||||||
|
customPackage: existingCover?.item2,
|
||||||
|
customColor: existingCover?.item3,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (coverSelection == null) return;
|
if (selectedCover == null) return;
|
||||||
|
|
||||||
final isCustom = coverSelection.item1;
|
if (filter is AlbumFilter) {
|
||||||
await covers.set(filter, isCustom ? coverSelection.item2?.id : null);
|
context.read<AvesColorsData>().clearAppColor(filter.album);
|
||||||
|
}
|
||||||
|
|
||||||
|
final selectedEntry = selectedCover.item1;
|
||||||
|
final selectedPackage = selectedCover.item2;
|
||||||
|
final selectedColor = selectedCover.item3;
|
||||||
|
await covers.set(
|
||||||
|
filter: filter,
|
||||||
|
entryId: selectedEntry?.id,
|
||||||
|
packageName: selectedPackage,
|
||||||
|
color: selectedColor,
|
||||||
|
);
|
||||||
|
|
||||||
_browse(context);
|
_browse(context);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:aves/model/covers.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
|
@ -61,7 +62,9 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<CollectionSource>(
|
return StreamBuilder<Set<CollectionFilter>?>(
|
||||||
|
stream: covers.entryChangeStream.where((event) => event == null || event.contains(filter)),
|
||||||
|
builder: (context, snapshot) => Consumer<CollectionSource>(
|
||||||
builder: (context, source, child) {
|
builder: (context, source, child) {
|
||||||
switch (T) {
|
switch (T) {
|
||||||
case AlbumFilter:
|
case AlbumFilter:
|
||||||
|
@ -92,6 +95,7 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
|
||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:aves/app_mode.dart';
|
import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/covers.dart';
|
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/highlight.dart';
|
import 'package:aves/model/highlight.dart';
|
||||||
import 'package:aves/model/selection.dart';
|
import 'package:aves/model/selection.dart';
|
||||||
|
@ -84,9 +83,7 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
||||||
child: GestureAreaProtectorStack(
|
child: GestureAreaProtectorStack(
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: AnimatedBuilder(
|
child: FilterGrid<T>(
|
||||||
animation: covers,
|
|
||||||
builder: (context, child) => FilterGrid<T>(
|
|
||||||
// key is expected by test driver
|
// key is expected by test driver
|
||||||
key: const Key('filter-grid'),
|
key: const Key('filter-grid'),
|
||||||
settingsRouteKey: settingsRouteKey,
|
settingsRouteKey: settingsRouteKey,
|
||||||
|
@ -106,7 +103,6 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
drawer: const AppDrawer(),
|
drawer: const AppDrawer(),
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
),
|
),
|
||||||
|
|
|
@ -62,9 +62,9 @@ extension ExtraAlbumImportance on AlbumImportance {
|
||||||
case AlbumImportance.pinned:
|
case AlbumImportance.pinned:
|
||||||
return AIcons.pin;
|
return AIcons.pin;
|
||||||
case AlbumImportance.special:
|
case AlbumImportance.special:
|
||||||
return Icons.label_important_outline;
|
return AIcons.important;
|
||||||
case AlbumImportance.apps:
|
case AlbumImportance.apps:
|
||||||
return Icons.apps_outlined;
|
return AIcons.app;
|
||||||
case AlbumImportance.regular:
|
case AlbumImportance.regular:
|
||||||
return AIcons.album;
|
return AIcons.album;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi
|
||||||
final AvesEntry entry;
|
final AvesEntry entry;
|
||||||
final CollectionLens? collection;
|
final CollectionLens? collection;
|
||||||
|
|
||||||
final StreamController<ActionEvent<EntryInfoAction>> _eventStreamController = StreamController<ActionEvent<EntryInfoAction>>.broadcast();
|
final StreamController<ActionEvent<EntryInfoAction>> _eventStreamController = StreamController.broadcast();
|
||||||
|
|
||||||
Stream<ActionEvent<EntryInfoAction>> get eventStream => _eventStreamController.stream;
|
Stream<ActionEvent<EntryInfoAction>> get eventStream => _eventStreamController.stream;
|
||||||
|
|
||||||
|
|
|
@ -169,15 +169,15 @@ void main() {
|
||||||
const albumFilter = AlbumFilter(testAlbum, 'whatever');
|
const albumFilter = AlbumFilter(testAlbum, 'whatever');
|
||||||
expect(albumFilter.test(image1), true);
|
expect(albumFilter.test(image1), true);
|
||||||
expect(covers.count, 0);
|
expect(covers.count, 0);
|
||||||
expect(covers.coverEntryId(albumFilter), null);
|
expect(covers.of(albumFilter), null);
|
||||||
|
|
||||||
await covers.set(albumFilter, image1.id);
|
await covers.set(filter: albumFilter, entryId: image1.id, packageName: null, color: null);
|
||||||
expect(covers.count, 1);
|
expect(covers.count, 1);
|
||||||
expect(covers.coverEntryId(albumFilter), image1.id);
|
expect(covers.of(albumFilter)?.item1, image1.id);
|
||||||
|
|
||||||
await covers.set(albumFilter, null);
|
await covers.set(filter: albumFilter, entryId: null, packageName: null, color: null);
|
||||||
expect(covers.count, 0);
|
expect(covers.count, 0);
|
||||||
expect(covers.coverEntryId(albumFilter), null);
|
expect(covers.of(albumFilter), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('favourites and covers are kept when renaming entries', () async {
|
test('favourites and covers are kept when renaming entries', () async {
|
||||||
|
@ -189,7 +189,7 @@ void main() {
|
||||||
final source = await _initSource();
|
final source = await _initSource();
|
||||||
await image1.toggleFavourite();
|
await image1.toggleFavourite();
|
||||||
const albumFilter = AlbumFilter(testAlbum, 'whatever');
|
const albumFilter = AlbumFilter(testAlbum, 'whatever');
|
||||||
await covers.set(albumFilter, image1.id);
|
await covers.set(filter: albumFilter, entryId: image1.id, packageName: null, color: null);
|
||||||
await source.updateAfterRename(
|
await source.updateAfterRename(
|
||||||
todoEntries: {image1},
|
todoEntries: {image1},
|
||||||
movedOps: {
|
movedOps: {
|
||||||
|
@ -201,7 +201,7 @@ void main() {
|
||||||
expect(favourites.count, 1);
|
expect(favourites.count, 1);
|
||||||
expect(image1.isFavourite, true);
|
expect(image1.isFavourite, true);
|
||||||
expect(covers.count, 1);
|
expect(covers.count, 1);
|
||||||
expect(covers.coverEntryId(albumFilter), image1.id);
|
expect(covers.of(albumFilter)?.item1, image1.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('favourites and covers are cleared when removing entries', () async {
|
test('favourites and covers are cleared when removing entries', () async {
|
||||||
|
@ -213,13 +213,13 @@ void main() {
|
||||||
final source = await _initSource();
|
final source = await _initSource();
|
||||||
await image1.toggleFavourite();
|
await image1.toggleFavourite();
|
||||||
final albumFilter = AlbumFilter(image1.directory!, 'whatever');
|
final albumFilter = AlbumFilter(image1.directory!, 'whatever');
|
||||||
await covers.set(albumFilter, image1.id);
|
await covers.set(filter: albumFilter, entryId: image1.id, packageName: null, color: null);
|
||||||
await source.removeEntries({image1.uri}, includeTrash: true);
|
await source.removeEntries({image1.uri}, includeTrash: true);
|
||||||
|
|
||||||
expect(source.rawAlbums.length, 0);
|
expect(source.rawAlbums.length, 0);
|
||||||
expect(favourites.count, 0);
|
expect(favourites.count, 0);
|
||||||
expect(covers.count, 0);
|
expect(covers.count, 0);
|
||||||
expect(covers.coverEntryId(albumFilter), null);
|
expect(covers.of(albumFilter), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('albums are updated when moving entries', () async {
|
test('albums are updated when moving entries', () async {
|
||||||
|
@ -284,7 +284,7 @@ void main() {
|
||||||
final source = await _initSource();
|
final source = await _initSource();
|
||||||
expect(source.rawAlbums.length, 1);
|
expect(source.rawAlbums.length, 1);
|
||||||
const sourceAlbumFilter = AlbumFilter(sourceAlbum, 'whatever');
|
const sourceAlbumFilter = AlbumFilter(sourceAlbum, 'whatever');
|
||||||
await covers.set(sourceAlbumFilter, image1.id);
|
await covers.set(filter: sourceAlbumFilter, entryId: image1.id, packageName: null, color: null);
|
||||||
|
|
||||||
await source.updateAfterMove(
|
await source.updateAfterMove(
|
||||||
todoEntries: {image1},
|
todoEntries: {image1},
|
||||||
|
@ -297,7 +297,7 @@ void main() {
|
||||||
|
|
||||||
expect(source.rawAlbums.length, 2);
|
expect(source.rawAlbums.length, 2);
|
||||||
expect(covers.count, 0);
|
expect(covers.count, 0);
|
||||||
expect(covers.coverEntryId(sourceAlbumFilter), null);
|
expect(covers.of(sourceAlbumFilter), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('favourites and covers are kept when renaming albums', () async {
|
test('favourites and covers are kept when renaming albums', () async {
|
||||||
|
@ -309,7 +309,7 @@ void main() {
|
||||||
final source = await _initSource();
|
final source = await _initSource();
|
||||||
await image1.toggleFavourite();
|
await image1.toggleFavourite();
|
||||||
var albumFilter = const AlbumFilter(sourceAlbum, 'whatever');
|
var albumFilter = const AlbumFilter(sourceAlbum, 'whatever');
|
||||||
await covers.set(albumFilter, image1.id);
|
await covers.set(filter: albumFilter, entryId: image1.id, packageName: null, color: null);
|
||||||
await source.renameAlbum(sourceAlbum, destinationAlbum, {
|
await source.renameAlbum(sourceAlbum, destinationAlbum, {
|
||||||
image1
|
image1
|
||||||
}, {
|
}, {
|
||||||
|
@ -320,7 +320,7 @@ void main() {
|
||||||
expect(favourites.count, 1);
|
expect(favourites.count, 1);
|
||||||
expect(image1.isFavourite, true);
|
expect(image1.isFavourite, true);
|
||||||
expect(covers.count, 1);
|
expect(covers.count, 1);
|
||||||
expect(covers.coverEntryId(albumFilter), image1.id);
|
expect(covers.of(albumFilter)?.item1, image1.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('unique album names', (tester) async {
|
testWidgets('unique album names', (tester) async {
|
||||||
|
|
|
@ -2,49 +2,97 @@
|
||||||
"de": [
|
"de": [
|
||||||
"entryActionShowGeoTiffOnMap",
|
"entryActionShowGeoTiffOnMap",
|
||||||
"entryActionConvertMotionPhotoToStillImage",
|
"entryActionConvertMotionPhotoToStillImage",
|
||||||
"convertMotionPhotoToStillImageWarningDialogMessage"
|
"setCoverDialogAuto",
|
||||||
|
"convertMotionPhotoToStillImageWarningDialogMessage",
|
||||||
|
"coverDialogTabCover",
|
||||||
|
"coverDialogTabApp",
|
||||||
|
"coverDialogTabColor",
|
||||||
|
"appPickDialogTitle",
|
||||||
|
"appPickDialogNone"
|
||||||
],
|
],
|
||||||
|
|
||||||
"es": [
|
"es": [
|
||||||
"entryActionShowGeoTiffOnMap",
|
"entryActionShowGeoTiffOnMap",
|
||||||
"entryActionConvertMotionPhotoToStillImage",
|
"entryActionConvertMotionPhotoToStillImage",
|
||||||
"convertMotionPhotoToStillImageWarningDialogMessage"
|
"setCoverDialogAuto",
|
||||||
|
"convertMotionPhotoToStillImageWarningDialogMessage",
|
||||||
|
"coverDialogTabCover",
|
||||||
|
"coverDialogTabApp",
|
||||||
|
"coverDialogTabColor",
|
||||||
|
"appPickDialogTitle",
|
||||||
|
"appPickDialogNone"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fr": [
|
"fr": [
|
||||||
"entryActionShowGeoTiffOnMap",
|
"entryActionShowGeoTiffOnMap",
|
||||||
"entryActionConvertMotionPhotoToStillImage",
|
"entryActionConvertMotionPhotoToStillImage",
|
||||||
"convertMotionPhotoToStillImageWarningDialogMessage"
|
"setCoverDialogAuto",
|
||||||
|
"convertMotionPhotoToStillImageWarningDialogMessage",
|
||||||
|
"coverDialogTabCover",
|
||||||
|
"coverDialogTabApp",
|
||||||
|
"coverDialogTabColor",
|
||||||
|
"appPickDialogTitle",
|
||||||
|
"appPickDialogNone"
|
||||||
],
|
],
|
||||||
|
|
||||||
"id": [
|
"id": [
|
||||||
"entryActionShowGeoTiffOnMap",
|
"entryActionShowGeoTiffOnMap",
|
||||||
"entryActionConvertMotionPhotoToStillImage",
|
"entryActionConvertMotionPhotoToStillImage",
|
||||||
"convertMotionPhotoToStillImageWarningDialogMessage"
|
"setCoverDialogAuto",
|
||||||
|
"convertMotionPhotoToStillImageWarningDialogMessage",
|
||||||
|
"coverDialogTabCover",
|
||||||
|
"coverDialogTabApp",
|
||||||
|
"coverDialogTabColor",
|
||||||
|
"appPickDialogTitle",
|
||||||
|
"appPickDialogNone"
|
||||||
],
|
],
|
||||||
|
|
||||||
"it": [
|
"it": [
|
||||||
"entryActionShowGeoTiffOnMap",
|
"entryActionShowGeoTiffOnMap",
|
||||||
"entryActionConvertMotionPhotoToStillImage",
|
"entryActionConvertMotionPhotoToStillImage",
|
||||||
"convertMotionPhotoToStillImageWarningDialogMessage"
|
"setCoverDialogAuto",
|
||||||
|
"convertMotionPhotoToStillImageWarningDialogMessage",
|
||||||
|
"coverDialogTabCover",
|
||||||
|
"coverDialogTabApp",
|
||||||
|
"coverDialogTabColor",
|
||||||
|
"appPickDialogTitle",
|
||||||
|
"appPickDialogNone"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ja": [
|
"ja": [
|
||||||
"entryActionShowGeoTiffOnMap",
|
"entryActionShowGeoTiffOnMap",
|
||||||
"entryActionConvertMotionPhotoToStillImage",
|
"entryActionConvertMotionPhotoToStillImage",
|
||||||
"convertMotionPhotoToStillImageWarningDialogMessage"
|
"setCoverDialogAuto",
|
||||||
|
"convertMotionPhotoToStillImageWarningDialogMessage",
|
||||||
|
"coverDialogTabCover",
|
||||||
|
"coverDialogTabApp",
|
||||||
|
"coverDialogTabColor",
|
||||||
|
"appPickDialogTitle",
|
||||||
|
"appPickDialogNone"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ko": [
|
"ko": [
|
||||||
"entryActionShowGeoTiffOnMap",
|
"entryActionShowGeoTiffOnMap",
|
||||||
"entryActionConvertMotionPhotoToStillImage",
|
"entryActionConvertMotionPhotoToStillImage",
|
||||||
"convertMotionPhotoToStillImageWarningDialogMessage"
|
"setCoverDialogAuto",
|
||||||
|
"convertMotionPhotoToStillImageWarningDialogMessage",
|
||||||
|
"coverDialogTabCover",
|
||||||
|
"coverDialogTabApp",
|
||||||
|
"coverDialogTabColor",
|
||||||
|
"appPickDialogTitle",
|
||||||
|
"appPickDialogNone"
|
||||||
],
|
],
|
||||||
|
|
||||||
"pt": [
|
"pt": [
|
||||||
"entryActionShowGeoTiffOnMap",
|
"entryActionShowGeoTiffOnMap",
|
||||||
"entryActionConvertMotionPhotoToStillImage",
|
"entryActionConvertMotionPhotoToStillImage",
|
||||||
"convertMotionPhotoToStillImageWarningDialogMessage"
|
"setCoverDialogAuto",
|
||||||
|
"convertMotionPhotoToStillImageWarningDialogMessage",
|
||||||
|
"coverDialogTabCover",
|
||||||
|
"coverDialogTabApp",
|
||||||
|
"coverDialogTabColor",
|
||||||
|
"appPickDialogTitle",
|
||||||
|
"appPickDialogNone"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ru": [
|
"ru": [
|
||||||
|
@ -57,6 +105,7 @@
|
||||||
"themeBrightnessBlack",
|
"themeBrightnessBlack",
|
||||||
"moveUndatedConfirmationDialogMessage",
|
"moveUndatedConfirmationDialogMessage",
|
||||||
"moveUndatedConfirmationDialogSetDate",
|
"moveUndatedConfirmationDialogSetDate",
|
||||||
|
"setCoverDialogAuto",
|
||||||
"renameEntrySetPageTitle",
|
"renameEntrySetPageTitle",
|
||||||
"renameEntrySetPagePatternFieldLabel",
|
"renameEntrySetPagePatternFieldLabel",
|
||||||
"renameEntrySetPageInsertTooltip",
|
"renameEntrySetPageInsertTooltip",
|
||||||
|
@ -65,6 +114,11 @@
|
||||||
"renameProcessorName",
|
"renameProcessorName",
|
||||||
"editEntryDateDialogCopyItem",
|
"editEntryDateDialogCopyItem",
|
||||||
"convertMotionPhotoToStillImageWarningDialogMessage",
|
"convertMotionPhotoToStillImageWarningDialogMessage",
|
||||||
|
"coverDialogTabCover",
|
||||||
|
"coverDialogTabApp",
|
||||||
|
"coverDialogTabColor",
|
||||||
|
"appPickDialogTitle",
|
||||||
|
"appPickDialogNone",
|
||||||
"collectionRenameFailureFeedback",
|
"collectionRenameFailureFeedback",
|
||||||
"collectionRenameSuccessFeedback",
|
"collectionRenameSuccessFeedback",
|
||||||
"settingsConfirmationDialogMoveUndatedItems",
|
"settingsConfirmationDialogMoveUndatedItems",
|
||||||
|
@ -77,6 +131,12 @@
|
||||||
|
|
||||||
"zh": [
|
"zh": [
|
||||||
"entryActionConvertMotionPhotoToStillImage",
|
"entryActionConvertMotionPhotoToStillImage",
|
||||||
"convertMotionPhotoToStillImageWarningDialogMessage"
|
"setCoverDialogAuto",
|
||||||
|
"convertMotionPhotoToStillImageWarningDialogMessage",
|
||||||
|
"coverDialogTabCover",
|
||||||
|
"coverDialogTabApp",
|
||||||
|
"coverDialogTabColor",
|
||||||
|
"appPickDialogTitle",
|
||||||
|
"appPickDialogNone"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue