(Android) Show our own download notifs

This commit is contained in:
Ming Ming 2021-10-02 17:12:54 +08:00
parent 808c654e75
commit 60a0a387b2
17 changed files with 509 additions and 103 deletions

View file

@ -41,6 +41,15 @@ class DownloadChannelHandler(activity: Activity) :
result.error("systemException", e.toString(), null)
}
}
"cancel" -> {
try {
cancel(
call.argument("id")!!, result
)
} catch (e: Throwable) {
result.error("systemException", e.toString(), null)
}
}
else -> {
result.notImplemented()
}
@ -85,6 +94,13 @@ class DownloadChannelHandler(activity: Activity) :
result.success(id)
}
private fun cancel(
id: Long, result: MethodChannel.Result
) {
val count = _downloadManager.remove(id)
result.success(count > 0)
}
private val _activity = activity
private val _context get() = _activity
private val _downloadManager by lazy {

View file

@ -94,3 +94,42 @@ class DownloadEventCompleteChannelHandler(context: Context) :
_context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
}
}
class DownloadEventCancelChannelHandler(context: Context) : BroadcastReceiver(),
EventChannel.StreamHandler {
companion object {
@JvmStatic
val CHANNEL =
"com.nkming.nc_photos/download_event/action_download_cancel"
}
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action != K.ACTION_DOWNLOAD_CANCEL || !intent.hasExtra(
K.EXTRA_NOTIFICATION_ID
)
) {
return
}
val id = intent.getIntExtra(K.EXTRA_NOTIFICATION_ID, 0)
_eventSink?.success(
mapOf(
"notificationId" to id
)
)
}
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
_context.registerReceiver(
this, IntentFilter(K.ACTION_DOWNLOAD_CANCEL)
)
_eventSink = events
}
override fun onCancel(arguments: Any?) {
_context.unregisterReceiver(this)
}
private val _context = context
private var _eventSink: EventChannel.EventSink? = null
}

View file

@ -4,5 +4,9 @@ interface K {
companion object {
const val DOWNLOAD_NOTIFICATION_ID_MIN = 1000
const val DOWNLOAD_NOTIFICATION_ID_MAX = 2000
const val ACTION_DOWNLOAD_CANCEL = "com.nkming.nc_photos.ACTION_DOWNLOAD_CANCEL"
const val EXTRA_NOTIFICATION_ID = "com.nkming.nc_photos.EXTRA_NOTIFICATION_ID"
}
}

View file

@ -28,5 +28,8 @@ class MainActivity : FlutterActivity() {
EventChannel(flutterEngine.dartExecutor.binaryMessenger,
DownloadEventCompleteChannelHandler.CHANNEL).setStreamHandler(
DownloadEventCompleteChannelHandler(this))
EventChannel(flutterEngine.dartExecutor.binaryMessenger,
DownloadEventCancelChannelHandler.CHANNEL).setStreamHandler(
DownloadEventCancelChannelHandler(this))
}
}

View file

@ -30,14 +30,14 @@ class NotificationChannelHandler(activity: Activity) :
companion object {
const val CHANNEL = "com.nkming.nc_photos/notification"
private fun getNextNotificationId(): Int {
fun getNextNotificationId(): Int {
if (++notificationId >= K.DOWNLOAD_NOTIFICATION_ID_MAX) {
notificationId = K.DOWNLOAD_NOTIFICATION_ID_MIN
}
return notificationId
}
private const val DOWNLOAD_CHANNEL_ID = "download"
const val DOWNLOAD_CHANNEL_ID = "download"
private var notificationId = K.DOWNLOAD_NOTIFICATION_ID_MIN
}
@ -47,11 +47,25 @@ class NotificationChannelHandler(activity: Activity) :
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"notifyItemsDownloadSuccessful" -> {
"notifyDownloadSuccessful" -> {
try {
notifyItemsDownloadSuccessful(
notifyDownloadSuccessful(
call.argument("fileUris")!!,
call.argument("mimeTypes")!!,
call.argument("notificationId"),
result
)
} catch (e: Throwable) {
result.error("systemException", e.toString(), null)
}
}
"notifyDownloadProgress" -> {
try {
notifyDownloadProgress(
call.argument("progress")!!,
call.argument("max")!!,
call.argument("currentItemTitle"),
call.argument("notificationId"),
result
)
} catch (e: Throwable) {
@ -67,15 +81,23 @@ class NotificationChannelHandler(activity: Activity) :
result.error("systemException", e.toString(), null)
}
}
"dismiss" -> {
try {
dismiss(call.argument("notificationId")!!, result)
} catch (e: Throwable) {
result.error("systemException", e.toString(), null)
}
}
else -> {
result.notImplemented()
}
}
}
private fun notifyItemsDownloadSuccessful(
private fun notifyDownloadSuccessful(
fileUris: List<String>,
mimeTypes: List<String?>,
notificationId: Int?,
result: MethodChannel.Result
) {
assert(fileUris.isNotEmpty())
@ -88,7 +110,7 @@ class NotificationChannelHandler(activity: Activity) :
RingtoneManager.getDefaultUri(
RingtoneManager.TYPE_NOTIFICATION
)
).setOnlyAlertOnce(true).setAutoCancel(true).setLocalOnly(true)
).setOnlyAlertOnce(false).setAutoCancel(true).setLocalOnly(true)
if (uris.size == 1) {
builder.setContentTitle(
@ -159,10 +181,57 @@ class NotificationChannelHandler(activity: Activity) :
), sharePendingIntent
)
val id = notificationId ?: getNextNotificationId()
with(NotificationManagerCompat.from(_context)) {
notify(getNextNotificationId(), builder.build())
notify(id, builder.build())
}
result.success(null)
result.success(id)
}
private fun notifyDownloadProgress(
progress: Int,
max: Int,
currentItemTitle: String?,
notificationId: Int?,
result: MethodChannel.Result
) {
val id = notificationId ?: getNextNotificationId()
val builder = NotificationCompat.Builder(_context, DOWNLOAD_CHANNEL_ID)
.setSmallIcon(android.R.drawable.stat_sys_download)
.setWhen(System.currentTimeMillis())
.setPriority(NotificationCompat.PRIORITY_HIGH).setSound(
RingtoneManager.getDefaultUri(
RingtoneManager.TYPE_NOTIFICATION
)
).setOnlyAlertOnce(true).setAutoCancel(false).setLocalOnly(true)
.setProgress(max, progress, false).setContentText("$progress/$max")
if (currentItemTitle == null) {
builder.setContentTitle(_context.getString(R.string.download_progress_notification_untitled_text))
} else {
builder.setContentTitle(
_context.getString(
R.string.download_progress_notification_text,
currentItemTitle
)
)
}
val cancelIntent = Intent().apply {
`package` = BuildConfig.APPLICATION_ID
action = K.ACTION_DOWNLOAD_CANCEL
putExtra(K.EXTRA_NOTIFICATION_ID, id)
}
val cancelPendingIntent = PendingIntent.getBroadcast(
_context, 0, cancelIntent, PendingIntent.FLAG_UPDATE_CURRENT
)
builder.addAction(
0, _context.getString(android.R.string.cancel), cancelPendingIntent
)
with(NotificationManagerCompat.from(_context)) {
notify(id, builder.build())
}
result.success(id)
}
private fun notifyLogSaveSuccessful(
@ -205,8 +274,16 @@ class NotificationChannelHandler(activity: Activity) :
// can't add the share action here because android will share the URI as
// plain text instead of treating it as a text file...
val id = getNextNotificationId()
with(NotificationManagerCompat.from(_context)) {
notify(getNextNotificationId(), builder.build())
notify(id, builder.build())
}
result.success(id)
}
private fun dismiss(notificationId: Int, result: MethodChannel.Result) {
with(NotificationManagerCompat.from(_context)) {
cancel(notificationId)
}
result.success(null)
}

View file

@ -9,6 +9,8 @@
<string name="download_successful_notification_action_share">SHARE</string>
<string name="download_successful_notification_action_share_chooser">Share with:</string>
<string name="download_multiple_successful_notification_title">Downloaded %1$d items successfully</string>
<string name="download_progress_notification_text">Downloading %1$s</string>
<string name="download_progress_notification_untitled_text">Downloading</string>
<string name="log_save_successful_notification_title">Logs saved successfully</string>
<string name="log_save_successful_notification_text">Tap to view your saved logs</string>
</resources>

View file

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:logging/logging.dart';
@ -6,7 +8,9 @@ import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/exception.dart';
import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/mobile/android/download.dart';
import 'package:nc_photos/mobile/notification.dart';
import 'package:nc_photos/platform/k.dart' as platform_k;
import 'package:nc_photos/snack_bar_manager.dart';
@ -18,6 +22,136 @@ class DownloadHandler {
Account account,
List<File> files, {
String? parentDir,
}) {
final _DownloadHandlerBase handler;
if (platform_k.isAndroid) {
handler = _DownlaodHandlerAndroid();
} else {
handler = _DownloadHandlerWeb();
}
return handler.downloadFiles(
account,
files,
parentDir: parentDir,
);
}
}
abstract class _DownloadHandlerBase {
Future<void> downloadFiles(
Account account,
List<File> files, {
String? parentDir,
});
}
class _DownlaodHandlerAndroid extends _DownloadHandlerBase {
@override
downloadFiles(
Account account,
List<File> files, {
String? parentDir,
}) async {
_log.info("[downloadFiles] Downloading ${files.length} file");
final notif = AndroidDownloadProgressNotification(
0,
files.length,
currentItemTitle: files.firstOrNull?.filename,
);
await notif.notify();
final successes = <Tuple2<File, dynamic>>[];
StreamSubscription<DownloadCancelEvent>? subscription;
try {
bool isCancel = false;
subscription = DownloadEvent.listenDownloadCancel()
..onData((data) {
if (data.notificationId == notif.notificationId) {
isCancel = true;
}
});
int count = 0;
for (final f in files) {
if (isCancel == true) {
_log.info("[downloadFiles] User canceled remaining files");
break;
}
notif.update(
count++,
currentItemTitle: f.filename,
);
StreamSubscription<DownloadCancelEvent>? itemSubscription;
try {
final download = DownloadFile().build(
account,
f,
parentDir: parentDir,
shouldNotify: false,
);
itemSubscription = DownloadEvent.listenDownloadCancel()
..onData((data) {
if (data.notificationId == notif.notificationId) {
_log.info("[downloadFiles] Cancel requested");
download.cancel();
}
});
final result = await download();
successes.add(Tuple2(f, result));
} on PermissionException catch (_) {
_log.warning("[downloadFiles] Permission not granted");
SnackBarManager().showSnackBar(SnackBar(
content:
Text(L10n.global().downloadFailureNoPermissionNotification),
duration: k.snackBarDurationNormal,
));
break;
} on JobCanceledException catch (_) {
_log.info("[downloadFiles] User canceled");
break;
} catch (e, stackTrace) {
_log.shout(
"[downloadFiles] Failed while DownloadFile", e, stackTrace);
SnackBarManager().showSnackBar(SnackBar(
content: Text("${L10n.global().downloadFailureNotification}: "
"${exception_util.toUserString(e)}"),
duration: k.snackBarDurationNormal,
));
} finally {
itemSubscription?.cancel();
}
}
} finally {
subscription?.cancel();
if (successes.isNotEmpty) {
await _onDownloadSuccessful(successes.map((e) => e.item1).toList(),
successes.map((e) => e.item2).toList(), notif.notificationId);
} else {
await notif.dismiss();
}
}
}
Future<void> _onDownloadSuccessful(
List<File> files, List<dynamic> results, int? notificationId) async {
final notif = AndroidDownloadSuccessfulNotification(
results.cast<String>(),
files.map((e) => e.contentType).toList(),
notificationId: notificationId,
);
await notif.notify();
}
static final _log = Logger("download_handler._DownloadHandlerAndroid");
}
class _DownloadHandlerWeb extends _DownloadHandlerBase {
@override
downloadFiles(
Account account,
List<File> files, {
String? parentDir,
}) async {
_log.info("[downloadFiles] Downloading ${files.length} file");
var controller = SnackBarManager().showSnackBar(SnackBar(
@ -27,15 +161,15 @@ class DownloadHandler {
controller?.closed.whenComplete(() {
controller = null;
});
final successes = <Tuple2<File, dynamic>>[];
int successCount = 0;
for (final f in files) {
try {
final result = await DownloadFile()(
await DownloadFile()(
account,
f,
parentDir: parentDir,
);
successes.add(Tuple2(f, result));
++successCount;
} on PermissionException catch (_) {
_log.warning("[downloadFiles] Permission not granted");
controller?.close();
@ -57,38 +191,14 @@ class DownloadHandler {
));
}
}
if (successes.isNotEmpty) {
if (successCount > 0) {
controller?.close();
await _onDownloadSuccessful(successes.map((e) => e.item1).toList(),
successes.map((e) => e.item2).toList());
SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global().downloadSuccessNotification),
duration: k.snackBarDurationShort,
));
}
}
Future<void> _onDownloadSuccessful(
List<File> files, List<dynamic> results) async {
dynamic notif;
if (platform_k.isAndroid) {
notif = AndroidItemDownloadSuccessfulNotification(
results.cast<String>(), files.map((e) => e.contentType).toList());
}
if (notif != null) {
try {
await notif.notify();
return;
} catch (e, stacktrace) {
_log.shout(
"[_onDownloadSuccessful] Failed showing platform notification",
e,
stacktrace);
}
}
// fallback
SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global().downloadSuccessNotification),
duration: k.snackBarDurationShort,
));
}
static final _log = Logger("download_handler.DownloadHandler");
static final _log = Logger("download_handler._DownloadHandlerWeb");
}

View file

@ -19,6 +19,14 @@ class Download {
}))!;
}
static Future<bool> cancel({
required int id,
}) async {
return (await _channel.invokeMethod<bool>("cancel", <String, dynamic>{
"id": id,
}))!;
}
/// The download job has failed
static const exceptionCodeDownloadError = "downloadError";
@ -27,7 +35,10 @@ class Download {
class DownloadEvent {
static StreamSubscription<DownloadCompleteEvent> listenDownloadComplete() =>
_stream.listen(null);
_completeStream.listen(null);
static StreamSubscription<DownloadCancelEvent> listenDownloadCancel() =>
_cancelStream.listen(null);
/// User canceled the download job
static const exceptionCodeUserCanceled = "userCanceled";
@ -35,7 +46,7 @@ class DownloadEvent {
static const _downloadCompleteChannel = EventChannel(
"com.nkming.nc_photos/download_event/action_download_complete");
static late final _stream = _downloadCompleteChannel
static late final _completeStream = _downloadCompleteChannel
.receiveBroadcastStream()
.map((data) => DownloadCompleteEvent(
data["downloadId"],
@ -50,6 +61,15 @@ class DownloadEvent {
e.details is Map &&
e.details["downloadId"] is int,
);
static const _downloadCancelChannel = EventChannel(
"com.nkming.nc_photos/download_event/action_download_cancel");
static late final _cancelStream = _downloadCancelChannel
.receiveBroadcastStream()
.map((data) => DownloadCancelEvent(
data["notificationId"],
));
}
class DownloadCompleteEvent {
@ -66,3 +86,9 @@ class AndroidDownloadError implements Exception {
final dynamic error;
final StackTrace stackTrace;
}
class DownloadCancelEvent {
const DownloadCancelEvent(this.notificationId);
final int notificationId;
}

View file

@ -1,17 +1,32 @@
import 'package:flutter/services.dart';
class Notification {
static Future<void> notifyItemsDownloadSuccessful(
List<String> fileUris, List<String?> mimeTypes) =>
_channel.invokeMethod("notifyItemsDownloadSuccessful", <String, dynamic>{
static Future<int?> notifyDownloadSuccessful(List<String> fileUris,
List<String?> mimeTypes, int? notificationId) =>
_channel.invokeMethod("notifyDownloadSuccessful", <String, dynamic>{
"fileUris": fileUris,
"mimeTypes": mimeTypes,
"notificationId": notificationId,
});
static Future<void> notifyLogSaveSuccessful(String fileUri) =>
static Future<int?> notifyDownloadProgress(int progress, int max,
String? currentItemTitle, int? notificationId) =>
_channel.invokeMethod("notifyDownloadProgress", <String, dynamic>{
"progress": progress,
"max": max,
"currentItemTitle": currentItemTitle,
"notificationId": notificationId,
});
static Future<int?> notifyLogSaveSuccessful(String fileUri) =>
_channel.invokeMethod("notifyLogSaveSuccessful", <String, dynamic>{
"fileUri": fileUri,
});
static Future<void> dismiss(int notificationId) =>
_channel.invokeMethod("dismiss", <String, dynamic>{
"notificationId": notificationId,
});
static const _channel = MethodChannel("com.nkming.nc_photos/notification");
}

View file

@ -3,14 +3,14 @@ import 'dart:async';
import 'package:flutter/services.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/exception.dart';
import 'package:nc_photos/mobile/android/download.dart';
import 'package:nc_photos/mobile/android/download.dart' as android;
import 'package:nc_photos/mobile/android/media_store.dart';
import 'package:nc_photos/platform/file_downloader.dart' as itf;
import 'package:nc_photos/platform/download.dart' as itf;
import 'package:nc_photos/platform/k.dart' as platform_k;
class FileDownloader extends itf.FileDownloader {
class DownloadBuilder extends itf.DownloadBuilder {
@override
downloadUrl({
build({
required String url,
Map<String, String>? headers,
String? mimeType,
@ -19,7 +19,7 @@ class FileDownloader extends itf.FileDownloader {
bool? shouldNotify,
}) {
if (platform_k.isAndroid) {
return _downloadUrlAndroid(
return _AndroidDownload(
url: url,
headers: headers,
mimeType: mimeType,
@ -31,15 +31,20 @@ class FileDownloader extends itf.FileDownloader {
throw UnimplementedError();
}
}
}
Future<String> _downloadUrlAndroid({
required String url,
Map<String, String>? headers,
String? mimeType,
required String filename,
String? parentDir,
bool? shouldNotify,
}) async {
class _AndroidDownload extends itf.Download {
_AndroidDownload({
required this.url,
this.headers,
this.mimeType,
required this.filename,
this.parentDir,
this.shouldNotify,
});
@override
call() async {
final String path;
if (parentDir?.isNotEmpty == true) {
path = "$parentDir/$filename";
@ -48,32 +53,32 @@ class FileDownloader extends itf.FileDownloader {
}
try {
_log.info("[_downloadUrlAndroid] Start downloading '$url'");
final id = await Download.downloadUrl(
_log.info("[call] Start downloading '$url'");
_downloadId = await android.Download.downloadUrl(
url: url,
headers: headers,
mimeType: mimeType,
filename: path,
shouldNotify: shouldNotify,
);
_log.info("[call] #$_downloadId -> '$url'");
late final String uri;
final completer = Completer();
onDownloadComplete(DownloadCompleteEvent ev) {
if (ev.downloadId == id) {
_log.info(
"[_downloadUrlAndroid] Finished downloading '$url' to '${ev.uri}'");
onDownloadComplete(android.DownloadCompleteEvent ev) {
if (ev.downloadId == _downloadId) {
_log.info("[call] Finished downloading '$url' to '${ev.uri}'");
uri = ev.uri;
completer.complete();
}
}
StreamSubscription<DownloadCompleteEvent>? subscription;
StreamSubscription<android.DownloadCompleteEvent>? subscription;
try {
subscription = DownloadEvent.listenDownloadComplete()
subscription = android.DownloadEvent.listenDownloadComplete()
..onData(onDownloadComplete)
..onError((e, stackTrace) {
if (e is AndroidDownloadError) {
if (e.downloadId != id) {
if (e is android.AndroidDownloadError) {
if (e.downloadId != _downloadId) {
// not us, ignore
return;
}
@ -92,10 +97,10 @@ class FileDownloader extends itf.FileDownloader {
case MediaStore.exceptionCodePermissionError:
throw PermissionException();
case Download.exceptionCodeDownloadError:
case android.Download.exceptionCodeDownloadError:
throw DownloadException(e.message);
case DownloadEvent.exceptionCodeUserCanceled:
case android.DownloadEvent.exceptionCodeUserCanceled:
throw JobCanceledException(e.message);
default:
@ -104,5 +109,23 @@ class FileDownloader extends itf.FileDownloader {
}
}
static final _log = Logger("mobile.file_downloader.FileDownloader");
@override
cancel() async {
if (_downloadId != null) {
_log.info("[cancel] Cancel #$_downloadId");
return await android.Download.cancel(id: _downloadId!);
} else {
return false;
}
}
final String url;
final Map<String, String>? headers;
final String? mimeType;
final String filename;
final String? parentDir;
final bool? shouldNotify;
int? _downloadId;
static final _log = Logger("mobile.download._AndroidDownload");
}

View file

@ -1,27 +1,72 @@
import 'package:flutter/foundation.dart';
import 'package:nc_photos/mobile/android/notification.dart';
import 'package:nc_photos/platform/notification.dart' as itf;
class AndroidItemDownloadSuccessfulNotification
extends itf.ItemDownloadSuccessfulNotification {
AndroidItemDownloadSuccessfulNotification(this.fileUris, this.mimeTypes);
class AndroidDownloadSuccessfulNotification extends _AndroidNotification {
AndroidDownloadSuccessfulNotification(
this.fileUris,
this.mimeTypes, {
int? notificationId,
}) : replaceId = notificationId;
@override
Future<void> notify() {
return Notification.notifyItemsDownloadSuccessful(fileUris, mimeTypes);
}
doNotify() =>
Notification.notifyDownloadSuccessful(fileUris, mimeTypes, replaceId);
final List<String> fileUris;
final List<String?> mimeTypes;
final int? replaceId;
}
class AndroidLogSaveSuccessfulNotification
extends itf.LogSaveSuccessfulNotification {
class AndroidDownloadProgressNotification extends _AndroidNotification {
AndroidDownloadProgressNotification(
this.progress,
this.max, {
this.currentItemTitle,
});
@override
doNotify() => Notification.notifyDownloadProgress(
progress, max, currentItemTitle, notificationId);
Future<void> update(
int progress, {
String? currentItemTitle,
}) async {
this.progress = progress;
this.currentItemTitle = currentItemTitle;
await doNotify();
}
int progress;
final int max;
String? currentItemTitle;
}
class AndroidLogSaveSuccessfulNotification extends _AndroidNotification {
AndroidLogSaveSuccessfulNotification(this.fileUri);
@override
Future<void> notify() {
return Notification.notifyLogSaveSuccessful(fileUri);
}
doNotify() => Notification.notifyLogSaveSuccessful(fileUri);
final String fileUri;
}
abstract class _AndroidNotification extends itf.Notification {
@override
notify() async {
notificationId = await doNotify();
}
@override
dismiss() async {
if (notificationId != null) {
await Notification.dismiss(notificationId!);
}
}
@protected
Future<int?> doNotify();
int? notificationId;
}

View file

@ -1,5 +1,5 @@
export 'db_util.dart';
export 'file_downloader.dart';
export 'download.dart';
export 'file_saver.dart';
export 'map_widget.dart';
export 'universal_storage.dart';

View file

@ -1,9 +1,19 @@
abstract class FileDownloader {
abstract class Download {
/// Download a file
///
/// The return data depends on the platform
/// - web: null
/// - android: Uri to the downloaded file
Future call();
/// Cancel a download
///
/// Not all platforms support canceling an ongoing download
Future<bool> cancel();
}
abstract class DownloadBuilder {
/// Create a platform specific download
///
/// [parentDir] is a hint that set the parent directory where the files are
/// saved. Whether this is supported or not is implementation specific
@ -11,7 +21,7 @@ abstract class FileDownloader {
/// [shouldNotify] is a hint that suggest whether to notify user about the
/// progress. The actual decision is made by the underlying platform code and
/// is not guaranteed to respect this flag
Future<dynamic> downloadUrl({
Download build({
required String url,
Map<String, String>? headers,
String? mimeType,

View file

@ -1,7 +1,5 @@
abstract class ItemDownloadSuccessfulNotification {
abstract class Notification {
Future<void> notify();
}
abstract class LogSaveSuccessfulNotification {
Future<void> notify();
Future<void> dismiss();
}

View file

@ -3,20 +3,18 @@ import 'package:nc_photos/api/api.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/mobile/platform.dart'
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform;
import 'package:nc_photos/platform/download.dart';
class DownloadFile {
/// Download [file]
///
/// See [FileDownloader.downloadUrl]
Future<dynamic> call(
/// Create a new download but don't start it yet
Download build(
Account account,
File file, {
String? parentDir,
bool? shouldNotify,
}) {
final downloader = platform.FileDownloader();
final url = "${account.url}/${file.path}";
return downloader.downloadUrl(
return platform.DownloadBuilder().build(
url: url,
headers: {
"authorization": Api.getAuthorizationHeaderValue(account),
@ -27,4 +25,20 @@ class DownloadFile {
shouldNotify: shouldNotify,
);
}
/// Download [file]
///
/// See [DownloadBuilder]
Future<dynamic> call(
Account account,
File file, {
String? parentDir,
bool? shouldNotify,
}) =>
build(
account,
file,
parentDir: parentDir,
shouldNotify: shouldNotify,
)();
}

View file

@ -1,18 +1,35 @@
import 'package:http/http.dart' as http;
import 'package:nc_photos/exception.dart';
import 'package:nc_photos/platform/file_downloader.dart' as itf;
import 'package:nc_photos/platform/download.dart' as itf;
import 'package:nc_photos/web/file_saver.dart';
class FileDownloader extends itf.FileDownloader {
class DownloadBuilder extends itf.DownloadBuilder {
@override
downloadUrl({
build({
required String url,
Map<String, String>? headers,
String? mimeType,
required String filename,
String? parentDir,
bool? shouldNotify,
}) async {
}) {
return _WebDownload(
url: url,
headers: headers,
filename: filename,
);
}
}
class _WebDownload extends itf.Download {
_WebDownload({
required this.url,
this.headers,
required this.filename,
});
@override
call() async {
final uri = Uri.parse(url);
final req = http.Request("GET", uri)..headers.addAll(headers ?? {});
final response =
@ -24,4 +41,11 @@ class FileDownloader extends itf.FileDownloader {
final saver = FileSaver();
await saver.saveFile(filename, response.bodyBytes);
}
@override
cancel() => Future.value(false);
final String url;
final Map<String, String>? headers;
final String filename;
}

View file

@ -1,5 +1,5 @@
export 'db_util.dart';
export 'file_downloader.dart';
export 'download.dart';
export 'file_saver.dart';
export 'map_widget.dart';
export 'universal_storage.dart';