added favourites
This commit is contained in:
parent
5df815e5c1
commit
edd410d854
24 changed files with 524 additions and 266 deletions
|
@ -1,239 +0,0 @@
|
||||||
import 'package:aves/model/image_entry.dart';
|
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
|
||||||
import 'package:aves/utils/color_utils.dart';
|
|
||||||
import 'package:aves/widgets/common/icons.dart';
|
|
||||||
import 'package:aves/widgets/common/image_providers/app_icon_image_provider.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:outline_material_icons/outline_material_icons.dart';
|
|
||||||
import 'package:palette_generator/palette_generator.dart';
|
|
||||||
import 'package:path/path.dart';
|
|
||||||
|
|
||||||
abstract class CollectionFilter implements Comparable<CollectionFilter> {
|
|
||||||
static const List<String> collectionFilterOrder = [
|
|
||||||
VideoFilter.type,
|
|
||||||
GifFilter.type,
|
|
||||||
AlbumFilter.type,
|
|
||||||
CountryFilter.type,
|
|
||||||
TagFilter.type,
|
|
||||||
QueryFilter.type,
|
|
||||||
];
|
|
||||||
|
|
||||||
const CollectionFilter();
|
|
||||||
|
|
||||||
bool filter(ImageEntry entry);
|
|
||||||
|
|
||||||
String get label;
|
|
||||||
|
|
||||||
String get tooltip => label;
|
|
||||||
|
|
||||||
Widget iconBuilder(BuildContext context, double size);
|
|
||||||
|
|
||||||
Future<Color> color(BuildContext context) => SynchronousFuture(stringToColor(label));
|
|
||||||
|
|
||||||
String get typeKey;
|
|
||||||
|
|
||||||
int get displayPriority => collectionFilterOrder.indexOf(typeKey);
|
|
||||||
|
|
||||||
@override
|
|
||||||
int compareTo(CollectionFilter other) {
|
|
||||||
final c = displayPriority.compareTo(other.displayPriority);
|
|
||||||
return c != 0 ? c : compareAsciiUpperCase(label, other.label);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AlbumFilter extends CollectionFilter {
|
|
||||||
static const type = 'album';
|
|
||||||
|
|
||||||
static Map<String, Color> _appColors = Map();
|
|
||||||
|
|
||||||
final String album;
|
|
||||||
|
|
||||||
const AlbumFilter(this.album);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool filter(ImageEntry entry) => entry.directory == album;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get label => album.split(separator).last;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get tooltip => album;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget iconBuilder(context, size) {
|
|
||||||
return IconUtils.getAlbumIcon(context: context, album: album, size: size) ?? Icon(OMIcons.photoAlbum, size: size);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Color> color(BuildContext context) {
|
|
||||||
// do not use async/await and rely on `SynchronousFuture`
|
|
||||||
// to prevent rebuilding of the `FutureBuilder` listening on this future
|
|
||||||
if (androidFileUtils.getAlbumType(album) == AlbumType.App) {
|
|
||||||
if (_appColors.containsKey(album)) return SynchronousFuture(_appColors[album]);
|
|
||||||
|
|
||||||
return PaletteGenerator.fromImageProvider(
|
|
||||||
AppIconImage(
|
|
||||||
packageName: androidFileUtils.getAlbumAppPackageName(album),
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
).then((palette) {
|
|
||||||
final color = palette.dominantColor?.color ?? super.color(context);
|
|
||||||
_appColors[album] = color;
|
|
||||||
return color;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return super.color(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get typeKey => type;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (other.runtimeType != runtimeType) return false;
|
|
||||||
return other is AlbumFilter && other.album == album;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => hashValues('AlbumFilter', album);
|
|
||||||
}
|
|
||||||
|
|
||||||
class TagFilter extends CollectionFilter {
|
|
||||||
static const type = 'tag';
|
|
||||||
|
|
||||||
final String tag;
|
|
||||||
|
|
||||||
const TagFilter(this.tag);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool filter(ImageEntry entry) => entry.xmpSubjects.contains(tag);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get label => tag;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget iconBuilder(context, size) => Icon(OMIcons.localOffer, size: size);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get typeKey => type;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (other.runtimeType != runtimeType) return false;
|
|
||||||
return other is TagFilter && other.tag == tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => hashValues('TagFilter', tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
class CountryFilter extends CollectionFilter {
|
|
||||||
static const type = 'country';
|
|
||||||
|
|
||||||
final String country;
|
|
||||||
|
|
||||||
const CountryFilter(this.country);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool filter(ImageEntry entry) => entry.isLocated && entry.addressDetails.countryName == country;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get label => country;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget iconBuilder(context, size) => Icon(OMIcons.place, size: size);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get typeKey => type;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (other.runtimeType != runtimeType) return false;
|
|
||||||
return other is CountryFilter && other.country == country;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => hashValues('CountryFilter', country);
|
|
||||||
}
|
|
||||||
|
|
||||||
class VideoFilter extends CollectionFilter {
|
|
||||||
static const type = 'video';
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool filter(ImageEntry entry) => entry.isVideo;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get label => 'Video';
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget iconBuilder(context, size) => Icon(OMIcons.movie, size: size);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get typeKey => type;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (other.runtimeType != runtimeType) return false;
|
|
||||||
return other is VideoFilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => 'VideoFilter'.hashCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
class GifFilter extends CollectionFilter {
|
|
||||||
static const type = 'gif';
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool filter(ImageEntry entry) => entry.isGif;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get label => 'GIF';
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget iconBuilder(context, size) => Icon(OMIcons.gif, size: size);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get typeKey => type;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (other.runtimeType != runtimeType) return false;
|
|
||||||
return other is GifFilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => 'GifFilter'.hashCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
class QueryFilter extends CollectionFilter {
|
|
||||||
static const type = 'query';
|
|
||||||
|
|
||||||
final String query;
|
|
||||||
|
|
||||||
const QueryFilter(this.query);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool filter(ImageEntry entry) => entry.search(query);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get label => '${query}';
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget iconBuilder(context, size) => Icon(OMIcons.formatQuote, size: size);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get typeKey => type;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (other.runtimeType != runtimeType) return false;
|
|
||||||
return other is QueryFilter && other.query == query;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => hashValues('MetadataFilter', query);
|
|
||||||
}
|
|
|
@ -1,8 +1,8 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:aves/model/collection_filters.dart';
|
|
||||||
import 'package:aves/model/collection_source.dart';
|
import 'package:aves/model/collection_source.dart';
|
||||||
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
31
lib/model/favourite_repo.dart
Normal file
31
lib/model/favourite_repo.dart
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:aves/model/image_metadata.dart';
|
||||||
|
import 'package:aves/model/metadata_db.dart';
|
||||||
|
|
||||||
|
final FavouriteRepo favourites = FavouriteRepo._private();
|
||||||
|
|
||||||
|
class FavouriteRepo {
|
||||||
|
List<FavouriteRow> _rows = List();
|
||||||
|
|
||||||
|
FavouriteRepo._private();
|
||||||
|
|
||||||
|
Future<void> init() async {
|
||||||
|
_rows = await metadataDb.loadFavourites();
|
||||||
|
}
|
||||||
|
|
||||||
|
int get count => _rows.length;
|
||||||
|
|
||||||
|
bool isFavourite(ImageEntry entry) => _rows.any((row) => row.contentId == entry.contentId);
|
||||||
|
|
||||||
|
Future<void> add(ImageEntry entry) async {
|
||||||
|
final newRows = [FavouriteRow(contentId: entry.contentId, path: entry.path)];
|
||||||
|
await metadataDb.addFavourites(newRows);
|
||||||
|
_rows.addAll(newRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> remove(ImageEntry entry) async {
|
||||||
|
final removedRows = [FavouriteRow(contentId: entry.contentId, path: entry.path)];
|
||||||
|
await metadataDb.removeFavourites(removedRows);
|
||||||
|
removedRows.forEach(_rows.remove);
|
||||||
|
}
|
||||||
|
}
|
68
lib/model/filters/album.dart
Normal file
68
lib/model/filters/album.dart
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import 'package:aves/model/filters/filters.dart';
|
||||||
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
|
import 'package:aves/widgets/common/icons.dart';
|
||||||
|
import 'package:aves/widgets/common/image_providers/app_icon_image_provider.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:outline_material_icons/outline_material_icons.dart';
|
||||||
|
import 'package:palette_generator/palette_generator.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
|
||||||
|
class AlbumFilter extends CollectionFilter {
|
||||||
|
static const type = 'album';
|
||||||
|
|
||||||
|
static Map<String, Color> _appColors = Map();
|
||||||
|
|
||||||
|
final String album;
|
||||||
|
|
||||||
|
const AlbumFilter(this.album);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool filter(ImageEntry entry) => entry.directory == album;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get label => album.split(separator).last;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tooltip => album;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget iconBuilder(context, size) {
|
||||||
|
return IconUtils.getAlbumIcon(context: context, album: album, size: size) ?? Icon(OMIcons.photoAlbum, size: size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Color> color(BuildContext context) {
|
||||||
|
// do not use async/await and rely on `SynchronousFuture`
|
||||||
|
// to prevent rebuilding of the `FutureBuilder` listening on this future
|
||||||
|
if (androidFileUtils.getAlbumType(album) == AlbumType.App) {
|
||||||
|
if (_appColors.containsKey(album)) return SynchronousFuture(_appColors[album]);
|
||||||
|
|
||||||
|
return PaletteGenerator.fromImageProvider(
|
||||||
|
AppIconImage(
|
||||||
|
packageName: androidFileUtils.getAlbumAppPackageName(album),
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
).then((palette) {
|
||||||
|
final color = palette.dominantColor?.color ?? super.color(context);
|
||||||
|
_appColors[album] = color;
|
||||||
|
return color;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return super.color(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get typeKey => type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other.runtimeType != runtimeType) return false;
|
||||||
|
return other is AlbumFilter && other.album == album;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => hashValues('AlbumFilter', album);
|
||||||
|
}
|
33
lib/model/filters/country.dart
Normal file
33
lib/model/filters/country.dart
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import 'package:aves/model/filters/filters.dart';
|
||||||
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:outline_material_icons/outline_material_icons.dart';
|
||||||
|
|
||||||
|
class CountryFilter extends CollectionFilter {
|
||||||
|
static const type = 'country';
|
||||||
|
|
||||||
|
final String country;
|
||||||
|
|
||||||
|
const CountryFilter(this.country);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool filter(ImageEntry entry) => entry.isLocated && entry.addressDetails.countryName == country;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get label => country;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget iconBuilder(context, size) => Icon(OMIcons.place, size: size);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get typeKey => type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other.runtimeType != runtimeType) return false;
|
||||||
|
return other is CountryFilter && other.country == country;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => hashValues('CountryFilter', country);
|
||||||
|
}
|
30
lib/model/filters/favourite.dart
Normal file
30
lib/model/filters/favourite.dart
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import 'package:aves/model/filters/filters.dart';
|
||||||
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:outline_material_icons/outline_material_icons.dart';
|
||||||
|
|
||||||
|
class FavouriteFilter extends CollectionFilter {
|
||||||
|
static const type = 'favourite';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool filter(ImageEntry entry) => entry.isFavourite;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get label => 'Favourite';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget iconBuilder(context, size) => Icon(OMIcons.favoriteBorder, size: size);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get typeKey => type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other.runtimeType != runtimeType) return false;
|
||||||
|
return other is FavouriteFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => 'FavouriteFilter'.hashCode;
|
||||||
|
}
|
46
lib/model/filters/filters.dart
Normal file
46
lib/model/filters/filters.dart
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import 'package:aves/model/filters/album.dart';
|
||||||
|
import 'package:aves/model/filters/country.dart';
|
||||||
|
import 'package:aves/model/filters/favourite.dart';
|
||||||
|
import 'package:aves/model/filters/gif.dart';
|
||||||
|
import 'package:aves/model/filters/query.dart';
|
||||||
|
import 'package:aves/model/filters/tag.dart';
|
||||||
|
import 'package:aves/model/filters/video.dart';
|
||||||
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:aves/utils/color_utils.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
abstract class CollectionFilter implements Comparable<CollectionFilter> {
|
||||||
|
static const List<String> collectionFilterOrder = [
|
||||||
|
QueryFilter.type,
|
||||||
|
FavouriteFilter.type,
|
||||||
|
VideoFilter.type,
|
||||||
|
GifFilter.type,
|
||||||
|
AlbumFilter.type,
|
||||||
|
CountryFilter.type,
|
||||||
|
TagFilter.type,
|
||||||
|
];
|
||||||
|
|
||||||
|
const CollectionFilter();
|
||||||
|
|
||||||
|
bool filter(ImageEntry entry);
|
||||||
|
|
||||||
|
String get label;
|
||||||
|
|
||||||
|
String get tooltip => label;
|
||||||
|
|
||||||
|
Widget iconBuilder(BuildContext context, double size);
|
||||||
|
|
||||||
|
Future<Color> color(BuildContext context) => SynchronousFuture(stringToColor(label));
|
||||||
|
|
||||||
|
String get typeKey;
|
||||||
|
|
||||||
|
int get displayPriority => collectionFilterOrder.indexOf(typeKey);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int compareTo(CollectionFilter other) {
|
||||||
|
final c = displayPriority.compareTo(other.displayPriority);
|
||||||
|
return c != 0 ? c : compareAsciiUpperCase(label, other.label);
|
||||||
|
}
|
||||||
|
}
|
29
lib/model/filters/gif.dart
Normal file
29
lib/model/filters/gif.dart
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import 'package:aves/model/filters/filters.dart';
|
||||||
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:outline_material_icons/outline_material_icons.dart';
|
||||||
|
|
||||||
|
class GifFilter extends CollectionFilter {
|
||||||
|
static const type = 'gif';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool filter(ImageEntry entry) => entry.isGif;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get label => 'GIF';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget iconBuilder(context, size) => Icon(OMIcons.gif, size: size);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get typeKey => type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other.runtimeType != runtimeType) return false;
|
||||||
|
return other is GifFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => 'GifFilter'.hashCode;
|
||||||
|
}
|
41
lib/model/filters/query.dart
Normal file
41
lib/model/filters/query.dart
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import 'package:aves/model/filters/filters.dart';
|
||||||
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
|
import 'package:aves/utils/color_utils.dart';
|
||||||
|
import 'package:aves/widgets/common/icons.dart';
|
||||||
|
import 'package:aves/widgets/common/image_providers/app_icon_image_provider.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:outline_material_icons/outline_material_icons.dart';
|
||||||
|
import 'package:palette_generator/palette_generator.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
|
||||||
|
class QueryFilter extends CollectionFilter {
|
||||||
|
static const type = 'query';
|
||||||
|
|
||||||
|
final String query;
|
||||||
|
|
||||||
|
const QueryFilter(this.query);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool filter(ImageEntry entry) => entry.search(query);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get label => '${query}';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget iconBuilder(context, size) => Icon(OMIcons.formatQuote, size: size);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get typeKey => type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other.runtimeType != runtimeType) return false;
|
||||||
|
return other is QueryFilter && other.query == query;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => hashValues('MetadataFilter', query);
|
||||||
|
}
|
33
lib/model/filters/tag.dart
Normal file
33
lib/model/filters/tag.dart
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import 'package:aves/model/filters/filters.dart';
|
||||||
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:outline_material_icons/outline_material_icons.dart';
|
||||||
|
|
||||||
|
class TagFilter extends CollectionFilter {
|
||||||
|
static const type = 'tag';
|
||||||
|
|
||||||
|
final String tag;
|
||||||
|
|
||||||
|
const TagFilter(this.tag);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool filter(ImageEntry entry) => entry.xmpSubjects.contains(tag);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get label => tag;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget iconBuilder(context, size) => Icon(OMIcons.localOffer, size: size);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get typeKey => type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other.runtimeType != runtimeType) return false;
|
||||||
|
return other is TagFilter && other.tag == tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => hashValues('TagFilter', tag);
|
||||||
|
}
|
29
lib/model/filters/video.dart
Normal file
29
lib/model/filters/video.dart
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import 'package:aves/model/filters/filters.dart';
|
||||||
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:outline_material_icons/outline_material_icons.dart';
|
||||||
|
|
||||||
|
class VideoFilter extends CollectionFilter {
|
||||||
|
static const type = 'video';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool filter(ImageEntry entry) => entry.isVideo;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get label => 'Video';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget iconBuilder(context, size) => Icon(OMIcons.movie, size: size);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get typeKey => type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other.runtimeType != runtimeType) return false;
|
||||||
|
return other is VideoFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => 'VideoFilter'.hashCode;
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:aves/model/favourite_repo.dart';
|
||||||
import 'package:aves/model/image_file_service.dart';
|
import 'package:aves/model/image_file_service.dart';
|
||||||
import 'package:aves/model/image_metadata.dart';
|
import 'package:aves/model/image_metadata.dart';
|
||||||
import 'package:aves/model/metadata_service.dart';
|
import 'package:aves/model/metadata_service.dart';
|
||||||
|
@ -29,6 +30,7 @@ class ImageEntry {
|
||||||
AddressDetails addressDetails;
|
AddressDetails addressDetails;
|
||||||
|
|
||||||
final AChangeNotifier imageChangeNotifier = AChangeNotifier(), metadataChangeNotifier = AChangeNotifier(), addressChangeNotifier = AChangeNotifier();
|
final AChangeNotifier imageChangeNotifier = AChangeNotifier(), metadataChangeNotifier = AChangeNotifier(), addressChangeNotifier = AChangeNotifier();
|
||||||
|
final isFavouriteNotifier = ValueNotifier(false);
|
||||||
|
|
||||||
ImageEntry({
|
ImageEntry({
|
||||||
this.uri,
|
this.uri,
|
||||||
|
@ -44,7 +46,9 @@ class ImageEntry {
|
||||||
this.sourceDateTakenMillis,
|
this.sourceDateTakenMillis,
|
||||||
this.bucketDisplayName,
|
this.bucketDisplayName,
|
||||||
this.durationMillis,
|
this.durationMillis,
|
||||||
}) : directory = path != null ? dirname(path) : null;
|
}) : directory = path != null ? dirname(path) : null {
|
||||||
|
isFavouriteNotifier.value = isFavourite;
|
||||||
|
}
|
||||||
|
|
||||||
factory ImageEntry.fromMap(Map map) {
|
factory ImageEntry.fromMap(Map map) {
|
||||||
return ImageEntry(
|
return ImageEntry(
|
||||||
|
@ -86,6 +90,7 @@ class ImageEntry {
|
||||||
imageChangeNotifier.dispose();
|
imageChangeNotifier.dispose();
|
||||||
metadataChangeNotifier.dispose();
|
metadataChangeNotifier.dispose();
|
||||||
addressChangeNotifier.dispose();
|
addressChangeNotifier.dispose();
|
||||||
|
isFavouriteNotifier.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -97,6 +102,8 @@ class ImageEntry {
|
||||||
|
|
||||||
String get mimeTypeAnySubtype => mimeType.replaceAll(RegExp('/.*'), '/*');
|
String get mimeTypeAnySubtype => mimeType.replaceAll(RegExp('/.*'), '/*');
|
||||||
|
|
||||||
|
bool get isFavourite => favourites.isFavourite(this);
|
||||||
|
|
||||||
bool get isGif => mimeType == MimeTypes.MIME_GIF;
|
bool get isGif => mimeType == MimeTypes.MIME_GIF;
|
||||||
|
|
||||||
bool get isSvg => mimeType == MimeTypes.MIME_SVG;
|
bool get isSvg => mimeType == MimeTypes.MIME_SVG;
|
||||||
|
@ -238,4 +245,13 @@ class ImageEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> delete() => ImageFileService.delete(this);
|
Future<bool> delete() => ImageFileService.delete(this);
|
||||||
|
|
||||||
|
void toggleFavourite() {
|
||||||
|
if (isFavourite) {
|
||||||
|
favourites.remove(this);
|
||||||
|
} else {
|
||||||
|
favourites.add(this);
|
||||||
|
}
|
||||||
|
isFavouriteNotifier.value = !isFavouriteNotifier.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:geocoder/model.dart';
|
import 'package:geocoder/model.dart';
|
||||||
|
|
||||||
class CatalogMetadata {
|
class CatalogMetadata {
|
||||||
|
@ -106,3 +107,39 @@ class AddressDetails {
|
||||||
return 'AddressDetails{contentId=$contentId, addressLine=$addressLine, countryName=$countryName, adminArea=$adminArea, locality=$locality}';
|
return 'AddressDetails{contentId=$contentId, addressLine=$addressLine, countryName=$countryName, adminArea=$adminArea, locality=$locality}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FavouriteRow {
|
||||||
|
final int contentId;
|
||||||
|
final String path;
|
||||||
|
|
||||||
|
FavouriteRow({
|
||||||
|
this.contentId,
|
||||||
|
this.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory FavouriteRow.fromMap(Map map) {
|
||||||
|
return FavouriteRow(
|
||||||
|
contentId: map['contentId'],
|
||||||
|
path: map['path'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() => {
|
||||||
|
'contentId': contentId,
|
||||||
|
'path': path,
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other.runtimeType != runtimeType) return false;
|
||||||
|
return other is FavouriteRow && other.contentId == contentId && other.path == path;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => hashValues(contentId, path);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'FavouriteRow{contentId=$contentId, path=$path}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ class MetadataDb {
|
||||||
|
|
||||||
static const metadataTable = 'metadata';
|
static const metadataTable = 'metadata';
|
||||||
static const addressTable = 'address';
|
static const addressTable = 'address';
|
||||||
|
static const favouriteTable = 'favourites';
|
||||||
|
|
||||||
MetadataDb._private();
|
MetadataDb._private();
|
||||||
|
|
||||||
|
@ -24,6 +25,7 @@ class MetadataDb {
|
||||||
onCreate: (db, version) async {
|
onCreate: (db, version) async {
|
||||||
await db.execute('CREATE TABLE $metadataTable(contentId INTEGER PRIMARY KEY, dateMillis INTEGER, videoRotation INTEGER, xmpSubjects TEXT, latitude REAL, longitude REAL)');
|
await db.execute('CREATE TABLE $metadataTable(contentId INTEGER PRIMARY KEY, dateMillis INTEGER, videoRotation INTEGER, xmpSubjects TEXT, latitude REAL, longitude REAL)');
|
||||||
await db.execute('CREATE TABLE $addressTable(contentId INTEGER PRIMARY KEY, addressLine TEXT, countryName TEXT, adminArea TEXT, locality TEXT)');
|
await db.execute('CREATE TABLE $addressTable(contentId INTEGER PRIMARY KEY, addressLine TEXT, countryName TEXT, adminArea TEXT, locality TEXT)');
|
||||||
|
await db.execute('CREATE TABLE $favouriteTable(contentId INTEGER PRIMARY KEY, path TEXT)');
|
||||||
},
|
},
|
||||||
version: 1,
|
version: 1,
|
||||||
);
|
);
|
||||||
|
@ -98,4 +100,53 @@ class MetadataDb {
|
||||||
await batch.commit(noResult: true);
|
await batch.commit(noResult: true);
|
||||||
debugPrint('$runtimeType saveAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms with ${addresses.length} entries');
|
debugPrint('$runtimeType saveAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms with ${addresses.length} entries');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// favourites
|
||||||
|
|
||||||
|
Future<void> clearFavourites() async {
|
||||||
|
final db = await _database;
|
||||||
|
final count = await db.delete(favouriteTable, where: '1');
|
||||||
|
debugPrint('$runtimeType clearFavourites deleted $count entries');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<FavouriteRow>> loadFavourites() async {
|
||||||
|
// final stopwatch = Stopwatch()..start();
|
||||||
|
final db = await _database;
|
||||||
|
final maps = await db.query(favouriteTable);
|
||||||
|
final favouriteRows = maps.map((map) => FavouriteRow.fromMap(map)).toList();
|
||||||
|
// debugPrint('$runtimeType loadFavourites complete in ${stopwatch.elapsed.inMilliseconds}ms with ${favouriteRows.length} entries');
|
||||||
|
return favouriteRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addFavourites(Iterable<FavouriteRow> favouriteRows) async {
|
||||||
|
if (favouriteRows == null || favouriteRows.isEmpty) return;
|
||||||
|
// final stopwatch = Stopwatch()..start();
|
||||||
|
final db = await _database;
|
||||||
|
final batch = db.batch();
|
||||||
|
favouriteRows.where((row) => row != null).forEach((row) => batch.insert(
|
||||||
|
favouriteTable,
|
||||||
|
row.toMap(),
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||||
|
));
|
||||||
|
await batch.commit(noResult: true);
|
||||||
|
// debugPrint('$runtimeType addFavourites complete in ${stopwatch.elapsed.inMilliseconds}ms with ${favouriteRows.length} entries');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeFavourites(Iterable<FavouriteRow> favouriteRows) async {
|
||||||
|
if (favouriteRows == null || favouriteRows.isEmpty) return;
|
||||||
|
final ids = favouriteRows.where((row) => row != null).map((row) => row.contentId);
|
||||||
|
if (ids.isEmpty) return;
|
||||||
|
|
||||||
|
// using array in `whereArgs` and using it with `where contentId IN ?` is a pain, so we prefer `batch` instead
|
||||||
|
// final stopwatch = Stopwatch()..start();
|
||||||
|
final db = await _database;
|
||||||
|
final batch = db.batch();
|
||||||
|
ids.forEach((id) => batch.delete(
|
||||||
|
favouriteTable,
|
||||||
|
where: 'contentId = ?',
|
||||||
|
whereArgs: [id],
|
||||||
|
));
|
||||||
|
await batch.commit(noResult: true);
|
||||||
|
// debugPrint('$runtimeType removeFavourites complete in ${stopwatch.elapsed.inMilliseconds}ms with ${favouriteRows.length} entries');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:aves/model/collection_filters.dart';
|
|
||||||
import 'package:aves/model/collection_lens.dart';
|
import 'package:aves/model/collection_lens.dart';
|
||||||
import 'package:aves/model/collection_source.dart';
|
import 'package:aves/model/collection_source.dart';
|
||||||
|
import 'package:aves/model/filters/album.dart';
|
||||||
|
import 'package:aves/model/filters/country.dart';
|
||||||
|
import 'package:aves/model/filters/favourite.dart';
|
||||||
|
import 'package:aves/model/filters/filters.dart';
|
||||||
|
import 'package:aves/model/filters/gif.dart';
|
||||||
|
import 'package:aves/model/filters/tag.dart';
|
||||||
|
import 'package:aves/model/filters/video.dart';
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/utils/color_utils.dart';
|
import 'package:aves/utils/color_utils.dart';
|
||||||
|
@ -81,6 +87,12 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
|
||||||
title: 'GIFs',
|
title: 'GIFs',
|
||||||
filter: GifFilter(),
|
filter: GifFilter(),
|
||||||
);
|
);
|
||||||
|
final favouriteEntry = _FilteredCollectionNavTile(
|
||||||
|
source: source,
|
||||||
|
leading: const Icon(OMIcons.favoriteBorder),
|
||||||
|
title: 'Favourites',
|
||||||
|
filter: FavouriteFilter(),
|
||||||
|
);
|
||||||
final buildAlbumEntry = (album) => _FilteredCollectionNavTile(
|
final buildAlbumEntry = (album) => _FilteredCollectionNavTile(
|
||||||
source: source,
|
source: source,
|
||||||
leading: IconUtils.getAlbumIcon(context: context, album: album),
|
leading: IconUtils.getAlbumIcon(context: context, album: album),
|
||||||
|
@ -131,6 +143,7 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
|
||||||
allMediaEntry,
|
allMediaEntry,
|
||||||
videoEntry,
|
videoEntry,
|
||||||
gifEntry,
|
gifEntry,
|
||||||
|
favouriteEntry,
|
||||||
if (specialAlbums.isNotEmpty) ...[
|
if (specialAlbums.isNotEmpty) ...[
|
||||||
const Divider(),
|
const Divider(),
|
||||||
...specialAlbums.map(buildAlbumEntry),
|
...specialAlbums.map(buildAlbumEntry),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:aves/model/collection_filters.dart';
|
|
||||||
import 'package:aves/model/collection_lens.dart';
|
import 'package:aves/model/collection_lens.dart';
|
||||||
|
import 'package:aves/model/filters/query.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/widgets/album/thumbnail_collection.dart';
|
import 'package:aves/widgets/album/thumbnail_collection.dart';
|
||||||
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
|
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/model/collection_filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:outline_material_icons/outline_material_icons.dart';
|
import 'package:outline_material_icons/outline_material_icons.dart';
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:aves/model/collection_lens.dart';
|
import 'package:aves/model/collection_lens.dart';
|
||||||
import 'package:aves/model/collection_source.dart';
|
import 'package:aves/model/collection_source.dart';
|
||||||
|
import 'package:aves/model/favourite_repo.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/image_file_service.dart';
|
import 'package:aves/model/image_file_service.dart';
|
||||||
import 'package:aves/model/metadata_db.dart';
|
import 'package:aves/model/metadata_db.dart';
|
||||||
|
@ -39,6 +40,7 @@ class _MediaStoreCollectionProviderState extends State<MediaStoreCollectionProvi
|
||||||
);
|
);
|
||||||
|
|
||||||
await metadataDb.init(); // <20ms
|
await metadataDb.init(); // <20ms
|
||||||
|
await favourites.init();
|
||||||
final currentTimeZone = await FlutterNativeTimezone.getLocalTimezone(); // <20ms
|
final currentTimeZone = await FlutterNativeTimezone.getLocalTimezone(); // <20ms
|
||||||
final catalogTimeZone = settings.catalogTimeZone;
|
final catalogTimeZone = settings.catalogTimeZone;
|
||||||
if (currentTimeZone != catalogTimeZone) {
|
if (currentTimeZone != catalogTimeZone) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/model/collection_source.dart';
|
import 'package:aves/model/collection_source.dart';
|
||||||
|
import 'package:aves/model/favourite_repo.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/image_metadata.dart';
|
import 'package:aves/model/image_metadata.dart';
|
||||||
import 'package:aves/model/metadata_db.dart';
|
import 'package:aves/model/metadata_db.dart';
|
||||||
|
@ -22,6 +23,7 @@ class DebugPageState extends State<DebugPage> {
|
||||||
Future<int> _dbFileSizeLoader;
|
Future<int> _dbFileSizeLoader;
|
||||||
Future<List<CatalogMetadata>> _dbMetadataLoader;
|
Future<List<CatalogMetadata>> _dbMetadataLoader;
|
||||||
Future<List<AddressDetails>> _dbAddressLoader;
|
Future<List<AddressDetails>> _dbAddressLoader;
|
||||||
|
Future<List<FavouriteRow>> _dbFavouritesLoader;
|
||||||
|
|
||||||
List<ImageEntry> get entries => widget.source.entries;
|
List<ImageEntry> get entries => widget.source.entries;
|
||||||
|
|
||||||
|
@ -86,6 +88,14 @@ class DebugPageState extends State<DebugPage> {
|
||||||
return Text('DB address rows: ${snapshot.data.length}');
|
return Text('DB address rows: ${snapshot.data.length}');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
FutureBuilder(
|
||||||
|
future: _dbFavouritesLoader,
|
||||||
|
builder: (context, AsyncSnapshot<List<FavouriteRow>> snapshot) {
|
||||||
|
if (snapshot.hasError) return Text(snapshot.error.toString());
|
||||||
|
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
|
||||||
|
return Text('DB favourite rows: ${snapshot.data.length} (${favourites.count} in memory)');
|
||||||
|
},
|
||||||
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
Text('Image cache: ${imageCache.currentSize} items, ${formatFilesize(imageCache.currentSizeBytes)}'),
|
Text('Image cache: ${imageCache.currentSize} items, ${formatFilesize(imageCache.currentSizeBytes)}'),
|
||||||
Text('SVG cache: ${PictureProvider.cacheCount} items'),
|
Text('SVG cache: ${PictureProvider.cacheCount} items'),
|
||||||
|
@ -110,6 +120,7 @@ class DebugPageState extends State<DebugPage> {
|
||||||
_dbFileSizeLoader = metadataDb.dbFileSize();
|
_dbFileSizeLoader = metadataDb.dbFileSize();
|
||||||
_dbMetadataLoader = metadataDb.loadMetadataEntries();
|
_dbMetadataLoader = metadataDb.loadMetadataEntries();
|
||||||
_dbAddressLoader = metadataDb.loadAddresses();
|
_dbAddressLoader = metadataDb.loadAddresses();
|
||||||
|
_dbFavouritesLoader = metadataDb.loadFavourites();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import 'package:pdf/widgets.dart' as pdf;
|
||||||
import 'package:pedantic/pedantic.dart';
|
import 'package:pedantic/pedantic.dart';
|
||||||
import 'package:printing/printing.dart';
|
import 'package:printing/printing.dart';
|
||||||
|
|
||||||
enum FullscreenAction { delete, edit, info, open, openMap, print, rename, rotateCCW, rotateCW, setAs, share }
|
enum FullscreenAction { delete, edit, info, open, openMap, print, rename, rotateCCW, rotateCW, setAs, share, toggleFavourite }
|
||||||
|
|
||||||
class FullscreenActionDelegate {
|
class FullscreenActionDelegate {
|
||||||
final CollectionLens collection;
|
final CollectionLens collection;
|
||||||
|
@ -25,6 +25,9 @@ class FullscreenActionDelegate {
|
||||||
|
|
||||||
void onActionSelected(BuildContext context, ImageEntry entry, FullscreenAction action) {
|
void onActionSelected(BuildContext context, ImageEntry entry, FullscreenAction action) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
case FullscreenAction.toggleFavourite:
|
||||||
|
entry.toggleFavourite();
|
||||||
|
break;
|
||||||
case FullscreenAction.delete:
|
case FullscreenAction.delete:
|
||||||
_showDeleteDialog(context, entry);
|
_showDeleteDialog(context, entry);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
import 'package:aves/model/collection_filters.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
|
import 'package:aves/model/filters/favourite.dart';
|
||||||
|
import 'package:aves/model/filters/gif.dart';
|
||||||
|
import 'package:aves/model/filters/tag.dart';
|
||||||
|
import 'package:aves/model/filters/video.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/utils/file_utils.dart';
|
import 'package:aves/utils/file_utils.dart';
|
||||||
import 'package:aves/widgets/common/aves_filter_chip.dart';
|
import 'package:aves/widgets/common/aves_filter_chip.dart';
|
||||||
|
@ -25,11 +29,6 @@ class BasicSection extends StatelessWidget {
|
||||||
final resolutionText = '${entry.width ?? '?'} × ${entry.height ?? '?'}${showMegaPixels ? ' (${entry.megaPixels} MP)' : ''}';
|
final resolutionText = '${entry.width ?? '?'} × ${entry.height ?? '?'}${showMegaPixels ? ' (${entry.megaPixels} MP)' : ''}';
|
||||||
|
|
||||||
final tags = entry.xmpSubjects..sort(compareAsciiUpperCase);
|
final tags = entry.xmpSubjects..sort(compareAsciiUpperCase);
|
||||||
final filters = [
|
|
||||||
if (entry.directory != null) AlbumFilter(entry.directory),
|
|
||||||
...tags.map((tag) => TagFilter(tag)),
|
|
||||||
];
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
@ -42,19 +41,31 @@ class BasicSection extends StatelessWidget {
|
||||||
'URI': entry.uri ?? '?',
|
'URI': entry.uri ?? '?',
|
||||||
if (entry.path != null) 'Path': entry.path,
|
if (entry.path != null) 'Path': entry.path,
|
||||||
}),
|
}),
|
||||||
if (filters.isNotEmpty != null)
|
ValueListenableBuilder(
|
||||||
Padding(
|
valueListenable: entry.isFavouriteNotifier,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: AvesFilterChip.buttonBorderWidth / 2) + const EdgeInsets.only(top: 8),
|
builder: (context, isFavourite, child) {
|
||||||
child: Wrap(
|
final filters = [
|
||||||
spacing: 8,
|
if (entry.isVideo) VideoFilter(),
|
||||||
children: filters
|
if (entry.isGif) GifFilter(),
|
||||||
.map((filter) => AvesFilterChip(
|
if (isFavourite) FavouriteFilter(),
|
||||||
filter: filter,
|
if (entry.directory != null) AlbumFilter(entry.directory),
|
||||||
onPressed: onFilter,
|
...tags.map((tag) => TagFilter(tag)),
|
||||||
))
|
]..sort();
|
||||||
.toList(),
|
if (filters.isEmpty) return const SizedBox.shrink();
|
||||||
),
|
return Padding(
|
||||||
),
|
padding: const EdgeInsets.symmetric(horizontal: AvesFilterChip.buttonBorderWidth / 2) + const EdgeInsets.only(top: 8),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
children: filters
|
||||||
|
.map((filter) => AvesFilterChip(
|
||||||
|
filter: filter,
|
||||||
|
onPressed: onFilter,
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:aves/model/collection_filters.dart';
|
|
||||||
import 'package:aves/model/collection_lens.dart';
|
import 'package:aves/model/collection_lens.dart';
|
||||||
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/widgets/album/collection_page.dart';
|
import 'package:aves/widgets/album/collection_page.dart';
|
||||||
import 'package:aves/widgets/common/aves_filter_chip.dart';
|
import 'package:aves/widgets/common/aves_filter_chip.dart';
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:aves/model/collection_filters.dart';
|
|
||||||
import 'package:aves/model/collection_lens.dart';
|
import 'package:aves/model/collection_lens.dart';
|
||||||
|
import 'package:aves/model/filters/country.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings.dart';
|
||||||
import 'package:aves/utils/android_app_service.dart';
|
import 'package:aves/utils/android_app_service.dart';
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/widgets/common/menu_row.dart';
|
import 'package:aves/widgets/common/menu_row.dart';
|
||||||
import 'package:aves/widgets/fullscreen/fullscreen_action_delegate.dart';
|
import 'package:aves/widgets/fullscreen/fullscreen_action_delegate.dart';
|
||||||
import 'package:aves/widgets/fullscreen/overlay/common.dart';
|
import 'package:aves/widgets/fullscreen/overlay/common.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:outline_material_icons/outline_material_icons.dart';
|
import 'package:outline_material_icons/outline_material_icons.dart';
|
||||||
|
|
||||||
|
@ -37,6 +38,18 @@ class FullscreenTopOverlay extends StatelessWidget {
|
||||||
child: ModalRoute.of(context)?.canPop ?? true ? const BackButton() : const CloseButton(),
|
child: ModalRoute.of(context)?.canPop ?? true ? const BackButton() : const CloseButton(),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
OverlayButton(
|
||||||
|
scale: scale,
|
||||||
|
child: ValueListenableBuilder(
|
||||||
|
valueListenable: entry.isFavouriteNotifier,
|
||||||
|
builder: (context, isFavourite, child) => IconButton(
|
||||||
|
icon: Icon(isFavourite ? Icons.favorite : Icons.favorite_border),
|
||||||
|
onPressed: () => onActionSelected?.call(FullscreenAction.toggleFavourite),
|
||||||
|
tooltip: isFavourite ? 'Remove favourite' : 'Add favourite',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
OverlayButton(
|
OverlayButton(
|
||||||
scale: scale,
|
scale: scale,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
|
|
Loading…
Reference in a new issue