10 Commits

Author SHA1 Message Date
NewTab
558b780235 Merge pull request #4 from huoxue1/main
新增订阅管理,新增环境变量备份和导入导出
2022-06-23 16:43:12 +08:00
3343780376
1cd3724805 新增订阅管理
新增环境变量导入和导出
2022-06-23 16:30:39 +08:00
jyuesong
7e3a63169e 依赖管理支持批量添加 2022-06-16 15:54:19 +08:00
jyuesong
1298dba590 支持上传脚本 2022-06-16 14:37:47 +08:00
jyuesong
2005083d2e 新增代码文件支持行号显示 2022-06-15 11:05:49 +08:00
jyuesong
4e3f8a0df9 1.1.0 release 2022-06-08 10:42:26 +08:00
jyuesong
5698277301 1.1.0 release 2022-06-08 10:18:24 +08:00
jyuesong
02be737264 1.1.0 release 2022-06-08 10:11:49 +08:00
jyuesong
9558f3d235 1.1.0 release 2022-06-08 10:11:32 +08:00
jyuesong
14c3b1a965 优化使用体验 2022-06-08 10:06:54 +08:00
178 changed files with 9323 additions and 1268 deletions

1
.gitignore vendored
View File

@@ -44,3 +44,4 @@ app.*.map.json
/android/app/debug /android/app/debug
/android/app/profile /android/app/profile
/android/app/release /android/app/release
.fvm/

View File

@@ -1,10 +1,36 @@
# This file tracks properties of this Flutter project. # This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc. # Used by Flutter tool to assess capabilities and perform upgrades etc.
# #
# This file should be version controlled and should not be manually edited. # This file should be version controlled.
version: version:
revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268
channel: stable channel: stable
project_type: app project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268
base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268
- platform: linux
create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268
base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268
- platform: macos
create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268
base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268
- platform: windows
create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268
base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
assets/images/js.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
assets/images/json.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
assets/images/other.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
assets/images/py.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
assets/images/shell.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
assets/images/ts.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -24,12 +24,14 @@ class BaseStateWidget<T extends BaseViewModel> extends ConsumerStatefulWidget {
_BaseStateWidgetState<T> createState() => _BaseStateWidgetState<T>(); _BaseStateWidgetState<T> createState() => _BaseStateWidgetState<T>();
} }
class _BaseStateWidgetState<T extends BaseViewModel> extends ConsumerState<BaseStateWidget<T>> with LazyLoadState<BaseStateWidget<T>> { class _BaseStateWidgetState<T extends BaseViewModel>
extends ConsumerState<BaseStateWidget<T>>
with LazyLoadState<BaseStateWidget<T>> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var viewModel = ref.watch<T>(widget.model); var viewModel = ref.watch<T>(widget.model);
if (viewModel.failedToast != null) { if (viewModel.failedToast != null) {
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
(viewModel.failedToast ?? "").toast(); (viewModel.failedToast ?? "").toast();
viewModel.clearToast(); viewModel.clearToast();
}); });
@@ -72,7 +74,7 @@ class _BaseStateWidgetState<T extends BaseViewModel> extends ConsumerState<BaseS
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (widget.onReady != null && !widget.lazyLoad) { if (widget.onReady != null && !widget.lazyLoad) {
widget.onReady!(ref.read<T>(widget.model)); widget.onReady!(ref.read<T>(widget.model));
} }

View File

@@ -9,6 +9,7 @@ import 'package:qinglong_app/module/login/user_bean.dart';
import 'package:qinglong_app/module/others/dependencies/dependency_bean.dart'; import 'package:qinglong_app/module/others/dependencies/dependency_bean.dart';
import 'package:qinglong_app/module/others/login_log/login_log_bean.dart'; import 'package:qinglong_app/module/others/login_log/login_log_bean.dart';
import 'package:qinglong_app/module/others/scripts/script_bean.dart'; import 'package:qinglong_app/module/others/scripts/script_bean.dart';
import 'package:qinglong_app/module/others/subscription/subscription_bean.dart';
import 'package:qinglong_app/module/others/task_log/task_log_bean.dart'; import 'package:qinglong_app/module/others/task_log/task_log_bean.dart';
import 'package:qinglong_app/module/task/task_bean.dart'; import 'package:qinglong_app/module/task/task_bean.dart';
import 'package:qinglong_app/utils/utils.dart'; import 'package:qinglong_app/utils/utils.dart';
@@ -353,15 +354,10 @@ class Api {
); );
} }
static Future<HttpResponse<NullResponse>> addDependency(String name, int type) async { static Future<HttpResponse<NullResponse>> addDependency(List<Map<String, dynamic>> list) async {
return await Http.post<NullResponse>( return await Http.post<NullResponse>(
Url.dependencies, Url.dependencies,
[ list,
{
"name": name,
"type": type,
}
],
); );
} }
@@ -382,4 +378,89 @@ class Api {
[id], [id],
); );
} }
// 获取订阅
static Future<HttpResponse<List<Subscription>>> getSubscription()async{
return await Http.get<List<Subscription>>(
Url.subscriptions,
{}
);
}
// 新增订阅
static Future<HttpResponse<Subscription>> postSubscription(Subscription subscription)async{
var data = subscription.toJson();
data.remove("id");
data.remove("status");
data.remove("pid");
data.remove("log_path");
data.remove("is_disabled");
data.remove("createdAt");
data.remove("updatedAt");
data.remove("pullType");
data.remove("pullOption");
return await Http.post<Subscription>(
Url.subscriptions,
data
);
}
// 修改订阅
static Future<HttpResponse<Subscription>> putSubscription(Subscription subscription)async{
return await Http.put<Subscription>(
Url.subscriptions,
subscription.toJson()
);
}
// 删除订阅 参数未一个id数组
static Future<HttpResponse<NullResponse>> deleteSubscription(List<int> ids)async{
return await Http.delete<NullResponse>(
Url.subscriptions,
ids
);
}
// 删除订阅 参数未一个id数组
static Future<HttpResponse<NullResponse>> disableSubscription(List<int> ids)async{
return await Http.put<NullResponse>(
Url.disableSubscriptions,
ids
);
}
// 删除订阅 参数未一个id数组
static Future<HttpResponse<NullResponse>> enableSubscription(List<int> ids)async{
return await Http.put<NullResponse>(
Url.enableSubscriptions,
ids
);
}
// 开始执行订阅
static Future<HttpResponse<NullResponse>> startSubscription(
List<int> subscriptions) async {
return await Http.put<NullResponse>(
Url.runSubscriptions,
subscriptions,
);
}
// 暂停订阅执行
static Future<HttpResponse<NullResponse>> stopSubscription(
List<int> subscriptions) async {
return await Http.put<NullResponse>(
Url.stopSubscriptions,
subscriptions,
);
}
// 获取订阅的日志
static Future<HttpResponse<String>> subTimeLog(int subId) async {
return await Http.get<String>(
Url.subtimeLog(subId),
null,
);
}
} }

View File

@@ -122,7 +122,8 @@ class Http {
if (!pushedLoginPage) { if (!pushedLoginPage) {
"身份已过期,请重新登录".toast(); "身份已过期,请重新登录".toast();
pushedLoginPage = true; pushedLoginPage = true;
navigatorState.currentState?.pushNamedAndRemoveUntil(Routes.routeLogin, (route) => false); navigatorState.currentState
?.pushNamedAndRemoveUntil(Routes.routeLogin, (route) => false);
} }
} }
@@ -136,9 +137,15 @@ class Http {
} }
if (e.response != null && e.response!.data != null) { if (e.response != null && e.response!.data != null) {
return HttpResponse(success: false, message: e.response?.data["message"] ?? e.message, code: e.response?.data["code"] ?? 0); return HttpResponse(
success: false,
message: e.response?.data["message"] ?? e.message,
code: e.response?.data["code"] ?? 0);
} else { } else {
return HttpResponse(success: false, message: e.message, code: e.response?.statusCode ?? 0); return HttpResponse(
success: false,
message: e.message,
code: e.response?.statusCode ?? 0);
} }
} }
@@ -223,7 +230,8 @@ class HttpResponse<T> {
late int code; late int code;
T? bean; T? bean;
HttpResponse({required this.success, this.message, required this.code, this.bean}); HttpResponse(
{required this.success, this.message, required this.code, this.bean});
} }
class DeserializeAction<T> { class DeserializeAction<T> {

View File

@@ -7,8 +7,7 @@ import '../userinfo_viewmodel.dart';
class TokenInterceptor extends Interceptor { class TokenInterceptor extends Interceptor {
@override @override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) { void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
options.headers["User-Agent"] = options.headers["User-Agent"] = "qinglong_client";
"qinglong_client";
options.headers["Content-Type"] = "application/json;charset=UTF-8"; options.headers["Content-Type"] = "application/json;charset=UTF-8";

View File

@@ -14,70 +14,159 @@ class Url {
static const updatePassword = "/api/user"; static const updatePassword = "/api/user";
static get tasks => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons" : "/api/crons"; static get tasks => getIt<UserInfoViewModel>().useSecretLogined
? "/open/crons"
: "/api/crons";
static get runTasks => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/run" : "/api/crons/run"; static get runTasks => getIt<UserInfoViewModel>().useSecretLogined
? "/open/crons/run"
: "/api/crons/run";
static get stopTasks => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/stop" : "/api/crons/stop"; static get stopTasks => getIt<UserInfoViewModel>().useSecretLogined
? "/open/crons/stop"
: "/api/crons/stop";
static get taskDetail => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/" : "/api/crons/"; static get taskDetail => getIt<UserInfoViewModel>().useSecretLogined
? "/open/crons/"
: "/api/crons/";
static get addTask => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons" : "/api/crons"; static get addTask => getIt<UserInfoViewModel>().useSecretLogined
? "/open/crons"
: "/api/crons";
static get pinTask => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/pin" : "/api/crons/pin"; static get pinTask => getIt<UserInfoViewModel>().useSecretLogined
? "/open/crons/pin"
: "/api/crons/pin";
static get unpinTask => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/unpin" : "/api/crons/unpin"; static get unpinTask => getIt<UserInfoViewModel>().useSecretLogined
? "/open/crons/unpin"
: "/api/crons/unpin";
static get enableTask => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/enable" : "/api/crons/enable"; static get enableTask => getIt<UserInfoViewModel>().useSecretLogined
? "/open/crons/enable"
: "/api/crons/enable";
static get disableTask => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/disable" : "/api/crons/disable"; static get disableTask => getIt<UserInfoViewModel>().useSecretLogined
? "/open/crons/disable"
: "/api/crons/disable";
static get files => getIt<UserInfoViewModel>().useSecretLogined ? "/open/configs/files" : "/api/configs/files"; static get files => getIt<UserInfoViewModel>().useSecretLogined
? "/open/configs/files"
: "/api/configs/files";
static get configContent => getIt<UserInfoViewModel>().useSecretLogined ? "/open/configs/" : "/api/configs/"; static get configContent => getIt<UserInfoViewModel>().useSecretLogined
? "/open/configs/"
: "/api/configs/";
static get saveFile => getIt<UserInfoViewModel>().useSecretLogined ? "/open/configs/save" : "/api/configs/save"; static get saveFile => getIt<UserInfoViewModel>().useSecretLogined
? "/open/configs/save"
: "/api/configs/save";
static get envs => getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs" : "/api/envs"; static get envs =>
getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs" : "/api/envs";
static get addEnv => getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs" : "/api/envs"; static get addEnv =>
getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs" : "/api/envs";
static get delEnv => getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs" : "/api/envs"; static get delEnv =>
getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs" : "/api/envs";
static get disableEnvs => getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs/disable" : "/api/envs/disable"; static get disableEnvs => getIt<UserInfoViewModel>().useSecretLogined
? "/open/envs/disable"
: "/api/envs/disable";
static get enableEnvs => getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs/enable" : "/api/envs/enable"; static get enableEnvs => getIt<UserInfoViewModel>().useSecretLogined
? "/open/envs/enable"
: "/api/envs/enable";
static get loginLog => getIt<UserInfoViewModel>().useSecretLogined ? "/open/user/login-log" : "/api/user/login-log"; static get loginLog => getIt<UserInfoViewModel>().useSecretLogined
? "/open/user/login-log"
: "/api/user/login-log";
static get taskLog => getIt<UserInfoViewModel>().useSecretLogined ? "/open/logs" : "/api/logs"; static get taskLog =>
getIt<UserInfoViewModel>().useSecretLogined ? "/open/logs" : "/api/logs";
static get taskLogDetail => getIt<UserInfoViewModel>().useSecretLogined ? "/open/logs/" : "/api/logs/"; static get taskLogDetail => getIt<UserInfoViewModel>().useSecretLogined
? "/open/logs/"
: "/api/logs/";
static get scripts => getIt<UserInfoViewModel>().useSecretLogined ? "/open/scripts/files" : "/api/scripts/files"; static get scripts => getIt<UserInfoViewModel>().useSecretLogined
? "/open/scripts/files"
: "/api/scripts/files";
static get scripts2 => getIt<UserInfoViewModel>().useSecretLogined ? "/open/scripts" : "/api/scripts"; static get scripts2 => getIt<UserInfoViewModel>().useSecretLogined
? "/open/scripts"
: "/api/scripts";
static get scriptUpdate => getIt<UserInfoViewModel>().useSecretLogined ? "/open/scripts" : "/api/scripts"; static get scriptUpdate => getIt<UserInfoViewModel>().useSecretLogined
? "/open/scripts"
: "/api/scripts";
static get scriptDetail => getIt<UserInfoViewModel>().useSecretLogined ? "/open/scripts/" : "/api/scripts/"; static get scriptDetail => getIt<UserInfoViewModel>().useSecretLogined
? "/open/scripts/"
: "/api/scripts/";
static get dependencies => getIt<UserInfoViewModel>().useSecretLogined ? "/open/dependencies" : "/api/dependencies"; static get dependencies => getIt<UserInfoViewModel>().useSecretLogined
? "/open/dependencies"
: "/api/dependencies";
static get addScript => getIt<UserInfoViewModel>().useSecretLogined ? "/open/scripts" : "/api/scripts"; static get addScript => getIt<UserInfoViewModel>().useSecretLogined
? "/open/scripts"
: "/api/scripts";
static get dependencyReinstall => getIt<UserInfoViewModel>().useSecretLogined ? "/open/dependencies/reinstall" : "/api/dependencies/reinstall"; static get dependencyReinstall => getIt<UserInfoViewModel>().useSecretLogined
? "/open/dependencies/reinstall"
: "/api/dependencies/reinstall";
static intimeLog(String cronId) { static intimeLog(String cronId) {
return getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/$cronId/log" : "/api/crons/$cronId/log"; return getIt<UserInfoViewModel>().useSecretLogined
? "/open/crons/$cronId/log"
: "/api/crons/$cronId/log";
} }
static envMove(String envId) { static envMove(String envId) {
return getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs/$envId/move" : "/api/envs/$envId/move"; return getIt<UserInfoViewModel>().useSecretLogined
? "/open/envs/$envId/move"
: "/api/envs/$envId/move";
}
// 运行订阅
static get runSubscriptions => getIt<UserInfoViewModel>().useSecretLogined
? "/open/subscriptions/run"
: "/api/subscriptions/run";
// 停止订阅
static get stopSubscriptions => getIt<UserInfoViewModel>().useSecretLogined
? "/open/subscriptions/stop"
: "/api/subscriptions/stop";
// 启用订阅
static get enableSubscriptions => getIt<UserInfoViewModel>().useSecretLogined
? "/open/subscriptions/enable"
: "/api/subscriptions/enable";
// 禁用订阅
static get disableSubscriptions => getIt<UserInfoViewModel>().useSecretLogined
? "/open/subscriptions/disable"
: "/api/subscriptions/disable";
// GET 获取订阅 POST 提交订阅 PUT 修改订阅 DELETE 删除订阅
static get subscriptions => getIt<UserInfoViewModel>().useSecretLogined
? "/open/subscriptions"
: "/api/subscriptions";
// 获取订阅日志
static subtimeLog(int cronId) {
return getIt<UserInfoViewModel>().useSecretLogined
? "/open/subscriptions/$cronId/log"
: "/api/subscriptions/$cronId/log";
} }
static bool inWhiteList(String path) { static bool inWhiteList(String path) {
if (path == login || path == loginByClientId || path == loginTwo || path == loginOld) { if (path == login ||
path == loginByClientId ||
path == loginTwo ||
path == loginOld) {
return true; return true;
} }
return false; return false;

View File

@@ -8,12 +8,18 @@ import 'package:qinglong_app/module/env/env_detail_page.dart';
import 'package:qinglong_app/module/home/home_page.dart'; import 'package:qinglong_app/module/home/home_page.dart';
import 'package:qinglong_app/module/login/login_page.dart'; import 'package:qinglong_app/module/login/login_page.dart';
import 'package:qinglong_app/module/others/about_page.dart'; import 'package:qinglong_app/module/others/about_page.dart';
import 'package:qinglong_app/module/others/backup/backup_page.dart';
import 'package:qinglong_app/module/others/dependencies/add_dependency_page.dart'; import 'package:qinglong_app/module/others/dependencies/add_dependency_page.dart';
import 'package:qinglong_app/module/others/dependencies/dependency_page.dart'; import 'package:qinglong_app/module/others/dependencies/dependency_page.dart';
import 'package:qinglong_app/module/others/login_log/login_log_page.dart'; import 'package:qinglong_app/module/others/login_log/login_log_page.dart';
import 'package:qinglong_app/module/others/scripts/acript_add_page.dart';
import 'package:qinglong_app/module/others/scripts/script_detail_page.dart'; import 'package:qinglong_app/module/others/scripts/script_detail_page.dart';
import 'package:qinglong_app/module/others/scripts/script_edit_page.dart'; import 'package:qinglong_app/module/others/scripts/script_edit_page.dart';
import 'package:qinglong_app/module/others/scripts/script_page.dart'; import 'package:qinglong_app/module/others/scripts/script_page.dart';
import 'package:qinglong_app/module/others/subscription/subscription_add_page.dart';
import 'package:qinglong_app/module/others/subscription/subscription_bean.dart';
import 'package:qinglong_app/module/others/subscription/subscription_detail_page.dart';
import 'package:qinglong_app/module/others/subscription/subscription_page.dart';
import 'package:qinglong_app/module/others/task_log/task_log_detail_page.dart'; import 'package:qinglong_app/module/others/task_log/task_log_detail_page.dart';
import 'package:qinglong_app/module/others/task_log/task_log_page.dart'; import 'package:qinglong_app/module/others/task_log/task_log_page.dart';
import 'package:qinglong_app/module/others/theme_page.dart'; import 'package:qinglong_app/module/others/theme_page.dart';
@@ -43,6 +49,12 @@ class Routes {
static const String routeAbout = "/about"; static const String routeAbout = "/about";
static const String routeTheme = "/theme"; static const String routeTheme = "/theme";
static const String routeChangeAccount = "/changeAccount"; static const String routeChangeAccount = "/changeAccount";
static const String routerAddScript = "/script/add";
static const String routerSubscription = "/subscription";
static const String routerSubscriptionDetail = "/subscription/detail";
static const String routerSubscriptionAdd = "/subscription/add";
static const String routerBackup = "/backup";
static Route<dynamic>? generateRoute(RouteSettings settings) { static Route<dynamic>? generateRoute(RouteSettings settings) {
switch (settings.name) { switch (settings.name) {
@@ -58,99 +70,128 @@ class Routes {
return MaterialPageRoute(builder: (context) => const LoginPage()); return MaterialPageRoute(builder: (context) => const LoginPage());
} }
case routeChangeAccount: case routeChangeAccount:
return CupertinoPageRoute( return MaterialPageRoute(
builder: (context) => const ChangeAccountPage(), builder: (context) => const ChangeAccountPage(),
); );
case routeAddTask: case routeAddTask:
if (settings.arguments != null) { if (settings.arguments != null) {
return CupertinoPageRoute( return MaterialPageRoute(
builder: (context) => AddTaskPage( builder: (context) => AddTaskPage(
taskBean: settings.arguments as TaskBean, taskBean: settings.arguments as TaskBean,
)); ));
} else { } else {
return CupertinoPageRoute(builder: (context) => const AddTaskPage()); return MaterialPageRoute(builder: (context) => const AddTaskPage());
} }
case routeAddDependency: case routeAddDependency:
return CupertinoPageRoute(builder: (context) => const AddDependencyPage()); return MaterialPageRoute(
builder: (context) => const AddDependencyPage());
case routeAddEnv: case routeAddEnv:
if (settings.arguments != null) { if (settings.arguments != null) {
return CupertinoPageRoute( return MaterialPageRoute(
builder: (context) => AddEnvPage( builder: (context) => AddEnvPage(
envBean: settings.arguments as EnvBean, envBean: settings.arguments as EnvBean,
)); ));
} else { } else {
return CupertinoPageRoute(builder: (context) => const AddEnvPage()); return MaterialPageRoute(builder: (context) => const AddEnvPage());
} }
case routeConfigEdit: case routeConfigEdit:
return CupertinoPageRoute( return MaterialPageRoute(
builder: (context) => ConfigEditPage( builder: (context) => ConfigEditPage(
(settings.arguments as Map)["title"], (settings.arguments as Map)["title"],
(settings.arguments as Map)["content"], (settings.arguments as Map)["content"],
), ),
); );
case routeLoginLog: case routeLoginLog:
return CupertinoPageRoute( return MaterialPageRoute(
builder: (context) => const LoginLogPage(), builder: (context) => const LoginLogPage(),
); );
case routeTaskLog: case routeTaskLog:
return CupertinoPageRoute( return MaterialPageRoute(
builder: (context) => const TaskLogPage(), builder: (context) => const TaskLogPage(),
); );
case routeScript: case routeScript:
return CupertinoPageRoute( return MaterialPageRoute(
builder: (context) => const ScriptPage(), builder: (context) => const ScriptPage(),
); );
case routeDependency: case routeDependency:
return CupertinoPageRoute( return MaterialPageRoute(
builder: (context) => const DependencyPage(), builder: (context) => const DependencyPage(),
); );
case routeTaskLogDetail: case routeTaskLogDetail:
return CupertinoPageRoute( return MaterialPageRoute(
builder: (context) => TaskLogDetailPage( builder: (context) => TaskLogDetailPage(
title: (settings.arguments as Map)['title'], title: (settings.arguments as Map)['title'],
path: (settings.arguments as Map)['path'], path: (settings.arguments as Map)['path'],
), ),
); );
case routeScriptDetail: case routeScriptDetail:
return CupertinoPageRoute( return MaterialPageRoute(
builder: (context) => ScriptDetailPage( builder: (context) => ScriptDetailPage(
title: (settings.arguments as Map)["title"], title: (settings.arguments as Map)["title"],
path: (settings.arguments as Map)["path"], path: (settings.arguments as Map)["path"],
), ),
); );
case routeTaskDetail: case routeTaskDetail:
return CupertinoPageRoute( return MaterialPageRoute(
builder: (context) => TaskDetailPage( builder: (context) => TaskDetailPage(
settings.arguments as TaskBean, settings.arguments as TaskBean,
), ),
); );
case routeEnvDetail: case routeEnvDetail:
return CupertinoPageRoute( return MaterialPageRoute(
builder: (context) => EnvDetailPage( builder: (context) => EnvDetailPage(
settings.arguments as EnvBean, settings.arguments as EnvBean,
), ),
); );
case routeUpdatePassword: case routeUpdatePassword:
return CupertinoPageRoute( return MaterialPageRoute(
builder: (context) => const UpdatePasswordPage(), builder: (context) => const UpdatePasswordPage(),
); );
case routeAbout: case routeAbout:
return CupertinoPageRoute( return MaterialPageRoute(
builder: (context) => const AboutPage(), builder: (context) => const AboutPage(),
); );
case routeTheme: case routeTheme:
return CupertinoPageRoute( return MaterialPageRoute(
builder: (context) => const ThemePage(), builder: (context) => const ThemePage(),
); );
case routeScriptUpdate: case routeScriptUpdate:
return CupertinoPageRoute( return MaterialPageRoute(
builder: (context) => ScriptEditPage( builder: (context) => ScriptEditPage(
(settings.arguments as Map)["title"], (settings.arguments as Map)["title"],
(settings.arguments as Map)["path"], (settings.arguments as Map)["path"],
(settings.arguments as Map)["content"], (settings.arguments as Map)["content"],
), ),
); );
case routeScriptAdd:
return CupertinoPageRoute(
builder: (context) => const ScriptAddPage(),
);
case routerSubscription:
return CupertinoPageRoute(
builder: (context) => const SubscriptionPage(),
);
case routerSubscriptionDetail:
return CupertinoPageRoute(
builder: (context) => SubscriptionDetailPage(
settings.arguments as Subscription,
),
);
case routerSubscriptionAdd:
if (settings.arguments != null) {
return MaterialPageRoute(
builder: (context) => SubscriptionAddPage(
settings.arguments as Subscription,
));
} else {
return MaterialPageRoute(builder: (context) => const SubscriptionAddPage(null));
} }
case routerBackup:
return CupertinoPageRoute(
builder: (context) => const BackUpPage(),
);
}
return null; return null;
} }

View File

@@ -6,3 +6,4 @@ String spTheme = "dart_mode";
String spSecretLogined = "secret_logined"; String spSecretLogined = "secret_logined";
String spCustomColor = "customColor"; String spCustomColor = "customColor";
String spLoginHistory = "loginHistory"; String spLoginHistory = "loginHistory";
String spShowLine = "spShowLine";

View File

@@ -24,7 +24,7 @@ class ThemeViewModel extends ChangeNotifier {
ThemeViewModel() { ThemeViewModel() {
_primaryColor = Color(getIt<UserInfoViewModel>().primaryColor); _primaryColor = Color(getIt<UserInfoViewModel>().primaryColor);
primaryColor = Color(getIt<UserInfoViewModel>().primaryColor); primaryColor = Color(getIt<UserInfoViewModel>().primaryColor);
var brightness = SchedulerBinding.instance!.window.platformBrightness; var brightness = SchedulerBinding.instance.window.platformBrightness;
_isInDarkMode = brightness == Brightness.dark; _isInDarkMode = brightness == Brightness.dark;
changeThemeReal(_isInDarkMode, false); changeThemeReal(_isInDarkMode, false);
} }

View File

@@ -6,7 +6,7 @@ mixin LazyLoadState<T extends StatefulWidget> on State<T> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
var route = ModalRoute.of(context); var route = ModalRoute.of(context);
void handler(status) { void handler(status) {
if (status == AnimationStatus.completed) { if (status == AnimationStatus.completed) {

View File

@@ -5,7 +5,8 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/gestures.dart' show kMinFlingVelocity, kLongPressTimeout; import 'package:flutter/gestures.dart'
show kMinFlingVelocity, kLongPressTimeout;
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@@ -42,7 +43,8 @@ typedef _ContextMenuPreviewBuilderChildless = Widget Function(
// paintBounds in global coordinates. // paintBounds in global coordinates.
Rect _getRect(GlobalKey globalKey) { Rect _getRect(GlobalKey globalKey) {
assert(globalKey.currentContext != null); assert(globalKey.currentContext != null);
final RenderBox renderBoxContainer = globalKey.currentContext!.findRenderObject()! as RenderBox; final RenderBox renderBoxContainer =
globalKey.currentContext!.findRenderObject()! as RenderBox;
final Offset containerOffset = renderBoxContainer.localToGlobal( final Offset containerOffset = renderBoxContainer.localToGlobal(
renderBoxContainer.paintBounds.topLeft, renderBoxContainer.paintBounds.topLeft,
); );
@@ -87,7 +89,7 @@ enum _ContextMenuLocation {
/// See also: /// See also:
/// ///
/// * [Apple's HIG for Context Menus](https://developer.apple.com/design/human-interface-guidelines/ios/controls/context-menus/) /// * [Apple's HIG for Context Menus](https://developer.apple.com/design/human-interface-guidelines/ios/controls/context-menus/)
class QlCupertinoContextMenu extends StatefulWidget { class QlCupertinoContextMenu<T> extends StatefulWidget {
/// Create a context menu. /// Create a context menu.
/// ///
/// [actions] is required and cannot be null or empty. /// [actions] is required and cannot be null or empty.
@@ -103,7 +105,7 @@ class QlCupertinoContextMenu extends StatefulWidget {
assert(child != null), assert(child != null),
super(key: key); super(key: key);
final TaskBean bean; final T bean;
/// The widget that can be "opened" with the [QlCupertinoContextMenu]. /// The widget that can be "opened" with the [QlCupertinoContextMenu].
/// ///
@@ -194,7 +196,8 @@ class QlCupertinoContextMenu extends StatefulWidget {
State<QlCupertinoContextMenu> createState() => _QlCupertinoContextMenuState(); State<QlCupertinoContextMenu> createState() => _QlCupertinoContextMenuState();
} }
class _QlCupertinoContextMenuState extends State<QlCupertinoContextMenu> with TickerProviderStateMixin { class _QlCupertinoContextMenuState extends State<QlCupertinoContextMenu>
with TickerProviderStateMixin {
final GlobalKey _childGlobalKey = GlobalKey(); final GlobalKey _childGlobalKey = GlobalKey();
bool _childHidden = false; bool _childHidden = false;
@@ -227,7 +230,8 @@ class _QlCupertinoContextMenuState extends State<QlCupertinoContextMenu> with Ti
final double screenWidth = MediaQuery.of(context).size.width; final double screenWidth = MediaQuery.of(context).size.width;
final double center = screenWidth / 2; final double center = screenWidth / 2;
final bool centerDividesChild = childRect.left < center && childRect.right > center; final bool centerDividesChild =
childRect.left < center && childRect.right > center;
final double distanceFromCenter = (center - childRect.center.dx).abs(); final double distanceFromCenter = (center - childRect.center.dx).abs();
if (centerDividesChild && distanceFromCenter <= childRect.width / 4) { if (centerDividesChild && distanceFromCenter <= childRect.width / 4) {
return _ContextMenuLocation.center; return _ContextMenuLocation.center;
@@ -318,7 +322,14 @@ class _QlCupertinoContextMenuState extends State<QlCupertinoContextMenu> with Ti
_openController.reverse(); _openController.reverse();
} else { } else {
if (_openController.isDismissed) { if (_openController.isDismissed) {
Navigator.of(context).pushNamed(Routes.routeTaskDetail, arguments: widget.bean); if (widget.bean is TaskBean){
Navigator.of(context)
.pushNamed(Routes.routeTaskDetail, arguments: widget.bean);
}else{
Navigator.of(context)
.pushNamed(Routes.routerSubscriptionDetail, arguments: widget.bean);
}
} }
} }
} }
@@ -419,7 +430,8 @@ class _DecoyChild extends StatefulWidget {
_DecoyChildState createState() => _DecoyChildState(); _DecoyChildState createState() => _DecoyChildState();
} }
class _DecoyChildState extends State<_DecoyChild> with TickerProviderStateMixin { class _DecoyChildState extends State<_DecoyChild>
with TickerProviderStateMixin {
// TODO(justinmc): Dark mode support. // TODO(justinmc): Dark mode support.
// See https://github.com/flutter/flutter/issues/43211. // See https://github.com/flutter/flutter/issues/43211.
static const Color _lightModeMaskColor = Color(0xFF888888); static const Color _lightModeMaskColor = Color(0xFF888888);
@@ -481,7 +493,9 @@ class _DecoyChildState extends State<_DecoyChild> with TickerProviderStateMixin
} }
Widget _buildAnimation(BuildContext context, Widget? child) { Widget _buildAnimation(BuildContext context, Widget? child) {
final Color color = widget.controller.status == AnimationStatus.reverse ? _masklessColor : _mask.value; final Color color = widget.controller.status == AnimationStatus.reverse
? _masklessColor
: _mask.value;
return Positioned.fromRect( return Positioned.fromRect(
rect: _rect.value!, rect: _rect.value!,
child: ShaderMask( child: ShaderMask(
@@ -538,7 +552,8 @@ class _ContextMenuRoute<T> extends PopupRoute<T> {
// The duration of the transition used when a modal popup is shown. Eyeballed // The duration of the transition used when a modal popup is shown. Eyeballed
// from a physical device running iOS 13.1.2. // from a physical device running iOS 13.1.2.
static const Duration _kModalPopupTransitionDuration = Duration(milliseconds: 335); static const Duration _kModalPopupTransitionDuration =
Duration(milliseconds: 335);
final List<Widget> _actions; final List<Widget> _actions;
final _ContextMenuPreviewBuilderChildless? _builder; final _ContextMenuPreviewBuilderChildless? _builder;
@@ -562,7 +577,8 @@ class _ContextMenuRoute<T> extends PopupRoute<T> {
static final RectTween _rectTween = RectTween(); static final RectTween _rectTween = RectTween();
static final Animatable<Rect?> _rectAnimatable = _rectTween.chain(_curve); static final Animatable<Rect?> _rectAnimatable = _rectTween.chain(_curve);
static final RectTween _rectTweenReverse = RectTween(); static final RectTween _rectTweenReverse = RectTween();
static final Animatable<Rect?> _rectAnimatableReverse = _rectTweenReverse.chain( static final Animatable<Rect?> _rectAnimatableReverse =
_rectTweenReverse.chain(
_curveReverse, _curveReverse,
); );
static final RectTween _sheetRectTween = RectTween(); static final RectTween _sheetRectTween = RectTween();
@@ -573,10 +589,12 @@ class _ContextMenuRoute<T> extends PopupRoute<T> {
_curveReverse, _curveReverse,
); );
static final Tween<double> _sheetScaleTween = Tween<double>(); static final Tween<double> _sheetScaleTween = Tween<double>();
static final Animatable<double> _sheetScaleAnimatable = _sheetScaleTween.chain( static final Animatable<double> _sheetScaleAnimatable =
_sheetScaleTween.chain(
_curve, _curve,
); );
static final Animatable<double> _sheetScaleAnimatableReverse = _sheetScaleTween.chain( static final Animatable<double> _sheetScaleAnimatableReverse =
_sheetScaleTween.chain(
_curveReverse, _curveReverse,
); );
final Tween<double> _opacityTween = Tween<double>(begin: 0.0, end: 1.0); final Tween<double> _opacityTween = Tween<double>(begin: 0.0, end: 1.0);
@@ -611,7 +629,8 @@ class _ContextMenuRoute<T> extends PopupRoute<T> {
// Get the alignment for the _ContextMenuSheet's Transform.scale based on the // Get the alignment for the _ContextMenuSheet's Transform.scale based on the
// contextMenuLocation. // contextMenuLocation.
static AlignmentDirectional getSheetAlignment(_ContextMenuLocation contextMenuLocation) { static AlignmentDirectional getSheetAlignment(
_ContextMenuLocation contextMenuLocation) {
switch (contextMenuLocation) { switch (contextMenuLocation) {
case _ContextMenuLocation.center: case _ContextMenuLocation.center:
return AlignmentDirectional.topCenter; return AlignmentDirectional.topCenter;
@@ -623,17 +642,27 @@ class _ContextMenuRoute<T> extends PopupRoute<T> {
} }
// The place to start the sheetRect animation from. // The place to start the sheetRect animation from.
static Rect _getSheetRectBegin(Orientation? orientation, _ContextMenuLocation contextMenuLocation, Rect childRect, Rect sheetRect) { static Rect _getSheetRectBegin(
Orientation? orientation,
_ContextMenuLocation contextMenuLocation,
Rect childRect,
Rect sheetRect) {
switch (contextMenuLocation) { switch (contextMenuLocation) {
case _ContextMenuLocation.center: case _ContextMenuLocation.center:
final Offset target = orientation == Orientation.portrait ? childRect.bottomCenter : childRect.topCenter; final Offset target = orientation == Orientation.portrait
? childRect.bottomCenter
: childRect.topCenter;
final Offset centered = target - Offset(sheetRect.width / 2, 0.0); final Offset centered = target - Offset(sheetRect.width / 2, 0.0);
return centered & sheetRect.size; return centered & sheetRect.size;
case _ContextMenuLocation.right: case _ContextMenuLocation.right:
final Offset target = orientation == Orientation.portrait ? childRect.bottomRight : childRect.topRight; final Offset target = orientation == Orientation.portrait
? childRect.bottomRight
: childRect.topRight;
return (target - Offset(sheetRect.width, 0.0)) & sheetRect.size; return (target - Offset(sheetRect.width, 0.0)) & sheetRect.size;
case _ContextMenuLocation.left: case _ContextMenuLocation.left:
final Offset target = orientation == Orientation.portrait ? childRect.bottomLeft : childRect.topLeft; final Offset target = orientation == Orientation.portrait
? childRect.bottomLeft
: childRect.topLeft;
return target & sheetRect.size; return target & sheetRect.size;
} }
} }
@@ -651,7 +680,9 @@ class _ContextMenuRoute<T> extends PopupRoute<T> {
// Take measurements on the child and _ContextMenuSheet and update the // Take measurements on the child and _ContextMenuSheet and update the
// animation tweens to match. // animation tweens to match.
void _updateTweenRects() { void _updateTweenRects() {
final Rect childRect = _scale == null ? _getRect(_childGlobalKey) : _getScaledRect(_childGlobalKey, _scale!); final Rect childRect = _scale == null
? _getRect(_childGlobalKey)
: _getScaledRect(_childGlobalKey, _scale!);
_rectTween.begin = _previousChildRect; _rectTween.begin = _previousChildRect;
_rectTween.end = childRect; _rectTween.end = childRect;
@@ -725,7 +756,8 @@ class _ContextMenuRoute<T> extends PopupRoute<T> {
} }
@override @override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
// This is usually used to build the "page", which is then passed to // This is usually used to build the "page", which is then passed to
// buildTransitions as child, the idea being that buildTransitions will // buildTransitions as child, the idea being that buildTransitions will
// animate the entire page into the scene. In the case of _ContextMenuRoute, // animate the entire page into the scene. In the case of _ContextMenuRoute,
@@ -735,7 +767,8 @@ class _ContextMenuRoute<T> extends PopupRoute<T> {
} }
@override @override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return OrientationBuilder( return OrientationBuilder(
builder: (BuildContext context, Orientation orientation) { builder: (BuildContext context, Orientation orientation) {
_lastOrientation = orientation; _lastOrientation = orientation;
@@ -744,9 +777,15 @@ class _ContextMenuRoute<T> extends PopupRoute<T> {
// they're movable. // they're movable.
if (!animation.isCompleted) { if (!animation.isCompleted) {
final bool reverse = animation.status == AnimationStatus.reverse; final bool reverse = animation.status == AnimationStatus.reverse;
final Rect rect = reverse ? _rectAnimatableReverse.evaluate(animation)! : _rectAnimatable.evaluate(animation)!; final Rect rect = reverse
final Rect sheetRect = reverse ? _sheetRectAnimatableReverse.evaluate(animation)! : _sheetRectAnimatable.evaluate(animation)!; ? _rectAnimatableReverse.evaluate(animation)!
final double sheetScale = reverse ? _sheetScaleAnimatableReverse.evaluate(animation) : _sheetScaleAnimatable.evaluate(animation); : _rectAnimatable.evaluate(animation)!;
final Rect sheetRect = reverse
? _sheetRectAnimatableReverse.evaluate(animation)!
: _sheetRectAnimatable.evaluate(animation)!;
final double sheetScale = reverse
? _sheetScaleAnimatableReverse.evaluate(animation)
: _sheetScaleAnimatable.evaluate(animation);
return Stack( return Stack(
children: <Widget>[ children: <Widget>[
Positioned.fromRect( Positioned.fromRect(
@@ -818,7 +857,8 @@ class _ContextMenuRouteStatic extends StatefulWidget {
_ContextMenuRouteStaticState createState() => _ContextMenuRouteStaticState(); _ContextMenuRouteStaticState createState() => _ContextMenuRouteStaticState();
} }
class _ContextMenuRouteStaticState extends State<_ContextMenuRouteStatic> with TickerProviderStateMixin { class _ContextMenuRouteStaticState extends State<_ContextMenuRouteStatic>
with TickerProviderStateMixin {
// The child is scaled down as it is dragged down until it hits this minimum // The child is scaled down as it is dragged down until it hits this minimum
// value. // value.
static const double _kMinScale = 0.8; static const double _kMinScale = 0.8;
@@ -838,7 +878,8 @@ class _ContextMenuRouteStaticState extends State<_ContextMenuRouteStatic> with T
late Animation<double> _sheetOpacityAnimation; late Animation<double> _sheetOpacityAnimation;
// The scale of the child changes as a function of the distance it is dragged. // The scale of the child changes as a function of the distance it is dragged.
static double _getScale(Orientation orientation, double maxDragDistance, double dy) { static double _getScale(
Orientation orientation, double maxDragDistance, double dy) {
final double dyDirectional = dy <= 0.0 ? dy : -dy; final double dyDirectional = dy <= 0.0 ? dy : -dy;
return math.max( return math.max(
_kMinScale, _kMinScale,
@@ -859,11 +900,13 @@ class _ContextMenuRouteStaticState extends State<_ContextMenuRouteStatic> with T
// If flung, animate a bit before handling the potential dismiss. // If flung, animate a bit before handling the potential dismiss.
if (details.velocity.pixelsPerSecond.dy.abs() >= kMinFlingVelocity) { if (details.velocity.pixelsPerSecond.dy.abs() >= kMinFlingVelocity) {
final bool flingIsAway = details.velocity.pixelsPerSecond.dy > 0; final bool flingIsAway = details.velocity.pixelsPerSecond.dy > 0;
final double finalPosition = flingIsAway ? _moveAnimation.value.dy + 100.0 : 0.0; final double finalPosition =
flingIsAway ? _moveAnimation.value.dy + 100.0 : 0.0;
if (flingIsAway && _sheetController.status != AnimationStatus.forward) { if (flingIsAway && _sheetController.status != AnimationStatus.forward) {
_sheetController.forward(); _sheetController.forward();
} else if (!flingIsAway && _sheetController.status != AnimationStatus.reverse) { } else if (!flingIsAway &&
_sheetController.status != AnimationStatus.reverse) {
_sheetController.reverse(); _sheetController.reverse();
} }
@@ -918,21 +961,30 @@ class _ContextMenuRouteStaticState extends State<_ContextMenuRouteStatic> with T
widget.onDismiss!(context, _lastScale, _sheetOpacityAnimation.value); widget.onDismiss!(context, _lastScale, _sheetOpacityAnimation.value);
} }
Alignment _getChildAlignment(Orientation orientation, _ContextMenuLocation contextMenuLocation) { Alignment _getChildAlignment(
Orientation orientation, _ContextMenuLocation contextMenuLocation) {
switch (contextMenuLocation) { switch (contextMenuLocation) {
case _ContextMenuLocation.center: case _ContextMenuLocation.center:
return orientation == Orientation.portrait ? Alignment.bottomCenter : Alignment.topRight; return orientation == Orientation.portrait
? Alignment.bottomCenter
: Alignment.topRight;
case _ContextMenuLocation.right: case _ContextMenuLocation.right:
return orientation == Orientation.portrait ? Alignment.bottomCenter : Alignment.topLeft; return orientation == Orientation.portrait
? Alignment.bottomCenter
: Alignment.topLeft;
case _ContextMenuLocation.left: case _ContextMenuLocation.left:
return orientation == Orientation.portrait ? Alignment.bottomCenter : Alignment.topRight; return orientation == Orientation.portrait
? Alignment.bottomCenter
: Alignment.topRight;
} }
} }
void _setDragOffset(Offset dragOffset) { void _setDragOffset(Offset dragOffset) {
// Allow horizontal and negative vertical movement, but damp it. // Allow horizontal and negative vertical movement, but damp it.
final double endX = _kPadding * dragOffset.dx / _kDamping; final double endX = _kPadding * dragOffset.dx / _kDamping;
final double endY = dragOffset.dy >= 0.0 ? dragOffset.dy : _kPadding * dragOffset.dy / _kDamping; final double endY = dragOffset.dy >= 0.0
? dragOffset.dy
: _kPadding * dragOffset.dy / _kDamping;
setState(() { setState(() {
_dragOffset = dragOffset; _dragOffset = dragOffset;
_moveAnimation = Tween<Offset>( _moveAnimation = Tween<Offset>(
@@ -949,9 +1001,13 @@ class _ContextMenuRouteStaticState extends State<_ContextMenuRouteStatic> with T
); );
// Fade the _ContextMenuSheet out or in, if needed. // Fade the _ContextMenuSheet out or in, if needed.
if (_lastScale <= _kSheetScaleThreshold && _sheetController.status != AnimationStatus.forward && _sheetScaleAnimation.value != 0.0) { if (_lastScale <= _kSheetScaleThreshold &&
_sheetController.status != AnimationStatus.forward &&
_sheetScaleAnimation.value != 0.0) {
_sheetController.forward(); _sheetController.forward();
} else if (_lastScale > _kSheetScaleThreshold && _sheetController.status != AnimationStatus.reverse && _sheetScaleAnimation.value != 1.0) { } else if (_lastScale > _kSheetScaleThreshold &&
_sheetController.status != AnimationStatus.reverse &&
_sheetScaleAnimation.value != 1.0) {
_sheetController.reverse(); _sheetController.reverse();
} }
}); });
@@ -960,7 +1016,8 @@ class _ContextMenuRouteStaticState extends State<_ContextMenuRouteStatic> with T
// The order and alignment of the _ContextMenuSheet and the child depend on // The order and alignment of the _ContextMenuSheet and the child depend on
// both the orientation of the screen as well as the position on the screen of // both the orientation of the screen as well as the position on the screen of
// the original child. // the original child.
List<Widget> _getChildren(Orientation orientation, _ContextMenuLocation contextMenuLocation) { List<Widget> _getChildren(
Orientation orientation, _ContextMenuLocation contextMenuLocation) {
final Expanded child = Expanded( final Expanded child = Expanded(
child: Align( child: Align(
alignment: _getChildAlignment( alignment: _getChildAlignment(
@@ -995,7 +1052,9 @@ class _ContextMenuRouteStaticState extends State<_ContextMenuRouteStatic> with T
case _ContextMenuLocation.center: case _ContextMenuLocation.center:
return <Widget>[child, spacer, sheet]; return <Widget>[child, spacer, sheet];
case _ContextMenuLocation.right: case _ContextMenuLocation.right:
return orientation == Orientation.portrait ? <Widget>[child, spacer, sheet] : <Widget>[sheet, spacer, child]; return orientation == Orientation.portrait
? <Widget>[child, spacer, sheet]
: <Widget>[sheet, spacer, child];
case _ContextMenuLocation.left: case _ContextMenuLocation.left:
return <Widget>[child, spacer, sheet]; return <Widget>[child, spacer, sheet];
} }
@@ -1004,7 +1063,8 @@ class _ContextMenuRouteStaticState extends State<_ContextMenuRouteStatic> with T
// Build the animation for the _ContextMenuSheet. // Build the animation for the _ContextMenuSheet.
Widget _buildSheetAnimation(BuildContext context, Widget? child) { Widget _buildSheetAnimation(BuildContext context, Widget? child) {
return Transform.scale( return Transform.scale(
alignment: _ContextMenuRoute.getSheetAlignment(widget.contextMenuLocation), alignment:
_ContextMenuRoute.getSheetAlignment(widget.contextMenuLocation),
scale: _sheetScaleAnimation.value, scale: _sheetScaleAnimation.value,
child: FadeTransition( child: FadeTransition(
opacity: _sheetOpacityAnimation, opacity: _sheetOpacityAnimation,

View File

@@ -17,7 +17,6 @@ class SearchCell extends ConsumerStatefulWidget {
} }
class _SearchCellState extends ConsumerState<SearchCell> { class _SearchCellState extends ConsumerState<SearchCell> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -35,9 +34,7 @@ class _SearchCellState extends ConsumerState<SearchCell> {
), ),
onSuffixTap: () { onSuffixTap: () {
widget.controller.text = ""; widget.controller.text = "";
setState(() { setState(() {});
});
}, },
controller: widget.controller, controller: widget.controller,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(

View File

@@ -29,9 +29,8 @@ class SourceCodeView extends StatefulWidget {
this.syntaxHighlighterStyle, this.syntaxHighlighterStyle,
}) : super(key: key); }) : super(key: key);
String? get codeLink => codeLinkPrefix == null String? get codeLink =>
? null codeLinkPrefix == null ? null : '$codeLinkPrefix/$filePath';
: '$codeLinkPrefix/$filePath';
@override @override
_SourceCodeViewState createState() { _SourceCodeViewState createState() {

View File

@@ -17,7 +17,10 @@ class SyntaxHighlighterStyle {
this.constantStyle}); this.constantStyle});
static SyntaxHighlighterStyle lightThemeStyle() => SyntaxHighlighterStyle( static SyntaxHighlighterStyle lightThemeStyle() => SyntaxHighlighterStyle(
baseStyle: const TextStyle(color: const Color(0xFF000000),height: 1,), baseStyle: const TextStyle(
color: const Color(0xFF000000),
height: 1,
),
numberStyle: const TextStyle(color: const Color(0xFF1565C0)), numberStyle: const TextStyle(color: const Color(0xFF1565C0)),
commentStyle: const TextStyle(color: const Color(0xFF9E9E9E)), commentStyle: const TextStyle(color: const Color(0xFF9E9E9E)),
keywordStyle: const TextStyle(color: const Color(0xFF9C27B0)), keywordStyle: const TextStyle(color: const Color(0xFF9C27B0)),

View File

@@ -26,7 +26,8 @@ class UserInfoViewModel {
_useSecertLogined = SpUtil.getBool(spSecretLogined, defValue: false); _useSecertLogined = SpUtil.getBool(spSecretLogined, defValue: false);
_host = SpUtil.getString(spHost, defValue: ''); _host = SpUtil.getString(spHost, defValue: '');
List<dynamic>? tempList = jsonDecode(SpUtil.getString(spLoginHistory, defValue: '[]')); List<dynamic>? tempList =
jsonDecode(SpUtil.getString(spLoginHistory, defValue: '[]'));
if (tempList != null && tempList.isNotEmpty) { if (tempList != null && tempList.isNotEmpty) {
for (Map<String, dynamic> value in tempList) { for (Map<String, dynamic> value in tempList) {
@@ -40,7 +41,8 @@ class UserInfoViewModel {
SpUtil.putString(spUserInfo, token); SpUtil.putString(spUserInfo, token);
} }
void updateUserName(String host, String userName, String password, bool secretLogin) { void updateUserName(
String host, String userName, String password, bool secretLogin) {
updateHost(host); updateHost(host);
_useSecretLogin(secretLogin); _useSecretLogin(secretLogin);
_userName = userName; _userName = userName;
@@ -93,7 +95,13 @@ class UserInfoViewModel {
historyAccounts.removeWhere((element) => element.host == _host); historyAccounts.removeWhere((element) => element.host == _host);
historyAccounts.insert(0, UserInfoBean(userName: _userName, password: _passWord, useSecretLogined: _useSecertLogined, host: _host)); historyAccounts.insert(
0,
UserInfoBean(
userName: _userName,
password: _passWord,
useSecretLogined: _useSecertLogined,
host: _host));
while (historyAccounts.length > 3) { while (historyAccounts.length > 3) {
historyAccounts.removeLast(); historyAccounts.removeLast();
@@ -117,7 +125,8 @@ class UserInfoBean {
bool useSecretLogined = false; bool useSecretLogined = false;
String? host; String? host;
UserInfoBean({this.userName, this.password, this.useSecretLogined = false, this.host}); UserInfoBean(
{this.userName, this.password, this.useSecretLogined = false, this.host});
UserInfoBean.fromJson(Map<String, dynamic> json) { UserInfoBean.fromJson(Map<String, dynamic> json) {
userName = json['userName']; userName = json['userName'];

View File

@@ -8,6 +8,7 @@ import 'package:qinglong_app/module/login/user_bean.dart';
import 'package:qinglong_app/module/others/dependencies/dependency_bean.dart'; import 'package:qinglong_app/module/others/dependencies/dependency_bean.dart';
import 'package:qinglong_app/module/others/login_log/login_log_bean.dart'; import 'package:qinglong_app/module/others/login_log/login_log_bean.dart';
import 'package:qinglong_app/module/others/scripts/script_bean.dart'; import 'package:qinglong_app/module/others/scripts/script_bean.dart';
import 'package:qinglong_app/module/others/subscription/subscription_bean.dart';
import 'package:qinglong_app/module/others/task_log/task_log_bean.dart'; import 'package:qinglong_app/module/others/task_log/task_log_bean.dart';
import 'package:qinglong_app/module/task/task_bean.dart'; import 'package:qinglong_app/module/task/task_bean.dart';
@@ -66,6 +67,10 @@ class JsonConversion$Json {
return TaskBean.jsonConversion(json) as M; return TaskBean.jsonConversion(json) as M;
} }
if (type == (Subscription).toString()){
return Subscription.fromJson(json) as M;
}
throw Exception("not found"); throw Exception("not found");
} }
@@ -110,6 +115,10 @@ class JsonConversion$Json {
return data.map<TaskBean>((e) => TaskBean.jsonConversion(e)).toList() as M; return data.map<TaskBean>((e) => TaskBean.jsonConversion(e)).toList() as M;
} }
if (<Subscription>[] is M){
return data.map<Subscription>((e) => Subscription.jsonConversion(e)).toList() as M;
}
throw Exception("not found"); throw Exception("not found");
} }
} }

View File

@@ -41,7 +41,8 @@ void main() async {
), ),
); );
if (Platform.isAndroid) { if (Platform.isAndroid) {
SystemUiOverlayStyle style = const SystemUiOverlayStyle(statusBarColor: Colors.transparent); SystemUiOverlayStyle style =
const SystemUiOverlayStyle(statusBarColor: Colors.transparent);
SystemChrome.setSystemUIOverlayStyle(style); SystemChrome.setSystemUIOverlayStyle(style);
} }
} }
@@ -62,7 +63,8 @@ class QlAppState extends ConsumerState<QlApp> {
FocusScope.of(context).requestFocus(FocusNode()); FocusScope.of(context).requestFocus(FocusNode());
}, },
child: MediaQuery( child: MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance!.window).copyWith( data:
MediaQueryData.fromWindow(WidgetsBinding.instance.window).copyWith(
textScaleFactor: 1, textScaleFactor: 1,
), ),
child: MaterialApp( child: MaterialApp(
@@ -87,7 +89,9 @@ class QlAppState extends ConsumerState<QlApp> {
if (!kReleaseMode) { if (!kReleaseMode) {
showDebugBtn(context); showDebugBtn(context);
} }
return getIt<UserInfoViewModel>().isLogined() ? const HomePage() : const LoginPage(); return getIt<UserInfoViewModel>().isLogined()
? const HomePage()
: const LoginPage();
}, },
), ),
// home: LoginPage(), // home: LoginPage(),

View File

@@ -64,13 +64,17 @@ class _ChangeAccountPageState extends ConsumerState<ChangeAccountPage> {
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == getIt<UserInfoViewModel>().historyAccounts.length) { if (index ==
getIt<UserInfoViewModel>().historyAccounts.length) {
return addAccount(); return addAccount();
} }
return ClipRRect( return ClipRRect(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
child: Container( child: Container(
color: ref.watch(themeProvider).themeColor.settingBordorColor(), color: ref
.watch(themeProvider)
.themeColor
.settingBordorColor(),
child: buildCell(index), child: buildCell(index),
), ),
); );
@@ -80,7 +84,8 @@ class _ChangeAccountPageState extends ConsumerState<ChangeAccountPage> {
height: 10, height: 10,
); );
}, },
itemCount: getIt<UserInfoViewModel>().historyAccounts.length + 1), itemCount:
getIt<UserInfoViewModel>().historyAccounts.length + 1),
), ),
], ],
), ),
@@ -113,11 +118,13 @@ class _ChangeAccountPageState extends ConsumerState<ChangeAccountPage> {
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
border: Border.all(color: ref.watch(themeProvider).primaryColor, width: 1), border: Border.all(
color: ref.watch(themeProvider).primaryColor, width: 1),
), ),
child: Text( child: Text(
"已登录", "已登录",
style: TextStyle(color: ref.watch(themeProvider).primaryColor, fontSize: 12), style: TextStyle(
color: ref.watch(themeProvider).primaryColor, fontSize: 12),
), ),
) )
: (isLoginingHost.isNotEmpty : (isLoginingHost.isNotEmpty
@@ -132,7 +139,8 @@ class _ChangeAccountPageState extends ConsumerState<ChangeAccountPage> {
: const SizedBox.shrink()), : const SizedBox.shrink()),
); );
if (getIt<UserInfoViewModel>().historyAccounts[index].host == getIt<UserInfoViewModel>().host) { if (getIt<UserInfoViewModel>().historyAccounts[index].host ==
getIt<UserInfoViewModel>().host) {
return child; return child;
} }
@@ -146,7 +154,8 @@ class _ChangeAccountPageState extends ConsumerState<ChangeAccountPage> {
backgroundColor: Colors.red, backgroundColor: Colors.red,
flex: 1, flex: 1,
onPressed: (_) { onPressed: (_) {
getIt<UserInfoViewModel>().removeHistoryAccount(getIt<UserInfoViewModel>().historyAccounts[index].host); getIt<UserInfoViewModel>().removeHistoryAccount(
getIt<UserInfoViewModel>().historyAccounts[index].host);
setState(() {}); setState(() {});
}, },
foregroundColor: Colors.white, foregroundColor: Colors.white,
@@ -181,7 +190,8 @@ class _ChangeAccountPageState extends ConsumerState<ChangeAccountPage> {
void dealLoginResponse(int response) { void dealLoginResponse(int response) {
if (response == LoginHelper.success) { if (response == LoginHelper.success) {
Navigator.of(context).pushNamedAndRemoveUntil(Routes.routeHomePage, (_) => false); Navigator.of(context)
.pushNamedAndRemoveUntil(Routes.routeHomePage, (_) => false);
} else if (response == LoginHelper.failed) { } else if (response == LoginHelper.failed) {
loginFailed(); loginFailed();
} else { } else {

View File

@@ -1,12 +1,16 @@
import 'package:code_text_field/code_text_field.dart'; import 'package:code_text_field/code_text_field.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:highlight/languages/powershell.dart'; import 'package:highlight/languages/powershell.dart';
import 'package:qinglong_app/base/http/api.dart'; import 'package:qinglong_app/base/http/api.dart';
import 'package:qinglong_app/base/http/http.dart'; import 'package:qinglong_app/base/http/http.dart';
import 'package:qinglong_app/base/ql_app_bar.dart'; import 'package:qinglong_app/base/ql_app_bar.dart';
import 'package:qinglong_app/base/sp_const.dart';
import 'package:qinglong_app/base/theme.dart'; import 'package:qinglong_app/base/theme.dart';
import 'package:qinglong_app/utils/extension.dart'; import 'package:qinglong_app/utils/extension.dart';
import 'package:qinglong_app/utils/sp_utils.dart';
class ConfigEditPage extends ConsumerStatefulWidget { class ConfigEditPage extends ConsumerStatefulWidget {
final String content; final String content;
@@ -21,47 +25,122 @@ class ConfigEditPage extends ConsumerStatefulWidget {
class _ConfigEditPageState extends ConsumerState<ConfigEditPage> { class _ConfigEditPageState extends ConsumerState<ConfigEditPage> {
CodeController? _codeController; CodeController? _codeController;
late String result; late String result;
late String preResult;
List<String> operateList = [];
@override @override
void dispose() { void dispose() {
_codeController?.dispose(); _codeController?.dispose();
super.dispose(); super.dispose();
} }
@override @override
void initState() { void initState() {
result = widget.content; result = widget.content;
preResult = widget.content;
super.initState(); super.initState();
generateOperateList();
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
focusNode.requestFocus(); focusNode.requestFocus();
checkClipBoard();
}); });
} }
Future<void> notifyICloud(
BuildContext context, String? title, String? content) async {}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_codeController ??= CodeController( _codeController ??= CodeController(
text: widget.content, text: result,
language: powershell, language: powershell,
onChange: (value) { onChange: (value) {
result = value; result = value;
}, },
theme: ref.watch(themeProvider).themeColor.codeEditorTheme(), theme: ref.watch(themeProvider).themeColor.codeEditorTheme(),
stringMap: { stringMap: {
"export": const TextStyle(fontWeight: FontWeight.normal, color: Color(0xff6B2375)), "export": const TextStyle(
fontWeight: FontWeight.normal, color: Color(0xff6B2375)),
}, },
); );
return Scaffold( return Scaffold(
appBar: QlAppBar( appBar: QlAppBar(
canBack: true, canBack: true,
backCall: () { backCall: () {
FocusManager.instance.primaryFocus?.unfocus();
if (preResult == result) {
Navigator.of(context).pop(); Navigator.of(context).pop();
} else {
showCupertinoDialog(
context: context,
useRootNavigator: false,
builder: (childContext) => CupertinoAlertDialog(
title: const Text("温馨提示"),
content: const Text("你编辑的内容还没用提交,确定退出吗?"),
actions: [
CupertinoDialogAction(
child: const Text(
"取消",
style: TextStyle(
color: Color(0xff999999),
),
),
onPressed: () {
Navigator.of(childContext).pop();
},
),
CupertinoDialogAction(
child: Text(
"确定",
style: TextStyle(
color: ref.watch(themeProvider).primaryColor,
),
),
onPressed: () {
Navigator.of(childContext).pop();
Navigator.of(context).pop();
},
),
],
),
);
}
}, },
title: '编辑${widget.title}', title: '编辑${widget.title}',
actions: [ actions: [
const SizedBox(
width: 15,
),
Material(
color: Colors.transparent,
child: PopupMenuButton<String>(
onSelected: (String result) {
updateValueBykey(result);
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
...operateList
.map(
(e) => PopupMenuItem<String>(
child: Text(e),
value: e,
),
)
.toList(),
],
child: const Center(
child: Icon(
CupertinoIcons.arrow_up_right_diamond,
),
),
),
),
InkWell( InkWell(
onTap: () async { onTap: () async {
HttpResponse<NullResponse> response = await Api.saveFile(widget.title, result); HttpResponse<NullResponse> response =
await Api.saveFile(widget.title, result);
await notifyICloud(context, widget.title, result);
if (response.success) { if (response.success) {
"提交成功".toast(); "提交成功".toast();
Navigator.of(context).pop(widget.title); Navigator.of(context).pop(widget.title);
@@ -88,14 +167,177 @@ class _ConfigEditPageState extends ConsumerState<ConfigEditPage> {
), ),
body: SafeArea( body: SafeArea(
top: false, top: false,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: SpUtil.getBool(spShowLine, defValue: false) ? 0 : 10,
),
child: CodeField( child: CodeField(
controller: _codeController!, controller: _codeController!,
expands: true, expands: true,
background: ref.watch(themeProvider).themeColor.settingBgColor(), background: Colors.white,
wrap: SpUtil.getBool(spShowLine, defValue: false) ? false : true,
hideColumn: !SpUtil.getBool(spShowLine, defValue: false),
lineNumberStyle: LineNumberStyle(
textStyle: TextStyle(
color: ref.watch(themeProvider).themeColor.descColor(),
fontSize: 12,
),
),
),
), ),
), ),
); );
} }
FocusNode focusNode = FocusNode(); FocusNode focusNode = FocusNode();
void generateOperateList() {
operateList.clear();
List<String> array = result.split("\n");
for (String a in array) {
String t = a.replaceAll(" ", "");
if (t.trim().startsWith("export")) {
int i = t.indexOf("export") + 6;
int j = t.indexOf("=");
operateList.add(t.substring(i, j));
}
}
}
void updateValueBykey(String key) async {
String defaultValue = "";
try {
var clipBoard = await Clipboard.getData(Clipboard.kTextPlain);
if (clipBoard != null && clipBoard.text != null) {
String tempText = clipBoard.text!;
if (tempText.trim().contains("export")) {
int i = tempText.trim().indexOf("\"");
int j = tempText.trim().lastIndexOf("\"");
if (i == -1 || j == -1) {
i = tempText.trim().indexOf("'");
j = tempText.trim().lastIndexOf("'");
}
defaultValue = tempText.trim().substring(i, j);
} else {
defaultValue = tempText;
}
}
} catch (e) {}
TextEditingController controller = TextEditingController(
text: defaultValue.replaceAll("\"", "").replaceAll("'", ""));
showCupertinoDialog(
useRootNavigator: false,
context: context,
builder: (context) => CupertinoAlertDialog(
title: Text("编辑$key:"),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: 10,
),
child: TextField(
controller: controller,
decoration: const InputDecoration(
isDense: true,
contentPadding: EdgeInsets.all(4),
hintText: "请输入值",
hintStyle: TextStyle(
fontSize: 14,
),
),
autofocus: false,
),
),
],
),
actions: [
CupertinoDialogAction(
child: const Text(
"取消",
style: TextStyle(
color: Color(0xff999999),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
CupertinoDialogAction(
child: Text(
"确定",
style: TextStyle(
color: ref.watch(themeProvider).primaryColor,
),
),
onPressed: () async {
Navigator.of(context).pop();
updateValueByKey(key, controller.text);
},
),
],
),
);
}
void updateValueByKey(String key, String text) {
List<String> array = result.split("\n");
for (String a in array) {
String t = a.replaceAll(" ", "");
if (t.trim().startsWith("export")) {
int i = t.indexOf("export") + 6;
int j = t.indexOf("=");
String tempResult = t.substring(i, j);
if (tempResult == key) {
result = result.replaceAll(a, "\nexport $key = \"$text\" \n\n");
break;
}
}
}
_codeController = null;
setState(() {});
"已修改".toast();
}
void checkClipBoard() async {
try {
String key = "";
String value = "";
var clipBoard = await Clipboard.getData(Clipboard.kTextPlain);
if (clipBoard != null && clipBoard.text != null) {
String tempText = clipBoard.text!;
if (tempText.trim().contains("export")) {
int kI = tempText.trim().indexOf("export");
int kJ = tempText.trim().indexOf("=");
key = tempText.trim().substring(kI + 6, kJ);
int i = tempText.trim().indexOf("\"");
int j = tempText.trim().lastIndexOf("\"");
if (i == -1 || j == -1) {
i = tempText.trim().indexOf("'");
j = tempText.trim().lastIndexOf("'");
}
value = tempText.trim().substring(i, j);
if (key.isNotEmpty && result.contains(key) && value.isNotEmpty) {
WidgetsBinding.instance.endOfFrame.then((value) {
updateValueBykey(key);
});
}
}
}
} catch (e) {}
}
} }

View File

@@ -1,13 +1,18 @@
import 'dart:ui'; import 'dart:ui';
import 'package:code_text_field/code_text_field.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:highlight/languages/powershell.dart';
import 'package:qinglong_app/base/base_state_widget.dart'; import 'package:qinglong_app/base/base_state_widget.dart';
import 'package:qinglong_app/base/routes.dart'; import 'package:qinglong_app/base/routes.dart';
import 'package:qinglong_app/base/sp_const.dart';
import 'package:qinglong_app/base/theme.dart';
import 'package:qinglong_app/base/ui/abs_underline_tabindicator.dart'; import 'package:qinglong_app/base/ui/abs_underline_tabindicator.dart';
import 'package:qinglong_app/base/ui/empty_widget.dart'; import 'package:qinglong_app/base/ui/empty_widget.dart';
import 'package:qinglong_app/main.dart'; import 'package:qinglong_app/main.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:qinglong_app/utils/sp_utils.dart';
import '../../base/ui/syntax_highlighter.dart'; import '../../base/ui/syntax_highlighter.dart';
import 'config_viewmodel.dart'; import 'config_viewmodel.dart';
@@ -105,7 +110,7 @@ class ConfigPageState extends State<ConfigPage>
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
} }
class CodeWidget extends StatefulWidget { class CodeWidget extends ConsumerStatefulWidget {
final String content; final String content;
const CodeWidget({ const CodeWidget({
@@ -114,29 +119,63 @@ class CodeWidget extends StatefulWidget {
}) : super(key: key); }) : super(key: key);
@override @override
State<CodeWidget> createState() => _CodeWidgetState(); ConsumerState<CodeWidget> createState() => _CodeWidgetState();
} }
class _CodeWidgetState extends State<CodeWidget> class _CodeWidgetState extends ConsumerState<CodeWidget>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin {
CodeController? _codeController;
@override
void dispose() {
_codeController?.dispose();
super.dispose();
}
String result = "";
@override
void initState() {
result = widget.content;
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SelectableText.rich( super.build(context);
TextSpan( _codeController ??= CodeController(
style: GoogleFonts.droidSansMono(fontSize: 14).apply( text: result,
fontSizeFactor: 1, language: powershell,
onChange: (value) {
result = value;
},
theme: ref.watch(themeProvider).themeColor.codeEditorTheme(),
stringMap: {
"export": const TextStyle(
fontWeight: FontWeight.normal, color: Color(0xff6B2375)),
},
);
return SafeArea(
top: false,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: SpUtil.getBool(spShowLine, defValue: false) ? 0 : 10,
),
child: CodeField(
controller: _codeController!,
expands: true,
readOnly: true,
background: Colors.white,
wrap: SpUtil.getBool(spShowLine, defValue: false) ? false : true,
hideColumn: !SpUtil.getBool(spShowLine, defValue: false),
lineNumberStyle: LineNumberStyle(
textStyle: TextStyle(
color: ref.watch(themeProvider).themeColor.descColor(),
fontSize: 12,
),
), ),
children: <TextSpan>[
DartSyntaxHighlighter(SyntaxHighlighterStyle.lightThemeStyle())
.format(widget.content)
],
), ),
style: DefaultTextStyle.of(context).style.apply(
fontSizeFactor: 1,
), ),
selectionWidthStyle: BoxWidthStyle.max,
selectionHeightStyle: BoxHeightStyle.max,
autofocus: true,
); );
} }

View File

@@ -35,7 +35,7 @@ class _AddEnvPageState extends ConsumerState<AddEnvPage> {
} else { } else {
envBean = EnvBean(); envBean = EnvBean();
} }
WidgetsBinding.instance?.addPostFrameCallback( WidgetsBinding.instance.addPostFrameCallback(
(timeStamp) { (timeStamp) {
focusNode.requestFocus(); focusNode.requestFocus();
}, },
@@ -186,8 +186,12 @@ class _AddEnvPageState extends ConsumerState<AddEnvPage> {
envBean.value = _valueController.text; envBean.value = _valueController.text;
envBean.remarks = _remarkController.text; envBean.remarks = _remarkController.text;
HttpResponse<NullResponse> response = await Api.addEnv( HttpResponse<NullResponse> response = await Api.addEnv(
_nameController.text, _valueController.text, _remarkController.text, _nameController.text,
id: envBean.id,nId: envBean.nId,); _valueController.text,
_remarkController.text,
id: envBean.id,
nId: envBean.nId,
);
if (response.success) { if (response.success) {
(envBean.sId == null) ? "新增成功" : "修改成功".toast(); (envBean.sId == null) ? "新增成功" : "修改成功".toast();

View File

@@ -12,7 +12,14 @@ class EnvBean {
String? name; String? name;
String? remarks; String? remarks;
EnvBean({this.value, this.sId, this.created, this.status, this.timestamp, this.name, this.remarks}); EnvBean(
{this.value,
this.sId,
this.created,
this.status,
this.timestamp,
this.name,
this.remarks});
get nId => _id; get nId => _id;
@@ -20,7 +27,9 @@ class EnvBean {
value = json['value']; value = json['value'];
id = json['id']; id = json['id'];
_id = json['_id']; _id = json['_id'];
sId = json.containsKey('_id') ? json['_id'].toString() : (json.containsKey('id') ? json['id'].toString() : ""); sId = json.containsKey('_id')
? json['_id'].toString()
: (json.containsKey('id') ? json['id'].toString() : "");
created = int.tryParse(json['created'].toString()); created = int.tryParse(json['created'].toString());
status = json['status']; status = json['status'];
timestamp = json['timestamp']; timestamp = json['timestamp'];

View File

@@ -36,7 +36,8 @@ class _TaskDetailPageState extends ConsumerState<EnvDetailPage> {
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
Navigator.of(context).pushNamed(Routes.routeAddEnv, arguments: widget.envBean); Navigator.of(context)
.pushNamed(Routes.routeAddEnv, arguments: widget.envBean);
}, },
child: Container( child: Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@@ -225,7 +226,9 @@ class _TaskDetailPageState extends ConsumerState<EnvDetailPage> {
} }
void enableTask() async { void enableTask() async {
await ref.read(envProvider).enableEnv(widget.envBean.sId!, widget.envBean.status!); await ref
.read(envProvider)
.enableEnv(widget.envBean.sId!, widget.envBean.status!);
setState(() {}); setState(() {});
} }
@@ -322,14 +325,16 @@ class EnvDetailCell extends ConsumerWidget {
} }
}, },
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.descColor(), color:
ref.watch(themeProvider).themeColor.descColor(),
fontSize: 14, fontSize: 14,
), ),
), ),
), ),
) )
: Expanded( : Expanded(
child: Align(alignment: Alignment.centerRight, child: icon!), child:
Align(alignment: Alignment.centerRight, child: icon!),
), ),
], ],
), ),

View File

@@ -83,7 +83,8 @@ class _EnvPageState extends State<EnvPage> {
], ],
) )
: ReorderableListView( : ReorderableListView(
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, keyboardDismissBehavior:
ScrollViewKeyboardDismissBehavior.onDrag,
header: searchCell(context, ref), header: searchCell(context, ref),
onReorder: (int oldIndex, int newIndex) { onReorder: (int oldIndex, int newIndex) {
if (list.length != model.list.length) { if (list.length != model.list.length) {
@@ -97,9 +98,11 @@ class _EnvPageState extends State<EnvPage> {
if (newIndex > oldIndex) { if (newIndex > oldIndex) {
newIndex -= 1; newIndex -= 1;
} }
final EnvBean item = model.list.removeAt(oldIndex); final EnvBean item =
model.list.removeAt(oldIndex);
model.list.insert(newIndex, item); model.list.insert(newIndex, item);
model.update(item.sId ?? "", newIndex, oldIndex); model.update(
item.sId ?? "", newIndex, oldIndex);
}, },
); );
}, },
@@ -145,7 +148,9 @@ class _EnvPageState extends State<EnvPage> {
child: Text( child: Text(
EnvViewModel.allStr, EnvViewModel.allStr,
style: TextStyle( style: TextStyle(
color: currentState == EnvViewModel.allStr ? ref.watch(themeProvider).primaryColor : ref.watch(themeProvider).themeColor.titleColor(), color: currentState == EnvViewModel.allStr
? ref.watch(themeProvider).primaryColor
: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 14, fontSize: 14,
), ),
), ),
@@ -155,7 +160,9 @@ class _EnvPageState extends State<EnvPage> {
child: Text( child: Text(
EnvViewModel.enabledStr, EnvViewModel.enabledStr,
style: TextStyle( style: TextStyle(
color: currentState == EnvViewModel.enabledStr ? ref.watch(themeProvider).primaryColor : ref.watch(themeProvider).themeColor.titleColor(), color: currentState == EnvViewModel.enabledStr
? ref.watch(themeProvider).primaryColor
: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 14, fontSize: 14,
), ),
), ),
@@ -165,8 +172,9 @@ class _EnvPageState extends State<EnvPage> {
child: Text( child: Text(
EnvViewModel.disabledStr, EnvViewModel.disabledStr,
style: TextStyle( style: TextStyle(
color: color: currentState == EnvViewModel.disabledStr
currentState == EnvViewModel.disabledStr ? ref.watch(themeProvider).primaryColor : ref.watch(themeProvider).themeColor.titleColor(), ? ref.watch(themeProvider).primaryColor
: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 14, fontSize: 14,
), ),
), ),
@@ -199,7 +207,8 @@ class EnvItemCell extends StatelessWidget {
final int index; final int index;
final WidgetRef ref; final WidgetRef ref;
const EnvItemCell(this.bean, this.index, this.ref, {Key? key}) : super(key: key); const EnvItemCell(this.bean, this.index, this.ref, {Key? key})
: super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -212,7 +221,8 @@ class EnvItemCell extends StatelessWidget {
SlidableAction( SlidableAction(
backgroundColor: const Color(0xff5D5E70), backgroundColor: const Color(0xff5D5E70),
onPressed: (_) { onPressed: (_) {
Navigator.of(context).pushNamed(Routes.routeAddEnv, arguments: bean); Navigator.of(context)
.pushNamed(Routes.routeAddEnv, arguments: bean);
}, },
foregroundColor: Colors.white, foregroundColor: Colors.white,
icon: CupertinoIcons.pencil_outline, icon: CupertinoIcons.pencil_outline,
@@ -223,7 +233,9 @@ class EnvItemCell extends StatelessWidget {
enableEnv(context); enableEnv(context);
}, },
foregroundColor: Colors.white, foregroundColor: Colors.white,
icon: bean.status == 0 ? Icons.dnd_forwardslash : Icons.check_circle_outline_sharp, icon: bean.status == 0
? Icons.dnd_forwardslash
: Icons.check_circle_outline_sharp,
), ),
SlidableAction( SlidableAction(
backgroundColor: const Color(0xffEA4D3E), backgroundColor: const Color(0xffEA4D3E),
@@ -245,7 +257,8 @@ class EnvItemCell extends StatelessWidget {
color: ref.watch(themeProvider).themeColor.settingBgColor(), color: ref.watch(themeProvider).themeColor.settingBgColor(),
child: InkWell( child: InkWell(
onTap: () { onTap: () {
Navigator.of(context).pushNamed(Routes.routeEnvDetail, arguments: bean); Navigator.of(context)
.pushNamed(Routes.routeEnvDetail, arguments: bean);
}, },
child: Container( child: Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@@ -272,13 +285,22 @@ class EnvItemCell extends StatelessWidget {
horizontal: 5, horizontal: 5,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(3), borderRadius:
border: Border.all(color: ref.watch(themeProvider).themeColor.descColor(), width: 1), BorderRadius.circular(3),
border: Border.all(
color: ref
.watch(themeProvider)
.themeColor
.descColor(),
width: 1),
), ),
child: Text( child: Text(
"${getIndexByIndex(context, index)}", "${getIndexByIndex(context, index)}",
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.descColor(), color: ref
.watch(themeProvider)
.themeColor
.descColor(),
fontSize: 12, fontSize: 12,
), ),
), ),
@@ -299,7 +321,10 @@ class EnvItemCell extends StatelessWidget {
maxLines: 1, maxLines: 1,
style: TextStyle( style: TextStyle(
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -315,7 +340,8 @@ class EnvItemCell extends StatelessWidget {
), ),
), ),
child: Visibility( child: Visibility(
visible: bean.remarks != null && bean.remarks!.isNotEmpty, visible: bean.remarks != null &&
bean.remarks!.isNotEmpty,
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: Text( child: Text(
@@ -324,7 +350,10 @@ class EnvItemCell extends StatelessWidget {
style: TextStyle( style: TextStyle(
height: 1, height: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
color: ref.watch(themeProvider).themeColor.descColor(), color: ref
.watch(themeProvider)
.themeColor
.descColor(),
fontSize: 12, fontSize: 12,
), ),
), ),
@@ -358,7 +387,10 @@ class EnvItemCell extends StatelessWidget {
maxLines: 1, maxLines: 1,
style: TextStyle( style: TextStyle(
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
color: ref.watch(themeProvider).themeColor.descColor(), color: ref
.watch(themeProvider)
.themeColor
.descColor(),
fontSize: 12, fontSize: 12,
), ),
), ),
@@ -375,7 +407,8 @@ class EnvItemCell extends StatelessWidget {
maxLines: 1, maxLines: 1,
style: TextStyle( style: TextStyle(
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
color: ref.watch(themeProvider).themeColor.descColor(), color:
ref.watch(themeProvider).themeColor.descColor(),
fontSize: 12, fontSize: 12,
), ),
), ),

View File

@@ -15,7 +15,6 @@ class EnvViewModel extends BaseViewModel {
List<EnvBean> disabledList = []; List<EnvBean> disabledList = [];
List<EnvBean> enabledList = []; List<EnvBean> enabledList = [];
Future<void> loadData([isLoading = true]) async { Future<void> loadData([isLoading = true]) async {
if (isLoading && list.isEmpty) { if (isLoading && list.isEmpty) {
loading(notify: true); loading(notify: true);
@@ -23,12 +22,12 @@ class EnvViewModel extends BaseViewModel {
HttpResponse<List<EnvBean>> result = await Api.envs(""); HttpResponse<List<EnvBean>> result = await Api.envs("");
if (result.success && result.bean != null) { if (result.success && result.bean != null) {
list.clear(); list.clear();
list.addAll(result.bean!); list.addAll(result.bean!);
disabledList.clear(); disabledList.clear();
disabledList.addAll(list.where((element) => element.status == 1).toList()); disabledList
.addAll(list.where((element) => element.status == 1).toList());
enabledList.clear(); enabledList.clear();
enabledList.addAll(list.where((element) => element.status == 0).toList()); enabledList.addAll(list.where((element) => element.status == 0).toList());
success(); success();

View File

@@ -44,7 +44,7 @@ class _HomePageState extends ConsumerState<HomePage>
initTitles(); initTitles();
_title = titles[0].title; _title = titles[0].title;
super.initState(); super.initState();
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
getSystemBean(); getSystemBean();
}); });
} }

View File

@@ -2,6 +2,7 @@ import 'package:dio_log/dio_log.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_animator/flutter_animator.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qinglong_app/base/http/http.dart'; import 'package:qinglong_app/base/http/http.dart';
import 'package:qinglong_app/base/routes.dart'; import 'package:qinglong_app/base/routes.dart';
@@ -43,7 +44,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
if (!widget.doNotLoadLocalData) { if (!widget.doNotLoadLocalData) {
_hostController.text = getIt<UserInfoViewModel>().host ?? ""; _hostController.text = getIt<UserInfoViewModel>().host ?? "";
useSecretLogin = getIt<UserInfoViewModel>().useSecretLogined; useSecretLogin = getIt<UserInfoViewModel>().useSecretLogined;
if (getIt<UserInfoViewModel>().userName != null && getIt<UserInfoViewModel>().userName!.isNotEmpty) { if (getIt<UserInfoViewModel>().userName != null &&
getIt<UserInfoViewModel>().userName!.isNotEmpty) {
if (getIt<UserInfoViewModel>().useSecretLogined) { if (getIt<UserInfoViewModel>().useSecretLogined) {
_cIdController.text = getIt<UserInfoViewModel>().userName!; _cIdController.text = getIt<UserInfoViewModel>().userName!;
} else { } else {
@@ -53,7 +55,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
} else { } else {
rememberPassword = false; rememberPassword = false;
} }
if (getIt<UserInfoViewModel>().passWord != null && getIt<UserInfoViewModel>().passWord!.isNotEmpty) { if (getIt<UserInfoViewModel>().passWord != null &&
getIt<UserInfoViewModel>().passWord!.isNotEmpty) {
if (getIt<UserInfoViewModel>().useSecretLogined) { if (getIt<UserInfoViewModel>().useSecretLogined) {
_cSecretController.text = getIt<UserInfoViewModel>().passWord!; _cSecretController.text = getIt<UserInfoViewModel>().passWord!;
} else { } else {
@@ -62,34 +65,25 @@ class _LoginPageState extends ConsumerState<LoginPage> {
} }
} }
getIt<UserInfoViewModel>().updateToken(""); getIt<UserInfoViewModel>().updateToken("");
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (useSecretLogin) { if (useSecretLogin) {
cardKey.currentState?.toggleCard(); cardKey.currentState?.toggleCard();
} }
}); });
} }
GlobalKey<AnimatorWidgetState> loginKey = GlobalKey<AnimatorWidgetState>();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: AnnotatedRegion<SystemUiOverlayStyle>( body: AnnotatedRegion<SystemUiOverlayStyle>(
value: ref.watch(themeProvider).darkMode == true ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark, value: ref.watch(themeProvider).darkMode == true
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
child: ColoredBox( child: ColoredBox(
color: ref.watch(themeProvider).themeColor.settingBgColor(), color: ref.watch(themeProvider).themeColor.settingBgColor(),
child: SizedBox( child: SingleChildScrollView(
height: MediaQuery.of(context).size.height,
child: Stack(
children: [
Positioned(
bottom: 0,
child: Image.asset(
"assets/images/login_bg.png",
width: MediaQuery.of(context).size.width,
),
),
Scaffold(
backgroundColor: Colors.transparent,
body: SingleChildScrollView(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -103,16 +97,23 @@ class _LoginPageState extends ConsumerState<LoginPage> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox( SizedBox(
height: MediaQuery.of(context).size.height / 10, height: MediaQuery.of(context).size.height / 8,
), ),
Row( Row(
children: [ children: [
Expanded( Expanded(
child: Align( child: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Image.asset( child: Text(
"assets/images/login_tip.png", useSecretLogin ? "client_id登录" : "账号登录",
height: 45, style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
),
), ),
), ),
), ),
@@ -121,9 +122,11 @@ class _LoginPageState extends ConsumerState<LoginPage> {
if (debugBtnIsShow()) { if (debugBtnIsShow()) {
dismissDebugBtn(); dismissDebugBtn();
} else { } else {
showDebugBtn(context, btnColor: ref.watch(themeProvider).primaryColor); showDebugBtn(context,
btnColor:
ref.watch(themeProvider).primaryColor);
} }
WidgetsBinding.instance?.endOfFrame; WidgetsBinding.instance.endOfFrame;
}, },
child: Image.asset( child: Image.asset(
"assets/images/ql.png", "assets/images/ql.png",
@@ -135,24 +138,52 @@ class _LoginPageState extends ConsumerState<LoginPage> {
SizedBox( SizedBox(
height: MediaQuery.of(context).size.height / 15, height: MediaQuery.of(context).size.height / 15,
), ),
const Text( Row(
"域名:", children: [
const SizedBox(
width: 40,
child: Text(
"域名",
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600,
), ),
), ),
TextField( ),
const SizedBox(
width: 30,
),
Expanded(
child: TextField(
onChanged: (_) { onChanged: (_) {
setState(() {}); setState(() {});
}, },
controller: _hostController, controller: _hostController,
decoration: const InputDecoration( decoration: InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5), border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
isDense: true,
contentPadding: const EdgeInsets.all(4),
hintText: "http://1.1.1.1:5700", hintText: "http://1.1.1.1:5700",
hintStyle: TextStyle(
fontSize: 16,
color: ref
.watch(themeProvider)
.themeColor
.descColor(),
),
), ),
autofocus: false, autofocus: false,
), ),
),
],
),
const Divider(
color: Color(0xff999999),
),
const SizedBox(
height: 10,
),
FlipCard( FlipCard(
key: cardKey, key: cardKey,
flipOnTouch: false, flipOnTouch: false,
@@ -162,118 +193,209 @@ class _LoginPageState extends ConsumerState<LoginPage> {
}, },
direction: FlipDirection.HORIZONTAL, direction: FlipDirection.HORIZONTAL,
front: SizedBox( front: SizedBox(
height: 200, height: 110,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [ children: [
const SizedBox( const SizedBox(
height: 15, child: Text(
), "账户",
const Text(
"用户名:",
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600,
), ),
), ),
TextField( width: 40,
),
const SizedBox(
width: 30,
),
Expanded(
child: TextField(
onChanged: (_) { onChanged: (_) {
setState(() {}); setState(() {});
}, },
controller: _userNameController, controller: _userNameController,
decoration: const InputDecoration( decoration: InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5), border: InputBorder.none,
hintText: "请输入用户名", enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
isDense: true,
contentPadding: const EdgeInsets.all(4),
hintText: "请输入账户",
hintStyle: TextStyle(
fontSize: 16,
color: ref
.watch(themeProvider)
.themeColor
.descColor(),
),
), ),
autofocus: false, autofocus: false,
), ),
const SizedBox(
height: 15,
), ),
const Text( ],
"密码:", ),
const Divider(
color: Color(0xff999999),
),
const SizedBox(
height: 10,
),
Row(
children: [
const SizedBox(
width: 40,
child: Text(
"密码",
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600,
), ),
), ),
TextField( ),
const SizedBox(
width: 30,
),
Expanded(
child: TextField(
onChanged: (_) { onChanged: (_) {
setState(() {}); setState(() {});
}, },
controller: _passwordController, controller: _passwordController,
obscureText: true, obscureText: true,
decoration: const InputDecoration( decoration: InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5), border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
isDense: true,
contentPadding: const EdgeInsets.all(4),
hintText: "请输入密码", hintText: "请输入密码",
hintStyle: TextStyle(
fontSize: 16,
color: ref
.watch(themeProvider)
.themeColor
.descColor(),
),
), ),
autofocus: false, autofocus: false,
), ),
const SizedBox( ),
height: 10, ],
),
const Divider(
color: Color(0xff999999),
), ),
], ],
), ),
), ),
back: SizedBox( back: SizedBox(
height: 200, height: 110,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [ children: [
const SizedBox( const SizedBox(
height: 15, width: 40,
), child: Text(
const Text( "id",
"client_id:",
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600,
), ),
), ),
TextField( ),
const SizedBox(
width: 30,
),
Expanded(
child: TextField(
onChanged: (_) { onChanged: (_) {
setState(() {}); setState(() {});
}, },
controller: _cIdController, controller: _cIdController,
decoration: const InputDecoration( decoration: InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5), border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
isDense: true,
contentPadding: const EdgeInsets.all(4),
hintText: "请输入client_id", hintText: "请输入client_id",
hintStyle: TextStyle(
fontSize: 16,
color: ref
.watch(themeProvider)
.themeColor
.descColor(),
),
), ),
autofocus: false, autofocus: false,
), ),
const SizedBox(
height: 15,
), ),
const Text( ],
"client_secret:", ),
const Divider(
color: Color(0xff999999),
),
const SizedBox(
height: 10,
),
Row(
children: [
const SizedBox(
width: 50,
child: Text(
"secret",
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600,
), ),
), ),
TextField( ),
const SizedBox(
width: 20,
),
Expanded(
child: TextField(
onChanged: (_) { onChanged: (_) {
setState(() {}); setState(() {});
}, },
controller: _cSecretController, controller: _cSecretController,
obscureText: true, obscureText: true,
decoration: const InputDecoration( decoration: InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5), border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
isDense: true,
contentPadding: const EdgeInsets.all(4),
hintText: "请输入client_secret", hintText: "请输入client_secret",
hintStyle: TextStyle(
fontSize: 16,
color: ref
.watch(themeProvider)
.themeColor
.descColor(),
),
), ),
autofocus: false, autofocus: false,
), ),
const SizedBox( ),
height: 10, ],
),
const Divider(
color: Color(0xff999999),
), ),
], ],
), ),
), ),
), ),
const SizedBox(
height: 10,
),
], ],
), ),
), ),
@@ -291,8 +413,9 @@ class _LoginPageState extends ConsumerState<LoginPage> {
}, },
), ),
const Text( const Text(
"记住密码/client", "记住密码",
style: TextStyle( style: TextStyle(
color: Color(0xff555555),
fontSize: 14, fontSize: 14,
), ),
), ),
@@ -300,13 +423,15 @@ class _LoginPageState extends ConsumerState<LoginPage> {
GestureDetector( GestureDetector(
onTap: () { onTap: () {
cardKey.currentState?.toggleCard(); cardKey.currentState?.toggleCard();
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { WidgetsBinding.instance
.addPostFrameCallback((timeStamp) {
setState(() {}); setState(() {});
}); });
}, },
child: Text( child: Text(
loginByUserName() ? "client_id登录" : "账号登录", loginByUserName() ? "client_id登录" : "账号登录",
style: const TextStyle( style: const TextStyle(
color: Color(0xff555555),
fontSize: 14, fontSize: 14,
), ),
), ),
@@ -320,37 +445,67 @@ class _LoginPageState extends ConsumerState<LoginPage> {
const SizedBox( const SizedBox(
height: 30, height: 30,
), ),
Container( Shake(
preferences: const AnimationPreferences(
autoPlay: AnimationPlayStates.None),
key: loginKey,
child: Center(
child: Container(
alignment: Alignment.center, alignment: Alignment.center,
width: MediaQuery.of(context).size.width * 0.8,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 40, horizontal: 40,
), ),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: canClickLoginBtn()
? [
const Color(0xff5DD16F),
const Color(0xff089556),
]
: [
const Color(0xff5DD16F).withOpacity(0.6),
const Color(0xff089556).withOpacity(0.6),
],
),
borderRadius: const BorderRadius.all(
Radius.circular(10),
),
),
child: SizedBox( child: SizedBox(
width: MediaQuery.of(context).size.width - 80, width: MediaQuery.of(context).size.width - 80,
child: IgnorePointer( child: IgnorePointer(
ignoring: !canClickLoginBtn(), ignoring: !canClickLoginBtn(),
child: CupertinoButton( child: Builder(
builder: (context) {
return CupertinoButton(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 5, vertical: 5,
), ),
color: canClickLoginBtn() ? ref.watch(themeProvider).primaryColor : ref.watch(themeProvider).primaryColor.withOpacity(0.4),
child: isLoading child: isLoading
? const CupertinoActivityIndicator() ? const CupertinoActivityIndicator()
: const Text( : const Text(
"登 录", "登 录",
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 18,
color: Colors.white,
), ),
), ),
onPressed: () { onPressed: () async {
Http.pushedLoginPage = false; Http.pushedLoginPage = false;
Utils.hideKeyBoard(context); Utils.hideKeyBoard(context);
if (loginByUserName()) { if (loginByUserName()) {
login(_userNameController.text, _passwordController.text); login(_userNameController.text,
_passwordController.text);
} else { } else {
login(_cIdController.text, _cSecretController.text); login(_cIdController.text,
_cSecretController.text);
} }
}, },
);
},
),
),
), ),
), ),
), ),
@@ -358,15 +513,11 @@ class _LoginPageState extends ConsumerState<LoginPage> {
const SizedBox( const SizedBox(
height: 30, height: 30,
), ),
], (getIt<UserInfoViewModel>().historyAccounts.isEmpty)
),
),
),
Positioned(
bottom: 10,
child: (getIt<UserInfoViewModel>().historyAccounts.isEmpty)
? const SizedBox.shrink() ? const SizedBox.shrink()
: SizedBox( : SafeArea(
top: false,
child: SizedBox(
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
child: Center( child: Center(
child: Material( child: Material(
@@ -375,22 +526,38 @@ class _LoginPageState extends ConsumerState<LoginPage> {
onSelected: (UserInfoBean result) { onSelected: (UserInfoBean result) {
selected(result); selected(result);
}, },
itemBuilder: (BuildContext context) => <PopupMenuEntry<UserInfoBean>>[ itemBuilder: (BuildContext context) =>
<PopupMenuEntry<UserInfoBean>>[
...getIt<UserInfoViewModel>() ...getIt<UserInfoViewModel>()
.historyAccounts .historyAccounts
.map((e) => PopupMenuItem<UserInfoBean>( .map(
(e) => PopupMenuItem<UserInfoBean>(
value: e, value: e,
child: buildCell(e), child: buildCell(context, e),
)) ),
)
.toList(), .toList(),
], ],
child: Text( child: Row(
"切换账号", mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
"assets/images/icon_history.png",
fit: BoxFit.cover,
width: 16,
),
const SizedBox(
width: 5,
),
const Text(
"历史账号",
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).primaryColor, color: Color(0xff555555),
fontSize: 14, fontSize: 14,
), ),
), ),
],
),
), ),
), ),
), ),
@@ -416,7 +583,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
isLoading = true; isLoading = true;
setState(() {}); setState(() {});
helper = LoginHelper(useSecretLogin, _hostController.text, userName, password, rememberPassword); helper = LoginHelper(useSecretLogin, _hostController.text, userName,
password, rememberPassword);
var response = await helper!.login(); var response = await helper!.login();
dealLoginResponse(response); dealLoginResponse(response);
} }
@@ -434,6 +602,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
void loginFailed() { void loginFailed() {
isLoading = false; isLoading = false;
loginKey.currentState?.forward();
setState(() {}); setState(() {});
} }
@@ -442,9 +611,11 @@ class _LoginPageState extends ConsumerState<LoginPage> {
if (_hostController.text.isEmpty) return false; if (_hostController.text.isEmpty) return false;
if (!loginByUserName()) { if (!loginByUserName()) {
return _cIdController.text.isNotEmpty && _cSecretController.text.isNotEmpty; return _cIdController.text.isNotEmpty &&
_cSecretController.text.isNotEmpty;
} else { } else {
return _userNameController.text.isNotEmpty && _passwordController.text.isNotEmpty; return _userNameController.text.isNotEmpty &&
_passwordController.text.isNotEmpty;
} }
} }
@@ -512,7 +683,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
}); });
} }
Widget buildCell(UserInfoBean bean) { Widget buildCell(BuildContext context, UserInfoBean bean) {
return ListTile( return ListTile(
title: Text( title: Text(
bean.host ?? "", bean.host ?? "",
@@ -525,6 +696,19 @@ class _LoginPageState extends ConsumerState<LoginPage> {
bean.userName ?? "", bean.userName ?? "",
), ),
), ),
contentPadding: EdgeInsets.zero,
trailing: GestureDetector(
onTap: () {
getIt<UserInfoViewModel>().removeHistoryAccount(bean.host);
Navigator.pop(context);
setState(() {});
},
child: const Icon(
CupertinoIcons.clear_thick,
size: 20,
),
),
); );
} }
@@ -534,7 +718,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
_cIdController.text = result.userName ?? ""; _cIdController.text = result.userName ?? "";
_cSecretController.text = result.password ?? ""; _cSecretController.text = result.password ?? "";
if (cardKey.currentState?.isFront ?? false) { if (cardKey.currentState?.isFront ?? false) {
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
cardKey.currentState?.toggleCard(); cardKey.currentState?.toggleCard();
}); });
} }
@@ -542,7 +726,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
_userNameController.text = result.userName ?? ""; _userNameController.text = result.userName ?? "";
_passwordController.text = result.password ?? ""; _passwordController.text = result.password ?? "";
if (!(cardKey.currentState?.isFront ?? false)) { if (!(cardKey.currentState?.isFront ?? false)) {
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
cardKey.currentState?.toggleCard(); cardKey.currentState?.toggleCard();
}); });
} }

View File

@@ -19,6 +19,7 @@ class UserBean {
data['twoFactorActivated'] = this.twoFactorActivated; data['twoFactorActivated'] = this.twoFactorActivated;
return data; return data;
} }
static UserBean jsonConversion(Map<String, dynamic> json) { static UserBean jsonConversion(Map<String, dynamic> json) {
return UserBean.fromJson(json); return UserBean.fromJson(json);
} }

View File

@@ -23,7 +23,6 @@ class _AboutPageState extends ConsumerState<AboutPage> {
void initState() { void initState() {
super.initState(); super.initState();
getInfo(); getInfo();
} }
@override @override
@@ -88,7 +87,8 @@ class _AboutPageState extends ConsumerState<AboutPage> {
const Spacer(), const Spacer(),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
_launchURL("https://github.com/qinglong-app/qinglong_app/releases"); _launchURL(
"https://github.com/qinglong-app/qinglong_app/releases");
}, },
child: Text( child: Text(
"版本更新", "版本更新",

View File

@@ -0,0 +1,278 @@
import 'dart:convert';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path_provider/path_provider.dart';
import 'package:qinglong_app/base/http/api.dart';
import 'package:qinglong_app/base/ql_app_bar.dart';
import 'package:qinglong_app/json.jc.dart';
import 'package:qinglong_app/module/env/env_bean.dart';
import 'package:qinglong_app/utils/extension.dart';
import 'package:share_plus/share_plus.dart';
import '../../../base/theme.dart';
import '../../../base/userinfo_viewmodel.dart';
import '../../../main.dart';
class BackUpPage extends ConsumerStatefulWidget {
const BackUpPage({Key? key}) : super(key: key);
@override
_BackUpPageState createState() => _BackUpPageState();
}
class _BackUpPageState extends ConsumerState<BackUpPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: QlAppBar(
canBack: true,
title: '数据备份',
),
body: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
margin: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 15,
),
decoration: BoxDecoration(
color: ref.watch(themeProvider).themeColor.settingBordorColor(),
borderRadius: const BorderRadius.all(
Radius.circular(15),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
getEnv();
},
child: Padding(
padding: const EdgeInsets.only(
top: 10,
bottom: 5,
left: 15,
right: 15,
),
child: Row(
children: [
Text(
"环境变量备份到本地",
style: TextStyle(
color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16,
),
),
const Spacer(),
const Icon(
CupertinoIcons.right_chevron,
size: 16,
),
],
),
),
),
const Divider(
indent: 15,
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
uploadEnv(context);
},
child: Padding(
padding: const EdgeInsets.only(
top: 10,
bottom: 5,
left: 15,
right: 15,
),
child: Row(
children: [
Text(
"从本地导入环境变量",
style: TextStyle(
color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16,
),
),
const Spacer(),
const Icon(
CupertinoIcons.right_chevron,
size: 16,
),
],
),
),
),
const Divider(
indent: 15,
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
uploadEnvFromFile();
},
child: Padding(
padding: const EdgeInsets.only(
top: 10,
bottom: 5,
left: 15,
right: 15,
),
child: Row(
children: [
Text(
"从文件导入环境变量",
style: TextStyle(
color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16,
),
),
const Spacer(),
const Icon(
CupertinoIcons.right_chevron,
size: 16,
),
],
),
),
),
const Divider(
indent: 15,
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
shareEnvs();
},
child: Padding(
padding: const EdgeInsets.only(
top: 10,
bottom: 5,
left: 15,
right: 15,
),
child: Row(
children: [
Text(
"导出环境变量",
style: TextStyle(
color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16,
),
),
const Spacer(),
const Icon(
CupertinoIcons.right_chevron,
size: 16,
),
],
),
),
),
const Divider(
indent: 15,
),
]),
)
],
),
);
}
Future<void> shareEnvs() async {
Directory appDocDir = await getApplicationDocumentsDirectory();
var dir = Directory("${appDocDir.path}/backup/");
var fileList = await dir.list().toList();
List<String> paths = [];
for(var i=0;i<fileList.length;i++){
paths.add(fileList[i].path);
}
await Share.shareFiles(paths);
}
void getEnv() async {
var envs = await Api.envs("");
var data = json.encode(envs.bean);
Directory appDocDir = await getApplicationDocumentsDirectory();
// String? path = await FilePicker.platform.getDirectoryPath(initialDirectory:appDocDir.path,dialogTitle: "请选择保存的文件夹");
Directory("${appDocDir.path}/backup/").create();
Uri uri = Uri.parse(getIt<UserInfoViewModel>().host!);
File file = File("${appDocDir.path}/backup/${uri.host}-${uri.port}.env");
file = await file.writeAsString(data, flush: true);
"备份成功".toast();
}
void uploadEnvFromFile()async{
var result = await FilePicker.platform.pickFiles(dialogTitle: "请选择env文件");
if (result == null)return;
File f = File(result.paths[0]!);
var data = await f.readAsString();
var content = jsonDecode(data) as List<dynamic>;
for (var j = 0; j < content.length; j++) {
var bean = JsonConversion$Json.fromJson<EnvBean>(content[j]);
var resp = await Api.addEnv(bean.name!, bean.value!, bean.remarks!);
if (!resp.success){
resp.message.toast();
}
}
"备份完成".toast();
}
}
void uploadEnv(BuildContext context) async {
Directory appDocDir = await getApplicationDocumentsDirectory();
List<Widget> widgets = [];
var dir = Directory("${appDocDir.path}/backup/");
var fileList = await dir.list().toList();
for (var i = 0; i < fileList.length; i++) {
var paths = fileList[i].path.split("/");
widgets.add(
ListTile(
title: Text(paths[paths.length - 1]),
onTap: () async {
File f = File(fileList[i].path);
var data = await f.readAsString();
var content = jsonDecode(data) as List<dynamic>;
for (var j = 0; j < content.length; j++) {
var bean = JsonConversion$Json.fromJson<EnvBean>(content[j]);
var resp = await Api.addEnv(bean.name!, bean.value!, bean.remarks!);
if (!resp.success){
resp.message.toast();
}
}
"备份完成".toast();
},
));
}
var dialog = SimpleDialog(title: const Text("请选择备份文件"), children: widgets);
await showDialog(
context: context,
builder: (BuildContext context) {
return dialog;
});
}

View File

@@ -129,6 +129,8 @@ class _AddDependencyPageState extends ConsumerState<AddDependencyPage> {
), ),
TextField( TextField(
controller: _nameController, controller: _nameController,
maxLines: 10,
minLines: 1,
decoration: const InputDecoration( decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5), contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "请输入名称", hintText: "请输入名称",
@@ -149,10 +151,18 @@ class _AddDependencyPageState extends ConsumerState<AddDependencyPage> {
return; return;
} }
HttpResponse<NullResponse> response = await Api.addDependency( List<Map<String, dynamic>> list = [];
_nameController.text,
depedencyType.index, List<String> names = _nameController.text.split("\n");
); list.addAll(names
.map(
(e) => {
"name": e,
"type": depedencyType.index,
},
)
.toList());
HttpResponse<NullResponse> response = await Api.addDependency(list);
if (response.success) { if (response.success) {
"新增成功".toast(); "新增成功".toast();

View File

@@ -21,7 +21,8 @@ class DependencyPage extends StatefulWidget {
_DependcyPageState createState() => _DependcyPageState(); _DependcyPageState createState() => _DependcyPageState();
} }
class _DependcyPageState extends State<DependencyPage> with TickerProviderStateMixin { class _DependcyPageState extends State<DependencyPage>
with TickerProviderStateMixin {
List<DepedencyEnum> types = []; List<DepedencyEnum> types = [];
TabController? _tabController; TabController? _tabController;
@@ -125,7 +126,9 @@ class _DependcyPageState extends State<DependencyPage> with TickerProviderStateM
itemCount: list.length, itemCount: list.length,
), ),
onRefresh: () { onRefresh: () {
return model.loadData(types[_tabController!.index].name.toLowerCase(), false); return model.loadData(
types[_tabController!.index].name.toLowerCase(),
false);
}, },
); );
}, },
@@ -222,7 +225,10 @@ class DependencyCell extends ConsumerWidget {
maxLines: 1, maxLines: 1,
style: TextStyle( style: TextStyle(
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 18, fontSize: 18,
), ),
), ),
@@ -255,11 +261,16 @@ class DependencyCell extends ConsumerWidget {
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: Text( child: Text(
(bean.created == null || bean.created == 0) ? "-" : Utils.formatMessageTime(bean.created!), (bean.created == null || bean.created == 0)
? "-"
: Utils.formatMessageTime(bean.created!),
maxLines: 1, maxLines: 1,
style: TextStyle( style: TextStyle(
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
color: ref.watch(themeProvider).themeColor.descColor(), color: ref
.watch(themeProvider)
.themeColor
.descColor(),
fontSize: 12, fontSize: 12,
), ),
), ),
@@ -283,7 +294,7 @@ class DependencyCell extends ConsumerWidget {
} }
void showLog(WidgetRef ref, String? sId) { void showLog(WidgetRef ref, String? sId) {
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
showCupertinoDialog( showCupertinoDialog(
builder: (BuildContext context) { builder: (BuildContext context) {
return CupertinoAlertDialog( return CupertinoAlertDialog(
@@ -357,7 +368,9 @@ class DependencyCell extends ConsumerWidget {
), ),
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
ref.read(dependencyProvider).del(type.name.toLowerCase(), sId ?? ""); ref
.read(dependencyProvider)
.del(type.name.toLowerCase(), sId ?? "");
}, },
), ),
], ],

View File

@@ -15,7 +15,10 @@ class DependencyViewModel extends BaseViewModel {
List<DependencyBean> linuxList = []; List<DependencyBean> linuxList = [];
Future<void> loadData(String type, [bool showLoading = true]) async { Future<void> loadData(String type, [bool showLoading = true]) async {
if (showLoading && ((type == "nodejs" && nodeJsList.isEmpty) || (type == "python3" && python3List.isEmpty) || (type == "linux" && linuxList.isEmpty))) { if (showLoading &&
((type == "nodejs" && nodeJsList.isEmpty) ||
(type == "python3" && python3List.isEmpty) ||
(type == "linux" && linuxList.isEmpty))) {
loading(notify: true); loading(notify: true);
} }

View File

@@ -128,8 +128,7 @@ class _LoginLogPageState extends ConsumerState<LoginLogPage>
} }
Future<void> loadData() async { Future<void> loadData() async {
HttpResponse<List<LoginLogBean>> response = HttpResponse<List<LoginLogBean>> response = await Api.loginLog();
await Api.loginLog();
if (response.success) { if (response.success) {
if (response.bean == null || response.bean!.isEmpty) { if (response.bean == null || response.bean!.isEmpty) {

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qinglong_app/base/routes.dart'; import 'package:qinglong_app/base/routes.dart';
import 'package:qinglong_app/base/theme.dart'; import 'package:qinglong_app/base/theme.dart';
@@ -26,8 +26,15 @@ class _OtherPageState extends ConsumerState<OtherPage> {
children: [ children: [
Container( Container(
margin: const EdgeInsets.symmetric( margin: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 15, vertical: 15,
), ),
decoration: BoxDecoration(
color: ref.watch(themeProvider).themeColor.settingBordorColor(),
borderRadius: const BorderRadius.all(
Radius.circular(15),
),
),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -51,7 +58,10 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text( Text(
"脚本管理", "脚本管理",
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -84,7 +94,10 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text( Text(
"依赖管理", "依赖管理",
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -119,7 +132,10 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text( Text(
"任务日志", "任务日志",
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -159,7 +175,10 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text( Text(
"登录日志", "登录日志",
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -200,7 +219,10 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text( Text(
"修改密码", "修改密码",
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -213,13 +235,103 @@ class _OtherPageState extends ConsumerState<OtherPage> {
), ),
), ),
), ),
const Divider(
indent: 15,
),
GestureDetector(
onTap: () {
Navigator.of(context).pushNamed(
Routes.routerSubscription,
);
},
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.only(
top: 5,
bottom: 10,
left: 15,
right: 15,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"订阅管理",
style: TextStyle(
color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16,
),
),
const Spacer(),
const Icon(
CupertinoIcons.right_chevron,
size: 16,
),
],
),
),
),
const Divider(
indent: 15,
),
GestureDetector(
onTap: () {
Navigator.of(context).pushNamed(
Routes.routerBackup,
);
},
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.only(
top: 5,
bottom: 10,
left: 15,
right: 15,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"数据备份",
style: TextStyle(
color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16,
),
),
const Spacer(),
const Icon(
CupertinoIcons.right_chevron,
size: 16,
),
],
),
),
),
const Divider(
indent: 15,
),
], ],
), ),
), ),
Container( Container(
margin: const EdgeInsets.symmetric( margin: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 15, vertical: 15,
), ),
decoration: BoxDecoration(
color: ref.watch(themeProvider).themeColor.settingBordorColor(),
borderRadius: const BorderRadius.all(
Radius.circular(15),
),
),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -237,7 +349,8 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text( Text(
"夜间模式", "夜间模式",
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(), color:
ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -251,7 +364,6 @@ class _OtherPageState extends ConsumerState<OtherPage> {
], ],
), ),
), ),
const Divider( const Divider(
indent: 15, indent: 15,
), ),
@@ -274,7 +386,10 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text( Text(
"主题设置", "主题设置",
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -309,7 +424,10 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text( Text(
"切换账号", "切换账号",
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -346,7 +464,10 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text( Text(
"关于", "关于",
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -405,7 +526,8 @@ class _OtherPageState extends ConsumerState<OtherPage> {
), ),
onPressed: () { onPressed: () {
getIt<UserInfoViewModel>().updateToken(""); getIt<UserInfoViewModel>().updateToken("");
Navigator.of(context).pushReplacementNamed(Routes.routeLogin); Navigator.of(context)
.pushReplacementNamed(Routes.routeLogin);
}, },
), ),
], ],

View File

@@ -0,0 +1,154 @@
import 'package:code_text_field/code_text_field.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:highlight/languages/javascript.dart';
import 'package:highlight/languages/json.dart';
import 'package:highlight/languages/powershell.dart';
import 'package:highlight/languages/python.dart';
import 'package:highlight/languages/vbscript-html.dart';
import 'package:highlight/languages/yaml.dart';
import 'package:qinglong_app/base/ql_app_bar.dart';
import 'package:qinglong_app/base/sp_const.dart';
import 'package:qinglong_app/base/theme.dart';
import 'package:qinglong_app/utils/sp_utils.dart';
/// @author NewTab
class ScriptCodeDetailPage extends ConsumerStatefulWidget {
final String title;
final String content;
const ScriptCodeDetailPage({
Key? key,
required this.title,
required this.content,
}) : super(key: key);
@override
ScriptCodeDetailPageState createState() => ScriptCodeDetailPageState();
}
class ScriptCodeDetailPageState extends ConsumerState<ScriptCodeDetailPage> {
CodeController? _codeController;
GlobalKey<CodeFieldState> codeFieldKey = GlobalKey();
bool buttonshow = false;
void scrollToTop() {
codeFieldKey.currentState?.getCodeScroll()?.animateTo(0,
duration: const Duration(milliseconds: 200), curve: Curves.linear);
}
void floatingButtonVisibility() {
double y = codeFieldKey.currentState?.getCodeScroll()?.offset ?? 0;
if (y > MediaQuery.of(context).size.height / 2) {
if (buttonshow == true) return;
setState(() {
buttonshow = true;
});
} else {
if (buttonshow == false) return;
setState(() {
buttonshow = false;
});
}
}
String suffix = "\n\n\n";
@override
void dispose() {
_codeController?.dispose();
_codeController = null;
super.dispose();
}
getLanguageType(String title) {
if (title.endsWith(".js")) {
return javascript;
}
if (title.endsWith(".sh")) {
return powershell;
}
if (title.endsWith(".py")) {
return python;
}
if (title.endsWith(".json")) {
return json;
}
if (title.endsWith(".yaml")) {
return yaml;
}
return vbscriptHtml;
}
late String content;
@override
void initState() {
content = widget.content;
super.initState();
}
@override
Widget build(BuildContext context) {
_codeController ??= CodeController(
text: (content) + suffix,
language: getLanguageType(widget.title),
onChange: (value) {
content = value + suffix;
},
theme: ref.watch(themeProvider).themeColor.codeEditorTheme(),
stringMap: {
"export": const TextStyle(
fontWeight: FontWeight.normal, color: Color(0xff6B2375)),
},
);
return Scaffold(
floatingActionButton: Visibility(
visible: buttonshow,
child: FloatingActionButton(
mini: true,
onPressed: () {
scrollToTop();
},
elevation: 2,
backgroundColor: Colors.white,
child: const Icon(CupertinoIcons.up_arrow),
),
),
appBar: QlAppBar(
canBack: true,
backCall: () {
Navigator.of(context).pop();
},
title: widget.title,
),
body: SafeArea(
top: false,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: SpUtil.getBool(spShowLine, defValue: false) ? 0 : 10,
),
child: CodeField(
key: codeFieldKey,
controller: _codeController!,
expands: true,
readOnly: true,
wrap: SpUtil.getBool(spShowLine, defValue: false) ? false : true,
hideColumn: !SpUtil.getBool(spShowLine, defValue: false),
lineNumberStyle: LineNumberStyle(
textStyle: TextStyle(
color: ref.watch(themeProvider).themeColor.descColor(),
fontSize: 12,
),
),
background: Colors.white,
),
),
),
);
}
}

View File

@@ -1,20 +1,22 @@
import 'dart:ui'; import 'package:code_text_field/code_text_field.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:highlight/languages/javascript.dart';
import 'package:highlight/languages/json.dart';
import 'package:highlight/languages/powershell.dart';
import 'package:highlight/languages/python.dart';
import 'package:highlight/languages/vbscript-html.dart';
import 'package:highlight/languages/yaml.dart';
import 'package:qinglong_app/base/http/api.dart'; import 'package:qinglong_app/base/http/api.dart';
import 'package:qinglong_app/base/http/http.dart'; import 'package:qinglong_app/base/http/http.dart';
import 'package:qinglong_app/base/ql_app_bar.dart'; import 'package:qinglong_app/base/ql_app_bar.dart';
import 'package:qinglong_app/base/routes.dart'; import 'package:qinglong_app/base/routes.dart';
import 'package:qinglong_app/base/sp_const.dart';
import 'package:qinglong_app/base/theme.dart'; import 'package:qinglong_app/base/theme.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:qinglong_app/base/ui/lazy_load_state.dart'; import 'package:qinglong_app/base/ui/lazy_load_state.dart';
import 'package:qinglong_app/utils/extension.dart'; import 'package:qinglong_app/utils/extension.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:qinglong_app/utils/sp_utils.dart';
import '../../../base/ui/syntax_highlighter.dart';
import '../../config/config_page.dart';
/// @author NewTab /// @author NewTab
class ScriptDetailPage extends ConsumerStatefulWidget { class ScriptDetailPage extends ConsumerStatefulWidget {
@@ -34,8 +36,61 @@ class ScriptDetailPage extends ConsumerStatefulWidget {
class _ScriptDetailPageState extends ConsumerState<ScriptDetailPage> class _ScriptDetailPageState extends ConsumerState<ScriptDetailPage>
with LazyLoadState<ScriptDetailPage> { with LazyLoadState<ScriptDetailPage> {
String? content; String? content;
CodeController? _codeController;
GlobalKey<CodeFieldState> codeFieldKey = GlobalKey();
List<Widget> actions = []; List<Widget> actions = [];
bool buttonshow = false;
void scrollToTop() {
codeFieldKey.currentState?.getCodeScroll()?.animateTo(0,
duration: const Duration(milliseconds: 200), curve: Curves.linear);
}
void floatingButtonVisibility() {
double y = codeFieldKey.currentState?.getCodeScroll()?.offset ?? 0;
if (y > MediaQuery.of(context).size.height / 2) {
if (buttonshow == true) return;
setState(() {
buttonshow = true;
});
} else {
if (buttonshow == false) return;
setState(() {
buttonshow = false;
});
}
}
String suffix = "\n\n\n";
@override
void dispose() {
_codeController?.dispose();
_codeController = null;
super.dispose();
}
getLanguageType(String title) {
if (title.endsWith(".js")) {
return javascript;
}
if (title.endsWith(".sh")) {
return powershell;
}
if (title.endsWith(".py")) {
return python;
}
if (title.endsWith(".json")) {
return json;
}
if (title.endsWith(".yaml")) {
return yaml;
}
return vbscriptHtml;
}
@override @override
void initState() { void initState() {
@@ -85,6 +140,7 @@ class _ScriptDetailPageState extends ConsumerState<ScriptDetailPage>
Navigator.of(context).pop(); Navigator.of(context).pop();
showCupertinoDialog( showCupertinoDialog(
useRootNavigator: false,
context: context, context: context,
builder: (context) => CupertinoAlertDialog( builder: (context) => CupertinoAlertDialog(
title: const Text("确认删除"), title: const Text("确认删除"),
@@ -147,18 +203,45 @@ class _ScriptDetailPageState extends ConsumerState<ScriptDetailPage>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (content != null) {
_codeController ??= CodeController(
text: (content ?? "") + suffix,
language: getLanguageType(widget.title),
onChange: (value) {
content = value + suffix;
},
theme: ref.watch(themeProvider).themeColor.codeEditorTheme(),
stringMap: {
"export": const TextStyle(
fontWeight: FontWeight.normal, color: Color(0xff6B2375)),
},
);
}
return Scaffold( return Scaffold(
floatingActionButton: Visibility(
visible: buttonshow,
child: FloatingActionButton(
mini: true,
onPressed: () {
scrollToTop();
},
elevation: 2,
backgroundColor: Colors.white,
child: const Icon(CupertinoIcons.up_arrow),
),
),
appBar: QlAppBar( appBar: QlAppBar(
canBack: true, canBack: true,
backCall: () { backCall: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
title: "脚本详情", title: widget.title,
actions: [ actions: [
InkWell( InkWell(
onTap: () { onTap: () {
showCupertinoModalPopup( showCupertinoModalPopup(
context: context, context: context,
useRootNavigator: false,
builder: (context) { builder: (context) {
return CupertinoActionSheet( return CupertinoActionSheet(
title: Container( title: Container(
@@ -219,8 +302,31 @@ class _ScriptDetailPageState extends ConsumerState<ScriptDetailPage>
? const Center( ? const Center(
child: CupertinoActivityIndicator(), child: CupertinoActivityIndicator(),
) )
: CodeWidget( : SafeArea(
content: content ?? "", top: false,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal:
SpUtil.getBool(spShowLine, defValue: false) ? 0 : 10,
),
child: CodeField(
key: codeFieldKey,
controller: _codeController!,
expands: true,
readOnly: true,
wrap: SpUtil.getBool(spShowLine, defValue: false)
? false
: true,
hideColumn: !SpUtil.getBool(spShowLine, defValue: false),
lineNumberStyle: LineNumberStyle(
textStyle: TextStyle(
color: ref.watch(themeProvider).themeColor.descColor(),
fontSize: 12,
),
),
background: Colors.white,
),
),
), ),
); );
} }
@@ -234,32 +340,21 @@ class _ScriptDetailPageState extends ConsumerState<ScriptDetailPage>
if (response.success) { if (response.success) {
content = response.bean; content = response.bean;
setState(() {}); setState(() {});
Future.delayed(
const Duration(
seconds: 1,
),
() {
codeFieldKey.currentState
?.getCodeScroll()
?.addListener(floatingButtonVisibility);
},
);
} else { } else {
response.message?.toast(); response.message?.toast();
} }
} }
getLanguageType(String title) {
if (title.endsWith(".js")) {
return "js";
}
if (title.endsWith(".sh")) {
return "sh";
}
if (title.endsWith(".py")) {
return "py";
}
if (title.endsWith(".json")) {
return "json";
}
if (title.endsWith(".yaml")) {
return "yaml";
}
return "html";
}
@override @override
void onLazyLoad() { void onLazyLoad() {
loadData(); loadData();

View File

@@ -1,4 +1,5 @@
import 'package:code_text_field/code_text_field.dart'; import 'package:code_text_field/code_text_field.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:highlight/languages/javascript.dart'; import 'package:highlight/languages/javascript.dart';
@@ -10,15 +11,18 @@ import 'package:highlight/languages/yaml.dart';
import 'package:qinglong_app/base/http/api.dart'; import 'package:qinglong_app/base/http/api.dart';
import 'package:qinglong_app/base/http/http.dart'; import 'package:qinglong_app/base/http/http.dart';
import 'package:qinglong_app/base/ql_app_bar.dart'; import 'package:qinglong_app/base/ql_app_bar.dart';
import 'package:qinglong_app/base/sp_const.dart';
import 'package:qinglong_app/base/theme.dart'; import 'package:qinglong_app/base/theme.dart';
import 'package:qinglong_app/utils/extension.dart'; import 'package:qinglong_app/utils/extension.dart';
import 'package:qinglong_app/utils/sp_utils.dart';
class ScriptEditPage extends ConsumerStatefulWidget { class ScriptEditPage extends ConsumerStatefulWidget {
final String content; final String content;
final String title; final String title;
final String path; final String path;
const ScriptEditPage(this.title, this.path, this.content, {Key? key}) : super(key: key); const ScriptEditPage(this.title, this.path, this.content, {Key? key})
: super(key: key);
@override @override
_ScriptEditPageState createState() => _ScriptEditPageState(); _ScriptEditPageState createState() => _ScriptEditPageState();
@@ -28,6 +32,7 @@ class _ScriptEditPageState extends ConsumerState<ScriptEditPage> {
CodeController? _codeController; CodeController? _codeController;
late String result; late String result;
FocusNode focusNode = FocusNode(); FocusNode focusNode = FocusNode();
late String preResult;
@override @override
void dispose() { void dispose() {
@@ -38,10 +43,10 @@ class _ScriptEditPageState extends ConsumerState<ScriptEditPage> {
@override @override
void initState() { void initState() {
result = widget.content; result = widget.content;
preResult = widget.content;
super.initState(); super.initState();
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
focusNode.requestFocus(); focusNode.requestFocus();
}); });
} }
@@ -77,20 +82,60 @@ class _ScriptEditPageState extends ConsumerState<ScriptEditPage> {
}, },
theme: ref.watch(themeProvider).themeColor.codeEditorTheme(), theme: ref.watch(themeProvider).themeColor.codeEditorTheme(),
stringMap: { stringMap: {
"export": const TextStyle(fontWeight: FontWeight.normal, color: Color(0xff6B2375)), "export": const TextStyle(
fontWeight: FontWeight.normal, color: Color(0xff6B2375)),
}, },
); );
return Scaffold( return Scaffold(
appBar: QlAppBar( appBar: QlAppBar(
canBack: true, canBack: true,
backCall: () { backCall: () {
FocusManager.instance.primaryFocus?.unfocus();
if (preResult == result) {
Navigator.of(context).pop(); Navigator.of(context).pop();
} else {
showCupertinoDialog(
context: context,
useRootNavigator: false,
builder: (childContext) => CupertinoAlertDialog(
title: const Text("温馨提示"),
content: const Text("你编辑的内容还没用提交,确定退出吗?"),
actions: [
CupertinoDialogAction(
child: const Text(
"取消",
style: TextStyle(
color: Color(0xff999999),
),
),
onPressed: () {
Navigator.of(childContext).pop();
},
),
CupertinoDialogAction(
child: Text(
"确定",
style: TextStyle(
color: ref.watch(themeProvider).primaryColor,
),
),
onPressed: () {
Navigator.of(childContext).pop();
Navigator.of(context).pop();
},
),
],
),
);
}
}, },
title: '编辑${widget.title}', title: '编辑${widget.title}',
actions: [ actions: [
InkWell( InkWell(
onTap: () async { onTap: () async {
HttpResponse<NullResponse> response = await Api.updateScript(widget.title, widget.path, result); HttpResponse<NullResponse> response =
await Api.updateScript(widget.title, widget.path, result);
if (response.success) { if (response.success) {
"提交成功".toast(); "提交成功".toast();
Navigator.of(context).pop(true); Navigator.of(context).pop(true);
@@ -118,22 +163,21 @@ class _ScriptEditPageState extends ConsumerState<ScriptEditPage> {
body: SafeArea( body: SafeArea(
top: false, top: false,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: 10, horizontal: SpUtil.getBool(spShowLine, defValue: false) ? 0 : 10,
vertical: 10,
), ),
child: CodeField( child: CodeField(
controller: _codeController!, controller: _codeController!,
expands: true, expands: true,
wrap: true, background: Colors.white,
wrap: SpUtil.getBool(spShowLine, defValue: false) ? false : true,
hideColumn: !SpUtil.getBool(spShowLine, defValue: false),
lineNumberStyle: LineNumberStyle( lineNumberStyle: LineNumberStyle(
width: 0,
margin: 0,
textStyle: TextStyle( textStyle: TextStyle(
color: ref.watch(themeProvider).themeColor.descColor(), color: ref.watch(themeProvider).themeColor.descColor(),
fontSize: 12,
), ),
), ),
background: ref.watch(themeProvider).themeColor.tabBarColor(),
), ),
), ),
), ),

View File

@@ -11,6 +11,8 @@ import 'package:qinglong_app/base/ui/search_cell.dart';
import 'package:qinglong_app/module/others/scripts/script_bean.dart'; import 'package:qinglong_app/module/others/scripts/script_bean.dart';
import 'package:qinglong_app/utils/extension.dart'; import 'package:qinglong_app/utils/extension.dart';
import 'script_upload_page.dart';
/// @author NewTab /// @author NewTab
class ScriptPage extends ConsumerStatefulWidget { class ScriptPage extends ConsumerStatefulWidget {
const ScriptPage({Key? key}) : super(key: key); const ScriptPage({Key? key}) : super(key: key);
@@ -19,7 +21,8 @@ class ScriptPage extends ConsumerStatefulWidget {
_ScriptPageState createState() => _ScriptPageState(); _ScriptPageState createState() => _ScriptPageState();
} }
class _ScriptPageState extends ConsumerState<ScriptPage> with LazyLoadState<ScriptPage> { class _ScriptPageState extends ConsumerState<ScriptPage>
with LazyLoadState<ScriptPage> {
List<ScriptBean> list = []; List<ScriptBean> list = [];
final TextEditingController _searchController = TextEditingController(); final TextEditingController _searchController = TextEditingController();
@@ -67,7 +70,16 @@ class _ScriptPageState extends ConsumerState<ScriptPage> with LazyLoadState<Scri
? const Center( ? const Center(
child: CupertinoActivityIndicator(), child: CupertinoActivityIndicator(),
) )
: ListView.builder( : RefreshIndicator(
color: Theme.of(context).primaryColor,
onRefresh: () async {
await loadData();
return Future.value();
},
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
keyboardDismissBehavior:
ScrollViewKeyboardDismissBehavior.onDrag,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == 0) { if (index == 0) {
return searchCell(ref); return searchCell(ref);
@@ -79,19 +91,30 @@ class _ScriptPageState extends ConsumerState<ScriptPage> with LazyLoadState<Scri
(item.title?.contains(_searchController.text) ?? false) || (item.title?.contains(_searchController.text) ?? false) ||
(item.value?.contains(_searchController.text) ?? false) || (item.value?.contains(_searchController.text) ?? false) ||
((item.children?.where((e) { ((item.children?.where((e) {
return (e.title?.contains(_searchController.text) ?? false) || (e.value?.contains(_searchController.text) ?? false); return (e.title?.contains(_searchController.text) ??
false) ||
(e.value?.contains(_searchController.text) ??
false);
}).isNotEmpty ?? }).isNotEmpty ??
false))) { false))) {
return ColoredBox( return ColoredBox(
color: ref.watch(themeProvider).themeColor.settingBgColor(), color:
child: (item.children != null && item.children!.isNotEmpty) ref.watch(themeProvider).themeColor.settingBgColor(),
child:
(item.children != null && item.children!.isNotEmpty)
? ExpansionTile( ? ExpansionTile(
title: Text( title: Text(
item.title ?? "", item.title ?? "",
style: TextStyle( style: TextStyle(
color: (item.disabled ?? false) color: (item.disabled ?? false)
? ref.watch(themeProvider).themeColor.descColor() ? ref
: ref.watch(themeProvider).themeColor.titleColor(), .watch(themeProvider)
.themeColor
.descColor()
: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -100,8 +123,12 @@ class _ScriptPageState extends ConsumerState<ScriptPage> with LazyLoadState<Scri
if (_searchController.text.isEmpty) { if (_searchController.text.isEmpty) {
return true; return true;
} }
return (element.title?.contains(_searchController.text) ?? false) || return (element.title?.contains(
(element.value?.contains(_searchController.text) ?? false); _searchController.text) ??
false) ||
(element.value?.contains(
_searchController.text) ??
false);
}) })
.map((e) => ListTile( .map((e) => ListTile(
onTap: () { onTap: () {
@@ -112,7 +139,8 @@ class _ScriptPageState extends ConsumerState<ScriptPage> with LazyLoadState<Scri
"path": e.parent, "path": e.parent,
}, },
).then((value) { ).then((value) {
if (value != null && value == true) { if (value != null &&
value == true) {
loadData(); loadData();
} }
}); });
@@ -121,8 +149,14 @@ class _ScriptPageState extends ConsumerState<ScriptPage> with LazyLoadState<Scri
e.title ?? "", e.title ?? "",
style: TextStyle( style: TextStyle(
color: (item.disabled ?? false) color: (item.disabled ?? false)
? ref.watch(themeProvider).themeColor.descColor() ? ref
: ref.watch(themeProvider).themeColor.titleColor(), .watch(themeProvider)
.themeColor
.descColor()
: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 14, fontSize: 14,
), ),
), ),
@@ -149,8 +183,14 @@ class _ScriptPageState extends ConsumerState<ScriptPage> with LazyLoadState<Scri
item.title ?? "", item.title ?? "",
style: TextStyle( style: TextStyle(
color: (item.disabled ?? false) color: (item.disabled ?? false)
? ref.watch(themeProvider).themeColor.descColor() ? ref
: ref.watch(themeProvider).themeColor.titleColor(), .watch(themeProvider)
.themeColor
.descColor()
: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -162,6 +202,7 @@ class _ScriptPageState extends ConsumerState<ScriptPage> with LazyLoadState<Scri
}, },
itemCount: list.length + 1, itemCount: list.length + 1,
), ),
),
); );
} }
@@ -171,7 +212,9 @@ class _ScriptPageState extends ConsumerState<ScriptPage> with LazyLoadState<Scri
horizontal: 15, horizontal: 15,
vertical: 10, vertical: 10,
), ),
child:SearchCell(controller: _searchController,), child: SearchCell(
controller: _searchController,
),
); );
} }
@@ -198,135 +241,12 @@ class _ScriptPageState extends ConsumerState<ScriptPage> with LazyLoadState<Scri
String scriptPath = ""; String scriptPath = "";
void addScript() { void addScript() {
showCupertinoDialog( List<String?> paths = list
useRootNavigator: false,
context: context,
builder: (context) => CupertinoAlertDialog(
title: const Text("新增脚本"),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 10,
),
const Text(
"脚本名称:",
style: TextStyle(
fontSize: 14,
),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 10,
),
child: TextField(
controller: _nameController,
decoration: const InputDecoration(
isDense: true,
contentPadding: EdgeInsets.all(4),
hintText: "请输入脚本名称",
hintStyle: TextStyle(
fontSize: 14,
),
),
autofocus: false,
),
),
const SizedBox(
height: 10,
),
const Text(
"脚本所属文件夹:",
style: TextStyle(
fontSize: 14,
),
),
const SizedBox(
height: 10,
),
DropdownButtonFormField<String>(
items: list
.where((element) => element.children?.isNotEmpty ?? false) .where((element) => element.children?.isNotEmpty ?? false)
.map((e) => DropdownMenuItem( .map((e) => e.title)
value: e.value, .toList();
child: SizedBox(
width: MediaQuery.of(context).size.width / 2, Navigator.of(context).push(MaterialPageRoute(
child: Text( builder: (context) => ScriptUploadPage(paths: paths)));
e.value ?? "",
maxLines: 2,
),
),
))
.toList()
..insert(
0,
DropdownMenuItem(
value: "",
child: SizedBox(
width: MediaQuery.of(context).size.width / 2,
child: const Text(
"根目录",
maxLines: 2,
),
),
)),
value: scriptPath,
onChanged: (value) {
scriptPath = value ?? "";
},
),
],
),
actions: [
CupertinoDialogAction(
child: const Text(
"取消",
style: TextStyle(
color: Color(0xff999999),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
CupertinoDialogAction(
child: Text(
"确定",
style: TextStyle(
color: ref.watch(themeProvider).primaryColor,
),
),
onPressed: () async {
"提交中...".toast();
HttpResponse<NullResponse> response = await Api.addScript(
_nameController.text,
scriptPath,
"## created by 青龙客户端 ${DateTime.now()}\n\n",
);
if (response.success) {
"提交成功".toast();
Navigator.of(context).pop();
Navigator.of(context).pushNamed(
Routes.routeScriptUpdate,
arguments: {
"title": _nameController.text,
"path": scriptPath,
"content": "## created by 青龙客户端 ${DateTime.now()}\n\n",
},
).then((value) {
if (value != null && value == true) {
_nameController.text = "";
loadData();
}
});
} else {
(response.message ?? "").toast();
}
},
),
],
),
);
} }
} }

View File

@@ -0,0 +1,425 @@
import 'dart:io';
import 'dart:math';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qinglong_app/base/http/api.dart';
import 'package:qinglong_app/base/http/http.dart';
import 'package:qinglong_app/base/ql_app_bar.dart';
import 'package:qinglong_app/base/routes.dart';
import 'package:qinglong_app/base/theme.dart';
import 'package:path/path.dart';
import 'package:qinglong_app/module/others/scripts/script_code_detail_page.dart';
import 'package:qinglong_app/module/task/task_bean.dart';
import 'package:qinglong_app/utils/extension.dart';
/// @author NewTab
class ScriptUploadPage extends ConsumerStatefulWidget {
final List<String?> paths;
const ScriptUploadPage({
Key? key,
required this.paths,
}) : super(key: key);
@override
ConsumerState<ScriptUploadPage> createState() => ScriptUploadPageState();
}
class ScriptUploadPageState extends ConsumerState<ScriptUploadPage> {
List<String> list = [];
final TextEditingController _nameController = TextEditingController();
String scriptPath = "";
File? file;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: QlAppBar(
title: "新增脚本",
actions: [
InkWell(
onTap: () {
submit(context);
},
child: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 15,
),
child: Center(
child: Text(
"提交",
style: TextStyle(
color: Colors.white,
fontSize: 16,
),
),
),
),
)
],
),
body: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 30,
),
const TitleWidget(
"脚本名称",
),
TextField(
controller: _nameController,
decoration: InputDecoration(
contentPadding: const EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "请输入脚本名称",
hintStyle: TextStyle(
fontSize: 14,
color: ref.watch(themeProvider).themeColor.descColor(),
),
),
autofocus: false,
),
const SizedBox(
height: 30,
),
const TitleWidget(
"脚本目录",
),
const SizedBox(
height: 10,
),
DropdownButtonFormField<String>(
items: widget.paths
.map((e) => DropdownMenuItem(
value: e,
child: SizedBox(
width: MediaQuery.of(context).size.width / 2,
child: Text(
e ?? "",
maxLines: 2,
),
),
))
.toList()
..insert(
0,
DropdownMenuItem(
value: "",
child: SizedBox(
width: MediaQuery.of(context).size.width / 2,
child: const Text(
"根目录",
maxLines: 2,
),
),
),
),
style: TextStyle(
fontSize: 14,
color: ref.watch(themeProvider).themeColor.titleColor(),
),
decoration: const InputDecoration(
isDense: true,
contentPadding: EdgeInsets.symmetric(
vertical: 15,
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xfff5f5f5),
),
),
border: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xfff5f5f5),
),
),
),
value: scriptPath,
onChanged: (value) {
scriptPath = value ?? "";
},
),
const SizedBox(
height: 30,
),
const TitleWidget(
"上传脚本",
),
const SizedBox(
height: 10,
),
Container(
height: 80,
alignment: Alignment.centerLeft,
child: file == null ? addWidget() : addedWidget(context),
),
],
),
),
const SizedBox(
height: 50,
),
],
),
),
);
}
Widget addWidget() {
return GestureDetector(
onTap: () async {
FilePickerResult? result = await FilePicker.platform.pickFiles();
if (result != null &&
result.files.isNotEmpty &&
result.files.single.path != null) {
file = File(result.files.single.path!);
if (file == null) return;
if (file!.lengthSync() > 5242880) {
file = null;
"最大支持上传5M的文件".toast();
return;
}
_nameController.text = getFileName();
setState(() {});
}
},
child: Container(
margin: const EdgeInsets.only(
top: 10,
),
width: 70,
height: 70,
decoration: BoxDecoration(
color: const Color(0xfff3f5f7),
borderRadius: BorderRadius.circular(5),
),
child: Center(
child: Image.asset(
"assets/images/icon_add_file.png",
width: 50,
fit: BoxFit.cover,
),
),
),
);
}
Widget addedWidget(BuildContext context) {
return GestureDetector(
onTap: () async {
try {
String content = await file!.readAsString();
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ScriptCodeDetailPage(
title: getFileName(),
content: content,
),
),
);
} catch (e) {
e.toString().toast();
}
},
behavior: HitTestBehavior.opaque,
child: Container(
height: 80,
padding: const EdgeInsets.symmetric(
vertical: 10,
),
child: Row(
children: [
Image.asset(
getIconBySuffix(),
width: 50,
fit: BoxFit.cover,
),
const SizedBox(
width: 15,
),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
getFileName(),
style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 16,
),
),
const SizedBox(
height: 5,
),
Text(
getFileSize(file!.path, 1),
style: TextStyle(
color: ref.watch(themeProvider).themeColor.descColor(),
fontSize: 12,
),
),
],
),
),
const Spacer(),
GestureDetector(
onTap: () {
file = null;
_nameController.text = "";
setState(() {});
},
child: Icon(
CupertinoIcons.clear,
size: 20,
color: ref.watch(themeProvider).themeColor.descColor(),
),
),
],
),
),
);
}
String getFileSize(String filepath, int decimals) {
var file = File(filepath);
int bytes = file.lengthSync();
if (bytes <= 0) return "0 B";
const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
var i = (log(bytes) / log(1024)).floor();
return '${(bytes / pow(1024, i)).toStringAsFixed(decimals)} ${suffixes[i]}';
}
String getFileName() {
return basename(file!.path);
}
String getIconBySuffix() {
String end = file!.path;
if (end.endsWith(".py")) {
return "assets/images/py.png";
}
if (end.endsWith(".js")) {
return "assets/images/js.png";
}
if (end.endsWith(".ts")) {
return "assets/images/ts.png";
}
if (end.endsWith(".json")) {
return "assets/images/json.png";
}
if (end.endsWith(".sh")) {
return "assets/images/shell.png";
}
return "assets/images/other.png";
}
void submit(BuildContext context) async {
try {
if (_nameController.text.isEmpty) {
"请输入文件名称".toast();
return;
}
if (file == null) {
Navigator.of(context).pushNamed(
Routes.routeScriptAdd,
arguments: {
"title": _nameController.text,
"path": scriptPath,
},
).then((value) {
if (value != null && value == true) {
Navigator.of(context).pop(true);
}
});
} else {
String content = await file!.readAsString();
HttpResponse<NullResponse> response = await Api.addScript(
_nameController.text,
scriptPath,
content,
);
if (response.success) {
"提交成功".toast();
String command =
"task $scriptPath${(scriptPath.isNotEmpty) ? separator : ""}${getFileName()} ";
String? cron = getCronString(content, getFileName());
Navigator.of(context).popAndPushNamed(
Routes.routeAddTask,
arguments: TaskBean(
name: _nameController.text,
command: command,
schedule: cron,
),
);
} else {
(response.message ?? "").toast();
}
}
} catch (e) {
e.toString().toast();
}
}
static String? getCronString(String pre, String fileName) {
String reg =
"([\\d\\*]*[\\*-\\/,\\d]*[\\d\\*] ){4,5}[\\d\\*]*[\\*-\\/,\\d]*[\\d\\*]( |,|\").*$fileName";
RegExp regExp = RegExp(reg);
RegExpMatch? result = regExp.firstMatch(pre);
return result?[0]?.replaceAll(fileName, "").trim();
}
}
class TitleWidget extends ConsumerWidget {
final String title;
final bool required;
const TitleWidget(
this.title, {
Key? key,
this.required = false,
}) : super(key: key);
@override
Widget build(BuildContext context, ref) {
return RichText(
text: TextSpan(
text: !required ? "" : "* ",
style: const TextStyle(
color: Color(0xFFF02D2D),
),
children: <TextSpan>[
TextSpan(
text: title,
style: TextStyle(
fontSize: 16,
color: ref.watch(themeProvider).themeColor.titleColor(),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,596 @@
import 'package:flutter/material.dart';
import 'package:qinglong_app/base/http/api.dart';
import 'package:qinglong_app/base/ql_app_bar.dart';
import 'package:qinglong_app/module/others/subscription/subscription_bean.dart';
import 'package:qinglong_app/utils/extension.dart';
class SubscriptionAddPage extends StatefulWidget {
final Subscription? bean;
const SubscriptionAddPage(this.bean, {Key? key}) : super(key: key);
@override
State<SubscriptionAddPage> createState() => _SubscriptionAddPageState();
}
class _SubscriptionAddPageState extends State<SubscriptionAddPage> {
late Subscription _bean;
late String? _scheduleType;
late String? _type;
late String? _intervalScheduleType;
late String? _intervalScheduleValue;
@override
void initState() {
super.initState();
if (widget.bean == null) {
_bean = Subscription();
_bean.scheduleType = "";
_type = "public-repo";
_scheduleType = "crontab";
_intervalScheduleType = "days";
} else {
_bean = widget.bean!;
_type = widget.bean?.type;
_scheduleType = widget.bean?.scheduleType;
}
}
final TextEditingController _nameController = TextEditingController();
final TextEditingController _urlController = TextEditingController();
final TextEditingController _branchController = TextEditingController();
final TextEditingController _intervalScheduleValueController = TextEditingController();
final TextEditingController _scheduleController = TextEditingController();
final TextEditingController _whiteListController = TextEditingController();
final TextEditingController _blackListController = TextEditingController();
final TextEditingController _dependenceController = TextEditingController();
final TextEditingController _extensionsController = TextEditingController();
final TextEditingController _taskBeforeController = TextEditingController();
final TextEditingController _taskAfterController = TextEditingController();
FocusNode focusNode = FocusNode();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: QlAppBar(
canBack: true,
backCall: () {
Navigator.of(context).pop();
},
title: _bean.name == null ? "新增订阅" : "编辑订阅",
actions: [
InkWell(
onTap: () {
submit(context);
},
child: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 15,
),
child: Center(
child: Text(
"提交",
style: TextStyle(
color: Colors.white,
fontSize: 16,
),
),
),
),
)
],
),
body: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 15,
),
const Text(
"名称:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
TextField(
focusNode: focusNode,
controller: _nameController,
decoration: const InputDecoration(
hintText: "请输入名称",
),
autofocus: false,
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 15,
),
const Text(
"类型:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Row(
children: [
Radio(
value: "public-repo",
groupValue: _type,
onChanged: (data) {
setState(() {
_type = data as String?;
});
}),
const Text("公开仓库"),
],
),
Row(
children: [
Radio(
value: "private-repo",
groupValue: _type,
onChanged: (data) {
setState(() {
_type = data as String?;
});
}),
const Text("私有仓库"),
],
),
Row(
children: [
Radio(
value: "file",
groupValue: _type,
onChanged: (data) {
setState(() {
_type = data as String?;
});
}),
const Text("单文件"),
],
),
],
)
],
),
),
const Divider(
height: 10,
thickness: 2,
indent: 15,
endIndent: 15,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 15,
),
const Text(
"链接:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
TextField(
controller: _urlController,
maxLines: 8,
minLines: 1,
decoration: const InputDecoration(
hintText: "请输入仓库链接",
),
autofocus: false,
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 15,
),
const Text(
"分支:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
TextField(
controller: _branchController,
decoration: const InputDecoration(
hintText: "请输入分支",
),
autofocus: false,
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 15,
),
const Text(
"定时类型:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Row(
children: [
Radio(
value: "crontab",
groupValue: _scheduleType,
onChanged: (data) {
setState(() {
_scheduleType = data as String?;
});
}),
const Text("crontab"),
],
),
Row(
children: [
Radio(
value: "interval",
groupValue: _scheduleType,
onChanged: (data) {
setState(() {
_scheduleType = data as String?;
});
}),
const Text("interval"),
],
),
],
)
],
),
),
const Divider(
height: 10,
thickness: 2,
indent: 15,
endIndent: 15,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 15,
),
const Text(
"定时规则:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
getScheduler(context)
],
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 15,
),
const Text(
"白名单:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
TextField(
controller: _whiteListController,
decoration: const InputDecoration(
hintText: "请输入白名单",
),
autofocus: false,
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 15,
),
const Text(
"黑名单:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
TextField(
controller: _blackListController,
decoration: const InputDecoration(
hintText: "请输入黑名单",
),
autofocus: false,
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 15,
),
const Text(
"依赖文件:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
TextField(
controller: _dependenceController,
decoration: const InputDecoration(
hintText: "请输入依赖文件",
),
autofocus: false,
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 15,
),
const Text(
"文件后缀:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
TextField(
controller: _extensionsController,
decoration: const InputDecoration(
hintText: "请输入文件后缀",
),
autofocus: false,
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 15,
),
const Text(
"执行前:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
TextField(
controller: _taskBeforeController,
decoration: const InputDecoration(
hintText: "执行前",
),
autofocus: false,
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 15,
),
const Text(
"执行后:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
TextField(
controller: _taskAfterController,
decoration: const InputDecoration(
hintText: "执行后",
),
autofocus: false,
),
],
),
),
],
),
),
);
}
Widget getScheduler(BuildContext context) {
if (_scheduleType == "crontab") {
return TextField(
controller: _scheduleController,
decoration: const InputDecoration(
hintText: "秒(可选) 分 时 天 月 周",
),
autofocus: false,
);
} else {
return Row(
children: [
const Text(""),
SizedBox(
width: 350,
child: TextField(
controller: _intervalScheduleValueController,
textAlign: TextAlign.center,
decoration: const InputDecoration(
hintText: "",
),
autofocus: false,
),
),
DropdownButton<String>(
underline: Container(height: 0),
value: _intervalScheduleType,
items: const [
DropdownMenuItem(child: Text(""),value: "hours",),
DropdownMenuItem(child: Text(""),value: "days",),
DropdownMenuItem(child: Text(""),value: "weeks",),
],
onChanged: (data) {setState(() {
_intervalScheduleType = data;
});})
],
);
}
}
void submit(BuildContext context) async {
if (_nameController.text.isEmpty){
"名称不能为空".toast();
return;
}else{
_bean.name = _nameController.value.text;
}
if (_urlController.text.isEmpty){
"链接不能为空".toast();
return;
}else{
_bean.url = _urlController.value.text;
}
_bean.type = _type;
_bean.branch = _branchController.value.text;
if (_scheduleType != null && _scheduleType == "crontab"){
if (_scheduleController.text.isEmpty){
"定时规则不能为空".toast();
return;
}else{
_bean.schedule = _scheduleController.value.text;
}
}else{
if (_intervalScheduleValueController.text.isEmpty){
"定时规则不能为空".toast();
return;
}else{
var inter = IntervalSchedule();
inter.type = _intervalScheduleType;
inter.value = int.tryParse(_intervalScheduleValueController.value.text);
_bean.intervalSchedule = inter;
}
}
_bean.alias = _bean.name;
_bean.scheduleType = _scheduleType;
_bean.whitelist = _whiteListController.value.text;
_bean.blacklist = _blackListController.value.text;
_bean.extensions = _extensionsController.value.text;
_bean.dependences = _dependenceController.value.text;
_bean.subBefore = _taskBeforeController.value.text;
_bean.subAfter = _taskAfterController.value.text;
var data = await Api.postSubscription(_bean);
if (!data.success){
data.message.toast();
}else{
"添加成功".toast();
Navigator.of(context).pop();
}
}
}

View File

@@ -0,0 +1,310 @@
class Subscription {
int? _id;
String? _name;
String? _url;
dynamic _schedule;
IntervalSchedule? _intervalSchedule;
String? _type;
String? _whitelist;
String? _blacklist;
int? _status;
String? _dependences;
dynamic _extensions;
dynamic _subBefore;
dynamic _subAfter;
String? _branch;
dynamic _pullType;
dynamic _pullOption;
int? _pid;
int? _isDisabled;
String? _logPath;
String? _scheduleType;
String? _alias;
String? _createdAt;
String? _updatedAt;
Subscription(
{int? id,
String? name,
String? url,
dynamic schedule,
IntervalSchedule? intervalSchedule,
String? type,
String? whitelist,
String? blacklist,
int? status,
String? dependences,
dynamic extensions,
dynamic subBefore,
dynamic subAfter,
String? branch,
dynamic pullType,
dynamic pullOption,
int? pid,
int? isDisabled,
String? logPath,
String? scheduleType,
String? alias,
String? createdAt,
String? updatedAt}) {
if (id != null) {
this._id = id;
}
if (name != null) {
this._name = name;
}
if (url != null) {
this._url = url;
}
if (schedule != null) {
this._schedule = schedule;
}
if (intervalSchedule != null) {
this._intervalSchedule = intervalSchedule;
}
if (type != null) {
this._type = type;
}
if (whitelist != null) {
this._whitelist = whitelist;
}
if (blacklist != null) {
this._blacklist = blacklist;
}
if (status != null) {
this._status = status;
}
if (dependences != null) {
this._dependences = dependences;
}
if (extensions != null) {
this._extensions = extensions;
}
if (subBefore != null) {
this._subBefore = subBefore;
}
if (subAfter != null) {
this._subAfter = subAfter;
}
if (branch != null) {
this._branch = branch;
}
if (pullType != null) {
this._pullType = pullType;
}
if (pullOption != null) {
this._pullOption = pullOption;
}
if (pid != null) {
this._pid = pid;
}
if (isDisabled != null) {
this._isDisabled = isDisabled;
}
if (logPath != null) {
this._logPath = logPath;
}
if (scheduleType != null) {
this._scheduleType = scheduleType;
}
if (alias != null) {
this._alias = alias;
}
if (createdAt != null) {
this._createdAt = createdAt;
}
if (updatedAt != null) {
this._updatedAt = updatedAt;
}
}
int? get id => _id;
set id(int? id) => _id = id;
String? get name => _name;
set name(String? name) => _name = name;
String? get url => _url;
set url(String? url) => _url = url;
dynamic get schedule => _schedule;
set schedule(dynamic schedule) => _schedule = schedule;
IntervalSchedule? get intervalSchedule => _intervalSchedule;
set intervalSchedule(IntervalSchedule? intervalSchedule) =>
_intervalSchedule = intervalSchedule;
String? get type => _type;
set type(String? type) => _type = type;
String? get whitelist => _whitelist;
set whitelist(String? whitelist) => _whitelist = whitelist;
String? get blacklist => _blacklist;
set blacklist(String? blacklist) => _blacklist = blacklist;
int? get status => _status;
set status(int? status) => _status = status;
String? get dependences => _dependences;
set dependences(String? dependences) => _dependences = dependences;
dynamic get extensions => _extensions;
set extensions(dynamic extensions) => _extensions = extensions;
dynamic get subBefore => _subBefore;
set subBefore(dynamic subBefore) => _subBefore = subBefore;
dynamic get subAfter => _subAfter;
set subAfter(dynamic subAfter) => _subAfter = subAfter;
String? get branch => _branch;
set branch(String? branch) => _branch = branch;
dynamic get pullType => _pullType;
set pullType(dynamic pullType) => _pullType = pullType;
dynamic get pullOption => _pullOption;
set pullOption(dynamic pullOption) => _pullOption = pullOption;
int? get pid => _pid;
set pid(int? pid) => _pid = pid;
int? get isDisabled => _isDisabled;
set isDisabled(dynamic isDisabled) => _isDisabled = isDisabled;
String? get logPath => _logPath;
set logPath(String? logPath) => _logPath = logPath;
String? get scheduleType => _scheduleType;
set scheduleType(String? scheduleType) => _scheduleType = scheduleType;
String? get alias => _alias;
set alias(String? alias) => _alias = alias;
String? get createdAt => _createdAt;
set createdAt(String? createdAt) => _createdAt = createdAt;
String? get updatedAt => _updatedAt;
set updatedAt(String? updatedAt) => _updatedAt = updatedAt;
Subscription.fromJson(Map<String, dynamic> json) {
_id = json['id'];
_name = json['name'];
_url = json['url'];
_schedule = json['schedule'];
_intervalSchedule = json['interval_schedule'] != null
? new IntervalSchedule.fromJson(json['interval_schedule'])
: null;
_type = json['type'];
_whitelist = json['whitelist'];
_blacklist = json['blacklist'];
_status = json['status'];
_dependences = json['dependences'];
_extensions = json['extensions'];
_subBefore = json['sub_before'];
_subAfter = json['sub_after'];
_branch = json['branch'];
_pullType = json['pull_type'];
_pullOption = json['pull_option'];
_pid = json['pid'];
_isDisabled = json['is_disabled'];
_logPath = json['log_path'];
_scheduleType = json['schedule_type'];
_alias = json['alias'];
_createdAt = json['createdAt'];
_updatedAt = json['updatedAt'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this._id;
data['name'] = this._name;
data['url'] = this._url;
data['schedule'] = this._schedule;
if (this._intervalSchedule != null) {
data['interval_schedule'] = this._intervalSchedule!.toJson();
}
data['type'] = this._type;
data['whitelist'] = this._whitelist;
data['blacklist'] = this._blacklist;
data['status'] = this._status;
data['dependences'] = this._dependences;
data['extensions'] = this._extensions;
data['sub_before'] = this._subBefore;
data['sub_after'] = this._subAfter;
data['branch'] = this._branch;
data['pull_type'] = this._pullType;
data['pull_option'] = this._pullOption;
data['pid'] = this._pid;
data['is_disabled'] = this._isDisabled;
data['log_path'] = this._logPath;
data['schedule_type'] = this._scheduleType;
data['alias'] = this._alias;
data['createdAt'] = this._createdAt;
data['updatedAt'] = this._updatedAt;
return data;
}
static Subscription jsonConversion(Map<String, dynamic> json) {
return Subscription.fromJson(json);
}
}
class IntervalSchedule {
String? _type;
int? _value;
IntervalSchedule({String? type, int? value}) {
if (type != null) {
this._type = type;
}
if (value != null) {
this._value = value;
}
}
String? get type => _type;
set type(String? type) => _type = type;
int? get value => _value;
set value(int? value) => _value = value;
IntervalSchedule.fromJson(Map<String, dynamic> json) {
_type = json['type'];
_value = json['value'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['type'] = this._type;
data['value'] = this._value;
return data;
}
}

View File

@@ -0,0 +1,338 @@
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:qinglong_app/base/ql_app_bar.dart';
import 'package:qinglong_app/module/others/subscription/subscription_bean.dart';
import 'package:qinglong_app/module/others/subscription/subscription_time_log_page.dart';
import '../../../base/http/api.dart';
import '../../../base/routes.dart';
import '../../../base/theme.dart';
import '../../task/task_viewmodel.dart';
class SubscriptionDetailPage extends StatefulWidget {
final Subscription bean;
final bool hideAppbar;
const SubscriptionDetailPage(this.bean, {Key? key, this.hideAppbar = false})
: super(key: key);
@override
State<SubscriptionDetailPage> createState() => _SubscriptionDetailPageState();
}
class _SubscriptionDetailPageState extends State<SubscriptionDetailPage> {
List<Widget> actions = [];
@override
void initState() {
super.initState();
//actions.clear();
actions.addAll(
[
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () async {
Navigator.of(context).pop();
if (widget.bean.status! == 1) {
await startCron(context);
} else {
await stopCron(context);
}
},
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 15,
),
alignment: Alignment.center,
child: Material(
color: Colors.transparent,
child: Text(
widget.bean.status! == 1 ? "运行" : "停止运行",
style: const TextStyle(
fontSize: 16,
),
),
),
),
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.of(context).pop();
showLog();
},
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 15,
),
alignment: Alignment.center,
child: const Material(
color: Colors.transparent,
child: Text(
"查看日志",
style: TextStyle(
fontSize: 16,
),
),
),
),
),
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: QlAppBar(
title: "订阅管理",
canBack: true,
actions: [
InkWell(
onTap: () {
showCupertinoModalPopup(
context: context,
builder: (context) {
return CupertinoActionSheet(
title: Container(
alignment: Alignment.center,
child: const Material(
color: Colors.transparent,
child: Text(
"更多操作",
style: TextStyle(
fontSize: 16,
),
),
),
),
actions: actions,
cancelButton: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.pop(context);
},
child: Container(
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(
vertical: 10,
),
child: const Material(
color: Colors.transparent,
child: Text(
"取消",
style: TextStyle(
color: Colors.red,
fontSize: 16,
),
),
),
),
),
);
});
},
child: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 15,
),
child: Center(
child: Icon(
Icons.more_horiz,
color: Colors.white,
size: 26,
),
),
),
)
],
),
body: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TaskDetailCell(
title: "名称",
desc: widget.bean.name,
),
TaskDetailCell(
title: "仓库类型",
desc: widget.bean.type == "public-repo"
? "公开仓库"
: widget.bean.type == "private-repo"
? "私有仓库"
: "单文件",
),
TaskDetailCell(
title: "链接",
desc: widget.bean.url ?? "",
),
TaskDetailCell(
title: "分支",
desc: widget.bean.branch ?? "",
),
TaskDetailCell(
title: "定时类型",
desc: widget.bean.scheduleType ?? "",
),
TaskDetailCell(
title: "定时规则",
desc: widget.bean.scheduleType == "crontab"
? widget.bean.schedule ?? ""
: widget.bean.intervalSchedule!.type == "days"
? "" + widget.bean.intervalSchedule!.value.toString() + ""
: widget.bean.intervalSchedule!.type == "hours"
? "" +
widget.bean.intervalSchedule!.value.toString() +
"小时"
: widget.bean.intervalSchedule!.type == "minutes"
? "" +
widget.bean.intervalSchedule!.value.toString() +
"分钟"
: "" +
widget.bean.intervalSchedule!.value.toString() +
"",
),
TaskDetailCell(
title: "白名单",
desc: widget.bean.whitelist ?? "",
),
TaskDetailCell(
title: "黑名单",
desc: widget.bean.blacklist ?? "",
),
TaskDetailCell(
title: "依赖文件",
desc: widget.bean.dependences ?? "",
),
TaskDetailCell(
title: "文件后缀",
desc: widget.bean.extensions ?? "",
),
TaskDetailCell(
title: "执行前",
desc: widget.bean.subAfter ?? "",
),
TaskDetailCell(
title: "执行后",
desc: widget.bean.subBefore ?? "",
),
],
),
);
}
startCron(BuildContext context) async {
await Api.startSubscription([widget.bean.id!]);
setState(() {});
showLog();
}
stopCron(BuildContext context) async {
await Api.stopSubscription([widget.bean.id!]);
setState(() {});
}
void showLog() {
showCupertinoModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context) =>
SubscriptionTimeLogPage(
widget.bean.id!,
true,
widget.bean.name ?? "",
),
);
}
}
class TaskDetailCell extends ConsumerWidget {
final String title;
final String? desc;
final Widget? icon;
final bool hideDivide;
final Function? taped;
const TaskDetailCell({
Key? key,
required this.title,
this.desc,
this.icon,
this.hideDivide = false,
this.taped,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(
top: 10,
left: 15,
right: 10,
bottom: 10,
),
child: Row(
children: [
Text(
title,
style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 16,
),
),
const SizedBox(
width: 30,
),
desc != null
? Expanded(
child: Align(
alignment: Alignment.centerRight,
child: SelectableText(
desc!,
textAlign: TextAlign.right,
selectionHeightStyle: BoxHeightStyle.max,
selectionWidthStyle: BoxWidthStyle.max,
onTap: () {
if (taped != null) {
taped!();
}
},
style: TextStyle(
color:
ref.watch(themeProvider).themeColor.descColor(),
fontSize: 14,
),
),
),
)
: Expanded(
child:
Align(alignment: Alignment.centerRight, child: icon!),
),
],
),
),
hideDivide
? const SizedBox.shrink()
: const Divider(
indent: 15,
),
],
);
}
}

View File

@@ -0,0 +1,351 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:highlight/languages/routeros.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:qinglong_app/base/http/http.dart';
import 'package:qinglong_app/base/ql_app_bar.dart';
import 'package:qinglong_app/module/others/subscription/subscription_bean.dart';
import 'package:qinglong_app/module/others/subscription/subscription_time_log_page.dart';
import 'package:qinglong_app/utils/extension.dart';
import '../../../base/http/api.dart';
import '../../../base/routes.dart';
import '../../../base/theme.dart';
import '../../../base/ui/empty_widget.dart';
import '../../../base/ui/lazy_load_state.dart';
class SubscriptionPage extends ConsumerStatefulWidget {
const SubscriptionPage({Key? key}) : super(key: key);
@override
_ScriptPageState createState() => _ScriptPageState();
}
class _ScriptPageState extends ConsumerState<SubscriptionPage>
with LazyLoadState<SubscriptionPage> {
List<Subscription> list = [];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: QlAppBar(
title: "订阅管理",
canBack: true,
actions: [
InkWell(
onTap: () {
Navigator.of(context).pushNamed(
Routes.routerSubscriptionAdd,
);
},
child: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 15,
),
child: Center(
child: Icon(
CupertinoIcons.add,
size: 24,
color: Colors.white,
),
),
),
),
],
),
body: RefreshIndicator(
color: Theme.of(context).primaryColor,
onRefresh: () async {
return loadData();
},
child: list.isEmpty
? const EmptyWidget()
: ListView.builder(
keyboardDismissBehavior:
ScrollViewKeyboardDismissBehavior.onDrag,
itemBuilder: (context, index) {
Subscription item = list[index];
return SubscriptionCell(list[index], index, ref);
},
itemCount: list.length,
),
));
}
Future<void> loadData() async {
HttpResponse<List<Subscription>> response = await Api.getSubscription();
if (response.success) {
if (response.bean == null || response.bean!.isEmpty) {
"暂无数据".toast();
}
list.clear();
list.addAll(response.bean ?? []);
setState(() {});
} else {
response.message?.toast();
}
}
@override
void onLazyLoad() {
loadData();
}
}
class SubscriptionCell extends StatelessWidget {
final Subscription bean;
final int index;
final WidgetRef ref;
const SubscriptionCell(this.bean, this.index, this.ref, {Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
return ColoredBox(
color: ref.watch(themeProvider).themeColor.settingBgColor(),
child: Slidable(
key: ValueKey(bean.id),
endActionPane: ActionPane(
motion: const StretchMotion(),
extentRatio: 0.7,
children: [
SlidableAction(
backgroundColor: const Color(0xff5D5E70),
onPressed: (_) {
WidgetsBinding.instance.endOfFrame.then((timeStamp) {
Navigator.of(context).pushNamed(
Routes.routerSubscriptionAdd,
arguments: bean);
});
},
foregroundColor: Colors.white,
icon: CupertinoIcons.pencil_outline,
),
SlidableAction(
backgroundColor: const Color(0xffA356D6),
onPressed: (_) {
changeSubscriptionEnable();
},
foregroundColor: Colors.white,
icon: bean.isDisabled == null || bean.isDisabled == 0
? Icons.dnd_forwardslash
: Icons.check_circle_outline_sharp,
),
SlidableAction(
backgroundColor: const Color(0xffEA4D3E),
onPressed: (_) {
Api.deleteSubscription([bean.id!]);
},
foregroundColor: Colors.white,
icon: CupertinoIcons.delete,
),
],
),
startActionPane: ActionPane(
motion: const StretchMotion(),
extentRatio: 0.4,
children: [
SlidableAction(
backgroundColor: const Color(0xffD25535),
onPressed: (_) {
changeSubscriptionStatus();
},
foregroundColor: Colors.white,
icon: bean.status! == 1
? CupertinoIcons.memories
: CupertinoIcons.stop_circle,
),
SlidableAction(
backgroundColor: const Color(0xff606467),
onPressed: (_) {
Future.delayed(
const Duration(
milliseconds: 250,
), () {
showLog(context);
});
},
foregroundColor: Colors.white,
icon: CupertinoIcons.text_justifyleft,
),
],
),
child: InkWell(
onTap: (){
Navigator.of(context).pushNamed(
Routes.routerSubscriptionDetail,
arguments: bean);
},
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: MediaQuery.of(context).size.width,
color: Colors.transparent,
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
children: [
bean.status == 1
? const SizedBox.shrink()
: SizedBox(
width: 15,
height: 15,
child: CircularProgressIndicator(
strokeWidth: 2,
color: ref
.watch(themeProvider)
.primaryColor,
),
),
SizedBox(
width: bean.status == 1 ? 0 : 5,
),
Expanded(
child: Material(
color: Colors.transparent,
child: Text(
bean.name ?? "",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 18,
),
),
),
),
],
),
),
Material(
color: Colors.transparent,
child: Text(
bean.branch!,
maxLines: 1,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color: ref
.watch(themeProvider)
.themeColor
.descColor(),
fontSize: 12,
),
),
),
],
),
const SizedBox(
height: 8,
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
bean.isDisabled == 1
? const Icon(
Icons.dnd_forwardslash,
size: 12,
color: Colors.red,
)
: const SizedBox.shrink(),
const SizedBox(
width: 5,
),
Material(
color: Colors.transparent,
child: Text(
bean.schedule ?? "",
maxLines: 1,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color: ref
.watch(themeProvider)
.themeColor
.descColor(),
fontSize: 12,
),
),
),
],
),
const SizedBox(
height: 8,
),
Material(
color: Colors.transparent,
child: Text(
bean.url ?? "",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color:
ref.watch(themeProvider).themeColor.descColor(),
fontSize: 12,
),
),
),
],
),
),
const Divider(
height: 1,
indent: 15,
),
],
),
)),
);
}
void showLog(BuildContext context) {
showCupertinoModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context) => SubscriptionTimeLogPage(
bean.id!,
true,
bean.name ?? "",
),
);
}
Future<void> changeSubscriptionStatus() async {
if (bean.status == null || bean.status == 0) {
await Api.stopSubscription([bean.id!]);
} else {
await Api.startSubscription([bean.id!]);
}
}
Future<void> changeSubscriptionEnable() async {
if (bean.isDisabled == null || bean.isDisabled == 0) {
await Api.disableSubscription([bean.id!]);
} else {
await Api.enableSubscription([bean.id!]);
}
}
}

View File

@@ -0,0 +1,181 @@
import 'dart:async';
import 'dart:math';
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:qinglong_app/base/http/api.dart';
import 'package:qinglong_app/base/http/http.dart';
import 'package:qinglong_app/module/others/subscription/subscription_page.dart';
import 'package:qinglong_app/module/others/subscription/subscription_page.dart';
import 'package:qinglong_app/module/others/subscription/subscription_page.dart';
import 'package:qinglong_app/module/others/subscription/subscription_page.dart';
import 'package:qinglong_app/module/others/subscription/subscription_page.dart';
import 'package:qinglong_app/module/others/subscription/subscription_page.dart';
import 'package:qinglong_app/module/others/subscription/subscription_page.dart';
import 'package:share_plus/share_plus.dart';
import '../../../base/ui/lazy_load_state.dart';
class SubscriptionTimeLogPage extends StatefulWidget {
final int subId;
final bool needTimer;
final String title;
const SubscriptionTimeLogPage(this.subId, this.needTimer, this.title, {Key? key})
: super(key: key);
@override
_SubscriptionTimeLogPageState createState() => _SubscriptionTimeLogPageState();
}
class _SubscriptionTimeLogPageState extends State<SubscriptionTimeLogPage>
with LazyLoadState<SubscriptionTimeLogPage> {
Timer? _timer;
String? content;
@override
void initState() {
super.initState();
}
bool isRequest = false;
bool canRequest = true;
getLogData() async {
if (!canRequest) return;
if (isRequest) return;
isRequest = true;
HttpResponse<String> response = await Api.subTimeLog(widget.subId);
if (response.success) {
content = response.bean;
setState(() {});
}
isRequest = false;
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Material(
child: SafeArea(
child: Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(
vertical: 10,
),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Row(
children: [
const SizedBox(
width: 15,
),
InkWell(
onTap: () {
Navigator.of(context).pop();
},
child: const Icon(
CupertinoIcons.chevron_down,
size: 18,
),
),
Expanded(
child: Center(
child: Text(
widget.title,
style: const TextStyle(
fontSize: 16,
),
),
),
),
InkWell(
onTap: () {
Share.share(content ?? "");
},
child: const Icon(
CupertinoIcons.share,
size: 16,
),
),
const SizedBox(
width: 15,
),
],
),
const SizedBox(
height: 15,
),
if (content == null)
const Expanded(
child: Center(
child: CupertinoActivityIndicator(),
),
)
else
Expanded(
child: SingleChildScrollView(
primary: true,
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(
content!,
scrollPhysics: const NeverScrollableScrollPhysics(),
onSelectionChanged: (TextSelection selection,
SelectionChangedCause? cause) {
final int newStart = min(
selection.baseOffset, selection.extentOffset);
final int newEnd = max(
selection.baseOffset, selection.extentOffset);
if (newEnd == newStart) {
canRequest = true;
} else {
canRequest = false;
}
},
selectionHeightStyle: BoxHeightStyle.max,
selectionWidthStyle: BoxWidthStyle.max,
style: const TextStyle(
fontSize: 12,
),
),
const SizedBox(
height: 400,
),
],
),
),
)
],
),
),
),
);
}
@override
void onLazyLoad() {
if (widget.needTimer) {
_timer = Timer.periodic(
const Duration(seconds: 2),
(timer) {
getLogData();
},
);
} else {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
getLogData();
});
}
}
}

View File

@@ -24,7 +24,8 @@ class TaskLogDetailPage extends ConsumerStatefulWidget {
_TaskLogDetailPageState createState() => _TaskLogDetailPageState(); _TaskLogDetailPageState createState() => _TaskLogDetailPageState();
} }
class _TaskLogDetailPageState extends ConsumerState<TaskLogDetailPage> with LazyLoadState<TaskLogDetailPage> { class _TaskLogDetailPageState extends ConsumerState<TaskLogDetailPage>
with LazyLoadState<TaskLogDetailPage> {
String? content; String? content;
@override @override

View File

@@ -19,7 +19,8 @@ class TaskLogPage extends ConsumerStatefulWidget {
_TaskLogPageState createState() => _TaskLogPageState(); _TaskLogPageState createState() => _TaskLogPageState();
} }
class _TaskLogPageState extends ConsumerState<TaskLogPage> with LazyLoadState<TaskLogPage> { class _TaskLogPageState extends ConsumerState<TaskLogPage>
with LazyLoadState<TaskLogPage> {
List<TaskLogBean> list = []; List<TaskLogBean> list = [];
final TextEditingController _searchController = TextEditingController(); final TextEditingController _searchController = TextEditingController();
@@ -63,7 +64,8 @@ class _TaskLogPageState extends ConsumerState<TaskLogPage> with LazyLoadState<Ta
return searchCell(ref); return searchCell(ref);
} }
TaskLogBean item = list[index - 1]; TaskLogBean item = list[index - 1];
if (_searchController.text.isNotEmpty && !(item.name?.contains(_searchController.text) ?? false)) { if (_searchController.text.isNotEmpty &&
!(item.name?.contains(_searchController.text) ?? false)) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return ColoredBox( return ColoredBox(
@@ -73,7 +75,10 @@ class _TaskLogPageState extends ConsumerState<TaskLogPage> with LazyLoadState<Ta
title: Text( title: Text(
item.name ?? "", item.name ?? "",
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -81,7 +86,9 @@ class _TaskLogPageState extends ConsumerState<TaskLogPage> with LazyLoadState<Ta
? item.files! ? item.files!
.map((e) => ListTile( .map((e) => ListTile(
onTap: () { onTap: () {
Navigator.of(context).pushNamed(Routes.routeTaskLogDetail, arguments: { Navigator.of(context).pushNamed(
Routes.routeTaskLogDetail,
arguments: {
"path": item.name, "path": item.name,
"title": e, "title": e,
}); });
@@ -89,7 +96,10 @@ class _TaskLogPageState extends ConsumerState<TaskLogPage> with LazyLoadState<Ta
title: Text( title: Text(
e, e,
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 14, fontSize: 14,
), ),
), ),
@@ -98,7 +108,9 @@ class _TaskLogPageState extends ConsumerState<TaskLogPage> with LazyLoadState<Ta
: (item.children ?? []) : (item.children ?? [])
.map((e) => ListTile( .map((e) => ListTile(
onTap: () { onTap: () {
Navigator.of(context).pushNamed(Routes.routeTaskLogDetail, arguments: { Navigator.of(context).pushNamed(
Routes.routeTaskLogDetail,
arguments: {
"path": item.name, "path": item.name,
"title": e.title, "title": e.title,
}); });
@@ -106,7 +118,10 @@ class _TaskLogPageState extends ConsumerState<TaskLogPage> with LazyLoadState<Ta
title: Text( title: Text(
e.title ?? "", e.title ?? "",
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 14, fontSize: 14,
), ),
), ),
@@ -119,7 +134,9 @@ class _TaskLogPageState extends ConsumerState<TaskLogPage> with LazyLoadState<Ta
"该文件夹为空".toast(); "该文件夹为空".toast();
return; return;
} }
Navigator.of(context).pushNamed(Routes.routeTaskLogDetail, arguments: { Navigator.of(context).pushNamed(
Routes.routeTaskLogDetail,
arguments: {
"path": "", "path": "",
"title": item.name, "title": item.name,
}); });
@@ -127,7 +144,10 @@ class _TaskLogPageState extends ConsumerState<TaskLogPage> with LazyLoadState<Ta
title: Text( title: Text(
item.name ?? "", item.name ?? "",
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),

View File

@@ -122,7 +122,8 @@ class _ThemePageState extends ConsumerState<ThemePage> {
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment:
MainAxisAlignment.start,
crossAxisAlignment: crossAxisAlignment:
CrossAxisAlignment.start, CrossAxisAlignment.start,
children: [ children: [
@@ -144,7 +145,8 @@ class _ThemePageState extends ConsumerState<ThemePage> {
child: child:
CircularProgressIndicator( CircularProgressIndicator(
strokeWidth: 2, strokeWidth: 2,
color: _primaryColor, color:
_primaryColor,
), ),
), ),
SizedBox( SizedBox(
@@ -158,8 +160,8 @@ class _ThemePageState extends ConsumerState<ThemePage> {
child: Text( child: Text(
"示例名称", "示例名称",
maxLines: 1, maxLines: 1,
overflow: overflow: TextOverflow
TextOverflow.ellipsis, .ellipsis,
style: TextStyle( style: TextStyle(
overflow: TextOverflow overflow: TextOverflow
.ellipsis, .ellipsis,
@@ -182,7 +184,8 @@ class _ThemePageState extends ConsumerState<ThemePage> {
"上午1000", "上午1000",
maxLines: 1, maxLines: 1,
style: TextStyle( style: TextStyle(
overflow: TextOverflow.ellipsis, overflow:
TextOverflow.ellipsis,
color: ref color: ref
.watch(themeProvider) .watch(themeProvider)
.themeColor .themeColor
@@ -216,7 +219,8 @@ class _ThemePageState extends ConsumerState<ThemePage> {
"10 1-12/2 * * *", "10 1-12/2 * * *",
maxLines: 1, maxLines: 1,
style: TextStyle( style: TextStyle(
overflow: TextOverflow.ellipsis, overflow:
TextOverflow.ellipsis,
color: ref color: ref
.watch(themeProvider) .watch(themeProvider)
.themeColor .themeColor

View File

@@ -31,7 +31,7 @@ class _UpdatePasswordPageState extends ConsumerState<UpdatePasswordPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance?.addPostFrameCallback( WidgetsBinding.instance.addPostFrameCallback(
(timeStamp) { (timeStamp) {
focusNode.requestFocus(); focusNode.requestFocus();
}, },

View File

@@ -36,7 +36,7 @@ class _AddTaskPageState extends ConsumerState<AddTaskPage> {
} else { } else {
taskBean = TaskBean(); taskBean = TaskBean();
} }
WidgetsBinding.instance?.addPostFrameCallback( WidgetsBinding.instance.addPostFrameCallback(
(timeStamp) { (timeStamp) {
focusNode.requestFocus(); focusNode.requestFocus();
}, },
@@ -207,8 +207,11 @@ class _AddTaskPageState extends ConsumerState<AddTaskPage> {
taskBean.command = _commandController.text; taskBean.command = _commandController.text;
taskBean.schedule = _cronController.text; taskBean.schedule = _cronController.text;
HttpResponse<NullResponse> response = await Api.addTask( HttpResponse<NullResponse> response = await Api.addTask(
_nameController.text, _commandController.text, _cronController.text, _nameController.text,
id: taskBean.id,); _commandController.text,
_cronController.text,
id: taskBean.id,
);
if (response.success) { if (response.success) {
(widget.taskBean?.sId == null) ? "新增成功" : "修改成功".toast(); (widget.taskBean?.sId == null) ? "新增成功" : "修改成功".toast();

View File

@@ -1,22 +1,28 @@
import 'dart:async'; import 'dart:async';
import 'dart:math';
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:qinglong_app/base/http/api.dart'; import 'package:qinglong_app/base/http/api.dart';
import 'package:qinglong_app/base/http/http.dart'; import 'package:qinglong_app/base/http/http.dart';
import 'package:qinglong_app/base/ui/lazy_load_state.dart';
import 'package:share_plus/share_plus.dart';
class InTimeLogPage extends StatefulWidget { class InTimeLogPage extends StatefulWidget {
final String cronId; final String cronId;
final bool needTimer; final bool needTimer;
final String title;
const InTimeLogPage(this.cronId, this.needTimer, {Key? key}) : super(key: key); const InTimeLogPage(this.cronId, this.needTimer, this.title, {Key? key})
: super(key: key);
@override @override
_InTimeLogPageState createState() => _InTimeLogPageState(); _InTimeLogPageState createState() => _InTimeLogPageState();
} }
class _InTimeLogPageState extends State<InTimeLogPage> { class _InTimeLogPageState extends State<InTimeLogPage>
with LazyLoadState<InTimeLogPage> {
Timer? _timer; Timer? _timer;
String? content; String? content;
@@ -24,12 +30,6 @@ class _InTimeLogPageState extends State<InTimeLogPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_timer = Timer.periodic(
const Duration(seconds: 2),
(timer) {
getLogData();
},
);
} }
bool isRequest = false; bool isRequest = false;
@@ -40,7 +40,6 @@ class _InTimeLogPageState extends State<InTimeLogPage> {
if (isRequest) return; if (isRequest) return;
isRequest = true; isRequest = true;
HttpResponse<String> response = await Api.inTimeLog(widget.cronId); HttpResponse<String> response = await Api.inTimeLog(widget.cronId);
if (response.success) { if (response.success) {
content = response.bean; content = response.bean;
setState(() {}); setState(() {});
@@ -56,22 +55,119 @@ class _InTimeLogPageState extends State<InTimeLogPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Material(
child: SafeArea(
child: Container(
color: Colors.white,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 10, vertical: 10,
), ),
child: content == null child: Column(
? const Center( mainAxisSize: MainAxisSize.max,
children: [
Row(
children: [
const SizedBox(
width: 15,
),
InkWell(
onTap: () {
Navigator.of(context).pop();
},
child: const Icon(
CupertinoIcons.chevron_down,
size: 18,
),
),
Expanded(
child: Center(
child: Text(
widget.title,
style: const TextStyle(
fontSize: 16,
),
),
),
),
InkWell(
onTap: () {
Share.share(content ?? "");
},
child: const Icon(
CupertinoIcons.share,
size: 16,
),
),
const SizedBox(
width: 15,
),
],
),
const SizedBox(
height: 15,
),
if (content == null)
const Expanded(
child: Center(
child: CupertinoActivityIndicator(), child: CupertinoActivityIndicator(),
),
) )
: CupertinoScrollbar( else
child: SelectableText( Expanded(
child: SingleChildScrollView(
primary: true,
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(
content!, content!,
scrollPhysics: const NeverScrollableScrollPhysics(),
onSelectionChanged: (TextSelection selection,
SelectionChangedCause? cause) {
final int newStart = min(
selection.baseOffset, selection.extentOffset);
final int newEnd = max(
selection.baseOffset, selection.extentOffset);
if (newEnd == newStart) {
canRequest = true;
} else {
canRequest = false;
}
},
selectionHeightStyle: BoxHeightStyle.max, selectionHeightStyle: BoxHeightStyle.max,
selectionWidthStyle: BoxWidthStyle.max, selectionWidthStyle: BoxWidthStyle.max,
style: const TextStyle(
fontSize: 12,
),
),
const SizedBox(
height: 400,
),
],
),
),
)
],
),
), ),
), ),
); );
} }
@override
void onLazyLoad() {
if (widget.needTimer) {
_timer = Timer.periodic(
const Duration(seconds: 2),
(timer) {
getLogData();
},
);
} else {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
getLogData();
});
}
}
} }

View File

@@ -51,7 +51,9 @@ class TaskBean {
saved = json['saved']; saved = json['saved'];
id = json['id']; id = json['id'];
_id = json['_id']; _id = json['_id'];
sId = json.containsKey('_id') ? json['_id'].toString() : (json.containsKey('id') ? json['id'].toString() : ""); sId = json.containsKey('_id')
? json['_id'].toString()
: (json.containsKey('id') ? json['id'].toString() : "");
created = int.tryParse(json['created'].toString()); created = int.tryParse(json['created'].toString());
status = json['status']; status = json['status'];
timestamp = json['timestamp'].toString(); timestamp = json['timestamp'].toString();

View File

@@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:qinglong_app/base/ql_app_bar.dart'; import 'package:qinglong_app/base/ql_app_bar.dart';
import 'package:qinglong_app/base/routes.dart'; import 'package:qinglong_app/base/routes.dart';
import 'package:qinglong_app/base/theme.dart'; import 'package:qinglong_app/base/theme.dart';
@@ -16,7 +17,8 @@ class TaskDetailPage extends ConsumerStatefulWidget {
final TaskBean taskBean; final TaskBean taskBean;
final bool hideAppbar; final bool hideAppbar;
const TaskDetailPage(this.taskBean, {Key? key, this.hideAppbar = false}) : super(key: key); const TaskDetailPage(this.taskBean, {Key? key, this.hideAppbar = false})
: super(key: key);
@override @override
_TaskDetailPageState createState() => _TaskDetailPageState(); _TaskDetailPageState createState() => _TaskDetailPageState();
@@ -70,11 +72,13 @@ class _TaskDetailPageState extends ConsumerState<TaskDetailPage> {
), ),
TaskDetailCell( TaskDetailCell(
title: "创建时间", title: "创建时间",
desc: Utils.formatMessageTime(widget.taskBean.created ?? 0), desc:
Utils.formatMessageTime(widget.taskBean.created ?? 0),
), ),
TaskDetailCell( TaskDetailCell(
title: "更新时间", title: "更新时间",
desc: Utils.formatGMTTime(widget.taskBean.timestamp ?? ""), desc:
Utils.formatGMTTime(widget.taskBean.timestamp ?? ""),
), ),
TaskDetailCell( TaskDetailCell(
title: "任务定时", title: "任务定时",
@@ -82,11 +86,14 @@ class _TaskDetailPageState extends ConsumerState<TaskDetailPage> {
), ),
TaskDetailCell( TaskDetailCell(
title: "最后运行时间", title: "最后运行时间",
desc: Utils.formatMessageTime(widget.taskBean.lastExecutionTime ?? 0), desc: Utils.formatMessageTime(
widget.taskBean.lastExecutionTime ?? 0),
), ),
TaskDetailCell( TaskDetailCell(
title: "最后运行时长", title: "最后运行时长",
desc: widget.taskBean.lastRunningTime == null ? "-" : "${widget.taskBean.lastRunningTime ?? "-"}", desc: widget.taskBean.lastRunningTime == null
? "-"
: "${widget.taskBean.lastRunningTime ?? "-"}",
), ),
GestureDetector( GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
@@ -204,7 +211,8 @@ class _TaskDetailPageState extends ConsumerState<TaskDetailPage> {
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
Navigator.of(context).pushNamed(Routes.routeAddTask, arguments: widget.taskBean); Navigator.of(context)
.pushNamed(Routes.routeAddTask, arguments: widget.taskBean);
}, },
child: Container( child: Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@@ -345,7 +353,7 @@ class _TaskDetailPageState extends ConsumerState<TaskDetailPage> {
startCron(BuildContext context, WidgetRef ref) async { startCron(BuildContext context, WidgetRef ref) async {
await ref.read(taskProvider).runCrons(widget.taskBean.sId!); await ref.read(taskProvider).runCrons(widget.taskBean.sId!);
setState(() {}); setState(() {});
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
showLog(); showLog();
}); });
} }
@@ -356,12 +364,16 @@ class _TaskDetailPageState extends ConsumerState<TaskDetailPage> {
} }
void enableTask() async { void enableTask() async {
await ref.read(taskProvider).enableTask(widget.taskBean.sId!, widget.taskBean.isDisabled!); await ref
.read(taskProvider)
.enableTask(widget.taskBean.sId!, widget.taskBean.isDisabled!);
setState(() {}); setState(() {});
} }
void pinTask() async { void pinTask() async {
await ref.read(taskProvider).pinTask(widget.taskBean.sId!, widget.taskBean.isPinned!); await ref
.read(taskProvider)
.pinTask(widget.taskBean.sId!, widget.taskBean.isPinned!);
setState(() {}); setState(() {});
} }
@@ -402,30 +414,16 @@ class _TaskDetailPageState extends ConsumerState<TaskDetailPage> {
} }
void showLog() { void showLog() {
showCupertinoDialog( showCupertinoModalBottomSheet(
builder: (BuildContext context) { expand: true,
return CupertinoAlertDialog( context: context,
title: Text( backgroundColor: Colors.transparent,
"${widget.taskBean.name}运行日志", builder: (context) => InTimeLogPage(
maxLines: 1, widget.taskBean.sId!,
style: const TextStyle(overflow: TextOverflow.ellipsis), true,
widget.taskBean.name ?? "",
), ),
content: InTimeLogPage(widget.taskBean.sId!, widget.taskBean.status == 0),
actions: [
CupertinoDialogAction(
child: Text(
"知道了",
style: TextStyle(color: Theme.of(context).primaryColor),
),
onPressed: () {
Navigator.of(context).pop();
ref.read(taskProvider).loadData(false);
},
),
],
); );
},
context: context);
} }
} }
@@ -485,14 +483,16 @@ class TaskDetailCell extends ConsumerWidget {
} }
}, },
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.descColor(), color:
ref.watch(themeProvider).themeColor.descColor(),
fontSize: 14, fontSize: 14,
), ),
), ),
), ),
) )
: Expanded( : Expanded(
child: Align(alignment: Alignment.centerRight, child: icon!), child:
Align(alignment: Alignment.centerRight, child: icon!),
), ),
], ],
), ),

View File

@@ -4,6 +4,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:qinglong_app/base/base_state_widget.dart'; import 'package:qinglong_app/base/base_state_widget.dart';
import 'package:qinglong_app/base/routes.dart'; import 'package:qinglong_app/base/routes.dart';
import 'package:qinglong_app/base/theme.dart'; import 'package:qinglong_app/base/theme.dart';
@@ -66,9 +67,17 @@ class _TaskPageState extends ConsumerState<TaskPage> {
} }
TaskBean item = list[index - 1]; TaskBean item = list[index - 1];
if (_searchController.text.isEmpty || if (_searchController.text.isEmpty ||
(item.name?.toLowerCase().contains(_searchController.text.toLowerCase()) ?? false) || (item.name
(item.command?.toLowerCase().contains(_searchController.text.toLowerCase()) ?? false) || ?.toLowerCase()
(item.schedule?.contains(_searchController.text.toLowerCase()) ?? false)) { .contains(_searchController.text.toLowerCase()) ??
false) ||
(item.command
?.toLowerCase()
.contains(_searchController.text.toLowerCase()) ??
false) ||
(item.schedule
?.contains(_searchController.text.toLowerCase()) ??
false)) {
return TaskItemCell(item, ref); return TaskItemCell(item, ref);
} else { } else {
return const SizedBox.shrink(); return const SizedBox.shrink();
@@ -79,9 +88,17 @@ class _TaskPageState extends ConsumerState<TaskPage> {
if (index == 0) return const SizedBox.shrink(); if (index == 0) return const SizedBox.shrink();
TaskBean item = list[index - 1]; TaskBean item = list[index - 1];
if (_searchController.text.isEmpty || if (_searchController.text.isEmpty ||
(item.name?.toLowerCase().contains(_searchController.text.toLowerCase()) ?? false) || (item.name
(item.command?.toLowerCase().contains(_searchController.text.toLowerCase()) ?? false) || ?.toLowerCase()
(item.schedule?.contains(_searchController.text.toLowerCase()) ?? false)) { .contains(_searchController.text.toLowerCase()) ??
false) ||
(item.command
?.toLowerCase()
.contains(_searchController.text.toLowerCase()) ??
false) ||
(item.schedule
?.contains(_searchController.text.toLowerCase()) ??
false)) {
return Container( return Container(
color: ref.watch(themeProvider).themeColor.settingBgColor(), color: ref.watch(themeProvider).themeColor.settingBgColor(),
child: const Divider( child: const Divider(
@@ -129,7 +146,9 @@ class _TaskPageState extends ConsumerState<TaskPage> {
child: Text( child: Text(
TaskViewModel.allStr, TaskViewModel.allStr,
style: TextStyle( style: TextStyle(
color: currentState == TaskViewModel.allStr ? ref.watch(themeProvider).primaryColor : ref.watch(themeProvider).themeColor.titleColor(), color: currentState == TaskViewModel.allStr
? ref.watch(themeProvider).primaryColor
: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 14, fontSize: 14,
), ),
), ),
@@ -139,8 +158,9 @@ class _TaskPageState extends ConsumerState<TaskPage> {
child: Text( child: Text(
TaskViewModel.runningStr, TaskViewModel.runningStr,
style: TextStyle( style: TextStyle(
color: color: currentState == TaskViewModel.runningStr
currentState == TaskViewModel.runningStr ? ref.watch(themeProvider).primaryColor : ref.watch(themeProvider).themeColor.titleColor(), ? ref.watch(themeProvider).primaryColor
: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 14, fontSize: 14,
), ),
), ),
@@ -150,7 +170,9 @@ class _TaskPageState extends ConsumerState<TaskPage> {
child: Text( child: Text(
TaskViewModel.neverStr, TaskViewModel.neverStr,
style: TextStyle( style: TextStyle(
color: currentState == TaskViewModel.neverStr ? ref.watch(themeProvider).primaryColor : ref.watch(themeProvider).themeColor.titleColor(), color: currentState == TaskViewModel.neverStr
? ref.watch(themeProvider).primaryColor
: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 14, fontSize: 14,
), ),
), ),
@@ -164,7 +186,10 @@ class _TaskPageState extends ConsumerState<TaskPage> {
style: TextStyle( style: TextStyle(
color: currentState == TaskViewModel.notScriptStr color: currentState == TaskViewModel.notScriptStr
? ref.watch(themeProvider).primaryColor ? ref.watch(themeProvider).primaryColor
: ref.watch(themeProvider).themeColor.titleColor(), : ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 14, fontSize: 14,
), ),
), ),
@@ -177,8 +202,9 @@ class _TaskPageState extends ConsumerState<TaskPage> {
TaskViewModel.disableStr, TaskViewModel.disableStr,
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: color: currentState == TaskViewModel.disableStr
currentState == TaskViewModel.disableStr ? ref.watch(themeProvider).primaryColor : ref.watch(themeProvider).themeColor.titleColor(), ? ref.watch(themeProvider).primaryColor
: ref.watch(themeProvider).themeColor.titleColor(),
), ),
), ),
value: TaskViewModel.disableStr, value: TaskViewModel.disableStr,
@@ -235,7 +261,9 @@ class TaskItemCell extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ColoredBox( return ColoredBox(
color: bean.isPinned == 1 ? ref.watch(themeProvider).themeColor.pinColor() : ref.watch(themeProvider).themeColor.settingBgColor(), color: bean.isPinned == 1
? ref.watch(themeProvider).themeColor.pinColor()
: ref.watch(themeProvider).themeColor.settingBgColor(),
child: Slidable( child: Slidable(
key: ValueKey(bean.sId), key: ValueKey(bean.sId),
endActionPane: ActionPane( endActionPane: ActionPane(
@@ -245,8 +273,9 @@ class TaskItemCell extends StatelessWidget {
SlidableAction( SlidableAction(
backgroundColor: const Color(0xff5D5E70), backgroundColor: const Color(0xff5D5E70),
onPressed: (_) { onPressed: (_) {
WidgetsBinding.instance?.endOfFrame.then((timeStamp) { WidgetsBinding.instance.endOfFrame.then((timeStamp) {
Navigator.of(context).pushNamed(Routes.routeAddTask, arguments: bean); Navigator.of(context)
.pushNamed(Routes.routeAddTask, arguments: bean);
}); });
}, },
foregroundColor: Colors.white, foregroundColor: Colors.white,
@@ -258,7 +287,9 @@ class TaskItemCell extends StatelessWidget {
pinTask(context); pinTask(context);
}, },
foregroundColor: Colors.white, foregroundColor: Colors.white,
icon: (bean.isPinned ?? 0) == 0 ? CupertinoIcons.pin : CupertinoIcons.pin_slash, icon: (bean.isPinned ?? 0) == 0
? CupertinoIcons.pin
: CupertinoIcons.pin_slash,
), ),
SlidableAction( SlidableAction(
backgroundColor: const Color(0xffA356D6), backgroundColor: const Color(0xffA356D6),
@@ -266,7 +297,9 @@ class TaskItemCell extends StatelessWidget {
enableTask(context); enableTask(context);
}, },
foregroundColor: Colors.white, foregroundColor: Colors.white,
icon: bean.isDisabled! == 0 ? Icons.dnd_forwardslash : Icons.check_circle_outline_sharp, icon: bean.isDisabled! == 0
? Icons.dnd_forwardslash
: Icons.check_circle_outline_sharp,
), ),
SlidableAction( SlidableAction(
backgroundColor: const Color(0xffEA4D3E), backgroundColor: const Color(0xffEA4D3E),
@@ -298,7 +331,9 @@ class TaskItemCell extends StatelessWidget {
} }
}, },
foregroundColor: Colors.white, foregroundColor: Colors.white,
icon: bean.status! == 1 ? CupertinoIcons.memories : CupertinoIcons.stop_circle, icon: bean.status! == 1
? CupertinoIcons.memories
: CupertinoIcons.stop_circle,
), ),
SlidableAction( SlidableAction(
backgroundColor: const Color(0xff606467), backgroundColor: const Color(0xff606467),
@@ -316,10 +351,13 @@ class TaskItemCell extends StatelessWidget {
], ],
), ),
child: Material( child: Material(
color: bean.isPinned == 1 ? ref.watch(themeProvider).themeColor.pinColor() : ref.watch(themeProvider).themeColor.settingBgColor(), color: bean.isPinned == 1
? ref.watch(themeProvider).themeColor.pinColor()
: ref.watch(themeProvider).themeColor.settingBgColor(),
child: InkWell( child: InkWell(
onTap: () { onTap: () {
Navigator.of(context).pushNamed(Routes.routeTaskDetail, arguments: bean); Navigator.of(context)
.pushNamed(Routes.routeTaskDetail, arguments: bean);
}, },
child: Container( child: Container(
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
@@ -355,7 +393,10 @@ class TaskItemCell extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -391,11 +432,16 @@ class TaskItemCell extends StatelessWidget {
Material( Material(
color: Colors.transparent, color: Colors.transparent,
child: Text( child: Text(
(bean.lastExecutionTime == null || bean.lastExecutionTime == 0) ? "-" : Utils.formatMessageTime(bean.lastExecutionTime!), (bean.lastExecutionTime == null ||
bean.lastExecutionTime == 0)
? "-"
: Utils.formatMessageTime(
bean.lastExecutionTime!),
maxLines: 1, maxLines: 1,
style: TextStyle( style: TextStyle(
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
color: ref.watch(themeProvider).themeColor.descColor(), color:
ref.watch(themeProvider).themeColor.descColor(),
fontSize: 12, fontSize: 12,
), ),
), ),
@@ -454,31 +500,16 @@ class TaskItemCell extends StatelessWidget {
} }
logCron(BuildContext context, WidgetRef ref) { logCron(BuildContext context, WidgetRef ref) {
showCupertinoDialog( showCupertinoModalBottomSheet(
useRootNavigator: false, expand: true,
builder: (BuildContext context) { context: context,
return CupertinoAlertDialog( backgroundColor: Colors.transparent,
title: Text( builder: (context) => InTimeLogPage(
"${bean.name}运行日志", bean.sId!,
maxLines: 1, true,
style: const TextStyle(overflow: TextOverflow.ellipsis), bean.name ?? "",
), ),
content: InTimeLogPage(bean.sId!, bean.status == 0),
actions: [
CupertinoDialogAction(
child: Text(
"知道了",
style: TextStyle(color: Theme.of(context).primaryColor),
),
onPressed: () {
Navigator.of(context).pop();
ref.read(taskProvider).loadData(false);
},
),
],
); );
},
context: context);
} }
void enableTask(BuildContext context) { void enableTask(BuildContext context) {

View File

@@ -62,7 +62,8 @@ class TaskViewModel extends BaseViewModel {
} }
p.sort((TaskBean a, TaskBean b) { p.sort((TaskBean a, TaskBean b) {
bool c = DateTime.fromMillisecondsSinceEpoch(a.created ?? 0).isBefore(DateTime.fromMillisecondsSinceEpoch(b.created ?? 0)); bool c = DateTime.fromMillisecondsSinceEpoch(a.created ?? 0)
.isBefore(DateTime.fromMillisecondsSinceEpoch(b.created ?? 0));
if (c == true) { if (c == true) {
return 1; return 1;
} }
@@ -75,7 +76,8 @@ class TaskViewModel extends BaseViewModel {
p.sort((a, b) { p.sort((a, b) {
if (a.status == 0 && b.status == 0) { if (a.status == 0 && b.status == 0) {
bool c = DateTime.fromMillisecondsSinceEpoch(a.created ?? 0).isBefore(DateTime.fromMillisecondsSinceEpoch(b.created ?? 0)); bool c = DateTime.fromMillisecondsSinceEpoch(a.created ?? 0)
.isBefore(DateTime.fromMillisecondsSinceEpoch(b.created ?? 0));
if (c == true) { if (c == true) {
return 1; return 1;
} }
@@ -86,7 +88,8 @@ class TaskViewModel extends BaseViewModel {
}); });
r.sort((a, b) { r.sort((a, b) {
bool c = DateTime.fromMillisecondsSinceEpoch(a.created ?? 0).isBefore(DateTime.fromMillisecondsSinceEpoch(b.created ?? 0)); bool c = DateTime.fromMillisecondsSinceEpoch(a.created ?? 0)
.isBefore(DateTime.fromMillisecondsSinceEpoch(b.created ?? 0));
if (c == true) { if (c == true) {
return 1; return 1;
} }
@@ -94,7 +97,8 @@ class TaskViewModel extends BaseViewModel {
}); });
d.sort((a, b) { d.sort((a, b) {
bool c = DateTime.fromMillisecondsSinceEpoch(a.created ?? 0).isBefore(DateTime.fromMillisecondsSinceEpoch(b.created ?? 0)); bool c = DateTime.fromMillisecondsSinceEpoch(a.created ?? 0)
.isBefore(DateTime.fromMillisecondsSinceEpoch(b.created ?? 0));
if (c == true) { if (c == true) {
return 1; return 1;
} }
@@ -102,7 +106,8 @@ class TaskViewModel extends BaseViewModel {
}); });
list.sort((a, b) { list.sort((a, b) {
bool c = DateTime.fromMillisecondsSinceEpoch(a.created ?? 0).isBefore(DateTime.fromMillisecondsSinceEpoch(b.created ?? 0)); bool c = DateTime.fromMillisecondsSinceEpoch(a.created ?? 0)
.isBefore(DateTime.fromMillisecondsSinceEpoch(b.created ?? 0));
if (c == true) { if (c == true) {
return 1; return 1;
} }
@@ -116,9 +121,12 @@ class TaskViewModel extends BaseViewModel {
running.clear(); running.clear();
running.addAll(list.where((element) => element.status == 0)); running.addAll(list.where((element) => element.status == 0));
neverRunning.clear(); neverRunning.clear();
neverRunning.addAll(list.where((element) => element.lastRunningTime == null)); neverRunning
.addAll(list.where((element) => element.lastRunningTime == null));
notScripts.clear(); notScripts.clear();
notScripts.addAll(list.where((element) => (element.command != null && (element.command!.startsWith("ql repo") || element.command!.startsWith("ql raw"))))); notScripts.addAll(list.where((element) => (element.command != null &&
(element.command!.startsWith("ql repo") ||
element.command!.startsWith("ql raw")))));
disabled.clear(); disabled.clear();
disabled.addAll(list.where((element) => element.isDisabled == 1)); disabled.addAll(list.where((element) => element.isDisabled == 1));
} }

View File

@@ -9,6 +9,7 @@ class Utils {
static bool isUpperVersion() { static bool isUpperVersion() {
return systemBean.isUpperVersion(); return systemBean.isUpperVersion();
} }
static bool isUpperVersion2_12_2() { static bool isUpperVersion2_12_2() {
return systemBean.isUpperVersion2_12_2(); return systemBean.isUpperVersion2_12_2();
} }

74
pub/code_field-master/.gitignore vendored Normal file
View File

@@ -0,0 +1,74 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
build/
# Android related
**/android/**/gradle-wrapper.jar
**/android/.gradle
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
# iOS/XCode related
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Flutter.podspec
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/Flutter/flutter_export_environment.sh
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3

View File

@@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: c5a4b4029c0798f37c4a39b479d7cb75daa7b05c
channel: stable
project_type: package

View File

@@ -0,0 +1,46 @@
## [1.0.0-1] - 2021-03-06
* Initial release
## [1.0.0-4] - 2021-03-11
* Added horizontal scrolling support
* Added code modifiers
* Cleaner padding API
## [1.0.0-6] - 2021-03-12
* Added a temporary fix for https://github.com/flutter/flutter/issues/77929
## [1.0.0-7] - 2021-03-12
* Added a rawText getter to CodeController
## [1.0.0-9] - 2021-04-21
* Removed dependency on flutter_keyboard_visibility
## [1.0.1-0] - 2021-05-22
* TextEditingController.buildTextSpan breaking change migration for flutter 2.2.0
## [1.0.1-1] - 2021-06-04
* Added wrap paramerter to disable horizontal scrolling
## [1.0.1-2] - 2021-07-23
* Fixed highlight parsing on web (issue #11)
## [1.0.2] - 2021-07-23
* removeChar & removeSelection methods added
* added onChange callback
* added enabled flag
* fixed middle dot issue
## [1.0.3] - 2022-05-02
* added onTap to CodeField API
* fixed tab behavior in read-only mode
* added setCursor method to CodeController

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Bertrand Bevillard
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,242 @@
# CodeField
A customizable code text field supporting syntax highlighting
[![Pub](https://img.shields.io/pub/v/code_text_field.svg)](https://pub.dev/packages/code_text_field)
[![Website shields.io](https://img.shields.io/website-up-down-green-red/http/shields.io.svg)](https://bertrandbev.github.io/code_field/)
[![GitHub license](https://img.shields.io/github/license/Naereen/StrapDown.js.svg)](https://raw.githubusercontent.com/BertrandBev/code_field/master/LICENSE)
[![Awesome Flutter](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter)
<img src="https://raw.githubusercontent.com/BertrandBev/code_field/master/doc/images/top.gif" width="70%">
## Live demo
A [live demo](https://bertrandbev.github.io/code_field/#/) showcasing a few language / theme combinations
## Showcase
The experimental VM [dlox](https://github.com/BertrandBev/dlox) uses **CodeField** in its [online editor](https://bertrandbev.github.io/dlox/#/)
## Features
- Code highlight for 189 built-in languages with 90 themes thanks to [flutter_highlight](https://pub.dev/packages/flutter_highlight)
- Easy language highlight customization through the use of theme maps
- Fully customizable code field style through a TextField like API
- Handles horizontal/vertical scrolling and vertical expansion
- Supports code modifiers
- Works on Android, iOS, and Web
Code modifiers help manage indents automatically
<img src="https://raw.githubusercontent.com/BertrandBev/code_field/master/doc/images/typing.gif" width="70%">
The editor is wrapped in a horizontal scrollable container to handle long lines
<img src="https://raw.githubusercontent.com/BertrandBev/code_field/master/doc/images/long_line.gif" width="70%">
## Installing
In the `pubspec.yaml` of your flutter project, add the following dependency:
```yaml
dependencies:
...
code_text_field: <latest_version>
```
[latest version](https://pub.dev/packages/code_text_field/install)
In your library add the following import:
```dart
import 'package:code_text_field/code_field.dart';
```
## Simple example
A CodeField widget works with a **CodeController** which dynamically parses the text input according to a language and renders it with a theme map
```dart
import 'package:flutter/material.dart';
import 'package:code_text_field/code_field.dart';
// Import the language & theme
import 'package:highlight/languages/dart.dart';
import 'package:flutter_highlight/themes/monokai-sublime.dart';
class CodeEditor extends StatefulWidget {
@override
_CodeEditorState createState() => _CodeEditorState();
}
class _CodeEditorState extends State<CodeEditor> {
CodeController? _codeController;
@override
void initState() {
super.initState();
final source = "void main() {\n print(\"Hello, world!\");\n}";
// Instantiate the CodeController
_codeController = CodeController(
text: source,
language: dart,
theme: monokaiSublimeTheme,
);
}
@override
void dispose() {
_codeController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CodeField(
controller: _codeController!,
textStyle: TextStyle(fontFamily: 'SourceCode'),
);
}
}
```
<img src="https://raw.githubusercontent.com/BertrandBev/code_field/master/doc/images/example_0.png" width="60%">
Here, the monospace font [Source Code Pro](https://fonts.google.com/specimen/Source+Code+Pro?preview.text_type=custom) has been added to the assets folder and to the [pubspec.yaml](https://github.com/BertrandBev/code_field/blob/master/example/pubspec.yaml) file
## Parser options
On top of a language definition, world-wise styling can be specified in the **stringMap** field
```dart
_codeController = CodeController(
//...
stringMap: {
"Hello": TextStyle(fontWeight: FontWeight.bold, color: Colors.red),
"world": TextStyle(fontStyle: FontStyle.italic, color: Colors.green),
},
);
```
<img src="https://raw.githubusercontent.com/BertrandBev/code_field/master/doc/images/example_1.png" width="60%">
More complex regexes may also be used with the **patternMap**. When a language is used though, its regexes patterns take precedence over **patternMap** and **stringMap**.
```dart
_codeController = CodeController(
//...
patternMap: {
r"\B#[a-zA-Z0-9]+\b":
TextStyle(fontWeight: FontWeight.bold, color: Colors.purpleAccent),
},
);
```
<img src="https://raw.githubusercontent.com/BertrandBev/code_field/master/doc/images/example_2.png" width="60%">
Both **patternMap** and **stringMap** can be used without specifying a language
```dart
_codeController = CodeController(
text: source,
patternMap: {
r'".*"': TextStyle(color: Colors.yellow),
r'[a-zA-Z0-9]+\(.*\)': TextStyle(color: Colors.green),
},
stringMap: {
"void": TextStyle(fontWeight: FontWeight.bold, color: Colors.red),
"print": TextStyle(fontWeight: FontWeight.bold, color: Colors.blue),
},
);
```
<img src="https://raw.githubusercontent.com/BertrandBev/code_field/master/doc/images/example_3.png" width="60%">
## Code Modifiers
Code modifiers can be created to react to special keystrokes.
The default modifiers handle tab to space & automatic indentation. Here's the implementation of the default **TabModifier**
```dart
class TabModifier extends CodeModifier {
const TabModifier() : super('\t');
@override
TextEditingValue? updateString(
String text, TextSelection sel, EditorParams params) {
final tmp = replace(text, sel.start, sel.end, " " * params.tabSpaces);
return tmp;
}
}
```
## API
### CodeField
```dart
CodeField({
Key? key,
required this.controller,
this.minLines,
this.maxLines,
this.expands = false,
this.wrap = false,
this.background,
this.decoration,
this.textStyle,
this.padding = const EdgeInsets.symmetric(),
this.lineNumberStyle = const LineNumberStyle(),
this.enabled,
this.cursorColor,
this.textSelectionTheme,
this.lineNumberBuilder,
this.focusNode,
this.onTap,
})
```
```dart
LineNumberStyle({
this.width = 42.0,
this.textAlign = TextAlign.right,
this.margin = 10.0,
this.textStyle,
this.background,
})
```
### CodeController
```dart
CodeController({
String? text,
this.language,
this.theme,
this.patternMap,
this.stringMap,
this.params = const EditorParams(),
this.modifiers = const <CodeModifier>[
const IntendModifier(),
const CloseBlockModifier(),
const TabModifier(),
],
this.onChange,
})
```
## Limitations
- Autocomplete disabling on android [doesn't work yet](https://github.com/flutter/flutter/issues/71679)
- The TextField cursor doesn't seem to be handling space inputs properly on the web platform. Pending [issue resolution](https://github.com/flutter/flutter/issues/77929). The flag `webSpaceFix` fixes it by swapping spaces with transparent middle points.
## Notes
A [breaking change](https://flutter.dev/docs/release/breaking-changes/buildtextspan-buildcontext) to the `TextEditingController` was introduced in flutter beta, dev & master channels. The branch [beta](https://github.com/BertrandBev/code_field/tree/beta) should comply with those changes.

View File

@@ -0,0 +1,15 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"dart.vmAdditionalArgs": [
"--no-sound-null-safety"
],
"dart.flutterRunAdditionalArgs": [
"--no-sound-null-safety"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -0,0 +1,46 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

View File

@@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: c5a4b4029c0798f37c4a39b479d7cb75daa7b05c
channel: stable
project_type: app

View File

@@ -0,0 +1,11 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties

View File

@@ -0,0 +1,59 @@
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 30
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.example"
minSdkVersion 16
targetSdkVersion 30
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.example">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,41 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.example">
<application
android:label="example"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<!-- Displays an Android View that continues showing the launch screen
Drawable until Flutter paints its first frame, then this splash
screen fades out. A splash screen is useful to avoid any visual
gap between the end of Android's launch screen and the painting of
Flutter's first frame. -->
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>

View File

@@ -0,0 +1,6 @@
package com.example.example
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.example">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,31 @@
buildscript {
ext.kotlin_version = '1.3.50'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
jcenter()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -0,0 +1,6 @@
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip

View File

@@ -0,0 +1,11 @@
include ':app'
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"

Some files were not shown because too many files have changed in this diff Show More