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:
|
steps:
|
||||||
- uses: subosito/flutter-action@v1
|
- uses: subosito/flutter-action@v1
|
||||||
with:
|
with:
|
||||||
flutter-version: '1.17.5'
|
channel: beta
|
||||||
|
flutter-version: '1.22.0-12.1.pre'
|
||||||
|
|
||||||
- name: Clone the repository.
|
- name: Clone the repository.
|
||||||
uses: actions/checkout@v2
|
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
|
- uses: subosito/flutter-action@v1
|
||||||
with:
|
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):
|
# 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 +50,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
|
flutter build apk --bundle-sksl-path shaders_1.22.0-12.1.pre.sksl.json
|
||||||
flutter build appbundle
|
flutter build appbundle --bundle-sksl-path shaders_1.22.0-12.1.pre.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
|
||||||
|
|
|
@ -12,20 +12,18 @@ Aves is a gallery and metadata explorer app. It is built for Android, with Flutt
|
||||||
|
|
||||||
## Features
|
## 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 animated images: GIF, WEBP
|
||||||
- support vector images: SVG
|
- 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)
|
- search and filter by country, place, XMP tag, type (animated, raster, vector, video)
|
||||||
- bulk delete, share, copy, move
|
|
||||||
- favorites
|
- favorites
|
||||||
- statistics
|
- statistics
|
||||||
- handle intents to view or pick images
|
|
||||||
- support Android API 24 ~ 30 (Nougat ~ R)
|
- support Android API 24 ~ 30 (Nougat ~ R)
|
||||||
|
- Android integration (app shortcuts, handle view/pick intents)
|
||||||
|
|
||||||
## Known Issues
|
## 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))
|
- 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 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
|
- performance: image decoding is slow
|
||||||
|
|
|
@ -106,6 +106,9 @@
|
||||||
android:value="${googleApiKey}" />
|
android:value="${googleApiKey}" />
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
<meta-data
|
||||||
|
android:name="firebase_crashlytics_collection_enabled"
|
||||||
|
android:value="false" />
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
|
|
@ -199,6 +199,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
private Map<String, String> getVideoAllMetadataByMediaMetadataRetriever(String uri) {
|
private Map<String, String> getVideoAllMetadataByMediaMetadataRetriever(String uri) {
|
||||||
Map<String, String> dirMap = new HashMap<>();
|
Map<String, String> dirMap = new HashMap<>();
|
||||||
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, Uri.parse(uri));
|
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, Uri.parse(uri));
|
||||||
|
if (retriever != null) {
|
||||||
try {
|
try {
|
||||||
for (Map.Entry<Integer, String> kv : VIDEO_MEDIA_METADATA_KEYS.entrySet()) {
|
for (Map.Entry<Integer, String> kv : VIDEO_MEDIA_METADATA_KEYS.entrySet()) {
|
||||||
Integer key = kv.getKey();
|
Integer key = kv.getKey();
|
||||||
|
@ -221,6 +222,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
|
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
|
||||||
retriever.release();
|
retriever.release();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return dirMap;
|
return dirMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,6 +319,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
|
||||||
private Map<String, Object> getVideoCatalogMetadataByMediaMetadataRetriever(String uri) {
|
private Map<String, Object> getVideoCatalogMetadataByMediaMetadataRetriever(String uri) {
|
||||||
Map<String, Object> metadataMap = new HashMap<>();
|
Map<String, Object> metadataMap = new HashMap<>();
|
||||||
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, Uri.parse(uri));
|
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, Uri.parse(uri));
|
||||||
|
if (retriever != null) {
|
||||||
try {
|
try {
|
||||||
String dateString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE);
|
String dateString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE);
|
||||||
String rotationString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
|
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
|
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
|
||||||
retriever.release();
|
retriever.release();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return metadataMap;
|
return metadataMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ class VideoThumbnailFetcher implements DataFetcher<InputStream> {
|
||||||
@Override
|
@Override
|
||||||
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
|
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
|
||||||
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(model.getContext(), model.getUri());
|
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(model.getContext(), model.getUri());
|
||||||
|
if (retriever != null) {
|
||||||
try {
|
try {
|
||||||
byte[] picture = retriever.getEmbeddedPicture();
|
byte[] picture = retriever.getEmbeddedPicture();
|
||||||
if (picture != null) {
|
if (picture != null) {
|
||||||
|
@ -46,6 +47,7 @@ class VideoThumbnailFetcher implements DataFetcher<InputStream> {
|
||||||
retriever.release();
|
retriever.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void cleanup() {
|
public void cleanup() {
|
||||||
|
|
|
@ -135,6 +135,7 @@ public class SourceImageEntry {
|
||||||
// finds: width, height, orientation/rotation, date, title, duration
|
// finds: width, height, orientation/rotation, date, title, duration
|
||||||
private void fillByMediaMetadataRetriever(@NonNull Context context) {
|
private void fillByMediaMetadataRetriever(@NonNull Context context) {
|
||||||
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, uri);
|
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, uri);
|
||||||
|
if (retriever != null) {
|
||||||
try {
|
try {
|
||||||
String width = null, height = null, rotation = null, durationMillis = null;
|
String width = null, height = null, rotation = null, durationMillis = null;
|
||||||
if (isImage()) {
|
if (isImage()) {
|
||||||
|
@ -180,6 +181,7 @@ public class SourceImageEntry {
|
||||||
retriever.release();
|
retriever.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// expects entry with: uri, mimeType
|
// expects entry with: uri, mimeType
|
||||||
// finds: width, height, orientation, date
|
// finds: width, height, orientation, date
|
||||||
|
|
|
@ -460,7 +460,8 @@ public class StorageUtils {
|
||||||
}
|
}
|
||||||
retriever.setDataSource(context, uri);
|
retriever.setDataSource(context, uri);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(LOG_TAG, "failed to open MediaMetadataRetriever for uri=" + uri, e);
|
// unsupported format
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return retriever;
|
return retriever;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ buildscript {
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.6.3'
|
classpath 'com.android.tools.build:gradle:3.6.3'
|
||||||
classpath 'com.google.gms:google-services:4.3.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/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/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';
|
||||||
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:overlay_support/overlay_support.dart';
|
import 'package:overlay_support/overlay_support.dart';
|
||||||
|
@ -11,9 +17,15 @@ import 'package:overlay_support/overlay_support.dart';
|
||||||
void main() {
|
void main() {
|
||||||
// HttpClient.enableTimelineLogging = true; // enable network traffic logging
|
// HttpClient.enableTimelineLogging = true; // enable network traffic logging
|
||||||
// debugPrintGestureArenaDiagnostics = true;
|
// 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());
|
runApp(AvesApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,24 +40,11 @@ class AvesApp extends StatefulWidget {
|
||||||
|
|
||||||
class _AvesAppState extends State<AvesApp> {
|
class _AvesAppState extends State<AvesApp> {
|
||||||
Future<void> _appSetup;
|
Future<void> _appSetup;
|
||||||
|
final NavigatorObserver _routeTracker = CrashlyticsRouteTracker();
|
||||||
|
|
||||||
static const accentColor = Colors.indigoAccent;
|
static const accentColor = Colors.indigoAccent;
|
||||||
|
|
||||||
@override
|
static final darkTheme = ThemeData(
|
||||||
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(
|
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
accentColor: accentColor,
|
accentColor: accentColor,
|
||||||
scaffoldBackgroundColor: Colors.grey[900],
|
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,
|
future: _appSetup,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasError) return Icon(AIcons.error);
|
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done) {
|
||||||
if (snapshot.connectionState != ConnectionState.done) return Scaffold();
|
|
||||||
return settings.hasAcceptedTerms ? HomePage() : WelcomePage();
|
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/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:geocoder/geocoder.dart';
|
import 'package:geocoder/geocoder.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart' as ppath;
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
import 'mime_types.dart';
|
import 'mime_types.dart';
|
||||||
|
|
||||||
class ImageEntry {
|
class ImageEntry {
|
||||||
String uri;
|
String uri;
|
||||||
String _path;
|
String _path, _directory, _filename, _extension;
|
||||||
String _directory;
|
|
||||||
String _filename;
|
|
||||||
int contentId;
|
int contentId;
|
||||||
final String sourceMimeType;
|
final String sourceMimeType;
|
||||||
int width;
|
int width;
|
||||||
|
@ -131,20 +129,26 @@ class ImageEntry {
|
||||||
_path = path;
|
_path = path;
|
||||||
_directory = null;
|
_directory = null;
|
||||||
_filename = null;
|
_filename = null;
|
||||||
|
_extension = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String get path => _path;
|
String get path => _path;
|
||||||
|
|
||||||
String get directory {
|
String get directory {
|
||||||
_directory ??= path != null ? dirname(path) : null;
|
_directory ??= path != null ? ppath.dirname(path) : null;
|
||||||
return _directory;
|
return _directory;
|
||||||
}
|
}
|
||||||
|
|
||||||
String get filenameWithoutExtension {
|
String get filenameWithoutExtension {
|
||||||
_filename ??= path != null ? basenameWithoutExtension(path) : null;
|
_filename ??= path != null ? ppath.basenameWithoutExtension(path) : null;
|
||||||
return _filename;
|
return _filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get extension {
|
||||||
|
_extension ??= path != null ? ppath.extension(path) : null;
|
||||||
|
return _extension;
|
||||||
|
}
|
||||||
|
|
||||||
// the MIME type reported by the Media Store is unreliable
|
// the MIME type reported by the Media Store is unreliable
|
||||||
// so we use the one found during cataloguing if possible
|
// so we use the one found during cataloguing if possible
|
||||||
String get mimeType => catalogMetadata?.mimeType ?? sourceMimeType;
|
String get mimeType => catalogMetadata?.mimeType ?? sourceMimeType;
|
||||||
|
@ -318,7 +322,7 @@ class ImageEntry {
|
||||||
Future<bool> rename(String newName) async {
|
Future<bool> rename(String newName) async {
|
||||||
if (newName == filenameWithoutExtension) return true;
|
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;
|
if (newFields.isEmpty) return false;
|
||||||
|
|
||||||
final uri = newFields['uri'];
|
final uri = newFields['uri'];
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import 'package:aves/model/settings/coordinate_format.dart';
|
import 'package:aves/model/settings/coordinate_format.dart';
|
||||||
import 'package:aves/model/settings/home_page.dart';
|
import 'package:aves/model/settings/home_page.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/location_section.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/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:pedantic/pedantic.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../source/enums.dart';
|
import '../source/enums.dart';
|
||||||
|
@ -18,6 +21,7 @@ class Settings extends ChangeNotifier {
|
||||||
|
|
||||||
// app
|
// app
|
||||||
static const hasAcceptedTermsKey = 'has_accepted_terms';
|
static const hasAcceptedTermsKey = 'has_accepted_terms';
|
||||||
|
static const isCrashlyticsEnabledKey = 'is_crashlytics_enabled';
|
||||||
static const mustBackTwiceToExitKey = 'must_back_twice_to_exit';
|
static const mustBackTwiceToExitKey = 'must_back_twice_to_exit';
|
||||||
static const homePageKey = 'home_page';
|
static const homePageKey = 'home_page';
|
||||||
static const catalogTimeZoneKey = 'catalog_time_zone';
|
static const catalogTimeZoneKey = 'catalog_time_zone';
|
||||||
|
@ -29,6 +33,8 @@ class Settings extends ChangeNotifier {
|
||||||
|
|
||||||
// filter grids
|
// filter grids
|
||||||
static const albumSortFactorKey = 'album_sort_factor';
|
static const albumSortFactorKey = 'album_sort_factor';
|
||||||
|
static const countrySortFactorKey = 'country_sort_factor';
|
||||||
|
static const tagSortFactorKey = 'tag_sort_factor';
|
||||||
|
|
||||||
// info
|
// info
|
||||||
static const infoMapStyleKey = 'info_map_style';
|
static const infoMapStyleKey = 'info_map_style';
|
||||||
|
@ -40,6 +46,12 @@ class Settings extends ChangeNotifier {
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
_prefs = await SharedPreferences.getInstance();
|
_prefs = await SharedPreferences.getInstance();
|
||||||
|
await _setupCrashlytics();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setupCrashlytics() async {
|
||||||
|
await Firebase.app().setAutomaticDataCollectionEnabled(isCrashlyticsEnabled);
|
||||||
|
await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(isCrashlyticsEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> reset() {
|
Future<void> reset() {
|
||||||
|
@ -52,6 +64,13 @@ class Settings extends ChangeNotifier {
|
||||||
|
|
||||||
set hasAcceptedTerms(bool newValue) => setAndNotify(hasAcceptedTermsKey, newValue);
|
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);
|
bool get mustBackTwiceToExit => getBoolOrDefault(mustBackTwiceToExitKey, true);
|
||||||
|
|
||||||
set mustBackTwiceToExit(bool newValue) => setAndNotify(mustBackTwiceToExitKey, newValue);
|
set mustBackTwiceToExit(bool newValue) => setAndNotify(mustBackTwiceToExitKey, newValue);
|
||||||
|
@ -84,6 +103,14 @@ class Settings extends ChangeNotifier {
|
||||||
|
|
||||||
set albumSortFactor(ChipSortFactor newValue) => setAndNotify(albumSortFactorKey, newValue.toString());
|
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
|
// info
|
||||||
|
|
||||||
EntryMapStyle get infoMapStyle => getEnumOrDefault(infoMapStyleKey, EntryMapStyle.stamenWatercolor, EntryMapStyle.values);
|
EntryMapStyle get infoMapStyle => getEnumOrDefault(infoMapStyleKey, EntryMapStyle.stamenWatercolor, EntryMapStyle.values);
|
||||||
|
|
|
@ -83,19 +83,6 @@ mixin LocationMixin on SourceBase {
|
||||||
invalidateFilterEntryCounts();
|
invalidateFilterEntryCounts();
|
||||||
eventBus.fire(LocationsChangedEvent());
|
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 {}
|
class AddressMetadataChangedEvent {}
|
||||||
|
|
|
@ -59,14 +59,6 @@ mixin TagMixin on SourceBase {
|
||||||
invalidateFilterEntryCounts();
|
invalidateFilterEntryCounts();
|
||||||
eventBus.fire(TagsChangedEvent());
|
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 {}
|
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(
|
UriPicture(
|
||||||
uri: entry.uri,
|
uri: entry.uri,
|
||||||
mimeType: entry.mimeType,
|
mimeType: entry.mimeType,
|
||||||
colorFilter: colorFilter,
|
|
||||||
),
|
),
|
||||||
width: extent,
|
width: extent,
|
||||||
height: extent,
|
height: extent,
|
||||||
|
colorFilter: colorFilter,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -15,6 +15,7 @@ class CreateAlbumDialog extends StatefulWidget {
|
||||||
class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
||||||
final TextEditingController _nameController = TextEditingController();
|
final TextEditingController _nameController = TextEditingController();
|
||||||
final ValueNotifier<bool> _existsNotifier = ValueNotifier(false);
|
final ValueNotifier<bool> _existsNotifier = ValueNotifier(false);
|
||||||
|
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
|
||||||
Set<StorageVolume> _allVolumes;
|
Set<StorageVolume> _allVolumes;
|
||||||
StorageVolume _primaryVolume, _selectedVolume;
|
StorageVolume _primaryVolume, _selectedVolume;
|
||||||
|
|
||||||
|
@ -24,7 +25,6 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
||||||
_allVolumes = androidFileUtils.storageVolumes;
|
_allVolumes = androidFileUtils.storageVolumes;
|
||||||
_primaryVolume = _allVolumes.firstWhere((volume) => volume.isPrimary, orElse: () => _allVolumes.first);
|
_primaryVolume = _allVolumes.firstWhere((volume) => volume.isPrimary, orElse: () => _allVolumes.first);
|
||||||
_selectedVolume = _primaryVolume;
|
_selectedVolume = _primaryVolume;
|
||||||
_initAlbumName();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -48,7 +48,7 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
||||||
groupValue: _selectedVolume,
|
groupValue: _selectedVolume,
|
||||||
onChanged: (volume) {
|
onChanged: (volume) {
|
||||||
_selectedVolume = volume;
|
_selectedVolume = volume;
|
||||||
_checkAlbumExists();
|
_validate();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
title: Text(
|
title: Text(
|
||||||
|
@ -67,7 +67,7 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
Padding(
|
Padding(
|
||||||
padding: AvesDialog.contentHorizontalPadding,
|
padding: AvesDialog.contentHorizontalPadding + EdgeInsets.only(bottom: 8),
|
||||||
child: ValueListenableBuilder<bool>(
|
child: ValueListenableBuilder<bool>(
|
||||||
valueListenable: _existsNotifier,
|
valueListenable: _existsNotifier,
|
||||||
builder: (context, exists, child) {
|
builder: (context, exists, child) {
|
||||||
|
@ -75,8 +75,10 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
||||||
controller: _nameController,
|
controller: _nameController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
helperText: exists ? 'Album already exists' : '',
|
helperText: exists ? 'Album already exists' : '',
|
||||||
|
hintText: 'Album name',
|
||||||
),
|
),
|
||||||
onChanged: (_) => _checkAlbumExists(),
|
autofocus: _allVolumes.length == 1,
|
||||||
|
onChanged: (_) => _validate(),
|
||||||
onSubmitted: (_) => _submit(context),
|
onSubmitted: (_) => _submit(context),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
@ -87,9 +89,14 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: Text('Cancel'.toUpperCase()),
|
child: Text('Cancel'.toUpperCase()),
|
||||||
),
|
),
|
||||||
FlatButton(
|
ValueListenableBuilder<bool>(
|
||||||
onPressed: () => _submit(context),
|
valueListenable: _isValidNotifier,
|
||||||
|
builder: (context, isValid, child) {
|
||||||
|
return FlatButton(
|
||||||
|
onPressed: isValid ? () => _submit(context) : null,
|
||||||
child: Text('Create'.toUpperCase()),
|
child: Text('Create'.toUpperCase()),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -100,21 +107,10 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
||||||
return join(_selectedVolume.path, 'Pictures', name);
|
return join(_selectedVolume.path, 'Pictures', name);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initAlbumName() async {
|
Future<void> _validate() 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 {
|
|
||||||
final path = _buildAlbumPath(_nameController.text);
|
final path = _buildAlbumPath(_nameController.text);
|
||||||
_existsNotifier.value = path.isEmpty ? false : await Directory(path).exists();
|
_existsNotifier.value = path.isEmpty ? false : await Directory(path).exists();
|
||||||
|
_isValidNotifier.value = (_nameController.text ?? '').isNotEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _submit(BuildContext context) => Navigator.pop(context, _buildAlbumPath(_nameController.text));
|
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:aves/model/image_entry.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
|
||||||
import '../aves_dialog.dart';
|
import '../aves_dialog.dart';
|
||||||
|
|
||||||
|
@ -14,11 +17,13 @@ class RenameEntryDialog extends StatefulWidget {
|
||||||
|
|
||||||
class _RenameEntryDialogState extends State<RenameEntryDialog> {
|
class _RenameEntryDialogState extends State<RenameEntryDialog> {
|
||||||
final TextEditingController _nameController = TextEditingController();
|
final TextEditingController _nameController = TextEditingController();
|
||||||
|
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
|
||||||
|
|
||||||
|
ImageEntry get entry => widget.entry;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final entry = widget.entry;
|
|
||||||
_nameController.text = entry.filenameWithoutExtension ?? entry.sourceTitle;
|
_nameController.text = entry.filenameWithoutExtension ?? entry.sourceTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,17 +39,35 @@ class _RenameEntryDialogState extends State<RenameEntryDialog> {
|
||||||
content: TextField(
|
content: TextField(
|
||||||
controller: _nameController,
|
controller: _nameController,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
|
onChanged: (_) => _validate(),
|
||||||
|
onSubmitted: (_) => _submit(context),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
FlatButton(
|
FlatButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: Text('Cancel'.toUpperCase()),
|
child: Text('Cancel'.toUpperCase()),
|
||||||
),
|
),
|
||||||
FlatButton(
|
ValueListenableBuilder<bool>(
|
||||||
onPressed: () => Navigator.pop(context, _nameController.text),
|
valueListenable: _isValidNotifier,
|
||||||
|
builder: (context, isValid, child) {
|
||||||
|
return FlatButton(
|
||||||
|
onPressed: isValid ? () => _submit(context) : null,
|
||||||
child: Text('Apply'.toUpperCase()),
|
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> {
|
class _AvesFilterChipState extends State<AvesFilterChip> {
|
||||||
Future<Color> _colorFuture;
|
Future<Color> _colorFuture;
|
||||||
|
Color _outlineColor;
|
||||||
bool _tapped;
|
bool _tapped;
|
||||||
|
|
||||||
CollectionFilter get filter => widget.filter;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -160,11 +170,13 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
||||||
child: FutureBuilder<Color>(
|
child: FutureBuilder<Color>(
|
||||||
future: _colorFuture,
|
future: _colorFuture,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final outlineColor = snapshot.hasData ? snapshot.data : Colors.transparent;
|
if (snapshot.hasData) {
|
||||||
|
_outlineColor = snapshot.data;
|
||||||
|
}
|
||||||
return DecoratedBox(
|
return DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: outlineColor,
|
color: _outlineColor,
|
||||||
width: AvesFilterChip.outlineWidth,
|
width: AvesFilterChip.outlineWidth,
|
||||||
),
|
),
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
|
|
|
@ -8,13 +8,10 @@ class UriPicture extends PictureProvider<UriPicture> {
|
||||||
const UriPicture({
|
const UriPicture({
|
||||||
@required this.uri,
|
@required this.uri,
|
||||||
@required this.mimeType,
|
@required this.mimeType,
|
||||||
this.colorFilter,
|
|
||||||
}) : assert(uri != null);
|
}) : assert(uri != null);
|
||||||
|
|
||||||
final String uri, mimeType;
|
final String uri, mimeType;
|
||||||
|
|
||||||
final ColorFilter colorFilter;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<UriPicture> obtainKey(PictureConfiguration configuration) {
|
Future<UriPicture> obtainKey(PictureConfiguration configuration) {
|
||||||
return SynchronousFuture<UriPicture>(this);
|
return SynchronousFuture<UriPicture>(this);
|
||||||
|
@ -37,22 +34,22 @@ class UriPicture extends PictureProvider<UriPicture> {
|
||||||
|
|
||||||
final decoder = SvgPicture.svgByteDecoder;
|
final decoder = SvgPicture.svgByteDecoder;
|
||||||
if (onError != null) {
|
if (onError != null) {
|
||||||
final future = decoder(data, colorFilter, key.toString());
|
final future = decoder(data, null, key.toString());
|
||||||
unawaited(future.catchError(onError));
|
unawaited(future.catchError(onError));
|
||||||
return future;
|
return future;
|
||||||
}
|
}
|
||||||
return decoder(data, colorFilter, key.toString());
|
return decoder(data, null, key.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (other.runtimeType != runtimeType) return false;
|
if (other.runtimeType != runtimeType) return false;
|
||||||
return other is UriPicture && other.uri == uri && other.colorFilter == colorFilter;
|
return other is UriPicture && other.uri == uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => hashValues(uri, colorFilter);
|
int get hashCode => uri.hashCode;
|
||||||
|
|
||||||
@override
|
@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/utils/file_utils.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/fullscreen/info/info_page.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/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
@ -95,6 +97,21 @@ class DebugPageState extends State<DebugPage> {
|
||||||
label: '$timeDilation',
|
label: '$timeDilation',
|
||||||
),
|
),
|
||||||
Divider(),
|
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('Entries: ${entries.length}'),
|
||||||
Text('Catalogued: ${catalogued.length}'),
|
Text('Catalogued: ${catalogued.length}'),
|
||||||
Text('With GPS: ${withGps.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/collection_source.dart';
|
||||||
import 'package:aves/model/source/enums.dart';
|
import 'package:aves/model/source/enums.dart';
|
||||||
import 'package:aves/utils/android_file_utils.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/collection/empty.dart';
|
||||||
import 'package:aves/widgets/common/aves_selection_dialog.dart';
|
|
||||||
import 'package:aves/widgets/common/icons.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:aves/widgets/filter_grids/filter_grid_page.dart';
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class AlbumListPage extends StatelessWidget {
|
class AlbumListPage extends StatelessWidget {
|
||||||
|
@ -21,95 +17,51 @@ class AlbumListPage extends StatelessWidget {
|
||||||
|
|
||||||
final CollectionSource source;
|
final CollectionSource source;
|
||||||
|
|
||||||
|
static final ChipActionDelegate actionDelegate = AlbumChipActionDelegate();
|
||||||
|
|
||||||
const AlbumListPage({@required this.source});
|
const AlbumListPage({@required this.source});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Selector<Settings, ChipSortFactor>(
|
return Selector<Settings, ChipSortFactor>(
|
||||||
selector: (context, s) => s.albumSortFactor,
|
selector: (context, s) => s.albumSortFactor,
|
||||||
builder: (context, albumSortFactor, child) {
|
builder: (context, sortFactor, child) {
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: androidFileUtils.appNameChangeNotifier,
|
animation: androidFileUtils.appNameChangeNotifier,
|
||||||
builder: (context, child) => StreamBuilder(
|
builder: (context, child) => StreamBuilder(
|
||||||
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) => FilterNavigationPage(
|
||||||
return FilterNavigationPage(
|
|
||||||
source: source,
|
source: source,
|
||||||
title: 'Albums',
|
title: 'Albums',
|
||||||
actions: _buildActions(context),
|
actionDelegate: actionDelegate,
|
||||||
filterEntries: getAlbumEntries(source),
|
filterEntries: getAlbumEntries(source),
|
||||||
filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)),
|
filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)),
|
||||||
emptyBuilder: () => EmptyContent(
|
emptyBuilder: () => EmptyContent(
|
||||||
icon: AIcons.album,
|
icon: AIcons.album,
|
||||||
text: 'No albums',
|
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
|
// common with album selection page to move/copy entries
|
||||||
|
|
||||||
static Map<String, ImageEntry> getAlbumEntries(CollectionSource source) {
|
static Map<String, ImageEntry> getAlbumEntries(CollectionSource source) {
|
||||||
final entriesByDate = source.sortedEntriesForFilterList;
|
final entriesByDate = source.sortedEntriesForFilterList;
|
||||||
final albumEntries = source.sortedAlbums.map((album) {
|
final albums = source.sortedAlbums
|
||||||
return MapEntry(
|
.map((album) => MapEntry(
|
||||||
album,
|
album,
|
||||||
entriesByDate.firstWhere((entry) => entry.directory == album, orElse: () => null),
|
entriesByDate.firstWhere((entry) => entry.directory == album, orElse: () => null),
|
||||||
);
|
))
|
||||||
}).toList();
|
.toList();
|
||||||
|
|
||||||
switch (settings.albumSortFactor) {
|
switch (settings.albumSortFactor) {
|
||||||
case ChipSortFactor.date:
|
case ChipSortFactor.date:
|
||||||
albumEntries.sort((a, b) {
|
albums.sort(FilterNavigationPage.compareChipByDate);
|
||||||
final c = b.value.bestDate?.compareTo(a.value.bestDate) ?? -1;
|
return Map.fromEntries(albums);
|
||||||
return c != 0 ? c : compareAsciiUpperCase(a.key, b.key);
|
|
||||||
});
|
|
||||||
return Map.fromEntries(albumEntries);
|
|
||||||
case ChipSortFactor.name:
|
case ChipSortFactor.name:
|
||||||
default:
|
default:
|
||||||
final regularAlbums = <String>[], appAlbums = <String>[], specialAlbums = <String>[];
|
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/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/collection_source.dart';
|
||||||
|
import 'package:aves/model/source/enums.dart';
|
||||||
import 'package:aves/model/source/location.dart';
|
import 'package:aves/model/source/location.dart';
|
||||||
import 'package:aves/widgets/collection/empty.dart';
|
import 'package:aves/widgets/collection/empty.dart';
|
||||||
import 'package:aves/widgets/common/icons.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:aves/widgets/filter_grids/filter_grid_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class CountryListPage extends StatelessWidget {
|
class CountryListPage extends StatelessWidget {
|
||||||
static const routeName = '/countries';
|
static const routeName = '/countries';
|
||||||
|
|
||||||
final CollectionSource source;
|
final CollectionSource source;
|
||||||
|
|
||||||
|
static final ChipActionDelegate actionDelegate = CountryChipActionDelegate();
|
||||||
|
|
||||||
const CountryListPage({@required this.source});
|
const CountryListPage({@required this.source});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
return Selector<Settings, ChipSortFactor>(
|
||||||
|
selector: (context, s) => s.countrySortFactor,
|
||||||
|
builder: (context, sortFactor, child) {
|
||||||
return StreamBuilder(
|
return StreamBuilder(
|
||||||
stream: source.eventBus.on<LocationsChangedEvent>(),
|
stream: source.eventBus.on<LocationsChangedEvent>(),
|
||||||
builder: (context, snapshot) => FilterNavigationPage(
|
builder: (context, snapshot) => FilterNavigationPage(
|
||||||
source: source,
|
source: source,
|
||||||
title: 'Countries',
|
title: 'Countries',
|
||||||
filterEntries: source.getCountryEntries(),
|
actionDelegate: actionDelegate,
|
||||||
|
filterEntries: _getCountryEntries(),
|
||||||
filterBuilder: (s) => LocationFilter(LocationLevel.country, s),
|
filterBuilder: (s) => LocationFilter(LocationLevel.country, s),
|
||||||
emptyBuilder: () => EmptyContent(
|
emptyBuilder: () => EmptyContent(
|
||||||
icon: AIcons.location,
|
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/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/double_back_pop.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/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/decorated_filter_chip.dart';
|
||||||
import 'package:aves/widgets/filter_grids/search_button.dart';
|
import 'package:aves/widgets/filter_grids/search_button.dart';
|
||||||
|
import 'package:collection/collection.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';
|
||||||
|
@ -23,7 +28,7 @@ import 'package:provider/provider.dart';
|
||||||
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 ChipActionDelegate actionDelegate;
|
||||||
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;
|
||||||
|
@ -31,7 +36,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.actionDelegate,
|
||||||
@required this.filterEntries,
|
@required this.filterEntries,
|
||||||
@required this.filterBuilder,
|
@required this.filterBuilder,
|
||||||
@required this.emptyBuilder,
|
@required this.emptyBuilder,
|
||||||
|
@ -49,10 +54,7 @@ class FilterNavigationPage extends StatelessWidget {
|
||||||
source: source,
|
source: source,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: _buildActions(context),
|
||||||
SearchButton(source),
|
|
||||||
...(actions ?? []),
|
|
||||||
],
|
|
||||||
titleSpacing: 0,
|
titleSpacing: 0,
|
||||||
floating: true,
|
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) {
|
void _goToSearch(BuildContext context) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
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 {
|
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/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/collection_source.dart';
|
||||||
|
import 'package:aves/model/source/enums.dart';
|
||||||
import 'package:aves/model/source/tag.dart';
|
import 'package:aves/model/source/tag.dart';
|
||||||
import 'package:aves/widgets/collection/empty.dart';
|
import 'package:aves/widgets/collection/empty.dart';
|
||||||
import 'package:aves/widgets/common/icons.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:aves/widgets/filter_grids/filter_grid_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class TagListPage extends StatelessWidget {
|
class TagListPage extends StatelessWidget {
|
||||||
static const routeName = '/tags';
|
static const routeName = '/tags';
|
||||||
|
|
||||||
final CollectionSource source;
|
final CollectionSource source;
|
||||||
|
|
||||||
|
static final ChipActionDelegate actionDelegate = TagChipActionDelegate();
|
||||||
|
|
||||||
const TagListPage({@required this.source});
|
const TagListPage({@required this.source});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
return Selector<Settings, ChipSortFactor>(
|
||||||
|
selector: (context, s) => s.tagSortFactor,
|
||||||
|
builder: (context, sortFactor, child) {
|
||||||
return StreamBuilder(
|
return StreamBuilder(
|
||||||
stream: source.eventBus.on<TagsChangedEvent>(),
|
stream: source.eventBus.on<TagsChangedEvent>(),
|
||||||
builder: (context, snapshot) => FilterNavigationPage(
|
builder: (context, snapshot) => FilterNavigationPage(
|
||||||
source: source,
|
source: source,
|
||||||
title: 'Tags',
|
title: 'Tags',
|
||||||
filterEntries: source.getTagEntries(),
|
actionDelegate: actionDelegate,
|
||||||
|
filterEntries: _getTagEntries(),
|
||||||
filterBuilder: (s) => TagFilter(s),
|
filterBuilder: (s) => TagFilter(s),
|
||||||
emptyBuilder: () => EmptyContent(
|
emptyBuilder: () => EmptyContent(
|
||||||
icon: AIcons.tag,
|
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(
|
UriPicture(
|
||||||
uri: entry.uri,
|
uri: entry.uri,
|
||||||
mimeType: entry.mimeType,
|
mimeType: entry.mimeType,
|
||||||
colorFilter: colorFilter,
|
|
||||||
),
|
),
|
||||||
placeholderBuilder: (context) => loadingBuilder(context, fastThumbnailProvider),
|
placeholderBuilder: (context) => loadingBuilder(context, fastThumbnailProvider),
|
||||||
|
colorFilter: colorFilter,
|
||||||
),
|
),
|
||||||
backgroundDecoration: backgroundDecoration,
|
backgroundDecoration: backgroundDecoration,
|
||||||
scaleStateChangedCallback: onScaleChanged,
|
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/common/routes.dart';
|
||||||
import 'package:aves/widgets/filter_grids/albums_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:firebase_crashlytics/firebase_crashlytics.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:pedantic/pedantic.dart';
|
import 'package:pedantic/pedantic.dart';
|
||||||
|
@ -94,6 +95,7 @@ class _HomePageState extends State<HomePage> {
|
||||||
_shortcutFilters = extraFilters != null ? (extraFilters as List).cast<String>() : null;
|
_shortcutFilters = extraFilters != null ? (extraFilters as List).cast<String>() : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
unawaited(FirebaseCrashlytics.instance.setCustomKey('app_mode', AvesApp.mode.toString()));
|
||||||
|
|
||||||
if (AvesApp.mode != AppMode.view) {
|
if (AvesApp.mode != AppMode.view) {
|
||||||
_mediaStore = MediaStoreSource();
|
_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: [
|
children: [
|
||||||
..._buildTop(context),
|
..._buildTop(context),
|
||||||
Flexible(child: _buildTerms(terms)),
|
Flexible(child: _buildTerms(terms)),
|
||||||
|
SizedBox(height: 16),
|
||||||
..._buildBottomControls(context),
|
..._buildBottomControls(context),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -92,12 +93,23 @@ class _WelcomePageState extends State<WelcomePage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildBottomControls(BuildContext context) {
|
List<Widget> _buildBottomControls(BuildContext context) {
|
||||||
final checkbox = LabeledCheckbox(
|
final checkboxes = Column(
|
||||||
key: Key('agree-checkbox'),
|
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,
|
value: _hasAcceptedTerms,
|
||||||
onChanged: (v) => setState(() => _hasAcceptedTerms = v),
|
onChanged: (v) => setState(() => _hasAcceptedTerms = v),
|
||||||
text: 'I agree to the terms and conditions',
|
text: 'I agree to the terms and conditions',
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
final button = RaisedButton(
|
final button = RaisedButton(
|
||||||
key: Key('continue-button'),
|
key: Key('continue-button'),
|
||||||
child: Text('Continue'),
|
child: Text('Continue'),
|
||||||
|
@ -114,16 +126,17 @@ class _WelcomePageState extends State<WelcomePage> {
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
return MediaQuery.of(context).orientation == Orientation.portrait
|
return MediaQuery.of(context).orientation == Orientation.portrait
|
||||||
? [
|
? [
|
||||||
checkbox,
|
checkboxes,
|
||||||
button,
|
button,
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
SizedBox(height: 16),
|
|
||||||
Row(
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
checkbox,
|
checkboxes,
|
||||||
Spacer(),
|
Spacer(),
|
||||||
button,
|
button,
|
||||||
],
|
],
|
||||||
|
|
221
pubspec.lock
221
pubspec.lock
|
@ -7,21 +7,21 @@ packages:
|
||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.0"
|
version: "9.0.0"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.39.17"
|
version: "0.40.2"
|
||||||
ansicolor:
|
ansicolor:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: ansicolor
|
name: ansicolor
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.2"
|
version: "1.0.5"
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -42,35 +42,42 @@ packages:
|
||||||
name: async
|
name: async
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.1"
|
version: "2.5.0-nullsafety"
|
||||||
barcode:
|
barcode:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: barcode
|
name: barcode
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.1"
|
version: "1.17.0"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: boolean_selector
|
name: boolean_selector
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.1.0-nullsafety"
|
||||||
cached_network_image:
|
cached_network_image:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: cached_network_image
|
name: cached_network_image
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
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:
|
charcode:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: charcode
|
name: charcode
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.3"
|
version: "1.2.0-nullsafety"
|
||||||
charts_common:
|
charts_common:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -98,14 +105,14 @@ packages:
|
||||||
name: clock
|
name: clock
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
version: "1.1.0-nullsafety"
|
||||||
collection:
|
collection:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.14.12"
|
version: "1.15.0-nullsafety.2"
|
||||||
console_log_handler:
|
console_log_handler:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -126,21 +133,14 @@ packages:
|
||||||
name: coverage
|
name: coverage
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.11"
|
version: "0.14.1"
|
||||||
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.4"
|
version: "2.1.5"
|
||||||
csslib:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: csslib
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.16.2"
|
|
||||||
draggable_scrollbar:
|
draggable_scrollbar:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -166,20 +166,62 @@ 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-nullsafety"
|
||||||
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.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:
|
firebase_crashlytics:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: firebase_crashlytics
|
name: firebase_crashlytics
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
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:
|
flushbar:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -192,13 +234,20 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
flutter_cache_manager:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_cache_manager
|
name: flutter_cache_manager
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.2"
|
||||||
flutter_driver:
|
flutter_driver:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -233,7 +282,7 @@ packages:
|
||||||
name: flutter_markdown
|
name: flutter_markdown
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.3"
|
version: "0.4.4"
|
||||||
flutter_native_timezone:
|
flutter_native_timezone:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -247,7 +296,7 @@ packages:
|
||||||
name: flutter_plugin_android_lifecycle
|
name: flutter_plugin_android_lifecycle
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.8"
|
version: "1.0.9"
|
||||||
flutter_staggered_animations:
|
flutter_staggered_animations:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -261,7 +310,7 @@ packages:
|
||||||
name: flutter_svg
|
name: flutter_svg
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.17.4"
|
version: "0.19.0"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -297,7 +346,7 @@ packages:
|
||||||
name: google_maps_flutter
|
name: google_maps_flutter
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.30"
|
version: "0.5.32"
|
||||||
google_maps_flutter_platform_interface:
|
google_maps_flutter_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -305,13 +354,6 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "1.0.4"
|
||||||
html:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: html
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.14.0+3"
|
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -339,7 +381,7 @@ packages:
|
||||||
name: image
|
name: image
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.12"
|
version: "2.1.15"
|
||||||
intl:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -360,14 +402,14 @@ packages:
|
||||||
name: js
|
name: js
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.2"
|
version: "0.6.3-nullsafety"
|
||||||
json_rpc_2:
|
json_rpc_2:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: json_rpc_2
|
name: json_rpc_2
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.2.1"
|
||||||
latlong:
|
latlong:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -395,21 +437,21 @@ packages:
|
||||||
name: markdown
|
name: markdown
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.7"
|
version: "2.1.8"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.6"
|
version: "0.12.10-nullsafety"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.8"
|
version: "1.3.0-nullsafety.2"
|
||||||
mgrs_dart:
|
mgrs_dart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -424,13 +466,6 @@ 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:
|
||||||
|
@ -459,6 +494,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.12"
|
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:
|
outline_material_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -486,7 +528,7 @@ packages:
|
||||||
name: package_info
|
name: package_info
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.1"
|
version: "0.4.3"
|
||||||
palette_generator:
|
palette_generator:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -500,14 +542,14 @@ packages:
|
||||||
name: path
|
name: path
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.6.4"
|
version: "1.8.0-nullsafety"
|
||||||
path_drawing:
|
path_drawing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_drawing
|
name: path_drawing
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.1"
|
version: "0.4.1+1"
|
||||||
path_parsing:
|
path_parsing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -521,7 +563,7 @@ packages:
|
||||||
name: path_provider
|
name: path_provider
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.6.11"
|
version: "1.6.14"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -535,35 +577,35 @@ packages:
|
||||||
name: path_provider_macos
|
name: path_provider_macos
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.4+3"
|
version: "0.0.4+4"
|
||||||
path_provider_platform_interface:
|
path_provider_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_platform_interface
|
name: path_provider_platform_interface
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.2"
|
version: "1.0.3"
|
||||||
pdf:
|
pdf:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: pdf
|
name: pdf
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.1"
|
version: "1.11.1"
|
||||||
pedantic:
|
pedantic:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: pedantic
|
name: pedantic
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.0"
|
version: "1.10.0-nullsafety"
|
||||||
percent_indicator:
|
percent_indicator:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: percent_indicator
|
name: percent_indicator
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.5"
|
version: "2.1.6"
|
||||||
permission_handler:
|
permission_handler:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -584,7 +626,7 @@ packages:
|
||||||
name: petitparser
|
name: petitparser
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.0"
|
version: "3.1.0"
|
||||||
photo_view:
|
photo_view:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -600,7 +642,7 @@ packages:
|
||||||
name: platform
|
name: platform
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "3.0.0-nullsafety.1"
|
||||||
platform_detect:
|
platform_detect:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -621,7 +663,7 @@ packages:
|
||||||
name: pool
|
name: pool
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.5.0-nullsafety"
|
||||||
positioned_tap_detector:
|
positioned_tap_detector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -635,14 +677,14 @@ packages:
|
||||||
name: printing
|
name: printing
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.6.0"
|
version: "3.6.1"
|
||||||
process:
|
process:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: process
|
name: process
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.12"
|
version: "4.0.0-nullsafety.1"
|
||||||
proj4dart:
|
proj4dart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -656,7 +698,7 @@ packages:
|
||||||
name: provider
|
name: provider
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.3.2"
|
version: "4.3.2+2"
|
||||||
pub_semver:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -698,14 +740,14 @@ packages:
|
||||||
name: shared_preferences
|
name: shared_preferences
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.8"
|
version: "0.5.10"
|
||||||
shared_preferences_linux:
|
shared_preferences_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_linux
|
name: shared_preferences_linux
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.2+1"
|
version: "0.0.2+2"
|
||||||
shared_preferences_macos:
|
shared_preferences_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -766,28 +808,28 @@ packages:
|
||||||
name: source_map_stack_trace
|
name: source_map_stack_trace
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.1.0-nullsafety.1"
|
||||||
source_maps:
|
source_maps:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_maps
|
name: source_maps
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.10.9"
|
version: "0.10.10-nullsafety"
|
||||||
source_span:
|
source_span:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_span
|
name: source_span
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.7.0"
|
version: "1.8.0-nullsafety"
|
||||||
sqflite:
|
sqflite:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: sqflite
|
name: sqflite
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.1+1"
|
||||||
sqflite_common:
|
sqflite_common:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -801,14 +843,14 @@ packages:
|
||||||
name: stack_trace
|
name: stack_trace
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.3"
|
version: "1.10.0-nullsafety"
|
||||||
stream_channel:
|
stream_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stream_channel
|
name: stream_channel
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.1.0-nullsafety"
|
||||||
stream_transform:
|
stream_transform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -829,7 +871,7 @@ packages:
|
||||||
name: string_scanner
|
name: string_scanner
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.5"
|
version: "1.1.0-nullsafety"
|
||||||
sync_http:
|
sync_http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -850,28 +892,28 @@ packages:
|
||||||
name: term_glyph
|
name: term_glyph
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.2.0-nullsafety"
|
||||||
test:
|
test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: test
|
name: test
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.14.4"
|
version: "1.16.0-nullsafety.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.15"
|
version: "0.2.19-nullsafety"
|
||||||
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.4"
|
version: "0.3.12-nullsafety.4"
|
||||||
transparent_image:
|
transparent_image:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -892,7 +934,7 @@ packages:
|
||||||
name: typed_data
|
name: typed_data
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.6"
|
version: "1.3.0-nullsafety.2"
|
||||||
unicode:
|
unicode:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -906,7 +948,7 @@ packages:
|
||||||
name: url_launcher
|
name: url_launcher
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.5.0"
|
version: "5.6.0"
|
||||||
url_launcher_linux:
|
url_launcher_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -920,21 +962,28 @@ packages:
|
||||||
name: url_launcher_macos
|
name: url_launcher_macos
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.1+7"
|
version: "0.0.1+8"
|
||||||
url_launcher_platform_interface:
|
url_launcher_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_platform_interface
|
name: url_launcher_platform_interface
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.7"
|
version: "1.0.8"
|
||||||
url_launcher_web:
|
url_launcher_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_web
|
name: url_launcher_web
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
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:
|
utf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -948,7 +997,7 @@ packages:
|
||||||
name: uuid
|
name: uuid
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.2"
|
||||||
validate:
|
validate:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -962,14 +1011,14 @@ packages:
|
||||||
name: vector_math
|
name: vector_math
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.8"
|
version: "2.1.0-nullsafety.2"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.0"
|
version: "5.0.0+1"
|
||||||
vm_service_client:
|
vm_service_client:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1018,14 +1067,14 @@ packages:
|
||||||
name: xdg_directories
|
name: xdg_directories
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.0"
|
version: "0.1.2"
|
||||||
xml:
|
xml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: xml
|
name: xml
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.6.1"
|
version: "4.5.1"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1034,5 +1083,5 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.8.0 <3.0.0"
|
dart: ">=2.10.0-4.0.dev <2.10.0"
|
||||||
flutter: ">=1.17.0 <2.0.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.
|
# 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.10+22
|
version: 1.1.11+23
|
||||||
|
|
||||||
# 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)
|
||||||
|
@ -47,7 +47,8 @@ dependencies:
|
||||||
# path: ../expansion_tile_card
|
# path: ../expansion_tile_card
|
||||||
git:
|
git:
|
||||||
url: git://github.com/deckerst/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:
|
flushbar:
|
||||||
flutter_ijkplayer:
|
flutter_ijkplayer:
|
||||||
# path: ../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 {
|
String get adb {
|
||||||
final env = Platform.environment;
|
final env = Platform.environment;
|
||||||
|
// e.g. C:\Users\<username>\AppData\Local\Android\Sdk
|
||||||
final sdkDir = env['ANDROID_SDK_ROOT'] ?? env['ANDROID_SDK'];
|
final sdkDir = env['ANDROID_SDK_ROOT'] ?? env['ANDROID_SDK'];
|
||||||
return path.join(sdkDir, 'platform-tools', Platform.isWindows ? 'adb.exe' : 'adb');
|
return path.join(sdkDir, 'platform-tools', Platform.isWindows ? 'adb.exe' : 'adb');
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue