Merge branch 'develop'
This commit is contained in:
commit
60a778f50e
68 changed files with 1030 additions and 257 deletions
|
@ -103,6 +103,7 @@ dependencies {
|
|||
// enable support for Java 8 language APIs (stream, optional, etc.)
|
||||
// coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.9'
|
||||
|
||||
implementation 'androidx.core:core:1.5.0-alpha02' // v1.5.0-alpha02 for ShortcutManagerCompat.setDynamicShortcuts
|
||||
implementation "androidx.exifinterface:exifinterface:1.2.0"
|
||||
implementation 'com.commonsware.cwac:document:0.4.1'
|
||||
implementation 'com.drewnoakes:metadata-extractor:2.14.0'
|
||||
|
|
|
@ -45,19 +45,21 @@
|
|||
|
||||
<application
|
||||
android:name="io.flutter.app.FlutterApplication"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:label="@string/app_name"
|
||||
android:requestLegacyExternalStorage="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/AppTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/AppTheme"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<meta-data
|
||||
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
|
||||
android:value="false" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
@ -65,14 +67,17 @@
|
|||
<intent-filter tools:ignore="AppLinkUrlError">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
<data android:mimeType="vnd.android.cursor.dir/image" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.GET_CONTENT" />
|
||||
|
||||
<category android:name="android.intent.category.OPENABLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
<data android:mimeType="vnd.android.cursor.dir/image" />
|
||||
|
@ -80,6 +85,7 @@
|
|||
<intent-filter>
|
||||
<action android:name="android.intent.action.PICK" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
<data android:mimeType="vnd.android.cursor.dir/image" />
|
||||
|
|
|
@ -2,15 +2,24 @@ package deckers.thibault.aves;
|
|||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.content.pm.ShortcutInfoCompat;
|
||||
import androidx.core.content.pm.ShortcutManagerCompat;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.loup.streams_channel.StreamsChannel;
|
||||
import deckers.thibault.aves.channel.calls.AppAdapterHandler;
|
||||
import deckers.thibault.aves.channel.calls.AppShortcutHandler;
|
||||
import deckers.thibault.aves.channel.calls.ImageFileHandler;
|
||||
import deckers.thibault.aves.channel.calls.MetadataHandler;
|
||||
import deckers.thibault.aves.channel.calls.StorageHandler;
|
||||
|
@ -29,7 +38,7 @@ public class MainActivity extends FlutterActivity {
|
|||
|
||||
public static final String VIEWER_CHANNEL = "deckers.thibault/aves/viewer";
|
||||
|
||||
private Map<String, String> intentDataMap;
|
||||
private Map<String, Object> intentDataMap;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
@ -40,6 +49,7 @@ public class MainActivity extends FlutterActivity {
|
|||
BinaryMessenger messenger = Objects.requireNonNull(getFlutterEngine()).getDartExecutor().getBinaryMessenger();
|
||||
|
||||
new MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(new AppAdapterHandler(this));
|
||||
new MethodChannel(messenger, AppShortcutHandler.CHANNEL).setMethodCallHandler(new AppShortcutHandler(this));
|
||||
new MethodChannel(messenger, ImageFileHandler.CHANNEL).setMethodCallHandler(new ImageFileHandler(this));
|
||||
new MethodChannel(messenger, MetadataHandler.CHANNEL).setMethodCallHandler(new MetadataHandler(this));
|
||||
new MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(new StorageHandler(this));
|
||||
|
@ -69,6 +79,32 @@ public class MainActivity extends FlutterActivity {
|
|||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
setupShortcuts();
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
||||
private void setupShortcuts() {
|
||||
// do not use 'route' as extra key, as the Flutter framework acts on it
|
||||
|
||||
ShortcutInfoCompat search = new ShortcutInfoCompat.Builder(this, "search")
|
||||
.setShortLabel(getString(R.string.search_shortcut_short_label))
|
||||
.setIcon(IconCompat.createWithResource(this, R.mipmap.ic_shortcut_search))
|
||||
.setIntent(new Intent(Intent.ACTION_MAIN, null, this, MainActivity.class)
|
||||
.putExtra("page", "/search"))
|
||||
.build();
|
||||
|
||||
ShortcutInfoCompat videos = new ShortcutInfoCompat.Builder(this, "videos")
|
||||
.setShortLabel(getString(R.string.videos_shortcut_short_label))
|
||||
.setIcon(IconCompat.createWithResource(this, R.mipmap.ic_shortcut_movie))
|
||||
.setIntent(new Intent(Intent.ACTION_MAIN, null, this, MainActivity.class)
|
||||
.putExtra("page", "/collection")
|
||||
.putExtra("filters", new String[]{"{\"type\":\"mime\",\"mime\":\"video/*\"}"}))
|
||||
.build();
|
||||
|
||||
ShortcutManagerCompat.setDynamicShortcuts(this, Arrays.asList(videos, search));
|
||||
}
|
||||
|
||||
private void handleIntent(Intent intent) {
|
||||
|
@ -77,6 +113,15 @@ public class MainActivity extends FlutterActivity {
|
|||
String action = intent.getAction();
|
||||
if (action == null) return;
|
||||
switch (action) {
|
||||
case Intent.ACTION_MAIN:
|
||||
String page = intent.getStringExtra("page");
|
||||
if (page != null) {
|
||||
intentDataMap = new HashMap<>();
|
||||
intentDataMap.put("page", page);
|
||||
String[] filters = intent.getStringArrayExtra("filters");
|
||||
intentDataMap.put("filters", filters != null ? new ArrayList<>(Arrays.asList(filters)) : null);
|
||||
}
|
||||
break;
|
||||
case Intent.ACTION_VIEW:
|
||||
Uri uri = intent.getData();
|
||||
String mimeType = intent.getType();
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
package deckers.thibault.aves.channel.calls;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.pm.ShortcutInfoCompat;
|
||||
import androidx.core.content.pm.ShortcutManagerCompat;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import deckers.thibault.aves.MainActivity;
|
||||
import deckers.thibault.aves.R;
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
public class AppShortcutHandler implements MethodChannel.MethodCallHandler {
|
||||
public static final String CHANNEL = "deckers.thibault/aves/shortcut";
|
||||
|
||||
private Context context;
|
||||
|
||||
public AppShortcutHandler(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||
switch (call.method) {
|
||||
case "canPin": {
|
||||
result.success(ShortcutManagerCompat.isRequestPinShortcutSupported(context));
|
||||
break;
|
||||
}
|
||||
case "pin": {
|
||||
String label = call.argument("label");
|
||||
List<String> filters = call.argument("filters");
|
||||
pin(label, filters);
|
||||
result.success(null);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
result.notImplemented();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void pin(String label, @Nullable List<String> filters) {
|
||||
if (!ShortcutManagerCompat.isRequestPinShortcutSupported(context) || filters == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(context, "collection-" + TextUtils.join("-", filters))
|
||||
.setShortLabel(label)
|
||||
.setIcon(IconCompat.createWithResource(context, R.mipmap.ic_shortcut_collection))
|
||||
.setIntent(new Intent(Intent.ACTION_MAIN, null, context, MainActivity.class)
|
||||
.putExtra("page", "/collection")
|
||||
.putExtra("filters", filters.toArray(new String[0])))
|
||||
.build();
|
||||
|
||||
ShortcutManagerCompat.requestPinShortcut(context, shortcut, null);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108"
|
||||
android:tint="#455A64">
|
||||
<group android:scaleX="1.7226"
|
||||
android:scaleY="1.7226"
|
||||
android:translateX="33.3288"
|
||||
android:translateY="33.3288">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,4v12L8,16L8,4h12m0,-2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM11.5,11.67l1.69,2.26 2.48,-3.1L19,15L9,15zM2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6L2,6z"/>
|
||||
</group>
|
||||
</vector>
|
|
@ -0,0 +1,15 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108"
|
||||
android:tint="#455A64">
|
||||
<group android:scaleX="1.7226"
|
||||
android:scaleY="1.7226"
|
||||
android:translateX="33.3288"
|
||||
android:translateY="33.3288">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M4,6.47L5.76,10H20v8H4V6.47M22,4h-4l2,4h-3l-2,-4h-2l2,4h-3l-2,-4H8l2,4H7L5,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V4z"/>
|
||||
</group>
|
||||
</vector>
|
|
@ -0,0 +1,15 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108"
|
||||
android:tint="#455A64">
|
||||
<group android:scaleX="1.7226"
|
||||
android:scaleY="1.7226"
|
||||
android:translateX="33.3288"
|
||||
android:translateY="33.3288">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
|
||||
</group>
|
||||
</vector>
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_shortcut_background"/>
|
||||
<foreground android:drawable="@drawable/ic_shortcut_collection_foreground"/>
|
||||
</adaptive-icon>
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_shortcut_background"/>
|
||||
<foreground android:drawable="@drawable/ic_shortcut_movie_foreground"/>
|
||||
</adaptive-icon>
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_shortcut_background"/>
|
||||
<foreground android:drawable="@drawable/ic_shortcut_search_foreground"/>
|
||||
</adaptive-icon>
|
|
@ -1,4 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
<color name="ic_shortcut_background">#FFFFFF</color>
|
||||
</resources>
|
|
@ -1,4 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Aves</string>
|
||||
<string name="search_shortcut_short_label">Search</string>
|
||||
<string name="videos_shortcut_short_label">Videos</string>
|
||||
</resources>
|
|
@ -18,6 +18,19 @@ class AlbumFilter extends CollectionFilter {
|
|||
|
||||
const AlbumFilter(this.album, this.uniqueName);
|
||||
|
||||
AlbumFilter.fromJson(Map<String, dynamic> json)
|
||||
: this(
|
||||
json['album'],
|
||||
json['uniqueName'],
|
||||
);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => {
|
||||
'type': type,
|
||||
'album': album,
|
||||
'uniqueName': uniqueName,
|
||||
};
|
||||
|
||||
@override
|
||||
bool filter(ImageEntry entry) => entry.directory == album;
|
||||
|
||||
|
|
|
@ -7,6 +7,11 @@ import 'package:flutter/widgets.dart';
|
|||
class FavouriteFilter extends CollectionFilter {
|
||||
static const type = 'favourite';
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => {
|
||||
'type': type,
|
||||
};
|
||||
|
||||
@override
|
||||
bool filter(ImageEntry entry) => entry.isFavourite;
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/favourite.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
|
@ -20,8 +22,31 @@ abstract class CollectionFilter implements Comparable<CollectionFilter> {
|
|||
TagFilter.type,
|
||||
];
|
||||
|
||||
static CollectionFilter fromJson(String jsonString) {
|
||||
final jsonMap = jsonDecode(jsonString);
|
||||
final type = jsonMap['type'];
|
||||
switch (type) {
|
||||
case AlbumFilter.type:
|
||||
return AlbumFilter.fromJson(jsonMap);
|
||||
case FavouriteFilter.type:
|
||||
return FavouriteFilter();
|
||||
case LocationFilter.type:
|
||||
return LocationFilter.fromJson(jsonMap);
|
||||
case MimeFilter.type:
|
||||
return MimeFilter.fromJson(jsonMap);
|
||||
case QueryFilter.type:
|
||||
return QueryFilter.fromJson(jsonMap);
|
||||
case TagFilter.type:
|
||||
return TagFilter.fromJson(jsonMap);
|
||||
}
|
||||
debugPrint('failed to parse filter from json=$jsonString');
|
||||
return null;
|
||||
}
|
||||
|
||||
const CollectionFilter();
|
||||
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
bool filter(ImageEntry entry);
|
||||
|
||||
bool get isUnique => true;
|
||||
|
|
|
@ -5,17 +5,31 @@ import 'package:flutter/widgets.dart';
|
|||
|
||||
class LocationFilter extends CollectionFilter {
|
||||
static const type = 'country';
|
||||
static const locationSeparator = ';';
|
||||
|
||||
final LocationLevel level;
|
||||
String _location;
|
||||
String _countryCode;
|
||||
|
||||
LocationFilter(this.level, this._location) {
|
||||
final split = _location.split(';');
|
||||
final split = _location.split(locationSeparator);
|
||||
if (split.isNotEmpty) _location = split[0];
|
||||
if (split.length > 1) _countryCode = split[1];
|
||||
}
|
||||
|
||||
LocationFilter.fromJson(Map<String, dynamic> json)
|
||||
: this(
|
||||
LocationLevel.values.firstWhere((v) => v.toString() == json['level'], orElse: () => null),
|
||||
json['location'],
|
||||
);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => {
|
||||
'type': type,
|
||||
'level': level.toString(),
|
||||
'location': _countryCode != null ? '$_location$locationSeparator$_countryCode' : _location,
|
||||
};
|
||||
|
||||
@override
|
||||
bool filter(ImageEntry entry) => entry.isLocated && ((level == LocationLevel.country && entry.addressDetails.countryName == _location) || (level == LocationLevel.place && entry.addressDetails.place == _location));
|
||||
|
||||
|
|
|
@ -38,6 +38,17 @@ class MimeFilter extends CollectionFilter {
|
|||
_icon ??= AIcons.vector;
|
||||
}
|
||||
|
||||
MimeFilter.fromJson(Map<String, dynamic> json)
|
||||
: this(
|
||||
json['mime'],
|
||||
);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => {
|
||||
'type': type,
|
||||
'mime': mime,
|
||||
};
|
||||
|
||||
static String displayType(String mime) {
|
||||
return mime.toUpperCase().replaceFirst(RegExp('.*/(X-)?'), '').replaceFirst('+XML', '').replaceFirst('VND.', '');
|
||||
}
|
||||
|
|
|
@ -32,6 +32,17 @@ class QueryFilter extends CollectionFilter {
|
|||
_filter = not ? (entry) => !entry.search(upQuery) : (entry) => entry.search(upQuery);
|
||||
}
|
||||
|
||||
QueryFilter.fromJson(Map<String, dynamic> json)
|
||||
: this(
|
||||
json['query'],
|
||||
);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => {
|
||||
'type': type,
|
||||
'query': query,
|
||||
};
|
||||
|
||||
@override
|
||||
bool filter(ImageEntry entry) => _filter(entry);
|
||||
|
||||
|
|
|
@ -10,6 +10,17 @@ class TagFilter extends CollectionFilter {
|
|||
|
||||
const TagFilter(this.tag);
|
||||
|
||||
TagFilter.fromJson(Map<String, dynamic> json)
|
||||
: this(
|
||||
json['tag'],
|
||||
);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => {
|
||||
'type': type,
|
||||
'tag': tag,
|
||||
};
|
||||
|
||||
@override
|
||||
bool filter(ImageEntry entry) => entry.xmpSubjects.contains(tag);
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:aves/widgets/album/collection_page.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
|
||||
enum HomePageSetting { collection, albums }
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/image_metadata.dart';
|
||||
import 'package:aves/model/metadata_db.dart';
|
||||
|
@ -76,7 +77,7 @@ mixin LocationMixin on SourceBase {
|
|||
void updateLocations() {
|
||||
final locations = rawEntries.where((entry) => entry.isLocated).map((entry) => entry.addressDetails).toList();
|
||||
List<String> lister(String Function(AddressDetails a) f) => List<String>.unmodifiable(locations.map(f).where((s) => s != null && s.isNotEmpty).toSet().toList()..sort(compareAsciiUpperCase));
|
||||
sortedCountries = lister((address) => '${address.countryName};${address.countryCode}');
|
||||
sortedCountries = lister((address) => '${address.countryName}${LocationFilter.locationSeparator}${address.countryCode}');
|
||||
sortedPlaces = lister((address) => address.place);
|
||||
|
||||
invalidateFilterEntryCounts();
|
||||
|
@ -86,7 +87,7 @@ mixin LocationMixin on SourceBase {
|
|||
Map<String, ImageEntry> getCountryEntries() {
|
||||
final locatedEntries = sortedEntriesForFilterList.where((entry) => entry.isLocated);
|
||||
return Map.fromEntries(sortedCountries.map((countryNameAndCode) {
|
||||
final split = countryNameAndCode.split(';');
|
||||
final split = countryNameAndCode.split(LocationFilter.locationSeparator);
|
||||
ImageEntry entry;
|
||||
if (split.length > 1) {
|
||||
final countryCode = split[1];
|
||||
|
|
37
lib/services/app_shortcut_service.dart
Normal file
37
lib/services/app_shortcut_service.dart
Normal file
|
@ -0,0 +1,37 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class AppShortcutService {
|
||||
static const platform = MethodChannel('deckers.thibault/aves/shortcut');
|
||||
|
||||
// this ability will not change over the lifetime of the app
|
||||
static bool _canPin;
|
||||
|
||||
static Future<bool> canPin() async {
|
||||
if (_canPin != null) {
|
||||
return SynchronousFuture(_canPin);
|
||||
}
|
||||
|
||||
try {
|
||||
_canPin = await platform.invokeMethod('canPin');
|
||||
return _canPin;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('canPin failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static Future<void> pin(String label, Set<CollectionFilter> filters) async {
|
||||
try {
|
||||
await platform.invokeMethod('pin', <String, dynamic>{
|
||||
'label': label,
|
||||
'filters': filters.map((filter) => jsonEncode(filter.toJson())).toList(),
|
||||
});
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('pin failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,17 +3,22 @@ import 'dart:async';
|
|||
import 'package:aves/main.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/services/app_shortcut_service.dart';
|
||||
import 'package:aves/utils/durations.dart';
|
||||
import 'package:aves/widgets/album/filter_bar.dart';
|
||||
import 'package:aves/widgets/album/search/search_delegate.dart';
|
||||
import 'package:aves/widgets/collection/collection_actions.dart';
|
||||
import 'package:aves/widgets/collection/filter_bar.dart';
|
||||
import 'package:aves/widgets/collection/search/search_delegate.dart';
|
||||
import 'package:aves/widgets/common/action_delegates/selection_action_delegate.dart';
|
||||
import 'package:aves/widgets/common/app_bar_subtitle.dart';
|
||||
import 'package:aves/widgets/common/app_bar_title.dart';
|
||||
import 'package:aves/widgets/common/aves_selection_dialog.dart';
|
||||
import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart';
|
||||
import 'package:aves/widgets/common/entry_actions.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:aves/widgets/common/menu_row.dart';
|
||||
import 'package:aves/widgets/filter_grids/search_button.dart';
|
||||
import 'package:aves/widgets/stats/stats.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -39,9 +44,12 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
final TextEditingController _searchFieldController = TextEditingController();
|
||||
SelectionActionDelegate _actionDelegate;
|
||||
AnimationController _browseToSelectAnimation;
|
||||
Future<bool> _canAddShortcutsLoader;
|
||||
|
||||
CollectionLens get collection => widget.collection;
|
||||
|
||||
CollectionSource get source => collection.source;
|
||||
|
||||
bool get hasFilters => collection.filters.isNotEmpty;
|
||||
|
||||
@override
|
||||
|
@ -54,6 +62,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
duration: Durations.iconAnimation,
|
||||
vsync: this,
|
||||
);
|
||||
_canAddShortcutsLoader = AppShortcutService.canPin();
|
||||
_registerWidget(widget);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _updateHeight());
|
||||
}
|
||||
|
@ -91,7 +100,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
return AnimatedBuilder(
|
||||
animation: collection.filterChangeNotifier,
|
||||
builder: (context, child) => SliverAppBar(
|
||||
titleSpacing: 0,
|
||||
leading: _buildAppBarLeading(),
|
||||
title: _buildAppBarTitle(),
|
||||
actions: _buildActions(),
|
||||
|
@ -101,6 +109,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
onPressed: collection.removeFilter,
|
||||
)
|
||||
: null,
|
||||
titleSpacing: 0,
|
||||
floating: true,
|
||||
),
|
||||
);
|
||||
|
@ -135,26 +144,18 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
Widget _buildAppBarTitle() {
|
||||
if (collection.isBrowsing) {
|
||||
Widget title = Text(
|
||||
AvesApp.mode == AppMode.pick ? 'Select' : 'Collection',
|
||||
AvesApp.mode == AppMode.pick ? 'Pick' : 'Collection',
|
||||
key: Key('appbar-title'),
|
||||
);
|
||||
if (AvesApp.mode == AppMode.main) {
|
||||
title = SourceStateAwareAppBarTitle(
|
||||
title: title,
|
||||
source: collection.source,
|
||||
source: source,
|
||||
);
|
||||
}
|
||||
return GestureDetector(
|
||||
return TappableAppBarTitle(
|
||||
onTap: _goToSearch,
|
||||
// use a `Container` with a dummy color to make it expand
|
||||
// so that we can also detect taps around the title `Text`
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
padding: EdgeInsets.symmetric(horizontal: NavigationToolbar.kMiddleSpacing),
|
||||
color: Colors.transparent,
|
||||
height: kToolbarHeight,
|
||||
child: title,
|
||||
),
|
||||
);
|
||||
} else if (collection.isSelecting) {
|
||||
return AnimatedBuilder(
|
||||
|
@ -171,10 +172,9 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
List<Widget> _buildActions() {
|
||||
return [
|
||||
if (collection.isBrowsing)
|
||||
IconButton(
|
||||
key: Key('search-button'),
|
||||
icon: Icon(AIcons.search),
|
||||
onPressed: _goToSearch,
|
||||
SearchButton(
|
||||
source,
|
||||
parentCollection: collection,
|
||||
),
|
||||
if (collection.isSelecting)
|
||||
...EntryActions.selection.map((action) => AnimatedBuilder(
|
||||
|
@ -187,8 +187,11 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
);
|
||||
},
|
||||
)),
|
||||
Builder(
|
||||
builder: (context) => PopupMenuButton<CollectionAction>(
|
||||
FutureBuilder<bool>(
|
||||
future: _canAddShortcutsLoader,
|
||||
builder: (context, snapshot) {
|
||||
final canAddShortcuts = snapshot.data ?? false;
|
||||
return PopupMenuButton<CollectionAction>(
|
||||
key: Key('appbar-menu-button'),
|
||||
itemBuilder: (context) {
|
||||
final hasSelection = collection.selection.isNotEmpty;
|
||||
|
@ -219,6 +222,11 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
value: CollectionAction.stats,
|
||||
child: MenuRow(text: 'Stats', icon: AIcons.stats),
|
||||
),
|
||||
if (canAddShortcuts)
|
||||
PopupMenuItem(
|
||||
value: CollectionAction.addShortcut,
|
||||
child: MenuRow(text: 'Add shortcut', icon: AIcons.addShortcut),
|
||||
),
|
||||
],
|
||||
if (collection.isSelecting) ...[
|
||||
PopupMenuDivider(),
|
||||
|
@ -251,7 +259,8 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
];
|
||||
},
|
||||
onSelected: _onCollectionActionSelected,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
@ -279,10 +288,9 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
_actionDelegate.onCollectionActionSelected(context, action);
|
||||
break;
|
||||
case CollectionAction.refresh:
|
||||
final source = collection.source;
|
||||
if (source is MediaStoreSource) {
|
||||
source.clearEntries();
|
||||
unawaited(source.refresh());
|
||||
unawaited((source as MediaStoreSource).refresh());
|
||||
}
|
||||
break;
|
||||
case CollectionAction.select:
|
||||
|
@ -295,7 +303,10 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
collection.clearSelection();
|
||||
break;
|
||||
case CollectionAction.stats:
|
||||
unawaited(_goToStats());
|
||||
_goToStats();
|
||||
break;
|
||||
case CollectionAction.addShortcut:
|
||||
unawaited(AppShortcutService.pin('Collection', collection.filters));
|
||||
break;
|
||||
case CollectionAction.group:
|
||||
final value = await showDialog<EntryGroupFactor>(
|
||||
|
@ -338,14 +349,18 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
}
|
||||
|
||||
void _goToSearch() {
|
||||
showSearch(
|
||||
context: context,
|
||||
delegate: ImageSearchDelegate(collection.source, collection.addFilter),
|
||||
);
|
||||
Navigator.push(
|
||||
context,
|
||||
SearchPageRoute(
|
||||
delegate: ImageSearchDelegate(
|
||||
source: collection.source,
|
||||
parentCollection: collection,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> _goToStats() {
|
||||
return Navigator.push(
|
||||
void _goToStats() {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: RouteSettings(name: StatsPage.routeName),
|
||||
|
@ -356,16 +371,3 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum CollectionAction {
|
||||
copy,
|
||||
group,
|
||||
move,
|
||||
refresh,
|
||||
refreshMetadata,
|
||||
select,
|
||||
selectAll,
|
||||
selectNone,
|
||||
sort,
|
||||
stats,
|
||||
}
|
13
lib/widgets/collection/collection_actions.dart
Normal file
13
lib/widgets/collection/collection_actions.dart
Normal file
|
@ -0,0 +1,13 @@
|
|||
enum CollectionAction {
|
||||
addShortcut,
|
||||
copy,
|
||||
group,
|
||||
move,
|
||||
refresh,
|
||||
refreshMetadata,
|
||||
select,
|
||||
selectAll,
|
||||
selectNone,
|
||||
sort,
|
||||
stats,
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/widgets/album/thumbnail_collection.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail_collection.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/drawer/app_drawer.dart';
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/widgets/album/grid/header_generic.dart';
|
||||
import 'package:aves/widgets/collection/grid/header_generic.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:aves/utils/time_utils.dart';
|
||||
import 'package:aves/widgets/album/grid/header_generic.dart';
|
||||
import 'package:aves/widgets/collection/grid/header_generic.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
|
@ -6,8 +6,8 @@ import 'package:aves/model/source/enums.dart';
|
|||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/utils/durations.dart';
|
||||
import 'package:aves/widgets/album/grid/header_album.dart';
|
||||
import 'package:aves/widgets/album/grid/header_date.dart';
|
||||
import 'package:aves/widgets/collection/grid/header_album.dart';
|
||||
import 'package:aves/widgets/collection/grid/header_date.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
|
@ -1,6 +1,6 @@
|
|||
import 'dart:math' as math;
|
||||
|
||||
import 'package:aves/widgets/album/grid/list_section_layout.dart';
|
||||
import 'package:aves/widgets/collection/grid/list_section_layout.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
|
@ -2,8 +2,8 @@ import 'dart:math';
|
|||
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/widgets/album/grid/header_generic.dart';
|
||||
import 'package:aves/widgets/album/grid/tile_extent_manager.dart';
|
||||
import 'package:aves/widgets/collection/grid/header_generic.dart';
|
||||
import 'package:aves/widgets/collection/grid/tile_extent_manager.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
|
@ -2,9 +2,9 @@ import 'package:aves/main.dart';
|
|||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/services/viewer_service.dart';
|
||||
import 'package:aves/widgets/album/grid/list_known_extent.dart';
|
||||
import 'package:aves/widgets/album/grid/list_section_layout.dart';
|
||||
import 'package:aves/widgets/album/thumbnail/decorated.dart';
|
||||
import 'package:aves/widgets/collection/grid/list_known_extent.dart';
|
||||
import 'package:aves/widgets/collection/grid/list_section_layout.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/decorated.dart';
|
||||
import 'package:aves/widgets/common/routes.dart';
|
||||
import 'package:aves/widgets/fullscreen/fullscreen_page.dart';
|
||||
import 'package:flutter/material.dart';
|
|
@ -3,10 +3,10 @@ import 'dart:ui' as ui;
|
|||
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/utils/durations.dart';
|
||||
import 'package:aves/widgets/album/grid/list_section_layout.dart';
|
||||
import 'package:aves/widgets/album/grid/list_sliver.dart';
|
||||
import 'package:aves/widgets/album/grid/tile_extent_manager.dart';
|
||||
import 'package:aves/widgets/album/thumbnail/decorated.dart';
|
||||
import 'package:aves/widgets/collection/grid/list_section_layout.dart';
|
||||
import 'package:aves/widgets/collection/grid/list_sliver.dart';
|
||||
import 'package:aves/widgets/collection/grid/tile_extent_manager.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/decorated.dart';
|
||||
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
|
@ -6,40 +6,46 @@ import 'package:aves/model/filters/mime.dart';
|
|||
import 'package:aves/model/filters/query.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/mime_types.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/album.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/location.dart';
|
||||
import 'package:aves/model/source/tag.dart';
|
||||
import 'package:aves/widgets/album/search/expandable_filter_row.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/collection/search/expandable_filter_row.dart';
|
||||
import 'package:aves/widgets/collection/search_page.dart';
|
||||
import 'package:aves/widgets/common/aves_filter_chip.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class ImageSearchDelegate extends SearchDelegate<CollectionFilter> {
|
||||
class ImageSearchDelegate {
|
||||
final CollectionSource source;
|
||||
final ValueNotifier<String> expandedSectionNotifier = ValueNotifier(null);
|
||||
final FilterCallback onSelection;
|
||||
final CollectionLens parentCollection;
|
||||
|
||||
ImageSearchDelegate(this.source, this.onSelection);
|
||||
ImageSearchDelegate({@required this.source, this.parentCollection});
|
||||
|
||||
@override
|
||||
ThemeData appBarTheme(BuildContext context) {
|
||||
return Theme.of(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildLeading(BuildContext context) {
|
||||
return IconButton(
|
||||
return Navigator.canPop(context)
|
||||
? IconButton(
|
||||
icon: AnimatedIcon(
|
||||
icon: AnimatedIcons.menu_arrow,
|
||||
progress: transitionAnimation,
|
||||
),
|
||||
onPressed: () => _select(context, null),
|
||||
tooltip: 'Back',
|
||||
onPressed: () => _goBack(context),
|
||||
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||
)
|
||||
: CloseButton(
|
||||
onPressed: SystemNavigator.pop,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Widget> buildActions(BuildContext context) {
|
||||
return [
|
||||
if (query.isNotEmpty)
|
||||
|
@ -54,7 +60,6 @@ class ImageSearchDelegate extends SearchDelegate<CollectionFilter> {
|
|||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildSuggestions(BuildContext context) {
|
||||
final upQuery = query.trim().toUpperCase();
|
||||
bool containQuery(String s) => s.toUpperCase().contains(upQuery);
|
||||
|
@ -137,7 +142,6 @@ class ImageSearchDelegate extends SearchDelegate<CollectionFilter> {
|
|||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildResults(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// `buildResults` is called in the build phase,
|
||||
|
@ -154,14 +158,160 @@ class ImageSearchDelegate extends SearchDelegate<CollectionFilter> {
|
|||
}
|
||||
|
||||
void _select(BuildContext context, CollectionFilter filter) {
|
||||
if (parentCollection != null) {
|
||||
_applyToParentCollectionPage(context, filter);
|
||||
} else {
|
||||
_goToCollectionPage(context, filter);
|
||||
}
|
||||
}
|
||||
|
||||
void _applyToParentCollectionPage(BuildContext context, CollectionFilter filter) {
|
||||
if (filter != null) {
|
||||
onSelection(filter);
|
||||
parentCollection.addFilter(filter);
|
||||
}
|
||||
// we post closing the search page after applying the filter selection
|
||||
// so that hero animation target is ready in the `FilterBar`,
|
||||
// even when the target is a child of an `AnimatedList`
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
close(context, null);
|
||||
_goBack(context);
|
||||
});
|
||||
}
|
||||
|
||||
void _goBack(BuildContext context) {
|
||||
_clean();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
void _goToCollectionPage(BuildContext context, CollectionFilter filter) {
|
||||
_clean();
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: RouteSettings(name: CollectionPage.routeName),
|
||||
builder: (context) => CollectionPage(CollectionLens(
|
||||
source: source,
|
||||
filters: [filter],
|
||||
groupFactor: settings.collectionGroupFactor,
|
||||
sortFactor: settings.collectionSortFactor,
|
||||
)),
|
||||
),
|
||||
settings.navRemoveRoutePredicate(CollectionPage.routeName),
|
||||
);
|
||||
}
|
||||
|
||||
void _clean() {
|
||||
currentBody = null;
|
||||
focusNode?.unfocus();
|
||||
}
|
||||
|
||||
// adapted from `SearchDelegate`
|
||||
|
||||
void showResults(BuildContext context) {
|
||||
focusNode?.unfocus();
|
||||
currentBody = SearchBody.results;
|
||||
}
|
||||
|
||||
void showSuggestions(BuildContext context) {
|
||||
assert(focusNode != null, '_focusNode must be set by route before showSuggestions is called.');
|
||||
focusNode.requestFocus();
|
||||
currentBody = SearchBody.suggestions;
|
||||
}
|
||||
|
||||
Animation<double> get transitionAnimation => proxyAnimation;
|
||||
|
||||
FocusNode focusNode;
|
||||
|
||||
final TextEditingController queryTextController = TextEditingController();
|
||||
|
||||
final ProxyAnimation proxyAnimation = ProxyAnimation(kAlwaysDismissedAnimation);
|
||||
|
||||
String get query => queryTextController.text;
|
||||
|
||||
set query(String value) {
|
||||
assert(query != null);
|
||||
queryTextController.text = value;
|
||||
}
|
||||
|
||||
final ValueNotifier<SearchBody> currentBodyNotifier = ValueNotifier<SearchBody>(null);
|
||||
|
||||
SearchBody get currentBody => currentBodyNotifier.value;
|
||||
|
||||
set currentBody(SearchBody value) {
|
||||
currentBodyNotifier.value = value;
|
||||
}
|
||||
|
||||
SearchPageRoute route;
|
||||
}
|
||||
|
||||
// adapted from `SearchDelegate`
|
||||
enum SearchBody { suggestions, results }
|
||||
|
||||
// adapted from `SearchDelegate`
|
||||
class SearchPageRoute<T> extends PageRoute<T> {
|
||||
SearchPageRoute({
|
||||
@required this.delegate,
|
||||
}) : assert(delegate != null),
|
||||
super(settings: RouteSettings(name: SearchPage.routeName)) {
|
||||
assert(
|
||||
delegate.route == null,
|
||||
'The ${delegate.runtimeType} instance is currently used by another active '
|
||||
'search. Please close that search by calling close() on the SearchDelegate '
|
||||
'before openening another search with the same delegate instance.',
|
||||
);
|
||||
delegate.route = this;
|
||||
}
|
||||
|
||||
final ImageSearchDelegate delegate;
|
||||
|
||||
@override
|
||||
Color get barrierColor => null;
|
||||
|
||||
@override
|
||||
String get barrierLabel => null;
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => const Duration(milliseconds: 300);
|
||||
|
||||
@override
|
||||
bool get maintainState => false;
|
||||
|
||||
@override
|
||||
Widget buildTransitions(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child,
|
||||
) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Animation<double> createAnimation() {
|
||||
final animation = super.createAnimation();
|
||||
delegate.proxyAnimation.parent = animation;
|
||||
return animation;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildPage(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SearchPage(
|
||||
delegate: delegate,
|
||||
animation: animation,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didComplete(T result) {
|
||||
super.didComplete(result);
|
||||
assert(delegate.route == this);
|
||||
delegate.route = null;
|
||||
delegate.currentBody = null;
|
||||
}
|
||||
}
|
127
lib/widgets/collection/search_page.dart
Normal file
127
lib/widgets/collection/search_page.dart
Normal file
|
@ -0,0 +1,127 @@
|
|||
import 'package:aves/widgets/collection/search/search_delegate.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SearchPage extends StatefulWidget {
|
||||
static const routeName = '/search';
|
||||
|
||||
final ImageSearchDelegate delegate;
|
||||
final Animation<double> animation;
|
||||
|
||||
const SearchPage({
|
||||
this.delegate,
|
||||
this.animation,
|
||||
});
|
||||
|
||||
@override
|
||||
_SearchPageState createState() => _SearchPageState();
|
||||
}
|
||||
|
||||
class _SearchPageState extends State<SearchPage> {
|
||||
FocusNode focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.delegate.queryTextController.addListener(_onQueryChanged);
|
||||
widget.animation.addStatusListener(_onAnimationStatusChanged);
|
||||
widget.delegate.currentBodyNotifier.addListener(_onSearchBodyChanged);
|
||||
focusNode.addListener(_onFocusChanged);
|
||||
widget.delegate.focusNode = focusNode;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
widget.delegate.queryTextController.removeListener(_onQueryChanged);
|
||||
widget.animation.removeStatusListener(_onAnimationStatusChanged);
|
||||
widget.delegate.currentBodyNotifier.removeListener(_onSearchBodyChanged);
|
||||
widget.delegate.focusNode = null;
|
||||
focusNode.dispose();
|
||||
}
|
||||
|
||||
void _onAnimationStatusChanged(AnimationStatus status) {
|
||||
if (status != AnimationStatus.completed) {
|
||||
return;
|
||||
}
|
||||
widget.animation.removeStatusListener(_onAnimationStatusChanged);
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(SearchPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.delegate != oldWidget.delegate) {
|
||||
oldWidget.delegate.queryTextController.removeListener(_onQueryChanged);
|
||||
widget.delegate.queryTextController.addListener(_onQueryChanged);
|
||||
oldWidget.delegate.currentBodyNotifier.removeListener(_onSearchBodyChanged);
|
||||
widget.delegate.currentBodyNotifier.addListener(_onSearchBodyChanged);
|
||||
oldWidget.delegate.focusNode = null;
|
||||
widget.delegate.focusNode = focusNode;
|
||||
}
|
||||
}
|
||||
|
||||
void _onFocusChanged() {
|
||||
if (focusNode.hasFocus && widget.delegate.currentBody != SearchBody.suggestions) {
|
||||
widget.delegate.showSuggestions(context);
|
||||
}
|
||||
}
|
||||
|
||||
void _onQueryChanged() {
|
||||
setState(() {
|
||||
// rebuild ourselves because query changed.
|
||||
});
|
||||
}
|
||||
|
||||
void _onSearchBodyChanged() {
|
||||
setState(() {
|
||||
// rebuild ourselves because search body changed.
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = widget.delegate.appBarTheme(context);
|
||||
Widget body;
|
||||
switch (widget.delegate.currentBody) {
|
||||
case SearchBody.suggestions:
|
||||
body = KeyedSubtree(
|
||||
key: ValueKey<SearchBody>(SearchBody.suggestions),
|
||||
child: widget.delegate.buildSuggestions(context),
|
||||
);
|
||||
break;
|
||||
case SearchBody.results:
|
||||
body = KeyedSubtree(
|
||||
key: ValueKey<SearchBody>(SearchBody.results),
|
||||
child: widget.delegate.buildResults(context),
|
||||
);
|
||||
break;
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: theme.primaryColor,
|
||||
iconTheme: theme.primaryIconTheme,
|
||||
textTheme: theme.primaryTextTheme,
|
||||
brightness: theme.primaryColorBrightness,
|
||||
leading: widget.delegate.buildLeading(context),
|
||||
title: TextField(
|
||||
controller: widget.delegate.queryTextController,
|
||||
focusNode: focusNode,
|
||||
style: theme.textTheme.headline6,
|
||||
textInputAction: TextInputAction.search,
|
||||
onSubmitted: (_) => widget.delegate.showResults(context),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: MaterialLocalizations.of(context).searchFieldLabel,
|
||||
hintStyle: theme.inputDecorationTheme.hintStyle,
|
||||
),
|
||||
),
|
||||
actions: widget.delegate.buildActions(context),
|
||||
),
|
||||
body: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: body,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/widgets/album/thumbnail/overlay.dart';
|
||||
import 'package:aves/widgets/album/thumbnail/raster.dart';
|
||||
import 'package:aves/widgets/album/thumbnail/vector.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/overlay.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/raster.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/vector.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DecoratedThumbnail extends StatelessWidget {
|
|
@ -6,12 +6,12 @@ import 'package:aves/model/mime_types.dart';
|
|||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/utils/durations.dart';
|
||||
import 'package:aves/widgets/album/app_bar.dart';
|
||||
import 'package:aves/widgets/album/empty.dart';
|
||||
import 'package:aves/widgets/album/grid/list_section_layout.dart';
|
||||
import 'package:aves/widgets/album/grid/list_sliver.dart';
|
||||
import 'package:aves/widgets/album/grid/scaling.dart';
|
||||
import 'package:aves/widgets/album/grid/tile_extent_manager.dart';
|
||||
import 'package:aves/widgets/collection/app_bar.dart';
|
||||
import 'package:aves/widgets/collection/empty.dart';
|
||||
import 'package:aves/widgets/collection/grid/list_section_layout.dart';
|
||||
import 'package:aves/widgets/collection/grid/list_sliver.dart';
|
||||
import 'package:aves/widgets/collection/grid/scaling.dart';
|
||||
import 'package:aves/widgets/collection/grid/tile_extent_manager.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:aves/widgets/common/scroll_thumb.dart';
|
||||
import 'package:aves/widgets/common/sloppy_scroll_physics.dart';
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/services/android_app_service.dart';
|
||||
|
@ -12,6 +10,7 @@ import 'package:aves/widgets/common/entry_actions.dart';
|
|||
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
|
||||
import 'package:aves/widgets/fullscreen/debug.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:pdf/pdf.dart';
|
||||
import 'package:pdf/widgets.dart' as pdf;
|
||||
|
@ -154,7 +153,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
|||
}
|
||||
} else {
|
||||
// leave viewer
|
||||
exit(0);
|
||||
unawaited(SystemNavigator.pop());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,8 +9,8 @@ import 'package:aves/model/source/collection_source.dart';
|
|||
import 'package:aves/services/android_app_service.dart';
|
||||
import 'package:aves/services/image_file_service.dart';
|
||||
import 'package:aves/utils/durations.dart';
|
||||
import 'package:aves/widgets/album/app_bar.dart';
|
||||
import 'package:aves/widgets/album/empty.dart';
|
||||
import 'package:aves/widgets/collection/collection_actions.dart';
|
||||
import 'package:aves/widgets/collection/empty.dart';
|
||||
import 'package:aves/widgets/common/action_delegates/create_album_dialog.dart';
|
||||
import 'package:aves/widgets/common/action_delegates/feedback.dart';
|
||||
import 'package:aves/widgets/common/action_delegates/permission_aware.dart';
|
||||
|
|
27
lib/widgets/common/app_bar_title.dart
Normal file
27
lib/widgets/common/app_bar_title.dart
Normal file
|
@ -0,0 +1,27 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class TappableAppBarTitle extends StatelessWidget {
|
||||
final GestureTapCallback onTap;
|
||||
final Widget child;
|
||||
|
||||
const TappableAppBarTitle({
|
||||
this.onTap,
|
||||
@required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
// use a `Container` with a dummy color to make it expand
|
||||
// so that we can also detect taps around the title `Text`
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
padding: EdgeInsets.symmetric(horizontal: NavigationToolbar.kMiddleSpacing),
|
||||
color: Colors.transparent,
|
||||
height: kToolbarHeight,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -24,11 +24,18 @@ class AvesDialog extends AlertDialog {
|
|||
// to size itself to the content intrinsic size,
|
||||
// but the `ListView` viewport does not have one
|
||||
width: 1,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: Divider.createBorderSide(context, width: 1),
|
||||
),
|
||||
),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: scrollableContent,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: content,
|
||||
contentPadding: scrollableContent != null ? EdgeInsets.zero : EdgeInsets.fromLTRB(24, 20, 24, 24),
|
||||
|
@ -47,10 +54,14 @@ class DialogTitle extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: Divider.createBorderSide(context, width: 1),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
|
@ -58,9 +69,6 @@ class DialogTitle extends StatelessWidget {
|
|||
fontFamily: 'Concourse Caps',
|
||||
),
|
||||
),
|
||||
),
|
||||
Divider(height: 1),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,7 +96,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
content,
|
||||
widget.details,
|
||||
Flexible(child: widget.details),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,21 @@
|
|||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum EntryAction { delete, edit, info, open, openMap, print, rename, rotateCCW, rotateCW, setAs, share, toggleFavourite, debug }
|
||||
enum EntryAction {
|
||||
delete,
|
||||
edit,
|
||||
info,
|
||||
open,
|
||||
openMap,
|
||||
print,
|
||||
rename,
|
||||
rotateCCW,
|
||||
rotateCW,
|
||||
setAs,
|
||||
share,
|
||||
toggleFavourite,
|
||||
debug,
|
||||
}
|
||||
|
||||
class EntryActions {
|
||||
static const selection = [
|
||||
|
|
|
@ -24,6 +24,7 @@ class AIcons {
|
|||
static const IconData tag = OMIcons.localOffer;
|
||||
|
||||
// actions
|
||||
static const IconData addShortcut = OMIcons.bookmarkBorder;
|
||||
static const IconData clear = OMIcons.clear;
|
||||
static const IconData collapse = OMIcons.expandLess;
|
||||
static const IconData createAlbum = OMIcons.addCircleOutline;
|
||||
|
|
|
@ -208,6 +208,7 @@ class _AppDrawerState extends State<AppDrawer> {
|
|||
Widget get settingsTile => NavTile(
|
||||
icon: AIcons.settings,
|
||||
title: 'Settings',
|
||||
topLevel: false,
|
||||
routeName: SettingsPage.routeName,
|
||||
pageBuilder: (_) => SettingsPage(),
|
||||
);
|
||||
|
@ -215,6 +216,7 @@ class _AppDrawerState extends State<AppDrawer> {
|
|||
Widget get aboutTile => NavTile(
|
||||
icon: AIcons.info,
|
||||
title: 'About',
|
||||
topLevel: false,
|
||||
routeName: AboutPage.routeName,
|
||||
pageBuilder: (_) => AboutPage(),
|
||||
);
|
||||
|
@ -222,6 +224,7 @@ class _AppDrawerState extends State<AppDrawer> {
|
|||
Widget get debugTile => NavTile(
|
||||
icon: AIcons.debug,
|
||||
title: 'Debug',
|
||||
topLevel: false,
|
||||
routeName: DebugPage.routeName,
|
||||
pageBuilder: (_) => DebugPage(source: source),
|
||||
);
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'package:aves/model/filters/filters.dart';
|
|||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/widgets/album/collection_page.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ class NavTile extends StatelessWidget {
|
|||
final IconData icon;
|
||||
final String title;
|
||||
final Widget trailing;
|
||||
final bool topLevel;
|
||||
final String routeName;
|
||||
final WidgetBuilder pageBuilder;
|
||||
|
||||
|
@ -16,6 +17,7 @@ class NavTile extends StatelessWidget {
|
|||
@required this.icon,
|
||||
@required this.title,
|
||||
this.trailing,
|
||||
this.topLevel = true,
|
||||
@required this.routeName,
|
||||
@required this.pageBuilder,
|
||||
});
|
||||
|
@ -42,14 +44,19 @@ class NavTile extends StatelessWidget {
|
|||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
if (routeName != context.currentRouteName) {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
final route = MaterialPageRoute(
|
||||
settings: RouteSettings(name: routeName),
|
||||
builder: pageBuilder,
|
||||
),
|
||||
);
|
||||
if (topLevel) {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
route,
|
||||
settings.navRemoveRoutePredicate(routeName),
|
||||
);
|
||||
} else {
|
||||
Navigator.push(context, route);
|
||||
}
|
||||
}
|
||||
},
|
||||
selected: context.currentRouteName == routeName,
|
||||
|
|
|
@ -6,7 +6,7 @@ import 'package:aves/model/source/collection_source.dart';
|
|||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/utils/durations.dart';
|
||||
import 'package:aves/widgets/album/empty.dart';
|
||||
import 'package:aves/widgets/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';
|
||||
|
@ -36,7 +36,7 @@ class AlbumListPage extends StatelessWidget {
|
|||
return FilterNavigationPage(
|
||||
source: source,
|
||||
title: 'Albums',
|
||||
actions: _buildActions(),
|
||||
actions: _buildActions(context),
|
||||
filterEntries: getAlbumEntries(source),
|
||||
filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)),
|
||||
emptyBuilder: () => EmptyContent(
|
||||
|
@ -51,10 +51,9 @@ class AlbumListPage extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildActions() {
|
||||
List<Widget> _buildActions(BuildContext context) {
|
||||
return [
|
||||
Builder(
|
||||
builder: (context) => PopupMenuButton<ChipAction>(
|
||||
PopupMenuButton<ChipAction>(
|
||||
key: Key('appbar-menu-button'),
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
|
@ -67,7 +66,6 @@ class AlbumListPage extends StatelessWidget {
|
|||
},
|
||||
onSelected: (action) => _onChipActionSelected(context, action),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/location.dart';
|
||||
import 'package:aves/widgets/album/empty.dart';
|
||||
import 'package:aves/widgets/collection/empty.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:aves/widgets/filter_grids/filter_grid_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
@ -5,8 +5,8 @@ import 'package:aves/model/filters/filters.dart';
|
|||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/widgets/album/thumbnail/raster.dart';
|
||||
import 'package:aves/widgets/album/thumbnail/vector.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/raster.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/vector.dart';
|
||||
import 'package:aves/widgets/common/aves_filter_chip.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:aves/widgets/filter_grids/filter_grid_page.dart';
|
||||
|
@ -19,11 +19,12 @@ class DecoratedFilterChip extends StatelessWidget {
|
|||
final FilterCallback onPressed;
|
||||
|
||||
const DecoratedFilterChip({
|
||||
Key key,
|
||||
@required this.source,
|
||||
@required this.filter,
|
||||
@required this.entry,
|
||||
@required this.onPressed,
|
||||
});
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
@ -6,13 +6,16 @@ import 'package:aves/model/settings/settings.dart';
|
|||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/utils/durations.dart';
|
||||
import 'package:aves/widgets/album/collection_page.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/collection/search/search_delegate.dart';
|
||||
import 'package:aves/widgets/common/app_bar_subtitle.dart';
|
||||
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/drawer/app_drawer.dart';
|
||||
import 'package:aves/widgets/filter_grids/decorated_filter_chip.dart';
|
||||
import 'package:aves/widgets/filter_grids/search_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -39,11 +42,18 @@ class FilterNavigationPage extends StatelessWidget {
|
|||
return FilterGridPage(
|
||||
source: source,
|
||||
appBar: SliverAppBar(
|
||||
title: SourceStateAwareAppBarTitle(
|
||||
title: TappableAppBarTitle(
|
||||
onTap: () => _goToSearch(context),
|
||||
child: SourceStateAwareAppBarTitle(
|
||||
title: Text(title),
|
||||
source: source,
|
||||
),
|
||||
actions: actions,
|
||||
),
|
||||
actions: [
|
||||
SearchButton(source),
|
||||
...(actions ?? []),
|
||||
],
|
||||
titleSpacing: 0,
|
||||
floating: true,
|
||||
),
|
||||
filterEntries: filterEntries,
|
||||
|
@ -69,6 +79,16 @@ class FilterNavigationPage extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _goToSearch(BuildContext context) {
|
||||
Navigator.push(
|
||||
context,
|
||||
SearchPageRoute(
|
||||
delegate: ImageSearchDelegate(
|
||||
source: source,
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class FilterGridPage extends StatelessWidget {
|
||||
|
@ -119,6 +139,7 @@ class FilterGridPage extends StatelessWidget {
|
|||
(context, i) {
|
||||
final key = filterKeys[i];
|
||||
final child = DecoratedFilterChip(
|
||||
key: Key(key),
|
||||
source: source,
|
||||
filter: filterBuilder(key),
|
||||
entry: filterEntries[key],
|
||||
|
|
32
lib/widgets/filter_grids/search_button.dart
Normal file
32
lib/widgets/filter_grids/search_button.dart
Normal file
|
@ -0,0 +1,32 @@
|
|||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/widgets/collection/search/search_delegate.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SearchButton extends StatelessWidget {
|
||||
final CollectionSource source;
|
||||
final CollectionLens parentCollection;
|
||||
|
||||
const SearchButton(this.source, {this.parentCollection});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
key: Key('search-button'),
|
||||
icon: Icon(AIcons.search),
|
||||
onPressed: () => _goToSearch(context),
|
||||
);
|
||||
}
|
||||
|
||||
void _goToSearch(BuildContext context) {
|
||||
Navigator.push(
|
||||
context,
|
||||
SearchPageRoute(
|
||||
delegate: ImageSearchDelegate(
|
||||
source: source,
|
||||
parentCollection: parentCollection,
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/tag.dart';
|
||||
import 'package:aves/widgets/album/empty.dart';
|
||||
import 'package:aves/widgets/collection/empty.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:aves/widgets/filter_grids/filter_grid_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
@ -7,7 +7,7 @@ import 'package:aves/model/settings/settings.dart';
|
|||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/utils/change_notifier.dart';
|
||||
import 'package:aves/utils/durations.dart';
|
||||
import 'package:aves/widgets/album/collection_page.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/common/action_delegates/entry_action_delegate.dart';
|
||||
import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart';
|
||||
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
|
||||
|
@ -325,11 +325,12 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
|||
}
|
||||
|
||||
void _onLeave() {
|
||||
if (!Navigator.canPop(context)) {
|
||||
// exit app when trying to pop a fullscreen page that is a viewer for a single entry
|
||||
exit(0);
|
||||
}
|
||||
if (Navigator.canPop(context)) {
|
||||
_showSystemUI();
|
||||
} else {
|
||||
// exit app when trying to pop a fullscreen page that is a viewer for a single entry
|
||||
SystemNavigator.pop();
|
||||
}
|
||||
}
|
||||
|
||||
// system UI
|
||||
|
|
|
@ -86,11 +86,14 @@ class SingleImagePageState extends State<SingleImagePage> with AutomaticKeepAliv
|
|||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
|
||||
return ImageView(
|
||||
return PhotoViewGestureDetectorScope(
|
||||
axis: [Axis.vertical],
|
||||
child: ImageView(
|
||||
entry: widget.entry,
|
||||
onScaleChanged: widget.onScaleChanged,
|
||||
onTap: widget.onTap,
|
||||
videoControllers: widget.videoControllers,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/widgets/album/empty.dart';
|
||||
import 'package:aves/widgets/collection/empty.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart';
|
||||
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
|
||||
|
|
|
@ -80,7 +80,7 @@ class _LocationSectionState extends State<LocationSection> {
|
|||
final address = entry.addressDetails;
|
||||
location = address.addressLine;
|
||||
final country = address.countryName;
|
||||
if (country != null && country.isNotEmpty) filters.add(LocationFilter(LocationLevel.country, '$country;${address.countryCode}'));
|
||||
if (country != null && country.isNotEmpty) filters.add(LocationFilter(LocationLevel.country, '$country${LocationFilter.locationSeparator}${address.countryCode}'));
|
||||
final place = address.place;
|
||||
if (place != null && place.isNotEmpty) filters.add(LocationFilter(LocationLevel.place, place));
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:aves/main.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/settings/home_page.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
|
@ -6,7 +7,9 @@ import 'package:aves/model/source/collection_lens.dart';
|
|||
import 'package:aves/services/image_file_service.dart';
|
||||
import 'package:aves/services/viewer_service.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/widgets/album/collection_page.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/collection/search/search_delegate.dart';
|
||||
import 'package:aves/widgets/collection/search_page.dart';
|
||||
import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart';
|
||||
import 'package:aves/widgets/common/routes.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
|
@ -29,6 +32,10 @@ class HomePage extends StatefulWidget {
|
|||
class _HomePageState extends State<HomePage> {
|
||||
MediaStoreSource _mediaStore;
|
||||
ImageEntry _viewerEntry;
|
||||
String _shortcutRouteName;
|
||||
List<String> _shortcutFilters;
|
||||
|
||||
static const allowedShortcutRoutes = [CollectionPage.routeName, AlbumListPage.routeName, SearchPage.routeName];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -77,6 +84,14 @@ class _HomePageState extends State<HomePage> {
|
|||
String pickMimeTypes = intentData['mimeType'];
|
||||
debugPrint('pick mimeType=$pickMimeTypes');
|
||||
break;
|
||||
default:
|
||||
// do not use 'route' as extra key, as the Flutter framework acts on it
|
||||
final extraRoute = intentData['page'];
|
||||
if (allowedShortcutRoutes.contains(extraRoute)) {
|
||||
_shortcutRouteName = extraRoute;
|
||||
}
|
||||
final extraFilters = intentData['filters'];
|
||||
_shortcutFilters = extraFilters != null ? (extraFilters as List).cast<String>() : null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,27 +115,39 @@ class _HomePageState extends State<HomePage> {
|
|||
}
|
||||
|
||||
Route _getRedirectRoute() {
|
||||
switch (AvesApp.mode) {
|
||||
case AppMode.view:
|
||||
if (AvesApp.mode == AppMode.view) {
|
||||
return DirectMaterialPageRoute(
|
||||
settings: RouteSettings(name: SingleFullscreenPage.routeName),
|
||||
builder: (_) => SingleFullscreenPage(entry: _viewerEntry),
|
||||
);
|
||||
case AppMode.main:
|
||||
case AppMode.pick:
|
||||
if (_mediaStore != null) {
|
||||
switch (settings.homePage) {
|
||||
case HomePageSetting.albums:
|
||||
}
|
||||
|
||||
String routeName;
|
||||
Iterable<CollectionFilter> filters;
|
||||
if (AvesApp.mode == AppMode.pick) {
|
||||
routeName = CollectionPage.routeName;
|
||||
} else {
|
||||
routeName = _shortcutRouteName ?? settings.homePage.routeName;
|
||||
filters = (_shortcutFilters ?? []).map(CollectionFilter.fromJson);
|
||||
}
|
||||
switch (routeName) {
|
||||
case AlbumListPage.routeName:
|
||||
return DirectMaterialPageRoute(
|
||||
settings: RouteSettings(name: AlbumListPage.routeName),
|
||||
builder: (_) => AlbumListPage(source: _mediaStore),
|
||||
);
|
||||
case HomePageSetting.collection:
|
||||
case SearchPage.routeName:
|
||||
return SearchPageRoute(
|
||||
delegate: ImageSearchDelegate(source: _mediaStore),
|
||||
);
|
||||
case CollectionPage.routeName:
|
||||
default:
|
||||
return DirectMaterialPageRoute(
|
||||
settings: RouteSettings(name: CollectionPage.routeName),
|
||||
builder: (_) => CollectionPage(
|
||||
CollectionLens(
|
||||
source: _mediaStore,
|
||||
filters: filters,
|
||||
groupFactor: settings.collectionGroupFactor,
|
||||
sortFactor: settings.collectionSortFactor,
|
||||
),
|
||||
|
@ -128,7 +155,4 @@ class _HomePageState extends State<HomePage> {
|
|||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'package:aves/model/filters/filters.dart';
|
|||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/utils/color_utils.dart';
|
||||
import 'package:aves/widgets/album/collection_page.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/common/aves_filter_chip.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
@ -9,8 +9,8 @@ import 'package:aves/model/settings/settings.dart';
|
|||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/utils/color_utils.dart';
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/album/collection_page.dart';
|
||||
import 'package:aves/widgets/album/empty.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/collection/empty.dart';
|
||||
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
import 'package:aves/widgets/stats/filter_table.dart';
|
||||
|
@ -36,7 +36,7 @@ class StatsPage extends StatelessWidget {
|
|||
final address = entry.addressDetails;
|
||||
var country = address.countryName;
|
||||
if (country != null && country.isNotEmpty) {
|
||||
country += ';${address.countryCode}';
|
||||
country += '${LocationFilter.locationSeparator}${address.countryCode}';
|
||||
entryCountPerCountry[country] = (entryCountPerCountry[country] ?? 0) + 1;
|
||||
}
|
||||
final place = address.place;
|
||||
|
|
|
@ -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.9+21
|
||||
version: 1.1.10+22
|
||||
|
||||
# video_player (as of v0.10.8+2, backed by ExoPlayer):
|
||||
# - does not support content URIs (by default, but trivial by fork)
|
||||
|
|
35
test/model/filters_test.dart
Normal file
35
test/model/filters_test.dart
Normal file
|
@ -0,0 +1,35 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/favourite.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/mime.dart';
|
||||
import 'package:aves/model/filters/query.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/mime_types.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
test('Filter serialization', () {
|
||||
CollectionFilter jsonRoundTrip(filter) => CollectionFilter.fromJson(jsonEncode(filter.toJson()));
|
||||
|
||||
final album = AlbumFilter('path/to/album', 'album');
|
||||
expect(album, jsonRoundTrip(album));
|
||||
|
||||
final fav = FavouriteFilter();
|
||||
expect(fav, jsonRoundTrip(fav));
|
||||
|
||||
final location = LocationFilter(LocationLevel.country, 'France${LocationFilter.locationSeparator}FR');
|
||||
expect(location, jsonRoundTrip(location));
|
||||
|
||||
final mime = MimeFilter(MimeTypes.anyVideo);
|
||||
expect(mime, jsonRoundTrip(mime));
|
||||
|
||||
final query = QueryFilter('some query');
|
||||
expect(query, jsonRoundTrip(query));
|
||||
|
||||
final tag = TagFilter('some tag');
|
||||
expect(tag, jsonRoundTrip(tag));
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue