Merge branch 'develop'
This commit is contained in:
commit
8815a793f1
35 changed files with 740 additions and 416 deletions
3
.github/workflows/check.yml
vendored
3
.github/workflows/check.yml
vendored
|
@ -14,7 +14,8 @@ jobs:
|
|||
steps:
|
||||
- uses: subosito/flutter-action@v1
|
||||
with:
|
||||
flutter-version: '1.17.5'
|
||||
channel: beta
|
||||
flutter-version: '1.22.0-12.1.pre'
|
||||
|
||||
- name: Clone the repository.
|
||||
uses: actions/checkout@v2
|
||||
|
|
7
.github/workflows/release.yml
vendored
7
.github/workflows/release.yml
vendored
|
@ -16,7 +16,8 @@ jobs:
|
|||
|
||||
- uses: subosito/flutter-action@v1
|
||||
with:
|
||||
flutter-version: '1.17.5'
|
||||
channel: beta
|
||||
flutter-version: '1.22.0-12.1.pre'
|
||||
|
||||
# Workaround for this Android Gradle Plugin issue (supposedly fixed in AGP 4.1):
|
||||
# https://issuetracker.google.com/issues/144111441
|
||||
|
@ -49,8 +50,8 @@ jobs:
|
|||
echo "${{ secrets.KEY_JKS }}" > release.keystore.asc
|
||||
gpg -d --passphrase "${{ secrets.KEY_JKS_PASSPHRASE }}" --batch release.keystore.asc > $AVES_STORE_FILE
|
||||
rm release.keystore.asc
|
||||
flutter build apk
|
||||
flutter build appbundle
|
||||
flutter build apk --bundle-sksl-path shaders_1.22.0-12.1.pre.sksl.json
|
||||
flutter build appbundle --bundle-sksl-path shaders_1.22.0-12.1.pre.sksl.json
|
||||
rm $AVES_STORE_FILE
|
||||
env:
|
||||
AVES_STORE_FILE: ${{ github.workspace }}/key.jks
|
||||
|
|
|
@ -12,20 +12,18 @@ Aves is a gallery and metadata explorer app. It is built for Android, with Flutt
|
|||
|
||||
## Features
|
||||
|
||||
- support raster images: JPEG, PNG, GIF, WEBP, BMP, WBMP, HEIC (from Android Pie), DNG
|
||||
- support raster images: BMP, DNG, GIF, HEIC (from Android Pie), ICO, JPEG, PNG, WBMP, WEBP
|
||||
- support animated images: GIF, WEBP
|
||||
- support vector images: SVG
|
||||
- support videos: MP4, AVI & probably others
|
||||
- support videos: MP4, AVI, AVCHD & probably others
|
||||
- search and filter by country, place, XMP tag, type (animated, raster, vector, video)
|
||||
- bulk delete, share, copy, move
|
||||
- favorites
|
||||
- statistics
|
||||
- handle intents to view or pick images
|
||||
- support Android API 24 ~ 30 (Nougat ~ R)
|
||||
- Android integration (app shortcuts, handle view/pick intents)
|
||||
|
||||
## Known Issues
|
||||
|
||||
- privacy: cannot opt out of Crashlytics reporting (cf [flutterfire issue #1143](https://github.com/FirebaseExtended/flutterfire/issues/1143))
|
||||
- gesture: double tap on image does not zoom on tapped area (cf [photo_view issue #82](https://github.com/renancaraujo/photo_view/issues/82))
|
||||
- performance: image info page stutters the first time it loads a Google Maps view (cf [flutter issue #28493](https://github.com/flutter/flutter/issues/28493))
|
||||
- performance: image decoding is slow
|
||||
|
|
|
@ -106,6 +106,9 @@
|
|||
android:value="${googleApiKey}" />
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="firebase_crashlytics_collection_enabled"
|
||||
android:value="false" />
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
|
|
@ -199,6 +199,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
|||
private Map<String, String> getVideoAllMetadataByMediaMetadataRetriever(String uri) {
|
||||
Map<String, String> dirMap = new HashMap<>();
|
||||
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, Uri.parse(uri));
|
||||
if (retriever != null) {
|
||||
try {
|
||||
for (Map.Entry<Integer, String> kv : VIDEO_MEDIA_METADATA_KEYS.entrySet()) {
|
||||
Integer key = kv.getKey();
|
||||
|
@ -221,6 +222,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
|||
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
|
||||
retriever.release();
|
||||
}
|
||||
}
|
||||
return dirMap;
|
||||
}
|
||||
|
||||
|
@ -317,6 +319,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
|||
private Map<String, Object> getVideoCatalogMetadataByMediaMetadataRetriever(String uri) {
|
||||
Map<String, Object> metadataMap = new HashMap<>();
|
||||
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, Uri.parse(uri));
|
||||
if (retriever != null) {
|
||||
try {
|
||||
String dateString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE);
|
||||
String rotationString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
|
||||
|
@ -357,6 +360,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
|||
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
|
||||
retriever.release();
|
||||
}
|
||||
}
|
||||
return metadataMap;
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ class VideoThumbnailFetcher implements DataFetcher<InputStream> {
|
|||
@Override
|
||||
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
|
||||
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(model.getContext(), model.getUri());
|
||||
if (retriever != null) {
|
||||
try {
|
||||
byte[] picture = retriever.getEmbeddedPicture();
|
||||
if (picture != null) {
|
||||
|
@ -46,6 +47,7 @@ class VideoThumbnailFetcher implements DataFetcher<InputStream> {
|
|||
retriever.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
|
|
|
@ -135,6 +135,7 @@ public class SourceImageEntry {
|
|||
// finds: width, height, orientation/rotation, date, title, duration
|
||||
private void fillByMediaMetadataRetriever(@NonNull Context context) {
|
||||
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, uri);
|
||||
if (retriever != null) {
|
||||
try {
|
||||
String width = null, height = null, rotation = null, durationMillis = null;
|
||||
if (isImage()) {
|
||||
|
@ -180,6 +181,7 @@ public class SourceImageEntry {
|
|||
retriever.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// expects entry with: uri, mimeType
|
||||
// finds: width, height, orientation, date
|
||||
|
|
|
@ -460,7 +460,8 @@ public class StorageUtils {
|
|||
}
|
||||
retriever.setDataSource(context, uri);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "failed to open MediaMetadataRetriever for uri=" + uri, e);
|
||||
// unsupported format
|
||||
return null;
|
||||
}
|
||||
return retriever;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ buildscript {
|
|||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.6.3'
|
||||
classpath 'com.google.gms:google-services:4.3.3'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.1.1'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.2.0'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
102
lib/main.dart
102
lib/main.dart
|
@ -1,9 +1,15 @@
|
|||
import 'dart:isolate';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/utils/route_tracker.dart';
|
||||
import 'package:aves/widgets/common/data_providers/settings_provider.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:aves/widgets/home_page.dart';
|
||||
import 'package:aves/widgets/welcome_page.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:overlay_support/overlay_support.dart';
|
||||
|
@ -11,9 +17,15 @@ import 'package:overlay_support/overlay_support.dart';
|
|||
void main() {
|
||||
// HttpClient.enableTimelineLogging = true; // enable network traffic logging
|
||||
// debugPrintGestureArenaDiagnostics = true;
|
||||
Crashlytics.instance.enableInDevMode = true;
|
||||
|
||||
FlutterError.onError = Crashlytics.instance.recordFlutterError;
|
||||
Isolate.current.addErrorListener(RawReceivePort((pair) async {
|
||||
final List<dynamic> errorAndStacktrace = pair;
|
||||
await FirebaseCrashlytics.instance.recordError(
|
||||
errorAndStacktrace.first,
|
||||
errorAndStacktrace.last,
|
||||
);
|
||||
}).sendPort);
|
||||
|
||||
runApp(AvesApp());
|
||||
}
|
||||
|
||||
|
@ -28,24 +40,11 @@ class AvesApp extends StatefulWidget {
|
|||
|
||||
class _AvesAppState extends State<AvesApp> {
|
||||
Future<void> _appSetup;
|
||||
final NavigatorObserver _routeTracker = CrashlyticsRouteTracker();
|
||||
|
||||
static const accentColor = Colors.indigoAccent;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_appSetup = settings.init();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// place the settings provider above `MaterialApp`
|
||||
// so it can be used during navigation transitions
|
||||
return SettingsProvider(
|
||||
child: OverlaySupport(
|
||||
child: MaterialApp(
|
||||
title: 'Aves',
|
||||
theme: ThemeData(
|
||||
static final darkTheme = ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
accentColor: accentColor,
|
||||
scaffoldBackgroundColor: Colors.grey[900],
|
||||
|
@ -63,15 +62,74 @@ class _AvesAppState extends State<AvesApp> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
home: FutureBuilder<void>(
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_appSetup = _setup();
|
||||
}
|
||||
|
||||
Future<void> _setup() async {
|
||||
await Firebase.initializeApp().then((app) {
|
||||
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError;
|
||||
FirebaseCrashlytics.instance.setCustomKey('locales', window.locales.join(', '));
|
||||
final now = DateTime.now();
|
||||
FirebaseCrashlytics.instance.setCustomKey('timezone', '${now.timeZoneName} (${now.timeZoneOffset})');
|
||||
FirebaseCrashlytics.instance.setCustomKey(
|
||||
'build_mode',
|
||||
kReleaseMode
|
||||
? 'release'
|
||||
: kProfileMode
|
||||
? 'profile'
|
||||
: 'debug');
|
||||
});
|
||||
await settings.init();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// place the settings provider above `MaterialApp`
|
||||
// so it can be used during navigation transitions
|
||||
final home = FutureBuilder<void>(
|
||||
future: _appSetup,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) return Icon(AIcons.error);
|
||||
if (snapshot.connectionState != ConnectionState.done) return Scaffold();
|
||||
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done) {
|
||||
return settings.hasAcceptedTerms ? HomePage() : WelcomePage();
|
||||
},
|
||||
}
|
||||
return Scaffold(
|
||||
body: snapshot.hasError
|
||||
? Container(
|
||||
alignment: Alignment.center,
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(AIcons.error),
|
||||
SizedBox(height: 16),
|
||||
Text(snapshot.error.toString()),
|
||||
],
|
||||
),
|
||||
)
|
||||
: SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
);
|
||||
return SettingsProvider(
|
||||
child: OverlaySupport(
|
||||
child: FutureBuilder<void>(
|
||||
future: _appSetup,
|
||||
builder: (context, snapshot) {
|
||||
return MaterialApp(
|
||||
home: home,
|
||||
navigatorObservers: [
|
||||
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done) _routeTracker,
|
||||
],
|
||||
title: 'Aves',
|
||||
darkTheme: darkTheme,
|
||||
themeMode: ThemeMode.dark,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -10,16 +10,14 @@ import 'package:aves/utils/time_utils.dart';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geocoder/geocoder.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path/path.dart' as ppath;
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
import 'mime_types.dart';
|
||||
|
||||
class ImageEntry {
|
||||
String uri;
|
||||
String _path;
|
||||
String _directory;
|
||||
String _filename;
|
||||
String _path, _directory, _filename, _extension;
|
||||
int contentId;
|
||||
final String sourceMimeType;
|
||||
int width;
|
||||
|
@ -131,20 +129,26 @@ class ImageEntry {
|
|||
_path = path;
|
||||
_directory = null;
|
||||
_filename = null;
|
||||
_extension = null;
|
||||
}
|
||||
|
||||
String get path => _path;
|
||||
|
||||
String get directory {
|
||||
_directory ??= path != null ? dirname(path) : null;
|
||||
_directory ??= path != null ? ppath.dirname(path) : null;
|
||||
return _directory;
|
||||
}
|
||||
|
||||
String get filenameWithoutExtension {
|
||||
_filename ??= path != null ? basenameWithoutExtension(path) : null;
|
||||
_filename ??= path != null ? ppath.basenameWithoutExtension(path) : null;
|
||||
return _filename;
|
||||
}
|
||||
|
||||
String get extension {
|
||||
_extension ??= path != null ? ppath.extension(path) : null;
|
||||
return _extension;
|
||||
}
|
||||
|
||||
// the MIME type reported by the Media Store is unreliable
|
||||
// so we use the one found during cataloguing if possible
|
||||
String get mimeType => catalogMetadata?.mimeType ?? sourceMimeType;
|
||||
|
@ -318,7 +322,7 @@ class ImageEntry {
|
|||
Future<bool> rename(String newName) async {
|
||||
if (newName == filenameWithoutExtension) return true;
|
||||
|
||||
final newFields = await ImageFileService.rename(this, '$newName${extension(this.path)}');
|
||||
final newFields = await ImageFileService.rename(this, '$newName$extension');
|
||||
if (newFields.isEmpty) return false;
|
||||
|
||||
final uri = newFields['uri'];
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import 'package:aves/model/settings/coordinate_format.dart';
|
||||
import 'package:aves/model/settings/home_page.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/location_section.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../source/enums.dart';
|
||||
|
@ -18,6 +21,7 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
// app
|
||||
static const hasAcceptedTermsKey = 'has_accepted_terms';
|
||||
static const isCrashlyticsEnabledKey = 'is_crashlytics_enabled';
|
||||
static const mustBackTwiceToExitKey = 'must_back_twice_to_exit';
|
||||
static const homePageKey = 'home_page';
|
||||
static const catalogTimeZoneKey = 'catalog_time_zone';
|
||||
|
@ -29,6 +33,8 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
// filter grids
|
||||
static const albumSortFactorKey = 'album_sort_factor';
|
||||
static const countrySortFactorKey = 'country_sort_factor';
|
||||
static const tagSortFactorKey = 'tag_sort_factor';
|
||||
|
||||
// info
|
||||
static const infoMapStyleKey = 'info_map_style';
|
||||
|
@ -40,6 +46,12 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
Future<void> init() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
await _setupCrashlytics();
|
||||
}
|
||||
|
||||
Future<void> _setupCrashlytics() async {
|
||||
await Firebase.app().setAutomaticDataCollectionEnabled(isCrashlyticsEnabled);
|
||||
await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(isCrashlyticsEnabled);
|
||||
}
|
||||
|
||||
Future<void> reset() {
|
||||
|
@ -52,6 +64,13 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
set hasAcceptedTerms(bool newValue) => setAndNotify(hasAcceptedTermsKey, newValue);
|
||||
|
||||
bool get isCrashlyticsEnabled => getBoolOrDefault(isCrashlyticsEnabledKey, true);
|
||||
|
||||
set isCrashlyticsEnabled(bool newValue) {
|
||||
setAndNotify(isCrashlyticsEnabledKey, newValue);
|
||||
unawaited(_setupCrashlytics());
|
||||
}
|
||||
|
||||
bool get mustBackTwiceToExit => getBoolOrDefault(mustBackTwiceToExitKey, true);
|
||||
|
||||
set mustBackTwiceToExit(bool newValue) => setAndNotify(mustBackTwiceToExitKey, newValue);
|
||||
|
@ -84,6 +103,14 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
set albumSortFactor(ChipSortFactor newValue) => setAndNotify(albumSortFactorKey, newValue.toString());
|
||||
|
||||
ChipSortFactor get countrySortFactor => getEnumOrDefault(countrySortFactorKey, ChipSortFactor.name, ChipSortFactor.values);
|
||||
|
||||
set countrySortFactor(ChipSortFactor newValue) => setAndNotify(countrySortFactorKey, newValue.toString());
|
||||
|
||||
ChipSortFactor get tagSortFactor => getEnumOrDefault(tagSortFactorKey, ChipSortFactor.name, ChipSortFactor.values);
|
||||
|
||||
set tagSortFactor(ChipSortFactor newValue) => setAndNotify(tagSortFactorKey, newValue.toString());
|
||||
|
||||
// info
|
||||
|
||||
EntryMapStyle get infoMapStyle => getEnumOrDefault(infoMapStyleKey, EntryMapStyle.stamenWatercolor, EntryMapStyle.values);
|
||||
|
|
|
@ -83,19 +83,6 @@ mixin LocationMixin on SourceBase {
|
|||
invalidateFilterEntryCounts();
|
||||
eventBus.fire(LocationsChangedEvent());
|
||||
}
|
||||
|
||||
Map<String, ImageEntry> getCountryEntries() {
|
||||
final locatedEntries = sortedEntriesForFilterList.where((entry) => entry.isLocated);
|
||||
return Map.fromEntries(sortedCountries.map((countryNameAndCode) {
|
||||
final split = countryNameAndCode.split(LocationFilter.locationSeparator);
|
||||
ImageEntry entry;
|
||||
if (split.length > 1) {
|
||||
final countryCode = split[1];
|
||||
entry = locatedEntries.firstWhere((entry) => entry.addressDetails.countryCode == countryCode, orElse: () => null);
|
||||
}
|
||||
return MapEntry(countryNameAndCode, entry);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
class AddressMetadataChangedEvent {}
|
||||
|
|
|
@ -59,14 +59,6 @@ mixin TagMixin on SourceBase {
|
|||
invalidateFilterEntryCounts();
|
||||
eventBus.fire(TagsChangedEvent());
|
||||
}
|
||||
|
||||
Map<String, ImageEntry> getTagEntries() {
|
||||
final entries = sortedEntriesForFilterList;
|
||||
return Map.fromEntries(sortedTags.map((tag) => MapEntry(
|
||||
tag,
|
||||
entries.firstWhere((entry) => entry.xmpSubjects.contains(tag), orElse: () => null),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
class CatalogMetadataChangedEvent {}
|
||||
|
|
18
lib/utils/route_tracker.dart
Normal file
18
lib/utils/route_tracker.dart
Normal file
|
@ -0,0 +1,18 @@
|
|||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CrashlyticsRouteTracker extends NavigatorObserver {
|
||||
@override
|
||||
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) => FirebaseCrashlytics.instance.log('Nav didPush to ${_name(route)}');
|
||||
|
||||
@override
|
||||
void didPop(Route<dynamic> route, Route<dynamic> previousRoute) => FirebaseCrashlytics.instance.log('Nav didPop to ${_name(previousRoute)}');
|
||||
|
||||
@override
|
||||
void didRemove(Route<dynamic> route, Route<dynamic> previousRoute) => FirebaseCrashlytics.instance.log('Nav didRemove to ${_name(previousRoute)}');
|
||||
|
||||
@override
|
||||
void didReplace({Route<dynamic> newRoute, Route<dynamic> oldRoute}) => FirebaseCrashlytics.instance.log('Nav didReplace to ${_name(newRoute)}');
|
||||
|
||||
String _name(Route<dynamic> route) => route?.settings?.name ?? 'unnamed ${route?.runtimeType}';
|
||||
}
|
|
@ -32,10 +32,10 @@ class ThumbnailVectorImage extends StatelessWidget {
|
|||
UriPicture(
|
||||
uri: entry.uri,
|
||||
mimeType: entry.mimeType,
|
||||
colorFilter: colorFilter,
|
||||
),
|
||||
width: extent,
|
||||
height: extent,
|
||||
colorFilter: colorFilter,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
@ -15,6 +15,7 @@ class CreateAlbumDialog extends StatefulWidget {
|
|||
class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final ValueNotifier<bool> _existsNotifier = ValueNotifier(false);
|
||||
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
|
||||
Set<StorageVolume> _allVolumes;
|
||||
StorageVolume _primaryVolume, _selectedVolume;
|
||||
|
||||
|
@ -24,7 +25,6 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
|||
_allVolumes = androidFileUtils.storageVolumes;
|
||||
_primaryVolume = _allVolumes.firstWhere((volume) => volume.isPrimary, orElse: () => _allVolumes.first);
|
||||
_selectedVolume = _primaryVolume;
|
||||
_initAlbumName();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -48,7 +48,7 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
|||
groupValue: _selectedVolume,
|
||||
onChanged: (volume) {
|
||||
_selectedVolume = volume;
|
||||
_checkAlbumExists();
|
||||
_validate();
|
||||
setState(() {});
|
||||
},
|
||||
title: Text(
|
||||
|
@ -67,7 +67,7 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
|||
SizedBox(height: 8),
|
||||
],
|
||||
Padding(
|
||||
padding: AvesDialog.contentHorizontalPadding,
|
||||
padding: AvesDialog.contentHorizontalPadding + EdgeInsets.only(bottom: 8),
|
||||
child: ValueListenableBuilder<bool>(
|
||||
valueListenable: _existsNotifier,
|
||||
builder: (context, exists, child) {
|
||||
|
@ -75,8 +75,10 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
|||
controller: _nameController,
|
||||
decoration: InputDecoration(
|
||||
helperText: exists ? 'Album already exists' : '',
|
||||
hintText: 'Album name',
|
||||
),
|
||||
onChanged: (_) => _checkAlbumExists(),
|
||||
autofocus: _allVolumes.length == 1,
|
||||
onChanged: (_) => _validate(),
|
||||
onSubmitted: (_) => _submit(context),
|
||||
);
|
||||
}),
|
||||
|
@ -87,9 +89,14 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
|||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('Cancel'.toUpperCase()),
|
||||
),
|
||||
FlatButton(
|
||||
onPressed: () => _submit(context),
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: _isValidNotifier,
|
||||
builder: (context, isValid, child) {
|
||||
return FlatButton(
|
||||
onPressed: isValid ? () => _submit(context) : null,
|
||||
child: Text('Create'.toUpperCase()),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -100,21 +107,10 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
|||
return join(_selectedVolume.path, 'Pictures', name);
|
||||
}
|
||||
|
||||
Future<void> _initAlbumName() async {
|
||||
var count = 1;
|
||||
while (true) {
|
||||
var name = 'Album $count';
|
||||
if (!await Directory(_buildAlbumPath(name)).exists()) {
|
||||
_nameController.text = name;
|
||||
return;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _checkAlbumExists() async {
|
||||
Future<void> _validate() async {
|
||||
final path = _buildAlbumPath(_nameController.text);
|
||||
_existsNotifier.value = path.isEmpty ? false : await Directory(path).exists();
|
||||
_isValidNotifier.value = (_nameController.text ?? '').isNotEmpty;
|
||||
}
|
||||
|
||||
void _submit(BuildContext context) => Navigator.pop(context, _buildAlbumPath(_nameController.text));
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
import '../aves_dialog.dart';
|
||||
|
||||
|
@ -14,11 +17,13 @@ class RenameEntryDialog extends StatefulWidget {
|
|||
|
||||
class _RenameEntryDialogState extends State<RenameEntryDialog> {
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
|
||||
|
||||
ImageEntry get entry => widget.entry;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final entry = widget.entry;
|
||||
_nameController.text = entry.filenameWithoutExtension ?? entry.sourceTitle;
|
||||
}
|
||||
|
||||
|
@ -34,17 +39,35 @@ class _RenameEntryDialogState extends State<RenameEntryDialog> {
|
|||
content: TextField(
|
||||
controller: _nameController,
|
||||
autofocus: true,
|
||||
onChanged: (_) => _validate(),
|
||||
onSubmitted: (_) => _submit(context),
|
||||
),
|
||||
actions: [
|
||||
FlatButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('Cancel'.toUpperCase()),
|
||||
),
|
||||
FlatButton(
|
||||
onPressed: () => Navigator.pop(context, _nameController.text),
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: _isValidNotifier,
|
||||
builder: (context, isValid, child) {
|
||||
return FlatButton(
|
||||
onPressed: isValid ? () => _submit(context) : null,
|
||||
child: Text('Apply'.toUpperCase()),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _validate() async {
|
||||
var newName = _nameController.text ?? '';
|
||||
if (newName.isNotEmpty) {
|
||||
newName += entry.extension;
|
||||
}
|
||||
final type = await FileSystemEntity.type(join(entry.directory, newName));
|
||||
_isValidNotifier.value = type == FileSystemEntityType.notFound;
|
||||
}
|
||||
|
||||
void _submit(BuildContext context) => Navigator.pop(context, _nameController.text);
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ class AvesFilterChip extends StatefulWidget {
|
|||
|
||||
class _AvesFilterChipState extends State<AvesFilterChip> {
|
||||
Future<Color> _colorFuture;
|
||||
Color _outlineColor;
|
||||
bool _tapped;
|
||||
|
||||
CollectionFilter get filter => widget.filter;
|
||||
|
@ -60,7 +61,16 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
|||
}
|
||||
}
|
||||
|
||||
void _initColorLoader() => _colorFuture = filter.color(context);
|
||||
void _initColorLoader() {
|
||||
// For app albums, `filter.color` yields a regular async `Future` the first time
|
||||
// but it yields a `SynchronousFuture` when called again on a known album.
|
||||
// This works fine to avoid a frame with no Future data, for new widgets.
|
||||
// However, when the user moves away and back to a page with a chip using the async future,
|
||||
// the existing widget FutureBuilder cycles again from the start, with a frame in `waiting` state and no data.
|
||||
// So we save the result of the Future to a local variable because of this specific case.
|
||||
_colorFuture = filter.color(context);
|
||||
_outlineColor = Colors.transparent;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -160,11 +170,13 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
|||
child: FutureBuilder<Color>(
|
||||
future: _colorFuture,
|
||||
builder: (context, snapshot) {
|
||||
final outlineColor = snapshot.hasData ? snapshot.data : Colors.transparent;
|
||||
if (snapshot.hasData) {
|
||||
_outlineColor = snapshot.data;
|
||||
}
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: outlineColor,
|
||||
color: _outlineColor,
|
||||
width: AvesFilterChip.outlineWidth,
|
||||
),
|
||||
borderRadius: borderRadius,
|
||||
|
|
|
@ -8,13 +8,10 @@ class UriPicture extends PictureProvider<UriPicture> {
|
|||
const UriPicture({
|
||||
@required this.uri,
|
||||
@required this.mimeType,
|
||||
this.colorFilter,
|
||||
}) : assert(uri != null);
|
||||
|
||||
final String uri, mimeType;
|
||||
|
||||
final ColorFilter colorFilter;
|
||||
|
||||
@override
|
||||
Future<UriPicture> obtainKey(PictureConfiguration configuration) {
|
||||
return SynchronousFuture<UriPicture>(this);
|
||||
|
@ -37,22 +34,22 @@ class UriPicture extends PictureProvider<UriPicture> {
|
|||
|
||||
final decoder = SvgPicture.svgByteDecoder;
|
||||
if (onError != null) {
|
||||
final future = decoder(data, colorFilter, key.toString());
|
||||
final future = decoder(data, null, key.toString());
|
||||
unawaited(future.catchError(onError));
|
||||
return future;
|
||||
}
|
||||
return decoder(data, colorFilter, key.toString());
|
||||
return decoder(data, null, key.toString());
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
return other is UriPicture && other.uri == uri && other.colorFilter == colorFilter;
|
||||
return other is UriPicture && other.uri == uri;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(uri, colorFilter);
|
||||
int get hashCode => uri.hashCode;
|
||||
|
||||
@override
|
||||
String toString() => '${objectRuntimeType(this, 'UriPicture')}(uri=$uri, mimeType=$mimeType, colorFilter=$colorFilter)';
|
||||
String toString() => '${objectRuntimeType(this, 'UriPicture')}(uri=$uri, mimeType=$mimeType)';
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@ import 'package:aves/utils/android_file_utils.dart';
|
|||
import 'package:aves/utils/file_utils.dart';
|
||||
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/info_page.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
@ -95,6 +97,21 @@ class DebugPageState extends State<DebugPage> {
|
|||
label: '$timeDilation',
|
||||
),
|
||||
Divider(),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text('Crashlytics'),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
RaisedButton(
|
||||
onPressed: FirebaseCrashlytics.instance.crash,
|
||||
child: Text('Crash'),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text('Firebase data collection: ${Firebase.app().isAutomaticDataCollectionEnabled ? 'enabled' : 'disabled'}'),
|
||||
Text('Crashlytics collection: ${FirebaseCrashlytics.instance.isCrashlyticsCollectionEnabled ? 'enabled' : 'disabled'}'),
|
||||
Divider(),
|
||||
Text('Entries: ${entries.length}'),
|
||||
Text('Catalogued: ${catalogued.length}'),
|
||||
Text('With GPS: ${withGps.length}'),
|
||||
|
|
|
@ -5,15 +5,11 @@ 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/collection/empty.dart';
|
||||
import 'package:aves/widgets/common/aves_selection_dialog.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:aves/widgets/common/menu_row.dart';
|
||||
import 'package:aves/widgets/filter_grids/chip_action_delegate.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 {
|
||||
|
@ -21,95 +17,51 @@ class AlbumListPage extends StatelessWidget {
|
|||
|
||||
final CollectionSource source;
|
||||
|
||||
static final ChipActionDelegate actionDelegate = AlbumChipActionDelegate();
|
||||
|
||||
const AlbumListPage({@required this.source});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Settings, ChipSortFactor>(
|
||||
selector: (context, s) => s.albumSortFactor,
|
||||
builder: (context, albumSortFactor, child) {
|
||||
builder: (context, sortFactor, child) {
|
||||
return AnimatedBuilder(
|
||||
animation: androidFileUtils.appNameChangeNotifier,
|
||||
builder: (context, child) => StreamBuilder(
|
||||
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
||||
builder: (context, snapshot) {
|
||||
return FilterNavigationPage(
|
||||
builder: (context, snapshot) => FilterNavigationPage(
|
||||
source: source,
|
||||
title: 'Albums',
|
||||
actions: _buildActions(context),
|
||||
actionDelegate: actionDelegate,
|
||||
filterEntries: getAlbumEntries(source),
|
||||
filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)),
|
||||
emptyBuilder: () => EmptyContent(
|
||||
icon: AIcons.album,
|
||||
text: 'No albums',
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildActions(BuildContext context) {
|
||||
return [
|
||||
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) => AvesSelectionDialog<ChipSortFactor>(
|
||||
initialValue: settings.albumSortFactor,
|
||||
options: {
|
||||
ChipSortFactor.date: 'By date',
|
||||
ChipSortFactor.name: 'By name',
|
||||
},
|
||||
title: 'Sort',
|
||||
),
|
||||
);
|
||||
if (factor != null) {
|
||||
settings.albumSortFactor = factor;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// common with album selection page to move/copy entries
|
||||
|
||||
static Map<String, ImageEntry> getAlbumEntries(CollectionSource source) {
|
||||
final entriesByDate = source.sortedEntriesForFilterList;
|
||||
final albumEntries = source.sortedAlbums.map((album) {
|
||||
return MapEntry(
|
||||
final albums = source.sortedAlbums
|
||||
.map((album) => MapEntry(
|
||||
album,
|
||||
entriesByDate.firstWhere((entry) => entry.directory == album, orElse: () => null),
|
||||
);
|
||||
}).toList();
|
||||
))
|
||||
.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);
|
||||
albums.sort(FilterNavigationPage.compareChipByDate);
|
||||
return Map.fromEntries(albums);
|
||||
case ChipSortFactor.name:
|
||||
default:
|
||||
final regularAlbums = <String>[], appAlbums = <String>[], specialAlbums = <String>[];
|
||||
|
|
65
lib/widgets/filter_grids/chip_action_delegate.dart
Normal file
65
lib/widgets/filter_grids/chip_action_delegate.dart
Normal file
|
@ -0,0 +1,65 @@
|
|||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/utils/durations.dart';
|
||||
import 'package:aves/widgets/common/aves_selection_dialog.dart';
|
||||
import 'package:aves/widgets/filter_grids/chip_actions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
abstract class ChipActionDelegate {
|
||||
ChipSortFactor get sortFactor;
|
||||
|
||||
set sortFactor(ChipSortFactor factor);
|
||||
|
||||
Future<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:
|
||||
await _showSortDialog(context);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showSortDialog(BuildContext context) async {
|
||||
final factor = await showDialog<ChipSortFactor>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<ChipSortFactor>(
|
||||
initialValue: sortFactor,
|
||||
options: {
|
||||
ChipSortFactor.date: 'By date',
|
||||
ChipSortFactor.name: 'By name',
|
||||
},
|
||||
title: 'Sort',
|
||||
),
|
||||
);
|
||||
if (factor != null) {
|
||||
sortFactor = factor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumChipActionDelegate extends ChipActionDelegate {
|
||||
@override
|
||||
ChipSortFactor get sortFactor => settings.albumSortFactor;
|
||||
|
||||
@override
|
||||
set sortFactor(ChipSortFactor factor) => settings.albumSortFactor = factor;
|
||||
}
|
||||
|
||||
class CountryChipActionDelegate extends ChipActionDelegate {
|
||||
@override
|
||||
ChipSortFactor get sortFactor => settings.countrySortFactor;
|
||||
|
||||
@override
|
||||
set sortFactor(ChipSortFactor factor) => settings.countrySortFactor = factor;
|
||||
}
|
||||
|
||||
class TagChipActionDelegate extends ChipActionDelegate {
|
||||
@override
|
||||
ChipSortFactor get sortFactor => settings.tagSortFactor;
|
||||
|
||||
@override
|
||||
set sortFactor(ChipSortFactor factor) => settings.tagSortFactor = factor;
|
||||
}
|
3
lib/widgets/filter_grids/chip_actions.dart
Normal file
3
lib/widgets/filter_grids/chip_actions.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
enum ChipAction {
|
||||
sort,
|
||||
}
|
|
@ -1,26 +1,37 @@
|
|||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/model/source/location.dart';
|
||||
import 'package:aves/widgets/collection/empty.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:aves/widgets/filter_grids/chip_action_delegate.dart';
|
||||
import 'package:aves/widgets/filter_grids/filter_grid_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class CountryListPage extends StatelessWidget {
|
||||
static const routeName = '/countries';
|
||||
|
||||
final CollectionSource source;
|
||||
|
||||
static final ChipActionDelegate actionDelegate = CountryChipActionDelegate();
|
||||
|
||||
const CountryListPage({@required this.source});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Settings, ChipSortFactor>(
|
||||
selector: (context, s) => s.countrySortFactor,
|
||||
builder: (context, sortFactor, child) {
|
||||
return StreamBuilder(
|
||||
stream: source.eventBus.on<LocationsChangedEvent>(),
|
||||
builder: (context, snapshot) => FilterNavigationPage(
|
||||
source: source,
|
||||
title: 'Countries',
|
||||
filterEntries: source.getCountryEntries(),
|
||||
actionDelegate: actionDelegate,
|
||||
filterEntries: _getCountryEntries(),
|
||||
filterBuilder: (s) => LocationFilter(LocationLevel.country, s),
|
||||
emptyBuilder: () => EmptyContent(
|
||||
icon: AIcons.location,
|
||||
|
@ -28,5 +39,29 @@ class CountryListPage extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, ImageEntry> _getCountryEntries() {
|
||||
final entriesByDate = source.sortedEntriesForFilterList;
|
||||
final locatedEntries = entriesByDate.where((entry) => entry.isLocated);
|
||||
final countries = source.sortedCountries.map((countryNameAndCode) {
|
||||
final split = countryNameAndCode.split(LocationFilter.locationSeparator);
|
||||
ImageEntry entry;
|
||||
if (split.length > 1) {
|
||||
final countryCode = split[1];
|
||||
entry = locatedEntries.firstWhere((entry) => entry.addressDetails.countryCode == countryCode, orElse: () => null);
|
||||
}
|
||||
return MapEntry(countryNameAndCode, entry);
|
||||
}).toList();
|
||||
|
||||
switch (settings.countrySortFactor) {
|
||||
case ChipSortFactor.date:
|
||||
countries.sort(FilterNavigationPage.compareChipByDate);
|
||||
break;
|
||||
case ChipSortFactor.name:
|
||||
}
|
||||
return Map.fromEntries(countries);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,9 +13,14 @@ import 'package:aves/widgets/common/app_bar_title.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/double_back_pop.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:aves/widgets/common/menu_row.dart';
|
||||
import 'package:aves/widgets/drawer/app_drawer.dart';
|
||||
import 'package:aves/widgets/filter_grids/chip_action_delegate.dart';
|
||||
import 'package:aves/widgets/filter_grids/chip_actions.dart';
|
||||
import 'package:aves/widgets/filter_grids/decorated_filter_chip.dart';
|
||||
import 'package:aves/widgets/filter_grids/search_button.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -23,7 +28,7 @@ import 'package:provider/provider.dart';
|
|||
class FilterNavigationPage extends StatelessWidget {
|
||||
final CollectionSource source;
|
||||
final String title;
|
||||
final List<Widget> actions;
|
||||
final ChipActionDelegate actionDelegate;
|
||||
final Map<String, ImageEntry> filterEntries;
|
||||
final CollectionFilter Function(String key) filterBuilder;
|
||||
final Widget Function() emptyBuilder;
|
||||
|
@ -31,7 +36,7 @@ class FilterNavigationPage extends StatelessWidget {
|
|||
const FilterNavigationPage({
|
||||
@required this.source,
|
||||
@required this.title,
|
||||
this.actions,
|
||||
@required this.actionDelegate,
|
||||
@required this.filterEntries,
|
||||
@required this.filterBuilder,
|
||||
@required this.emptyBuilder,
|
||||
|
@ -49,10 +54,7 @@ class FilterNavigationPage extends StatelessWidget {
|
|||
source: source,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
SearchButton(source),
|
||||
...(actions ?? []),
|
||||
],
|
||||
actions: _buildActions(context),
|
||||
titleSpacing: 0,
|
||||
floating: true,
|
||||
),
|
||||
|
@ -80,6 +82,25 @@ class FilterNavigationPage extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildActions(BuildContext context) {
|
||||
return [
|
||||
SearchButton(source),
|
||||
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) => actionDelegate.onChipActionSelected(context, action),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
void _goToSearch(BuildContext context) {
|
||||
Navigator.push(
|
||||
context,
|
||||
|
@ -89,6 +110,11 @@ class FilterNavigationPage extends StatelessWidget {
|
|||
),
|
||||
));
|
||||
}
|
||||
|
||||
static int compareChipByDate(MapEntry<String, ImageEntry> a, MapEntry<String, ImageEntry> b) {
|
||||
final c = b.value.bestDate?.compareTo(a.value.bestDate) ?? -1;
|
||||
return c != 0 ? c : compareAsciiUpperCase(a.key, b.key);
|
||||
}
|
||||
}
|
||||
|
||||
class FilterGridPage extends StatelessWidget {
|
||||
|
@ -190,7 +216,3 @@ class FilterGridPage extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum ChipAction {
|
||||
sort,
|
||||
}
|
||||
|
|
|
@ -1,26 +1,37 @@
|
|||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/model/source/tag.dart';
|
||||
import 'package:aves/widgets/collection/empty.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:aves/widgets/filter_grids/chip_action_delegate.dart';
|
||||
import 'package:aves/widgets/filter_grids/filter_grid_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class TagListPage extends StatelessWidget {
|
||||
static const routeName = '/tags';
|
||||
|
||||
final CollectionSource source;
|
||||
|
||||
static final ChipActionDelegate actionDelegate = TagChipActionDelegate();
|
||||
|
||||
const TagListPage({@required this.source});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Settings, ChipSortFactor>(
|
||||
selector: (context, s) => s.tagSortFactor,
|
||||
builder: (context, sortFactor, child) {
|
||||
return StreamBuilder(
|
||||
stream: source.eventBus.on<TagsChangedEvent>(),
|
||||
builder: (context, snapshot) => FilterNavigationPage(
|
||||
source: source,
|
||||
title: 'Tags',
|
||||
filterEntries: source.getTagEntries(),
|
||||
actionDelegate: actionDelegate,
|
||||
filterEntries: _getTagEntries(),
|
||||
filterBuilder: (s) => TagFilter(s),
|
||||
emptyBuilder: () => EmptyContent(
|
||||
icon: AIcons.tag,
|
||||
|
@ -28,5 +39,25 @@ class TagListPage extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, ImageEntry> _getTagEntries() {
|
||||
final entriesByDate = source.sortedEntriesForFilterList;
|
||||
final tags = source.sortedTags
|
||||
.map((tag) => MapEntry(
|
||||
tag,
|
||||
entriesByDate.firstWhere((entry) => entry.xmpSubjects.contains(tag), orElse: () => null),
|
||||
))
|
||||
.toList();
|
||||
|
||||
switch (settings.tagSortFactor) {
|
||||
case ChipSortFactor.date:
|
||||
tags.sort(FilterNavigationPage.compareChipByDate);
|
||||
break;
|
||||
case ChipSortFactor.name:
|
||||
}
|
||||
return Map.fromEntries(tags);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,9 +83,9 @@ class ImageView extends StatelessWidget {
|
|||
UriPicture(
|
||||
uri: entry.uri,
|
||||
mimeType: entry.mimeType,
|
||||
colorFilter: colorFilter,
|
||||
),
|
||||
placeholderBuilder: (context) => loadingBuilder(context, fastThumbnailProvider),
|
||||
colorFilter: colorFilter,
|
||||
),
|
||||
backgroundDecoration: backgroundDecoration,
|
||||
scaleStateChangedCallback: onScaleChanged,
|
||||
|
|
|
@ -14,6 +14,7 @@ import 'package:aves/widgets/common/data_providers/media_store_collection_provid
|
|||
import 'package:aves/widgets/common/routes.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
import 'package:aves/widgets/fullscreen/fullscreen_page.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
|
@ -94,6 +95,7 @@ class _HomePageState extends State<HomePage> {
|
|||
_shortcutFilters = extraFilters != null ? (extraFilters as List).cast<String>() : null;
|
||||
}
|
||||
}
|
||||
unawaited(FirebaseCrashlytics.instance.setCustomKey('app_mode', AvesApp.mode.toString()));
|
||||
|
||||
if (AvesApp.mode != AppMode.view) {
|
||||
_mediaStore = MediaStoreSource();
|
||||
|
|
|
@ -70,6 +70,12 @@ class SettingsPage extends StatelessWidget {
|
|||
}
|
||||
},
|
||||
),
|
||||
SectionTitle('Privacy'),
|
||||
SwitchListTile(
|
||||
value: settings.isCrashlyticsEnabled,
|
||||
onChanged: (v) => settings.isCrashlyticsEnabled = v,
|
||||
title: Text('Allow anonymous crash reporting'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -52,6 +52,7 @@ class _WelcomePageState extends State<WelcomePage> {
|
|||
children: [
|
||||
..._buildTop(context),
|
||||
Flexible(child: _buildTerms(terms)),
|
||||
SizedBox(height: 16),
|
||||
..._buildBottomControls(context),
|
||||
],
|
||||
),
|
||||
|
@ -92,12 +93,23 @@ class _WelcomePageState extends State<WelcomePage> {
|
|||
}
|
||||
|
||||
List<Widget> _buildBottomControls(BuildContext context) {
|
||||
final checkbox = LabeledCheckbox(
|
||||
key: Key('agree-checkbox'),
|
||||
final checkboxes = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
LabeledCheckbox(
|
||||
value: settings.isCrashlyticsEnabled,
|
||||
onChanged: (v) => setState(() => settings.isCrashlyticsEnabled = v),
|
||||
text: 'Allow anonymous crash reporting',
|
||||
),
|
||||
LabeledCheckbox(
|
||||
key: Key('agree-termsCheckbox'),
|
||||
value: _hasAcceptedTerms,
|
||||
onChanged: (v) => setState(() => _hasAcceptedTerms = v),
|
||||
text: 'I agree to the terms and conditions',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final button = RaisedButton(
|
||||
key: Key('continue-button'),
|
||||
child: Text('Continue'),
|
||||
|
@ -114,16 +126,17 @@ class _WelcomePageState extends State<WelcomePage> {
|
|||
}
|
||||
: null,
|
||||
);
|
||||
|
||||
return MediaQuery.of(context).orientation == Orientation.portrait
|
||||
? [
|
||||
checkbox,
|
||||
checkboxes,
|
||||
button,
|
||||
]
|
||||
: [
|
||||
SizedBox(height: 16),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
checkbox,
|
||||
checkboxes,
|
||||
Spacer(),
|
||||
button,
|
||||
],
|
||||
|
|
221
pubspec.lock
221
pubspec.lock
|
@ -7,21 +7,21 @@ packages:
|
|||
name: _fe_analyzer_shared
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
version: "9.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.39.17"
|
||||
version: "0.40.2"
|
||||
ansicolor:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ansicolor
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
version: "1.0.5"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -42,35 +42,42 @@ packages:
|
|||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
version: "2.5.0-nullsafety"
|
||||
barcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: barcode
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.16.1"
|
||||
version: "1.17.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "2.1.0-nullsafety"
|
||||
cached_network_image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.0+1"
|
||||
version: "2.3.2+1"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0-nullsafety.2"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
version: "1.2.0-nullsafety"
|
||||
charts_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -98,14 +105,14 @@ packages:
|
|||
name: clock
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
version: "1.1.0-nullsafety"
|
||||
collection:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.14.12"
|
||||
version: "1.15.0-nullsafety.2"
|
||||
console_log_handler:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -126,21 +133,14 @@ packages:
|
|||
name: coverage
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.13.11"
|
||||
version: "0.14.1"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.16.2"
|
||||
version: "2.1.5"
|
||||
draggable_scrollbar:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -166,20 +166,62 @@ packages:
|
|||
url: "git://github.com/deckerst/expansion_tile_card.git"
|
||||
source: git
|
||||
version: "1.0.3"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0-nullsafety"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.1.0"
|
||||
version: "6.0.0-nullsafety.1"
|
||||
firebase:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "7.3.0"
|
||||
firebase_core:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_core
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
firebase_core_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
firebase_crashlytics:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_crashlytics
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.4+1"
|
||||
version: "0.2.0-dev.5"
|
||||
firebase_crashlytics_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_crashlytics_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0-dev.2"
|
||||
flushbar:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -192,13 +234,20 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_blurhash:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_blurhash
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
flutter_cache_manager:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_cache_manager
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
version: "1.4.2"
|
||||
flutter_driver:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
@ -233,7 +282,7 @@ packages:
|
|||
name: flutter_markdown
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.3"
|
||||
version: "0.4.4"
|
||||
flutter_native_timezone:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -247,7 +296,7 @@ packages:
|
|||
name: flutter_plugin_android_lifecycle
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
version: "1.0.9"
|
||||
flutter_staggered_animations:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -261,7 +310,7 @@ packages:
|
|||
name: flutter_svg
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.17.4"
|
||||
version: "0.19.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
@ -297,7 +346,7 @@ packages:
|
|||
name: google_maps_flutter
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.30"
|
||||
version: "0.5.32"
|
||||
google_maps_flutter_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -305,13 +354,6 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: html
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.14.0+3"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -339,7 +381,7 @@ packages:
|
|||
name: image
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.12"
|
||||
version: "2.1.15"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -360,14 +402,14 @@ packages:
|
|||
name: js
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.2"
|
||||
version: "0.6.3-nullsafety"
|
||||
json_rpc_2:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_rpc_2
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.2.1"
|
||||
latlong:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -395,21 +437,21 @@ packages:
|
|||
name: markdown
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.7"
|
||||
version: "2.1.8"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.6"
|
||||
version: "0.12.10-nullsafety"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.8"
|
||||
version: "1.3.0-nullsafety.2"
|
||||
mgrs_dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -424,13 +466,6 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -459,6 +494,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.4.12"
|
||||
octo_image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: octo_image
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
outline_material_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -486,7 +528,7 @@ packages:
|
|||
name: package_info
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.1"
|
||||
version: "0.4.3"
|
||||
palette_generator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -500,14 +542,14 @@ packages:
|
|||
name: path
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.6.4"
|
||||
version: "1.8.0-nullsafety"
|
||||
path_drawing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_drawing
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.1"
|
||||
version: "0.4.1+1"
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -521,7 +563,7 @@ packages:
|
|||
name: path_provider
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.6.11"
|
||||
version: "1.6.14"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -535,35 +577,35 @@ packages:
|
|||
name: path_provider_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.0.4+3"
|
||||
version: "0.0.4+4"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
version: "1.0.3"
|
||||
pdf:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pdf
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.10.1"
|
||||
version: "1.11.1"
|
||||
pedantic:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pedantic
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
version: "1.10.0-nullsafety"
|
||||
percent_indicator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: percent_indicator
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
version: "2.1.6"
|
||||
permission_handler:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -584,7 +626,7 @@ packages:
|
|||
name: petitparser
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
version: "3.1.0"
|
||||
photo_view:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -600,7 +642,7 @@ packages:
|
|||
name: platform
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
version: "3.0.0-nullsafety.1"
|
||||
platform_detect:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -621,7 +663,7 @@ packages:
|
|||
name: pool
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version: "1.5.0-nullsafety"
|
||||
positioned_tap_detector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -635,14 +677,14 @@ packages:
|
|||
name: printing
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.6.0"
|
||||
version: "3.6.1"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: process
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.12"
|
||||
version: "4.0.0-nullsafety.1"
|
||||
proj4dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -656,7 +698,7 @@ packages:
|
|||
name: provider
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.3.2"
|
||||
version: "4.3.2+2"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -698,14 +740,14 @@ packages:
|
|||
name: shared_preferences
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.8"
|
||||
version: "0.5.10"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.0.2+1"
|
||||
version: "0.0.2+2"
|
||||
shared_preferences_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -766,28 +808,28 @@ packages:
|
|||
name: source_map_stack_trace
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "2.1.0-nullsafety.1"
|
||||
source_maps:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_maps
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.10.9"
|
||||
version: "0.10.10-nullsafety"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
version: "1.8.0-nullsafety"
|
||||
sqflite:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqflite
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
version: "1.3.1+1"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -801,14 +843,14 @@ packages:
|
|||
name: stack_trace
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.9.3"
|
||||
version: "1.10.0-nullsafety"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "2.1.0-nullsafety"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -829,7 +871,7 @@ packages:
|
|||
name: string_scanner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
version: "1.1.0-nullsafety"
|
||||
sync_http:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -850,28 +892,28 @@ packages:
|
|||
name: term_glyph
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.2.0-nullsafety"
|
||||
test:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: test
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.14.4"
|
||||
version: "1.16.0-nullsafety.4"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.15"
|
||||
version: "0.2.19-nullsafety"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.4"
|
||||
version: "0.3.12-nullsafety.4"
|
||||
transparent_image:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -892,7 +934,7 @@ packages:
|
|||
name: typed_data
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.6"
|
||||
version: "1.3.0-nullsafety.2"
|
||||
unicode:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -906,7 +948,7 @@ packages:
|
|||
name: url_launcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.5.0"
|
||||
version: "5.6.0"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -920,21 +962,28 @@ packages:
|
|||
name: url_launcher_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.0.1+7"
|
||||
version: "0.0.1+8"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.7"
|
||||
version: "1.0.8"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.2+1"
|
||||
version: "0.1.3+2"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.0.1+1"
|
||||
utf:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -948,7 +997,7 @@ packages:
|
|||
name: uuid
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.2.2"
|
||||
validate:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -962,14 +1011,14 @@ packages:
|
|||
name: vector_math
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.8"
|
||||
version: "2.1.0-nullsafety.2"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.2.0"
|
||||
version: "5.0.0+1"
|
||||
vm_service_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1018,14 +1067,14 @@ packages:
|
|||
name: xdg_directories
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
version: "0.1.2"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.6.1"
|
||||
version: "4.5.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1034,5 +1083,5 @@ packages:
|
|||
source: hosted
|
||||
version: "2.2.1"
|
||||
sdks:
|
||||
dart: ">=2.8.0 <3.0.0"
|
||||
flutter: ">=1.17.0 <2.0.0"
|
||||
dart: ">=2.10.0-4.0.dev <2.10.0"
|
||||
flutter: ">=1.20.0 <2.0.0"
|
||||
|
|
|
@ -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.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 1.1.10+22
|
||||
version: 1.1.11+23
|
||||
|
||||
# video_player (as of v0.10.8+2, backed by ExoPlayer):
|
||||
# - does not support content URIs (by default, but trivial by fork)
|
||||
|
@ -47,7 +47,8 @@ dependencies:
|
|||
# path: ../expansion_tile_card
|
||||
git:
|
||||
url: git://github.com/deckerst/expansion_tile_card.git
|
||||
firebase_crashlytics:
|
||||
firebase_core: "^0.5.0"
|
||||
firebase_crashlytics: "^0.2.0-dev.5"
|
||||
flushbar:
|
||||
flutter_ijkplayer:
|
||||
# path: ../flutter_ijkplayer
|
||||
|
|
1
shaders_1.22.0-12.1.pre.sksl.json
Normal file
1
shaders_1.22.0-12.1.pre.sksl.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -4,6 +4,7 @@ import 'package:path/path.dart' as path;
|
|||
|
||||
String get adb {
|
||||
final env = Platform.environment;
|
||||
// e.g. C:\Users\<username>\AppData\Local\Android\Sdk
|
||||
final sdkDir = env['ANDROID_SDK_ROOT'] ?? env['ANDROID_SDK'];
|
||||
return path.join(sdkDir, 'platform-tools', Platform.isWindows ? 'adb.exe' : 'adb');
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue