mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-14 12:23:43 +00:00
Refactor project in get-cli standard (#80)
* refactor project in get-cli standard * use local trackers when github is not connectable upgrade stylish_bottom_bar to 1.0.0 responsive view for home_view treeview for file_list_vie fluent ui integrating * improve responsive view. treeView add light theme.fix overflow bugs * responsive view,treeView,file select downloaded and downloading pages file extension icons fix some minor bugs * use local trackers when github is not connectable upgrade stylish_bottom_bar to 1.0.0 responsive view for home_view treeview for file_list_vie fluent ui integrating * improve responsive view. treeView add light theme.fix overflow bugs * bottom bar index fix * refactor project in get-cli standard --------- Co-authored-by: wuke2022 <[email protected]>
This commit is contained in:
1 parent
9b1ed15fea
commit
74c24ad860
44 files changed
+1025
-360
No files matched your search
@@ -0,0 +1,8 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ApiProvider extends GetConnect {
|
||||
@override
|
||||
void onInit() {
|
||||
httpClient.baseUrl = 'YOUR-API-URL';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/app_controller.dart';
|
||||
|
||||
class AppBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<AppController>(
|
||||
() => AppController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../../../api/api.dart';
|
||||
import '../../../../api/model/downloader_config.dart';
|
||||
import '../../../../core/common/start_config.dart';
|
||||
import '../../../../i18n/messages.dart';
|
||||
import '../../../../util/log_util.dart';
|
||||
import '../../../../util/util.dart';
|
||||
|
||||
const _startConfigNetwork = "start.network";
|
||||
const _startConfigAddress = "start.address";
|
||||
const _startConfigApiToken = "start.apiToken";
|
||||
|
||||
const unixSocketPath = 'gopeed.sock';
|
||||
|
||||
const allTrackerSubscribeUrls = [
|
||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all.txt',
|
||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_http.txt',
|
||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_https.txt',
|
||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_ip.txt',
|
||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_udp.txt',
|
||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_ws.txt',
|
||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_best.txt',
|
||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_best_ip.txt',
|
||||
'https://github.com/XIU2/TrackersListCollection/raw/master/all.txt',
|
||||
'https://github.com/XIU2/TrackersListCollection/raw/master/best.txt',
|
||||
'https://github.com/XIU2/TrackersListCollection/raw/master/http.txt',
|
||||
];
|
||||
const allTrackerCdns = [
|
||||
// jsdelivr: https://cdn.jsdelivr.net/gh/ngosang/trackerslist/trackers_all.txt
|
||||
["https://cdn.jsdelivr.net/gh", r".*github.com(/.*)/raw/master(/.*)"],
|
||||
// nuaa: https://hub.nuaa.cf/ngosang/trackerslist/raw/master/trackers_all.txt
|
||||
["https://hub.nuaa.cf", r".*github.com(/.*)"]
|
||||
];
|
||||
final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls,
|
||||
key: (v) => v as String,
|
||||
value: (v) {
|
||||
final ret = [v as String];
|
||||
for (final cdn in allTrackerCdns) {
|
||||
final reg = RegExp(cdn[1]);
|
||||
final match = reg.firstMatch(v.toString());
|
||||
var matchStr = "";
|
||||
for (var i = 1; i <= match!.groupCount; i++) {
|
||||
matchStr += match.group(i)!;
|
||||
}
|
||||
ret.add("${cdn[0]}$matchStr");
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
|
||||
class AppController extends GetxController {
|
||||
static StartConfig? _defaultStartConfig;
|
||||
|
||||
final startConfig = StartConfig().obs;
|
||||
final runningPort = 0.obs;
|
||||
final downloaderConfig = DownloaderConfig().obs;
|
||||
|
||||
String runningAddress() {
|
||||
if (startConfig.value.network == 'unix') {
|
||||
return startConfig.value.address;
|
||||
}
|
||||
return '${startConfig.value.address.split(':').first}:$runningPort';
|
||||
}
|
||||
|
||||
Future<StartConfig> _initDefaultStartConfig() async {
|
||||
if (_defaultStartConfig != null) {
|
||||
return _defaultStartConfig!;
|
||||
}
|
||||
_defaultStartConfig = StartConfig();
|
||||
if (!Util.isUnix()) {
|
||||
// not support unix socket, use tcp
|
||||
_defaultStartConfig!.network = "tcp";
|
||||
_defaultStartConfig!.address = "127.0.0.1:0";
|
||||
} else {
|
||||
_defaultStartConfig!.network = "unix";
|
||||
if (Util.isDesktop()) {
|
||||
_defaultStartConfig!.address = unixSocketPath;
|
||||
}
|
||||
if (Util.isMobile()) {
|
||||
_defaultStartConfig!.address =
|
||||
"${(await getTemporaryDirectory()).path}/$unixSocketPath";
|
||||
}
|
||||
}
|
||||
_defaultStartConfig!.apiToken = '';
|
||||
return _defaultStartConfig!;
|
||||
}
|
||||
|
||||
Future<void> loadStartConfig() async {
|
||||
final defaultCfg = await _initDefaultStartConfig();
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
startConfig.value.network =
|
||||
prefs.getString(_startConfigNetwork) ?? defaultCfg.network;
|
||||
startConfig.value.address =
|
||||
prefs.getString(_startConfigAddress) ?? defaultCfg.address;
|
||||
startConfig.value.apiToken =
|
||||
prefs.getString(_startConfigApiToken) ?? defaultCfg.apiToken;
|
||||
}
|
||||
|
||||
Future<void> loadDownloaderConfig() async {
|
||||
try {
|
||||
downloaderConfig.value = await getConfig();
|
||||
} catch (e) {
|
||||
logger.w("load downloader config fail", e);
|
||||
downloaderConfig.value = DownloaderConfig();
|
||||
}
|
||||
await _initDownloaderConfig();
|
||||
}
|
||||
|
||||
Future<void> trackerUpdate() async {
|
||||
final btExtConfig = downloaderConfig.value.extra.bt;
|
||||
final result = <String>[];
|
||||
for (var u in btExtConfig.trackerSubscribeUrls) {
|
||||
var allFail = true;
|
||||
for (var cdn in allTrackerSubscribeUrlCdns[u]!) {
|
||||
try {
|
||||
result.addAll(await _fetchTrackers(u));
|
||||
allFail = false;
|
||||
break;
|
||||
} catch (e) {
|
||||
logger.w("subscribe trackers fail, url: $cdn", e);
|
||||
}
|
||||
}
|
||||
if (allFail) {
|
||||
throw Exception('subscribe trackers fail, network error');
|
||||
}
|
||||
}
|
||||
btExtConfig.subscribeTrackers.clear();
|
||||
btExtConfig.subscribeTrackers.addAll(result);
|
||||
downloaderConfig.update((val) {
|
||||
val!.extra.bt.lastTrackerUpdateTime = DateTime.now();
|
||||
});
|
||||
refreshTrackers();
|
||||
|
||||
await saveConfig();
|
||||
}
|
||||
|
||||
refreshTrackers() {
|
||||
final btConfig = downloaderConfig.value.protocolConfig.bt;
|
||||
final btExtConfig = downloaderConfig.value.extra.bt;
|
||||
btConfig.trackers.clear();
|
||||
btConfig.trackers.addAll(btExtConfig.subscribeTrackers);
|
||||
btConfig.trackers.addAll(btExtConfig.customTrackers);
|
||||
// remove duplicate
|
||||
btConfig.trackers.toSet().toList();
|
||||
}
|
||||
|
||||
Future<void> trackerUpdateOnStart() async {
|
||||
final btExtConfig = downloaderConfig.value.extra.bt;
|
||||
final lastUpdateTime = btExtConfig.lastTrackerUpdateTime;
|
||||
// if last update time is null or more than 1 day, update trackers
|
||||
if (lastUpdateTime == null ||
|
||||
lastUpdateTime.difference(DateTime.now()).inDays < 0) {
|
||||
try {
|
||||
await trackerUpdate();
|
||||
} catch (e) {
|
||||
logger.w("tracker update fail", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<String>> _fetchTrackers(String subscribeUrl) async {
|
||||
final resp = await proxyRequest(subscribeUrl);
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception('Failed to get trackers');
|
||||
}
|
||||
const ls = LineSplitter();
|
||||
return ls.convert(resp.data).where((e) => e.isNotEmpty).toList();
|
||||
}
|
||||
|
||||
_initDownloaderConfig() async {
|
||||
final config = downloaderConfig.value;
|
||||
if (config.protocolConfig.http.connections == 0) {
|
||||
config.protocolConfig.http.connections = 16;
|
||||
}
|
||||
final extra = config.extra;
|
||||
if (extra.themeMode.isEmpty) {
|
||||
extra.themeMode = ThemeMode.system.name;
|
||||
}
|
||||
if (extra.locale.isEmpty) {
|
||||
final systemLocale = getLocaleKey(ui.window.locale);
|
||||
extra.locale = messages.keys.containsKey(systemLocale)
|
||||
? systemLocale
|
||||
: getLocaleKey(fallbackLocale);
|
||||
}
|
||||
if (extra.bt.trackerSubscribeUrls.isEmpty) {
|
||||
// default select all tracker subscribe urls
|
||||
extra.bt.trackerSubscribeUrls.addAll(allTrackerSubscribeUrls);
|
||||
}
|
||||
|
||||
if (config.downloadDir.isEmpty) {
|
||||
if (Util.isDesktop()) {
|
||||
config.downloadDir = (await getDownloadsDirectory())?.path ?? "./";
|
||||
} else if (Util.isAndroid()) {
|
||||
config.downloadDir = (await getExternalStorageDirectory())?.path ??
|
||||
(await getApplicationDocumentsDirectory()).path;
|
||||
return;
|
||||
} else if (Util.isIOS()) {
|
||||
config.downloadDir = (await getApplicationDocumentsDirectory()).path;
|
||||
} else {
|
||||
config.downloadDir = './';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveConfig() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_startConfigNetwork, startConfig.value.network);
|
||||
await prefs.setString(_startConfigAddress, startConfig.value.address);
|
||||
await putConfig(downloaderConfig.value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../../generated/locales.g.dart';
|
||||
import '../../../../i18n/messages.dart';
|
||||
import '../../../../theme/theme.dart';
|
||||
import '../../../routes/app_pages.dart';
|
||||
import '../controllers/app_controller.dart';
|
||||
|
||||
class AppView extends GetView<AppController> {
|
||||
const AppView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final config = controller.downloaderConfig.value;
|
||||
return GetMaterialApp.router(
|
||||
useInheritedMediaQuery: true,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: GopeedTheme.light,
|
||||
darkTheme: GopeedTheme.dark,
|
||||
themeMode: ThemeMode.values.byName(config.extra.themeMode),
|
||||
// translations: messages,
|
||||
translationsKeys: AppTranslation.translations,
|
||||
locale: toLocale(config.extra.locale),
|
||||
fallbackLocale: fallbackLocale,
|
||||
localizationsDelegates: const [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: messages.keys.keys.map((e) => toLocale(e)).toList(),
|
||||
getPages: AppPages.routes,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/create_controller.dart';
|
||||
|
||||
class CreateBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<CreateController>(
|
||||
() => CreateController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class CreateController extends GetxController {
|
||||
final files = [].obs;
|
||||
final RxList selectedIndexs = [].obs;
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/api/model/resolve_result.dart';
|
||||
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 '../../../../util/util.dart';
|
||||
import '../../../routes/app_pages.dart';
|
||||
import '../../../views/views/directory_selector.dart';
|
||||
import '../../../views/views/file_list_view.dart';
|
||||
import '../../app/controllers/app_controller.dart';
|
||||
import '../controllers/create_controller.dart';
|
||||
|
||||
class CreateView extends GetView<CreateController> {
|
||||
final _resolveFormKey = GlobalKey<FormState>();
|
||||
|
||||
final _urlController = TextEditingController();
|
||||
final _confirmController = RoundedLoadingButtonController();
|
||||
|
||||
CreateView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Get.rootDelegate.offNamed(Routes.DOWNLOADING)),
|
||||
// actions: [],
|
||||
title: Text('create.title'.tr),
|
||||
),
|
||||
body: DropTarget(
|
||||
onDragDone: (details) {
|
||||
_urlController.text = details.files[0].path;
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
|
||||
child: Form(
|
||||
key: _resolveFormKey,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
autofocus: true,
|
||||
controller: _urlController,
|
||||
minLines: 1,
|
||||
maxLines: 30,
|
||||
decoration: InputDecoration(
|
||||
hintText: _hitText(),
|
||||
hintStyle: const TextStyle(fontSize: 12),
|
||||
labelText: 'create.downloadLink'.tr,
|
||||
icon: const Icon(Icons.link)),
|
||||
validator: (v) {
|
||||
return v!.trim().isNotEmpty
|
||||
? null
|
||||
: 'create.downloadLinkValid'.tr;
|
||||
}),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(15.0),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
width: 150,
|
||||
height: 40,
|
||||
),
|
||||
child: RoundedLoadingButton(
|
||||
color: Get.theme.colorScheme.secondary,
|
||||
onPressed: () async {
|
||||
if (_resolveFormKey.currentState!.validate()) {
|
||||
_confirmController.start();
|
||||
try {
|
||||
final rr = await resolve(Request(
|
||||
url: _urlController.text,
|
||||
));
|
||||
await _showResolveDialog(rr);
|
||||
} catch (e) {
|
||||
Get.snackbar('error'.tr, e.toString());
|
||||
} finally {
|
||||
_confirmController.reset();
|
||||
}
|
||||
}
|
||||
},
|
||||
controller: _confirmController,
|
||||
child: Text('confirm'.tr),
|
||||
),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _hitText() {
|
||||
return 'create.downloadLinkHit'.trParams({
|
||||
'append': Util.isDesktop() ? 'create.downloadLinkHitDesktop'.tr : '',
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _showResolveDialog(ResolveResult rr) async {
|
||||
controller.files.value = rr.res.files;
|
||||
final appController = Get.find<AppController>();
|
||||
final setting = appController.downloaderConfig.value;
|
||||
|
||||
final createFormKey = GlobalKey<FormState>();
|
||||
final pathController = TextEditingController(text: setting.downloadDir);
|
||||
final downloadController = RoundedLoadingButtonController();
|
||||
|
||||
return showDialog<void>(
|
||||
context: Get.context!,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => AlertDialog(
|
||||
content: Builder(
|
||||
builder: (context) {
|
||||
// Get available height and width of the build area of this widget. Make a choice depending on the size.
|
||||
var height = MediaQuery.of(context).size.height;
|
||||
var width = MediaQuery.of(context).size.width;
|
||||
|
||||
return SizedBox(
|
||||
height: height * 0.5,
|
||||
width: width,
|
||||
child: Form(
|
||||
key: createFormKey,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(child: FileListView()),
|
||||
DirectorySelector(
|
||||
controller: pathController,
|
||||
),
|
||||
],
|
||||
)),
|
||||
);
|
||||
},
|
||||
),
|
||||
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 {
|
||||
if (createFormKey.currentState!.validate()) {
|
||||
downloadController.start();
|
||||
|
||||
// if (Util.isAndroid()) {
|
||||
// if (!await Permission.storage.request().isGranted) {
|
||||
// Get.snackbar('error'.tr,
|
||||
// 'create.error.noStoragePermission'.tr);
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
await createTask(CreateTask(
|
||||
rid: rr.id,
|
||||
opts: Options(
|
||||
name: '',
|
||||
path: pathController.text,
|
||||
selectFiles:
|
||||
controller.selectedIndexs.cast<int>())));
|
||||
Get.back();
|
||||
Get.rootDelegate.offNamed(Routes.DOWNLOADING);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar('error'.tr, e.toString());
|
||||
rethrow;
|
||||
} finally {
|
||||
downloadController.reset();
|
||||
}
|
||||
},
|
||||
controller: downloadController,
|
||||
child: Text('create.download'.tr),
|
||||
),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/downloaded_controller.dart';
|
||||
|
||||
class DownloadedBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<DownloadedController>(
|
||||
() => DownloadedController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../../api/api.dart';
|
||||
import '../../../../api/model/task.dart';
|
||||
|
||||
class DownloadedController extends GetxController {
|
||||
final tasks = <Task>[].obs;
|
||||
late final Timer _timer;
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
super.onInit();
|
||||
await getTasksState();
|
||||
_timer = Timer.periodic(const Duration(milliseconds: 1000), (timer) async {
|
||||
// debugPrint('timer${DateTime.now()}');
|
||||
await getTasksState();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
super.onClose();
|
||||
_timer.cancel();
|
||||
}
|
||||
|
||||
getTasksState() async {
|
||||
tasks.value = await getTasks([Status.done]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/app/views/views/buid_task_list_view.dart';
|
||||
|
||||
import '../controllers/downloaded_controller.dart';
|
||||
|
||||
class DownloadedView extends GetView<DownloadedController> {
|
||||
const DownloadedView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BuildTaskListView(tasks: controller.tasks);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/downloading_controller.dart';
|
||||
|
||||
class DownloadingBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<DownloadingController>(
|
||||
() => DownloadingController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../../api/api.dart';
|
||||
import '../../../../api/model/task.dart';
|
||||
|
||||
class DownloadingController extends GetxController {
|
||||
final tasks = <Task>[].obs;
|
||||
|
||||
late final Timer _timer;
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
super.onInit();
|
||||
await getTasksState();
|
||||
_timer = Timer.periodic(const Duration(milliseconds: 1000), (timer) async {
|
||||
// debugPrint('timer${DateTime.now()}');
|
||||
await getTasksState();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
super.onClose();
|
||||
_timer.cancel();
|
||||
}
|
||||
|
||||
getTasksState() async {
|
||||
tasks.value = await getTasks(
|
||||
[Status.ready, Status.running, Status.pause, Status.error]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../views/views/buid_task_list_view.dart';
|
||||
import '../controllers/downloading_controller.dart';
|
||||
|
||||
class DownloadingView extends GetView<DownloadingController> {
|
||||
const DownloadingView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BuildTaskListView(tasks: controller.tasks);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/home_controller.dart';
|
||||
|
||||
class HomeBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<HomeController>(
|
||||
() => HomeController(),
|
||||
fenix: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class HomeController extends GetxController {
|
||||
var currentIndex = 0.obs;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:stylish_bottom_bar/model/bar_items.dart';
|
||||
import 'package:stylish_bottom_bar/stylish_bottom_bar.dart';
|
||||
|
||||
import '../../../routes/app_pages.dart';
|
||||
import '../../../views/views/responsive_builder.dart';
|
||||
import '../controllers/home_controller.dart';
|
||||
|
||||
class HomeView extends GetView<HomeController> {
|
||||
const HomeView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetRouterOutlet.builder(builder: (context, delegate, currentRoute) {
|
||||
return Scaffold(
|
||||
extendBody: true,
|
||||
body: Row(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
!ResponsiveBuilder.isNarrow(context)
|
||||
? NavigationRail(
|
||||
extended: true,
|
||||
labelType: NavigationRailLabelType.none,
|
||||
minExtendedWidth: 140,
|
||||
groupAlignment: 0,
|
||||
// useIndicator: false,
|
||||
onDestinationSelected: (int index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
delegate.toNamed(Routes.DOWNLOADING);
|
||||
controller.currentIndex.value = 0;
|
||||
break;
|
||||
case 1:
|
||||
delegate.toNamed(Routes.DOWNLOADED);
|
||||
controller.currentIndex.value = 1;
|
||||
break;
|
||||
case 2:
|
||||
delegate.toNamed(Routes.SETTING);
|
||||
controller.currentIndex.value = 2;
|
||||
break;
|
||||
}
|
||||
},
|
||||
destinations: [
|
||||
NavigationRailDestination(
|
||||
icon: const Icon(Icons.format_align_left_outlined),
|
||||
selectedIcon:
|
||||
const Icon(Icons.format_align_left_outlined),
|
||||
label: Text('downloading'.tr),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: const Icon(Icons.format_align_justify_outlined),
|
||||
selectedIcon:
|
||||
const Icon(Icons.format_align_justify_outlined),
|
||||
label: Text('downloaded'.tr),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: const Icon(Icons.settings),
|
||||
selectedIcon: const Icon(Icons.settings),
|
||||
label: Text('setting'.tr),
|
||||
),
|
||||
],
|
||||
selectedIndex: controller.currentIndex.value,
|
||||
leading: const Icon(Icons.logo_dev),
|
||||
// trailing: const Icon(Icons.info_outline),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: GetRouterOutlet(
|
||||
initialRoute: Routes.DOWNLOADING,
|
||||
// anchorRoute: '/',
|
||||
// filterPages: (afterAnchor) {
|
||||
// logger.w(afterAnchor);
|
||||
// logger.w(afterAnchor.take(1));
|
||||
// return afterAnchor.take(1);
|
||||
// },
|
||||
))
|
||||
]),
|
||||
bottomNavigationBar: ResponsiveBuilder.isNarrow(context)
|
||||
? StylishBottomBar(
|
||||
option: AnimatedBarOptions(
|
||||
iconSize: 32,
|
||||
barAnimation: BarAnimation.blink,
|
||||
iconStyle: IconStyle.Default,
|
||||
opacity: 0.3,
|
||||
),
|
||||
items: [
|
||||
BottomBarItem(
|
||||
icon: const Icon(Icons.format_align_left_outlined),
|
||||
selectedColor: Get.theme.primaryColor,
|
||||
title: Text('downloading'.tr)),
|
||||
BottomBarItem(
|
||||
icon: const Icon(Icons.format_align_justify_outlined),
|
||||
selectedColor: Get.theme.primaryColor,
|
||||
title: Text('downloaded'.tr)),
|
||||
BottomBarItem(
|
||||
icon: const Icon(Icons.settings),
|
||||
selectedColor: Get.theme.primaryColor,
|
||||
title: Text('setting'.tr)),
|
||||
],
|
||||
// hasNotch: true,
|
||||
currentIndex: controller.currentIndex.value,
|
||||
onTap: (index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
delegate.toNamed(Routes.DOWNLOADING);
|
||||
controller.currentIndex.value = 0;
|
||||
break;
|
||||
case 1:
|
||||
delegate.toNamed(Routes.DOWNLOADED);
|
||||
controller.currentIndex.value = 1;
|
||||
break;
|
||||
case 2:
|
||||
delegate.toNamed(Routes.SETTING);
|
||||
controller.currentIndex.value = 2;
|
||||
break;
|
||||
}
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/root_controller.dart';
|
||||
|
||||
class RootBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<RootController>(() => RootController(), fenix: true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class RootController extends GetxController {}
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../routes/app_pages.dart';
|
||||
import '../controllers/root_controller.dart';
|
||||
|
||||
class RootView extends GetView<RootController> {
|
||||
const RootView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetRouterOutlet.builder(
|
||||
builder: (context, delegate, current) {
|
||||
return GetRouterOutlet(
|
||||
initialRoute: Routes.HOME,
|
||||
// anchorRoute: '/',
|
||||
// filterPages: (afterAnchor) {
|
||||
// return afterAnchor.take(1);
|
||||
// },
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/setting_controller.dart';
|
||||
|
||||
class SettingBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<SettingController>(
|
||||
() => SettingController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class SettingController extends GetxController {
|
||||
final tapStatues = <String, bool>{}.obs;
|
||||
|
||||
// set all tap status to false
|
||||
void clearTap() {
|
||||
tapStatues.updateAll((key, value) => false);
|
||||
}
|
||||
|
||||
// set one tap status to true
|
||||
void onTap(String key) {
|
||||
clearTap();
|
||||
tapStatues[key] = true;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,68 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../modules/create/bindings/create_binding.dart';
|
||||
import '../modules/create/views/create_view.dart';
|
||||
import '../modules/downloaded/bindings/downloaded_binding.dart';
|
||||
import '../modules/downloaded/views/downloaded_view.dart';
|
||||
import '../modules/downloading/bindings/downloading_binding.dart';
|
||||
import '../modules/downloading/views/downloading_view.dart';
|
||||
import '../modules/home/bindings/home_binding.dart';
|
||||
import '../modules/home/views/home_view.dart';
|
||||
import '../modules/root/bindings/root_binding.dart';
|
||||
import '../modules/root/views/root_view.dart';
|
||||
import '../modules/setting/bindings/setting_binding.dart';
|
||||
import '../modules/setting/views/setting_view.dart';
|
||||
|
||||
part 'app_routes.dart';
|
||||
|
||||
class AppPages {
|
||||
AppPages._();
|
||||
|
||||
static const INITIAL = Routes.ROOT;
|
||||
|
||||
static final routes = [
|
||||
GetPage(
|
||||
name: _Paths.ROOT,
|
||||
participatesInRootNavigator: true,
|
||||
transition: Transition.topLevel,
|
||||
// preventDuplicates: true,
|
||||
page: () => const RootView(),
|
||||
binding: RootBinding(),
|
||||
children: [
|
||||
GetPage(
|
||||
name: _Paths.HOME,
|
||||
// participatesInRootNavigator: true,
|
||||
// transition: Transition.topLevel,
|
||||
// preventDuplicates: true,
|
||||
page: () => const HomeView(),
|
||||
binding: HomeBinding(),
|
||||
children: [
|
||||
GetPage(
|
||||
name: _Paths.DOWNLOADED,
|
||||
transition: Transition.noTransition,
|
||||
page: () => const DownloadedView(),
|
||||
binding: DownloadedBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.DOWNLOADING,
|
||||
transition: Transition.noTransition,
|
||||
page: () => const DownloadingView(),
|
||||
binding: DownloadingBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.SETTING,
|
||||
transition: Transition.noTransition,
|
||||
page: () => const SettingView(),
|
||||
binding: SettingBinding(),
|
||||
),
|
||||
]),
|
||||
GetPage(
|
||||
name: _Paths.CREATE,
|
||||
transition: Transition.downToUp,
|
||||
// preventDuplicates: true,
|
||||
page: () => CreateView(),
|
||||
binding: CreateBinding(),
|
||||
),
|
||||
]),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
part of 'app_pages.dart';
|
||||
// DO NOT EDIT. This is code generated via package:get_cli/get_cli.dart
|
||||
|
||||
abstract class Routes {
|
||||
Routes._();
|
||||
static const DOWNLOADED = _Paths.HOME + _Paths.DOWNLOADED;
|
||||
static const DOWNLOADING = _Paths.HOME + _Paths.DOWNLOADING;
|
||||
static const CREATE = _Paths.CREATE;
|
||||
static const ROOT = _Paths.ROOT;
|
||||
static const SETTING = _Paths.HOME + _Paths.SETTING;
|
||||
static const HOME = _Paths.HOME;
|
||||
}
|
||||
|
||||
abstract class _Paths {
|
||||
_Paths._();
|
||||
static const DOWNLOADED = '/downloaded';
|
||||
static const DOWNLOADING = '/downloading';
|
||||
static const CREATE = '/create';
|
||||
static const ROOT = '/';
|
||||
static const SETTING = '/setting';
|
||||
static const HOME = '/home';
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../api/api.dart';
|
||||
import '../../../api/model/task.dart';
|
||||
import '../../../util/util.dart';
|
||||
import '../../routes/app_pages.dart';
|
||||
|
||||
class BuildTaskListView extends GetView {
|
||||
final List<Task> tasks;
|
||||
|
||||
const BuildTaskListView({
|
||||
Key? key,
|
||||
required this.tasks,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'create.title'.tr,
|
||||
onPressed: () {
|
||||
Get.rootDelegate.toNamed(Routes.CREATE);
|
||||
},
|
||||
),
|
||||
//TODO appBar toggleALl/start selected/delete selected/
|
||||
// IconButton(
|
||||
// icon: const Icon(Icons.pause),
|
||||
// tooltip: 'create.title'.tr,
|
||||
// onPressed: () {
|
||||
// // pause all
|
||||
// },
|
||||
// ),
|
||||
// IconButton(
|
||||
// icon: const Icon(Icons.delete),
|
||||
// tooltip: 'create.title'.tr,
|
||||
// onPressed: () {
|
||||
// // delete all
|
||||
// },
|
||||
// )
|
||||
],
|
||||
),
|
||||
body: Obx(() {
|
||||
return buildTaskList(context, tasks);
|
||||
}));
|
||||
}
|
||||
|
||||
Widget buildTaskList(BuildContext context, tasks) {
|
||||
return ListView.builder(
|
||||
itemCount: tasks.length,
|
||||
itemBuilder: (context, index) {
|
||||
return item(context, tasks[index]);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget item(BuildContext context, Task task) {
|
||||
bool isDone() {
|
||||
return task.status == Status.done;
|
||||
}
|
||||
|
||||
bool isRunning() {
|
||||
return task.status == Status.running;
|
||||
}
|
||||
|
||||
Future<void> showDeleteDialog(String id) {
|
||||
final keep = true.obs;
|
||||
|
||||
final context = Get.context!;
|
||||
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => AlertDialog(
|
||||
title: Text('task.deleteTask'.tr),
|
||||
content: Obx(() => CheckboxListTile(
|
||||
value: keep.value,
|
||||
title: Text('task.deleteTaskTip'.tr,
|
||||
style: context.textTheme.bodyLarge),
|
||||
onChanged: (v) {
|
||||
keep.value = v!;
|
||||
})),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text('cancel'.tr),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
'task.delete'.tr,
|
||||
style: const TextStyle(color: Colors.redAccent),
|
||||
),
|
||||
onPressed: () async {
|
||||
try {
|
||||
await deleteTask(id, !keep.value);
|
||||
Get.back();
|
||||
} catch (e) {
|
||||
Get.snackbar('error'.tr, e.toString());
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
List<Widget> buildActions() {
|
||||
final list = <Widget>[];
|
||||
if (isDone()) {
|
||||
if (Util.isDesktop()) {
|
||||
list.add(IconButton(
|
||||
icon: const Icon(Icons.folder_open),
|
||||
onPressed: () async {
|
||||
final file = File(Util.buildAbsPath(task.meta.opts.path,
|
||||
task.meta.res.files[0].path, task.meta.res.files[0].name));
|
||||
await launchUrl(file.parent.uri);
|
||||
},
|
||||
));
|
||||
}
|
||||
} else {
|
||||
if (isRunning()) {
|
||||
list.add(IconButton(
|
||||
icon: const Icon(Icons.pause),
|
||||
onPressed: () async {
|
||||
await pauseTask(task.id);
|
||||
},
|
||||
));
|
||||
} else {
|
||||
list.add(IconButton(
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
onPressed: () async {
|
||||
await continueTask(task.id);
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
list.add(IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () {
|
||||
showDeleteDialog(task.id);
|
||||
},
|
||||
));
|
||||
return list;
|
||||
}
|
||||
|
||||
double getProgress() {
|
||||
return task.size <= 0 ? 1 : task.progress.downloaded / task.size;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
// onTap: () {},
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Stack(children: [
|
||||
!isDone()
|
||||
? Opacity(
|
||||
opacity: 0.6,
|
||||
child: LinearProgressIndicator(
|
||||
backgroundColor: Colors.transparent,
|
||||
color: pickColor(),
|
||||
minHeight: 66,
|
||||
value: getProgress(),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
ListTile(
|
||||
title: Text(task.meta.res.name,
|
||||
maxLines: 2, overflow: TextOverflow.ellipsis),
|
||||
subtitle: Text(
|
||||
"${isDone() ? "" : "${Util.fmtByte(task.progress.downloaded)} / "}${Util.fmtByte(task.size)}",
|
||||
style: context.textTheme.bodyLarge
|
||||
?.copyWith(color: Get.theme.disabledColor),
|
||||
),
|
||||
leading: const Icon(Icons.insert_drive_file),
|
||||
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()
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
])
|
||||
]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class CheckListView extends StatefulWidget {
|
||||
final List<String> items;
|
||||
final List<String> checked;
|
||||
final void Function(List<String> value) onChanged;
|
||||
|
||||
const CheckListView({
|
||||
Key? key,
|
||||
required this.items,
|
||||
required this.checked,
|
||||
required this.onChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<CheckListView> createState() => _CheckListView();
|
||||
}
|
||||
|
||||
class _CheckListView extends State<CheckListView> {
|
||||
bool get _allChecked => _checked.length == _items.length;
|
||||
|
||||
List<String> get _checked => widget.checked;
|
||||
|
||||
List<String> get _items => widget.items;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 10),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey, width: 1),
|
||||
borderRadius: BorderRadius.circular(5)),
|
||||
child: Column(
|
||||
children: [
|
||||
CheckboxListTile(
|
||||
value: _allChecked,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_checked.clear();
|
||||
if (value!) {
|
||||
_checked.addAll(_items);
|
||||
}
|
||||
widget.onChanged(_checked);
|
||||
});
|
||||
},
|
||||
title: Text('selectAll'.tr),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: _items.length,
|
||||
itemBuilder: (context, index) {
|
||||
var item = _items[index];
|
||||
return CheckboxListTile(
|
||||
value: _checked.contains(item),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_checked.contains(item)
|
||||
? _checked.remove(item)
|
||||
: _checked.add(item);
|
||||
widget.onChanged(_checked);
|
||||
});
|
||||
},
|
||||
title: Text(item),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../util/mac_secure_util.dart';
|
||||
import '../../../util/util.dart';
|
||||
|
||||
class DirectorySelector extends StatefulWidget {
|
||||
final TextEditingController controller;
|
||||
final bool showLabel;
|
||||
|
||||
const DirectorySelector(
|
||||
{Key? key, required this.controller, this.showLabel = true})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<DirectorySelector> createState() => _DirectorySelectorState();
|
||||
}
|
||||
|
||||
class _DirectorySelectorState extends State<DirectorySelector> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
readOnly: Util.isWeb() ? false : true,
|
||||
controller: widget.controller,
|
||||
decoration: widget.showLabel
|
||||
? InputDecoration(
|
||||
labelText: 'downloadDir'.tr,
|
||||
)
|
||||
: null,
|
||||
validator: (v) {
|
||||
return v!.trim().isNotEmpty ? null : 'downloadDirValid'.tr;
|
||||
},
|
||||
)),
|
||||
!Util.isDesktop()
|
||||
? null
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.folder_open),
|
||||
onPressed: () async {
|
||||
if (GetPlatform.isDesktop) {
|
||||
var dir = await getDirectoryPath();
|
||||
if (dir != null) {
|
||||
widget.controller.text = dir;
|
||||
MacSecureUtil.saveBookmark(dir);
|
||||
}
|
||||
}
|
||||
})
|
||||
].where((e) => e != null).map((e) => e!).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart' as fluent;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../../../util/file_icon.dart';
|
||||
import '../../../util/util.dart';
|
||||
import '../../modules/app/controllers/app_controller.dart';
|
||||
import '../../modules/create/controllers/create_controller.dart';
|
||||
|
||||
class FileListView extends GetView {
|
||||
FileListView({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
final parentController = Get.find<CreateController>();
|
||||
final appController = Get.find<AppController>();
|
||||
|
||||
List<fluent.TreeViewItem> buildTreeViewItemsRecursive(
|
||||
List fileInfos, int level, List<fluent.TreeViewItem> treeViewItems) {
|
||||
List children = fileInfos.where((e) => e['level'] == level).toList();
|
||||
for (int i = 0; i < children.length; i++) {
|
||||
Map fileInfo = children[i];
|
||||
if (fileInfo['size'] == null) {
|
||||
// folder
|
||||
treeViewItems.add(fluent.TreeViewItem(
|
||||
// expanded: false, bug on init
|
||||
leading: const Icon(Icons.folder_open),
|
||||
content: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
fileInfo['name'],
|
||||
overflow: TextOverflow.ellipsis,
|
||||
// style: context.textTheme.titleSmall,
|
||||
)),
|
||||
]),
|
||||
// onExpandToggle: (Fluent.TreeViewItem item, bool getsExpanded) {
|
||||
//TODO
|
||||
// item.expanded = getsExpanded;
|
||||
// },
|
||||
children: buildTreeViewItemsRecursive(
|
||||
fileInfos.where((e) => e['level'] > level).toList(),
|
||||
level + 1, [])));
|
||||
} else {
|
||||
// file
|
||||
if (fileInfo['selected']) {
|
||||
parentController.selectedIndexs.add(fileInfo['fileId']);
|
||||
}
|
||||
treeViewItems.add(fluent.TreeViewItem(
|
||||
value: fileInfo['fileId'],
|
||||
selected: fileInfo['selected'],
|
||||
collapsable: false,
|
||||
leading: fileInfo['name'].lastIndexOf('.') == -1
|
||||
? const Icon(fluent.FluentIcons.document)
|
||||
: Icon(fluent.FluentIcons.allIcons[findIcon(fileInfo['name']
|
||||
.substring(fileInfo['name'].lastIndexOf('.') + 1))]),
|
||||
content: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
fileInfo['name'],
|
||||
overflow: TextOverflow.ellipsis,
|
||||
// style: context.textTheme.titleSmall,
|
||||
)),
|
||||
Text(
|
||||
Util.fmtByte(
|
||||
fileInfo['size'],
|
||||
),
|
||||
// style: context.textTheme.labelMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
]),
|
||||
));
|
||||
}
|
||||
}
|
||||
return treeViewItems;
|
||||
}
|
||||
|
||||
List<fluent.TreeViewItem> get items {
|
||||
List fileInfos = [];
|
||||
int idNext = 0;
|
||||
//make fileInfos list
|
||||
for (var i = 0; i < parentController.files.length; i++) {
|
||||
//parentId -1 means path root
|
||||
int parentId = -1;
|
||||
List folders = path.split(parentController.files[i].path);
|
||||
for (var folder in folders) {
|
||||
int indexInfileInfos = fileInfos.lastIndexWhere((fileInfo) =>
|
||||
fileInfo['name'] == folder && fileInfo['parentId'] == parentId);
|
||||
//TODO calculate folder size
|
||||
if (indexInfileInfos != -1) {
|
||||
// folder exists
|
||||
parentId = fileInfos[indexInfileInfos]['id'];
|
||||
} else {
|
||||
// parent not exist
|
||||
// create one and add index to parent's children
|
||||
fileInfos.add({
|
||||
'id': idNext,
|
||||
'type': 'folder',
|
||||
'name': folder,
|
||||
'parentId': parentId,
|
||||
'level': folders.indexOf(folder),
|
||||
'children': [],
|
||||
});
|
||||
if (parentId != -1) {
|
||||
fileInfos[parentId]['children'].add(idNext);
|
||||
}
|
||||
parentId = idNext;
|
||||
idNext++;
|
||||
}
|
||||
}
|
||||
//add one file, add index to parent
|
||||
fileInfos.add({
|
||||
'id': idNext,
|
||||
'type': 'file',
|
||||
'fileId': i,
|
||||
'level': folders.length,
|
||||
'name': parentController.files[i].name,
|
||||
'size': parentController.files[i].size,
|
||||
'parentId': parentId,
|
||||
//TODO add unselected logic
|
||||
'selected': true
|
||||
});
|
||||
if (parentId != -1) {
|
||||
fileInfos[parentId]['children'].add(idNext);
|
||||
}
|
||||
idNext++;
|
||||
}
|
||||
List<fluent.TreeViewItem> treeItems =
|
||||
buildTreeViewItemsRecursive(fileInfos, 0, []);
|
||||
return treeItems;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appController = Get.find<AppController>();
|
||||
return fluent.FluentTheme(
|
||||
data: appController.downloaderConfig.value.extra.themeMode == 'dark'
|
||||
? fluent.ThemeData(brightness: Brightness.dark)
|
||||
: fluent.ThemeData(brightness: Brightness.light),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(padding: EdgeInsets.only(top: 10)),
|
||||
Text(
|
||||
'create.selectFile'.tr,
|
||||
// style: TextStyle(color: themeData.hintColor),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 10),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey, width: 1),
|
||||
borderRadius: BorderRadius.circular(5)),
|
||||
child: fluent.TreeView(
|
||||
//changes all when folder selector is changed
|
||||
onSelectionChanged: (selectedItems) async => {
|
||||
parentController.selectedIndexs.value =
|
||||
selectedItems.map((j) => j.value).toList()
|
||||
// for (var i = 0; i < parentController.files.length; i++)
|
||||
// {
|
||||
// selectedItems
|
||||
// .map((j) => j.value)
|
||||
// .toList()
|
||||
// .contains(i)
|
||||
// ? values[i] = true
|
||||
// : values[i] = false
|
||||
// }
|
||||
},
|
||||
narrowSpacing: true,
|
||||
scrollPrimary: true,
|
||||
selectionMode: fluent.TreeViewSelectionMode.multiple,
|
||||
items: items))),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class OutlinedButtonLoading extends StatefulWidget {
|
||||
final Widget child;
|
||||
final VoidCallback onPressed;
|
||||
final OutlinedButtonLoadingController controller;
|
||||
|
||||
const OutlinedButtonLoading(
|
||||
{super.key,
|
||||
required this.child,
|
||||
required this.onPressed,
|
||||
required this.controller});
|
||||
|
||||
@override
|
||||
State<OutlinedButtonLoading> createState() => _OutlinedButtonLoadingState();
|
||||
}
|
||||
|
||||
class _OutlinedButtonLoadingState extends State<OutlinedButtonLoading> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder<bool>(
|
||||
valueListenable: widget.controller,
|
||||
builder: (context, value, child) {
|
||||
return OutlinedButton(
|
||||
key: widget.key,
|
||||
onPressed: value ? null : widget.onPressed,
|
||||
child: value
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: widget.child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OutlinedButtonLoadingController extends ValueNotifier<bool> {
|
||||
OutlinedButtonLoadingController() : super(false);
|
||||
|
||||
void start() {
|
||||
value = true;
|
||||
}
|
||||
|
||||
void stop() {
|
||||
value = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ResponsiveBuilder extends StatelessWidget {
|
||||
const ResponsiveBuilder({
|
||||
required this.narrowBuilder,
|
||||
required this.mediumBuilder,
|
||||
required this.wideBuilder,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final Widget Function(
|
||||
BuildContext context,
|
||||
BoxConstraints constraints,
|
||||
) narrowBuilder;
|
||||
|
||||
final Widget Function(
|
||||
BuildContext context,
|
||||
BoxConstraints constraints,
|
||||
) mediumBuilder;
|
||||
|
||||
final Widget Function(
|
||||
BuildContext context,
|
||||
BoxConstraints constraints,
|
||||
) wideBuilder;
|
||||
|
||||
static bool isNarrow(BuildContext context) =>
|
||||
MediaQuery.of(context).size.width < 768;
|
||||
|
||||
static bool isMedium(BuildContext context) =>
|
||||
MediaQuery.of(context).size.width < 992 &&
|
||||
MediaQuery.of(context).size.width >= 768;
|
||||
|
||||
static bool isWide(BuildContext context) =>
|
||||
MediaQuery.of(context).size.width >= 992;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth >= 992) {
|
||||
return wideBuilder(context, constraints);
|
||||
} else if (constraints.maxWidth >= 768) {
|
||||
return mediumBuilder(context, constraints);
|
||||
} else {
|
||||
return narrowBuilder(context, constraints);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
Reference in new issue
Block a user