#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
|
||||
|
||||
- Albums / Countries / Tags: allow custom app / color along cover item
|
||||
- Info: improved GeoTIFF section
|
||||
- Cataloguing: locating from GeoTIFF metadata (requires rescan, limited to some projections)
|
||||
- Info: action to overlay GeoTIFF on map (limited to some projections)
|
||||
|
|
|
@ -308,6 +308,7 @@
|
|||
|
||||
"setCoverDialogTitle": "Set Cover",
|
||||
"setCoverDialogLatest": "Latest item",
|
||||
"setCoverDialogAuto": "Auto",
|
||||
"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?",
|
||||
|
@ -399,6 +400,13 @@
|
|||
"tileLayoutGrid": "Grid",
|
||||
"tileLayoutList": "List",
|
||||
|
||||
"coverDialogTabCover": "Cover",
|
||||
"coverDialogTabApp": "App",
|
||||
"coverDialogTabColor": "Color",
|
||||
|
||||
"appPickDialogTitle": "Pick App",
|
||||
"appPickDialogNone": "None",
|
||||
|
||||
"aboutPageTitle": "About",
|
||||
"aboutLinkSources": "Sources",
|
||||
"aboutLinkLicense": "License",
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/album.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:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
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 = {};
|
||||
|
||||
Covers._private();
|
||||
|
@ -23,58 +37,88 @@ class Covers with ChangeNotifier {
|
|||
|
||||
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
|
||||
if (filter is AlbumFilter) {
|
||||
filter = AlbumFilter(filter.album, null);
|
||||
}
|
||||
|
||||
_rows.removeWhere((row) => row.filter == filter);
|
||||
if (entryId == null) {
|
||||
final oldRows = _rows.where((row) => row.filter == filter).toSet();
|
||||
_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});
|
||||
} else {
|
||||
final row = CoverRow(filter: filter, entryId: entryId);
|
||||
final row = CoverRow(
|
||||
filter: filter,
|
||||
entryId: entryId,
|
||||
packageName: packageName,
|
||||
color: color,
|
||||
);
|
||||
_rows.add(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 rows = _rows.where((row) => row.entryId == entryId).toSet();
|
||||
for (final row in rows) {
|
||||
final filter = row.filter;
|
||||
if (!filter.test(entry)) {
|
||||
_rows.remove(row);
|
||||
if (persist) {
|
||||
await metadataDb.removeCovers({filter});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
await _removeEntryFromRows(_rows.where((row) => row.entryId == entryId && !row.filter.test(entry)).toSet());
|
||||
}
|
||||
|
||||
Future<void> removeEntries(Set<AvesEntry> entries) => removeIds(entries.map((entry) => entry.id).toSet());
|
||||
|
||||
Future<void> removeIds(Set<int> entryIds) async {
|
||||
final removedRows = _rows.where((row) => entryIds.contains(row.entryId)).toSet();
|
||||
|
||||
await metadataDb.removeCovers(removedRows.map((row) => row.filter).toSet());
|
||||
_rows.removeAll(removedRows);
|
||||
|
||||
notifyListeners();
|
||||
await _removeEntryFromRows(_rows.where((row) => entryIds.contains(row.entryId)).toSet());
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
await metadataDb.clearCovers();
|
||||
_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
|
||||
|
@ -85,16 +129,17 @@ class Covers with ChangeNotifier {
|
|||
.map((row) {
|
||||
final entryId = row.entryId;
|
||||
final path = visibleEntries.firstWhereOrNull((entry) => entryId == entry.id)?.path;
|
||||
if (path == null) return null;
|
||||
|
||||
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 {
|
||||
'filter': row.filter.toJson(),
|
||||
'volume': volume,
|
||||
'relativePath': relativePath,
|
||||
if (volume != null) 'volume': volume,
|
||||
if (relativePath != null) 'relativePath': relativePath,
|
||||
if (packageName != null) 'packageName': packageName,
|
||||
if (colorValue != null) 'color': colorValue,
|
||||
};
|
||||
})
|
||||
.whereNotNull()
|
||||
|
@ -116,18 +161,27 @@ class Covers with ChangeNotifier {
|
|||
return;
|
||||
}
|
||||
|
||||
final volume = row['volume'];
|
||||
final relativePath = row['relativePath'];
|
||||
if (volume is String && relativePath is String) {
|
||||
final volume = row['volume'] as String?;
|
||||
final relativePath = row['relativePath'] as 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 entry = visibleEntries.firstWhereOrNull((entry) => entry.path == path && filter.test(entry));
|
||||
if (entry != null) {
|
||||
covers.set(filter, entry.id);
|
||||
} else {
|
||||
debugPrint('failed to import cover for path=$path, filter=$filter');
|
||||
entry = visibleEntries.firstWhereOrNull((entry) => entry.path == path && filter.test(entry));
|
||||
if (entry == null) {
|
||||
debugPrint('failed to import cover entry 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
|
||||
class CoverRow extends Equatable {
|
||||
final CollectionFilter filter;
|
||||
final int entryId;
|
||||
final int? entryId;
|
||||
final String? packageName;
|
||||
final Color? color;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [filter, entryId];
|
||||
List<Object?> get props => [filter, entryId, packageName, color];
|
||||
|
||||
const CoverRow({
|
||||
required this.filter,
|
||||
required this.entryId,
|
||||
required this.packageName,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
static CoverRow? fromMap(Map map) {
|
||||
final filter = CollectionFilter.fromJson(map['filter']);
|
||||
if (filter == null) return null;
|
||||
|
||||
final colorValue = map['color'] as int?;
|
||||
final color = colorValue != null ? Color(colorValue) : null;
|
||||
return CoverRow(
|
||||
filter: filter,
|
||||
entryId: map['entryId'],
|
||||
entryId: map['entryId'] as int?,
|
||||
packageName: map['packageName'] as String?,
|
||||
color: color,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'filter': filter.toJson(),
|
||||
'entryId': entryId,
|
||||
'packageName': packageName,
|
||||
'color': color?.value,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -85,6 +85,8 @@ class SqfliteMetadataDb implements MetadataDb {
|
|||
await db.execute('CREATE TABLE $coverTable('
|
||||
'filter TEXT PRIMARY KEY'
|
||||
', entryId INTEGER'
|
||||
', packageName TEXT'
|
||||
', color INTEGER'
|
||||
')');
|
||||
await db.execute('CREATE TABLE $trashTable('
|
||||
'id INTEGER PRIMARY KEY'
|
||||
|
@ -97,7 +99,7 @@ class SqfliteMetadataDb implements MetadataDb {
|
|||
')');
|
||||
},
|
||||
onUpgrade: MetadataDbUpgrader.upgradeDb,
|
||||
version: 7,
|
||||
version: 8,
|
||||
);
|
||||
|
||||
final maxIdRows = await _db.rawQuery('SELECT max(id) AS maxId FROM $entryTable');
|
||||
|
|
|
@ -35,6 +35,9 @@ class MetadataDbUpgrader {
|
|||
case 6:
|
||||
await _upgradeFrom6(db);
|
||||
break;
|
||||
case 7:
|
||||
await _upgradeFrom7(db);
|
||||
break;
|
||||
}
|
||||
oldVersion++;
|
||||
}
|
||||
|
@ -269,4 +272,10 @@ class MetadataDbUpgrader {
|
|||
', 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/services/common/services.dart';
|
||||
import 'package:aves/theme/colors.dart';
|
||||
|
@ -8,7 +9,7 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class AlbumFilter extends CollectionFilter {
|
||||
class AlbumFilter extends CoveredCollectionFilter {
|
||||
static const type = 'album';
|
||||
|
||||
final String album;
|
||||
|
@ -53,10 +54,14 @@ class AlbumFilter extends CollectionFilter {
|
|||
|
||||
@override
|
||||
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`
|
||||
// to prevent rebuilding of the `FutureBuilder` listening on this future
|
||||
final albumType = androidFileUtils.getAlbumType(album);
|
||||
final albumType = covers.effectiveAlbumType(album);
|
||||
switch (albumType) {
|
||||
case AlbumType.regular:
|
||||
break;
|
||||
|
|
|
@ -35,7 +35,7 @@ class FavouriteFilter extends CollectionFilter {
|
|||
|
||||
@override
|
||||
Future<Color> color(BuildContext context) {
|
||||
final colors = context.watch<AvesColorsData>();
|
||||
final colors = context.read<AvesColorsData>();
|
||||
return SynchronousFuture(colors.favourite);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:aves/model/covers.dart';
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/album.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;
|
||||
|
||||
Future<Color> color(BuildContext context) {
|
||||
final colors = context.watch<AvesColorsData>();
|
||||
final colors = context.read<AvesColorsData>();
|
||||
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
|
||||
class FilterGridItem<T extends CollectionFilter> with EquatableMixin {
|
||||
final T filter;
|
||||
|
|
|
@ -5,7 +5,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class LocationFilter extends CollectionFilter {
|
||||
class LocationFilter extends CoveredCollectionFilter {
|
||||
static const type = 'location';
|
||||
static const locationSeparator = ';';
|
||||
|
||||
|
|
|
@ -77,7 +77,7 @@ class MimeFilter extends CollectionFilter {
|
|||
|
||||
@override
|
||||
Future<Color> color(BuildContext context) {
|
||||
final colors = context.watch<AvesColorsData>();
|
||||
final colors = context.read<AvesColorsData>();
|
||||
switch (mime) {
|
||||
case MimeTypes.anyImage:
|
||||
return SynchronousFuture(colors.image);
|
||||
|
|
|
@ -73,7 +73,7 @@ class QueryFilter extends CollectionFilter {
|
|||
return super.color(context);
|
||||
}
|
||||
|
||||
final colors = context.watch<AvesColorsData>();
|
||||
final colors = context.read<AvesColorsData>();
|
||||
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:flutter/widgets.dart';
|
||||
|
||||
class TagFilter extends CollectionFilter {
|
||||
class TagFilter extends CoveredCollectionFilter {
|
||||
static const type = 'tag';
|
||||
|
||||
final String tag;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TrashFilter extends CollectionFilter {
|
||||
static const type = 'trash';
|
||||
|
|
|
@ -101,7 +101,7 @@ class TypeFilter extends CollectionFilter {
|
|||
|
||||
@override
|
||||
Future<Color> color(BuildContext context) {
|
||||
final colors = context.watch<AvesColorsData>();
|
||||
final colors = context.read<AvesColorsData>();
|
||||
switch (itemType) {
|
||||
case _animated:
|
||||
return SynchronousFuture(colors.animated);
|
||||
|
|
|
@ -28,7 +28,7 @@ class Query extends ChangeNotifier {
|
|||
|
||||
void toggle() => enabled = !enabled;
|
||||
|
||||
final StreamController<bool> _enabledStreamController = StreamController<bool>.broadcast();
|
||||
final StreamController<bool> _enabledStreamController = StreamController.broadcast();
|
||||
|
||||
Stream<bool> get enabledStream => _enabledStreamController.stream;
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ final Settings settings = Settings._private();
|
|||
|
||||
class Settings extends ChangeNotifier {
|
||||
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;
|
||||
|
||||
|
|
|
@ -219,9 +219,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
entry.uri = 'file://${entry.trashDetails?.path}';
|
||||
}
|
||||
|
||||
await covers.moveEntry(entry, persist: persist);
|
||||
|
||||
if (persist) {
|
||||
await covers.moveEntry(entry);
|
||||
final id = entry.id;
|
||||
await metadataDb.updateEntry(id, entry);
|
||||
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 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);
|
||||
await updateAfterMove(
|
||||
todoEntries: entries,
|
||||
|
@ -441,7 +448,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
}
|
||||
|
||||
AvesEntry? coverEntry(CollectionFilter filter) {
|
||||
final id = covers.coverEntryId(filter);
|
||||
final id = covers.of(filter)?.item1;
|
||||
if (id != null) {
|
||||
final entry = visibleEntries.firstWhereOrNull((entry) => entry.id == id);
|
||||
if (entry != null) return entry;
|
||||
|
|
|
@ -8,7 +8,7 @@ import 'package:tuple/tuple.dart';
|
|||
final ServicePolicy servicePolicy = ServicePolicy._private();
|
||||
|
||||
class ServicePolicy {
|
||||
final StreamController<QueueState> _queueStreamController = StreamController<QueueState>.broadcast();
|
||||
final StreamController<QueueState> _queueStreamController = StreamController.broadcast();
|
||||
final Map<Object, Tuple2<int, _Task>> _paused = {};
|
||||
final SplayTreeMap<int, LinkedHashMap<Object, _Task>> _queues = SplayTreeMap();
|
||||
final LinkedHashMap<Object, _Task> _runningQueue = LinkedHashMap();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
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/settings.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:palette_generator/palette_generator.dart';
|
||||
|
@ -55,7 +55,7 @@ abstract class AvesColorsData {
|
|||
Future<Color>? appColor(String album) {
|
||||
if (_appColors.containsKey(album)) return SynchronousFuture(_appColors[album]!);
|
||||
|
||||
final packageName = androidFileUtils.getAlbumAppPackageName(album);
|
||||
final packageName = covers.effectiveAlbumPackage(album);
|
||||
if (packageName == null) return null;
|
||||
|
||||
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 _neutralOnLight = Color(0xAA000000);
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ class AIcons {
|
|||
|
||||
static const IconData accessibility = Icons.accessibility_new_outlined;
|
||||
static const IconData android = Icons.android;
|
||||
static const IconData app = Icons.apps_outlined;
|
||||
static const IconData bin = Icons.delete_outlined;
|
||||
static const IconData broken = Icons.broken_image_outlined;
|
||||
static const IconData checked = Icons.done_outlined;
|
||||
|
@ -20,6 +21,7 @@ class AIcons {
|
|||
static const IconData folder = Icons.folder_outlined;
|
||||
static const IconData grid = Icons.grid_on_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 location = Icons.place_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/source/collection_source.dart';
|
||||
import 'package:aves/model/source/section_keys.dart';
|
||||
|
@ -49,7 +50,7 @@ class AlbumSectionHeader extends StatelessWidget {
|
|||
context: context,
|
||||
maxWidth: maxWidth,
|
||||
title: source.getAlbumDisplayName(context, directory),
|
||||
hasLeading: androidFileUtils.getAlbumType(directory) != AlbumType.regular,
|
||||
hasLeading: covers.effectiveAlbumType(directory) != AlbumType.regular,
|
||||
hasTrailing: androidFileUtils.isOnRemovableStorage(directory),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ class ColorListTile extends StatelessWidget {
|
|||
final Color value;
|
||||
final ValueSetter<Color> onChanged;
|
||||
|
||||
static const radius = 16.0;
|
||||
static const double radius = 16.0;
|
||||
|
||||
const ColorListTile({
|
||||
Key? key,
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/app_mode.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/filters.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
|
@ -114,6 +117,7 @@ class AvesFilterChip extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _AvesFilterChipState extends State<AvesFilterChip> {
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
late Future<Color> _colorFuture;
|
||||
late Color _outlineColor;
|
||||
late bool _tapped;
|
||||
|
@ -131,6 +135,14 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
_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
|
||||
|
@ -148,6 +160,14 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscriptions
|
||||
..forEach((sub) => sub.cancel())
|
||||
..clear();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _initColorLoader() {
|
||||
// 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.
|
||||
|
@ -159,6 +179,13 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
|||
_outlineColor = context.read<AvesColorsData>().neutral;
|
||||
}
|
||||
|
||||
void _onCoverColorChange(Set<CollectionFilter>? event) {
|
||||
if (event == null || event.contains(filter)) {
|
||||
_initColorLoader();
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final decoration = widget.decoration;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
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/theme/icons.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
|
@ -267,7 +268,8 @@ class IconUtils {
|
|||
}) {
|
||||
size ??= IconTheme.of(context).size;
|
||||
Widget buildIcon(IconData icon) => Icon(icon, size: size);
|
||||
switch (androidFileUtils.getAlbumType(albumPath)) {
|
||||
|
||||
switch (covers.effectiveAlbumType(albumPath)) {
|
||||
case AlbumType.camera:
|
||||
return buildIcon(AIcons.cameraAlbum);
|
||||
case AlbumType.screenshots:
|
||||
|
@ -278,14 +280,17 @@ class IconUtils {
|
|||
case AlbumType.download:
|
||||
return buildIcon(AIcons.downloadAlbum);
|
||||
case AlbumType.app:
|
||||
return Image(
|
||||
image: AppIconImage(
|
||||
packageName: androidFileUtils.getAlbumAppPackageName(albumPath)!,
|
||||
size: size!,
|
||||
),
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
final package = covers.effectiveAlbumPackage(albumPath);
|
||||
return package != null
|
||||
? Image(
|
||||
image: AppIconImage(
|
||||
packageName: package,
|
||||
size: size!,
|
||||
),
|
||||
width: size,
|
||||
height: size,
|
||||
)
|
||||
: null;
|
||||
case AlbumType.regular:
|
||||
default:
|
||||
return null;
|
||||
|
|
|
@ -39,7 +39,7 @@ class _DebugAndroidAppSectionState extends State<DebugAndroidAppSection> with Au
|
|||
future: _loader,
|
||||
builder: (context, snapshot) {
|
||||
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 enabledTheme = IconTheme.of(context);
|
||||
final disabledTheme = enabledTheme.merge(const IconThemeData(opacity: .2));
|
||||
|
|
|
@ -38,7 +38,7 @@ class _AddShortcutDialogState extends State<AddShortcutDialog> {
|
|||
if (_collection != null) {
|
||||
final entries = _collection.sortedEntries;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ class _AddShortcutDialogState extends State<AddShortcutDialog> {
|
|||
final _collection = widget.collection;
|
||||
if (_collection == null) return;
|
||||
|
||||
final entry = await Navigator.push(
|
||||
final entry = await Navigator.push<AvesEntry>(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
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;
|
||||
if (_collection == null) return;
|
||||
|
||||
final entry = await Navigator.push(
|
||||
final entry = await Navigator.push<AvesEntry>(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
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/filters/album.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_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/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/item_pick_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/item_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class CoverSelectionDialog extends StatefulWidget {
|
||||
final CollectionFilter filter;
|
||||
final AvesEntry? customEntry;
|
||||
final String? customPackage;
|
||||
final Color? customColor;
|
||||
|
||||
const CoverSelectionDialog({
|
||||
Key? key,
|
||||
required this.filter,
|
||||
required this.customEntry,
|
||||
required this.customPackage,
|
||||
required this.customColor,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -26,83 +40,315 @@ class CoverSelectionDialog extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
|
||||
late bool _isCustom;
|
||||
late bool _isCustomEntry, _isCustomPackage, _isCustomColor;
|
||||
AvesEntry? _customEntry;
|
||||
String? _customPackage;
|
||||
Color? _customColor;
|
||||
|
||||
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
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_customEntry = widget.customEntry;
|
||||
_isCustom = _customEntry != null;
|
||||
_isCustomEntry = _customEntry != null;
|
||||
|
||||
_customPackage = widget.customPackage;
|
||||
_isCustomPackage = _customPackage != null;
|
||||
|
||||
_customColor = widget.customColor;
|
||||
_isCustomColor = _customColor != null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQueryDataProvider(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final l10n = context.l10n;
|
||||
return AvesDialog(
|
||||
title: l10n.setCoverDialogTitle,
|
||||
scrollableContent: [
|
||||
...[false, true].map(
|
||||
(isCustom) {
|
||||
final title = Text(
|
||||
isCustom ? l10n.setCoverDialogCustom : l10n.setCoverDialogLatest,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
);
|
||||
return RadioListTile<bool>(
|
||||
value: isCustom,
|
||||
groupValue: _isCustom,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
if (v && _customEntry == null) {
|
||||
_pickEntry();
|
||||
return;
|
||||
}
|
||||
_isCustom = v;
|
||||
setState(() {});
|
||||
},
|
||||
title: isCustom
|
||||
? Row(
|
||||
children: [
|
||||
title,
|
||||
const Spacer(),
|
||||
if (_customEntry != null)
|
||||
ItemPicker(
|
||||
extent: 46,
|
||||
entry: _customEntry!,
|
||||
onTap: _pickEntry,
|
||||
),
|
||||
],
|
||||
)
|
||||
: title,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, Tuple2<bool, AvesEntry?>(_isCustom, _customEntry)),
|
||||
child: Text(l10n.applyButtonLabel),
|
||||
),
|
||||
],
|
||||
final l10n = context.l10n;
|
||||
final tabs = <Tuple2<Tab, Widget>>[
|
||||
Tuple2(
|
||||
_buildTab(
|
||||
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) {
|
||||
final title = Text(
|
||||
isCustom ? l10n.setCoverDialogCustom : l10n.setCoverDialogLatest,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
);
|
||||
return RadioListTile<bool>(
|
||||
value: isCustom,
|
||||
groupValue: _isCustomEntry,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
if (v && _customEntry == null) {
|
||||
_pickEntry();
|
||||
return;
|
||||
}
|
||||
_isCustomEntry = v;
|
||||
setState(() {});
|
||||
},
|
||||
title: isCustom
|
||||
? Row(
|
||||
children: [
|
||||
title,
|
||||
const Spacer(),
|
||||
if (_customEntry != null)
|
||||
ItemPicker(
|
||||
extent: itemPickerExtent,
|
||||
entry: _customEntry!,
|
||||
onTap: _pickEntry,
|
||||
),
|
||||
],
|
||||
)
|
||||
: 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: title,
|
||||
);
|
||||
},
|
||||
).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 {
|
||||
final entry = await Navigator.push(
|
||||
final entry = await Navigator.push<AvesEntry>(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: ItemPickDialog.routeName),
|
||||
|
@ -117,8 +363,104 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
|
|||
);
|
||||
if (entry != null) {
|
||||
_customEntry = entry;
|
||||
_isCustom = true;
|
||||
_isCustomEntry = true;
|
||||
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;
|
||||
|
||||
static const int groupTabIndex = 1;
|
||||
|
||||
double tabBarHeight(BuildContext context) => 64 * max(1, MediaQuery.textScaleFactorOf(context));
|
||||
|
||||
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));
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
// crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Material(
|
||||
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(
|
||||
BuildContext context,
|
||||
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;
|
||||
|
||||
void _onTabChange() {
|
||||
if (!canGroup && _tabController.index == 1) {
|
||||
if (!canGroup && _tabController.index == groupTabIndex) {
|
||||
_tabController.index = _tabController.previousIndex;
|
||||
}
|
||||
}
|
||||
|
@ -291,20 +310,4 @@ class _TileViewDialogState<S, G, L> extends State<TileViewDialog<S, G, L>> with
|
|||
layoutOptions,
|
||||
].map((v) => v.length).fold(0, max) *
|
||||
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/filters.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
|
@ -38,17 +39,21 @@ class AlbumListPage extends StatelessWidget {
|
|||
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
||||
builder: (context, snapshot) {
|
||||
final gridItems = getAlbumGridItems(context, source);
|
||||
return FilterNavigationPage<AlbumFilter>(
|
||||
source: source,
|
||||
title: context.l10n.albumPageTitle,
|
||||
sortFactor: settings.albumSortFactor,
|
||||
showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none,
|
||||
actionDelegate: AlbumChipSetActionDelegate(gridItems),
|
||||
filterSections: groupToSections(context, source, gridItems),
|
||||
newFilters: source.getNewAlbumFilters(context),
|
||||
emptyBuilder: () => EmptyContent(
|
||||
icon: AIcons.album,
|
||||
text: context.l10n.albumEmpty,
|
||||
return StreamBuilder<Set<CollectionFilter>?>(
|
||||
// to update sections by tier
|
||||
stream: covers.packageChangeStream,
|
||||
builder: (context, snapshot) => FilterNavigationPage<AlbumFilter>(
|
||||
source: source,
|
||||
title: context.l10n.albumPageTitle,
|
||||
sortFactor: settings.albumSortFactor,
|
||||
showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none,
|
||||
actionDelegate: AlbumChipSetActionDelegate(gridItems),
|
||||
filterSections: groupToSections(context, source, gridItems),
|
||||
newFilters: source.getNewAlbumFilters(context),
|
||||
emptyBuilder: () => EmptyContent(
|
||||
icon: AIcons.album,
|
||||
text: context.l10n.albumEmpty,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -89,7 +94,7 @@ class AlbumListPage extends StatelessWidget {
|
|||
final appsKey = AlbumImportanceSectionKey.apps(context);
|
||||
final regularKey = AlbumImportanceSectionKey.regular(context);
|
||||
sections = groupBy<FilterGridItem<AlbumFilter>, ChipSectionKey>(unpinnedMapEntries, (kv) {
|
||||
switch (androidFileUtils.getAlbumType(kv.filter.album)) {
|
||||
switch (covers.effectiveAlbumType(kv.filter.album)) {
|
||||
case AlbumType.regular:
|
||||
return regularKey;
|
||||
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/covers.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/selection.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/theme/colors.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/common/action_mixins/feedback.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 {
|
||||
final entryId = covers.coverEntryId(filter);
|
||||
final customEntry = context.read<CollectionSource>().visibleEntries.firstWhereOrNull((entry) => entry.id == entryId);
|
||||
final coverSelection = await showDialog<Tuple2<bool, AvesEntry?>>(
|
||||
final existingCover = covers.of(filter);
|
||||
final entryId = existingCover?.item1;
|
||||
final customEntry = entryId != null ? context.read<CollectionSource>().visibleEntries.firstWhereOrNull((entry) => entry.id == entryId) : null;
|
||||
final selectedCover = await showDialog<Tuple3<AvesEntry?, String?, Color?>>(
|
||||
context: context,
|
||||
builder: (context) => CoverSelectionDialog(
|
||||
filter: filter,
|
||||
customEntry: customEntry,
|
||||
customPackage: existingCover?.item2,
|
||||
customColor: existingCover?.item3,
|
||||
),
|
||||
);
|
||||
if (coverSelection == null) return;
|
||||
if (selectedCover == null) return;
|
||||
|
||||
final isCustom = coverSelection.item1;
|
||||
await covers.set(filter, isCustom ? coverSelection.item2?.id : null);
|
||||
if (filter is AlbumFilter) {
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/model/covers.dart';
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
|
@ -61,37 +62,40 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<CollectionSource>(
|
||||
builder: (context, source, child) {
|
||||
switch (T) {
|
||||
case AlbumFilter:
|
||||
{
|
||||
final album = (filter as AlbumFilter).album;
|
||||
return StreamBuilder<AlbumSummaryInvalidatedEvent>(
|
||||
stream: source.eventBus.on<AlbumSummaryInvalidatedEvent>().where((event) => event.directories == null || event.directories!.contains(album)),
|
||||
builder: (context, snapshot) => _buildChip(context, source),
|
||||
);
|
||||
}
|
||||
case LocationFilter:
|
||||
{
|
||||
final countryCode = (filter as LocationFilter).countryCode;
|
||||
return StreamBuilder<CountrySummaryInvalidatedEvent>(
|
||||
stream: source.eventBus.on<CountrySummaryInvalidatedEvent>().where((event) => event.countryCodes == null || event.countryCodes!.contains(countryCode)),
|
||||
builder: (context, snapshot) => _buildChip(context, source),
|
||||
);
|
||||
}
|
||||
case TagFilter:
|
||||
{
|
||||
final tag = (filter as TagFilter).tag;
|
||||
return StreamBuilder<TagSummaryInvalidatedEvent>(
|
||||
stream: source.eventBus.on<TagSummaryInvalidatedEvent>().where((event) => event.tags == null || event.tags!.contains(tag)),
|
||||
builder: (context, snapshot) => _buildChip(context, source),
|
||||
);
|
||||
}
|
||||
default:
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
return StreamBuilder<Set<CollectionFilter>?>(
|
||||
stream: covers.entryChangeStream.where((event) => event == null || event.contains(filter)),
|
||||
builder: (context, snapshot) => Consumer<CollectionSource>(
|
||||
builder: (context, source, child) {
|
||||
switch (T) {
|
||||
case AlbumFilter:
|
||||
{
|
||||
final album = (filter as AlbumFilter).album;
|
||||
return StreamBuilder<AlbumSummaryInvalidatedEvent>(
|
||||
stream: source.eventBus.on<AlbumSummaryInvalidatedEvent>().where((event) => event.directories == null || event.directories!.contains(album)),
|
||||
builder: (context, snapshot) => _buildChip(context, source),
|
||||
);
|
||||
}
|
||||
case LocationFilter:
|
||||
{
|
||||
final countryCode = (filter as LocationFilter).countryCode;
|
||||
return StreamBuilder<CountrySummaryInvalidatedEvent>(
|
||||
stream: source.eventBus.on<CountrySummaryInvalidatedEvent>().where((event) => event.countryCodes == null || event.countryCodes!.contains(countryCode)),
|
||||
builder: (context, snapshot) => _buildChip(context, source),
|
||||
);
|
||||
}
|
||||
case TagFilter:
|
||||
{
|
||||
final tag = (filter as TagFilter).tag;
|
||||
return StreamBuilder<TagSummaryInvalidatedEvent>(
|
||||
stream: source.eventBus.on<TagSummaryInvalidatedEvent>().where((event) => event.tags == null || event.tags!.contains(tag)),
|
||||
builder: (context, snapshot) => _buildChip(context, source),
|
||||
);
|
||||
}
|
||||
default:
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/model/covers.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/model/selection.dart';
|
||||
|
@ -84,24 +83,21 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
|||
child: GestureAreaProtectorStack(
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: AnimatedBuilder(
|
||||
animation: covers,
|
||||
builder: (context, child) => FilterGrid<T>(
|
||||
// key is expected by test driver
|
||||
key: const Key('filter-grid'),
|
||||
settingsRouteKey: settingsRouteKey,
|
||||
appBar: appBar,
|
||||
appBarHeight: appBarHeight,
|
||||
sections: sections,
|
||||
newFilters: newFilters,
|
||||
sortFactor: sortFactor,
|
||||
showHeaders: showHeaders,
|
||||
selectable: selectable,
|
||||
queryNotifier: queryNotifier,
|
||||
applyQuery: applyQuery,
|
||||
emptyBuilder: emptyBuilder,
|
||||
heroType: heroType,
|
||||
),
|
||||
child: FilterGrid<T>(
|
||||
// key is expected by test driver
|
||||
key: const Key('filter-grid'),
|
||||
settingsRouteKey: settingsRouteKey,
|
||||
appBar: appBar,
|
||||
appBarHeight: appBarHeight,
|
||||
sections: sections,
|
||||
newFilters: newFilters,
|
||||
sortFactor: sortFactor,
|
||||
showHeaders: showHeaders,
|
||||
selectable: selectable,
|
||||
queryNotifier: queryNotifier,
|
||||
applyQuery: applyQuery,
|
||||
emptyBuilder: emptyBuilder,
|
||||
heroType: heroType,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -62,9 +62,9 @@ extension ExtraAlbumImportance on AlbumImportance {
|
|||
case AlbumImportance.pinned:
|
||||
return AIcons.pin;
|
||||
case AlbumImportance.special:
|
||||
return Icons.label_important_outline;
|
||||
return AIcons.important;
|
||||
case AlbumImportance.apps:
|
||||
return Icons.apps_outlined;
|
||||
return AIcons.app;
|
||||
case AlbumImportance.regular:
|
||||
return AIcons.album;
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi
|
|||
final AvesEntry entry;
|
||||
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;
|
||||
|
||||
|
|
|
@ -169,15 +169,15 @@ void main() {
|
|||
const albumFilter = AlbumFilter(testAlbum, 'whatever');
|
||||
expect(albumFilter.test(image1), true);
|
||||
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.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.coverEntryId(albumFilter), null);
|
||||
expect(covers.of(albumFilter), null);
|
||||
});
|
||||
|
||||
test('favourites and covers are kept when renaming entries', () async {
|
||||
|
@ -189,7 +189,7 @@ void main() {
|
|||
final source = await _initSource();
|
||||
await image1.toggleFavourite();
|
||||
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(
|
||||
todoEntries: {image1},
|
||||
movedOps: {
|
||||
|
@ -201,7 +201,7 @@ void main() {
|
|||
expect(favourites.count, 1);
|
||||
expect(image1.isFavourite, true);
|
||||
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 {
|
||||
|
@ -213,13 +213,13 @@ void main() {
|
|||
final source = await _initSource();
|
||||
await image1.toggleFavourite();
|
||||
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);
|
||||
|
||||
expect(source.rawAlbums.length, 0);
|
||||
expect(favourites.count, 0);
|
||||
expect(covers.count, 0);
|
||||
expect(covers.coverEntryId(albumFilter), null);
|
||||
expect(covers.of(albumFilter), null);
|
||||
});
|
||||
|
||||
test('albums are updated when moving entries', () async {
|
||||
|
@ -284,7 +284,7 @@ void main() {
|
|||
final source = await _initSource();
|
||||
expect(source.rawAlbums.length, 1);
|
||||
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(
|
||||
todoEntries: {image1},
|
||||
|
@ -297,7 +297,7 @@ void main() {
|
|||
|
||||
expect(source.rawAlbums.length, 2);
|
||||
expect(covers.count, 0);
|
||||
expect(covers.coverEntryId(sourceAlbumFilter), null);
|
||||
expect(covers.of(sourceAlbumFilter), null);
|
||||
});
|
||||
|
||||
test('favourites and covers are kept when renaming albums', () async {
|
||||
|
@ -309,7 +309,7 @@ void main() {
|
|||
final source = await _initSource();
|
||||
await image1.toggleFavourite();
|
||||
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, {
|
||||
image1
|
||||
}, {
|
||||
|
@ -320,7 +320,7 @@ void main() {
|
|||
expect(favourites.count, 1);
|
||||
expect(image1.isFavourite, true);
|
||||
expect(covers.count, 1);
|
||||
expect(covers.coverEntryId(albumFilter), image1.id);
|
||||
expect(covers.of(albumFilter)?.item1, image1.id);
|
||||
});
|
||||
|
||||
testWidgets('unique album names', (tester) async {
|
||||
|
|
|
@ -2,49 +2,97 @@
|
|||
"de": [
|
||||
"entryActionShowGeoTiffOnMap",
|
||||
"entryActionConvertMotionPhotoToStillImage",
|
||||
"convertMotionPhotoToStillImageWarningDialogMessage"
|
||||
"setCoverDialogAuto",
|
||||
"convertMotionPhotoToStillImageWarningDialogMessage",
|
||||
"coverDialogTabCover",
|
||||
"coverDialogTabApp",
|
||||
"coverDialogTabColor",
|
||||
"appPickDialogTitle",
|
||||
"appPickDialogNone"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"entryActionShowGeoTiffOnMap",
|
||||
"entryActionConvertMotionPhotoToStillImage",
|
||||
"convertMotionPhotoToStillImageWarningDialogMessage"
|
||||
"setCoverDialogAuto",
|
||||
"convertMotionPhotoToStillImageWarningDialogMessage",
|
||||
"coverDialogTabCover",
|
||||
"coverDialogTabApp",
|
||||
"coverDialogTabColor",
|
||||
"appPickDialogTitle",
|
||||
"appPickDialogNone"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
"entryActionShowGeoTiffOnMap",
|
||||
"entryActionConvertMotionPhotoToStillImage",
|
||||
"convertMotionPhotoToStillImageWarningDialogMessage"
|
||||
"setCoverDialogAuto",
|
||||
"convertMotionPhotoToStillImageWarningDialogMessage",
|
||||
"coverDialogTabCover",
|
||||
"coverDialogTabApp",
|
||||
"coverDialogTabColor",
|
||||
"appPickDialogTitle",
|
||||
"appPickDialogNone"
|
||||
],
|
||||
|
||||
"id": [
|
||||
"entryActionShowGeoTiffOnMap",
|
||||
"entryActionConvertMotionPhotoToStillImage",
|
||||
"convertMotionPhotoToStillImageWarningDialogMessage"
|
||||
"setCoverDialogAuto",
|
||||
"convertMotionPhotoToStillImageWarningDialogMessage",
|
||||
"coverDialogTabCover",
|
||||
"coverDialogTabApp",
|
||||
"coverDialogTabColor",
|
||||
"appPickDialogTitle",
|
||||
"appPickDialogNone"
|
||||
],
|
||||
|
||||
"it": [
|
||||
"entryActionShowGeoTiffOnMap",
|
||||
"entryActionConvertMotionPhotoToStillImage",
|
||||
"convertMotionPhotoToStillImageWarningDialogMessage"
|
||||
"setCoverDialogAuto",
|
||||
"convertMotionPhotoToStillImageWarningDialogMessage",
|
||||
"coverDialogTabCover",
|
||||
"coverDialogTabApp",
|
||||
"coverDialogTabColor",
|
||||
"appPickDialogTitle",
|
||||
"appPickDialogNone"
|
||||
],
|
||||
|
||||
"ja": [
|
||||
"entryActionShowGeoTiffOnMap",
|
||||
"entryActionConvertMotionPhotoToStillImage",
|
||||
"convertMotionPhotoToStillImageWarningDialogMessage"
|
||||
"setCoverDialogAuto",
|
||||
"convertMotionPhotoToStillImageWarningDialogMessage",
|
||||
"coverDialogTabCover",
|
||||
"coverDialogTabApp",
|
||||
"coverDialogTabColor",
|
||||
"appPickDialogTitle",
|
||||
"appPickDialogNone"
|
||||
],
|
||||
|
||||
"ko": [
|
||||
"entryActionShowGeoTiffOnMap",
|
||||
"entryActionConvertMotionPhotoToStillImage",
|
||||
"convertMotionPhotoToStillImageWarningDialogMessage"
|
||||
"setCoverDialogAuto",
|
||||
"convertMotionPhotoToStillImageWarningDialogMessage",
|
||||
"coverDialogTabCover",
|
||||
"coverDialogTabApp",
|
||||
"coverDialogTabColor",
|
||||
"appPickDialogTitle",
|
||||
"appPickDialogNone"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
"entryActionShowGeoTiffOnMap",
|
||||
"entryActionConvertMotionPhotoToStillImage",
|
||||
"convertMotionPhotoToStillImageWarningDialogMessage"
|
||||
"setCoverDialogAuto",
|
||||
"convertMotionPhotoToStillImageWarningDialogMessage",
|
||||
"coverDialogTabCover",
|
||||
"coverDialogTabApp",
|
||||
"coverDialogTabColor",
|
||||
"appPickDialogTitle",
|
||||
"appPickDialogNone"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
|
@ -57,6 +105,7 @@
|
|||
"themeBrightnessBlack",
|
||||
"moveUndatedConfirmationDialogMessage",
|
||||
"moveUndatedConfirmationDialogSetDate",
|
||||
"setCoverDialogAuto",
|
||||
"renameEntrySetPageTitle",
|
||||
"renameEntrySetPagePatternFieldLabel",
|
||||
"renameEntrySetPageInsertTooltip",
|
||||
|
@ -65,6 +114,11 @@
|
|||
"renameProcessorName",
|
||||
"editEntryDateDialogCopyItem",
|
||||
"convertMotionPhotoToStillImageWarningDialogMessage",
|
||||
"coverDialogTabCover",
|
||||
"coverDialogTabApp",
|
||||
"coverDialogTabColor",
|
||||
"appPickDialogTitle",
|
||||
"appPickDialogNone",
|
||||
"collectionRenameFailureFeedback",
|
||||
"collectionRenameSuccessFeedback",
|
||||
"settingsConfirmationDialogMoveUndatedItems",
|
||||
|
@ -77,6 +131,12 @@
|
|||
|
||||
"zh": [
|
||||
"entryActionConvertMotionPhotoToStillImage",
|
||||
"convertMotionPhotoToStillImageWarningDialogMessage"
|
||||
"setCoverDialogAuto",
|
||||
"convertMotionPhotoToStillImageWarningDialogMessage",
|
||||
"coverDialogTabCover",
|
||||
"coverDialogTabApp",
|
||||
"coverDialogTabColor",
|
||||
"appPickDialogTitle",
|
||||
"appPickDialogNone"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue