service review

This commit is contained in:
Thibault Deckers 2021-09-11 14:34:05 +09:00
parent 9fdb42892e
commit 4ae828710d
43 changed files with 227 additions and 282 deletions

View file

@ -53,18 +53,16 @@ class MainActivity : FlutterActivity() {
// dart -> platform -> dart // dart -> platform -> dart
MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(AppAdapterHandler(this)) MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(AppAdapterHandler(this))
MethodChannel(messenger, AppShortcutHandler.CHANNEL).setMethodCallHandler(AppShortcutHandler(this))
MethodChannel(messenger, DebugHandler.CHANNEL).setMethodCallHandler(DebugHandler(this)) MethodChannel(messenger, DebugHandler.CHANNEL).setMethodCallHandler(DebugHandler(this))
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler()) MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler())
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this)) MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
MethodChannel(messenger, ImageFileHandler.CHANNEL).setMethodCallHandler(ImageFileHandler(this))
MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this)) MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this))
MethodChannel(messenger, GlobalSearchHandler.CHANNEL).setMethodCallHandler(GlobalSearchHandler(this)) MethodChannel(messenger, GlobalSearchHandler.CHANNEL).setMethodCallHandler(GlobalSearchHandler(this))
MethodChannel(messenger, MediaFileHandler.CHANNEL).setMethodCallHandler(MediaFileHandler(this))
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this)) MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
MethodChannel(messenger, MetadataEditHandler.CHANNEL).setMethodCallHandler(MetadataEditHandler(this)) MethodChannel(messenger, MetadataEditHandler.CHANNEL).setMethodCallHandler(MetadataEditHandler(this))
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this)) MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this)) MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
MethodChannel(messenger, TimeHandler.CHANNEL).setMethodCallHandler(TimeHandler())
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(WindowHandler(this)) MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(WindowHandler(this))
// result streaming: dart -> platform ->->-> dart // result streaming: dart -> platform ->->-> dart

View file

@ -4,17 +4,25 @@ import android.content.*
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.R
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
@ -39,6 +47,8 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
"openMap" -> safe(call, result, ::openMap) "openMap" -> safe(call, result, ::openMap)
"setAs" -> safe(call, result, ::setAs) "setAs" -> safe(call, result, ::setAs)
"share" -> safe(call, result, ::share) "share" -> safe(call, result, ::share)
"canPin" -> safe(call, result, ::canPin)
"pin" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::pin) }
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
@ -307,6 +317,64 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
} }
} }
// shortcuts
private fun isPinSupported() = ShortcutManagerCompat.isRequestPinShortcutSupported(context)
private fun canPin(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
result.success(isPinSupported())
}
private fun pin(call: MethodCall, result: MethodChannel.Result) {
val label = call.argument<String>("label")
val iconBytes = call.argument<ByteArray>("iconBytes")
val filters = call.argument<List<String>>("filters")
if (label == null || filters == null) {
result.error("pin-args", "failed because of missing arguments", null)
return
}
if (!isPinSupported()) {
result.error("pin-unsupported", "failed because the launcher does not support pinning shortcuts", null)
return
}
var icon: IconCompat? = null
if (iconBytes?.isNotEmpty() == true) {
var bitmap = BitmapFactory.decodeByteArray(iconBytes, 0, iconBytes.size)
bitmap = BitmapUtils.centerSquareCrop(context, bitmap, 256)
if (bitmap != null) {
// adaptive, so the bitmap is used as background and covers the whole icon
icon = IconCompat.createWithAdaptiveBitmap(bitmap)
}
}
if (icon == null) {
// shortcut adaptive icons are placed in `mipmap`, not `drawable`,
// so that foreground is rendered at the intended scale
val supportAdaptiveIcon = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
icon = IconCompat.createWithResource(context, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_collection else R.drawable.ic_shortcut_collection)
}
val intent = Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
.putExtra("page", "/collection")
.putExtra("filters", filters.toTypedArray())
// on API 25, `String[]` or `ArrayList` extras are null when using the shortcut
// so we use a joined `String` as fallback
.putExtra("filtersString", filters.joinToString(MainActivity.EXTRA_STRING_ARRAY_SEPARATOR))
// multiple shortcuts sharing the same ID cannot be created with different labels or icons
// so we provide a unique ID for each one, and let the user manage duplicates (i.e. same filter set), if any
val shortcut = ShortcutInfoCompat.Builder(context, UUID.randomUUID().toString())
.setShortLabel(label)
.setIcon(icon)
.setIntent(intent)
.build()
ShortcutManagerCompat.requestPinShortcut(context, shortcut, null)
result.success(true)
}
companion object { companion object {
private val LOG_TAG = LogUtils.createTag<AppAdapterHandler>() private val LOG_TAG = LogUtils.createTag<AppAdapterHandler>()
const val CHANNEL = "deckers.thibault/aves/app" const val CHANNEL = "deckers.thibault/aves/app"

View file

@ -1,90 +0,0 @@
package deckers.thibault.aves.channel.calls
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.os.Build
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.R
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.utils.BitmapUtils.centerSquareCrop
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.util.*
class AppShortcutHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"canPin" -> safe(call, result, ::canPin)
"pin" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::pin) }
else -> result.notImplemented()
}
}
private fun isSupported() = ShortcutManagerCompat.isRequestPinShortcutSupported(context)
private fun canPin(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
result.success(isSupported())
}
private fun pin(call: MethodCall, result: MethodChannel.Result) {
val label = call.argument<String>("label")
val iconBytes = call.argument<ByteArray>("iconBytes")
val filters = call.argument<List<String>>("filters")
if (label == null || filters == null) {
result.error("pin-args", "failed because of missing arguments", null)
return
}
if (!isSupported()) {
result.error("pin-unsupported", "failed because the launcher does not support pinning shortcuts", null)
return
}
var icon: IconCompat? = null
if (iconBytes?.isNotEmpty() == true) {
var bitmap = BitmapFactory.decodeByteArray(iconBytes, 0, iconBytes.size)
bitmap = centerSquareCrop(context, bitmap, 256)
if (bitmap != null) {
// adaptive, so the bitmap is used as background and covers the whole icon
icon = IconCompat.createWithAdaptiveBitmap(bitmap)
}
}
if (icon == null) {
// shortcut adaptive icons are placed in `mipmap`, not `drawable`,
// so that foreground is rendered at the intended scale
val supportAdaptiveIcon = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
icon = IconCompat.createWithResource(context, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_collection else R.drawable.ic_shortcut_collection)
}
val intent = Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
.putExtra("page", "/collection")
.putExtra("filters", filters.toTypedArray())
// on API 25, `String[]` or `ArrayList` extras are null when using the shortcut
// so we use a joined `String` as fallback
.putExtra("filtersString", filters.joinToString(MainActivity.EXTRA_STRING_ARRAY_SEPARATOR))
// multiple shortcuts sharing the same ID cannot be created with different labels or icons
// so we provide a unique ID for each one, and let the user manage duplicates (i.e. same filter set), if any
val shortcut = ShortcutInfoCompat.Builder(context, UUID.randomUUID().toString())
.setShortLabel(label)
.setIcon(icon)
.setIntent(intent)
.build()
ShortcutManagerCompat.requestPinShortcut(context, shortcut, null)
result.success(true)
}
companion object {
const val CHANNEL = "deckers.thibault/aves/shortcut"
}
}

View file

@ -5,15 +5,21 @@ import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import java.util.*
class DeviceHandler : MethodCallHandler { class DeviceHandler : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"getDefaultTimeZone" -> safe(call, result, ::getDefaultTimeZone)
"getPerformanceClass" -> safe(call, result, ::getPerformanceClass) "getPerformanceClass" -> safe(call, result, ::getPerformanceClass)
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
private fun getDefaultTimeZone(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
result.success(TimeZone.getDefault().id)
}
private fun getPerformanceClass(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) { private fun getPerformanceClass(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
result.success(Build.VERSION.MEDIA_PERFORMANCE_CLASS) result.success(Build.VERSION.MEDIA_PERFORMANCE_CLASS)

View file

@ -23,7 +23,7 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.roundToInt import kotlin.math.roundToInt
class ImageFileHandler(private val activity: Activity) : MethodCallHandler { class MediaFileHandler(private val activity: Activity) : MethodCallHandler {
private val density = activity.resources.displayMetrics.density private val density = activity.resources.displayMetrics.density
private val regionFetcher = RegionFetcher(activity) private val regionFetcher = RegionFetcher(activity)
@ -196,6 +196,6 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
} }
companion object { companion object {
const val CHANNEL = "deckers.thibault/aves/image" const val CHANNEL = "deckers.thibault/aves/media_file"
} }
} }

View file

@ -38,6 +38,6 @@ class MediaStoreHandler(private val activity: Activity) : MethodCallHandler {
} }
companion object { companion object {
const val CHANNEL = "deckers.thibault/aves/mediastore" const val CHANNEL = "deckers.thibault/aves/media_store"
} }
} }

View file

@ -1,24 +0,0 @@
package deckers.thibault.aves.channel.calls
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import java.util.*
class TimeHandler : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getDefaultTimeZone" -> safe(call, result, ::getDefaultTimeZone)
else -> result.notImplemented()
}
}
private fun getDefaultTimeZone(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
result.success(TimeZone.getDefault().id)
}
companion object {
const val CHANNEL = "deckers.thibault/aves/time"
}
}

View file

@ -187,7 +187,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
companion object { companion object {
private val LOG_TAG = LogUtils.createTag<ImageByteStreamHandler>() private val LOG_TAG = LogUtils.createTag<ImageByteStreamHandler>()
const val CHANNEL = "deckers.thibault/aves/image_byte_stream" const val CHANNEL = "deckers.thibault/aves/media_byte_stream"
private const val BUFFER_SIZE = 2 shl 17 // 256kB private const val BUFFER_SIZE = 2 shl 17 // 256kB

View file

@ -177,6 +177,6 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
companion object { companion object {
private val LOG_TAG = LogUtils.createTag<ImageOpStreamHandler>() private val LOG_TAG = LogUtils.createTag<ImageOpStreamHandler>()
const val CHANNEL = "deckers.thibault/aves/image_op_stream" const val CHANNEL = "deckers.thibault/aves/media_op_stream"
} }
} }

View file

@ -59,6 +59,6 @@ class MediaStoreChangeStreamHandler(private val context: Context) : EventChannel
companion object { companion object {
private val LOG_TAG = LogUtils.createTag<MediaStoreChangeStreamHandler>() private val LOG_TAG = LogUtils.createTag<MediaStoreChangeStreamHandler>()
const val CHANNEL = "deckers.thibault/aves/mediastorechange" const val CHANNEL = "deckers.thibault/aves/media_store_change"
} }
} }

View file

@ -62,6 +62,6 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E
companion object { companion object {
private val LOG_TAG = LogUtils.createTag<MediaStoreStreamHandler>() private val LOG_TAG = LogUtils.createTag<MediaStoreStreamHandler>()
const val CHANNEL = "deckers.thibault/aves/mediastorestream" const val CHANNEL = "deckers.thibault/aves/media_store_stream"
} }
} }

View file

@ -33,7 +33,7 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
final mimeType = key.mimeType; final mimeType = key.mimeType;
final pageId = key.pageId; final pageId = key.pageId;
try { try {
final bytes = await imageFileService.getRegion( final bytes = await mediaFileService.getRegion(
uri, uri,
mimeType, mimeType,
key.rotationDegrees, key.rotationDegrees,
@ -56,11 +56,11 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
@override @override
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, RegionProviderKey key, ImageErrorListener handleError) { void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, RegionProviderKey key, ImageErrorListener handleError) {
imageFileService.resumeLoading(key); mediaFileService.resumeLoading(key);
super.resolveStreamForKey(configuration, stream, key, handleError); super.resolveStreamForKey(configuration, stream, key, handleError);
} }
void pause() => imageFileService.cancelRegion(key); void pause() => mediaFileService.cancelRegion(key);
} }
@immutable @immutable

View file

@ -35,7 +35,7 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
final mimeType = key.mimeType; final mimeType = key.mimeType;
final pageId = key.pageId; final pageId = key.pageId;
try { try {
final bytes = await imageFileService.getThumbnail( final bytes = await mediaFileService.getThumbnail(
uri: uri, uri: uri,
mimeType: mimeType, mimeType: mimeType,
pageId: pageId, pageId: pageId,
@ -57,11 +57,11 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
@override @override
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, ThumbnailProviderKey key, ImageErrorListener handleError) { void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, ThumbnailProviderKey key, ImageErrorListener handleError) {
imageFileService.resumeLoading(key); mediaFileService.resumeLoading(key);
super.resolveStreamForKey(configuration, stream, key, handleError); super.resolveStreamForKey(configuration, stream, key, handleError);
} }
void pause() => imageFileService.cancelThumbnail(key); void pause() => mediaFileService.cancelThumbnail(key);
} }
@immutable @immutable

View file

@ -49,7 +49,7 @@ class UriImage extends ImageProvider<UriImage> with EquatableMixin {
assert(key == this); assert(key == this);
try { try {
final bytes = await imageFileService.getImage( final bytes = await mediaFileService.getImage(
uri, uri,
mimeType, mimeType,
rotationDegrees, rotationDegrees,

View file

@ -615,7 +615,7 @@ class AvesEntry {
Future<bool> delete() { Future<bool> delete() {
final completer = Completer<bool>(); final completer = Completer<bool>();
imageFileService.delete([this]).listen( mediaFileService.delete([this]).listen(
(event) => completer.complete(event.success), (event) => completer.complete(event.success),
onError: completer.completeError, onError: completer.completeError,
onDone: () { onDone: () {

View file

@ -11,7 +11,6 @@ import 'package:aves/model/settings/enums.dart';
import 'package:aves/model/settings/map_style.dart'; import 'package:aves/model/settings/map_style.dart';
import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/model/settings/screen_on.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
import 'package:aves/services/device_service.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
@ -128,7 +127,7 @@ class Settings extends ChangeNotifier {
Future<void> setContextualDefaults() async { Future<void> setContextualDefaults() async {
// performance // performance
final performanceClass = await DeviceService.getPerformanceClass(); final performanceClass = await deviceService.getPerformanceClass();
enableOverlayBlurEffect = performanceClass >= 30; enableOverlayBlurEffect = performanceClass >= 30;
// availability // availability

View file

@ -159,7 +159,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
Future<bool> renameEntry(AvesEntry entry, String newName, {required bool persist}) async { Future<bool> renameEntry(AvesEntry entry, String newName, {required bool persist}) async {
if (newName == entry.filenameWithoutExtension) return true; if (newName == entry.filenameWithoutExtension) return true;
final newFields = await imageFileService.rename(entry, '$newName${entry.extension}'); final newFields = await mediaFileService.rename(entry, '$newName${entry.extension}');
if (newFields.isEmpty) return false; if (newFields.isEmpty) return false;
await _moveEntry(entry, newFields, persist: persist); await _moveEntry(entry, newFields, persist: persist);

View file

@ -25,7 +25,7 @@ class MediaStoreSource extends CollectionSource {
await metadataDb.init(); await metadataDb.init();
await favourites.init(); await favourites.init();
await covers.init(); await covers.init();
final currentTimeZone = await timeService.getDefaultTimeZone(); final currentTimeZone = await deviceService.getDefaultTimeZone();
if (currentTimeZone != null) { if (currentTimeZone != null) {
final catalogTimeZone = settings.catalogTimeZone; final catalogTimeZone = settings.catalogTimeZone;
if (currentTimeZone != catalogTimeZone) { if (currentTimeZone != catalogTimeZone) {
@ -153,7 +153,7 @@ class MediaStoreSource extends CollectionSource {
for (final kv in uriByContentId.entries) { for (final kv in uriByContentId.entries) {
final contentId = kv.key; final contentId = kv.key;
final uri = kv.value; final uri = kv.value;
final sourceEntry = await imageFileService.getEntry(uri, null); final sourceEntry = await mediaFileService.getEntry(uri, null);
if (sourceEntry != null) { if (sourceEntry != null) {
final existingEntry = allEntries.firstWhereOrNull((entry) => entry.contentId == contentId); final existingEntry = allEntries.firstWhereOrNull((entry) => entry.contentId == contentId);
// compare paths because some apps move files without updating their `last modified date` // compare paths because some apps move files without updating their `last modified date`

View file

@ -1,10 +1,12 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/math_utils.dart'; import 'package:aves/utils/math_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
@ -136,4 +138,49 @@ class AndroidAppService {
} }
return false; return false;
} }
// app shortcuts
// this ability will not change over the lifetime of the app
static bool? _canPin;
static Future<bool> canPinToHomeScreen() async {
if (_canPin != null) return SynchronousFuture(_canPin!);
try {
final result = await platform.invokeMethod('canPin');
if (result != null) {
_canPin = result;
return result;
}
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return false;
}
static Future<void> pinToHomeScreen(String label, AvesEntry? entry, Set<CollectionFilter> filters) async {
Uint8List? iconBytes;
if (entry != null) {
final size = entry.isVideo ? 0.0 : 256.0;
iconBytes = await mediaFileService.getThumbnail(
uri: entry.uri,
mimeType: entry.mimeType,
pageId: entry.pageId,
rotationDegrees: entry.rotationDegrees,
isFlipped: entry.isFlipped,
dateModifiedSecs: entry.dateModifiedSecs,
extent: size,
);
}
try {
await platform.invokeMethod('pin', <String, dynamic>{
'label': label,
'iconBytes': iconBytes,
'filters': filters.map((filter) => filter.toJson()).toList(),
});
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
}
} }

View file

@ -1,54 +0,0 @@
import 'dart:typed_data';
import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/services/common/services.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 {
final result = await platform.invokeMethod('canPin');
if (result != null) {
_canPin = result;
return result;
}
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return false;
}
static Future<void> pin(String label, AvesEntry? entry, Set<CollectionFilter> filters) async {
Uint8List? iconBytes;
if (entry != null) {
final size = entry.isVideo ? 0.0 : 256.0;
iconBytes = await imageFileService.getThumbnail(
uri: entry.uri,
mimeType: entry.mimeType,
pageId: entry.pageId,
rotationDegrees: entry.rotationDegrees,
isFlipped: entry.isFlipped,
dateModifiedSecs: entry.dateModifiedSecs,
extent: size,
);
}
try {
await platform.invokeMethod('pin', <String, dynamic>{
'label': label,
'iconBytes': iconBytes,
'filters': filters.map((filter) => filter.toJson()).toList(),
});
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
}
}

View file

@ -1,13 +1,13 @@
import 'package:aves/model/availability.dart'; import 'package:aves/model/availability.dart';
import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/metadata_db.dart';
import 'package:aves/services/embedded_data_service.dart'; import 'package:aves/services/device_service.dart';
import 'package:aves/services/image_file_service.dart'; import 'package:aves/services/media/embedded_data_service.dart';
import 'package:aves/services/media_store_service.dart'; import 'package:aves/services/media/media_file_service.dart';
import 'package:aves/services/media/media_store_service.dart';
import 'package:aves/services/metadata/metadata_edit_service.dart'; import 'package:aves/services/metadata/metadata_edit_service.dart';
import 'package:aves/services/metadata/metadata_fetch_service.dart'; import 'package:aves/services/metadata/metadata_fetch_service.dart';
import 'package:aves/services/report_service.dart'; import 'package:aves/services/report_service.dart';
import 'package:aves/services/storage_service.dart'; import 'package:aves/services/storage_service.dart';
import 'package:aves/services/time_service.dart';
import 'package:aves/services/window_service.dart'; import 'package:aves/services/window_service.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
@ -18,14 +18,14 @@ final p.Context pContext = getIt<p.Context>();
final AvesAvailability availability = getIt<AvesAvailability>(); final AvesAvailability availability = getIt<AvesAvailability>();
final MetadataDb metadataDb = getIt<MetadataDb>(); final MetadataDb metadataDb = getIt<MetadataDb>();
final DeviceService deviceService = getIt<DeviceService>();
final EmbeddedDataService embeddedDataService = getIt<EmbeddedDataService>(); final EmbeddedDataService embeddedDataService = getIt<EmbeddedDataService>();
final ImageFileService imageFileService = getIt<ImageFileService>(); final MediaFileService mediaFileService = getIt<MediaFileService>();
final MediaStoreService mediaStoreService = getIt<MediaStoreService>(); final MediaStoreService mediaStoreService = getIt<MediaStoreService>();
final MetadataEditService metadataEditService = getIt<MetadataEditService>(); final MetadataEditService metadataEditService = getIt<MetadataEditService>();
final MetadataFetchService metadataFetchService = getIt<MetadataFetchService>(); final MetadataFetchService metadataFetchService = getIt<MetadataFetchService>();
final ReportService reportService = getIt<ReportService>(); final ReportService reportService = getIt<ReportService>();
final StorageService storageService = getIt<StorageService>(); final StorageService storageService = getIt<StorageService>();
final TimeService timeService = getIt<TimeService>();
final WindowService windowService = getIt<WindowService>(); final WindowService windowService = getIt<WindowService>();
void initPlatformServices() { void initPlatformServices() {
@ -33,13 +33,13 @@ void initPlatformServices() {
getIt.registerLazySingleton<AvesAvailability>(() => LiveAvesAvailability()); getIt.registerLazySingleton<AvesAvailability>(() => LiveAvesAvailability());
getIt.registerLazySingleton<MetadataDb>(() => SqfliteMetadataDb()); getIt.registerLazySingleton<MetadataDb>(() => SqfliteMetadataDb());
getIt.registerLazySingleton<DeviceService>(() => PlatformDeviceService());
getIt.registerLazySingleton<EmbeddedDataService>(() => PlatformEmbeddedDataService()); getIt.registerLazySingleton<EmbeddedDataService>(() => PlatformEmbeddedDataService());
getIt.registerLazySingleton<ImageFileService>(() => PlatformImageFileService()); getIt.registerLazySingleton<MediaFileService>(() => PlatformMediaFileService());
getIt.registerLazySingleton<MediaStoreService>(() => PlatformMediaStoreService()); getIt.registerLazySingleton<MediaStoreService>(() => PlatformMediaStoreService());
getIt.registerLazySingleton<MetadataEditService>(() => PlatformMetadataEditService()); getIt.registerLazySingleton<MetadataEditService>(() => PlatformMetadataEditService());
getIt.registerLazySingleton<MetadataFetchService>(() => PlatformMetadataFetchService()); getIt.registerLazySingleton<MetadataFetchService>(() => PlatformMetadataFetchService());
getIt.registerLazySingleton<ReportService>(() => CrashlyticsReportService()); getIt.registerLazySingleton<ReportService>(() => CrashlyticsReportService());
getIt.registerLazySingleton<StorageService>(() => PlatformStorageService()); getIt.registerLazySingleton<StorageService>(() => PlatformStorageService());
getIt.registerLazySingleton<TimeService>(() => PlatformTimeService());
getIt.registerLazySingleton<WindowService>(() => PlatformWindowService()); getIt.registerLazySingleton<WindowService>(() => PlatformWindowService());
} }

View file

@ -1,10 +1,27 @@
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
class DeviceService { abstract class DeviceService {
Future<String?> getDefaultTimeZone();
Future<int> getPerformanceClass();
}
class PlatformDeviceService implements DeviceService {
static const platform = MethodChannel('deckers.thibault/aves/device'); static const platform = MethodChannel('deckers.thibault/aves/device');
static Future<int> getPerformanceClass() async { @override
Future<String?> getDefaultTimeZone() async {
try {
return await platform.invokeMethod('getDefaultTimeZone');
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return null;
}
@override
Future<int> getPerformanceClass() async {
try { try {
await platform.invokeMethod('getPerformanceClass'); await platform.invokeMethod('getPerformanceClass');
final result = await platform.invokeMethod('getPerformanceClass'); final result = await platform.invokeMethod('getPerformanceClass');

View file

@ -13,7 +13,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:streams_channel/streams_channel.dart'; import 'package:streams_channel/streams_channel.dart';
abstract class ImageFileService { abstract class MediaFileService {
Future<AvesEntry?> getEntry(String uri, String? mimeType); Future<AvesEntry?> getEntry(String uri, String? mimeType);
Future<Uint8List> getSvg( Future<Uint8List> getSvg(
@ -92,10 +92,10 @@ abstract class ImageFileService {
Future<Map<String, dynamic>> rename(AvesEntry entry, String newName); Future<Map<String, dynamic>> rename(AvesEntry entry, String newName);
} }
class PlatformImageFileService implements ImageFileService { class PlatformMediaFileService implements MediaFileService {
static const platform = MethodChannel('deckers.thibault/aves/image'); static const platform = MethodChannel('deckers.thibault/aves/media_file');
static final StreamsChannel _byteStreamChannel = StreamsChannel('deckers.thibault/aves/image_byte_stream'); static final StreamsChannel _byteStreamChannel = StreamsChannel('deckers.thibault/aves/media_byte_stream');
static final StreamsChannel _opStreamChannel = StreamsChannel('deckers.thibault/aves/image_op_stream'); static final StreamsChannel _opStreamChannel = StreamsChannel('deckers.thibault/aves/media_op_stream');
static const double thumbnailDefaultSize = 64.0; static const double thumbnailDefaultSize = 64.0;
static Map<String, dynamic> _toPlatformEntryMap(AvesEntry entry) { static Map<String, dynamic> _toPlatformEntryMap(AvesEntry entry) {

View file

@ -12,11 +12,14 @@ abstract class MediaStoreService {
// knownEntries: map of contentId -> dateModifiedSecs // knownEntries: map of contentId -> dateModifiedSecs
Stream<AvesEntry> getEntries(Map<int, int> knownEntries); Stream<AvesEntry> getEntries(Map<int, int> knownEntries);
// returns media URI
Future<Uri?> scanFile(String path, String mimeType);
} }
class PlatformMediaStoreService implements MediaStoreService { class PlatformMediaStoreService implements MediaStoreService {
static const platform = MethodChannel('deckers.thibault/aves/mediastore'); static const platform = MethodChannel('deckers.thibault/aves/media_store');
static final StreamsChannel _streamChannel = StreamsChannel('deckers.thibault/aves/mediastorestream'); static final StreamsChannel _streamChannel = StreamsChannel('deckers.thibault/aves/media_store_stream');
@override @override
Future<List<int>> checkObsoleteContentIds(List<int> knownContentIds) async { Future<List<int>> checkObsoleteContentIds(List<int> knownContentIds) async {
@ -55,4 +58,19 @@ class PlatformMediaStoreService implements MediaStoreService {
return Stream.error(e); return Stream.error(e);
} }
} }
// returns media URI
@override
Future<Uri?> scanFile(String path, String mimeType) async {
try {
final result = await platform.invokeMethod('scanFile', <String, dynamic>{
'path': path,
'mimeType': mimeType,
});
if (result != null) return Uri.tryParse(result);
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return null;
}
} }

View file

@ -18,7 +18,7 @@ class SvgMetadataService {
static Future<Size?> getSize(AvesEntry entry) async { static Future<Size?> getSize(AvesEntry entry) async {
try { try {
final data = await imageFileService.getSvg(entry.uri, entry.mimeType); final data = await mediaFileService.getSvg(entry.uri, entry.mimeType);
final document = XmlDocument.parse(utf8.decode(data)); final document = XmlDocument.parse(utf8.decode(data));
final root = document.rootElement; final root = document.rootElement;
@ -64,7 +64,7 @@ class SvgMetadataService {
} }
try { try {
final data = await imageFileService.getSvg(entry.uri, entry.mimeType); final data = await mediaFileService.getSvg(entry.uri, entry.mimeType);
final document = XmlDocument.parse(utf8.decode(data)); final document = XmlDocument.parse(utf8.decode(data));
final root = document.rootElement; final root = document.rootElement;

View file

@ -4,7 +4,6 @@ import 'dart:typed_data';
import 'package:aves/services/common/output_buffer.dart'; import 'package:aves/services/common/output_buffer.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:streams_channel/streams_channel.dart'; import 'package:streams_channel/streams_channel.dart';
@ -27,9 +26,6 @@ abstract class StorageService {
// returns number of deleted directories // returns number of deleted directories
Future<int> deleteEmptyDirectories(Iterable<String> dirPaths); Future<int> deleteEmptyDirectories(Iterable<String> dirPaths);
// returns media URI
Future<Uri?> scanFile(String path, String mimeType);
// return whether operation succeeded (`null` if user cancelled) // return whether operation succeeded (`null` if user cancelled)
Future<bool?> createFile(String name, String mimeType, Uint8List bytes); Future<bool?> createFile(String name, String mimeType, Uint8List bytes);
@ -154,22 +150,6 @@ class PlatformStorageService implements StorageService {
return 0; return 0;
} }
// returns media URI
@override
Future<Uri?> scanFile(String path, String mimeType) async {
debugPrint('scanFile with path=$path, mimeType=$mimeType');
try {
final result = await platform.invokeMethod('scanFile', <String, dynamic>{
'path': path,
'mimeType': mimeType,
});
if (result != null) return Uri.tryParse(result);
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return null;
}
@override @override
Future<bool?> createFile(String name, String mimeType, Uint8List bytes) async { Future<bool?> createFile(String name, String mimeType, Uint8List bytes) async {
try { try {

View file

@ -1,20 +0,0 @@
import 'package:aves/services/common/services.dart';
import 'package:flutter/services.dart';
abstract class TimeService {
Future<String?> getDefaultTimeZone();
}
class PlatformTimeService implements TimeService {
static const platform = MethodChannel('deckers.thibault/aves/time');
@override
Future<String?> getDefaultTimeZone() async {
try {
return await platform.invokeMethod('getDefaultTimeZone');
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return null;
}
}

View file

@ -41,7 +41,7 @@ class _AvesAppState extends State<AvesApp> {
// observers are not registered when using the same list object with different items // observers are not registered when using the same list object with different items
// the list itself needs to be reassigned // the list itself needs to be reassigned
List<NavigatorObserver> _navigatorObservers = []; List<NavigatorObserver> _navigatorObservers = [];
final EventChannel _mediaStoreChangeChannel = const EventChannel('deckers.thibault/aves/mediastorechange'); final EventChannel _mediaStoreChangeChannel = const EventChannel('deckers.thibault/aves/media_store_change');
final EventChannel _newIntentChannel = const EventChannel('deckers.thibault/aves/intent'); final EventChannel _newIntentChannel = const EventChannel('deckers.thibault/aves/intent');
final EventChannel _errorChannel = const EventChannel('deckers.thibault/aves/error'); final EventChannel _errorChannel = const EventChannel('deckers.thibault/aves/error');
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey(debugLabel: 'app-navigator'); final GlobalKey<NavigatorState> _navigatorKey = GlobalKey(debugLabel: 'app-navigator');

View file

@ -9,7 +9,7 @@ import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
import 'package:aves/services/app_shortcut_service.dart'; import 'package:aves/services/android_app_service.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/collection/entry_set_action_delegate.dart'; import 'package:aves/widgets/collection/entry_set_action_delegate.dart';
import 'package:aves/widgets/collection/filter_bar.dart'; import 'package:aves/widgets/collection/filter_bar.dart';
@ -61,7 +61,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
vsync: this, vsync: this,
); );
_isSelectingNotifier.addListener(_onActivityChange); _isSelectingNotifier.addListener(_onActivityChange);
_canAddShortcutsLoader = AppShortcutService.canPin(); _canAddShortcutsLoader = AndroidAppService.canPinToHomeScreen();
_registerWidget(widget); _registerWidget(widget);
WidgetsBinding.instance!.addPostFrameCallback((_) => _onFilterChanged()); WidgetsBinding.instance!.addPostFrameCallback((_) => _onFilterChanged());
} }
@ -370,7 +370,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
final name = result.item2; final name = result.item2;
if (name.isEmpty) return; if (name.isEmpty) return;
unawaited(AppShortcutService.pin(name, coverEntry, filters)); unawaited(AndroidAppService.pinToHomeScreen(name, coverEntry, filters));
} }
void _goToSearch() { void _goToSearch() {

View file

@ -122,7 +122,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
source.pauseMonitoring(); source.pauseMonitoring();
showOpReport<MoveOpEvent>( showOpReport<MoveOpEvent>(
context: context, context: context,
opStream: imageFileService.move(todoEntries, copy: copy, destinationAlbum: destinationAlbum), opStream: mediaFileService.move(todoEntries, copy: copy, destinationAlbum: destinationAlbum),
itemCount: todoCount, itemCount: todoCount,
onDone: (processed) async { onDone: (processed) async {
final movedOps = processed.where((e) => e.success).toSet(); final movedOps = processed.where((e) => e.success).toSet();
@ -222,7 +222,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
source.pauseMonitoring(); source.pauseMonitoring();
showOpReport<ImageOpEvent>( showOpReport<ImageOpEvent>(
context: context, context: context,
opStream: imageFileService.delete(selectedItems), opStream: mediaFileService.delete(selectedItems),
itemCount: todoCount, itemCount: todoCount,
onDone: (processed) async { onDone: (processed) async {
final deletedUris = processed.where((event) => event.success).map((event) => event.uri).toSet(); final deletedUris = processed.where((event) => event.success).map((event) => event.uri).toSet();

View file

@ -155,7 +155,7 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
if (widget.cancellableNotifier?.value ?? false) { if (widget.cancellableNotifier?.value ?? false) {
final key = await _currentProviderStream?.provider.provider.obtainKey(ImageConfiguration.empty); final key = await _currentProviderStream?.provider.provider.obtainKey(ImageConfiguration.empty);
if (key is ThumbnailProviderKey) { if (key is ThumbnailProviderKey) {
imageFileService.cancelThumbnail(key); mediaFileService.cancelThumbnail(key);
} }
} }
} }

View file

@ -45,7 +45,7 @@ class _DebugCacheSectionState extends State<DebugCacheSection> with AutomaticKee
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
ElevatedButton( ElevatedButton(
onPressed: imageFileService.clearSizedThumbnailDiskCache, onPressed: mediaFileService.clearSizedThumbnailDiskCache,
child: const Text('Clear'), child: const Text('Clear'),
), ),
], ],

View file

@ -167,7 +167,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
source.pauseMonitoring(); source.pauseMonitoring();
showOpReport<ImageOpEvent>( showOpReport<ImageOpEvent>(
context: context, context: context,
opStream: imageFileService.delete(todoEntries), opStream: mediaFileService.delete(todoEntries),
itemCount: todoCount, itemCount: todoCount,
onDone: (processed) async { onDone: (processed) async {
final deletedUris = processed.where((event) => event.success).map((event) => event.uri).toSet(); final deletedUris = processed.where((event) => event.success).map((event) => event.uri).toSet();
@ -226,7 +226,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
source.pauseMonitoring(); source.pauseMonitoring();
showOpReport<MoveOpEvent>( showOpReport<MoveOpEvent>(
context: context, context: context,
opStream: imageFileService.move(todoEntries, copy: false, destinationAlbum: destinationAlbum), opStream: mediaFileService.move(todoEntries, copy: false, destinationAlbum: destinationAlbum),
itemCount: todoCount, itemCount: todoCount,
onDone: (processed) async { onDone: (processed) async {
final movedOps = processed.where((e) => e.success).toSet(); final movedOps = processed.where((e) => e.success).toSet();

View file

@ -125,7 +125,7 @@ class _HomePageState extends State<HomePage> {
} }
Future<AvesEntry?> _initViewerEntry({required String uri, required String? mimeType}) async { Future<AvesEntry?> _initViewerEntry({required String uri, required String? mimeType}) async {
final entry = await imageFileService.getEntry(uri, mimeType); final entry = await mediaFileService.getEntry(uri, mimeType);
if (entry != null) { if (entry != null) {
// cataloguing is essential for coordinates and video rotation // cataloguing is essential for coordinates and video rotation
await entry.catalog(background: false, persist: false); await entry.catalog(background: false, persist: false);

View file

@ -204,7 +204,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
showOpReport<ExportOpEvent>( showOpReport<ExportOpEvent>(
context: context, context: context,
// TODO TLAD [SVG] export separately from raster images (sending bytes, like frame captures) // TODO TLAD [SVG] export separately from raster images (sending bytes, like frame captures)
opStream: imageFileService.export( opStream: mediaFileService.export(
selection, selection,
mimeType: MimeTypes.jpeg, mimeType: MimeTypes.jpeg,
destinationAlbum: destinationAlbum, destinationAlbum: destinationAlbum,
@ -286,7 +286,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
MaterialPageRoute( MaterialPageRoute(
settings: const RouteSettings(name: SourceViewerPage.routeName), settings: const RouteSettings(name: SourceViewerPage.routeName),
builder: (context) => SourceViewerPage( builder: (context) => SourceViewerPage(
loader: () => imageFileService.getSvg(entry.uri, entry.mimeType).then(utf8.decode), loader: () => mediaFileService.getSvg(entry.uri, entry.mimeType).then(utf8.decode),
), ),
), ),
); );

View file

@ -74,7 +74,7 @@ class EntryPrinter with FeedbackMixin {
Future<pdf.Widget?> _buildPageImage(AvesEntry entry) async { Future<pdf.Widget?> _buildPageImage(AvesEntry entry) async {
if (entry.isSvg) { if (entry.isSvg) {
final bytes = await imageFileService.getSvg(entry.uri, entry.mimeType); final bytes = await mediaFileService.getSvg(entry.uri, entry.mimeType);
if (bytes.isNotEmpty) { if (bytes.isNotEmpty) {
return pdf.SvgImage(svg: utf8.decode(bytes)); return pdf.SvgImage(svg: utf8.decode(bytes));
} }

View file

@ -85,7 +85,7 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
} }
}; };
final newFields = await imageFileService.captureFrame( final newFields = await mediaFileService.captureFrame(
entry, entry,
desiredName: '${entry.bestTitle}_${'$positionMillis'.padLeft(8, '0')}', desiredName: '${entry.bestTitle}_${'$positionMillis'.padLeft(8, '0')}',
exif: exif, exif: exif,

View file

@ -1,8 +1,8 @@
import 'package:aves/services/time_service.dart'; import 'package:aves/services/device_service.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
class FakeTimeService extends Fake implements TimeService { class FakeDeviceService extends Fake implements DeviceService {
@override @override
Future<String> getDefaultTimeZone() => SynchronousFuture(''); Future<String> getDefaultTimeZone() => SynchronousFuture('');
} }

View file

@ -1,11 +1,11 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/services/image_file_service.dart'; import 'package:aves/services/media/media_file_service.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'media_store_service.dart'; import 'media_store_service.dart';
class FakeImageFileService extends Fake implements ImageFileService { class FakeMediaFileService extends Fake implements MediaFileService {
@override @override
Future<Map<String, dynamic>> rename(AvesEntry entry, String newName) { Future<Map<String, dynamic>> rename(AvesEntry entry, String newName) {
final contentId = FakeMediaStoreService.nextContentId; final contentId = FakeMediaStoreService.nextContentId;

View file

@ -1,7 +1,7 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/image_op_events.dart'; import 'package:aves/services/common/image_op_events.dart';
import 'package:aves/services/media_store_service.dart'; import 'package:aves/services/media/media_store_service.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';

View file

@ -8,12 +8,12 @@ import 'package:aves/model/metadata_db.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
import 'package:aves/model/source/media_store_source.dart'; import 'package:aves/model/source/media_store_source.dart';
import 'package:aves/services/image_file_service.dart';
import 'package:aves/services/media_store_service.dart';
import 'package:aves/services/metadata/metadata_fetch_service.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/services/device_service.dart';
import 'package:aves/services/media/media_file_service.dart';
import 'package:aves/services/media/media_store_service.dart';
import 'package:aves/services/metadata/metadata_fetch_service.dart';
import 'package:aves/services/storage_service.dart'; import 'package:aves/services/storage_service.dart';
import 'package:aves/services/time_service.dart';
import 'package:aves/services/window_service.dart'; import 'package:aves/services/window_service.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -21,12 +21,12 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import '../fake/availability.dart'; import '../fake/availability.dart';
import '../fake/image_file_service.dart'; import '../fake/device_service.dart';
import '../fake/media_file_service.dart';
import '../fake/media_store_service.dart'; import '../fake/media_store_service.dart';
import '../fake/metadata_db.dart'; import '../fake/metadata_db.dart';
import '../fake/metadata_fetch_service.dart'; import '../fake/metadata_fetch_service.dart';
import '../fake/storage_service.dart'; import '../fake/storage_service.dart';
import '../fake/time_service.dart';
import '../fake/window_service.dart'; import '../fake/window_service.dart';
void main() { void main() {
@ -40,11 +40,11 @@ void main() {
getIt.registerLazySingleton<AvesAvailability>(() => FakeAvesAvailability()); getIt.registerLazySingleton<AvesAvailability>(() => FakeAvesAvailability());
getIt.registerLazySingleton<MetadataDb>(() => FakeMetadataDb()); getIt.registerLazySingleton<MetadataDb>(() => FakeMetadataDb());
getIt.registerLazySingleton<ImageFileService>(() => FakeImageFileService()); getIt.registerLazySingleton<DeviceService>(() => FakeDeviceService());
getIt.registerLazySingleton<MediaFileService>(() => FakeMediaFileService());
getIt.registerLazySingleton<MediaStoreService>(() => FakeMediaStoreService()); getIt.registerLazySingleton<MediaStoreService>(() => FakeMediaStoreService());
getIt.registerLazySingleton<MetadataFetchService>(() => FakeMetadataFetchService()); getIt.registerLazySingleton<MetadataFetchService>(() => FakeMetadataFetchService());
getIt.registerLazySingleton<StorageService>(() => FakeStorageService()); getIt.registerLazySingleton<StorageService>(() => FakeStorageService());
getIt.registerLazySingleton<TimeService>(() => FakeTimeService());
getIt.registerLazySingleton<WindowService>(() => FakeWindowService()); getIt.registerLazySingleton<WindowService>(() => FakeWindowService());
await settings.init(); await settings.init();

View file

@ -4,7 +4,7 @@ import 'package:aves/main.dart' as app;
import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/enums.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/services/storage_service.dart'; import 'package:aves/services/media/media_store_service.dart';
import 'package:aves/services/window_service.dart'; import 'package:aves/services/window_service.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/src/widgets/media_query.dart'; import 'package:flutter/src/widgets/media_query.dart';
@ -19,9 +19,9 @@ void main() {
// scan files copied from test assets // scan files copied from test assets
// we do it via the app instead of broadcasting via ADB // we do it via the app instead of broadcasting via ADB
// because `MEDIA_SCANNER_SCAN_FILE` intent got deprecated in API 29 // because `MEDIA_SCANNER_SCAN_FILE` intent got deprecated in API 29
final storageService = PlatformStorageService(); final mediaStoreService = PlatformMediaStoreService();
storageService.scanFile(p.join(targetPicturesDir, 'aves_logo.svg'), 'image/svg+xml'); mediaStoreService.scanFile(p.join(targetPicturesDir, 'aves_logo.svg'), 'image/svg+xml');
storageService.scanFile(p.join(targetPicturesDir, 'ipse.jpg'), 'image/jpeg'); mediaStoreService.scanFile(p.join(targetPicturesDir, 'ipse.jpg'), 'image/jpeg');
configureAndLaunch(); configureAndLaunch();
} }