Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2020-08-30 21:49:35 +09:00
commit c86f52b61a
37 changed files with 632 additions and 332 deletions

View file

@ -16,7 +16,7 @@ jobs:
- uses: subosito/flutter-action@v1 - uses: subosito/flutter-action@v1
with: with:
channel: 'stable' flutter-version: '1.17.5'
# Workaround for this Android Gradle Plugin issue (supposedly fixed in AGP 4.1): # Workaround for this Android Gradle Plugin issue (supposedly fixed in AGP 4.1):
# https://issuetracker.google.com/issues/144111441 # https://issuetracker.google.com/issues/144111441
@ -49,8 +49,8 @@ jobs:
echo "${{ secrets.KEY_JKS }}" > release.keystore.asc echo "${{ secrets.KEY_JKS }}" > release.keystore.asc
gpg -d --passphrase "${{ secrets.KEY_JKS_PASSPHRASE }}" --batch release.keystore.asc > $AVES_STORE_FILE gpg -d --passphrase "${{ secrets.KEY_JKS_PASSPHRASE }}" --batch release.keystore.asc > $AVES_STORE_FILE
rm release.keystore.asc rm release.keystore.asc
flutter build apk --bundle-sksl-path shaders.sksl.json flutter build apk --bundle-sksl-path shaders_1.17.5.sksl.json
flutter build appbundle --bundle-sksl-path shaders.sksl.json flutter build appbundle --bundle-sksl-path shaders_1.17.5.sksl.json
rm $AVES_STORE_FILE rm $AVES_STORE_FILE
env: env:
AVES_STORE_FILE: ${{ github.workspace }}/key.jks AVES_STORE_FILE: ${{ github.workspace }}/key.jks

View file

@ -6,8 +6,11 @@ import android.content.Intent;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo; import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -27,6 +30,7 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -110,14 +114,41 @@ public class AppAdapterHandler implements MethodChannel.MethodCallHandler {
Intent intent = new Intent(Intent.ACTION_MAIN, null); Intent intent = new Intent(Intent.ACTION_MAIN, null);
intent.addCategory(Intent.CATEGORY_LAUNCHER); intent.addCategory(Intent.CATEGORY_LAUNCHER);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
PackageManager packageManager = context.getPackageManager();
List<ResolveInfo> resolveInfoList = packageManager.queryIntentActivities(intent, 0); // apps tend to use their name in English when creating folders
// so we get their names in English as well as the current locale
Configuration config = new Configuration();
config.setLocale(Locale.ENGLISH);
PackageManager pm = context.getPackageManager();
List<ResolveInfo> resolveInfoList = pm.queryIntentActivities(intent, 0);
for (ResolveInfo resolveInfo : resolveInfoList) { for (ResolveInfo resolveInfo : resolveInfoList) {
ApplicationInfo applicationInfo = resolveInfo.activityInfo.applicationInfo; ApplicationInfo ai = resolveInfo.activityInfo.applicationInfo;
boolean isSystemPackage = (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; boolean isSystemPackage = (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
if (!isSystemPackage) { if (!isSystemPackage) {
String appName = String.valueOf(packageManager.getApplicationLabel(applicationInfo)); String packageName = ai.packageName;
nameMap.put(appName, applicationInfo.packageName);
String currentLabel = String.valueOf(pm.getApplicationLabel(ai));
nameMap.put(currentLabel, packageName);
int labelRes = ai.labelRes;
if (labelRes != 0) {
try {
Resources resources = pm.getResourcesForApplication(ai);
// `updateConfiguration` is deprecated but it seems to be the only way
// to query resources from another app with a specific locale.
// The following methods do not work:
// - `resources.getConfiguration().setLocale(...)`
// - getting a package manager from a custom context with `context.createConfigurationContext(config)`
resources.updateConfiguration(config, resources.getDisplayMetrics());
String englishLabel = resources.getString(labelRes);
if (!TextUtils.equals(englishLabel, currentLabel)) {
nameMap.put(englishLabel, packageName);
}
} catch (PackageManager.NameNotFoundException e) {
Log.w(LOG_TAG, "failed to get app englishLabel for packageName=" + packageName, e);
}
}
} }
} }
return nameMap; return nameMap;

View file

@ -1,4 +1,5 @@
import 'package:aves/model/settings.dart'; import 'package:aves/model/settings.dart';
import 'package:aves/widgets/common/data_providers/settings_provider.dart';
import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/home_page.dart'; import 'package:aves/widgets/home_page.dart';
import 'package:aves/widgets/welcome_page.dart'; import 'package:aves/widgets/welcome_page.dart';
@ -37,7 +38,10 @@ class _AvesAppState extends State<AvesApp> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( // place the settings provider above `MaterialApp`
// so it can be used during navigation transitions
return SettingsProvider(
child: MaterialApp(
title: 'Aves', title: 'Aves',
theme: ThemeData( theme: ThemeData(
brightness: Brightness.dark, brightness: Brightness.dark,
@ -66,6 +70,7 @@ class _AvesAppState extends State<AvesApp> {
return settings.hasAcceptedTerms ? HomePage() : WelcomePage(); return settings.hasAcceptedTerms ? HomePage() : WelcomePage();
}, },
), ),
),
); );
} }
} }

View file

@ -11,11 +11,9 @@ final Settings settings = Settings._private();
typedef SettingsCallback = void Function(String key, dynamic oldValue, dynamic newValue); typedef SettingsCallback = void Function(String key, dynamic oldValue, dynamic newValue);
class Settings { class Settings extends ChangeNotifier {
static SharedPreferences _prefs; static SharedPreferences _prefs;
final ObserverList<SettingsCallback> _listeners = ObserverList<SettingsCallback>();
Settings._private(); Settings._private();
// preferences // preferences
@ -28,6 +26,8 @@ class Settings {
static const infoMapZoomKey = 'info_map_zoom'; static const infoMapZoomKey = 'info_map_zoom';
static const launchPageKey = 'launch_page'; static const launchPageKey = 'launch_page';
static const coordinateFormatKey = 'coordinates_format'; static const coordinateFormatKey = 'coordinates_format';
static const svgBackgroundKey = 'svg_background';
static const albumSortFactorKey = 'album_sort_factor';
Future<void> init() async { Future<void> init() async {
_prefs = await SharedPreferences.getInstance(); _prefs = await SharedPreferences.getInstance();
@ -37,37 +37,17 @@ class Settings {
return _prefs.clear(); return _prefs.clear();
} }
void addListener(SettingsCallback listener) => _listeners.add(listener);
void removeListener(SettingsCallback listener) => _listeners.remove(listener);
void notifyListeners(String key, dynamic oldValue, dynamic newValue) {
debugPrint('$runtimeType notifyListeners key=$key, old=$oldValue, new=$newValue');
if (_listeners != null) {
final localListeners = _listeners.toList();
for (final listener in localListeners) {
try {
if (_listeners.contains(listener)) {
listener(key, oldValue, newValue);
}
} catch (exception, stack) {
debugPrint('$runtimeType failed to notify listeners with exception=$exception\n$stack');
}
}
}
}
String get catalogTimeZone => _prefs.getString(catalogTimeZoneKey) ?? ''; String get catalogTimeZone => _prefs.getString(catalogTimeZoneKey) ?? '';
set catalogTimeZone(String newValue) => setAndNotify(catalogTimeZoneKey, newValue); set catalogTimeZone(String newValue) => setAndNotify(catalogTimeZoneKey, newValue);
GroupFactor get collectionGroupFactor => getEnumOrDefault(collectionGroupFactorKey, GroupFactor.month, GroupFactor.values); EntryGroupFactor get collectionGroupFactor => getEnumOrDefault(collectionGroupFactorKey, EntryGroupFactor.month, EntryGroupFactor.values);
set collectionGroupFactor(GroupFactor newValue) => setAndNotify(collectionGroupFactorKey, newValue.toString()); set collectionGroupFactor(EntryGroupFactor newValue) => setAndNotify(collectionGroupFactorKey, newValue.toString());
SortFactor get collectionSortFactor => getEnumOrDefault(collectionSortFactorKey, SortFactor.date, SortFactor.values); EntrySortFactor get collectionSortFactor => getEnumOrDefault(collectionSortFactorKey, EntrySortFactor.date, EntrySortFactor.values);
set collectionSortFactor(SortFactor newValue) => setAndNotify(collectionSortFactorKey, newValue.toString()); set collectionSortFactor(EntrySortFactor newValue) => setAndNotify(collectionSortFactorKey, newValue.toString());
double get collectionTileExtent => _prefs.getDouble(collectionTileExtentKey) ?? 0; double get collectionTileExtent => _prefs.getDouble(collectionTileExtentKey) ?? 0;
@ -93,6 +73,14 @@ class Settings {
set coordinateFormat(CoordinateFormat newValue) => setAndNotify(coordinateFormatKey, newValue.toString()); set coordinateFormat(CoordinateFormat newValue) => setAndNotify(coordinateFormatKey, newValue.toString());
int get svgBackground => _prefs.getInt(svgBackgroundKey) ?? 0xFFFFFFFF;
set svgBackground(int newValue) => setAndNotify(svgBackgroundKey, newValue);
ChipSortFactor get albumSortFactor => getEnumOrDefault(albumSortFactorKey, ChipSortFactor.date, ChipSortFactor.values);
set albumSortFactor(ChipSortFactor newValue) => setAndNotify(albumSortFactorKey, newValue.toString());
// convenience methods // convenience methods
// ignore: avoid_positional_boolean_parameters // ignore: avoid_positional_boolean_parameters
@ -133,7 +121,7 @@ class Settings {
_prefs.setBool(key, newValue); _prefs.setBool(key, newValue);
} }
if (oldValue != newValue) { if (oldValue != newValue) {
notifyListeners(key, oldValue, newValue); notifyListeners();
} }
} }
} }

View file

@ -17,8 +17,8 @@ import 'enums.dart';
class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSelectionMixin { class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSelectionMixin {
final CollectionSource source; final CollectionSource source;
final Set<CollectionFilter> filters; final Set<CollectionFilter> filters;
GroupFactor groupFactor; EntryGroupFactor groupFactor;
SortFactor sortFactor; EntrySortFactor sortFactor;
final AChangeNotifier filterChangeNotifier = AChangeNotifier(); final AChangeNotifier filterChangeNotifier = AChangeNotifier();
final StreamController<ImageEntry> _highlightController = StreamController.broadcast(); final StreamController<ImageEntry> _highlightController = StreamController.broadcast();
@ -30,11 +30,11 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
CollectionLens({ CollectionLens({
@required this.source, @required this.source,
Iterable<CollectionFilter> filters, Iterable<CollectionFilter> filters,
@required GroupFactor groupFactor, @required EntryGroupFactor groupFactor,
@required SortFactor sortFactor, @required EntrySortFactor sortFactor,
}) : filters = {if (filters != null) ...filters.where((f) => f != null)}, }) : filters = {if (filters != null) ...filters.where((f) => f != null)},
groupFactor = groupFactor ?? GroupFactor.month, groupFactor = groupFactor ?? EntryGroupFactor.month,
sortFactor = sortFactor ?? SortFactor.date { sortFactor = sortFactor ?? EntrySortFactor.date {
_subscriptions.add(source.eventBus.on<EntryAddedEvent>().listen((e) => _refresh())); _subscriptions.add(source.eventBus.on<EntryAddedEvent>().listen((e) => _refresh()));
_subscriptions.add(source.eventBus.on<EntryRemovedEvent>().listen((e) => onEntryRemoved(e.entries))); _subscriptions.add(source.eventBus.on<EntryRemovedEvent>().listen((e) => onEntryRemoved(e.entries)));
_subscriptions.add(source.eventBus.on<EntryMovedEvent>().listen((e) => _refresh())); _subscriptions.add(source.eventBus.on<EntryMovedEvent>().listen((e) => _refresh()));
@ -85,11 +85,11 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
void highlight(ImageEntry entry) => _highlightController.add(entry); void highlight(ImageEntry entry) => _highlightController.add(entry);
bool get showHeaders { bool get showHeaders {
if (sortFactor == SortFactor.size) return false; if (sortFactor == EntrySortFactor.size) return false;
if (sortFactor == SortFactor.date && groupFactor == GroupFactor.none) return false; if (sortFactor == EntrySortFactor.date && groupFactor == EntryGroupFactor.none) return false;
final albumSections = sortFactor == SortFactor.name || (sortFactor == SortFactor.date && groupFactor == GroupFactor.album); final albumSections = sortFactor == EntrySortFactor.name || (sortFactor == EntrySortFactor.date && groupFactor == EntryGroupFactor.album);
final filterByAlbum = filters.any((f) => f is AlbumFilter); final filterByAlbum = filters.any((f) => f is AlbumFilter);
if (albumSections && filterByAlbum) return false; if (albumSections && filterByAlbum) return false;
@ -118,13 +118,13 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
filterChangeNotifier.notifyListeners(); filterChangeNotifier.notifyListeners();
} }
void sort(SortFactor sortFactor) { void sort(EntrySortFactor sortFactor) {
this.sortFactor = sortFactor; this.sortFactor = sortFactor;
_applySort(); _applySort();
_applyGroup(); _applyGroup();
} }
void group(GroupFactor groupFactor) { void group(EntryGroupFactor groupFactor) {
this.groupFactor = groupFactor; this.groupFactor = groupFactor;
_applyGroup(); _applyGroup();
} }
@ -136,16 +136,16 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
void _applySort() { void _applySort() {
switch (sortFactor) { switch (sortFactor) {
case SortFactor.date: case EntrySortFactor.date:
_filteredEntries.sort((a, b) { _filteredEntries.sort((a, b) {
final c = b.bestDate?.compareTo(a.bestDate) ?? -1; final c = b.bestDate?.compareTo(a.bestDate) ?? -1;
return c != 0 ? c : compareAsciiUpperCase(a.bestTitle, b.bestTitle); return c != 0 ? c : compareAsciiUpperCase(a.bestTitle, b.bestTitle);
}); });
break; break;
case SortFactor.size: case EntrySortFactor.size:
_filteredEntries.sort((a, b) => b.sizeBytes.compareTo(a.sizeBytes)); _filteredEntries.sort((a, b) => b.sizeBytes.compareTo(a.sizeBytes));
break; break;
case SortFactor.name: case EntrySortFactor.name:
_filteredEntries.sort((a, b) => compareAsciiUpperCase(a.bestTitle, b.bestTitle)); _filteredEntries.sort((a, b) => compareAsciiUpperCase(a.bestTitle, b.bestTitle));
break; break;
} }
@ -153,30 +153,30 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
void _applyGroup() { void _applyGroup() {
switch (sortFactor) { switch (sortFactor) {
case SortFactor.date: case EntrySortFactor.date:
switch (groupFactor) { switch (groupFactor) {
case GroupFactor.album: case EntryGroupFactor.album:
sections = groupBy<ImageEntry, String>(_filteredEntries, (entry) => entry.directory); sections = groupBy<ImageEntry, String>(_filteredEntries, (entry) => entry.directory);
break; break;
case GroupFactor.month: case EntryGroupFactor.month:
sections = groupBy<ImageEntry, DateTime>(_filteredEntries, (entry) => entry.monthTaken); sections = groupBy<ImageEntry, DateTime>(_filteredEntries, (entry) => entry.monthTaken);
break; break;
case GroupFactor.day: case EntryGroupFactor.day:
sections = groupBy<ImageEntry, DateTime>(_filteredEntries, (entry) => entry.dayTaken); sections = groupBy<ImageEntry, DateTime>(_filteredEntries, (entry) => entry.dayTaken);
break; break;
case GroupFactor.none: case EntryGroupFactor.none:
sections = Map.fromEntries([ sections = Map.fromEntries([
MapEntry(null, _filteredEntries), MapEntry(null, _filteredEntries),
]); ]);
break; break;
} }
break; break;
case SortFactor.size: case EntrySortFactor.size:
sections = Map.fromEntries([ sections = Map.fromEntries([
MapEntry(null, _filteredEntries), MapEntry(null, _filteredEntries),
]); ]);
break; break;
case SortFactor.name: case EntrySortFactor.name:
final byAlbum = groupBy<ImageEntry, String>(_filteredEntries, (entry) => entry.directory); final byAlbum = groupBy<ImageEntry, String>(_filteredEntries, (entry) => entry.directory);
int compare(a, b) { int compare(a, b) {
final ua = source.getUniqueAlbumName(a); final ua = source.getUniqueAlbumName(a);

View file

@ -39,8 +39,8 @@ class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin {
@override @override
List<ImageEntry> get sortedEntriesForFilterList => CollectionLens( List<ImageEntry> get sortedEntriesForFilterList => CollectionLens(
source: this, source: this,
groupFactor: GroupFactor.month, groupFactor: EntryGroupFactor.none,
sortFactor: SortFactor.date, sortFactor: EntrySortFactor.date,
).sortedEntries; ).sortedEntries;
ValueNotifier<SourceState> stateNotifier = ValueNotifier<SourceState>(SourceState.ready); ValueNotifier<SourceState> stateNotifier = ValueNotifier<SourceState>(SourceState.ready);

View file

@ -1,5 +1,7 @@
enum SortFactor { date, size, name }
enum GroupFactor { none, album, month, day }
enum Activity { browse, select } enum Activity { browse, select }
enum ChipSortFactor { date, name }
enum EntrySortFactor { date, size, name }
enum EntryGroupFactor { none, album, month, day }

View file

@ -1,5 +1,6 @@
import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/android_app_service.dart';
import 'package:aves/services/android_file_service.dart'; import 'package:aves/services/android_file_service.dart';
import 'package:aves/utils/change_notifier.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
final AndroidFileUtils androidFileUtils = AndroidFileUtils._private(); final AndroidFileUtils androidFileUtils = AndroidFileUtils._private();
@ -9,6 +10,8 @@ class AndroidFileUtils {
Set<StorageVolume> storageVolumes = {}; Set<StorageVolume> storageVolumes = {};
Map appNameMap = {}; Map appNameMap = {};
AChangeNotifier appNameChangeNotifier = AChangeNotifier();
AndroidFileUtils._private(); AndroidFileUtils._private();
Future<void> init() async { Future<void> init() async {
@ -19,8 +22,12 @@ class AndroidFileUtils {
downloadPath = join(primaryStorage, 'Download'); downloadPath = join(primaryStorage, 'Download');
moviesPath = join(primaryStorage, 'Movies'); moviesPath = join(primaryStorage, 'Movies');
picturesPath = join(primaryStorage, 'Pictures'); picturesPath = join(primaryStorage, 'Pictures');
}
Future<void> initAppNames() async {
appNameMap = await AndroidAppService.getAppNames() appNameMap = await AndroidAppService.getAppNames()
..addAll({'KakaoTalkDownload': 'com.kakao.talk'}); ..addAll({'KakaoTalkDownload': 'com.kakao.talk'});
appNameChangeNotifier.notifyListeners();
} }
bool isCameraPath(String path) => path != null && path.startsWith(dcimPath) && (path.endsWith('Camera') || path.endsWith('100ANDRO')); bool isCameraPath(String path) => path != null && path.startsWith(dcimPath) && (path.endsWith('Camera') || path.endsWith('100ANDRO'));

View file

@ -19,9 +19,6 @@ class Constants {
], ],
); );
static const svgBackground = Colors.white;
static const svgColorFilter = ColorFilter.mode(svgBackground, BlendMode.dstOver);
static const List<Dependency> androidDependencies = [ static const List<Dependency> androidDependencies = [
Dependency( Dependency(
name: 'CWAC-Document', name: 'CWAC-Document',

View file

@ -7,9 +7,9 @@ import 'package:aves/model/source/enums.dart';
import 'package:aves/utils/durations.dart'; import 'package:aves/utils/durations.dart';
import 'package:aves/widgets/album/filter_bar.dart'; import 'package:aves/widgets/album/filter_bar.dart';
import 'package:aves/widgets/album/search/search_delegate.dart'; import 'package:aves/widgets/album/search/search_delegate.dart';
import 'package:aves/widgets/common/action_delegates/group_collection_dialog.dart'; import 'package:aves/widgets/common/action_delegates/collection_group_dialog.dart';
import 'package:aves/widgets/common/action_delegates/collection_sort_dialog.dart';
import 'package:aves/widgets/common/action_delegates/selection_action_delegate.dart'; import 'package:aves/widgets/common/action_delegates/selection_action_delegate.dart';
import 'package:aves/widgets/common/action_delegates/sort_collection_dialog.dart';
import 'package:aves/widgets/common/app_bar_subtitle.dart'; import 'package:aves/widgets/common/app_bar_subtitle.dart';
import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart'; import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart';
import 'package:aves/widgets/common/entry_actions.dart'; import 'package:aves/widgets/common/entry_actions.dart';
@ -196,7 +196,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
value: CollectionAction.sort, value: CollectionAction.sort,
child: MenuRow(text: 'Sort...', icon: AIcons.sort), child: MenuRow(text: 'Sort...', icon: AIcons.sort),
), ),
if (collection.sortFactor == SortFactor.date) if (collection.sortFactor == EntrySortFactor.date)
PopupMenuItem( PopupMenuItem(
key: Key('menu-group'), key: Key('menu-group'),
value: CollectionAction.group, value: CollectionAction.group,
@ -296,9 +296,9 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
unawaited(_goToStats()); unawaited(_goToStats());
break; break;
case CollectionAction.group: case CollectionAction.group:
final factor = await showDialog<GroupFactor>( final factor = await showDialog<EntryGroupFactor>(
context: context, context: context,
builder: (context) => GroupCollectionDialog(), builder: (context) => CollectionGroupDialog(),
); );
if (factor != null) { if (factor != null) {
settings.collectionGroupFactor = factor; settings.collectionGroupFactor = factor;
@ -306,9 +306,9 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
} }
break; break;
case CollectionAction.sort: case CollectionAction.sort:
final factor = await showDialog<SortFactor>( final factor = await showDialog<EntrySortFactor>(
context: context, context: context,
builder: (context) => SortCollectionDialog(), builder: (context) => CollectionSortDialog(initialValue: settings.collectionSortFactor),
); );
if (factor != null) { if (factor != null) {
settings.collectionSortFactor = factor; settings.collectionSortFactor = factor;

View file

@ -29,24 +29,24 @@ class SectionHeader extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget header; Widget header;
switch (collection.sortFactor) { switch (collection.sortFactor) {
case SortFactor.date: case EntrySortFactor.date:
switch (collection.groupFactor) { switch (collection.groupFactor) {
case GroupFactor.album: case EntryGroupFactor.album:
header = _buildAlbumSectionHeader(); header = _buildAlbumSectionHeader();
break; break;
case GroupFactor.month: case EntryGroupFactor.month:
header = MonthSectionHeader(key: ValueKey(sectionKey), date: sectionKey as DateTime); header = MonthSectionHeader(key: ValueKey(sectionKey), date: sectionKey as DateTime);
break; break;
case GroupFactor.day: case EntryGroupFactor.day:
header = DaySectionHeader(key: ValueKey(sectionKey), date: sectionKey as DateTime); header = DaySectionHeader(key: ValueKey(sectionKey), date: sectionKey as DateTime);
break; break;
case GroupFactor.none: case EntryGroupFactor.none:
break; break;
} }
break; break;
case SortFactor.size: case EntrySortFactor.size:
break; break;
case SortFactor.name: case EntrySortFactor.name:
header = _buildAlbumSectionHeader(); header = _buildAlbumSectionHeader();
break; break;
} }
@ -87,7 +87,8 @@ class SectionHeader extends StatelessWidget {
// force a higher first line to match leading icon/selector dimension // force a higher first line to match leading icon/selector dimension
style: TextStyle(height: 2.3 * textScaleFactor), style: TextStyle(height: 2.3 * textScaleFactor),
), // 23 hair spaces match a width of 40.0 ), // 23 hair spaces match a width of 40.0
if (hasTrailing) TextSpan(text: '\u200A' * 17), if (hasTrailing)
TextSpan(text: '\u200A' * 17),
TextSpan( TextSpan(
text: text, text: text,
style: Constants.titleTextStyle, style: Constants.titleTextStyle,

View file

@ -1,8 +1,9 @@
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/utils/constants.dart'; import 'package:aves/model/settings.dart';
import 'package:aves/widgets/common/image_providers/uri_picture_provider.dart'; import 'package:aves/widgets/common/image_providers/uri_picture_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart';
class ThumbnailVectorImage extends StatelessWidget { class ThumbnailVectorImage extends StatelessWidget {
final ImageEntry entry; final ImageEntry entry;
@ -23,14 +24,20 @@ class ThumbnailVectorImage extends StatelessWidget {
// so that `SvgPicture` doesn't get aligned by the `Stack` like the overlay icons // so that `SvgPicture` doesn't get aligned by the `Stack` like the overlay icons
width: extent, width: extent,
height: extent, height: extent,
child: SvgPicture( child: Selector<Settings, int>(
selector: (context, s) => s.svgBackground,
builder: (context, svgBackground, child) {
final colorFilter = ColorFilter.mode(Color(svgBackground), BlendMode.dstOver);
return SvgPicture(
UriPicture( UriPicture(
uri: entry.uri, uri: entry.uri,
mimeType: entry.mimeType, mimeType: entry.mimeType,
colorFilter: Constants.svgColorFilter, colorFilter: colorFilter,
), ),
width: extent, width: extent,
height: extent, height: extent,
);
},
), ),
); );
return heroTag == null return heroTag == null

View file

@ -17,7 +17,9 @@ import 'package:aves/widgets/album/collection_page.dart';
import 'package:aves/widgets/common/aves_logo.dart'; import 'package:aves/widgets/common/aves_logo.dart';
import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/debug_page.dart'; import 'package:aves/widgets/debug_page.dart';
import 'package:aves/widgets/filter_grid_page.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/countries_page.dart';
import 'package:aves/widgets/filter_grids/tags_page.dart';
import 'package:aves/widgets/settings/settings_page.dart'; import 'package:aves/widgets/settings/settings_page.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View file

@ -1,22 +1,25 @@
import 'package:aves/model/settings.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import '../dialog.dart'; import '../dialog.dart';
class SortCollectionDialog extends StatefulWidget { class ChipSortDialog extends StatefulWidget {
final ChipSortFactor initialValue;
const ChipSortDialog({@required this.initialValue});
@override @override
_SortCollectionDialogState createState() => _SortCollectionDialogState(); _ChipSortDialogState createState() => _ChipSortDialogState();
} }
class _SortCollectionDialogState extends State<SortCollectionDialog> { class _ChipSortDialogState extends State<ChipSortDialog> {
SortFactor _selectedSort; ChipSortFactor _selectedSort;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_selectedSort = settings.collectionSortFactor; _selectedSort = widget.initialValue;
} }
@override @override
@ -24,9 +27,8 @@ class _SortCollectionDialogState extends State<SortCollectionDialog> {
return AvesDialog( return AvesDialog(
title: 'Sort', title: 'Sort',
scrollableContent: [ scrollableContent: [
_buildRadioListTile(SortFactor.date, 'By date'), _buildRadioListTile(ChipSortFactor.date, 'By date'),
_buildRadioListTile(SortFactor.size, 'By size'), _buildRadioListTile(ChipSortFactor.name, 'By name'),
_buildRadioListTile(SortFactor.name, 'By album & file name'),
], ],
actions: [ actions: [
FlatButton( FlatButton(
@ -42,7 +44,7 @@ class _SortCollectionDialogState extends State<SortCollectionDialog> {
); );
} }
Widget _buildRadioListTile(SortFactor value, String title) => RadioListTile<SortFactor>( Widget _buildRadioListTile(ChipSortFactor value, String title) => RadioListTile<ChipSortFactor>(
key: Key(value.toString()), key: Key(value.toString()),
value: value, value: value,
groupValue: _selectedSort, groupValue: _selectedSort,

View file

@ -5,13 +5,13 @@ import 'package:flutter/widgets.dart';
import '../dialog.dart'; import '../dialog.dart';
class GroupCollectionDialog extends StatefulWidget { class CollectionGroupDialog extends StatefulWidget {
@override @override
_GroupCollectionDialogState createState() => _GroupCollectionDialogState(); _CollectionGroupDialogState createState() => _CollectionGroupDialogState();
} }
class _GroupCollectionDialogState extends State<GroupCollectionDialog> { class _CollectionGroupDialogState extends State<CollectionGroupDialog> {
GroupFactor _selectedGroup; EntryGroupFactor _selectedGroup;
@override @override
void initState() { void initState() {
@ -24,10 +24,10 @@ class _GroupCollectionDialogState extends State<GroupCollectionDialog> {
return AvesDialog( return AvesDialog(
title: 'Group', title: 'Group',
scrollableContent: [ scrollableContent: [
_buildRadioListTile(GroupFactor.album, 'By album'), _buildRadioListTile(EntryGroupFactor.album, 'By album'),
_buildRadioListTile(GroupFactor.month, 'By month'), _buildRadioListTile(EntryGroupFactor.month, 'By month'),
_buildRadioListTile(GroupFactor.day, 'By day'), _buildRadioListTile(EntryGroupFactor.day, 'By day'),
_buildRadioListTile(GroupFactor.none, 'Do not group'), _buildRadioListTile(EntryGroupFactor.none, 'Do not group'),
], ],
actions: [ actions: [
FlatButton( FlatButton(
@ -43,7 +43,7 @@ class _GroupCollectionDialogState extends State<GroupCollectionDialog> {
); );
} }
Widget _buildRadioListTile(GroupFactor value, String title) => RadioListTile<GroupFactor>( Widget _buildRadioListTile(EntryGroupFactor value, String title) => RadioListTile<EntryGroupFactor>(
key: Key(value.toString()), key: Key(value.toString()),
value: value, value: value,
groupValue: _selectedGroup, groupValue: _selectedGroup,

View file

@ -0,0 +1,60 @@
import 'package:aves/model/source/enums.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import '../dialog.dart';
class CollectionSortDialog extends StatefulWidget {
final EntrySortFactor initialValue;
const CollectionSortDialog({@required this.initialValue});
@override
_CollectionSortDialogState createState() => _CollectionSortDialogState();
}
class _CollectionSortDialogState extends State<CollectionSortDialog> {
EntrySortFactor _selectedSort;
@override
void initState() {
super.initState();
_selectedSort = widget.initialValue;
}
@override
Widget build(BuildContext context) {
return AvesDialog(
title: 'Sort',
scrollableContent: [
_buildRadioListTile(EntrySortFactor.date, 'By date'),
_buildRadioListTile(EntrySortFactor.size, 'By size'),
_buildRadioListTile(EntrySortFactor.name, 'By album & file name'),
],
actions: [
FlatButton(
onPressed: () => Navigator.pop(context),
child: Text('Cancel'.toUpperCase()),
),
FlatButton(
key: Key('apply-button'),
onPressed: () => Navigator.pop(context, _selectedSort),
child: Text('Apply'.toUpperCase()),
),
],
);
}
Widget _buildRadioListTile(EntrySortFactor value, String title) => RadioListTile<EntrySortFactor>(
key: Key(value.toString()),
value: value,
groupValue: _selectedSort,
onChanged: (sort) => setState(() => _selectedSort = sort),
title: Text(
title,
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
);
}

View file

@ -17,7 +17,7 @@ import 'package:aves/widgets/common/action_delegates/permission_aware.dart';
import 'package:aves/widgets/common/dialog.dart'; import 'package:aves/widgets/common/dialog.dart';
import 'package:aves/widgets/common/entry_actions.dart'; import 'package:aves/widgets/common/entry_actions.dart';
import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/filter_grid_page.dart'; import 'package:aves/widgets/filter_grids/filter_grid_page.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';

View file

@ -0,0 +1,11 @@
import 'package:flutter/material.dart';
class AvesCircleBorder {
static BoxBorder build(BuildContext context) {
final subPixel = MediaQuery.of(context).devicePixelRatio > 2;
return Border.all(
color: Colors.white30,
width: subPixel ? 0.5 : 1.0,
);
}
}

View file

@ -0,0 +1,17 @@
import 'package:aves/model/settings.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
class SettingsProvider extends StatelessWidget {
final Widget child;
const SettingsProvider({@required this.child});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<Settings>.value(
value: settings,
child: child,
);
}
}

View file

@ -0,0 +1,128 @@
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/image_entry.dart';
import 'package:aves/model/settings.dart';
import 'package:aves/model/source/album.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/durations.dart';
import 'package:aves/widgets/album/empty.dart';
import 'package:aves/widgets/common/action_delegates/chip_sort_dialog.dart';
import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/common/menu_row.dart';
import 'package:aves/widgets/filter_grids/filter_grid_page.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart';
class AlbumListPage extends StatelessWidget {
final CollectionSource source;
const AlbumListPage({@required this.source});
@override
Widget build(BuildContext context) {
return Selector<Settings, ChipSortFactor>(
selector: (context, s) => s.albumSortFactor,
builder: (context, albumSortFactor, child) {
return AnimatedBuilder(
animation: androidFileUtils.appNameChangeNotifier,
builder: (context, child) => StreamBuilder(
stream: source.eventBus.on<AlbumsChangedEvent>(),
builder: (context, snapshot) {
return FilterNavigationPage(
source: source,
title: 'Albums',
actions: _buildActions(),
filterEntries: _getAlbumEntries(),
filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)),
emptyBuilder: () => EmptyContent(
icon: AIcons.album,
text: 'No albums',
),
);
},
),
);
},
);
}
Map<String, ImageEntry> _getAlbumEntries() {
final entriesByDate = source.sortedEntriesForFilterList;
final albumEntries = source.sortedAlbums.map((album) {
return MapEntry(
album,
entriesByDate.firstWhere((entry) => entry.directory == album, orElse: () => null),
);
}).toList();
switch (settings.albumSortFactor) {
case ChipSortFactor.date:
albumEntries.sort((a, b) {
final c = b.value.bestDate?.compareTo(a.value.bestDate) ?? -1;
return c != 0 ? c : compareAsciiUpperCase(a.key, b.key);
});
return Map.fromEntries(albumEntries);
case ChipSortFactor.name:
default:
final regularAlbums = <String>[], appAlbums = <String>[], specialAlbums = <String>[];
for (var album in source.sortedAlbums) {
switch (androidFileUtils.getAlbumType(album)) {
case AlbumType.regular:
regularAlbums.add(album);
break;
case AlbumType.app:
appAlbums.add(album);
break;
default:
specialAlbums.add(album);
break;
}
}
return Map.fromEntries([...specialAlbums, ...appAlbums, ...regularAlbums].map((album) {
return MapEntry(
album,
entriesByDate.firstWhere((entry) => entry.directory == album, orElse: () => null),
);
}));
}
}
List<Widget> _buildActions() {
return [
Builder(
builder: (context) => PopupMenuButton<ChipAction>(
key: Key('appbar-menu-button'),
itemBuilder: (context) {
return [
PopupMenuItem(
key: Key('menu-sort'),
value: ChipAction.sort,
child: MenuRow(text: 'Sort...', icon: AIcons.sort),
),
];
},
onSelected: (action) => _onChipActionSelected(context, action),
),
),
];
}
void _onChipActionSelected(BuildContext context, ChipAction action) async {
// wait for the popup menu to hide before proceeding with the action
await Future.delayed(Durations.popupMenuAnimation * timeDilation);
switch (action) {
case ChipAction.sort:
final factor = await showDialog<ChipSortFactor>(
context: context,
builder: (context) => ChipSortDialog(initialValue: settings.albumSortFactor),
);
if (factor != null) {
settings.albumSortFactor = factor;
}
break;
}
}
}

View file

@ -0,0 +1,30 @@
import 'package:aves/model/filters/location.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/location.dart';
import 'package:aves/widgets/album/empty.dart';
import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/filter_grids/filter_grid_page.dart';
import 'package:flutter/material.dart';
class CountryListPage extends StatelessWidget {
final CollectionSource source;
const CountryListPage({@required this.source});
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: source.eventBus.on<LocationsChangedEvent>(),
builder: (context, snapshot) => FilterNavigationPage(
source: source,
title: 'Countries',
filterEntries: source.getCountryEntries(),
filterBuilder: (s) => LocationFilter(LocationLevel.country, s),
emptyBuilder: () => EmptyContent(
icon: AIcons.location,
text: 'No countries',
),
),
);
}
}

View file

@ -0,0 +1,71 @@
import 'dart:ui';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/image_entry.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/album/thumbnail/raster.dart';
import 'package:aves/widgets/album/thumbnail/vector.dart';
import 'package:aves/widgets/common/aves_filter_chip.dart';
import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/filter_grids/filter_grid_page.dart';
import 'package:flutter/material.dart';
class DecoratedFilterChip extends StatelessWidget {
final CollectionSource source;
final CollectionFilter filter;
final ImageEntry entry;
final FilterCallback onPressed;
const DecoratedFilterChip({
@required this.source,
@required this.filter,
@required this.entry,
@required this.onPressed,
});
@override
Widget build(BuildContext context) {
Widget backgroundImage;
if (entry != null) {
backgroundImage = entry.isSvg
? ThumbnailVectorImage(
entry: entry,
extent: FilterGridPage.maxCrossAxisExtent,
)
: ThumbnailRasterImage(
entry: entry,
extent: FilterGridPage.maxCrossAxisExtent,
);
}
return AvesFilterChip(
filter: filter,
showGenericIcon: false,
background: backgroundImage,
details: _buildDetails(filter),
onPressed: onPressed,
);
}
Widget _buildDetails(CollectionFilter filter) {
final count = Text(
'${source.count(filter)}',
style: TextStyle(color: FilterGridPage.detailColor),
);
return filter is AlbumFilter && androidFileUtils.isOnRemovableStorage(filter.album)
? Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
AIcons.removableStorage,
size: 16,
color: FilterGridPage.detailColor,
),
SizedBox(width: 8),
count,
],
)
: count;
}
}

View file

@ -1,103 +1,25 @@
import 'dart:ui'; import 'dart:ui';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/location.dart';
import 'package:aves/model/filters/tag.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/model/source/album.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/location.dart';
import 'package:aves/model/source/tag.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/durations.dart'; import 'package:aves/utils/durations.dart';
import 'package:aves/widgets/album/collection_page.dart'; import 'package:aves/widgets/album/collection_page.dart';
import 'package:aves/widgets/album/empty.dart';
import 'package:aves/widgets/album/thumbnail/raster.dart';
import 'package:aves/widgets/album/thumbnail/vector.dart';
import 'package:aves/widgets/app_drawer.dart'; import 'package:aves/widgets/app_drawer.dart';
import 'package:aves/widgets/common/app_bar_subtitle.dart'; import 'package:aves/widgets/common/app_bar_subtitle.dart';
import 'package:aves/widgets/common/aves_filter_chip.dart'; import 'package:aves/widgets/common/aves_filter_chip.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';
import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/filter_grids/decorated_filter_chip.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class AlbumListPage extends StatelessWidget {
final CollectionSource source;
const AlbumListPage({@required this.source});
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: source.eventBus.on<AlbumsChangedEvent>(),
builder: (context, snapshot) => FilterNavigationPage(
source: source,
title: 'Albums',
filterEntries: source.getAlbumEntries(),
filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)),
emptyBuilder: () => EmptyContent(
icon: AIcons.album,
text: 'No albums',
),
),
);
}
}
class CountryListPage extends StatelessWidget {
final CollectionSource source;
const CountryListPage({@required this.source});
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: source.eventBus.on<LocationsChangedEvent>(),
builder: (context, snapshot) => FilterNavigationPage(
source: source,
title: 'Countries',
filterEntries: source.getCountryEntries(),
filterBuilder: (s) => LocationFilter(LocationLevel.country, s),
emptyBuilder: () => EmptyContent(
icon: AIcons.location,
text: 'No countries',
),
),
);
}
}
class TagListPage extends StatelessWidget {
final CollectionSource source;
const TagListPage({@required this.source});
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: source.eventBus.on<TagsChangedEvent>(),
builder: (context, snapshot) => FilterNavigationPage(
source: source,
title: 'Tags',
filterEntries: source.getTagEntries(),
filterBuilder: (s) => TagFilter(s),
emptyBuilder: () => EmptyContent(
icon: AIcons.tag,
text: 'No tags',
),
),
);
}
}
class FilterNavigationPage extends StatelessWidget { class FilterNavigationPage extends StatelessWidget {
final CollectionSource source; final CollectionSource source;
final String title; final String title;
final List<Widget> actions;
final Map<String, ImageEntry> filterEntries; final Map<String, ImageEntry> filterEntries;
final CollectionFilter Function(String key) filterBuilder; final CollectionFilter Function(String key) filterBuilder;
final Widget Function() emptyBuilder; final Widget Function() emptyBuilder;
@ -105,6 +27,7 @@ class FilterNavigationPage extends StatelessWidget {
const FilterNavigationPage({ const FilterNavigationPage({
@required this.source, @required this.source,
@required this.title, @required this.title,
this.actions,
@required this.filterEntries, @required this.filterEntries,
@required this.filterBuilder, @required this.filterBuilder,
@required this.emptyBuilder, @required this.emptyBuilder,
@ -119,6 +42,7 @@ class FilterNavigationPage extends StatelessWidget {
title: Text(title), title: Text(title),
source: source, source: source,
), ),
actions: actions,
floating: true, floating: true,
), ),
filterEntries: filterEntries, filterEntries: filterEntries,
@ -242,60 +166,6 @@ class FilterGridPage extends StatelessWidget {
} }
} }
class DecoratedFilterChip extends StatelessWidget { enum ChipAction {
final CollectionSource source; sort,
final CollectionFilter filter;
final ImageEntry entry;
final FilterCallback onPressed;
const DecoratedFilterChip({
@required this.source,
@required this.filter,
@required this.entry,
@required this.onPressed,
});
@override
Widget build(BuildContext context) {
Widget backgroundImage;
if (entry != null) {
backgroundImage = entry.isSvg
? ThumbnailVectorImage(
entry: entry,
extent: FilterGridPage.maxCrossAxisExtent,
)
: ThumbnailRasterImage(
entry: entry,
extent: FilterGridPage.maxCrossAxisExtent,
);
}
return AvesFilterChip(
filter: filter,
showGenericIcon: false,
background: backgroundImage,
details: _buildDetails(filter),
onPressed: onPressed,
);
}
Widget _buildDetails(CollectionFilter filter) {
final count = Text(
'${source.count(filter)}',
style: TextStyle(color: FilterGridPage.detailColor),
);
return filter is AlbumFilter && androidFileUtils.isOnRemovableStorage(filter.album)
? Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
AIcons.removableStorage,
size: 16,
color: FilterGridPage.detailColor,
),
SizedBox(width: 8),
count,
],
)
: count;
}
} }

View file

@ -0,0 +1,30 @@
import 'package:aves/model/filters/tag.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/tag.dart';
import 'package:aves/widgets/album/empty.dart';
import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/filter_grids/filter_grid_page.dart';
import 'package:flutter/material.dart';
class TagListPage extends StatelessWidget {
final CollectionSource source;
const TagListPage({@required this.source});
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: source.eventBus.on<TagsChangedEvent>(),
builder: (context, snapshot) => FilterNavigationPage(
source: source,
title: 'Tags',
filterEntries: source.getTagEntries(),
filterBuilder: (s) => TagFilter(s),
emptyBuilder: () => EmptyContent(
icon: AIcons.tag,
text: 'No tags',
),
),
);
}
}

View file

@ -1,5 +1,5 @@
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/utils/constants.dart'; import 'package:aves/model/settings.dart';
import 'package:aves/widgets/album/empty.dart'; import 'package:aves/widgets/album/empty.dart';
import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart'; import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart';
@ -77,12 +77,13 @@ class ImageView extends StatelessWidget {
Widget child; Widget child;
if (entry.isSvg) { if (entry.isSvg) {
final colorFilter = ColorFilter.mode(Color(settings.svgBackground), BlendMode.dstOver);
child = PhotoView.customChild( child = PhotoView.customChild(
child: SvgPicture( child: SvgPicture(
UriPicture( UriPicture(
uri: entry.uri, uri: entry.uri,
mimeType: entry.mimeType, mimeType: entry.mimeType,
colorFilter: Constants.svgColorFilter, colorFilter: colorFilter,
), ),
placeholderBuilder: (context) => loadingBuilder(context, fastThumbnailProvider), placeholderBuilder: (context) => loadingBuilder(context, fastThumbnailProvider),
), ),

View file

@ -1,6 +1,7 @@
import 'package:aves/model/settings.dart'; import 'package:aves/model/settings.dart';
import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/android_app_service.dart';
import 'package:aves/widgets/common/action_delegates/map_style_dialog.dart'; import 'package:aves/widgets/common/action_delegates/map_style_dialog.dart';
import 'package:aves/widgets/common/borders.dart';
import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/common/fx/blurred.dart';
import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/fullscreen/info/location_section.dart'; import 'package:aves/widgets/fullscreen/info/location_section.dart';
@ -115,10 +116,10 @@ class MapOverlayButton extends StatelessWidget {
return BlurredOval( return BlurredOval(
child: Material( child: Material(
type: MaterialType.circle, type: MaterialType.circle,
color: FullscreenOverlay.backgroundColor, color: kOverlayBackgroundColor,
child: Ink( child: Ink(
decoration: BoxDecoration( decoration: BoxDecoration(
border: FullscreenOverlay.buildBorder(context), border: AvesCircleBorder.build(context),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: IconButton( child: IconButton(

View file

@ -80,7 +80,7 @@ class _FullscreenBottomOverlayState extends State<FullscreenBottomOverlay> {
final overlayContentMaxWidth = mqWidth - viewPadding.horizontal - innerPadding.horizontal; final overlayContentMaxWidth = mqWidth - viewPadding.horizontal - innerPadding.horizontal;
return Container( return Container(
color: FullscreenOverlay.backgroundColor, color: kOverlayBackgroundColor,
padding: viewInsets + viewPadding.copyWith(top: 0), padding: viewInsets + viewPadding.copyWith(top: 0),
child: FutureBuilder<OverlayMetadata>( child: FutureBuilder<OverlayMetadata>(
future: _detailLoader, future: _detailLoader,

View file

@ -1,17 +1,8 @@
import 'package:aves/widgets/common/borders.dart';
import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/common/fx/blurred.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class FullscreenOverlay { const kOverlayBackgroundColor = Colors.black26;
static const backgroundColor = Colors.black26;
static BoxBorder buildBorder(BuildContext context) {
final subPixel = MediaQuery.of(context).devicePixelRatio > 2;
return Border.all(
color: Colors.white30,
width: subPixel ? 0.5 : 1.0,
);
}
}
class OverlayButton extends StatelessWidget { class OverlayButton extends StatelessWidget {
final Animation<double> scale; final Animation<double> scale;
@ -26,10 +17,10 @@ class OverlayButton extends StatelessWidget {
child: BlurredOval( child: BlurredOval(
child: Material( child: Material(
type: MaterialType.circle, type: MaterialType.circle,
color: FullscreenOverlay.backgroundColor, color: kOverlayBackgroundColor,
child: Ink( child: Ink(
decoration: BoxDecoration( decoration: BoxDecoration(
border: FullscreenOverlay.buildBorder(context), border: AvesCircleBorder.build(context),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: child, child: child,

View file

@ -4,6 +4,7 @@ import 'package:aves/model/image_entry.dart';
import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/android_app_service.dart';
import 'package:aves/utils/durations.dart'; import 'package:aves/utils/durations.dart';
import 'package:aves/utils/time_utils.dart'; import 'package:aves/utils/time_utils.dart';
import 'package:aves/widgets/common/borders.dart';
import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/common/fx/blurred.dart';
import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/fullscreen/overlay/common.dart'; import 'package:aves/widgets/fullscreen/overlay/common.dart';
@ -180,8 +181,8 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
child: Container( child: Container(
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 16) + EdgeInsets.only(bottom: 16), padding: EdgeInsets.symmetric(vertical: 4, horizontal: 16) + EdgeInsets.only(bottom: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: FullscreenOverlay.backgroundColor, color: kOverlayBackgroundColor,
border: FullscreenOverlay.buildBorder(context), border: AvesCircleBorder.build(context),
borderRadius: BorderRadius.circular(progressBarBorderRadius), borderRadius: BorderRadius.circular(progressBarBorderRadius),
), ),
child: Column( child: Column(

View file

@ -8,7 +8,7 @@ import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/album/collection_page.dart'; import 'package:aves/widgets/album/collection_page.dart';
import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart'; import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart';
import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/filter_grid_page.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/fullscreen/fullscreen_page.dart'; import 'package:aves/widgets/fullscreen/fullscreen_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -47,7 +47,8 @@ class _HomePageState extends State<HomePage> {
return; return;
} }
await androidFileUtils.init(); // 170ms await androidFileUtils.init();
unawaited(androidFileUtils.initAppNames());
final intentData = await ViewerService.getIntentData(); final intentData = await ViewerService.getIntentData();
if (intentData != null) { if (intentData != null) {

View file

@ -2,6 +2,7 @@ import 'package:aves/utils/constants.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';
import 'package:aves/widgets/settings/coordinate_format.dart'; import 'package:aves/widgets/settings/coordinate_format.dart';
import 'package:aves/widgets/settings/launch_page.dart'; import 'package:aves/widgets/settings/launch_page.dart';
import 'package:aves/widgets/settings/svg_background.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class SettingsPage extends StatelessWidget { class SettingsPage extends StatelessWidget {
@ -27,6 +28,14 @@ class SettingsPage extends StatelessWidget {
Flexible(child: LaunchPageSelector()), Flexible(child: LaunchPageSelector()),
], ],
), ),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('SVG background:'),
SizedBox(width: 8),
Flexible(child: SvgBackgroundSelector()),
],
),
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [

View file

@ -0,0 +1,43 @@
import 'package:aves/model/settings.dart';
import 'package:aves/widgets/common/borders.dart';
import 'package:flutter/material.dart';
class SvgBackgroundSelector extends StatefulWidget {
@override
_SvgBackgroundSelectorState createState() => _SvgBackgroundSelectorState();
}
class _SvgBackgroundSelectorState extends State<SvgBackgroundSelector> {
@override
Widget build(BuildContext context) {
const radius = 24.0;
return DropdownButton<int>(
items: [0xFFFFFFFF, 0xFF000000, 0x00000000].map((selected) {
return DropdownMenuItem(
value: selected,
child: Container(
height: radius,
width: radius,
decoration: BoxDecoration(
color: Color(selected),
border: AvesCircleBorder.build(context),
shape: BoxShape.circle,
),
child: selected == 0
? Icon(
Icons.clear,
size: 20,
color: Colors.white30,
)
: null,
),
);
}).toList(),
value: settings.svgBackground,
onChanged: (selected) {
settings.svgBackground = selected;
setState(() {});
},
);
}
}

View file

@ -42,7 +42,7 @@ packages:
name: async name: async
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.4.2" version: "2.4.1"
barcode: barcode:
dependency: transitive dependency: transitive
description: description:
@ -64,13 +64,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.0+1" version: "2.2.0+1"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
charcode: charcode:
dependency: transitive dependency: transitive
description: description:
@ -112,7 +105,7 @@ packages:
name: collection name: collection
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.14.13" version: "1.14.12"
console_log_handler: console_log_handler:
dependency: transitive dependency: transitive
description: description:
@ -133,14 +126,14 @@ packages:
name: coverage name: coverage
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.14.0" version: "0.13.11"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
name: crypto name: crypto
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.5" version: "2.1.4"
csslib: csslib:
dependency: transitive dependency: transitive
description: description:
@ -173,20 +166,13 @@ packages:
url: "git://github.com/deckerst/expansion_tile_card.git" url: "git://github.com/deckerst/expansion_tile_card.git"
source: git source: git
version: "1.0.3" version: "1.0.3"
fake_async:
dependency: transitive
description:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
file: file:
dependency: transitive dependency: transitive
description: description:
name: file name: file
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.2.1" version: "5.1.0"
firebase_crashlytics: firebase_crashlytics:
dependency: "direct main" dependency: "direct main"
description: description:
@ -275,7 +261,7 @@ packages:
name: flutter_svg name: flutter_svg
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.18.0" version: "0.17.4"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -353,7 +339,7 @@ packages:
name: image name: image
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.14" version: "2.1.12"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@ -381,7 +367,7 @@ packages:
name: json_rpc_2 name: json_rpc_2
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.1" version: "2.1.0"
latlong: latlong:
dependency: "direct main" dependency: "direct main"
description: description:
@ -416,7 +402,7 @@ packages:
name: matcher name: matcher
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.12.8" version: "0.12.6"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@ -438,6 +424,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.9.7" version: "0.9.7"
multi_server_socket:
dependency: transitive
description:
name: multi_server_socket
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
nested: nested:
dependency: transitive dependency: transitive
description: description:
@ -500,7 +493,7 @@ packages:
name: path name: path
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.7.0" version: "1.6.4"
path_drawing: path_drawing:
dependency: transitive dependency: transitive
description: description:
@ -584,7 +577,7 @@ packages:
name: petitparser name: petitparser
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.4" version: "2.4.0"
photo_view: photo_view:
dependency: "direct main" dependency: "direct main"
description: description:
@ -642,7 +635,7 @@ packages:
name: process name: process
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.13" version: "3.0.12"
proj4dart: proj4dart:
dependency: transitive dependency: transitive
description: description:
@ -801,7 +794,7 @@ packages:
name: stack_trace name: stack_trace
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.9.5" version: "1.9.3"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:
@ -857,21 +850,21 @@ packages:
name: test name: test
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.15.2" version: "1.14.4"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.17" version: "0.2.15"
test_core: test_core:
dependency: transitive dependency: transitive
description: description:
name: test_core name: test_core
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.3.10" version: "0.3.4"
transparent_image: transparent_image:
dependency: transitive dependency: transitive
description: description:
@ -892,7 +885,7 @@ packages:
name: typed_data name: typed_data
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.1.6"
unicode: unicode:
dependency: transitive dependency: transitive
description: description:
@ -1025,7 +1018,7 @@ packages:
name: xml name: xml
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.2.0" version: "3.6.1"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:
@ -1034,5 +1027,5 @@ packages:
source: hosted source: hosted
version: "2.2.1" version: "2.2.1"
sdks: sdks:
dart: ">=2.9.0-14.0.dev <3.0.0" dart: ">=2.8.0 <3.0.0"
flutter: ">=1.18.0-6.0.pre <2.0.0" flutter: ">=1.17.0 <2.0.0"

View file

@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.1.7+19 version: 1.1.8+20
# video_player (as of v0.10.8+2, backed by ExoPlayer): # video_player (as of v0.10.8+2, backed by ExoPlayer):
# - does not support content URIs (by default, but trivial by fork) # - does not support content URIs (by default, but trivial by fork)

1
shaders_1.17.5.sksl.json Normal file

File diff suppressed because one or more lines are too long

View file

@ -68,7 +68,7 @@ void groupCollection() {
await driver.tap(find.byValueKey('menu-group')); await driver.tap(find.byValueKey('menu-group'));
await driver.waitUntilNoTransientCallbacks(); await driver.waitUntilNoTransientCallbacks();
await driver.tap(find.byValueKey(GroupFactor.album.toString())); await driver.tap(find.byValueKey(EntryGroupFactor.album.toString()));
await driver.tap(find.byValueKey('apply-button')); await driver.tap(find.byValueKey('apply-button'));
}); });
} }
@ -81,7 +81,7 @@ void sortCollection() {
await driver.tap(find.byValueKey('menu-sort')); await driver.tap(find.byValueKey('menu-sort'));
await driver.waitUntilNoTransientCallbacks(); await driver.waitUntilNoTransientCallbacks();
await driver.tap(find.byValueKey(SortFactor.date.toString())); await driver.tap(find.byValueKey(EntrySortFactor.date.toString()));
await driver.tap(find.byValueKey('apply-button')); await driver.tap(find.byValueKey('apply-button'));
}); });
} }