mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-07 17:10:29 +00:00
feat: extension support (#78)
This commit is contained in:
118 files changed
+6971
-780
No files matched your search
@@ -62,3 +62,5 @@ macos/Flutter/GeneratedPluginRegistrant.swift
|
||||
windows/flutter/generated_plugin_registrant.cc
|
||||
windows/flutter/generated_plugin_registrant.h
|
||||
windows/flutter/generated_plugins.cmake
|
||||
|
||||
/extensions
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
@@ -58,7 +58,13 @@
|
||||
"deleteTaskTip": "Keep downloaded files",
|
||||
"delete": "Delete",
|
||||
"newVersionTitle": "Discover new version @version",
|
||||
"newVersionUpdate": "Download Now",
|
||||
"newVersionLater": "Later"
|
||||
"newVersionUpdate": "Update Now",
|
||||
"newVersionLater": "Later",
|
||||
"extensions": "Extensions",
|
||||
"extensionInstallUrl": "Install URL",
|
||||
"extensionInstallSuccess": "Installed successfully",
|
||||
"extensionUpdateSuccess": "Updated successfully",
|
||||
"extensionDelete": "Delete Extension",
|
||||
"extensionAlreadyLatest": "It's already the latest version"
|
||||
}
|
||||
|
||||
@@ -53,6 +53,6 @@
|
||||
"deleteTaskTip": "فایل های دانلود شده را نگه دارد",
|
||||
"delete": "پاک کردن",
|
||||
"newVersionTitle": "@version عنوان: کشف نسخه جدید",
|
||||
"newVersionUpdate": "دریافت اکنون",
|
||||
"newVersionUpdate": "بروزرسانی",
|
||||
"newVersionLater": "بعداً"
|
||||
}
|
||||
@@ -56,6 +56,6 @@
|
||||
"deleteTaskTip": "ダウンロードしたファイルを保持",
|
||||
"delete": "削除",
|
||||
"newVersionTitle": "新しいバージョン @version を発見する",
|
||||
"newVersionUpdate": "今すぐダウンロード",
|
||||
"newVersionUpdate": "アップデート",
|
||||
"newVersionLater": "後で"
|
||||
}
|
||||
@@ -56,6 +56,6 @@
|
||||
"deleteTaskTip": "Сохранить загруженные файлы",
|
||||
"delete": "Удалить",
|
||||
"newVersionTitle": "Обнаружена новая версия @version",
|
||||
"newVersionUpdate": "Скачать сейчас",
|
||||
"newVersionUpdate": "Обновить",
|
||||
"newVersionLater": "позже"
|
||||
}
|
||||
@@ -58,6 +58,12 @@
|
||||
"deleteTaskTip": "保留已下载的文件",
|
||||
"delete": "删除",
|
||||
"newVersionTitle": "发现新版本 @version",
|
||||
"newVersionUpdate": "立即下载",
|
||||
"newVersionLater": "稍后再说"
|
||||
"newVersionUpdate": "立即更新",
|
||||
"newVersionLater": "稍后再说",
|
||||
"extensions": "扩展",
|
||||
"extensionInstallUrl": "安装链接",
|
||||
"extensionInstallSuccess": "安装成功",
|
||||
"extensionUpdateSuccess": "更新成功",
|
||||
"extensionDelete": "删除扩展",
|
||||
"extensionAlreadyLatest": "已经是最新版本"
|
||||
}
|
||||
@@ -3,14 +3,19 @@ import 'dart:io';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio/io.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'model/resolve_result.dart';
|
||||
import 'package:gopeed/api/model/extension.dart';
|
||||
import 'package:gopeed/api/model/install_extension.dart';
|
||||
import 'package:gopeed/api/model/switch_extension.dart';
|
||||
|
||||
import '../util/util.dart';
|
||||
import 'model/create_task.dart';
|
||||
import 'model/downloader_config.dart';
|
||||
import 'model/request.dart';
|
||||
import 'model/resolve_result.dart';
|
||||
import 'model/result.dart';
|
||||
import 'model/task.dart';
|
||||
|
||||
import 'model/downloader_config.dart';
|
||||
import 'model/update_extension_settings.dart';
|
||||
import 'model/update_check_extension_resp.dart';
|
||||
|
||||
class _Client {
|
||||
static _Client? _instance;
|
||||
@@ -62,6 +67,12 @@ class _Client {
|
||||
}
|
||||
}
|
||||
|
||||
class TimeoutException implements Exception {
|
||||
final String message;
|
||||
|
||||
TimeoutException(this.message);
|
||||
}
|
||||
|
||||
late _Client _client;
|
||||
|
||||
void init(String network, String address, String apiToken) {
|
||||
@@ -85,7 +96,7 @@ Future<T> _parse<T>(
|
||||
if (e.type == DioExceptionType.sendTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout ||
|
||||
e.type == DioExceptionType.connectionTimeout) {
|
||||
throw Exception(Result(code: 1000, msg: "request timeout"));
|
||||
throw TimeoutException("request timeout");
|
||||
}
|
||||
throw Exception(Result(code: 1000, msg: e.message));
|
||||
}
|
||||
@@ -105,8 +116,8 @@ Future<String> createTask(CreateTask createTask) async {
|
||||
|
||||
Future<List<Task>> getTasks(List<Status> statuses) async {
|
||||
return _parse<List<Task>>(
|
||||
() => _client.dio
|
||||
.get("/api/v1/tasks?status=${statuses.map((e) => e.name).join(",")}"),
|
||||
() => _client.dio.get(
|
||||
"/api/v1/tasks?${statuses.map((e) => "status=${e.name}").join("&")}"),
|
||||
(data) => (data as List).map((e) => Task.fromJson(e)).toList());
|
||||
}
|
||||
|
||||
@@ -140,6 +151,47 @@ Future<void> putConfig(DownloaderConfig config) async {
|
||||
return _parse(() => _client.dio.put("/api/v1/config", data: config), null);
|
||||
}
|
||||
|
||||
Future<void> installExtension(InstallExtension installExtension) async {
|
||||
return _parse(
|
||||
() => _client.dio.post("/api/v1/extensions", data: installExtension),
|
||||
null);
|
||||
}
|
||||
|
||||
Future<List<Extension>> getExtensions() async {
|
||||
return _parse<List<Extension>>(() => _client.dio.get("/api/v1/extensions"),
|
||||
(data) => (data as List).map((e) => Extension.fromJson(e)).toList());
|
||||
}
|
||||
|
||||
Future<void> updateExtensionSettings(
|
||||
String identity, UpdateExtensionSettings updateExtensionSettings) async {
|
||||
return _parse(
|
||||
() => _client.dio.put("/api/v1/extensions/$identity/settings",
|
||||
data: updateExtensionSettings),
|
||||
null);
|
||||
}
|
||||
|
||||
Future<void> switchExtension(
|
||||
String identity, SwitchExtension switchExtension) async {
|
||||
return _parse(
|
||||
() => _client.dio
|
||||
.put("/api/v1/extensions/$identity/switch", data: switchExtension),
|
||||
null);
|
||||
}
|
||||
|
||||
Future<void> deleteExtension(String identity) async {
|
||||
return _parse(() => _client.dio.delete("/api/v1/extensions/$identity"), null);
|
||||
}
|
||||
|
||||
Future<UpdateCheckExtensionResp> upgradeCheckExtension(String identity) async {
|
||||
return _parse(() => _client.dio.get("/api/v1/extensions/$identity/update"),
|
||||
(data) => UpdateCheckExtensionResp.fromJson(data));
|
||||
}
|
||||
|
||||
Future<void> updateExtension(String identity) async {
|
||||
return _parse(
|
||||
() => _client.dio.post("/api/v1/extensions/$identity/update"), null);
|
||||
}
|
||||
|
||||
Future<Response<String>> proxyRequest<T>(String uri,
|
||||
{data, Options? options}) async {
|
||||
options ??= Options();
|
||||
@@ -152,3 +204,7 @@ Future<Response<String>> proxyRequest<T>(String uri,
|
||||
data: data,
|
||||
options: options);
|
||||
}
|
||||
|
||||
String join(String path) {
|
||||
return "${_client.dio.options.baseUrl}/${Util.cleanPath(path)}";
|
||||
}
|
||||
@@ -9,12 +9,12 @@ part 'create_task.g.dart';
|
||||
class CreateTask {
|
||||
String? rid;
|
||||
Request? req;
|
||||
Options? opts;
|
||||
Options? opt;
|
||||
|
||||
CreateTask({
|
||||
this.rid,
|
||||
this.req,
|
||||
this.opts,
|
||||
this.opt,
|
||||
});
|
||||
|
||||
factory CreateTask.fromJson(
|
||||
|
||||
@@ -11,9 +11,9 @@ CreateTask _$CreateTaskFromJson(Map<String, dynamic> json) => CreateTask(
|
||||
req: json['req'] == null
|
||||
? null
|
||||
: Request.fromJson(json['req'] as Map<String, dynamic>),
|
||||
opts: json['opts'] == null
|
||||
opt: json['opt'] == null
|
||||
? null
|
||||
: Options.fromJson(json['opts'] as Map<String, dynamic>),
|
||||
: Options.fromJson(json['opt'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CreateTaskToJson(CreateTask instance) {
|
||||
@@ -27,6 +27,6 @@ Map<String, dynamic> _$CreateTaskToJson(CreateTask instance) {
|
||||
|
||||
writeNotNull('rid', instance.rid);
|
||||
writeNotNull('req', instance.req?.toJson());
|
||||
writeNotNull('opts', instance.opts?.toJson());
|
||||
writeNotNull('opt', instance.opt?.toJson());
|
||||
return val;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'extension.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class Extension {
|
||||
String identity;
|
||||
String name;
|
||||
String author;
|
||||
String title;
|
||||
String description;
|
||||
String icon;
|
||||
String version;
|
||||
String homepage;
|
||||
Repository? repository;
|
||||
List<Setting>? settings;
|
||||
bool disabled;
|
||||
bool devMode;
|
||||
|
||||
Extension({
|
||||
required this.identity,
|
||||
required this.name,
|
||||
required this.author,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.icon,
|
||||
required this.version,
|
||||
required this.homepage,
|
||||
required this.repository,
|
||||
required this.disabled,
|
||||
required this.devMode,
|
||||
});
|
||||
|
||||
factory Extension.fromJson(Map<String, dynamic> json) =>
|
||||
_$ExtensionFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$ExtensionToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class Repository {
|
||||
String url;
|
||||
String directory;
|
||||
|
||||
Repository({
|
||||
required this.url,
|
||||
required this.directory,
|
||||
});
|
||||
|
||||
factory Repository.fromJson(Map<String, dynamic> json) =>
|
||||
_$RepositoryFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$RepositoryToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class Setting {
|
||||
String name;
|
||||
String title;
|
||||
String description;
|
||||
bool required;
|
||||
SettingType type;
|
||||
Object? value;
|
||||
List<Option>? options;
|
||||
|
||||
Setting({
|
||||
required this.name,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.required,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
factory Setting.fromJson(Map<String, dynamic> json) =>
|
||||
_$SettingFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$SettingToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class Option {
|
||||
String label;
|
||||
Object value;
|
||||
|
||||
Option({
|
||||
required this.label,
|
||||
required this.value,
|
||||
});
|
||||
|
||||
factory Option.fromJson(Map<String, dynamic> json) => _$OptionFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$OptionToJson(this);
|
||||
}
|
||||
|
||||
enum SettingType {
|
||||
string,
|
||||
number,
|
||||
boolean,
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'extension.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Extension _$ExtensionFromJson(Map<String, dynamic> json) => Extension(
|
||||
identity: json['identity'] as String,
|
||||
name: json['name'] as String,
|
||||
author: json['author'] as String,
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String,
|
||||
icon: json['icon'] as String,
|
||||
version: json['version'] as String,
|
||||
homepage: json['homepage'] as String,
|
||||
repository: json['repository'] == null
|
||||
? null
|
||||
: Repository.fromJson(json['repository'] as Map<String, dynamic>),
|
||||
disabled: json['disabled'] as bool,
|
||||
devMode: json['devMode'] as bool,
|
||||
)..settings = (json['settings'] as List<dynamic>?)
|
||||
?.map((e) => Setting.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
Map<String, dynamic> _$ExtensionToJson(Extension instance) {
|
||||
final val = <String, dynamic>{
|
||||
'identity': instance.identity,
|
||||
'name': instance.name,
|
||||
'author': instance.author,
|
||||
'title': instance.title,
|
||||
'description': instance.description,
|
||||
'icon': instance.icon,
|
||||
'version': instance.version,
|
||||
'homepage': instance.homepage,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('repository', instance.repository?.toJson());
|
||||
writeNotNull('settings', instance.settings?.map((e) => e.toJson()).toList());
|
||||
val['disabled'] = instance.disabled;
|
||||
val['devMode'] = instance.devMode;
|
||||
return val;
|
||||
}
|
||||
|
||||
Repository _$RepositoryFromJson(Map<String, dynamic> json) => Repository(
|
||||
url: json['url'] as String,
|
||||
directory: json['directory'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RepositoryToJson(Repository instance) =>
|
||||
<String, dynamic>{
|
||||
'url': instance.url,
|
||||
'directory': instance.directory,
|
||||
};
|
||||
|
||||
Setting _$SettingFromJson(Map<String, dynamic> json) => Setting(
|
||||
name: json['name'] as String,
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String,
|
||||
required: json['required'] as bool,
|
||||
type: $enumDecode(_$SettingTypeEnumMap, json['type']),
|
||||
)
|
||||
..value = json['value']
|
||||
..options = (json['options'] as List<dynamic>?)
|
||||
?.map((e) => Option.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
Map<String, dynamic> _$SettingToJson(Setting instance) {
|
||||
final val = <String, dynamic>{
|
||||
'name': instance.name,
|
||||
'title': instance.title,
|
||||
'description': instance.description,
|
||||
'required': instance.required,
|
||||
'type': _$SettingTypeEnumMap[instance.type]!,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('value', instance.value);
|
||||
writeNotNull('options', instance.options);
|
||||
return val;
|
||||
}
|
||||
|
||||
const _$SettingTypeEnumMap = {
|
||||
SettingType.string: 'string',
|
||||
SettingType.number: 'number',
|
||||
SettingType.boolean: 'boolean',
|
||||
};
|
||||
|
||||
Option _$OptionFromJson(Map<String, dynamic> json) => Option(
|
||||
label: json['label'] as String,
|
||||
value: json['value'] as Object,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$OptionToJson(Option instance) => <String, dynamic>{
|
||||
'label': instance.label,
|
||||
'value': instance.value,
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'install_extension.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class InstallExtension {
|
||||
bool devMode;
|
||||
String url;
|
||||
|
||||
InstallExtension({
|
||||
this.devMode = false,
|
||||
required this.url,
|
||||
});
|
||||
|
||||
factory InstallExtension.fromJson(Map<String, dynamic> json) =>
|
||||
_$InstallExtensionFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$InstallExtensionToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'install_extension.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
InstallExtension _$InstallExtensionFromJson(Map<String, dynamic> json) =>
|
||||
InstallExtension(
|
||||
devMode: json['devMode'] as bool? ?? false,
|
||||
url: json['url'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$InstallExtensionToJson(InstallExtension instance) =>
|
||||
<String, dynamic>{
|
||||
'devMode': instance.devMode,
|
||||
'url': instance.url,
|
||||
};
|
||||
@@ -8,12 +8,11 @@ part 'meta.g.dart';
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class Meta {
|
||||
Request req;
|
||||
Resource res;
|
||||
Resource? res;
|
||||
Options opts;
|
||||
|
||||
Meta({
|
||||
required this.req,
|
||||
required this.res,
|
||||
required this.opts,
|
||||
});
|
||||
|
||||
|
||||
@@ -8,12 +8,23 @@ part of 'meta.dart';
|
||||
|
||||
Meta _$MetaFromJson(Map<String, dynamic> json) => Meta(
|
||||
req: Request.fromJson(json['req'] as Map<String, dynamic>),
|
||||
res: Resource.fromJson(json['res'] as Map<String, dynamic>),
|
||||
opts: Options.fromJson(json['opts'] as Map<String, dynamic>),
|
||||
);
|
||||
)..res = json['res'] == null
|
||||
? null
|
||||
: Resource.fromJson(json['res'] as Map<String, dynamic>);
|
||||
|
||||
Map<String, dynamic> _$MetaToJson(Meta instance) => <String, dynamic>{
|
||||
'req': instance.req.toJson(),
|
||||
'res': instance.res.toJson(),
|
||||
'opts': instance.opts.toJson(),
|
||||
};
|
||||
Map<String, dynamic> _$MetaToJson(Meta instance) {
|
||||
final val = <String, dynamic>{
|
||||
'req': instance.req.toJson(),
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('res', instance.res?.toJson());
|
||||
val['opts'] = instance.opts.toJson();
|
||||
return val;
|
||||
}
|
||||
@@ -10,7 +10,7 @@ class ResolveResult {
|
||||
Resource res;
|
||||
|
||||
ResolveResult({
|
||||
required this.id,
|
||||
this.id = "",
|
||||
required this.res,
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ part of 'resolve_result.dart';
|
||||
|
||||
ResolveResult _$ResolveResultFromJson(Map<String, dynamic> json) =>
|
||||
ResolveResult(
|
||||
id: json['id'] as String,
|
||||
id: json['id'] as String? ?? "",
|
||||
res: Resource.fromJson(json['res'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:gopeed/api/model/request.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'resource.g.dart';
|
||||
@@ -7,17 +8,15 @@ class Resource {
|
||||
String name;
|
||||
int size;
|
||||
bool range;
|
||||
String rootDir;
|
||||
List<FileInfo> files;
|
||||
String hash;
|
||||
|
||||
Resource(
|
||||
{required this.name,
|
||||
required this.size,
|
||||
required this.range,
|
||||
required this.rootDir,
|
||||
{this.name = "",
|
||||
this.size = 0,
|
||||
this.range = false,
|
||||
required this.files,
|
||||
required this.hash});
|
||||
this.hash = ""});
|
||||
|
||||
factory Resource.fromJson(Map<String, dynamic> json) =>
|
||||
_$ResourceFromJson(json);
|
||||
@@ -25,18 +24,18 @@ class Resource {
|
||||
Map<String, dynamic> toJson() => _$ResourceToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class FileInfo {
|
||||
// String rootDir;
|
||||
String path;
|
||||
String name;
|
||||
int size;
|
||||
Request? req;
|
||||
|
||||
FileInfo({
|
||||
// required this.rootDir,
|
||||
required this.path,
|
||||
this.path = "",
|
||||
required this.name,
|
||||
required this.size,
|
||||
this.size = 0,
|
||||
this.req,
|
||||
});
|
||||
|
||||
factory FileInfo.fromJson(Map<String, dynamic> json) =>
|
||||
|
||||
@@ -7,33 +7,45 @@ part of 'resource.dart';
|
||||
// **************************************************************************
|
||||
|
||||
Resource _$ResourceFromJson(Map<String, dynamic> json) => Resource(
|
||||
name: json['name'] as String,
|
||||
size: json['size'] as int,
|
||||
range: json['range'] as bool,
|
||||
rootDir: json['rootDir'] as String,
|
||||
name: json['name'] as String? ?? "",
|
||||
size: json['size'] as int? ?? 0,
|
||||
range: json['range'] as bool? ?? false,
|
||||
files: (json['files'] as List<dynamic>)
|
||||
.map((e) => FileInfo.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
hash: json['hash'] as String,
|
||||
hash: json['hash'] as String? ?? "",
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ResourceToJson(Resource instance) => <String, dynamic>{
|
||||
'name': instance.name,
|
||||
'size': instance.size,
|
||||
'range': instance.range,
|
||||
'rootDir': instance.rootDir,
|
||||
'files': instance.files.map((e) => e.toJson()).toList(),
|
||||
'hash': instance.hash,
|
||||
};
|
||||
|
||||
FileInfo _$FileInfoFromJson(Map<String, dynamic> json) => FileInfo(
|
||||
path: json['path'] as String,
|
||||
path: json['path'] as String? ?? "",
|
||||
name: json['name'] as String,
|
||||
size: json['size'] as int,
|
||||
size: json['size'] as int? ?? 0,
|
||||
req: json['req'] == null
|
||||
? null
|
||||
: Request.fromJson(json['req'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$FileInfoToJson(FileInfo instance) => <String, dynamic>{
|
||||
'path': instance.path,
|
||||
'name': instance.name,
|
||||
'size': instance.size,
|
||||
};
|
||||
Map<String, dynamic> _$FileInfoToJson(FileInfo instance) {
|
||||
final val = <String, dynamic>{
|
||||
'path': instance.path,
|
||||
'name': instance.name,
|
||||
'size': instance.size,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('req', instance.req?.toJson());
|
||||
return val;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'switch_extension.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class SwitchExtension {
|
||||
bool status;
|
||||
|
||||
SwitchExtension({
|
||||
required this.status,
|
||||
});
|
||||
|
||||
factory SwitchExtension.fromJson(Map<String, dynamic> json) =>
|
||||
_$SwitchExtensionFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$SwitchExtensionToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'switch_extension.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
SwitchExtension _$SwitchExtensionFromJson(Map<String, dynamic> json) =>
|
||||
SwitchExtension(
|
||||
status: json['status'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SwitchExtensionToJson(SwitchExtension instance) =>
|
||||
<String, dynamic>{
|
||||
'status': instance.status,
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'update_check_extension_resp.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class UpdateCheckExtensionResp {
|
||||
String newVersion;
|
||||
|
||||
UpdateCheckExtensionResp({
|
||||
required this.newVersion,
|
||||
});
|
||||
|
||||
factory UpdateCheckExtensionResp.fromJson(Map<String, dynamic> json) =>
|
||||
_$UpdateCheckExtensionRespFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$UpdateCheckExtensionRespToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'update_check_extension_resp.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
UpdateCheckExtensionResp _$UpdateCheckExtensionRespFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
UpdateCheckExtensionResp(
|
||||
newVersion: json['newVersion'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$UpdateCheckExtensionRespToJson(
|
||||
UpdateCheckExtensionResp instance) =>
|
||||
<String, dynamic>{
|
||||
'newVersion': instance.newVersion,
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'update_extension_settings.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class UpdateExtensionSettings {
|
||||
Map<String, dynamic> settings;
|
||||
|
||||
UpdateExtensionSettings({
|
||||
required this.settings,
|
||||
});
|
||||
|
||||
factory UpdateExtensionSettings.fromJson(Map<String, dynamic> json) =>
|
||||
_$UpdateExtensionSettingsFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$UpdateExtensionSettingsToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'update_extension_settings.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
UpdateExtensionSettings _$UpdateExtensionSettingsFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
UpdateExtensionSettings(
|
||||
settings: json['settings'] as Map<String, dynamic>,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$UpdateExtensionSettingsToJson(
|
||||
UpdateExtensionSettings instance) =>
|
||||
<String, dynamic>{
|
||||
'settings': instance.settings,
|
||||
};
|
||||
@@ -334,7 +334,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
}
|
||||
final extra = config.extra;
|
||||
if (extra.themeMode.isEmpty) {
|
||||
extra.themeMode = ThemeMode.system.name;
|
||||
extra.themeMode = ThemeMode.dark.name;
|
||||
}
|
||||
if (extra.locale.isEmpty) {
|
||||
final systemLocale = getLocaleKey(PlatformDispatcher.instance.locale);
|
||||
|
||||
@@ -8,8 +8,8 @@ class CreateController extends GetxController
|
||||
with GetSingleTickerProviderStateMixin {
|
||||
// final files = [].obs;
|
||||
final RxList fileInfos = [].obs;
|
||||
final RxList selectedIndexes = [].obs;
|
||||
final RxList openedFolders = [].obs;
|
||||
final selectedIndexes = <int>[].obs;
|
||||
final isResolving = false.obs;
|
||||
final showAdvanced = false.obs;
|
||||
late TabController advancedTabController;
|
||||
|
||||
@@ -4,13 +4,16 @@ import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../../api/model/resolve_result.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'package:rounded_loading_button/rounded_loading_button.dart';
|
||||
|
||||
import '../../../../api/api.dart';
|
||||
import '../../../../api/model/create_task.dart';
|
||||
import '../../../../api/model/options.dart';
|
||||
import '../../../../api/model/request.dart';
|
||||
import '../../../../api/model/resolve_result.dart';
|
||||
import '../../../../api/model/resource.dart';
|
||||
import '../../../../util/input_formatter.dart';
|
||||
import '../../../../util/message.dart';
|
||||
import '../../../../util/util.dart';
|
||||
@@ -37,30 +40,31 @@ class CreateView extends GetView<CreateController> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String? filePath = Get.rootDelegate.arguments();
|
||||
if (_urlController.text.isEmpty && filePath != null) {
|
||||
_urlController.text = filePath;
|
||||
_urlController.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: _urlController.text.length));
|
||||
}
|
||||
if (_urlController.text.isEmpty) {
|
||||
if (filePath?.isNotEmpty ?? false) {
|
||||
// get file path from route arguments
|
||||
_urlController.text = filePath!;
|
||||
_urlController.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: _urlController.text.length));
|
||||
} else {
|
||||
// read clipboard
|
||||
Clipboard.getData('text/plain').then((value) {
|
||||
if (value?.text?.isNotEmpty ?? false) {
|
||||
if (_availableSchemes
|
||||
.where((e) =>
|
||||
value!.text!.startsWith(e) ||
|
||||
value.text!.startsWith(e.toUpperCase()))
|
||||
.isNotEmpty) {
|
||||
_urlController.text = value!.text!;
|
||||
_urlController.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: _urlController.text.length));
|
||||
return;
|
||||
}
|
||||
|
||||
// if no file path, read from clipboard
|
||||
if (filePath?.isEmpty ?? true) {
|
||||
Clipboard.getData('text/plain').then((value) {
|
||||
if (value?.text?.isNotEmpty ?? false) {
|
||||
if (_availableSchemes
|
||||
.where((e) =>
|
||||
value!.text!.startsWith(e) ||
|
||||
value.text!.startsWith(e.toUpperCase()))
|
||||
.isNotEmpty) {
|
||||
_urlController.text = value!.text!;
|
||||
_urlController.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: _urlController.text.length));
|
||||
return;
|
||||
recognizeMagnetUri(value!.text!);
|
||||
}
|
||||
|
||||
recognizeMagnetUri(value!.text!);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
@@ -266,29 +270,29 @@ class CreateView extends GetView<CreateController> {
|
||||
try {
|
||||
_confirmController.start();
|
||||
if (_resolveFormKey.currentState!.validate()) {
|
||||
Object? extra;
|
||||
final submitUrl = Util.isWeb() && controller.fileDataUri.isNotEmpty
|
||||
? controller.fileDataUri.value
|
||||
: _urlController.text;
|
||||
if (controller.showAdvanced.value) {
|
||||
final u = Uri.parse(submitUrl);
|
||||
if (u.scheme.startsWith("http")) {
|
||||
extra = ReqExtraHttp()
|
||||
..header = {
|
||||
"User-Agent": _httpUaController.text,
|
||||
"Cookie": _httpCookieController.text,
|
||||
"Referer": _httpRefererController.text,
|
||||
};
|
||||
} else {
|
||||
extra = ReqExtraBt()
|
||||
..trackers = Util.textToLines(_btTrackerController.text);
|
||||
}
|
||||
// check if is multi line urls
|
||||
final urls = Util.textToLines(_urlController.text);
|
||||
ResolveResult rr;
|
||||
bool isMuiltiLine = false;
|
||||
if (urls.length > 1) {
|
||||
isMuiltiLine = true;
|
||||
rr = ResolveResult(
|
||||
res: Resource(
|
||||
files: urls
|
||||
.map((u) => FileInfo(
|
||||
name: u,
|
||||
req: Request(url: u, extra: parseReqExtra(u))))
|
||||
.toList()));
|
||||
} else {
|
||||
final submitUrl = Util.isWeb() && controller.fileDataUri.isNotEmpty
|
||||
? controller.fileDataUri.value
|
||||
: _urlController.text;
|
||||
rr = await resolve(Request(
|
||||
url: submitUrl,
|
||||
extra: parseReqExtra(_urlController.text),
|
||||
));
|
||||
}
|
||||
final rr = await resolve(Request(
|
||||
url: submitUrl,
|
||||
extra: extra,
|
||||
));
|
||||
await _showResolveDialog(rr);
|
||||
await _showResolveDialog(rr, isMuiltiLine);
|
||||
}
|
||||
} catch (e) {
|
||||
showErrorMessage(e);
|
||||
@@ -298,6 +302,25 @@ class CreateView extends GetView<CreateController> {
|
||||
}
|
||||
}
|
||||
|
||||
Object? parseReqExtra(String url) {
|
||||
Object? reqExtra;
|
||||
if (controller.showAdvanced.value) {
|
||||
final u = Uri.parse(_urlController.text);
|
||||
if (u.scheme.startsWith("http")) {
|
||||
reqExtra = ReqExtraHttp()
|
||||
..header = {
|
||||
"User-Agent": _httpUaController.text,
|
||||
"Cookie": _httpCookieController.text,
|
||||
"Referer": _httpRefererController.text,
|
||||
};
|
||||
} else {
|
||||
reqExtra = ReqExtraBt()
|
||||
..trackers = Util.textToLines(_btTrackerController.text);
|
||||
}
|
||||
}
|
||||
return reqExtra;
|
||||
}
|
||||
|
||||
String _hitText() {
|
||||
return 'downloadLinkHit'.trParams({
|
||||
'append':
|
||||
@@ -305,8 +328,7 @@ class CreateView extends GetView<CreateController> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _showResolveDialog(ResolveResult rr) async {
|
||||
final files = rr.res.files;
|
||||
Future<void> _showResolveDialog(ResolveResult rr, bool isMuiltiLine) async {
|
||||
final appController = Get.find<AppController>();
|
||||
|
||||
final createFormKey = GlobalKey<FormState>();
|
||||
@@ -319,6 +341,7 @@ class CreateView extends GetView<CreateController> {
|
||||
context: Get.context!,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => AlertDialog(
|
||||
title: rr.res.name.isEmpty ? null : Text(rr.res.name),
|
||||
content: Builder(
|
||||
builder: (context) {
|
||||
// Get available height and width of the build area of this widget. Make a choice depending on the size.
|
||||
@@ -333,7 +356,7 @@ class CreateView extends GetView<CreateController> {
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(child: FileListView(files: files)),
|
||||
Expanded(child: FileListView(files: rr.res.files)),
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
@@ -391,19 +414,37 @@ class CreateView extends GetView<CreateController> {
|
||||
showMessage('tip'.tr, 'noFileSelected'.tr);
|
||||
return;
|
||||
}
|
||||
Object? optExtra = connectionsController.text.isEmpty
|
||||
? null
|
||||
: (OptsExtraHttp()
|
||||
..connections =
|
||||
int.parse(connectionsController.text));
|
||||
if (createFormKey.currentState!.validate()) {
|
||||
await createTask(CreateTask(
|
||||
rid: rr.id,
|
||||
opts: Options(
|
||||
name: nameController.text,
|
||||
path: pathController.text,
|
||||
selectFiles:
|
||||
controller.selectedIndexes.cast<int>(),
|
||||
extra: connectionsController.text.isEmpty
|
||||
? null
|
||||
: (OptsExtraHttp()
|
||||
..connections = int.parse(
|
||||
connectionsController.text)))));
|
||||
if (rr.id.isEmpty) {
|
||||
// create task batch, there has two ways to batch create task
|
||||
// 1. from multi line urls
|
||||
// 2. from extension resolve result
|
||||
await Future.wait(
|
||||
controller.selectedIndexes.map((index) {
|
||||
final file = rr.res.files[index];
|
||||
return createTask(CreateTask(
|
||||
req: file.req!,
|
||||
opt: Options(
|
||||
name: isMuiltiLine ? "" : file.name,
|
||||
path: path.join(
|
||||
pathController.text, rr.res.name),
|
||||
selectFiles: [],
|
||||
extra: optExtra)));
|
||||
}));
|
||||
} else {
|
||||
await createTask(CreateTask(
|
||||
rid: rr.id,
|
||||
opt: Options(
|
||||
name: nameController.text,
|
||||
path: pathController.text,
|
||||
selectFiles: controller.selectedIndexes,
|
||||
extra: optExtra)));
|
||||
}
|
||||
Get.back();
|
||||
Get.rootDelegate.offNamed(Routes.TASK);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/extension_controller.dart';
|
||||
|
||||
class ExtensionBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<ExtensionController>(
|
||||
() => ExtensionController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/api/api.dart';
|
||||
|
||||
import '../../../../api/model/extension.dart';
|
||||
|
||||
class ExtensionController extends GetxController {
|
||||
final extensions = <Extension>[].obs;
|
||||
final updateFlags = <String, String>{}.obs;
|
||||
final devMode = false.obs;
|
||||
var _devModeCount = 0;
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
super.onInit();
|
||||
await load();
|
||||
checkUpdate();
|
||||
}
|
||||
|
||||
Future<void> load() async {
|
||||
extensions.value = await getExtensions();
|
||||
}
|
||||
|
||||
Future<void> checkUpdate() async {
|
||||
for (final ext in extensions) {
|
||||
final resp = await upgradeCheckExtension(ext.identity);
|
||||
if (resp.newVersion.isNotEmpty) {
|
||||
updateFlags[ext.identity] = resp.newVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to open dev mode when install button is clicked 5 times in 2 seconds
|
||||
void tryOpenDevMode() {
|
||||
if (_devModeCount == 0) {
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
if (devMode.value) return;
|
||||
devMode.value = false;
|
||||
_devModeCount = 0;
|
||||
});
|
||||
}
|
||||
_devModeCount++;
|
||||
if (_devModeCount >= 5) {
|
||||
devMode.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:badges/badges.dart' as badges;
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/app/views/text_button_loading.dart';
|
||||
import '../../../../api/model/update_extension_settings.dart';
|
||||
import '../../../../util/mac_secure_util.dart';
|
||||
import '../../../../util/message.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:rounded_loading_button/rounded_loading_button.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../../api/api.dart';
|
||||
import '../../../../api/model/extension.dart';
|
||||
import '../../../../api/model/install_extension.dart';
|
||||
import '../../../../api/model/switch_extension.dart';
|
||||
import '../../../../util/util.dart';
|
||||
import '../../../views/icon_button_loading.dart';
|
||||
import '../controllers/extension_controller.dart';
|
||||
|
||||
class ExtensionView extends GetView<ExtensionController> {
|
||||
ExtensionView({Key? key}) : super(key: key);
|
||||
|
||||
final _installUrlController = TextEditingController();
|
||||
final _installBtnController = IconButtonLoadingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _installUrlController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'extensionInstallUrl'.tr,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
IconButtonLoading(
|
||||
controller: _installBtnController,
|
||||
onPressed: () async {
|
||||
if (_installUrlController.text.isEmpty) {
|
||||
controller.tryOpenDevMode();
|
||||
return;
|
||||
}
|
||||
_installBtnController.start();
|
||||
try {
|
||||
await installExtension(InstallExtension(
|
||||
url: _installUrlController.text));
|
||||
Get.snackbar('tip'.tr, 'extensionInstallSuccess'.tr);
|
||||
await controller.load();
|
||||
} catch (e) {
|
||||
showErrorMessage(e);
|
||||
} finally {
|
||||
_installBtnController.stop();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.download)),
|
||||
controller.devMode.value && Util.isDesktop()
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.folder_open),
|
||||
onPressed: () async {
|
||||
var dir =
|
||||
await FilePicker.platform.getDirectoryPath();
|
||||
if (dir != null) {
|
||||
MacSecureUtil.saveBookmark(dir);
|
||||
try {
|
||||
await installExtension(
|
||||
InstallExtension(devMode: true, url: dir));
|
||||
Get.snackbar(
|
||||
'tip'.tr, 'extensionInstallSuccess'.tr);
|
||||
await controller.load();
|
||||
} catch (e) {
|
||||
showErrorMessage(e);
|
||||
}
|
||||
}
|
||||
})
|
||||
: Container()
|
||||
],
|
||||
)),
|
||||
const SizedBox(height: 32),
|
||||
Expanded(
|
||||
child: Obx(() => ListView.builder(
|
||||
itemCount: controller.extensions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final extension = controller.extensions[index];
|
||||
return SizedBox(
|
||||
height: 112,
|
||||
child: Card(
|
||||
elevation: 4.0,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListTile(
|
||||
leading: extension.icon.isEmpty
|
||||
? Image.asset(
|
||||
"assets/extension/default_icon.png",
|
||||
width: 48,
|
||||
height: 48,
|
||||
)
|
||||
: Util.isWeb()
|
||||
? Image.network(join(
|
||||
'/fs/extensions/${extension.identity}/${extension.icon}'))
|
||||
: Image.file(
|
||||
File(path.join(
|
||||
Util.getStorageDir(),
|
||||
"extensions",
|
||||
extension.identity,
|
||||
extension.icon)),
|
||||
width: 48,
|
||||
height: 48,
|
||||
),
|
||||
trailing: Switch(
|
||||
value: !extension.disabled,
|
||||
onChanged: (value) async {
|
||||
try {
|
||||
await switchExtension(
|
||||
extension.identity,
|
||||
SwitchExtension(status: value));
|
||||
await controller.load();
|
||||
} catch (e) {
|
||||
showErrorMessage(e);
|
||||
}
|
||||
},
|
||||
),
|
||||
title: Row(
|
||||
children: () {
|
||||
final list = [
|
||||
Text(extension.title),
|
||||
const SizedBox(width: 8),
|
||||
Chip(
|
||||
label: Text('v${extension.version}'),
|
||||
)
|
||||
];
|
||||
if (extension.devMode) {
|
||||
list.add(const SizedBox(width: 8));
|
||||
list.add(Chip(
|
||||
label: Text('dev'.tr),
|
||||
backgroundColor: Colors.redAccent,
|
||||
));
|
||||
}
|
||||
return list;
|
||||
}(),
|
||||
),
|
||||
subtitle: Text(extension.description),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
extension.homepage.isNotEmpty == true
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
launchUrl(
|
||||
Uri.parse(extension.homepage),
|
||||
mode: LaunchMode
|
||||
.externalApplication);
|
||||
},
|
||||
icon: const Icon(Icons.home))
|
||||
: null,
|
||||
extension.repository?.url.isNotEmpty == true
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
launchUrl(
|
||||
Uri.parse(
|
||||
extension.repository!.url),
|
||||
mode: LaunchMode
|
||||
.externalApplication);
|
||||
},
|
||||
icon: const Icon(Icons.code))
|
||||
: null,
|
||||
extension.settings?.isNotEmpty == true
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
_showSettingDialog(extension);
|
||||
},
|
||||
icon: const Icon(Icons.settings))
|
||||
: null,
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_showDeleteDialog(extension);
|
||||
},
|
||||
icon: const Icon(Icons.delete)),
|
||||
Obx(() => controller.updateFlags
|
||||
.containsKey(extension.identity)
|
||||
? badges.Badge(
|
||||
position:
|
||||
badges.BadgePosition.topStart(
|
||||
start: 36),
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
_showUpdateDialog(extension);
|
||||
},
|
||||
icon: const Icon(Icons.refresh)))
|
||||
: IconButton(
|
||||
onPressed: () {
|
||||
showMessage('tip'.tr,
|
||||
'extensionAlreadyLatest'.tr);
|
||||
},
|
||||
icon: const Icon(Icons.refresh))),
|
||||
]
|
||||
.where((e) => e != null)
|
||||
.map((e) => e!)
|
||||
.toList(),
|
||||
).paddingOnly(right: 16)
|
||||
],
|
||||
),
|
||||
),
|
||||
).paddingOnly(top: 8);
|
||||
},
|
||||
))),
|
||||
],
|
||||
).paddingAll(32),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showSettingDialog(Extension extension) async {
|
||||
final formKey = GlobalKey<FormBuilderState>();
|
||||
final confrimController = RoundedLoadingButtonController();
|
||||
|
||||
return showDialog<void>(
|
||||
context: Get.context!,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => AlertDialog(
|
||||
content: Builder(builder: (context) {
|
||||
final height = MediaQuery.of(context).size.height;
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
|
||||
return SizedBox(
|
||||
height: height * 0.75,
|
||||
width: width,
|
||||
child: FormBuilder(
|
||||
key: formKey,
|
||||
// autovalidateMode: AutovalidateMode.always,
|
||||
child: Column(children: [
|
||||
Text('setting'.tr),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: extension.settings!
|
||||
.map((e) => _buildSettingItem(e))
|
||||
.toList()),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}),
|
||||
actions: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints.tightFor(
|
||||
width: Get.theme.buttonTheme.minWidth,
|
||||
height: Get.theme.buttonTheme.height,
|
||||
),
|
||||
child: ElevatedButton(
|
||||
style:
|
||||
ElevatedButton.styleFrom(shape: const StadiumBorder())
|
||||
.copyWith(
|
||||
backgroundColor: MaterialStateProperty.all(
|
||||
Get.theme.colorScheme.background)),
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
child: Text('cancel'.tr),
|
||||
),
|
||||
),
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints.tightFor(
|
||||
width: Get.theme.buttonTheme.minWidth,
|
||||
height: Get.theme.buttonTheme.height,
|
||||
),
|
||||
child: RoundedLoadingButton(
|
||||
color: Get.theme.colorScheme.secondary,
|
||||
onPressed: () async {
|
||||
try {
|
||||
confrimController.start();
|
||||
if (formKey.currentState?.saveAndValidate() == true) {
|
||||
await updateExtensionSettings(
|
||||
extension.identity,
|
||||
UpdateExtensionSettings(
|
||||
settings: formKey.currentState!.value));
|
||||
await controller.load();
|
||||
Get.back();
|
||||
}
|
||||
} catch (e) {
|
||||
showErrorMessage(e);
|
||||
} finally {
|
||||
confrimController.reset();
|
||||
}
|
||||
},
|
||||
controller: confrimController,
|
||||
child: Text(
|
||||
'confirm'.tr,
|
||||
)),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildSettingItem(Setting setting) {
|
||||
final requiredValidator =
|
||||
setting.required ? FormBuilderValidators.required() : null;
|
||||
|
||||
Widget buildTextField(TextInputFormatter? inputFormatter,
|
||||
FormFieldValidator<String>? validator, TextInputType? keyBoardType) {
|
||||
return FormBuilderTextField(
|
||||
name: setting.name,
|
||||
decoration: InputDecoration(
|
||||
labelText: setting.title, hintText: setting.description),
|
||||
initialValue: setting.value?.toString(),
|
||||
inputFormatters: inputFormatter != null ? [inputFormatter] : null,
|
||||
keyboardType: keyBoardType,
|
||||
validator: FormBuilderValidators.compose([
|
||||
requiredValidator,
|
||||
validator,
|
||||
].where((e) => e != null).map((e) => e!).toList()),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildDropdown() {
|
||||
return FormBuilderDropdown<String>(
|
||||
name: setting.name,
|
||||
decoration: InputDecoration(
|
||||
labelText: setting.title, hintText: setting.description),
|
||||
initialValue: setting.value?.toString(),
|
||||
validator: FormBuilderValidators.compose([
|
||||
requiredValidator,
|
||||
].where((e) => e != null).map((e) => e!).toList()),
|
||||
items: setting.options!
|
||||
.map((e) => DropdownMenuItem(
|
||||
value: e.value.toString(),
|
||||
child: Text(e.label),
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
switch (setting.type) {
|
||||
case SettingType.string:
|
||||
return setting.options?.isNotEmpty == true
|
||||
? buildDropdown()
|
||||
: buildTextField(null, null, null);
|
||||
case SettingType.number:
|
||||
return setting.options?.isNotEmpty == true
|
||||
? buildDropdown()
|
||||
: buildTextField(
|
||||
FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d*')),
|
||||
FormBuilderValidators.numeric(),
|
||||
const TextInputType.numberWithOptions(decimal: true));
|
||||
case SettingType.boolean:
|
||||
return FormBuilderSwitch(
|
||||
name: setting.name,
|
||||
initialValue: (setting.value as bool?) ?? false,
|
||||
title: Text(setting.title),
|
||||
validator: requiredValidator,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showDeleteDialog(Extension extension) {
|
||||
showDialog(
|
||||
context: Get.context!,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => AlertDialog(
|
||||
title: Text('extensionDelete'.tr),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text('cancel'.tr),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
'confirm'.tr,
|
||||
style: const TextStyle(color: Colors.redAccent),
|
||||
),
|
||||
onPressed: () async {
|
||||
try {
|
||||
await deleteExtension(extension.identity);
|
||||
await controller.load();
|
||||
Get.back();
|
||||
} catch (e) {
|
||||
showErrorMessage(e);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
void _showUpdateDialog(Extension extension) {
|
||||
final confrimController = TextButtonLoadingController();
|
||||
|
||||
showDialog(
|
||||
context: Get.context!,
|
||||
builder: (context) => AlertDialog(
|
||||
content: Text('newVersionTitle'.trParams({
|
||||
'version': 'v${controller.updateFlags[extension.identity]!}'
|
||||
})),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
child: Text('newVersionLater'.tr),
|
||||
),
|
||||
TextButtonLoading(
|
||||
controller: confrimController,
|
||||
onPressed: () async {
|
||||
confrimController.start();
|
||||
try {
|
||||
await updateExtension(extension.identity);
|
||||
await controller.load();
|
||||
controller.updateFlags.remove(extension.identity);
|
||||
Get.back();
|
||||
showMessage('tip'.tr, 'extensionUpdateSuccess'.tr);
|
||||
} catch (e) {
|
||||
showErrorMessage(e);
|
||||
} finally {
|
||||
confrimController.stop();
|
||||
}
|
||||
},
|
||||
child: Text('newVersionUpdate'.tr),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,12 @@ class HomeView extends GetView<HomeController> {
|
||||
Widget build(BuildContext context) {
|
||||
return GetRouterOutlet.builder(builder: (context, delegate, currentRoute) {
|
||||
switch (currentRoute?.location) {
|
||||
case Routes.SETTING:
|
||||
case Routes.EXTENSION:
|
||||
controller.currentIndex.value = 1;
|
||||
break;
|
||||
case Routes.SETTING:
|
||||
controller.currentIndex.value = 2;
|
||||
break;
|
||||
default:
|
||||
controller.currentIndex.value = 0;
|
||||
break;
|
||||
@@ -33,23 +36,30 @@ class HomeView extends GetView<HomeController> {
|
||||
groupAlignment: 0,
|
||||
// useIndicator: false,
|
||||
onDestinationSelected: (int index) {
|
||||
controller.currentIndex.value = index;
|
||||
switch (index) {
|
||||
case 0:
|
||||
controller.currentIndex.value = 0;
|
||||
delegate.toNamed(Routes.TASK);
|
||||
delegate.offAndToNamed(Routes.TASK);
|
||||
break;
|
||||
case 1:
|
||||
controller.currentIndex.value = 1;
|
||||
delegate.toNamed(Routes.SETTING);
|
||||
delegate.offAndToNamed(Routes.EXTENSION);
|
||||
break;
|
||||
case 2:
|
||||
delegate.offAndToNamed(Routes.SETTING);
|
||||
break;
|
||||
}
|
||||
},
|
||||
destinations: [
|
||||
NavigationRailDestination(
|
||||
icon: const Icon(Icons.list),
|
||||
selectedIcon: const Icon(Icons.list),
|
||||
icon: const Icon(Icons.task),
|
||||
selectedIcon: const Icon(Icons.task),
|
||||
label: Text('task'.tr),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: const Icon(Icons.extension),
|
||||
selectedIcon: const Icon(Icons.extension),
|
||||
label: Text('extensions'.tr),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: const Icon(Icons.settings),
|
||||
selectedIcon: const Icon(Icons.settings),
|
||||
@@ -76,9 +86,13 @@ class HomeView extends GetView<HomeController> {
|
||||
? BottomNavigationBar(
|
||||
items: <BottomNavigationBarItem>[
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.list),
|
||||
icon: const Icon(Icons.task),
|
||||
label: 'task'.tr,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.extension),
|
||||
label: 'extensions'.tr,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.settings),
|
||||
label: 'setting'.tr,
|
||||
@@ -87,14 +101,16 @@ class HomeView extends GetView<HomeController> {
|
||||
currentIndex: controller.currentIndex.value,
|
||||
// selectedItemColor: Get.theme.highlightColor,
|
||||
onTap: (index) {
|
||||
controller.currentIndex.value = index;
|
||||
switch (index) {
|
||||
case 0:
|
||||
controller.currentIndex.value = 0;
|
||||
delegate.toNamed(Routes.TASK);
|
||||
delegate.offAndToNamed(Routes.TASK);
|
||||
break;
|
||||
case 1:
|
||||
controller.currentIndex.value = 1;
|
||||
delegate.toNamed(Routes.SETTING);
|
||||
delegate.offAndToNamed(Routes.EXTENSION);
|
||||
break;
|
||||
case 2:
|
||||
delegate.offAndToNamed(Routes.SETTING);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:badges/badges.dart' as badges;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -27,7 +27,7 @@ class TaskFilesController extends GetxController {
|
||||
final taskId = Get.rootDelegate.parameters['id'];
|
||||
final tasks = await getTasks([]);
|
||||
task.value = tasks.firstWhere((element) => element.id == taskId);
|
||||
parseDirMap(task.value!.meta.res.files);
|
||||
parseDirMap(task.value!.meta.res!.files);
|
||||
toDir("/");
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class TaskFilesView extends GetView<TaskFilesController> {
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Get.rootDelegate.popRoute()),
|
||||
// actions: [],
|
||||
title: Obx(() => Text(controller.task.value?.meta.res.name ?? "")),
|
||||
title: Obx(() => Text(controller.task.value!.meta.res!.name)),
|
||||
),
|
||||
body: Obx(() {
|
||||
final fileList = controller.fileList;
|
||||
@@ -52,6 +52,8 @@ class TaskFilesView extends GetView<TaskFilesController> {
|
||||
itemBuilder: (context, index) {
|
||||
final meta = controller.task.value!.meta;
|
||||
final file = fileList[index];
|
||||
final filePath = Util.safePathJoin(
|
||||
[meta.opts.path, meta.res!.name, file.fullPath]);
|
||||
return ListTile(
|
||||
leading: file.isDirectory
|
||||
? const Icon(Icons.folder)
|
||||
@@ -74,22 +76,12 @@ class TaskFilesView extends GetView<TaskFilesController> {
|
||||
IconButton(
|
||||
icon: const Icon(Icons.play_circle),
|
||||
onPressed: () {
|
||||
OpenFile.open(
|
||||
Util.safePathJoin([
|
||||
meta.opts.path,
|
||||
meta.res.rootDir,
|
||||
file.fullPath
|
||||
]),
|
||||
);
|
||||
OpenFile.open(filePath);
|
||||
}),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.share),
|
||||
onPressed: () {
|
||||
final xfile = XFile(Util.safePathJoin([
|
||||
meta.opts.path,
|
||||
meta.res.rootDir,
|
||||
file.fullPath
|
||||
]));
|
||||
final xfile = XFile(filePath);
|
||||
Share.shareXFiles([xfile]);
|
||||
})
|
||||
],
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'package:gopeed/app/modules/task/views/task_view.dart';
|
||||
|
||||
import '../modules/create/bindings/create_binding.dart';
|
||||
import '../modules/create/views/create_view.dart';
|
||||
import '../modules/extension/bindings/extension_binding.dart';
|
||||
import '../modules/extension/views/extension_view.dart';
|
||||
import '../modules/home/bindings/home_binding.dart';
|
||||
import '../modules/home/views/home_view.dart';
|
||||
import '../modules/root/bindings/root_binding.dart';
|
||||
@@ -47,6 +49,11 @@ class AppPages {
|
||||
transition: Transition.noTransition,
|
||||
binding: TaskFilesBinding()),
|
||||
]),
|
||||
GetPage(
|
||||
name: _Paths.EXTENSION,
|
||||
page: () => ExtensionView(),
|
||||
transition: Transition.noTransition,
|
||||
binding: ExtensionBinding()),
|
||||
GetPage(
|
||||
name: _Paths.SETTING,
|
||||
page: () => const SettingView(),
|
||||
|
||||
@@ -8,6 +8,7 @@ abstract class Routes {
|
||||
static const CREATE = _Paths.CREATE;
|
||||
static const TASK = _Paths.HOME + _Paths.TASK;
|
||||
static const TASK_FILES = TASK + _Paths.TASK_FILES;
|
||||
static const EXTENSION = _Paths.HOME + _Paths.EXTENSION;
|
||||
static const SETTING = _Paths.HOME + _Paths.SETTING;
|
||||
}
|
||||
|
||||
@@ -18,5 +19,6 @@ abstract class _Paths {
|
||||
static const CREATE = '/create';
|
||||
static const TASK = '/task';
|
||||
static const TASK_FILES = '/files';
|
||||
static const EXTENSION = '/extension';
|
||||
static const SETTING = '/setting';
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/api/model/meta.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
import '../../api/api.dart';
|
||||
import '../../api/model/task.dart';
|
||||
@@ -37,8 +38,11 @@ class BuildTaskListView extends GetView {
|
||||
|
||||
Widget buildTaskList(BuildContext context, tasks) {
|
||||
return ListView.builder(
|
||||
itemCount: tasks.length,
|
||||
itemCount: tasks.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == tasks.length) {
|
||||
return const SizedBox(height: 75);
|
||||
}
|
||||
return item(context, tasks[index]);
|
||||
},
|
||||
);
|
||||
@@ -54,14 +58,12 @@ class BuildTaskListView extends GetView {
|
||||
}
|
||||
|
||||
String buildExplorerUrl(Task task) {
|
||||
if (task.meta.res.rootDir.trim().isEmpty) {
|
||||
return path.join(
|
||||
Util.safeDir(task.meta.opts.path),
|
||||
Util.safeDir(task.meta.res.files[0].path),
|
||||
task.meta.res.files[0].name);
|
||||
if (task.meta.res!.name.isEmpty) {
|
||||
return path.join(Util.safeDir(task.meta.opts.path),
|
||||
Util.safeDir(task.meta.res!.files[0].path), fileName(task.meta));
|
||||
} else {
|
||||
return path.join(Util.safeDir(task.meta.opts.path),
|
||||
Util.safeDir(task.meta.res.rootDir));
|
||||
Util.safeDir(fileName(task.meta)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +91,7 @@ class BuildTaskListView extends GetView {
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
'delete'.tr,
|
||||
'confirm'.tr,
|
||||
style: const TextStyle(color: Colors.redAccent),
|
||||
),
|
||||
onPressed: () async {
|
||||
@@ -156,80 +158,85 @@ class BuildTaskListView extends GetView {
|
||||
}
|
||||
|
||||
double getProgress() {
|
||||
return task.size <= 0 ? 1 : task.progress.downloaded / task.size;
|
||||
final totalSize = task.meta.res?.size ?? 0;
|
||||
return totalSize <= 0 ? 0 : task.progress.downloaded / totalSize;
|
||||
}
|
||||
|
||||
Color pickColor() {
|
||||
switch (task.status) {
|
||||
// ready, running, pause, error, done
|
||||
case Status.running:
|
||||
return Get.theme.colorScheme.primary;
|
||||
// case Status.pause:
|
||||
// return Get.theme.colorScheme.secondary;
|
||||
case Status.error:
|
||||
return Get.theme.colorScheme.error;
|
||||
default:
|
||||
return Get.theme.colorScheme.primary;
|
||||
String getProgressText() {
|
||||
if (isDone()) {
|
||||
return Util.fmtByte(task.meta.res!.size);
|
||||
}
|
||||
if (task.meta.res == null) {
|
||||
return "";
|
||||
}
|
||||
final total = task.meta.res!.size;
|
||||
return Util.fmtByte(task.progress.downloaded) +
|
||||
(total > 0 ? " / ${Util.fmtByte(total)}" : "");
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
// onTap: () {},
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Stack(children: [
|
||||
!isDone()
|
||||
? Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: Opacity(
|
||||
opacity: 0.6,
|
||||
child: LinearProgressIndicator(
|
||||
backgroundColor: Colors.transparent,
|
||||
color: pickColor(),
|
||||
// minHeight: 76,
|
||||
value: getProgress(),
|
||||
),
|
||||
))
|
||||
: const SizedBox.shrink(),
|
||||
ListTile(
|
||||
// isThreeLine: true,
|
||||
title: Text(fileName(task.meta),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Get.textTheme.titleSmall),
|
||||
subtitle: Text(
|
||||
"${isDone() ? "" : "${Util.fmtByte(task.progress.downloaded)} / "}${Util.fmtByte(task.size)}",
|
||||
style: context.textTheme.bodyLarge
|
||||
?.copyWith(color: Get.theme.disabledColor),
|
||||
return Card(
|
||||
elevation: 4.0,
|
||||
child: InkWell(
|
||||
onTap: () {},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(fileName(task.meta)),
|
||||
leading: (task.meta.res?.name.isNotEmpty ?? false
|
||||
? const Icon(FaIcons.folder)
|
||||
: Icon(FaIcons.allIcons[findIcon(fileName(task.meta))]))),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
getProgressText(),
|
||||
style: Get.textTheme.bodyLarge
|
||||
?.copyWith(color: Get.theme.disabledColor),
|
||||
).padding(left: 18)),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text("${Util.fmtByte(task.progress.speed)} / s",
|
||||
style: Get.textTheme.titleSmall),
|
||||
...buildActions()
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
LinearProgressIndicator(
|
||||
value: getProgress(),
|
||||
),
|
||||
],
|
||||
),
|
||||
leading: (task.meta.res.rootDir.isNotEmpty
|
||||
? const Icon(FaIcons.folder)
|
||||
: Icon(FaIcons.allIcons[findIcon(fileName(task.meta))])),
|
||||
|
||||
trailing: SizedBox(
|
||||
width: 180,
|
||||
child: Row(
|
||||
// crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
// textBaseline: DefaultTextStyle.of(context).style.textBaseline,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text("${Util.fmtByte(task.progress.speed)} / s",
|
||||
style: context.textTheme.titleSmall),
|
||||
...buildActions()
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
])
|
||||
]));
|
||||
)).padding(horizontal: 14, top: 8);
|
||||
}
|
||||
|
||||
String fileName(Meta meta) {
|
||||
if (meta.res.files.length > 1) {
|
||||
return meta.res.name;
|
||||
if (meta.opts.name.isNotEmpty) {
|
||||
return meta.opts.name;
|
||||
}
|
||||
return meta.opts.name.isEmpty ? meta.res.files[0].name : meta.opts.name;
|
||||
if (meta.res == null) {
|
||||
final u = Uri.parse(meta.req.url);
|
||||
if (u.scheme.startsWith("http")) {
|
||||
return u.path.isNotEmpty
|
||||
? u.path.substring(u.path.lastIndexOf("/") + 1)
|
||||
: u.host;
|
||||
} else {
|
||||
final params = u.queryParameters;
|
||||
if (params.containsKey("dn")) {
|
||||
return params["dn"]!;
|
||||
} else {
|
||||
return params["xt"]!.split(":").last;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (meta.res!.name.isNotEmpty) {
|
||||
return meta.res!.name;
|
||||
}
|
||||
return meta.res!.files[0].name;
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,7 @@ class FileListView extends GetView {
|
||||
if (reason == fluent.TreeViewItemInvokeReason.selectionToggle) {
|
||||
List childrenIds = findChildFileIdsR(fileInfo['children']);
|
||||
if (item.selected == true) {
|
||||
parentController.selectedIndexes.addAll(childrenIds);
|
||||
parentController.selectedIndexes.addAll(childrenIds.cast());
|
||||
parentController.fileInfos[fileInfo['id']]['size'] = 0;
|
||||
addDescendantFolderSizeR(item.value);
|
||||
addAncestorFolderSizeR(item.value, fileInfo['size']);
|
||||
@@ -178,13 +178,15 @@ class FileListView extends GetView {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
// style: context.textTheme.titleSmall,
|
||||
)),
|
||||
Text(
|
||||
Util.fmtByte(
|
||||
fileInfo['size'],
|
||||
),
|
||||
// style: context.textTheme.labelMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
fileInfo['size'] > 0
|
||||
? Text(
|
||||
Util.fmtByte(
|
||||
fileInfo['size'],
|
||||
),
|
||||
// style: context.textTheme.labelMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
]),
|
||||
));
|
||||
}
|
||||
@@ -251,7 +253,7 @@ class FileListView extends GetView {
|
||||
idNext++;
|
||||
}
|
||||
parentController.fileInfos.value = infos;
|
||||
parentController.selectedIndexes.value = selectedFileIds;
|
||||
parentController.selectedIndexes.value = selectedFileIds.cast();
|
||||
parentController.openedFolders.value = openedFolders;
|
||||
List<fluent.TreeViewItem> treeItems = buildTreeViewItemsR(0, -1);
|
||||
return treeItems;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class IconButtonLoading extends StatefulWidget {
|
||||
final Widget icon;
|
||||
final VoidCallback? onPressed;
|
||||
final IconButtonLoadingController controller;
|
||||
|
||||
const IconButtonLoading(
|
||||
{Key? key,
|
||||
required this.icon,
|
||||
required this.onPressed,
|
||||
required this.controller})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<IconButtonLoading> createState() => _IconButtonLoadingState();
|
||||
}
|
||||
|
||||
class _IconButtonLoadingState extends State<IconButtonLoading> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder<bool>(
|
||||
valueListenable: widget.controller,
|
||||
builder: (context, value, child) {
|
||||
return IconButton(
|
||||
key: widget.key,
|
||||
onPressed: value ? null : widget.onPressed,
|
||||
icon: value
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: widget.icon,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class IconButtonLoadingController extends ValueNotifier<bool> {
|
||||
IconButtonLoadingController() : super(false);
|
||||
|
||||
void start() {
|
||||
value = true;
|
||||
}
|
||||
|
||||
void stop() {
|
||||
value = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TextButtonLoading extends StatefulWidget {
|
||||
final Widget child;
|
||||
final VoidCallback? onPressed;
|
||||
final TextButtonLoadingController controller;
|
||||
|
||||
const TextButtonLoading(
|
||||
{Key? key,
|
||||
required this.child,
|
||||
required this.onPressed,
|
||||
required this.controller})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<TextButtonLoading> createState() => _TextButtonLoadingState();
|
||||
}
|
||||
|
||||
class _TextButtonLoadingState extends State<TextButtonLoading> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder<bool>(
|
||||
valueListenable: widget.controller,
|
||||
builder: (context, value, child) {
|
||||
return TextButton(
|
||||
key: widget.key,
|
||||
onPressed: value ? null : widget.onPressed,
|
||||
child: value
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: widget.child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TextButtonLoadingController extends ValueNotifier<bool> {
|
||||
TextButtonLoadingController() : super(false);
|
||||
|
||||
void start() {
|
||||
value = true;
|
||||
}
|
||||
|
||||
void stop() {
|
||||
value = false;
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,6 @@ import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../../util/util.dart';
|
||||
import '../common/libgopeed_channel.dart';
|
||||
import '../common/libgopeed_ffi.dart';
|
||||
@@ -26,15 +24,8 @@ class LibgopeedBootNative implements LibgopeedBoot {
|
||||
|
||||
@override
|
||||
Future<int> start(StartConfig cfg) async {
|
||||
var storageDir = "./";
|
||||
if (Platform.isAndroid) {
|
||||
storageDir = (await getExternalStorageDirectory())?.path ?? storageDir;
|
||||
}
|
||||
if (Platform.isIOS) {
|
||||
storageDir = (await getLibraryDirectory()).path;
|
||||
}
|
||||
cfg.storage = 'bolt';
|
||||
cfg.storageDir = storageDir;
|
||||
cfg.storageDir = Util.getStorageDir();
|
||||
cfg.refreshInterval = 0;
|
||||
var port =
|
||||
Util.isDesktop() ? await _ffiStart(cfg) : await _libgopeed.start(cfg);
|
||||
|
||||
@@ -76,6 +76,12 @@ class LocaleKeys {
|
||||
static const newVersionTitle = 'newVersionTitle';
|
||||
static const newVersionUpdate = 'newVersionUpdate';
|
||||
static const newVersionLater = 'newVersionLater';
|
||||
static const extensions = 'extensions';
|
||||
static const extensionInstallUrl = 'extensionInstallUrl';
|
||||
static const extensionInstallSuccess = 'extensionInstallSuccess';
|
||||
static const extensionUpdateSuccess = 'extensionUpdateSuccess';
|
||||
static const extensionDelete = 'extensionDelete';
|
||||
static const extensionAlreadyLatest = 'extensionAlreadyLatest';
|
||||
}
|
||||
|
||||
class Locales {
|
||||
@@ -141,8 +147,14 @@ class Locales {
|
||||
'deleteTaskTip': 'Keep downloaded files',
|
||||
'delete': 'Delete',
|
||||
'newVersionTitle': 'Discover new version @version',
|
||||
'newVersionUpdate': 'Download Now',
|
||||
'newVersionUpdate': 'Update Now',
|
||||
'newVersionLater': 'Later',
|
||||
'extensions': 'Extensions',
|
||||
'extensionInstallUrl': 'Install URL',
|
||||
'extensionInstallSuccess': 'Installed successfully',
|
||||
'extensionUpdateSuccess': 'Updated successfully',
|
||||
'extensionDelete': 'Delete Extension',
|
||||
'extensionAlreadyLatest': 'It\'s already the latest version',
|
||||
};
|
||||
static const fa_IR = {
|
||||
'label': 'فارسی',
|
||||
@@ -200,7 +212,7 @@ class Locales {
|
||||
'deleteTaskTip': 'فایل های دانلود شده را نگه دارد',
|
||||
'delete': 'پاک کردن',
|
||||
'newVersionTitle': '@version عنوان: کشف نسخه جدید',
|
||||
'newVersionUpdate': 'دریافت اکنون',
|
||||
'newVersionUpdate': 'بروزرسانی',
|
||||
'newVersionLater': 'بعداً',
|
||||
};
|
||||
static const ja_JP = {
|
||||
@@ -261,7 +273,7 @@ class Locales {
|
||||
'deleteTaskTip': 'ダウンロードしたファイルを保持',
|
||||
'delete': '削除',
|
||||
'newVersionTitle': '新しいバージョン @version を発見する',
|
||||
'newVersionUpdate': '今すぐダウンロード',
|
||||
'newVersionUpdate': 'アップデート',
|
||||
'newVersionLater': '後で',
|
||||
};
|
||||
static const ru_RU = {
|
||||
@@ -324,7 +336,7 @@ class Locales {
|
||||
'deleteTaskTip': 'Сохранить загруженные файлы',
|
||||
'delete': 'Удалить',
|
||||
'newVersionTitle': 'Обнаружена новая версия @version',
|
||||
'newVersionUpdate': 'Скачать сейчас',
|
||||
'newVersionUpdate': 'Обновить',
|
||||
'newVersionLater': 'позже',
|
||||
};
|
||||
static const zh_CN = {
|
||||
@@ -387,8 +399,14 @@ class Locales {
|
||||
'deleteTaskTip': '保留已下载的文件',
|
||||
'delete': '删除',
|
||||
'newVersionTitle': '发现新版本 @version',
|
||||
'newVersionUpdate': '立即下载',
|
||||
'newVersionUpdate': '立即更新',
|
||||
'newVersionLater': '稍后再说',
|
||||
'extensions': '扩展',
|
||||
'extensionInstallUrl': '安装链接',
|
||||
'extensionInstallSuccess': '安装成功',
|
||||
'extensionUpdateSuccess': '更新成功',
|
||||
'extensionDelete': '删除扩展',
|
||||
'extensionAlreadyLatest': '已经是最新版本',
|
||||
};
|
||||
static const zh_TW = {
|
||||
'label': '中文 (正體) ',
|
||||
|
||||
@@ -39,6 +39,9 @@ Future<void> init() async {
|
||||
|
||||
final controller = Get.put(AppController());
|
||||
try {
|
||||
if (!Util.isWeb()) {
|
||||
await Util.initStorageDir();
|
||||
}
|
||||
await controller.loadStartConfig();
|
||||
final startCfg = controller.startConfig.value;
|
||||
controller.runningPort.value = await LibgopeedBoot.instance.start(startCfg);
|
||||
|
||||
@@ -55,7 +55,9 @@ const List iconMaps = [
|
||||
'rmvb',
|
||||
'rm',
|
||||
'vob',
|
||||
'3gp'
|
||||
'3gp',
|
||||
'm4s',
|
||||
'ts'
|
||||
],
|
||||
'type': 'Video',
|
||||
'thumbnail': 'file_video'
|
||||
@@ -148,7 +150,7 @@ const List iconMaps = [
|
||||
{
|
||||
'extensions': ['apk'],
|
||||
'type': 'Android',
|
||||
'thumbnail': 'windows'
|
||||
'thumbnail': 'android'
|
||||
},
|
||||
//other
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'util.dart';
|
||||
|
||||
final logger = Logger(
|
||||
filter: ProductionFilter(),
|
||||
printer: LogfmtPrinter(),
|
||||
printer: SimplePrinter(printTime: true, colors: false),
|
||||
output: buildOutput(),
|
||||
);
|
||||
|
||||
|
||||
@@ -15,6 +15,14 @@ void showErrorMessage(msg) {
|
||||
Get.snackbar(title, msg.toString());
|
||||
}
|
||||
|
||||
var _showMessageFlag = true;
|
||||
|
||||
void showMessage(title, msg) {
|
||||
Get.snackbar(title, msg);
|
||||
if (_showMessageFlag) {
|
||||
_showMessageFlag = false;
|
||||
Get.snackbar(title, msg);
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
_showMessageFlag = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,23 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
class Util {
|
||||
static String? _storageDir;
|
||||
|
||||
static String cleanPath(String path) {
|
||||
path = path.replaceAll(RegExp(r'\\'), "/");
|
||||
if (path.startsWith(".")) {
|
||||
path = path.substring(1);
|
||||
}
|
||||
if (path.startsWith("/")) {
|
||||
path = path.substring(1);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
static String safeDir(String path) {
|
||||
if (path == "." || path == "./" || path == ".\\") {
|
||||
return "";
|
||||
@@ -33,6 +48,24 @@ class Util {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> initStorageDir() async {
|
||||
var storageDir = "./";
|
||||
if (Platform.isAndroid) {
|
||||
storageDir = (await getExternalStorageDirectory())?.path ?? storageDir;
|
||||
} else if (Platform.isIOS) {
|
||||
storageDir = (await getLibraryDirectory()).path;
|
||||
}
|
||||
_storageDir = storageDir;
|
||||
}
|
||||
|
||||
static String getStorageDir() {
|
||||
return _storageDir!;
|
||||
}
|
||||
|
||||
static String getStorageAsset(String asset) {
|
||||
return path.join(getStorageDir(), asset);
|
||||
}
|
||||
|
||||
static isAndroid() {
|
||||
return !kIsWeb && Platform.isAndroid;
|
||||
}
|
||||
@@ -76,7 +109,7 @@ class Util {
|
||||
return [];
|
||||
}
|
||||
const ls = LineSplitter();
|
||||
return ls.convert(text);
|
||||
return ls.convert(text).where((line) => line.isNotEmpty).toList();
|
||||
}
|
||||
|
||||
// if one future complete, return the result, only all future error, return the last error
|
||||
|
||||
@@ -318,6 +318,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_form_builder:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_form_builder
|
||||
sha256: "8973beed34b6d951d36bf688b52e9e3040b47b763c35c320bd6f4c2f6b13f3a2"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.1.1"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -357,6 +365,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
form_builder_validators:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: form_builder_validators
|
||||
sha256: e04998b1597d76a51da7f009ed3b2f12d4173f13e146e8744fd2453e8595a2c9
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.0.0"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -39,7 +39,7 @@ dependencies:
|
||||
lottie: ^1.4.2
|
||||
styled_widget: ^0.4.0+3
|
||||
context_menus: ^1.0.2
|
||||
json_annotation: ^4.6.0
|
||||
json_annotation: ^4.8.1
|
||||
dio: ^5.2.1
|
||||
path_provider: ^2.0.11
|
||||
file_picker: 5.3.1
|
||||
@@ -60,6 +60,8 @@ dependencies:
|
||||
autoscale_tabbarview: ^1.0.2
|
||||
share_plus: ^7.1.0
|
||||
open_file: ^3.3.2
|
||||
flutter_form_builder: ^9.1.1
|
||||
form_builder_validators: ^9.0.0
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
@@ -71,7 +73,7 @@ dev_dependencies:
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^2.0.0
|
||||
ffigen: ^8.0.2
|
||||
# build json model: flutter pub run build_runner build --delete-conflicting-outputs
|
||||
# build json model: dart run build_runner build --delete-conflicting-outputs
|
||||
build_runner: ^2.2.1
|
||||
json_serializable: ^6.3.2
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
@@ -89,6 +91,7 @@ flutter:
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
assets:
|
||||
- assets/tray_icon/
|
||||
- assets/extension/
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||
@@ -114,7 +117,7 @@ flutter:
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/custom-fonts/#from-packages
|
||||
|
||||
# flutter pub run flutter_launcher_icons:main
|
||||
# dart run flutter_launcher_icons:main
|
||||
flutter_icons:
|
||||
image_path: "assets/icon/icon_1024.png"
|
||||
android: true # can specify file name here e.g. "ic_launcher"
|
||||
@@ -130,7 +133,7 @@ flutter_icons:
|
||||
generate: true
|
||||
image_path: "assets/icon/icon_macos_1024.png"
|
||||
|
||||
# flutter pub run ffigen
|
||||
# dart run ffigen
|
||||
ffigen:
|
||||
name: LibgopeedBind
|
||||
description: Bindings to gopeed library.
|
||||
|
||||
Reference in new issue
Block a user