mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-14 20:33:41 +00:00
feat: Background download on iOS (#1442)
This commit is contained in:
1 parent
9d3221ebbb
commit
99924296ef
14 files changed
+383
-26
No files matched your search
@@ -99,6 +99,17 @@ void init(String network, String address, String apiToken) {
|
||||
_client = _Client(network, address, apiToken);
|
||||
}
|
||||
|
||||
/// Used by LocationKeepAliveCoordinator to reconcile keep-alive
|
||||
void Function()? _onTaskChanged;
|
||||
|
||||
void setTaskChangedListener(void Function() listener) {
|
||||
_onTaskChanged = listener;
|
||||
}
|
||||
|
||||
void _notifyTaskChanged() {
|
||||
_onTaskChanged?.call();
|
||||
}
|
||||
|
||||
Future<T> _parse<T>(
|
||||
Future<Response> Function() fetch,
|
||||
T Function(dynamic json)? fromJsonT,
|
||||
@@ -130,15 +141,19 @@ Future<ResolveResult> resolve(ResolveTask resolveTask) async {
|
||||
}
|
||||
|
||||
Future<String> createTask(CreateTask createTask) async {
|
||||
return _parse<String>(
|
||||
final result = await _parse<String>(
|
||||
() => _client.dio.post("api/v1/tasks", data: createTask),
|
||||
(data) => data as String);
|
||||
_notifyTaskChanged();
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<List<String>> createTaskBatch(CreateTaskBatch createTaskBatch) async {
|
||||
return _parse<List<String>>(
|
||||
final result = await _parse<List<String>>(
|
||||
() => _client.dio.post("api/v1/tasks/batch", data: createTaskBatch),
|
||||
(data) => (data as List).map((e) => e as String).toList());
|
||||
_notifyTaskChanged();
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> patchTask(String id, ResolveTask patchTask) async {
|
||||
@@ -154,41 +169,47 @@ Future<List<Task>> getTasks(List<Status> statuses) async {
|
||||
}
|
||||
|
||||
Future<void> pauseTask(String id) async {
|
||||
return _parse(() => _client.dio.put("api/v1/tasks/$id/pause"), null);
|
||||
await _parse(() => _client.dio.put("api/v1/tasks/$id/pause"), null);
|
||||
_notifyTaskChanged();
|
||||
}
|
||||
|
||||
Future<void> continueTask(String id) async {
|
||||
return _parse(() => _client.dio.put("api/v1/tasks/$id/continue"), null);
|
||||
await _parse(() => _client.dio.put("api/v1/tasks/$id/continue"), null);
|
||||
_notifyTaskChanged();
|
||||
}
|
||||
|
||||
Future<void> pauseAllTasks(List<String>? ids) async {
|
||||
return _parse(
|
||||
await _parse(
|
||||
() => _client.dio.put("api/v1/tasks/pause", queryParameters: {
|
||||
"id": ids,
|
||||
}),
|
||||
null);
|
||||
_notifyTaskChanged();
|
||||
}
|
||||
|
||||
Future<void> continueAllTasks(List<String>? ids) async {
|
||||
return _parse(
|
||||
await _parse(
|
||||
() => _client.dio.put("api/v1/tasks/continue", queryParameters: {
|
||||
"id": ids,
|
||||
}),
|
||||
null);
|
||||
_notifyTaskChanged();
|
||||
}
|
||||
|
||||
Future<void> deleteTask(String id, bool force) async {
|
||||
return _parse(
|
||||
await _parse(
|
||||
() => _client.dio.delete("api/v1/tasks/$id?force=$force"), null);
|
||||
_notifyTaskChanged();
|
||||
}
|
||||
|
||||
Future<void> deleteTasks(List<String>? ids, bool force) async {
|
||||
return _parse(
|
||||
await _parse(
|
||||
() => _client.dio.delete("api/v1/tasks", queryParameters: {
|
||||
"id": ids,
|
||||
"force": force,
|
||||
}),
|
||||
null);
|
||||
_notifyTaskChanged();
|
||||
}
|
||||
|
||||
Future<DownloaderConfig> getConfig() async {
|
||||
|
||||
@@ -113,6 +113,7 @@ class ExtraConfig {
|
||||
bool notifyWhenNewVersion;
|
||||
bool autoStartTasks;
|
||||
bool desktopNotification;
|
||||
bool backgroundLocationKeepAlive;
|
||||
List<DownloadCategory> downloadCategories;
|
||||
|
||||
ExtraConfigBt bt = ExtraConfigBt();
|
||||
@@ -127,6 +128,7 @@ class ExtraConfig {
|
||||
this.notifyWhenNewVersion = true,
|
||||
this.autoStartTasks = false,
|
||||
this.desktopNotification = true,
|
||||
this.backgroundLocationKeepAlive = false,
|
||||
this.downloadCategories = const [],
|
||||
});
|
||||
|
||||
|
||||
@@ -111,6 +111,8 @@ ExtraConfig _$ExtraConfigFromJson(Map<String, dynamic> json) => ExtraConfig(
|
||||
notifyWhenNewVersion: json['notifyWhenNewVersion'] as bool? ?? true,
|
||||
autoStartTasks: json['autoStartTasks'] as bool? ?? false,
|
||||
desktopNotification: json['desktopNotification'] as bool? ?? true,
|
||||
backgroundLocationKeepAlive:
|
||||
json['backgroundLocationKeepAlive'] as bool? ?? false,
|
||||
downloadCategories: (json['downloadCategories'] as List<dynamic>?)
|
||||
?.map((e) => DownloadCategory.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
@@ -130,6 +132,7 @@ Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
|
||||
'notifyWhenNewVersion': instance.notifyWhenNewVersion,
|
||||
'autoStartTasks': instance.autoStartTasks,
|
||||
'desktopNotification': instance.desktopNotification,
|
||||
'backgroundLocationKeepAlive': instance.backgroundLocationKeepAlive,
|
||||
'downloadCategories':
|
||||
instance.downloadCategories.map((e) => e.toJson()).toList(),
|
||||
'bt': instance.bt.toJson(),
|
||||
|
||||
@@ -32,6 +32,8 @@ import '../../../views/outlined_button_loading.dart';
|
||||
import '../../../views/text_button_loading.dart';
|
||||
import '../../app/controllers/app_controller.dart';
|
||||
import '../controllers/setting_controller.dart';
|
||||
import '../../../services/location_keep_alive.dart';
|
||||
import '../../../services/location_keep_alive_coordinator.dart';
|
||||
|
||||
const _padding = SizedBox(height: 10);
|
||||
final _divider = const Divider().paddingOnly(left: 10, right: 10);
|
||||
@@ -521,6 +523,44 @@ class SettingView extends GetView<SettingController> {
|
||||
);
|
||||
});
|
||||
|
||||
final buildBackgroundLocationKeepAlive = !Platform.isIOS
|
||||
? () => const SizedBox.shrink()
|
||||
: _buildConfigItem(
|
||||
'backgroundLocationKeepAlive',
|
||||
() {
|
||||
return appController
|
||||
.downloaderConfig.value.extra.backgroundLocationKeepAlive
|
||||
? 'on'.tr
|
||||
: 'off'.tr;
|
||||
},
|
||||
(Key key) {
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Switch(
|
||||
value: appController
|
||||
.downloaderConfig.value.extra.backgroundLocationKeepAlive,
|
||||
onChanged: (bool value) async {
|
||||
if (value) {
|
||||
final authorized =
|
||||
await LocationKeepAlive.requestPermission();
|
||||
|
||||
if (!authorized) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
appController.downloaderConfig.update((val) {
|
||||
val!.extra.backgroundLocationKeepAlive = value;
|
||||
});
|
||||
|
||||
await debounceSave();
|
||||
await Get.find<LocationKeepAliveCoordinator>().reconcile();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// http config items start
|
||||
final httpConfig = downloaderCfg.value.protocolConfig.http;
|
||||
final buildHttpUa =
|
||||
@@ -1748,6 +1788,7 @@ class SettingView extends GetView<SettingController> {
|
||||
buildAutoStartup(),
|
||||
buildMenubarMode(),
|
||||
buildDesktopNotification(),
|
||||
buildBackgroundLocationKeepAlive(),
|
||||
]),
|
||||
)),
|
||||
Text('archives'.tr),
|
||||
|
||||
@@ -50,4 +50,5 @@ abstract class TaskListController extends GetxController {
|
||||
tasks.sort(compare);
|
||||
this.tasks.value = tasks;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//for ios
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class LocationKeepAlive {
|
||||
static const _channel = MethodChannel('gopeed/location_keep_alive');
|
||||
|
||||
static Future<bool> requestPermission() async {
|
||||
if (!Platform.isIOS) return false;
|
||||
|
||||
try {
|
||||
final result = await _channel.invokeMethod<bool>('requestPermission');
|
||||
|
||||
return result ?? false;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> start() async {
|
||||
if (!Platform.isIOS) return;
|
||||
|
||||
await _channel.invokeMethod('start');
|
||||
}
|
||||
|
||||
static Future<void> stop() async {
|
||||
if (!Platform.isIOS) return;
|
||||
|
||||
await _channel.invokeMethod('stop');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'dart:async';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../api/api.dart' as api;
|
||||
import '../../../api/model/task.dart';
|
||||
import '../modules/app/controllers/app_controller.dart';
|
||||
import 'location_keep_alive.dart';
|
||||
|
||||
class LocationKeepAliveCoordinator extends GetxService {
|
||||
Timer? _pollTimer;
|
||||
bool _reconciling = false;
|
||||
|
||||
/// Overridable for testing, defaults to reading from AppController.
|
||||
bool Function() keepAliveEnabledProvider = () => Get.find<AppController>()
|
||||
.downloaderConfig
|
||||
.value
|
||||
.extra
|
||||
.backgroundLocationKeepAlive;
|
||||
|
||||
/// Overridable for testing so polling tests don't need to wait 10s.
|
||||
Duration pollInterval = const Duration(seconds: 10);
|
||||
|
||||
bool get _keepAliveEnabled => keepAliveEnabledProvider();
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
api.setTaskChangedListener(() {
|
||||
reconcile();
|
||||
});
|
||||
}
|
||||
|
||||
Future<int> _fetchGlobalRunningCount() async {
|
||||
try {
|
||||
final tasks = await api.getTasks([Status.running]);
|
||||
return tasks.length;
|
||||
} catch (_) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reconcile() async {
|
||||
if (_reconciling) return;
|
||||
_reconciling = true;
|
||||
try {
|
||||
if (!_keepAliveEnabled) {
|
||||
await LocationKeepAlive.stop();
|
||||
return;
|
||||
}
|
||||
final count = await _fetchGlobalRunningCount();
|
||||
if (count < 0) return;
|
||||
if (count > 0) {
|
||||
await LocationKeepAlive.start();
|
||||
} else {
|
||||
await LocationKeepAlive.stop();
|
||||
}
|
||||
} finally {
|
||||
_reconciling = false;
|
||||
}
|
||||
}
|
||||
|
||||
void startPolling() {
|
||||
_pollTimer?.cancel();
|
||||
_pollTimer = Timer.periodic(pollInterval, (_) => reconcile());
|
||||
}
|
||||
|
||||
void stopPolling() {
|
||||
_pollTimer?.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
stopPolling();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
@@ -226,6 +226,7 @@ const enUS = {
|
||||
'ed2kServerMetHint': 'Server.met URL or ed2k serverlist link, one per line',
|
||||
'ed2kNodesDatHint': 'nodes.dat path or URL, one per line',
|
||||
'desktopNotification': 'Desktop Notifications',
|
||||
'backgroundLocationKeepAlive': 'Background Download Keep Alive',
|
||||
'notificationTaskDone': 'Task completed',
|
||||
'notificationTaskError': 'Task error',
|
||||
},
|
||||
|
||||
@@ -221,6 +221,7 @@ const zhCN = {
|
||||
'ed2kServerMetHint': '请输入 Server.met 地址或 ed2k 服务器列表链接,每行一条',
|
||||
'ed2kNodesDatHint': '请输入 nodes.dat 本地路径或 URL,每行一条',
|
||||
'desktopNotification': '桌面通知',
|
||||
'backgroundLocationKeepAlive': '常驻后台',
|
||||
'notificationTaskDone': '任务完成',
|
||||
'notificationTaskError': '任务失败',
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import 'util/package_info.dart';
|
||||
import 'util/scheme_register/scheme_register.dart';
|
||||
import 'util/updater.dart';
|
||||
import 'util/util.dart';
|
||||
import 'app/services/location_keep_alive_coordinator.dart';
|
||||
|
||||
class StartupArgs {
|
||||
static const flagHidden = "hidden";
|
||||
@@ -135,6 +136,17 @@ Future<void> init(StartupArgs args) async {
|
||||
logger.w("auto-start tasks fail", e);
|
||||
}
|
||||
}
|
||||
|
||||
// iOS location keep-alive coordinator
|
||||
if (Util.isIOS()) {
|
||||
try {
|
||||
await Get.putAsync(() async => LocationKeepAliveCoordinator());
|
||||
await Get.find<LocationKeepAliveCoordinator>().reconcile();
|
||||
Get.find<LocationKeepAliveCoordinator>().startPolling();
|
||||
} catch (e) {
|
||||
logger.e("location keep-alive coordinator init fail", e);
|
||||
}
|
||||
}
|
||||
|
||||
() async {
|
||||
if (Util.isDesktop()) {
|
||||
|
||||
Reference in new issue
Block a user