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:
Neginegiandwuke2022 authored and GitHub committed 2023-02-19 20:06:28 +08:00
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
+68
View File
@@ -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(),
),
]),
];
}
+22
View File
@@ -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 @@