Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2020-07-12 21:52:45 +09:00
commit 9d689b74d1
16 changed files with 226 additions and 75 deletions

62
.github/workflows/main.yml vendored Normal file
View file

@ -0,0 +1,62 @@
name: Release an APK and an App Bundle on tagging
on:
push:
tags:
- v*
jobs:
build:
name: Build and release artifacts.
runs-on: ubuntu-latest
steps:
- uses: actions/setup-java@v1
with:
java-version: '11.x'
- uses: subosito/flutter-action@v1
with:
channel: 'stable'
# Workaround for this Android Gradle Plugin issue (supposedly fixed in AGP 4.1):
# https://issuetracker.google.com/issues/144111441
- name: Install NDK
run: echo "y" | sudo /usr/local/lib/android/sdk/tools/bin/sdkmanager --install "ndk;20.0.5594570" --sdk_root=${ANDROID_SDK_ROOT}
- name: Clone the repository.
uses: actions/checkout@v2
- name: Get packages for the Flutter project.
run: flutter pub get
- name: Update the flutter version file.
working-directory: ${{ github.workspace }}/scripts
run: ./update_flutter_version.sh
# `flutter test` fails if test directory is missing
#- name: Run the unit tests.
# run: flutter test
- name: Build signed artifacts.
# `KEY_JKS` should contain the result of:
# gpg -c --armor keystore.jks
# `KEY_JKS_PASSPHRASE` should contain the passphrase used for the command above
run: |
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
rm $AVES_STORE_FILE
env:
AVES_STORE_FILE: ${{ github.workspace }}/key.jks
AVES_STORE_PASSWORD: ${{ secrets.AVES_STORE_PASSWORD }}
AVES_KEY_ALIAS: ${{ secrets.AVES_KEY_ALIAS }}
AVES_KEY_PASSWORD: ${{ secrets.AVES_KEY_PASSWORD }}
AVES_GOOGLE_API_KEY: ${{ secrets.AVES_GOOGLE_API_KEY }}
- name: Create a release with the APK and App Bundle.
uses: ncipollo/release-action@v1
with:
artifacts: "build/app/outputs/apk/release/*.apk,build/app/outputs/bundle/release/*.aab"
token: ${{ secrets.RELEASE_WORKFLOW_TOKEN }}

View file

@ -27,7 +27,17 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
def keystoreProperties = new Properties() def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties') def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) { if (keystorePropertiesFile.exists()) {
// for release using credentials stored in a local file
keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
} else {
// for release using credentials in environment variables set up by Github Actions
// warning: in property file, single quotes should be escaped with a backslash
// but they should not be escaped when stored in env variables
keystoreProperties['storeFile'] = System.getenv('AVES_STORE_FILE')
keystoreProperties['storePassword'] = System.getenv('AVES_STORE_PASSWORD')
keystoreProperties['keyAlias'] = System.getenv('AVES_KEY_ALIAS')
keystoreProperties['keyPassword'] = System.getenv('AVES_KEY_PASSWORD')
keystoreProperties['googleApiKey'] = System.getenv('AVES_GOOGLE_API_KEY')
} }
android { android {

View file

@ -1,9 +1,7 @@
package deckers.thibault.aves.model.provider; package deckers.thibault.aves.model.provider;
import android.app.Activity; import android.app.Activity;
import android.content.ContentResolver;
import android.content.ContentUris; import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Bitmap; import android.graphics.Bitmap;
@ -11,7 +9,6 @@ import android.graphics.BitmapFactory;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.media.MediaScannerConnection; import android.media.MediaScannerConnection;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.provider.MediaStore; import android.provider.MediaStore;

View file

@ -1,7 +1,6 @@
package deckers.thibault.aves.utils; package deckers.thibault.aves.utils;
import android.app.Activity; import android.app.Activity;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.UriPermission; import android.content.UriPermission;
import android.net.Uri; import android.net.Uri;
@ -20,7 +19,6 @@ import androidx.core.app.ActivityCompat;
import java.io.File; import java.io.File;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
public class PermissionManager { public class PermissionManager {
private static final String LOG_TAG = Utils.createLogTag(PermissionManager.class); private static final String LOG_TAG = Utils.createLogTag(PermissionManager.class);

View file

@ -45,7 +45,10 @@ public class StorageUtils {
* Volume paths * Volume paths
*/ */
// volume paths, with trailing "/"
private static String[] mStorageVolumePaths; private static String[] mStorageVolumePaths;
// primary volume path, with trailing "/"
private static String mPrimaryVolumePath; private static String mPrimaryVolumePath;
private static String getPrimaryVolumePath() { private static String getPrimaryVolumePath() {
@ -90,8 +93,8 @@ public class StorageUtils {
private static String findPrimaryVolumePath() { private static String findPrimaryVolumePath() {
String primaryVolumePath = Environment.getExternalStorageDirectory().getAbsolutePath(); String primaryVolumePath = Environment.getExternalStorageDirectory().getAbsolutePath();
if (!primaryVolumePath.endsWith("/")) { if (!primaryVolumePath.endsWith(File.separator)) {
primaryVolumePath += "/"; primaryVolumePath += File.separator;
} }
return primaryVolumePath; return primaryVolumePath;
} }
@ -167,8 +170,8 @@ public class StorageUtils {
String[] paths = rv.toArray(new String[0]); String[] paths = rv.toArray(new String[0]);
for (int i = 0; i < paths.length; i++) { for (int i = 0; i < paths.length; i++) {
String path = paths[i]; String path = paths[i];
if (path.endsWith(File.separator)) { if (!path.endsWith(File.separator)) {
paths[i] = path.substring(0, path.length() - 1); paths[i] = path + File.separator;
} }
} }
return paths; return paths;
@ -361,8 +364,11 @@ public class StorageUtils {
*/ */
public static boolean requireAccessPermission(@NonNull String anyPath) { public static boolean requireAccessPermission(@NonNull String anyPath) {
// on Android R, we should always require access permission, even on primary volume
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
return true;
}
boolean onPrimaryVolume = anyPath.startsWith(getPrimaryVolumePath()); boolean onPrimaryVolume = anyPath.startsWith(getPrimaryVolumePath());
// TODO TLAD on Android R, we should require access permission even on primary
return !onPrimaryVolume; return !onPrimaryVolume;
} }

10
lib/flutter_version.dart Normal file
View file

@ -0,0 +1,10 @@
// run `scripts/update_flutter_version.sh` to update with the content of `flutter --version --machine`
const Map<String, String> version = {
'channel': 'unknown',
'dartSdkVersion': 'unknown',
'engineRevision': 'unknown',
'frameworkCommitDate': 'unknown',
'frameworkRevision': 'unknown',
'frameworkVersion': 'unknown',
'repositoryUrl': 'unknown',
};

View file

@ -62,7 +62,7 @@ class _AvesAppState extends State<AvesApp> {
future: _appSetup, future: _appSetup,
builder: (context, AsyncSnapshot<void> snapshot) { builder: (context, AsyncSnapshot<void> snapshot) {
if (snapshot.hasError) return const Icon(AIcons.error); if (snapshot.hasError) return const Icon(AIcons.error);
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); if (snapshot.connectionState != ConnectionState.done) return const Scaffold();
return settings.hasAcceptedTerms ? const HomePage() : const WelcomePage(); return settings.hasAcceptedTerms ? const HomePage() : const WelcomePage();
}, },
), ),

View file

@ -1,5 +1,7 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:aves/model/image_entry.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -86,7 +88,10 @@ class AndroidAppService {
} }
} }
static Future<void> share(Map<String, List<String>> urisByMimeType) async { static Future<void> share(Set<ImageEntry> entries) async {
// loosen mime type to a generic one, so we can share with badly defined apps
// e.g. Google Lens declares receiving "image/jpeg" only, but it can actually handle more formats
final urisByMimeType = groupBy<ImageEntry, String>(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList()));
try { try {
await platform.invokeMethod('share', <String, dynamic>{ await platform.invokeMethod('share', <String, dynamic>{
'title': 'Share via:', 'title': 'Share via:',

View file

@ -1,7 +1,10 @@
import 'package:aves/flutter_version.dart';
import 'package:aves/widgets/about/licenses.dart'; import 'package:aves/widgets/about/licenses.dart';
import 'package:aves/widgets/common/aves_logo.dart';
import 'package:aves/widgets/common/link_chip.dart'; import 'package:aves/widgets/common/link_chip.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:package_info/package_info.dart';
class AboutPage extends StatelessWidget { class AboutPage extends StatelessWidget {
@override @override
@ -19,31 +22,8 @@ class AboutPage extends StatelessWidget {
sliver: SliverList( sliver: SliverList(
delegate: SliverChildListDelegate( delegate: SliverChildListDelegate(
[ [
Center( AppReference(),
child: Column( const SizedBox(height: 16),
children: [
Text.rich(
TextSpan(
children: [
const TextSpan(text: 'Made with ❤️ and '),
WidgetSpan(
child: FlutterLogo(
size: Theme.of(context).textTheme.bodyText2.fontSize * 1.25,
),
),
],
),
),
const SizedBox(height: 8),
const LinkChip(
text: 'Sources',
url: 'https://github.com/deckerst/aves',
textStyle: TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
const SizedBox(height: 8),
const Divider(), const Divider(),
], ],
), ),
@ -57,3 +37,71 @@ class AboutPage extends StatelessWidget {
); );
} }
} }
class AppReference extends StatefulWidget {
@override
_AppReferenceState createState() => _AppReferenceState();
}
class _AppReferenceState extends State<AppReference> {
Future<PackageInfo> packageInfoLoader;
@override
void initState() {
super.initState();
packageInfoLoader = PackageInfo.fromPlatform();
}
@override
Widget build(BuildContext context) {
return Center(
child: Column(
children: [
_buildAvesLine(),
_buildFlutterLine(),
],
),
);
}
Widget _buildAvesLine() {
final textTheme = Theme.of(context).textTheme;
final style = textTheme.headline6.copyWith(fontWeight: FontWeight.bold);
return FutureBuilder<PackageInfo>(
future: packageInfoLoader,
builder: (context, snapshot) {
return LinkChip(
leading: AvesLogo(
size: style.fontSize * 1.25,
),
text: 'Aves ${snapshot.data?.version}',
url: 'https://github.com/deckerst/aves',
textStyle: style,
);
},
);
}
Widget _buildFlutterLine() {
final style = DefaultTextStyle.of(context).style;
final subColor = style.color.withOpacity(.6);
return Text.rich(
TextSpan(
children: [
WidgetSpan(
child: Padding(
padding: const EdgeInsetsDirectional.only(end: 4),
child: FlutterLogo(
size: style.fontSize * 1.25,
),
),
),
TextSpan(text: 'Flutter ${version['frameworkVersion']}'),
],
),
style: TextStyle(color: subColor),
);
}
}

View file

@ -64,9 +64,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin {
AndroidAppService.setAs(entry.uri, entry.mimeType); AndroidAppService.setAs(entry.uri, entry.mimeType);
break; break;
case EntryAction.share: case EntryAction.share:
AndroidAppService.share({ AndroidAppService.share({entry});
entry.mimeType: [entry.uri]
});
break; break;
case EntryAction.debug: case EntryAction.debug:
_goToDebug(context, entry); _goToDebug(context, entry);

View file

@ -17,7 +17,6 @@ import 'package:aves/widgets/common/action_delegates/permission_aware.dart';
import 'package:aves/widgets/common/entry_actions.dart'; import 'package:aves/widgets/common/entry_actions.dart';
import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/filter_grid_page.dart'; import 'package:aves/widgets/filter_grid_page.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
@ -38,7 +37,7 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
_showDeleteDialog(context); _showDeleteDialog(context);
break; break;
case EntryAction.share: case EntryAction.share:
_share(); AndroidAppService.share(collection.selection);
break; break;
default: default:
break; break;
@ -229,11 +228,6 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
); );
} }
void _share() {
final urisByMimeType = groupBy<ImageEntry, String>(collection.selection, (e) => e.mimeType).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList()));
AndroidAppService.share(urisByMimeType);
}
// selection action report overlay // selection action report overlay
OverlayEntry _opReportOverlayEntry; OverlayEntry _opReportOverlayEntry;

View file

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class LinkChip extends StatelessWidget { class LinkChip extends StatelessWidget {
final Widget leading;
final String text; final String text;
final String url; final String url;
final Color color; final Color color;
@ -12,6 +13,7 @@ class LinkChip extends StatelessWidget {
const LinkChip({ const LinkChip({
Key key, Key key,
this.leading,
@required this.text, @required this.text,
@required this.url, @required this.url,
this.color, this.color,
@ -20,32 +22,35 @@ class LinkChip extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final effectiveTextStyle = (textStyle ?? DefaultTextStyle.of(context).style).copyWith( return DefaultTextStyle.merge(
color: color, style: (textStyle ?? const TextStyle()).copyWith(color: color),
); child: InkWell(
return InkWell( borderRadius: borderRadius,
borderRadius: borderRadius, onTap: () async {
onTap: () async { if (await canLaunch(url)) {
if (await canLaunch(url)) { await launch(url);
await launch(url); }
} },
}, child: Padding(
child: Padding( padding: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(8.0), child: Row(
child: Row( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ if (leading != null) ...[
Text( leading,
text, const SizedBox(width: 8),
style: effectiveTextStyle, ],
), Text(text),
const SizedBox(width: 8), const SizedBox(width: 8),
Icon( Builder(
AIcons.openInNew, builder: (context) => Icon(
size: Theme.of(context).textTheme.bodyText2.fontSize, AIcons.openInNew,
color: color, size: DefaultTextStyle.of(context).style.fontSize,
) color: color,
], ),
),
],
),
), ),
), ),
); );

View file

@ -96,7 +96,7 @@ class _HomePageState extends State<HomePage> {
future: _appSetup, future: _appSetup,
builder: (context, AsyncSnapshot<void> snapshot) { builder: (context, AsyncSnapshot<void> snapshot) {
if (snapshot.hasError) return const Icon(AIcons.error); if (snapshot.hasError) return const Icon(AIcons.error);
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); if (snapshot.connectionState != ConnectionState.done) return const Scaffold();
if (AvesApp.mode == AppMode.view) { if (AvesApp.mode == AppMode.view) {
return SingleFullscreenPage(entry: _viewerEntry); return SingleFullscreenPage(entry: _viewerEntry);
} }

View file

@ -260,6 +260,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.1" version: "0.1.1"
package_info:
dependency: "direct main"
description:
name: package_info
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.1"
palette_generator: palette_generator:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -11,7 +11,7 @@ description: A new Flutter application.
# 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.0.8+9 version: 1.0.9+10
# 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)
@ -57,6 +57,7 @@ dependencies:
google_maps_flutter: google_maps_flutter:
intl: intl:
outline_material_icons: outline_material_icons:
package_info:
palette_generator: palette_generator:
pdf: pdf:
pedantic: pedantic:

View file

@ -0,0 +1,10 @@
#!/bin/bash
FILE_PATH="../lib/flutter_version.dart"
rm "$FILE_PATH"
echo "Updating flutter_version.dart:"
{
echo "const Map<String, String> version = "
flutter --version --machine
echo ";"
} >> "$FILE_PATH"
cat "$FILE_PATH"