Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2020-09-18 21:49:53 +09:00
commit 8815a793f1
35 changed files with 740 additions and 416 deletions

View file

@ -14,7 +14,8 @@ jobs:
steps:
- uses: subosito/flutter-action@v1
with:
flutter-version: '1.17.5'
channel: beta
flutter-version: '1.22.0-12.1.pre'
- name: Clone the repository.
uses: actions/checkout@v2

View file

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

View file

@ -12,20 +12,18 @@ Aves is a gallery and metadata explorer app. It is built for Android, with Flutt
## Features
- support raster images: JPEG, PNG, GIF, WEBP, BMP, WBMP, HEIC (from Android Pie), DNG
- support raster images: BMP, DNG, GIF, HEIC (from Android Pie), ICO, JPEG, PNG, WBMP, WEBP
- support animated images: GIF, WEBP
- support vector images: SVG
- support videos: MP4, AVI & probably others
- support videos: MP4, AVI, AVCHD & probably others
- search and filter by country, place, XMP tag, type (animated, raster, vector, video)
- bulk delete, share, copy, move
- favorites
- statistics
- handle intents to view or pick images
- support Android API 24 ~ 30 (Nougat ~ R)
- Android integration (app shortcuts, handle view/pick intents)
## Known Issues
- privacy: cannot opt out of Crashlytics reporting (cf [flutterfire issue #1143](https://github.com/FirebaseExtended/flutterfire/issues/1143))
- gesture: double tap on image does not zoom on tapped area (cf [photo_view issue #82](https://github.com/renancaraujo/photo_view/issues/82))
- performance: image info page stutters the first time it loads a Google Maps view (cf [flutter issue #28493](https://github.com/flutter/flutter/issues/28493))
- performance: image decoding is slow

View file

@ -106,6 +106,9 @@
android:value="${googleApiKey}" />
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="false" />
<meta-data
android:name="flutterEmbedding"
android:value="2" />

View file

@ -199,6 +199,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
private Map<String, String> getVideoAllMetadataByMediaMetadataRetriever(String uri) {
Map<String, String> dirMap = new HashMap<>();
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, Uri.parse(uri));
if (retriever != null) {
try {
for (Map.Entry<Integer, String> kv : VIDEO_MEDIA_METADATA_KEYS.entrySet()) {
Integer key = kv.getKey();
@ -221,6 +222,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
retriever.release();
}
}
return dirMap;
}
@ -317,6 +319,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
private Map<String, Object> getVideoCatalogMetadataByMediaMetadataRetriever(String uri) {
Map<String, Object> metadataMap = new HashMap<>();
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, Uri.parse(uri));
if (retriever != null) {
try {
String dateString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE);
String rotationString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
@ -357,6 +360,7 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
retriever.release();
}
}
return metadataMap;
}

View file

@ -25,6 +25,7 @@ class VideoThumbnailFetcher implements DataFetcher<InputStream> {
@Override
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(model.getContext(), model.getUri());
if (retriever != null) {
try {
byte[] picture = retriever.getEmbeddedPicture();
if (picture != null) {
@ -46,6 +47,7 @@ class VideoThumbnailFetcher implements DataFetcher<InputStream> {
retriever.release();
}
}
}
@Override
public void cleanup() {

View file

@ -135,6 +135,7 @@ public class SourceImageEntry {
// finds: width, height, orientation/rotation, date, title, duration
private void fillByMediaMetadataRetriever(@NonNull Context context) {
MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(context, uri);
if (retriever != null) {
try {
String width = null, height = null, rotation = null, durationMillis = null;
if (isImage()) {
@ -180,6 +181,7 @@ public class SourceImageEntry {
retriever.release();
}
}
}
// expects entry with: uri, mimeType
// finds: width, height, orientation, date

View file

@ -460,7 +460,8 @@ public class StorageUtils {
}
retriever.setDataSource(context, uri);
} catch (Exception e) {
Log.e(LOG_TAG, "failed to open MediaMetadataRetriever for uri=" + uri, e);
// unsupported format
return null;
}
return retriever;
}

View file

@ -7,7 +7,7 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:3.6.3'
classpath 'com.google.gms:google-services:4.3.3'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.1.1'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.2.0'
}
}

View file

@ -1,9 +1,15 @@
import 'dart:isolate';
import 'dart:ui';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/utils/route_tracker.dart';
import 'package:aves/widgets/common/data_providers/settings_provider.dart';
import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/home_page.dart';
import 'package:aves/widgets/welcome_page.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:overlay_support/overlay_support.dart';
@ -11,9 +17,15 @@ import 'package:overlay_support/overlay_support.dart';
void main() {
// HttpClient.enableTimelineLogging = true; // enable network traffic logging
// debugPrintGestureArenaDiagnostics = true;
Crashlytics.instance.enableInDevMode = true;
FlutterError.onError = Crashlytics.instance.recordFlutterError;
Isolate.current.addErrorListener(RawReceivePort((pair) async {
final List<dynamic> errorAndStacktrace = pair;
await FirebaseCrashlytics.instance.recordError(
errorAndStacktrace.first,
errorAndStacktrace.last,
);
}).sendPort);
runApp(AvesApp());
}
@ -28,24 +40,11 @@ class AvesApp extends StatefulWidget {
class _AvesAppState extends State<AvesApp> {
Future<void> _appSetup;
final NavigatorObserver _routeTracker = CrashlyticsRouteTracker();
static const accentColor = Colors.indigoAccent;
@override
void initState() {
super.initState();
_appSetup = settings.init();
}
@override
Widget build(BuildContext context) {
// place the settings provider above `MaterialApp`
// so it can be used during navigation transitions
return SettingsProvider(
child: OverlaySupport(
child: MaterialApp(
title: 'Aves',
theme: ThemeData(
static final darkTheme = ThemeData(
brightness: Brightness.dark,
accentColor: accentColor,
scaffoldBackgroundColor: Colors.grey[900],
@ -63,15 +62,74 @@ class _AvesAppState extends State<AvesApp> {
),
),
),
),
home: FutureBuilder<void>(
);
@override
void initState() {
super.initState();
_appSetup = _setup();
}
Future<void> _setup() async {
await Firebase.initializeApp().then((app) {
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError;
FirebaseCrashlytics.instance.setCustomKey('locales', window.locales.join(', '));
final now = DateTime.now();
FirebaseCrashlytics.instance.setCustomKey('timezone', '${now.timeZoneName} (${now.timeZoneOffset})');
FirebaseCrashlytics.instance.setCustomKey(
'build_mode',
kReleaseMode
? 'release'
: kProfileMode
? 'profile'
: 'debug');
});
await settings.init();
}
@override
Widget build(BuildContext context) {
// place the settings provider above `MaterialApp`
// so it can be used during navigation transitions
final home = FutureBuilder<void>(
future: _appSetup,
builder: (context, snapshot) {
if (snapshot.hasError) return Icon(AIcons.error);
if (snapshot.connectionState != ConnectionState.done) return Scaffold();
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done) {
return settings.hasAcceptedTerms ? HomePage() : WelcomePage();
},
}
return Scaffold(
body: snapshot.hasError
? Container(
alignment: Alignment.center,
padding: EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(AIcons.error),
SizedBox(height: 16),
Text(snapshot.error.toString()),
],
),
)
: SizedBox.shrink(),
);
},
);
return SettingsProvider(
child: OverlaySupport(
child: FutureBuilder<void>(
future: _appSetup,
builder: (context, snapshot) {
return MaterialApp(
home: home,
navigatorObservers: [
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done) _routeTracker,
],
title: 'Aves',
darkTheme: darkTheme,
themeMode: ThemeMode.dark,
);
},
),
),
);

View file

@ -10,16 +10,14 @@ import 'package:aves/utils/time_utils.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:geocoder/geocoder.dart';
import 'package:path/path.dart';
import 'package:path/path.dart' as ppath;
import 'package:tuple/tuple.dart';
import 'mime_types.dart';
class ImageEntry {
String uri;
String _path;
String _directory;
String _filename;
String _path, _directory, _filename, _extension;
int contentId;
final String sourceMimeType;
int width;
@ -131,20 +129,26 @@ class ImageEntry {
_path = path;
_directory = null;
_filename = null;
_extension = null;
}
String get path => _path;
String get directory {
_directory ??= path != null ? dirname(path) : null;
_directory ??= path != null ? ppath.dirname(path) : null;
return _directory;
}
String get filenameWithoutExtension {
_filename ??= path != null ? basenameWithoutExtension(path) : null;
_filename ??= path != null ? ppath.basenameWithoutExtension(path) : null;
return _filename;
}
String get extension {
_extension ??= path != null ? ppath.extension(path) : null;
return _extension;
}
// the MIME type reported by the Media Store is unreliable
// so we use the one found during cataloguing if possible
String get mimeType => catalogMetadata?.mimeType ?? sourceMimeType;
@ -318,7 +322,7 @@ class ImageEntry {
Future<bool> rename(String newName) async {
if (newName == filenameWithoutExtension) return true;
final newFields = await ImageFileService.rename(this, '$newName${extension(this.path)}');
final newFields = await ImageFileService.rename(this, '$newName$extension');
if (newFields.isEmpty) return false;
final uri = newFields['uri'];

View file

@ -1,8 +1,11 @@
import 'package:aves/model/settings/coordinate_format.dart';
import 'package:aves/model/settings/home_page.dart';
import 'package:aves/widgets/fullscreen/info/location_section.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:pedantic/pedantic.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../source/enums.dart';
@ -18,6 +21,7 @@ class Settings extends ChangeNotifier {
// app
static const hasAcceptedTermsKey = 'has_accepted_terms';
static const isCrashlyticsEnabledKey = 'is_crashlytics_enabled';
static const mustBackTwiceToExitKey = 'must_back_twice_to_exit';
static const homePageKey = 'home_page';
static const catalogTimeZoneKey = 'catalog_time_zone';
@ -29,6 +33,8 @@ class Settings extends ChangeNotifier {
// filter grids
static const albumSortFactorKey = 'album_sort_factor';
static const countrySortFactorKey = 'country_sort_factor';
static const tagSortFactorKey = 'tag_sort_factor';
// info
static const infoMapStyleKey = 'info_map_style';
@ -40,6 +46,12 @@ class Settings extends ChangeNotifier {
Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
await _setupCrashlytics();
}
Future<void> _setupCrashlytics() async {
await Firebase.app().setAutomaticDataCollectionEnabled(isCrashlyticsEnabled);
await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(isCrashlyticsEnabled);
}
Future<void> reset() {
@ -52,6 +64,13 @@ class Settings extends ChangeNotifier {
set hasAcceptedTerms(bool newValue) => setAndNotify(hasAcceptedTermsKey, newValue);
bool get isCrashlyticsEnabled => getBoolOrDefault(isCrashlyticsEnabledKey, true);
set isCrashlyticsEnabled(bool newValue) {
setAndNotify(isCrashlyticsEnabledKey, newValue);
unawaited(_setupCrashlytics());
}
bool get mustBackTwiceToExit => getBoolOrDefault(mustBackTwiceToExitKey, true);
set mustBackTwiceToExit(bool newValue) => setAndNotify(mustBackTwiceToExitKey, newValue);
@ -84,6 +103,14 @@ class Settings extends ChangeNotifier {
set albumSortFactor(ChipSortFactor newValue) => setAndNotify(albumSortFactorKey, newValue.toString());
ChipSortFactor get countrySortFactor => getEnumOrDefault(countrySortFactorKey, ChipSortFactor.name, ChipSortFactor.values);
set countrySortFactor(ChipSortFactor newValue) => setAndNotify(countrySortFactorKey, newValue.toString());
ChipSortFactor get tagSortFactor => getEnumOrDefault(tagSortFactorKey, ChipSortFactor.name, ChipSortFactor.values);
set tagSortFactor(ChipSortFactor newValue) => setAndNotify(tagSortFactorKey, newValue.toString());
// info
EntryMapStyle get infoMapStyle => getEnumOrDefault(infoMapStyleKey, EntryMapStyle.stamenWatercolor, EntryMapStyle.values);

View file

@ -83,19 +83,6 @@ mixin LocationMixin on SourceBase {
invalidateFilterEntryCounts();
eventBus.fire(LocationsChangedEvent());
}
Map<String, ImageEntry> getCountryEntries() {
final locatedEntries = sortedEntriesForFilterList.where((entry) => entry.isLocated);
return Map.fromEntries(sortedCountries.map((countryNameAndCode) {
final split = countryNameAndCode.split(LocationFilter.locationSeparator);
ImageEntry entry;
if (split.length > 1) {
final countryCode = split[1];
entry = locatedEntries.firstWhere((entry) => entry.addressDetails.countryCode == countryCode, orElse: () => null);
}
return MapEntry(countryNameAndCode, entry);
}));
}
}
class AddressMetadataChangedEvent {}

View file

@ -59,14 +59,6 @@ mixin TagMixin on SourceBase {
invalidateFilterEntryCounts();
eventBus.fire(TagsChangedEvent());
}
Map<String, ImageEntry> getTagEntries() {
final entries = sortedEntriesForFilterList;
return Map.fromEntries(sortedTags.map((tag) => MapEntry(
tag,
entries.firstWhere((entry) => entry.xmpSubjects.contains(tag), orElse: () => null),
)));
}
}
class CatalogMetadataChangedEvent {}

View 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}';
}

View file

@ -32,10 +32,10 @@ class ThumbnailVectorImage extends StatelessWidget {
UriPicture(
uri: entry.uri,
mimeType: entry.mimeType,
colorFilter: colorFilter,
),
width: extent,
height: extent,
colorFilter: colorFilter,
);
},
),

View file

@ -15,6 +15,7 @@ class CreateAlbumDialog extends StatefulWidget {
class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
final TextEditingController _nameController = TextEditingController();
final ValueNotifier<bool> _existsNotifier = ValueNotifier(false);
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
Set<StorageVolume> _allVolumes;
StorageVolume _primaryVolume, _selectedVolume;
@ -24,7 +25,6 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
_allVolumes = androidFileUtils.storageVolumes;
_primaryVolume = _allVolumes.firstWhere((volume) => volume.isPrimary, orElse: () => _allVolumes.first);
_selectedVolume = _primaryVolume;
_initAlbumName();
}
@override
@ -48,7 +48,7 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
groupValue: _selectedVolume,
onChanged: (volume) {
_selectedVolume = volume;
_checkAlbumExists();
_validate();
setState(() {});
},
title: Text(
@ -67,7 +67,7 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
SizedBox(height: 8),
],
Padding(
padding: AvesDialog.contentHorizontalPadding,
padding: AvesDialog.contentHorizontalPadding + EdgeInsets.only(bottom: 8),
child: ValueListenableBuilder<bool>(
valueListenable: _existsNotifier,
builder: (context, exists, child) {
@ -75,8 +75,10 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
controller: _nameController,
decoration: InputDecoration(
helperText: exists ? 'Album already exists' : '',
hintText: 'Album name',
),
onChanged: (_) => _checkAlbumExists(),
autofocus: _allVolumes.length == 1,
onChanged: (_) => _validate(),
onSubmitted: (_) => _submit(context),
);
}),
@ -87,9 +89,14 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
onPressed: () => Navigator.pop(context),
child: Text('Cancel'.toUpperCase()),
),
FlatButton(
onPressed: () => _submit(context),
ValueListenableBuilder<bool>(
valueListenable: _isValidNotifier,
builder: (context, isValid, child) {
return FlatButton(
onPressed: isValid ? () => _submit(context) : null,
child: Text('Create'.toUpperCase()),
);
},
),
],
);
@ -100,21 +107,10 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
return join(_selectedVolume.path, 'Pictures', name);
}
Future<void> _initAlbumName() async {
var count = 1;
while (true) {
var name = 'Album $count';
if (!await Directory(_buildAlbumPath(name)).exists()) {
_nameController.text = name;
return;
}
count++;
}
}
Future<void> _checkAlbumExists() async {
Future<void> _validate() async {
final path = _buildAlbumPath(_nameController.text);
_existsNotifier.value = path.isEmpty ? false : await Directory(path).exists();
_isValidNotifier.value = (_nameController.text ?? '').isNotEmpty;
}
void _submit(BuildContext context) => Navigator.pop(context, _buildAlbumPath(_nameController.text));

View file

@ -1,5 +1,8 @@
import 'dart:io';
import 'package:aves/model/image_entry.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart';
import '../aves_dialog.dart';
@ -14,11 +17,13 @@ class RenameEntryDialog extends StatefulWidget {
class _RenameEntryDialogState extends State<RenameEntryDialog> {
final TextEditingController _nameController = TextEditingController();
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
ImageEntry get entry => widget.entry;
@override
void initState() {
super.initState();
final entry = widget.entry;
_nameController.text = entry.filenameWithoutExtension ?? entry.sourceTitle;
}
@ -34,17 +39,35 @@ class _RenameEntryDialogState extends State<RenameEntryDialog> {
content: TextField(
controller: _nameController,
autofocus: true,
onChanged: (_) => _validate(),
onSubmitted: (_) => _submit(context),
),
actions: [
FlatButton(
onPressed: () => Navigator.pop(context),
child: Text('Cancel'.toUpperCase()),
),
FlatButton(
onPressed: () => Navigator.pop(context, _nameController.text),
ValueListenableBuilder<bool>(
valueListenable: _isValidNotifier,
builder: (context, isValid, child) {
return FlatButton(
onPressed: isValid ? () => _submit(context) : null,
child: Text('Apply'.toUpperCase()),
),
);
},
)
],
);
}
Future<void> _validate() async {
var newName = _nameController.text ?? '';
if (newName.isNotEmpty) {
newName += entry.extension;
}
final type = await FileSystemEntity.type(join(entry.directory, newName));
_isValidNotifier.value = type == FileSystemEntityType.notFound;
}
void _submit(BuildContext context) => Navigator.pop(context, _nameController.text);
}

View file

@ -40,6 +40,7 @@ class AvesFilterChip extends StatefulWidget {
class _AvesFilterChipState extends State<AvesFilterChip> {
Future<Color> _colorFuture;
Color _outlineColor;
bool _tapped;
CollectionFilter get filter => widget.filter;
@ -60,7 +61,16 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
}
}
void _initColorLoader() => _colorFuture = filter.color(context);
void _initColorLoader() {
// For app albums, `filter.color` yields a regular async `Future` the first time
// but it yields a `SynchronousFuture` when called again on a known album.
// This works fine to avoid a frame with no Future data, for new widgets.
// However, when the user moves away and back to a page with a chip using the async future,
// the existing widget FutureBuilder cycles again from the start, with a frame in `waiting` state and no data.
// So we save the result of the Future to a local variable because of this specific case.
_colorFuture = filter.color(context);
_outlineColor = Colors.transparent;
}
@override
Widget build(BuildContext context) {
@ -160,11 +170,13 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
child: FutureBuilder<Color>(
future: _colorFuture,
builder: (context, snapshot) {
final outlineColor = snapshot.hasData ? snapshot.data : Colors.transparent;
if (snapshot.hasData) {
_outlineColor = snapshot.data;
}
return DecoratedBox(
decoration: BoxDecoration(
border: Border.all(
color: outlineColor,
color: _outlineColor,
width: AvesFilterChip.outlineWidth,
),
borderRadius: borderRadius,

View file

@ -8,13 +8,10 @@ class UriPicture extends PictureProvider<UriPicture> {
const UriPicture({
@required this.uri,
@required this.mimeType,
this.colorFilter,
}) : assert(uri != null);
final String uri, mimeType;
final ColorFilter colorFilter;
@override
Future<UriPicture> obtainKey(PictureConfiguration configuration) {
return SynchronousFuture<UriPicture>(this);
@ -37,22 +34,22 @@ class UriPicture extends PictureProvider<UriPicture> {
final decoder = SvgPicture.svgByteDecoder;
if (onError != null) {
final future = decoder(data, colorFilter, key.toString());
final future = decoder(data, null, key.toString());
unawaited(future.catchError(onError));
return future;
}
return decoder(data, colorFilter, key.toString());
return decoder(data, null, key.toString());
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
return other is UriPicture && other.uri == uri && other.colorFilter == colorFilter;
return other is UriPicture && other.uri == uri;
}
@override
int get hashCode => hashValues(uri, colorFilter);
int get hashCode => uri.hashCode;
@override
String toString() => '${objectRuntimeType(this, 'UriPicture')}(uri=$uri, mimeType=$mimeType, colorFilter=$colorFilter)';
String toString() => '${objectRuntimeType(this, 'UriPicture')}(uri=$uri, mimeType=$mimeType)';
}

View file

@ -12,6 +12,8 @@ import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/file_utils.dart';
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
import 'package:aves/widgets/fullscreen/info/info_page.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_svg/flutter_svg.dart';
@ -95,6 +97,21 @@ class DebugPageState extends State<DebugPage> {
label: '$timeDilation',
),
Divider(),
Row(
children: [
Expanded(
child: Text('Crashlytics'),
),
SizedBox(width: 8),
RaisedButton(
onPressed: FirebaseCrashlytics.instance.crash,
child: Text('Crash'),
),
],
),
Text('Firebase data collection: ${Firebase.app().isAutomaticDataCollectionEnabled ? 'enabled' : 'disabled'}'),
Text('Crashlytics collection: ${FirebaseCrashlytics.instance.isCrashlyticsCollectionEnabled ? 'enabled' : 'disabled'}'),
Divider(),
Text('Entries: ${entries.length}'),
Text('Catalogued: ${catalogued.length}'),
Text('With GPS: ${withGps.length}'),

View file

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

View 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;
}

View file

@ -0,0 +1,3 @@
enum ChipAction {
sort,
}

View file

@ -1,26 +1,37 @@
import 'package:aves/model/filters/location.dart';
import 'package:aves/model/image_entry.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/model/source/location.dart';
import 'package:aves/widgets/collection/empty.dart';
import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/filter_grids/chip_action_delegate.dart';
import 'package:aves/widgets/filter_grids/filter_grid_page.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class CountryListPage extends StatelessWidget {
static const routeName = '/countries';
final CollectionSource source;
static final ChipActionDelegate actionDelegate = CountryChipActionDelegate();
const CountryListPage({@required this.source});
@override
Widget build(BuildContext context) {
return Selector<Settings, ChipSortFactor>(
selector: (context, s) => s.countrySortFactor,
builder: (context, sortFactor, child) {
return StreamBuilder(
stream: source.eventBus.on<LocationsChangedEvent>(),
builder: (context, snapshot) => FilterNavigationPage(
source: source,
title: 'Countries',
filterEntries: source.getCountryEntries(),
actionDelegate: actionDelegate,
filterEntries: _getCountryEntries(),
filterBuilder: (s) => LocationFilter(LocationLevel.country, s),
emptyBuilder: () => EmptyContent(
icon: AIcons.location,
@ -28,5 +39,29 @@ class CountryListPage extends StatelessWidget {
),
),
);
},
);
}
Map<String, ImageEntry> _getCountryEntries() {
final entriesByDate = source.sortedEntriesForFilterList;
final locatedEntries = entriesByDate.where((entry) => entry.isLocated);
final countries = source.sortedCountries.map((countryNameAndCode) {
final split = countryNameAndCode.split(LocationFilter.locationSeparator);
ImageEntry entry;
if (split.length > 1) {
final countryCode = split[1];
entry = locatedEntries.firstWhere((entry) => entry.addressDetails.countryCode == countryCode, orElse: () => null);
}
return MapEntry(countryNameAndCode, entry);
}).toList();
switch (settings.countrySortFactor) {
case ChipSortFactor.date:
countries.sort(FilterNavigationPage.compareChipByDate);
break;
case ChipSortFactor.name:
}
return Map.fromEntries(countries);
}
}

View file

@ -13,9 +13,14 @@ import 'package:aves/widgets/common/app_bar_title.dart';
import 'package:aves/widgets/common/aves_filter_chip.dart';
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/double_back_pop.dart';
import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/common/menu_row.dart';
import 'package:aves/widgets/drawer/app_drawer.dart';
import 'package:aves/widgets/filter_grids/chip_action_delegate.dart';
import 'package:aves/widgets/filter_grids/chip_actions.dart';
import 'package:aves/widgets/filter_grids/decorated_filter_chip.dart';
import 'package:aves/widgets/filter_grids/search_button.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:provider/provider.dart';
@ -23,7 +28,7 @@ import 'package:provider/provider.dart';
class FilterNavigationPage extends StatelessWidget {
final CollectionSource source;
final String title;
final List<Widget> actions;
final ChipActionDelegate actionDelegate;
final Map<String, ImageEntry> filterEntries;
final CollectionFilter Function(String key) filterBuilder;
final Widget Function() emptyBuilder;
@ -31,7 +36,7 @@ class FilterNavigationPage extends StatelessWidget {
const FilterNavigationPage({
@required this.source,
@required this.title,
this.actions,
@required this.actionDelegate,
@required this.filterEntries,
@required this.filterBuilder,
@required this.emptyBuilder,
@ -49,10 +54,7 @@ class FilterNavigationPage extends StatelessWidget {
source: source,
),
),
actions: [
SearchButton(source),
...(actions ?? []),
],
actions: _buildActions(context),
titleSpacing: 0,
floating: true,
),
@ -80,6 +82,25 @@ class FilterNavigationPage extends StatelessWidget {
);
}
List<Widget> _buildActions(BuildContext context) {
return [
SearchButton(source),
PopupMenuButton<ChipAction>(
key: Key('appbar-menu-button'),
itemBuilder: (context) {
return [
PopupMenuItem(
key: Key('menu-sort'),
value: ChipAction.sort,
child: MenuRow(text: 'Sort...', icon: AIcons.sort),
),
];
},
onSelected: (action) => actionDelegate.onChipActionSelected(context, action),
),
];
}
void _goToSearch(BuildContext context) {
Navigator.push(
context,
@ -89,6 +110,11 @@ class FilterNavigationPage extends StatelessWidget {
),
));
}
static int compareChipByDate(MapEntry<String, ImageEntry> a, MapEntry<String, ImageEntry> b) {
final c = b.value.bestDate?.compareTo(a.value.bestDate) ?? -1;
return c != 0 ? c : compareAsciiUpperCase(a.key, b.key);
}
}
class FilterGridPage extends StatelessWidget {
@ -190,7 +216,3 @@ class FilterGridPage extends StatelessWidget {
);
}
}
enum ChipAction {
sort,
}

View file

@ -1,26 +1,37 @@
import 'package:aves/model/filters/tag.dart';
import 'package:aves/model/image_entry.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/model/source/tag.dart';
import 'package:aves/widgets/collection/empty.dart';
import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/filter_grids/chip_action_delegate.dart';
import 'package:aves/widgets/filter_grids/filter_grid_page.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class TagListPage extends StatelessWidget {
static const routeName = '/tags';
final CollectionSource source;
static final ChipActionDelegate actionDelegate = TagChipActionDelegate();
const TagListPage({@required this.source});
@override
Widget build(BuildContext context) {
return Selector<Settings, ChipSortFactor>(
selector: (context, s) => s.tagSortFactor,
builder: (context, sortFactor, child) {
return StreamBuilder(
stream: source.eventBus.on<TagsChangedEvent>(),
builder: (context, snapshot) => FilterNavigationPage(
source: source,
title: 'Tags',
filterEntries: source.getTagEntries(),
actionDelegate: actionDelegate,
filterEntries: _getTagEntries(),
filterBuilder: (s) => TagFilter(s),
emptyBuilder: () => EmptyContent(
icon: AIcons.tag,
@ -28,5 +39,25 @@ class TagListPage extends StatelessWidget {
),
),
);
},
);
}
Map<String, ImageEntry> _getTagEntries() {
final entriesByDate = source.sortedEntriesForFilterList;
final tags = source.sortedTags
.map((tag) => MapEntry(
tag,
entriesByDate.firstWhere((entry) => entry.xmpSubjects.contains(tag), orElse: () => null),
))
.toList();
switch (settings.tagSortFactor) {
case ChipSortFactor.date:
tags.sort(FilterNavigationPage.compareChipByDate);
break;
case ChipSortFactor.name:
}
return Map.fromEntries(tags);
}
}

View file

@ -83,9 +83,9 @@ class ImageView extends StatelessWidget {
UriPicture(
uri: entry.uri,
mimeType: entry.mimeType,
colorFilter: colorFilter,
),
placeholderBuilder: (context) => loadingBuilder(context, fastThumbnailProvider),
colorFilter: colorFilter,
),
backgroundDecoration: backgroundDecoration,
scaleStateChangedCallback: onScaleChanged,

View file

@ -14,6 +14,7 @@ import 'package:aves/widgets/common/data_providers/media_store_collection_provid
import 'package:aves/widgets/common/routes.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/fullscreen/fullscreen_page.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pedantic/pedantic.dart';
@ -94,6 +95,7 @@ class _HomePageState extends State<HomePage> {
_shortcutFilters = extraFilters != null ? (extraFilters as List).cast<String>() : null;
}
}
unawaited(FirebaseCrashlytics.instance.setCustomKey('app_mode', AvesApp.mode.toString()));
if (AvesApp.mode != AppMode.view) {
_mediaStore = MediaStoreSource();

View file

@ -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'),
),
],
),
),

View file

@ -52,6 +52,7 @@ class _WelcomePageState extends State<WelcomePage> {
children: [
..._buildTop(context),
Flexible(child: _buildTerms(terms)),
SizedBox(height: 16),
..._buildBottomControls(context),
],
),
@ -92,12 +93,23 @@ class _WelcomePageState extends State<WelcomePage> {
}
List<Widget> _buildBottomControls(BuildContext context) {
final checkbox = LabeledCheckbox(
key: Key('agree-checkbox'),
final checkboxes = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LabeledCheckbox(
value: settings.isCrashlyticsEnabled,
onChanged: (v) => setState(() => settings.isCrashlyticsEnabled = v),
text: 'Allow anonymous crash reporting',
),
LabeledCheckbox(
key: Key('agree-termsCheckbox'),
value: _hasAcceptedTerms,
onChanged: (v) => setState(() => _hasAcceptedTerms = v),
text: 'I agree to the terms and conditions',
),
],
);
final button = RaisedButton(
key: Key('continue-button'),
child: Text('Continue'),
@ -114,16 +126,17 @@ class _WelcomePageState extends State<WelcomePage> {
}
: null,
);
return MediaQuery.of(context).orientation == Orientation.portrait
? [
checkbox,
checkboxes,
button,
]
: [
SizedBox(height: 16),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
checkbox,
checkboxes,
Spacer(),
button,
],

View file

@ -7,21 +7,21 @@ packages:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "7.0.0"
version: "9.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "0.39.17"
version: "0.40.2"
ansicolor:
dependency: transitive
description:
name: ansicolor
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
version: "1.0.5"
archive:
dependency: transitive
description:
@ -42,35 +42,42 @@ packages:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.1"
version: "2.5.0-nullsafety"
barcode:
dependency: transitive
description:
name: barcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.16.1"
version: "1.17.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.1.0-nullsafety"
cached_network_image:
dependency: transitive
description:
name: cached_network_image
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0+1"
version: "2.3.2+1"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.2"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.3"
version: "1.2.0-nullsafety"
charts_common:
dependency: transitive
description:
@ -98,14 +105,14 @@ packages:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
version: "1.1.0-nullsafety"
collection:
dependency: "direct main"
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.14.12"
version: "1.15.0-nullsafety.2"
console_log_handler:
dependency: transitive
description:
@ -126,21 +133,14 @@ packages:
name: coverage
url: "https://pub.dartlang.org"
source: hosted
version: "0.13.11"
version: "0.14.1"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.4"
csslib:
dependency: transitive
description:
name: csslib
url: "https://pub.dartlang.org"
source: hosted
version: "0.16.2"
version: "2.1.5"
draggable_scrollbar:
dependency: "direct main"
description:
@ -166,20 +166,62 @@ packages:
url: "git://github.com/deckerst/expansion_tile_card.git"
source: git
version: "1.0.3"
fake_async:
dependency: transitive
description:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety"
file:
dependency: transitive
description:
name: file
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.0"
version: "6.0.0-nullsafety.1"
firebase:
dependency: transitive
description:
name: firebase
url: "https://pub.dartlang.org"
source: hosted
version: "7.3.0"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.0"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
firebase_crashlytics:
dependency: "direct main"
description:
name: firebase_crashlytics
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.4+1"
version: "0.2.0-dev.5"
firebase_crashlytics_platform_interface:
dependency: transitive
description:
name: firebase_crashlytics_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0-dev.2"
flushbar:
dependency: "direct main"
description:
@ -192,13 +234,20 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_blurhash:
dependency: transitive
description:
name: flutter_blurhash
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.0"
flutter_cache_manager:
dependency: transitive
description:
name: flutter_cache_manager
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.1"
version: "1.4.2"
flutter_driver:
dependency: "direct dev"
description: flutter
@ -233,7 +282,7 @@ packages:
name: flutter_markdown
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.3"
version: "0.4.4"
flutter_native_timezone:
dependency: "direct main"
description:
@ -247,7 +296,7 @@ packages:
name: flutter_plugin_android_lifecycle
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.8"
version: "1.0.9"
flutter_staggered_animations:
dependency: "direct main"
description:
@ -261,7 +310,7 @@ packages:
name: flutter_svg
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.4"
version: "0.19.0"
flutter_test:
dependency: "direct dev"
description: flutter
@ -297,7 +346,7 @@ packages:
name: google_maps_flutter
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.30"
version: "0.5.32"
google_maps_flutter_platform_interface:
dependency: transitive
description:
@ -305,13 +354,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
html:
dependency: transitive
description:
name: html
url: "https://pub.dartlang.org"
source: hosted
version: "0.14.0+3"
http:
dependency: transitive
description:
@ -339,7 +381,7 @@ packages:
name: image
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.12"
version: "2.1.15"
intl:
dependency: "direct main"
description:
@ -360,14 +402,14 @@ packages:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.2"
version: "0.6.3-nullsafety"
json_rpc_2:
dependency: transitive
description:
name: json_rpc_2
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "2.2.1"
latlong:
dependency: "direct main"
description:
@ -395,21 +437,21 @@ packages:
name: markdown
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.7"
version: "2.1.8"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.6"
version: "0.12.10-nullsafety"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.8"
version: "1.3.0-nullsafety.2"
mgrs_dart:
dependency: transitive
description:
@ -424,13 +466,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.7"
multi_server_socket:
dependency: transitive
description:
name: multi_server_socket
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
nested:
dependency: transitive
description:
@ -459,6 +494,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.12"
octo_image:
dependency: transitive
description:
name: octo_image
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
outline_material_icons:
dependency: "direct main"
description:
@ -486,7 +528,7 @@ packages:
name: package_info
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.1"
version: "0.4.3"
palette_generator:
dependency: "direct main"
description:
@ -500,14 +542,14 @@ packages:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.4"
version: "1.8.0-nullsafety"
path_drawing:
dependency: transitive
description:
name: path_drawing
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.1"
version: "0.4.1+1"
path_parsing:
dependency: transitive
description:
@ -521,7 +563,7 @@ packages:
name: path_provider
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.11"
version: "1.6.14"
path_provider_linux:
dependency: transitive
description:
@ -535,35 +577,35 @@ packages:
name: path_provider_macos
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.4+3"
version: "0.0.4+4"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
version: "1.0.3"
pdf:
dependency: "direct main"
description:
name: pdf
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.1"
version: "1.11.1"
pedantic:
dependency: "direct main"
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.0"
version: "1.10.0-nullsafety"
percent_indicator:
dependency: "direct main"
description:
name: percent_indicator
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.5"
version: "2.1.6"
permission_handler:
dependency: "direct main"
description:
@ -584,7 +626,7 @@ packages:
name: petitparser
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.0"
version: "3.1.0"
photo_view:
dependency: "direct main"
description:
@ -600,7 +642,7 @@ packages:
name: platform
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.1"
version: "3.0.0-nullsafety.1"
platform_detect:
dependency: transitive
description:
@ -621,7 +663,7 @@ packages:
name: pool
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
version: "1.5.0-nullsafety"
positioned_tap_detector:
dependency: transitive
description:
@ -635,14 +677,14 @@ packages:
name: printing
url: "https://pub.dartlang.org"
source: hosted
version: "3.6.0"
version: "3.6.1"
process:
dependency: transitive
description:
name: process
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.12"
version: "4.0.0-nullsafety.1"
proj4dart:
dependency: transitive
description:
@ -656,7 +698,7 @@ packages:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "4.3.2"
version: "4.3.2+2"
pub_semver:
dependency: transitive
description:
@ -698,14 +740,14 @@ packages:
name: shared_preferences
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.8"
version: "0.5.10"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.2+1"
version: "0.0.2+2"
shared_preferences_macos:
dependency: transitive
description:
@ -766,28 +808,28 @@ packages:
name: source_map_stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.1.0-nullsafety.1"
source_maps:
dependency: transitive
description:
name: source_maps
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.9"
version: "0.10.10-nullsafety"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
version: "1.8.0-nullsafety"
sqflite:
dependency: "direct main"
description:
name: sqflite
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
version: "1.3.1+1"
sqflite_common:
dependency: transitive
description:
@ -801,14 +843,14 @@ packages:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.3"
version: "1.10.0-nullsafety"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.1.0-nullsafety"
stream_transform:
dependency: transitive
description:
@ -829,7 +871,7 @@ packages:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
version: "1.1.0-nullsafety"
sync_http:
dependency: transitive
description:
@ -850,28 +892,28 @@ packages:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
version: "1.2.0-nullsafety"
test:
dependency: "direct dev"
description:
name: test
url: "https://pub.dartlang.org"
source: hosted
version: "1.14.4"
version: "1.16.0-nullsafety.4"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.15"
version: "0.2.19-nullsafety"
test_core:
dependency: transitive
description:
name: test_core
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.4"
version: "0.3.12-nullsafety.4"
transparent_image:
dependency: transitive
description:
@ -892,7 +934,7 @@ packages:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.6"
version: "1.3.0-nullsafety.2"
unicode:
dependency: transitive
description:
@ -906,7 +948,7 @@ packages:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "5.5.0"
version: "5.6.0"
url_launcher_linux:
dependency: transitive
description:
@ -920,21 +962,28 @@ packages:
name: url_launcher_macos
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.1+7"
version: "0.0.1+8"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.7"
version: "1.0.8"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2+1"
version: "0.1.3+2"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.1+1"
utf:
dependency: transitive
description:
@ -948,7 +997,7 @@ packages:
name: uuid
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
version: "2.2.2"
validate:
dependency: transitive
description:
@ -962,14 +1011,14 @@ packages:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
version: "2.1.0-nullsafety.2"
vm_service:
dependency: transitive
description:
name: vm_service
url: "https://pub.dartlang.org"
source: hosted
version: "4.2.0"
version: "5.0.0+1"
vm_service_client:
dependency: transitive
description:
@ -1018,14 +1067,14 @@ packages:
name: xdg_directories
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.0"
version: "0.1.2"
xml:
dependency: transitive
description:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "3.6.1"
version: "4.5.1"
yaml:
dependency: transitive
description:
@ -1034,5 +1083,5 @@ packages:
source: hosted
version: "2.2.1"
sdks:
dart: ">=2.8.0 <3.0.0"
flutter: ">=1.17.0 <2.0.0"
dart: ">=2.10.0-4.0.dev <2.10.0"
flutter: ">=1.20.0 <2.0.0"

View file

@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.1.10+22
version: 1.1.11+23
# video_player (as of v0.10.8+2, backed by ExoPlayer):
# - does not support content URIs (by default, but trivial by fork)
@ -47,7 +47,8 @@ dependencies:
# path: ../expansion_tile_card
git:
url: git://github.com/deckerst/expansion_tile_card.git
firebase_crashlytics:
firebase_core: "^0.5.0"
firebase_crashlytics: "^0.2.0-dev.5"
flushbar:
flutter_ijkplayer:
# path: ../flutter_ijkplayer

File diff suppressed because one or more lines are too long

View file

@ -4,6 +4,7 @@ import 'package:path/path.dart' as path;
String get adb {
final env = Platform.environment;
// e.g. C:\Users\<username>\AppData\Local\Android\Sdk
final sdkDir = env['ANDROID_SDK_ROOT'] ?? env['ANDROID_SDK'];
return path.join(sdkDir, 'platform-tools', Platform.isWindows ? 'adb.exe' : 'adb');
}