cities -> places
This commit is contained in:
parent
fd5bb222d7
commit
48133d0bb8
9 changed files with 249 additions and 41 deletions
|
@ -0,0 +1,208 @@
|
||||||
|
package deckers.thibault.aves.channelhandlers;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.content.pm.ResolveInfo;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.core.content.FileProvider;
|
||||||
|
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
import com.bumptech.glide.load.Key;
|
||||||
|
import com.bumptech.glide.request.FutureTarget;
|
||||||
|
import com.bumptech.glide.request.RequestOptions;
|
||||||
|
import com.bumptech.glide.signature.ObjectKey;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import io.flutter.plugin.common.MethodCall;
|
||||||
|
import io.flutter.plugin.common.MethodChannel;
|
||||||
|
|
||||||
|
import static com.bumptech.glide.request.RequestOptions.centerCropTransform;
|
||||||
|
|
||||||
|
public class AppAdapterHandler implements MethodChannel.MethodCallHandler {
|
||||||
|
public static final String CHANNEL = "deckers.thibault/aves/app";
|
||||||
|
|
||||||
|
private Context context;
|
||||||
|
|
||||||
|
public AppAdapterHandler(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||||
|
switch (call.method) {
|
||||||
|
case "getAppIcon": {
|
||||||
|
new Thread(() -> getAppIcon(call, new MethodResultWrapper(result))).start();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "getAppNames": {
|
||||||
|
result.success(getAppNames());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "edit": {
|
||||||
|
String title = call.argument("title");
|
||||||
|
Uri uri = Uri.parse(call.argument("uri"));
|
||||||
|
String mimeType = call.argument("mimeType");
|
||||||
|
edit(title, uri, mimeType);
|
||||||
|
result.success(null);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "open": {
|
||||||
|
String title = call.argument("title");
|
||||||
|
Uri uri = Uri.parse(call.argument("uri"));
|
||||||
|
String mimeType = call.argument("mimeType");
|
||||||
|
open(title, uri, mimeType);
|
||||||
|
result.success(null);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "openMap": {
|
||||||
|
Uri geoUri = Uri.parse(call.argument("geoUri"));
|
||||||
|
openMap(geoUri);
|
||||||
|
result.success(null);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "setAs": {
|
||||||
|
String title = call.argument("title");
|
||||||
|
Uri uri = Uri.parse(call.argument("uri"));
|
||||||
|
String mimeType = call.argument("mimeType");
|
||||||
|
setAs(title, uri, mimeType);
|
||||||
|
result.success(null);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "share": {
|
||||||
|
String title = call.argument("title");
|
||||||
|
Uri uri = Uri.parse(call.argument("uri"));
|
||||||
|
String mimeType = call.argument("mimeType");
|
||||||
|
share(title, uri, mimeType);
|
||||||
|
result.success(null);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result.notImplemented();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> getAppNames() {
|
||||||
|
Map<String, String> nameMap = new HashMap<>();
|
||||||
|
Intent intent = new Intent(Intent.ACTION_MAIN, null);
|
||||||
|
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||||
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
|
||||||
|
PackageManager packageManager = context.getPackageManager();
|
||||||
|
List<ResolveInfo> resolveInfoList = packageManager.queryIntentActivities(intent, 0);
|
||||||
|
for (ResolveInfo resolveInfo : resolveInfoList) {
|
||||||
|
ApplicationInfo applicationInfo = resolveInfo.activityInfo.applicationInfo;
|
||||||
|
boolean isSystemPackage = (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
|
||||||
|
if (!isSystemPackage) {
|
||||||
|
String appName = String.valueOf(packageManager.getApplicationLabel(applicationInfo));
|
||||||
|
nameMap.put(appName, applicationInfo.packageName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nameMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getAppIcon(MethodCall call, MethodChannel.Result result) {
|
||||||
|
String packageName = call.argument("packageName");
|
||||||
|
Integer size = call.argument("size");
|
||||||
|
if (packageName == null || size == null) {
|
||||||
|
result.error("getAppIcon-args", "failed because of missing arguments", null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] data = null;
|
||||||
|
try {
|
||||||
|
int iconResourceId = context.getPackageManager().getApplicationInfo(packageName, 0).icon;
|
||||||
|
Uri uri = new Uri.Builder()
|
||||||
|
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
|
||||||
|
.authority(packageName)
|
||||||
|
.path(String.valueOf(iconResourceId))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// add signature to ignore cache for images which got modified but kept the same URI
|
||||||
|
Key signature = new ObjectKey(packageName + size);
|
||||||
|
RequestOptions options = new RequestOptions()
|
||||||
|
.signature(signature)
|
||||||
|
.override(size, size);
|
||||||
|
|
||||||
|
FutureTarget<Bitmap> target = Glide.with(context)
|
||||||
|
.asBitmap()
|
||||||
|
.apply(options)
|
||||||
|
.apply(centerCropTransform())
|
||||||
|
.load(uri)
|
||||||
|
.signature(signature)
|
||||||
|
.submit(size, size);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Bitmap bmp = target.get();
|
||||||
|
if (bmp != null) {
|
||||||
|
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||||
|
bmp.compress(Bitmap.CompressFormat.PNG, 100, stream);
|
||||||
|
data = stream.toByteArray();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
Glide.with(context).clear(target);
|
||||||
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data != null) {
|
||||||
|
result.success(data);
|
||||||
|
} else {
|
||||||
|
result.error("getAppIcon-null", "failed to get icon for packageName=" + packageName, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void edit(String title, Uri uri, String mimeType) {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_EDIT);
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||||
|
intent.setDataAndType(uri, mimeType);
|
||||||
|
context.startActivity(Intent.createChooser(intent, title));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void open(String title, Uri uri, String mimeType) {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||||
|
intent.setDataAndType(uri, mimeType);
|
||||||
|
context.startActivity(Intent.createChooser(intent, title));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openMap(Uri geoUri) {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_VIEW, geoUri);
|
||||||
|
if (intent.resolveActivity(context.getPackageManager()) != null) {
|
||||||
|
context.startActivity(intent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setAs(String title, Uri uri, String mimeType) {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_ATTACH_DATA);
|
||||||
|
intent.setDataAndType(uri, mimeType);
|
||||||
|
context.startActivity(Intent.createChooser(intent, title));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void share(String title, Uri uri, String mimeType) {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||||
|
if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(uri.getScheme())) {
|
||||||
|
String path = uri.getPath();
|
||||||
|
if (path == null) return;
|
||||||
|
String applicationId = context.getApplicationContext().getPackageName();
|
||||||
|
Uri apkUri = FileProvider.getUriForFile(context, applicationId + ".fileprovider", new File(path));
|
||||||
|
intent.putExtra(Intent.EXTRA_STREAM, apkUri);
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
} else {
|
||||||
|
intent.putExtra(Intent.EXTRA_STREAM, uri);
|
||||||
|
}
|
||||||
|
intent.setType(mimeType);
|
||||||
|
context.startActivity(Intent.createChooser(intent, title));
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,8 +12,8 @@ class CollectionSource {
|
||||||
final EventBus _eventBus = EventBus();
|
final EventBus _eventBus = EventBus();
|
||||||
|
|
||||||
List<String> sortedAlbums = List.unmodifiable(const Iterable.empty());
|
List<String> sortedAlbums = List.unmodifiable(const Iterable.empty());
|
||||||
List<String> sortedCities = List.unmodifiable(const Iterable.empty());
|
|
||||||
List<String> sortedCountries = List.unmodifiable(const Iterable.empty());
|
List<String> sortedCountries = List.unmodifiable(const Iterable.empty());
|
||||||
|
List<String> sortedPlaces = List.unmodifiable(const Iterable.empty());
|
||||||
List<String> sortedTags = List.unmodifiable(const Iterable.empty());
|
List<String> sortedTags = List.unmodifiable(const Iterable.empty());
|
||||||
|
|
||||||
List<ImageEntry> get entries => List.unmodifiable(_rawEntries);
|
List<ImageEntry> get entries => List.unmodifiable(_rawEntries);
|
||||||
|
@ -125,7 +125,7 @@ class CollectionSource {
|
||||||
final locations = _rawEntries.where((entry) => entry.isLocated).map((entry) => entry.addressDetails);
|
final locations = _rawEntries.where((entry) => entry.isLocated).map((entry) => entry.addressDetails);
|
||||||
final lister = (String Function(AddressDetails a) f) => List<String>.unmodifiable(locations.map(f).where((s) => s != null && s.isNotEmpty).toSet().toList()..sort(compareAsciiUpperCase));
|
final 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};${address.countryCode}');
|
||||||
sortedCities = lister((address) => address.city);
|
sortedPlaces = lister((address) => address.place);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addAll(Iterable<ImageEntry> entries) {
|
void addAll(Iterable<ImageEntry> entries) {
|
||||||
|
|
|
@ -17,7 +17,7 @@ class LocationFilter extends CollectionFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool filter(ImageEntry entry) => entry.isLocated && ((level == LocationLevel.country && entry.addressDetails.countryName == _location) || (level == LocationLevel.city && entry.addressDetails.city == _location));
|
bool filter(ImageEntry entry) => entry.isLocated && ((level == LocationLevel.country && entry.addressDetails.countryName == _location) || (level == LocationLevel.place && entry.addressDetails.place == _location));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get label => _location;
|
String get label => _location;
|
||||||
|
@ -50,4 +50,4 @@ class LocationFilter extends CollectionFilter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum LocationLevel { city, country }
|
enum LocationLevel { place, country }
|
||||||
|
|
|
@ -110,7 +110,7 @@ class AddressDetails {
|
||||||
final int contentId;
|
final int contentId;
|
||||||
final String addressLine, countryCode, countryName, adminArea, locality;
|
final String addressLine, countryCode, countryName, adminArea, locality;
|
||||||
|
|
||||||
String get city => locality != null && locality.isNotEmpty ? locality : adminArea;
|
String get place => locality != null && locality.isNotEmpty ? locality : adminArea;
|
||||||
|
|
||||||
AddressDetails({
|
AddressDetails({
|
||||||
this.contentId,
|
this.contentId,
|
||||||
|
|
0
lib/utils/android_file_service.dart
Normal file
0
lib/utils/android_file_service.dart
Normal file
|
@ -31,7 +31,7 @@ class CollectionDrawer extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CollectionDrawerState extends State<CollectionDrawer> {
|
class _CollectionDrawerState extends State<CollectionDrawer> {
|
||||||
bool _albumsExpanded = false, _citiesExpanded = false, _countriesExpanded = false, _tagsExpanded = false;
|
bool _albumsExpanded = false, _placesExpanded = false, _countriesExpanded = false, _tagsExpanded = false;
|
||||||
|
|
||||||
CollectionSource get source => widget.source;
|
CollectionSource get source => widget.source;
|
||||||
|
|
||||||
|
@ -153,8 +153,8 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final cities = source.sortedCities;
|
|
||||||
final countries = source.sortedCountries;
|
final countries = source.sortedCountries;
|
||||||
|
final places = source.sortedPlaces;
|
||||||
final tags = source.sortedTags;
|
final tags = source.sortedTags;
|
||||||
|
|
||||||
final drawerItems = <Widget>[
|
final drawerItems = <Widget>[
|
||||||
|
@ -193,28 +193,6 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (cities.isNotEmpty)
|
|
||||||
SafeArea(
|
|
||||||
top: false,
|
|
||||||
bottom: false,
|
|
||||||
child: ExpansionTile(
|
|
||||||
leading: const Icon(AIcons.location),
|
|
||||||
title: Row(
|
|
||||||
children: [
|
|
||||||
const Text('Cities'),
|
|
||||||
const Spacer(),
|
|
||||||
Text(
|
|
||||||
'${cities.length}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: (_citiesExpanded ? Theme.of(context).accentColor : Colors.white).withOpacity(.6),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onExpansionChanged: (expanded) => setState(() => _citiesExpanded = expanded),
|
|
||||||
children: cities.map((s) => buildLocationEntry(LocationLevel.city, s)).toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (countries.isNotEmpty)
|
if (countries.isNotEmpty)
|
||||||
SafeArea(
|
SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
|
@ -237,6 +215,28 @@ class _CollectionDrawerState extends State<CollectionDrawer> {
|
||||||
children: countries.map((s) => buildLocationEntry(LocationLevel.country, s)).toList(),
|
children: countries.map((s) => buildLocationEntry(LocationLevel.country, s)).toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (places.isNotEmpty)
|
||||||
|
SafeArea(
|
||||||
|
top: false,
|
||||||
|
bottom: false,
|
||||||
|
child: ExpansionTile(
|
||||||
|
leading: const Icon(AIcons.location),
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
const Text('Places'),
|
||||||
|
const Spacer(),
|
||||||
|
Text(
|
||||||
|
'${places.length}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: (_placesExpanded ? Theme.of(context).accentColor : Colors.white).withOpacity(.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onExpansionChanged: (expanded) => setState(() => _placesExpanded = expanded),
|
||||||
|
children: places.map((s) => buildLocationEntry(LocationLevel.place, s)).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
if (tags.isNotEmpty)
|
if (tags.isNotEmpty)
|
||||||
SafeArea(
|
SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
|
|
|
@ -71,13 +71,13 @@ class ImageSearchDelegate extends SearchDelegate<CollectionFilter> {
|
||||||
),
|
),
|
||||||
_buildFilterRow(
|
_buildFilterRow(
|
||||||
context: context,
|
context: context,
|
||||||
title: 'Cities',
|
title: 'Countries',
|
||||||
filters: source.sortedCities.where(containQuery).map((s) => LocationFilter(LocationLevel.city, s)),
|
filters: source.sortedCountries.where(containQuery).map((s) => LocationFilter(LocationLevel.country, s)),
|
||||||
),
|
),
|
||||||
_buildFilterRow(
|
_buildFilterRow(
|
||||||
context: context,
|
context: context,
|
||||||
title: 'Countries',
|
title: 'Places',
|
||||||
filters: source.sortedCountries.where(containQuery).map((s) => LocationFilter(LocationLevel.country, s)),
|
filters: source.sortedPlaces.where(containQuery).map((s) => LocationFilter(LocationLevel.place, s)),
|
||||||
),
|
),
|
||||||
_buildFilterRow(
|
_buildFilterRow(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
|
@ -82,8 +82,8 @@ class _LocationSectionState extends State<LocationSection> {
|
||||||
location = address.addressLine;
|
location = address.addressLine;
|
||||||
final country = address.countryName;
|
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;${address.countryCode}'));
|
||||||
final city = address.city;
|
final place = address.place;
|
||||||
if (city != null && city.isNotEmpty) filters.add(LocationFilter(LocationLevel.city, city));
|
if (place != null && place.isNotEmpty) filters.add(LocationFilter(LocationLevel.place, place));
|
||||||
} else if (entry.hasGps) {
|
} else if (entry.hasGps) {
|
||||||
location = toDMS(entry.latLng).join(', ');
|
location = toDMS(entry.latLng).join(', ');
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ import 'package:percent_indicator/linear_percent_indicator.dart';
|
||||||
|
|
||||||
class StatsPage extends StatelessWidget {
|
class StatsPage extends StatelessWidget {
|
||||||
final CollectionLens collection;
|
final CollectionLens collection;
|
||||||
final Map<String, int> entryCountPerCity = {}, entryCountPerCountry = {}, entryCountPerTag = {};
|
final Map<String, int> entryCountPerCountry = {}, entryCountPerPlace = {}, entryCountPerTag = {};
|
||||||
|
|
||||||
List<ImageEntry> get entries => collection.sortedEntries;
|
List<ImageEntry> get entries => collection.sortedEntries;
|
||||||
|
|
||||||
|
@ -30,15 +30,15 @@ class StatsPage extends StatelessWidget {
|
||||||
entries.forEach((entry) {
|
entries.forEach((entry) {
|
||||||
if (entry.isLocated) {
|
if (entry.isLocated) {
|
||||||
final address = entry.addressDetails;
|
final address = entry.addressDetails;
|
||||||
final city = address.city;
|
|
||||||
if (city != null && city.isNotEmpty) {
|
|
||||||
entryCountPerCity[city] = (entryCountPerCity[city] ?? 0) + 1;
|
|
||||||
}
|
|
||||||
var country = address.countryName;
|
var country = address.countryName;
|
||||||
if (country != null && country.isNotEmpty) {
|
if (country != null && country.isNotEmpty) {
|
||||||
country += ';${address.countryCode}';
|
country += ';${address.countryCode}';
|
||||||
entryCountPerCountry[country] = (entryCountPerCountry[country] ?? 0) + 1;
|
entryCountPerCountry[country] = (entryCountPerCountry[country] ?? 0) + 1;
|
||||||
}
|
}
|
||||||
|
final place = address.place;
|
||||||
|
if (place != null && place.isNotEmpty) {
|
||||||
|
entryCountPerPlace[place] = (entryCountPerPlace[place] ?? 0) + 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
entry.xmpSubjects.forEach((tag) {
|
entry.xmpSubjects.forEach((tag) {
|
||||||
entryCountPerTag[tag] = (entryCountPerTag[tag] ?? 0) + 1;
|
entryCountPerTag[tag] = (entryCountPerTag[tag] ?? 0) + 1;
|
||||||
|
@ -87,8 +87,8 @@ class StatsPage extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
..._buildTopFilters(context, 'Top cities', entryCountPerCity, (s) => LocationFilter(LocationLevel.city, s)),
|
|
||||||
..._buildTopFilters(context, 'Top countries', entryCountPerCountry, (s) => LocationFilter(LocationLevel.country, s)),
|
..._buildTopFilters(context, 'Top countries', entryCountPerCountry, (s) => LocationFilter(LocationLevel.country, s)),
|
||||||
|
..._buildTopFilters(context, 'Top places', entryCountPerPlace, (s) => LocationFilter(LocationLevel.place, s)),
|
||||||
..._buildTopFilters(context, 'Top tags', entryCountPerTag, (s) => TagFilter(s)),
|
..._buildTopFilters(context, 'Top tags', entryCountPerTag, (s) => TagFilter(s)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue