52 Commits
v1.0 ... v1.0.2

Author SHA1 Message Date
d4e8dec698 修改bug 2022-01-20 14:22:01 +08:00
a97aa1f347 添加关于页面 2022-01-20 14:05:32 +08:00
130459d646 优化样式 2022-01-20 13:33:52 +08:00
4f8285fcf1 Update token_interceptor.dart 2022-01-20 11:27:44 +08:00
25bc0d4101 更新样式 2022-01-20 11:27:11 +08:00
282695cc93 添加错误提示 2022-01-20 11:21:11 +08:00
df9aa8e72f 修改域名输错之后无法登录的bug 2022-01-20 11:16:18 +08:00
747b15fdea 添加日志监控 2022-01-20 10:57:01 +08:00
22f66d36a2 支持脚本的编辑删除 2022-01-20 10:25:10 +08:00
89ad8e38e5 Merge remote-tracking branch 'remotes/origin/main' into dev 2022-01-20 09:13:18 +08:00
fb802040ca Merge branch 'dev' of https://github.com/qinglong-app/qinglong_app into dev 2022-01-20 09:13:11 +08:00
72b9829dbe 修改使用secret登录权限的问题 2022-01-20 09:13:07 +08:00
b177298e07 Update README.md 2022-01-19 21:15:24 +08:00
364c0da236 增加client_id登录方式 2022-01-19 20:35:14 +08:00
e8694643d8 增加client_id登录方式 2022-01-19 19:39:25 +08:00
6dbed6cf49 支持clientid登录 2022-01-19 17:40:32 +08:00
fe3e0f1d50 优化暗黑体验 2022-01-19 16:38:02 +08:00
4fe730d414 优化Android返回键体验 2022-01-19 15:54:06 +08:00
346ebd68cf 增加变更日志 2022-01-19 15:38:07 +08:00
4bf2a1b61f 优化使用体验 2022-01-19 15:34:28 +08:00
0bdf267b36 优化样式 2022-01-19 15:29:11 +08:00
1981edfafe add demo 2022-01-19 15:25:54 +08:00
3959155c1b 优化使用体验 2022-01-19 15:14:29 +08:00
16f4f27f17 懒加载优化性能 2022-01-19 14:47:11 +08:00
6185a92759 环境变量增加序号,详情页面 2022-01-19 14:41:14 +08:00
38a6790c51 添加任务详情 2022-01-19 14:13:16 +08:00
8a85a838e7 新增任务详情页 2022-01-19 13:30:27 +08:00
51da0abb84 环境变量增加更新时间显示 2022-01-19 11:07:50 +08:00
fffa930eb1 Merge remote-tracking branch 'remotes/origin/main' into dev 2022-01-19 10:32:03 +08:00
d72f5bfd5e 不指定ndk 支持所有架构 2022-01-19 10:31:24 +08:00
2dfcf35191 添加正则 2022-01-19 10:27:43 +08:00
b048fdaad8 Update README.md 2022-01-19 10:04:47 +08:00
9ce85a34e9 Android禁止横竖屏切换 2022-01-19 09:59:06 +08:00
c51ebc16d2 增加两步验证提醒 2022-01-19 09:56:17 +08:00
e457f3c4c9 Update README.md 2022-01-19 09:49:45 +08:00
884ee84f8a Update README.md 2022-01-19 09:46:39 +08:00
7bd9e823b3 Update README.md 2022-01-19 09:39:51 +08:00
901944983f Update README.md 2022-01-19 09:34:47 +08:00
ca6f1db16f update style 2022-01-19 09:15:31 +08:00
0a2e9a3c09 Update extension.dart 2022-01-18 18:59:55 +08:00
5ba83212af 更新获取provider方式 2022-01-18 18:55:19 +08:00
e8a3a43e4a Update README.md 2022-01-18 15:30:57 +08:00
00bfee5827 update readme 2022-01-18 15:30:15 +08:00
5d56d9b1a8 update images 2022-01-18 15:24:44 +08:00
58f6a5d964 Update README.md 2022-01-18 15:20:39 +08:00
f66e0d2c1c Update README.md 2022-01-18 15:19:21 +08:00
98f807589b Update README.md 2022-01-18 15:19:00 +08:00
0426041288 Update README.md 2022-01-18 15:18:34 +08:00
da8fae1b6d Update README.md 2022-01-18 15:13:17 +08:00
708ee91d08 Update README.md 2022-01-18 15:10:55 +08:00
a211eb19cc Update README.md 2022-01-18 15:10:08 +08:00
ca0a54c3ab add image 2022-01-18 15:05:32 +08:00
63 changed files with 4694 additions and 1248 deletions

21
CHANGELOG.md Normal file
View File

@ -0,0 +1,21 @@
## 1.0.2
* 支持修改用户名密码(仅限使用用户名密码登录用户)
* 支持脚本的编辑删除
* 支持client_id登录之后只启用部分模块
* 支持两步验证
## 1.0.1
* 增加任务详情页
* 增加环境变量详情页
* 环境变量列表使用体验优化
* 部分页面进入卡顿优化
* 扩大任务的搜索范围
* 优化暗黑主题
* 支持client_id登录
* 修改bug
## 1.0.0
* 完成基础功能

View File

@ -1,3 +1,45 @@
# qinglong_app
基于[qinglong](https://github.com/whyour/qinglong)API的三方客户端
版本更新通知 https://t.me/qinglongapp
仅供学习交流, 禁止用于任何商业用途
基于[qinglong](https://github.com/whyour/qinglong)API实现的三方客户端,采用Flutter编写 (API基于v2.10.13)
Android端去[release](https://github.com/qinglong-app/qinglong_app/releases)下载
iOS端暂无上架打算用户自行下载main分支源码编译安装
<p float="left">
<img src="./art/1.jpg" width="200" />
<img src="./art/8.jpg" width="200" />
<img src="./art/9.jpg" width="200" />
</p>
<p float="left">
<img src="./art/10.jpg" width="200" />
<img src="./art/11.jpg" width="200" />
<img src="./art/12.jpg" width="200" />
</p>
<p float="left">
<img src="./art/2.jpg" width="200" />
<img src="./art/3.jpg" width="200" />
<img src="./art/4.jpg" width="200" />
</p>
<p float="left">
<img src="./art/5.jpg" width="200" />
<img src="./art/6.jpg" width="200" />
<img src="./art/7.jpg" width="200" />
</p>
<p float="left">
<img src="./art/13.jpg" width="200" />
</p>
### 尚未完成的功能
>* 修改用户名密码
>* 两步验证
>* 应用设置
>* 通知设置
>* 脚本管理中的增删改和调试

View File

@ -47,9 +47,6 @@ android {
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
ndk {
abiFilters 'armeabi-v7a'
}
}
buildTypes {

View File

@ -1,30 +1,37 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="work.master.qinglongapp">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:label="青龙"
<uses-permission android:name="android.permission.INTERNET" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
</queries>
<application
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:label="青龙">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:theme="@style/LaunchTheme"
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"
/>
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.

BIN
art/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

BIN
art/10.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

BIN
art/11.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
art/12.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

BIN
art/13.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

BIN
art/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

BIN
art/3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 KiB

BIN
art/4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
art/5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

BIN
art/6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 KiB

BIN
art/7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

BIN
art/8.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

BIN
art/9.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

41
ios/Podfile.lock Normal file
View File

@ -0,0 +1,41 @@
PODS:
- Flutter (1.0.0)
- fluttertoast (0.0.2):
- Flutter
- Toast
- move_to_background (0.0.1):
- Flutter
- shared_preferences_ios (0.0.1):
- Flutter
- Toast (4.0.0)
DEPENDENCIES:
- Flutter (from `Flutter`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- move_to_background (from `.symlinks/plugins/move_to_background/ios`)
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
SPEC REPOS:
trunk:
- Toast
EXTERNAL SOURCES:
Flutter:
:path: Flutter
fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios"
move_to_background:
:path: ".symlinks/plugins/move_to_background/ios"
shared_preferences_ios:
:path: ".symlinks/plugins/shared_preferences_ios/ios"
SPEC CHECKSUMS:
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
fluttertoast: 6122fa75143e992b1d3470f61000f591a798cc58
move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d
shared_preferences_ios: aef470a42dc4675a1cdd50e3158b42e3d1232b32
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
PODFILE CHECKSUM: aea2ed8a4b05dec076f6f7cea39202b1133ed51c
COCOAPODS: 1.11.2

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1320"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -28,6 +28,11 @@
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
<string>http</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>

View File

@ -1,5 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qinglong_app/base/ui/lazy_load_state.dart';
import 'package:qinglong_app/utils/extension.dart';
import 'base_viewmodel.dart';
@ -8,6 +9,7 @@ class BaseStateWidget<T extends BaseViewModel> extends ConsumerStatefulWidget {
final ProviderBase<T> model;
final Widget? child;
final Function(T)? onReady;
final bool lazyLoad;
const BaseStateWidget({
Key? key,
@ -15,26 +17,14 @@ class BaseStateWidget<T extends BaseViewModel> extends ConsumerStatefulWidget {
required this.model,
this.child,
this.onReady,
this.lazyLoad = false,
}) : super(key: key);
@override
_BaseStateWidgetState<T> createState() => _BaseStateWidgetState<T>();
}
class _BaseStateWidgetState<T extends BaseViewModel>
extends ConsumerState<BaseStateWidget<T>> {
@override
void initState() {
super.initState();
WidgetsBinding.instance?.addPostFrameCallback(
(timeStamp) {
if (widget.onReady != null) {
widget.onReady!(ref.read<T>(widget.model));
}
},
);
}
class _BaseStateWidgetState<T extends BaseViewModel> extends ConsumerState<BaseStateWidget<T>> with LazyLoadState<BaseStateWidget<T>> {
@override
Widget build(BuildContext context) {
var viewModel = ref.watch<T>(widget.model);
@ -65,32 +55,27 @@ class _BaseStateWidgetState<T extends BaseViewModel>
if (viewModel.currentState == PageState.EMPTY) {
return Container(
alignment: Alignment.center,
child: Text("暂无数据"),
child: const Text("暂无数据"),
);
}
return Container();
}
}
abstract class BaseState<T extends StatefulWidget> extends State {
late T parent;
@override
void onLazyLoad() {
if (widget.onReady != null && widget.lazyLoad) {
widget.onReady!(ref.read<T>(widget.model));
}
}
@override
void initState() {
parent = widget as T;
super.initState();
WidgetsBinding.instance?.endOfFrame.then((_) {
firstFrameCalled();
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
if (widget.onReady != null && !widget.lazyLoad) {
widget.onReady!(ref.read<T>(widget.model));
}
});
}
void firstFrameCalled() {}
@override
Widget build(BuildContext context) {
return buildWidgets(context);
}
Widget buildWidgets(BuildContext context);
}

View File

@ -2,18 +2,20 @@ import 'package:qinglong_app/base/http/http.dart';
import 'package:qinglong_app/module/config/config_bean.dart';
import 'package:qinglong_app/module/env/env_bean.dart';
import 'package:qinglong_app/module/login/login_bean.dart';
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/login_log/login_log_bean.dart';
import 'package:qinglong_app/module/others/scripts/script_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_detail/task_detail_bean.dart';
import 'url.dart';
class Api {
static Future<HttpResponse<LoginBean>> login(
String userName, String passWord) async {
String userName,
String passWord,
) async {
return await Http.post<LoginBean>(
Url.login,
{
@ -23,6 +25,41 @@ class Api {
);
}
static Future<HttpResponse<LoginBean>> loginTwo(
String userName,
String passWord,
String code,
) async {
return await Http.put<LoginBean>(
Url.loginTwo,
{
"username": userName,
"password": passWord,
"code": code,
},
);
}
static Future<HttpResponse<LoginBean>> loginByClientId(
String id,
String secret,
) async {
return await Http.get<LoginBean>(
Url.loginByClientId,
{
"client_id": id,
"client_secret": secret,
},
);
}
static Future<HttpResponse<UserBean>> user() async {
return await Http.get<UserBean>(
Url.user,
null,
);
}
static Future<HttpResponse<List<TaskBean>>> crons() async {
return await Http.get<List<TaskBean>>(
Url.tasks,
@ -30,22 +67,30 @@ class Api {
);
}
static Future<HttpResponse<NullResponse>> startTasks(
List<String> crons) async {
static Future<HttpResponse<NullResponse>> startTasks(List<String> crons) async {
return await Http.put<NullResponse>(
Url.runTasks,
crons,
);
}
static Future<HttpResponse<NullResponse>> stopTasks(
List<String> crons) async {
static Future<HttpResponse<NullResponse>> stopTasks(List<String> crons) async {
return await Http.put<NullResponse>(
Url.runTasks,
crons,
);
}
static Future<HttpResponse<NullResponse>> updatePassword(String name, String password) async {
return await Http.put<NullResponse>(
Url.updatePassword,
{
"username": name,
"password": password,
},
);
}
static Future<HttpResponse<String>> inTimeLog(String cron) async {
return await Http.get<String>(
Url.intimeLog(cron),
@ -53,26 +98,17 @@ class Api {
);
}
static Future<HttpResponse<TaskDetailBean>> taskDetail(String cron) async {
return await Http.get<TaskDetailBean>(
Url.taskDetail + cron,
null,
);
}
static Future<HttpResponse<TaskDetailBean>> addTask(
String name, String command, String cron,
{String? id}) async {
static Future<HttpResponse<NullResponse>> addTask(String name, String command, String cron, {String? id}) async {
var data = {"name": name, "command": command, "schedule": cron};
if (id != null) {
data["_id"] = id;
return await Http.put<TaskDetailBean>(
return await Http.put<NullResponse>(
Url.addTask,
data,
);
}
return await Http.post<TaskDetailBean>(
return await Http.post<NullResponse>(
Url.addTask,
data,
);
@ -127,8 +163,7 @@ class Api {
);
}
static Future<HttpResponse<NullResponse>> saveFile(
String name, String content) async {
static Future<HttpResponse<NullResponse>> saveFile(String name, String content) async {
return await Http.post<NullResponse>(
Url.saveFile,
{"content": content, "name": name},
@ -163,9 +198,7 @@ class Api {
);
}
static Future<HttpResponse<NullResponse>> addEnv(
String name, String value, String remarks,
{String? id}) async {
static Future<HttpResponse<NullResponse>> addEnv(String name, String value, String remarks, {String? id}) async {
var data = {
"value": value,
"remarks": remarks,
@ -185,8 +218,7 @@ class Api {
);
}
static Future<HttpResponse<NullResponse>> moveEnv(
String id, int fromIndex, int toIndex) async {
static Future<HttpResponse<NullResponse>> moveEnv(String id, int fromIndex, int toIndex) async {
return await Http.put<NullResponse>(
Url.envMove(id),
{"fromIndex": fromIndex, "toIndex": toIndex},
@ -201,8 +233,7 @@ class Api {
}
static Future<HttpResponse<List<TaskLogBean>>> taskLog() async {
return await Http.get<List<TaskLogBean>>(Url.taskLog, null,
serializationName: "dirs");
return await Http.get<List<TaskLogBean>>(Url.taskLog, null, serializationName: "dirs");
}
static Future<HttpResponse<String>> taskLogDetail(String name) async {
@ -219,8 +250,28 @@ class Api {
);
}
static Future<HttpResponse<String>> scriptDetail(
String name, String? path) async {
static Future<HttpResponse<NullResponse>> updateScript(String name, String path, String content) async {
return await Http.put<NullResponse>(
Url.scriptDetail,
{
"filename": name,
"path": path,
"content": content,
},
);
}
static Future<HttpResponse<NullResponse>> delScript(String name, String path) async {
return await Http.delete<NullResponse>(
Url.scriptDetail,
{
"filename": name,
"path": path,
},
);
}
static Future<HttpResponse<String>> scriptDetail(String name, String? path) async {
return await Http.get<String>(
Url.scriptDetail + name,
{
@ -229,8 +280,7 @@ class Api {
);
}
static Future<HttpResponse<List<DependencyBean>>> dependencies(
String type) async {
static Future<HttpResponse<List<DependencyBean>>> dependencies(String type) async {
return await Http.get<List<DependencyBean>>(
Url.dependencies,
{
@ -239,8 +289,7 @@ class Api {
);
}
static Future<HttpResponse<NullResponse>> dependencyReinstall(
String id) async {
static Future<HttpResponse<NullResponse>> dependencyReinstall(String id) async {
return await Http.put<NullResponse>(
Url.dependencies,
[id],
@ -254,8 +303,7 @@ class Api {
);
}
static Future<HttpResponse<NullResponse>> addDependency(
String name, int type) async {
static Future<HttpResponse<NullResponse>> addDependency(String name, int type) async {
return await Http.post<NullResponse>(
Url.dependencies,
[

View File

@ -6,15 +6,15 @@ import 'package:dio_log/dio_log.dart';
import 'package:flutter/foundation.dart';
import 'package:qinglong_app/base/http/token_interceptor.dart';
import 'package:qinglong_app/base/userinfo_viewmodel.dart';
import 'package:qinglong_app/utils/QlNavigatorObserver.dart';
import 'package:qinglong_app/utils/extension.dart';
import '../../json.jc.dart';
import '../../main.dart';
import '../routes.dart';
class Http {
static const int NOT_LOGIN = 1000;
static Dio? _dio;
static bool pushedLoginPage = false;
static void initDioConfig(
String host,
@ -22,9 +22,9 @@ class Http {
_dio = Dio(
BaseOptions(
baseUrl: host,
connectTimeout: 5000,
receiveTimeout: 5000,
sendTimeout: 5000,
connectTimeout: 50000,
receiveTimeout: 50000,
sendTimeout: 50000,
contentType: "application/json",
),
);
@ -38,6 +38,10 @@ class Http {
}
}
static void clear() {
_dio = null;
}
static Future<HttpResponse<T>> get<T>(
String uri,
Map<String, String?>? json, {
@ -50,13 +54,7 @@ class Http {
return decodeResponse<T>(response, serializationName, compute);
} on DioError catch (e) {
if (e.response?.statusCode == 401) {
exitLogin();
}
return HttpResponse(
success: false,
message: e.response?.statusMessage ?? e.message,
code: 0);
return exceptionHandler<T>(e);
}
}
@ -76,13 +74,7 @@ class Http {
compute,
);
} on DioError catch (e) {
if (e.response?.statusCode == 401) {
exitLogin();
}
return HttpResponse(
success: false,
message: e.response?.statusMessage ?? e.message,
code: 0);
return exceptionHandler<T>(e);
}
}
@ -102,13 +94,7 @@ class Http {
compute,
);
} on DioError catch (e) {
if (e.response?.statusCode == 401) {
exitLogin();
}
return HttpResponse(
success: false,
message: e.response?.statusMessage ?? e.message,
code: 0);
return exceptionHandler<T>(e);
}
}
@ -127,22 +113,30 @@ class Http {
compute,
);
} on DioError catch (e) {
if (e.response?.statusCode == 401) {
exitLogin();
}
return HttpResponse(
success: false,
message: e.response?.statusMessage ?? e.message,
code: 0);
return exceptionHandler<T>(e);
}
}
static bool pushedLoginPage = false;
static void exitLogin() {
if (!pushedLoginPage) {
"身份已过期,请重新登录".toast();
pushedLoginPage = true;
navigatorState.currentState?.pushReplacementNamed(Routes.routeLogin);
navigatorState.currentState?.pushNamedAndRemoveUntil(Routes.routeLogin, (route) => false);
}
}
static HttpResponse<T> exceptionHandler<T>(DioError e) {
if (e.response?.statusCode == 401) {
if (!getIt<UserInfoViewModel>().useSecretLogined) {
exitLogin();
}
return HttpResponse(success: false, message: "没有该模块的访问权限", code: 401);
}
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);
} else {
return HttpResponse(success: false, message: e.message, code: e.response?.statusCode ?? 0);
}
}
@ -198,7 +192,7 @@ class Http {
} else {
return HttpResponse<T>(
success: false,
code: -1000,
code: response.data["code"],
message: response.data["message"],
);
}
@ -227,8 +221,7 @@ class HttpResponse<T> {
late int code;
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> {

View File

@ -1,4 +1,5 @@
import 'package:dio/dio.dart';
import 'package:qinglong_app/base/http/url.dart';
import 'package:qinglong_app/main.dart';
import '../userinfo_viewmodel.dart';
@ -12,11 +13,13 @@ class TokenInterceptor extends Interceptor {
"Bearer " + getIt<UserInfoViewModel>().token!;
}
options.headers["User-Agent"] =
"Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1";
options.headers["User-Agent"] = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1";
options.headers["Content-Type"] = "application/json;charset=UTF-8";
options.queryParameters["t"] =
(DateTime.now().millisecondsSinceEpoch ~/ 1000).toString();
if (options.path != Url.loginByClientId && options.path!= Url.loginTwo) {
options.queryParameters["t"] =
(DateTime.now().millisecondsSinceEpoch ~/ 1000).toString();
}
return handler.next(options);
}
}

View File

@ -1,35 +1,71 @@
import '../../main.dart';
import '../userinfo_viewmodel.dart';
class Url {
static const login = "/api/user/login";
static const tasks = "/api/crons";
static const runTasks = "/api/crons/run";
static const stopTasks = "/api/crons/stop";
static const taskDetail = "/api/crons/";
static const addTask = "/api/crons";
static const pinTask = "/api/crons/pin";
static const unpinTask = "/api/crons/unpin";
static const enableTask = "/api/crons/enable";
static const disableTask = "/api/crons/disable";
static const files = "/api/configs/files";
static const configContent = "/api/configs/";
static const saveFile = "/api/configs/save";
static const envs = "/api/envs";
static const addEnv = "/api/envs";
static const delEnv = "/api/envs";
static const disableEnvs = "/api/envs/disable";
static const enableEnvs = "/api/envs/enable";
static const loginLog = "/api/user/login-log";
static const taskLog = "/api/logs";
static const taskLogDetail = "/api/logs/";
static const scripts = "/api/scripts/files";
static const scriptDetail = "/api/scripts/";
static const dependencies = "/api/dependencies";
static const dependencyReinstall = "/api/dependencies/reinstall";
static get login => "/api/user/login";
static get loginTwo => "/api/user/two-factor/login";
static const loginByClientId = "/open/auth/token";
static const user = "/api/user";
static const updatePassword = "/api/user";
static get tasks => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons" : "/api/crons";
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 taskDetail => 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 unpinTask => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/unpin" : "/api/crons/unpin";
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 files => getIt<UserInfoViewModel>().useSecretLogined ? "/open/configs/files" : "/api/configs/files";
static get configContent => getIt<UserInfoViewModel>().useSecretLogined ? "/open/configs/" : "/api/configs/";
static get saveFile => getIt<UserInfoViewModel>().useSecretLogined ? "/open/configs/save" : "/api/configs/save";
static get envs => 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 disableEnvs => getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs/disable" : "/api/envs/disable";
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 taskLog => 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 scriptUpdate => 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 dependencyReinstall => getIt<UserInfoViewModel>().useSecretLogined ? "/open/dependencies/reinstall" : "/api/dependencies/reinstall";
static intimeLog(String cronId) {
return "/api/crons/$cronId/log";
return getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/$cronId/log" : "/api/crons/$cronId/log";
}
static envMove(String envId) {
return "/api/envs/$envId/move";
return getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs/$envId/move" : "/api/envs/$envId/move";
}
}

View File

@ -20,16 +20,23 @@ class QlAppBar extends StatelessWidget with PreferredSizeWidget {
@override
Widget build(BuildContext context) {
Widget back = backWidget ??
GestureDetector(
InkWell(
onTap: () {
if (backCall != null) {
backCall!();
} else {
Navigator.of(context).pop();
}
},
child: const Center(
child: Icon(
CupertinoIcons.left_chevron,
color: Colors.white,
child: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 15,
),
child: Center(
child: Icon(
CupertinoIcons.left_chevron,
color: Colors.white,
),
),
),
);

View File

@ -3,22 +3,29 @@ import 'package:flutter/material.dart';
import 'package:qinglong_app/module/config/config_edit_page.dart';
import 'package:qinglong_app/module/env/add_env_page.dart';
import 'package:qinglong_app/module/env/env_bean.dart';
import 'package:qinglong_app/module/env/env_detail_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/others/about_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/login_log/login_log_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_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/update_password_page.dart';
import 'package:qinglong_app/module/task/add_task_page.dart';
import 'package:qinglong_app/module/task/task_bean.dart';
import 'package:qinglong_app/module/task/task_detail/task_detail_page.dart';
class Routes {
static const String routeHomePage = "/home/homepage";
static const String routeLogin = "/login";
static const String routeAddTask = "/task/add";
static const String routeTaskDetail = "/task/detail";
static const String routeEnvDetail = "/env/detail";
static const String routeAddDependency = "/task/dependency";
static const String routeAddEnv = "/env/add";
static const String routeConfigEdit = "/config/edit";
@ -27,7 +34,11 @@ class Routes {
static const String routeTaskLogDetail = "/log/taskDetail";
static const String routeScript = "/script";
static const String routeScriptDetail = "/script/detail";
static const String routeScriptUpdate = "/script/update";
static const String routeScriptAdd = "/script/add";
static const String routeDependency = "/Dependency";
static const String routeUpdatePassword = "/updatePassword";
static const String routeAbout = "/about";
static Route<dynamic>? generateRoute(RouteSettings settings) {
switch (settings.name) {
@ -45,12 +56,12 @@ class Routes {
return CupertinoPageRoute(builder: (context) => const AddTaskPage());
}
case routeAddDependency:
return CupertinoPageRoute(builder: (context) => const AddDependenyPage());
return CupertinoPageRoute(builder: (context) => const AddDependencyPage());
case routeAddEnv:
if (settings.arguments != null) {
return CupertinoPageRoute(
builder: (context) => AddEnvPage(
taskBean: settings.arguments as EnvBean,
envBean: settings.arguments as EnvBean,
));
} else {
return CupertinoPageRoute(builder: (context) => const AddEnvPage());
@ -91,6 +102,34 @@ class Routes {
path: (settings.arguments as Map)["path"],
),
);
case routeTaskDetail:
return CupertinoPageRoute(
builder: (context) => TaskDetailPage(
settings.arguments as TaskBean,
),
);
case routeEnvDetail:
return CupertinoPageRoute(
builder: (context) => EnvDetailPage(
settings.arguments as EnvBean,
),
);
case routeUpdatePassword:
return CupertinoPageRoute(
builder: (context) => const UpdatePasswordPage(),
);
case routeAbout:
return CupertinoPageRoute(
builder: (context) => const AboutPage(),
);
case routeScriptUpdate:
return CupertinoPageRoute(
builder: (context) => ScriptEditPage(
(settings.arguments as Map)["title"],
(settings.arguments as Map)["path"],
(settings.arguments as Map)["content"],
),
);
}
return null;

View File

@ -3,3 +3,4 @@ String spHost = "host";
String spUserName = "username";
String spPassWord = "password";
String spTheme = "dart_mode";
String spSecretLogined = "secret_logined";

View File

@ -13,13 +13,14 @@ get primaryColor => _primaryColor;
class ThemeViewModel extends ChangeNotifier {
ThemeData currentTheme = lightTheme;
bool _isInDarkMode = false;
ThemeColors themeColor = LightThemeColors();
ThemeViewModel() {
var brightness = SchedulerBinding.instance!.window.platformBrightness;
bool isDarkMode = brightness == Brightness.dark;
changeThemeReal(isDarkMode, false);
_isInDarkMode = brightness == Brightness.dark;
changeThemeReal(_isInDarkMode, false);
}
bool isInDartMode() {
@ -27,6 +28,7 @@ class ThemeViewModel extends ChangeNotifier {
}
void changeThemeReal(bool dark, [bool notify = true]) {
_isInDarkMode = dark;
SpUtil.putBool(spTheme, dark);
if (!dark) {
currentTheme = lightTheme;
@ -40,6 +42,8 @@ class ThemeViewModel extends ChangeNotifier {
}
}
get darkMode => _isInDarkMode;
void changeTheme() {
changeThemeReal(!SpUtil.getBool(spTheme, defValue: false), true);
}
@ -63,6 +67,16 @@ ThemeData darkTheme = ThemeData.dark().copyWith(
color: _primaryColor,
),
),
border: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xff999999),
),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xff999999),
),
),
),
tabBarTheme: const TabBarTheme(
labelStyle: TextStyle(
@ -92,6 +106,11 @@ ThemeData darkTheme = ThemeData.dark().copyWith(
},
),
),
cupertinoOverrideTheme: const NoDefaultCupertinoThemeData(
brightness: Brightness.dark,
primaryColor: Color(0xffffffff),
scaffoldBackgroundColor: Colors.black,
),
);
ThemeData lightTheme = ThemeData.light().copyWith(
brightness: Brightness.light,
@ -108,6 +127,16 @@ ThemeData lightTheme = ThemeData.light().copyWith(
color: _primaryColor,
),
),
border: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xff999999),
),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xff999999),
),
),
),
appBarTheme: const AppBarTheme(
backgroundColor: _primaryColor,
@ -152,12 +181,17 @@ ThemeData lightTheme = ThemeData.light().copyWith(
},
),
),
cupertinoOverrideTheme: const NoDefaultCupertinoThemeData(
brightness: Brightness.light,
primaryColor: _primaryColor,
scaffoldBackgroundColor: Color(0xfff5f5f5),
),
);
abstract class ThemeColors {
Color settingBgColor();
Color taskTitleColor();
Color titleColor();
Color descColor();
@ -170,7 +204,7 @@ abstract class ThemeColors {
class LightThemeColors extends ThemeColors {
@override
Color taskTitleColor() {
Color titleColor() {
return const Color(0xff333333);
}
@ -202,7 +236,7 @@ class LightThemeColors extends ThemeColors {
class DartThemeColors extends ThemeColors {
@override
Color taskTitleColor() {
Color titleColor() {
return Colors.white;
}
@ -223,7 +257,7 @@ class DartThemeColors extends ThemeColors {
@override
Color settingBgColor() {
return Colors.black12;
return Colors.black;
}
@override

File diff suppressed because it is too large Load Diff

View File

@ -7,12 +7,16 @@ class UserInfoViewModel {
String? _host = "";
String? _userName;
String? _passWord;
bool _useSecertLogined = false;
UserInfoViewModel() {
String userInfoJson = SpUtil.getString(spUserInfo);
_userName = SpUtil.getString(spUserName);
_passWord = SpUtil.getString(spPassWord);
_useSecertLogined = SpUtil.getBool(spSecretLogined, defValue: false);
_host = SpUtil.getString(spHost, defValue: '');
if (userInfoJson.isNotEmpty) {
_token = userInfoJson;
}
@ -30,6 +34,11 @@ class UserInfoViewModel {
SpUtil.putString(spPassWord, password);
}
void useSecretLogin(bool use) {
_useSecertLogined = use;
SpUtil.putBool(spSecretLogined, _useSecertLogined);
}
void updateHost(String host) {
_host = host;
SpUtil.putString(spHost, host);
@ -43,6 +52,8 @@ class UserInfoViewModel {
String? get passWord => _passWord;
bool get useSecretLogined => _useSecertLogined;
bool isLogined() {
return token != null && token!.isNotEmpty;
}

View File

@ -3,15 +3,17 @@
import 'package:qinglong_app/module/config/config_bean.dart';
import 'package:qinglong_app/module/env/env_bean.dart';
import 'package:qinglong_app/module/login/login_bean.dart';
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/login_log/login_log_bean.dart';
import 'package:qinglong_app/module/others/scripts/script_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_detail/task_detail_bean.dart';
class JsonConversion$Json {
static M fromJson<M>(dynamic json) {
static M fromJson<M>(dynamic json) {
if (json is List) {
return _getListChildType<M>(json);
} else {
@ -20,94 +22,83 @@ class JsonConversion$Json {
}
static M _fromJsonSingle<M>(dynamic json) {
String type = M.toString();
if (type == (ConfigBean).toString()) {
return ConfigBean.jsonConversion(json) as M;
if(type == (ConfigBean).toString()){
return ConfigBean.jsonConversion(json) as M;
}
if (type == (EnvBean).toString()) {
return EnvBean.jsonConversion(json) as M;
if(type == (EnvBean).toString()){
return EnvBean.jsonConversion(json) as M;
}
if (type == (LoginBean).toString()) {
return LoginBean.jsonConversion(json) as M;
if(type == (LoginBean).toString()){
return LoginBean.jsonConversion(json) as M;
}
if (type == (DependencyBean).toString()) {
return DependencyBean.jsonConversion(json) as M;
if(type == (UserBean).toString()){
return UserBean.jsonConversion(json) as M;
}
if (type == (LoginLogBean).toString()) {
return LoginLogBean.jsonConversion(json) as M;
if(type == (DependencyBean).toString()){
return DependencyBean.jsonConversion(json) as M;
}
if (type == (ScriptBean).toString()) {
return ScriptBean.jsonConversion(json) as M;
if(type == (LoginLogBean).toString()){
return LoginLogBean.jsonConversion(json) as M;
}
if (type == (TaskLogBean).toString()) {
return TaskLogBean.jsonConversion(json) as M;
if(type == (ScriptBean).toString()){
return ScriptBean.jsonConversion(json) as M;
}
if (type == (TaskBean).toString()) {
return TaskBean.jsonConversion(json) as M;
if(type == (TaskLogBean).toString()){
return TaskLogBean.jsonConversion(json) as M;
}
if (type == (TaskDetailBean).toString()) {
return TaskDetailBean.jsonConversion(json) as M;
if(type == (TaskBean).toString()){
return TaskBean.jsonConversion(json) as M;
}
throw Exception("not found");
}
static M _getListChildType<M>(List<dynamic> data) {
if (<ConfigBean>[] is M) {
return data.map<ConfigBean>((e) => ConfigBean.jsonConversion(e)).toList()
as M;
if(<ConfigBean>[] is M){
return data.map<ConfigBean>((e) => ConfigBean.jsonConversion(e)).toList() as M;
}
if (<EnvBean>[] is M) {
if(<EnvBean>[] is M){
return data.map<EnvBean>((e) => EnvBean.jsonConversion(e)).toList() as M;
}
if (<LoginBean>[] is M) {
return data.map<LoginBean>((e) => LoginBean.jsonConversion(e)).toList()
as M;
if(<LoginBean>[] is M){
return data.map<LoginBean>((e) => LoginBean.jsonConversion(e)).toList() as M;
}
if (<DependencyBean>[] is M) {
return data
.map<DependencyBean>((e) => DependencyBean.jsonConversion(e))
.toList() as M;
if(<UserBean>[] is M){
return data.map<UserBean>((e) => UserBean.jsonConversion(e)).toList() as M;
}
if (<LoginLogBean>[] is M) {
return data
.map<LoginLogBean>((e) => LoginLogBean.jsonConversion(e))
.toList() as M;
if(<DependencyBean>[] is M){
return data.map<DependencyBean>((e) => DependencyBean.jsonConversion(e)).toList() as M;
}
if (<ScriptBean>[] is M) {
return data.map<ScriptBean>((e) => ScriptBean.jsonConversion(e)).toList()
as M;
if(<LoginLogBean>[] is M){
return data.map<LoginLogBean>((e) => LoginLogBean.jsonConversion(e)).toList() as M;
}
if (<TaskLogBean>[] is M) {
return data
.map<TaskLogBean>((e) => TaskLogBean.jsonConversion(e))
.toList() as M;
if(<ScriptBean>[] is M){
return data.map<ScriptBean>((e) => ScriptBean.jsonConversion(e)).toList() as M;
}
if (<TaskBean>[] is M) {
return data.map<TaskBean>((e) => TaskBean.jsonConversion(e)).toList()
as M;
if(<TaskLogBean>[] is M){
return data.map<TaskLogBean>((e) => TaskLogBean.jsonConversion(e)).toList() as M;
}
if (<TaskDetailBean>[] is M) {
return data
.map<TaskDetailBean>((e) => TaskDetailBean.jsonConversion(e))
.toList() as M;
if(<TaskBean>[] is M){
return data.map<TaskBean>((e) => TaskBean.jsonConversion(e)).toList() as M;
}
throw Exception("not found");

View File

@ -9,7 +9,6 @@ import 'package:get_it/get_it.dart';
import 'package:logger/logger.dart';
import 'package:qinglong_app/base/theme.dart';
import 'package:qinglong_app/module/login/login_page.dart';
import 'package:qinglong_app/utils/QlNavigatorObserver.dart';
import 'package:qinglong_app/utils/sp_utils.dart';
import 'base/routes.dart';
@ -25,14 +24,13 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized();
await SpUtil.getInstance();
getIt.registerSingleton<UserInfoViewModel>(UserInfoViewModel());
getIt.registerSingleton<QlNavigatorObserver>(QlNavigatorObserver());
runApp(
ProviderScope(
overrides: [
themeProvider,
],
child: const MyApp(),
child: const QlApp(),
),
);
if (Platform.isAndroid) {
@ -41,33 +39,40 @@ void main() async {
}
}
class MyApp extends ConsumerWidget {
const MyApp({Key? key}) : super(key: key);
class QlApp extends ConsumerStatefulWidget {
const QlApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
ConsumerState<QlApp> createState() => QlAppState();
}
class QlAppState extends ConsumerState<QlApp> {
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: MaterialApp(
locale: const Locale('zh', 'cn'),
navigatorObservers: [getIt<QlNavigatorObserver>()],
navigatorKey: navigatorState,
theme: ref.watch<ThemeViewModel>(themeProvider).currentTheme,
onGenerateRoute: (setting) {
return Routes.generateRoute(setting);
},
home: Builder(
builder: (context) {
if (!kReleaseMode) {
showDebugBtn(context, btnColor: Colors.blue);
}
return getIt<UserInfoViewModel>().isLogined() ? const HomePage() : LoginPage();
child: MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance!.window).copyWith(textScaleFactor: 1,),
child: MaterialApp(
locale: const Locale('zh', 'cn'),
navigatorKey: navigatorState,
theme: ref.watch<ThemeViewModel>(themeProvider).currentTheme,
onGenerateRoute: (setting) {
return Routes.generateRoute(setting);
},
home: Builder(
builder: (context) {
if (!kReleaseMode) {
showDebugBtn(context, btnColor: Colors.blue);
}
return getIt<UserInfoViewModel>().isLogined() ? const HomePage() : const LoginPage();
},
),
// home: LoginPage(),
),
// home: LoginPage(),
),
);
}

View File

@ -18,14 +18,18 @@ class ConfigEditPage extends ConsumerStatefulWidget {
}
class _ConfigEditPageState extends ConsumerState<ConfigEditPage> {
String? value;
late TextEditingController _controller;
FocusNode node = FocusNode();
@override
void initState() {
_controller = TextEditingController(text: widget.content);
super.initState();
WidgetsBinding.instance?.addPostFrameCallback(
(timeStamp) {
node.requestFocus();
},
);
}
@override
@ -40,13 +44,10 @@ class _ConfigEditPageState extends ConsumerState<ConfigEditPage> {
actions: [
InkWell(
onTap: () async {
if (value == null) {
"请先点击保存".toast();
return;
}
HttpResponse<NullResponse> response =
await Api.saveFile(widget.title, _controller.text);
if (response.success) {
"提交成功".toast();
ref.read(configProvider).loadContent(widget.title);
Navigator.of(context).pop();
} else {
@ -58,7 +59,13 @@ class _ConfigEditPageState extends ConsumerState<ConfigEditPage> {
horizontal: 15,
),
child: Center(
child: Text("提交"),
child: Text(
"提交",
style: TextStyle(
color: Colors.white,
fontSize: 16,
),
),
),
),
)
@ -71,7 +78,7 @@ class _ConfigEditPageState extends ConsumerState<ConfigEditPage> {
),
child: SingleChildScrollView(
child: TextField(
focusNode: FocusNode(),
focusNode: node,
style: TextStyle(
color: ref.read(themeProvider).themeColor.descColor(),
fontSize: 14,

View File

@ -8,9 +8,9 @@ import 'package:qinglong_app/module/env/env_viewmodel.dart';
import 'package:qinglong_app/utils/extension.dart';
class AddEnvPage extends ConsumerStatefulWidget {
final EnvBean? taskBean;
final EnvBean? envBean;
const AddEnvPage({Key? key, this.taskBean}) : super(key: key);
const AddEnvPage({Key? key, this.envBean}) : super(key: key);
@override
ConsumerState<AddEnvPage> createState() => _AddEnvPageState();
@ -22,18 +22,24 @@ class _AddEnvPageState extends ConsumerState<AddEnvPage> {
final TextEditingController _nameController = TextEditingController();
final TextEditingController _valueController = TextEditingController();
final TextEditingController _remarkController = TextEditingController();
FocusNode focusNode = FocusNode();
@override
void initState() {
super.initState();
if (widget.taskBean != null) {
envBean = widget.taskBean!;
if (widget.envBean != null) {
envBean = widget.envBean!;
_nameController.text = envBean.name ?? "";
_valueController.text = envBean.value ?? "";
_remarkController.text = envBean.remarks ?? "";
} else {
envBean = EnvBean();
}
WidgetsBinding.instance?.addPostFrameCallback(
(timeStamp) {
focusNode.requestFocus();
},
);
}
@override
@ -82,7 +88,7 @@ class _AddEnvPageState extends ConsumerState<AddEnvPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 10,
height: 15,
),
const Text(
"名称:",
@ -91,13 +97,10 @@ class _AddEnvPageState extends ConsumerState<AddEnvPage> {
fontWeight: FontWeight.w600,
),
),
const SizedBox(
height: 10,
),
TextField(
focusNode: focusNode,
controller: _nameController,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "请输入名称",
),
autofocus: false,
@ -114,7 +117,7 @@ class _AddEnvPageState extends ConsumerState<AddEnvPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 10,
height: 15,
),
const Text(
"值:",
@ -123,15 +126,11 @@ class _AddEnvPageState extends ConsumerState<AddEnvPage> {
fontWeight: FontWeight.w600,
),
),
const SizedBox(
height: 10,
),
TextField(
controller: _valueController,
maxLines: 8,
minLines: 1,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "请输入值",
),
autofocus: false,
@ -148,7 +147,7 @@ class _AddEnvPageState extends ConsumerState<AddEnvPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 10,
height: 15,
),
const Text(
"备注:",
@ -157,13 +156,9 @@ class _AddEnvPageState extends ConsumerState<AddEnvPage> {
fontWeight: FontWeight.w600,
),
),
const SizedBox(
height: 10,
),
TextField(
controller: _remarkController,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "请输入备注",
),
autofocus: false,
@ -195,7 +190,7 @@ class _AddEnvPageState extends ConsumerState<AddEnvPage> {
id: envBean.sId);
if (response.success) {
"操作成功".toast();
(envBean.sId == null) ? "新增成功" : "修改成功".toast();
ref.read(envProvider).updateEnv(envBean);
Navigator.of(context).pop();
} else {

341
lib/module/env/env_detail_page.dart vendored Normal file
View File

@ -0,0 +1,341 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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:qinglong_app/module/env/env_bean.dart';
import 'package:qinglong_app/module/env/env_viewmodel.dart';
import 'package:qinglong_app/utils/utils.dart';
class EnvDetailPage extends ConsumerStatefulWidget {
final EnvBean envBean;
const EnvDetailPage(this.envBean, {Key? key}) : super(key: key);
@override
_TaskDetailPageState createState() => _TaskDetailPageState();
}
class _TaskDetailPageState extends ConsumerState<EnvDetailPage> {
List<Widget> actions = [];
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
actions.clear();
actions.addAll(
[
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed(Routes.routeAddEnv, arguments: widget.envBean);
},
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 15,
),
alignment: Alignment.center,
child: const Material(
color: Colors.transparent,
child: Text(
"编辑",
style: TextStyle(
fontSize: 16,
),
),
),
),
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.of(context).pop();
enableTask();
},
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 15,
),
alignment: Alignment.center,
child: Material(
color: Colors.transparent,
child: Text(
widget.envBean.status! == 0 ? "禁用" : "启用",
style: const TextStyle(
color: Colors.red,
fontSize: 16,
),
),
),
),
),
],
);
return Scaffold(
appBar: QlAppBar(
canBack: true,
backCall: () {
Navigator.of(context).pop();
},
title: widget.envBean.name ?? "",
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: SingleChildScrollView(
child: Column(
children: [
Container(
margin: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 15,
),
padding: const EdgeInsets.only(
top: 10,
bottom: 10,
),
decoration: BoxDecoration(
color: ref.watch(themeProvider).themeColor.settingBgColor(),
borderRadius: BorderRadius.circular(15),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
EnvDetailCell(
title: "ID",
desc: widget.envBean.sId ?? "",
),
EnvDetailCell(
title: "变量名称",
desc: widget.envBean.name ?? "",
),
EnvDetailCell(
title: "创建时间",
desc: Utils.formatMessageTime(widget.envBean.created ?? 0),
),
EnvDetailCell(
title: "更新时间",
desc: Utils.formatGMTTime(widget.envBean.timestamp ?? ""),
),
EnvDetailCell(
title: "",
desc: widget.envBean.value ?? "",
),
EnvDetailCell(
title: "备注",
desc: widget.envBean.remarks ?? "",
),
EnvDetailCell(
title: "变量状态",
desc: widget.envBean.status == 1 ? "已禁用" : "已启用",
hideDivide: true,
),
],
),
),
SizedBox(
width: MediaQuery.of(context).size.width - 80,
child: CupertinoButton(
padding: const EdgeInsets.symmetric(
vertical: 5,
),
color: Colors.red,
child: const Text(
"删 除",
style: TextStyle(
fontSize: 16,
),
),
onPressed: () {
delTask(context, ref);
}),
),
const SizedBox(
height: 15,
),
],
),
),
);
}
void enableTask() async {
await ref.read(envProvider).enableEnv(widget.envBean.sId!, widget.envBean.status!);
setState(() {});
}
void delTask(BuildContext context, WidgetRef ref) {
showCupertinoDialog(
context: context,
builder: (context) => CupertinoAlertDialog(
title: const Text("确认删除"),
content: Text("确认删除环境变量 ${widget.envBean.name ?? ""}"),
actions: [
CupertinoDialogAction(
child: const Text(
"取消",
style: TextStyle(
color: Color(0xff999999),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
CupertinoDialogAction(
child: Text(
"确定",
style: TextStyle(
color: primaryColor,
),
),
onPressed: () async {
Navigator.of(context).pop();
await ref.read(envProvider).delEnv(widget.envBean.sId!);
Navigator.of(context).pop();
},
),
],
),
);
}
}
class EnvDetailCell extends ConsumerWidget {
final String title;
final String? desc;
final Widget? icon;
final bool hideDivide;
final Function? taped;
const EnvDetailCell({
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,
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

@ -11,6 +11,7 @@ import 'package:qinglong_app/base/ui/empty_widget.dart';
import 'package:qinglong_app/module/env/env_bean.dart';
import 'package:qinglong_app/module/env/env_viewmodel.dart';
import 'package:qinglong_app/utils/extension.dart';
import 'package:qinglong_app/utils/utils.dart';
class EnvPage extends StatefulWidget {
const EnvPage({Key? key}) : super(key: key);
@ -28,13 +29,15 @@ class _EnvPageState extends State<EnvPage> {
builder: (ref, model, child) {
List<EnvItemCell> list = [];
for (var value in model.list) {
for (int i = 0; i < model.list.length; i++) {
EnvBean value = model.list[i];
if (_searchController.text.isEmpty ||
(value.name?.contains(_searchController.text) ?? false) ||
(value.value?.contains(_searchController.text) ?? false) ||
(value.remarks?.contains(_searchController.text) ?? false)) {
list.add(EnvItemCell(
value,
i + 1,
ref,
key: ValueKey(value.sId),
));
@ -49,8 +52,7 @@ class _EnvPageState extends State<EnvPage> {
return model.loadData(false);
},
child: ReorderableListView(
keyboardDismissBehavior:
ScrollViewKeyboardDismissBehavior.onDrag,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
header: searchCell(ref),
onReorder: (int oldIndex, int newIndex) {
if (list.length != model.list.length) {
@ -124,134 +126,179 @@ class _EnvPageState extends State<EnvPage> {
class EnvItemCell extends StatelessWidget {
final EnvBean bean;
final int index;
final WidgetRef ref;
const EnvItemCell(this.bean, this.ref, {Key? key}) : super(key: key);
const EnvItemCell(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.sId),
endActionPane: ActionPane(
motion: const ScrollMotion(),
extentRatio: 0.45,
children: [
SlidableAction(
backgroundColor: Colors.grey,
flex: 1,
onPressed: (_) {
Navigator.of(context)
.pushNamed(Routes.routeAddEnv, arguments: bean);
},
foregroundColor: Colors.white,
icon: CupertinoIcons.pencil,
),
SlidableAction(
backgroundColor: Colors.orange,
flex: 1,
onPressed: (_) {
enableEnv();
},
foregroundColor: Colors.white,
icon: bean.status == 0
? Icons.dnd_forwardslash
: Icons.check_circle_outline_sharp,
),
SlidableAction(
backgroundColor: Colors.red,
flex: 1,
onPressed: (_) {
delEnv(context, ref);
},
foregroundColor: Colors.white,
icon: CupertinoIcons.delete,
),
],
),
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
return GestureDetector(
onTap: () {
Navigator.of(context).pushNamed(Routes.routeEnvDetail, arguments: bean);
},
child: ColoredBox(
color: ref.watch(themeProvider).themeColor.settingBgColor(),
child: Slidable(
key: ValueKey(bean.sId),
endActionPane: ActionPane(
motion: const ScrollMotion(),
extentRatio: 0.45,
children: [
Container(
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: Material(
SlidableAction(
backgroundColor: Colors.grey,
flex: 1,
onPressed: (_) {
Navigator.of(context).pushNamed(Routes.routeAddEnv, arguments: bean);
},
foregroundColor: Colors.white,
icon: CupertinoIcons.pencil,
),
SlidableAction(
backgroundColor: Colors.orange,
flex: 1,
onPressed: (_) {
enableEnv();
},
foregroundColor: Colors.white,
icon: bean.status == 0 ? Icons.dnd_forwardslash : Icons.check_circle_outline_sharp,
),
SlidableAction(
backgroundColor: Colors.red,
flex: 1,
onPressed: (_) {
delEnv(context, ref);
},
foregroundColor: Colors.white,
icon: CupertinoIcons.delete,
),
],
),
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
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(
children: [
Material(
color: Colors.transparent,
child: Text(
bean.name ?? "",
maxLines: 1,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 18,
),
),
),
],
),
),
const SizedBox(
width: 5,
),
Material(
color: Colors.transparent,
child: Text(
bean.name ?? "",
Utils.formatGMTTime(bean.timestamp ?? ""),
maxLines: 1,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color: bean.status == 1
? const Color(0xffF85152)
: ref
.watch(themeProvider)
.themeColor
.taskTitleColor(),
fontSize: 18,
color: ref.watch(themeProvider).themeColor.descColor(),
fontSize: 12,
),
),
),
),
const SizedBox(
width: 5,
),
Material(
color: Colors.transparent,
child: Text(
bean.remarks ?? "-",
maxLines: 1,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color: ref
.watch(themeProvider)
.themeColor
.descColor(),
fontSize: 12,
],
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 5,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
border: Border.all(color: primaryColor, width: 1),
),
child: Text(
"$index",
style: TextStyle(color: primaryColor, fontSize: 12),
),
),
),
),
],
),
const SizedBox(
height: 8,
),
Material(
color: Colors.transparent,
child: Text(
bean.value ?? "",
maxLines: 1,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color:
ref.watch(themeProvider).themeColor.descColor(),
fontSize: 12,
const SizedBox(
width: 5,
),
Material(
color: Colors.transparent,
child: Text(
bean.remarks ?? "-",
maxLines: 1,
style: TextStyle(
height: 1,
overflow: TextOverflow.ellipsis,
color: ref.watch(themeProvider).themeColor.descColor(),
fontSize: 12,
),
),
),
const SizedBox(
width: 5,
),
bean.status == 1
? const Icon(
Icons.dnd_forwardslash,
size: 12,
color: Colors.red,
)
: const SizedBox.shrink(),
const Spacer(),
],
),
),
),
],
const SizedBox(
height: 8,
),
Material(
color: Colors.transparent,
child: Text(
bean.value ?? "",
maxLines: 1,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color: ref.watch(themeProvider).themeColor.descColor(),
fontSize: 12,
),
),
),
],
),
),
),
const Divider(
height: 1,
indent: 15,
),
],
const Divider(
height: 1,
indent: 15,
),
],
),
),
),
),
@ -270,13 +317,23 @@ class EnvItemCell extends StatelessWidget {
content: Text("确认删除环境变量 ${bean.name ?? ""}"),
actions: [
CupertinoDialogAction(
child: const Text("取消"),
child: const Text(
"取消",
style: TextStyle(
color: Color(0xff999999),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
CupertinoDialogAction(
child: const Text("确定"),
child: Text(
"确定",
style: TextStyle(
color: primaryColor,
),
),
onPressed: () {
Navigator.of(context).pop();
ref.read(envProvider).delEnv(bean.sId!);

View File

@ -3,6 +3,7 @@ import 'package:qinglong_app/base/base_viewmodel.dart';
import 'package:qinglong_app/base/http/api.dart';
import 'package:qinglong_app/base/http/http.dart';
import 'package:qinglong_app/module/env/env_bean.dart';
import 'package:qinglong_app/utils/extension.dart';
var envProvider = ChangeNotifierProvider((ref) => EnvViewModel());
@ -29,6 +30,7 @@ class EnvViewModel extends BaseViewModel {
Future<void> delEnv(String id) async {
HttpResponse<NullResponse> result = await Api.delEnv(id);
if (result.success) {
"删除成功".toast();
list.removeWhere((element) => element.sId == id);
notifyListeners();
} else {
@ -48,11 +50,12 @@ class EnvViewModel extends BaseViewModel {
notifyListeners();
}
void enableEnv(String sId, int status) async {
Future<void> enableEnv(String sId, int status) async {
if (status == 1) {
HttpResponse<NullResponse> response = await Api.enableEnv(sId);
if (response.success) {
"启用成功".toast();
list.firstWhere((element) => element.sId == sId).status = 0;
success();
} else {
@ -62,6 +65,7 @@ class EnvViewModel extends BaseViewModel {
HttpResponse<NullResponse> response = await Api.disableEnv(sId);
if (response.success) {
"禁用成功".toast();
list.firstWhere((element) => element.sId == sId).status = 1;
success();
} else {

View File

@ -1,5 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qinglong_app/base/ql_app_bar.dart';
import 'package:qinglong_app/base/routes.dart';
@ -7,6 +8,7 @@ import 'package:qinglong_app/module/config/config_page.dart';
import 'package:qinglong_app/module/env/env_page.dart';
import 'package:qinglong_app/module/others/other_page.dart';
import 'package:qinglong_app/module/task/task_page.dart';
import 'package:move_to_background/move_to_background.dart';
class HomePage extends ConsumerStatefulWidget {
const HomePage({Key? key}) : super(key: key);
@ -35,45 +37,49 @@ class _HomePageState extends ConsumerState<HomePage> {
List<Widget> actions = [];
if (_index == 0) {
actions.add(InkWell(
onTap: () {
Navigator.of(context).pushNamed(
Routes.routeAddTask,
);
},
child: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 15,
),
child: Center(
child: Icon(
CupertinoIcons.add,
size: 20,
color: Colors.white,
actions.add(
InkWell(
onTap: () {
Navigator.of(context).pushNamed(
Routes.routeAddTask,
);
},
child: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 15,
),
child: Center(
child: Icon(
CupertinoIcons.add,
size: 24,
color: Colors.white,
),
),
),
),
));
);
} else if (_index == 1) {
actions.add(InkWell(
onTap: () {
Navigator.of(context).pushNamed(
Routes.routeAddEnv,
);
},
child: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 15,
),
child: Center(
child: Icon(
CupertinoIcons.add,
size: 20,
color: Colors.white,
actions.add(
InkWell(
onTap: () {
Navigator.of(context).pushNamed(
Routes.routeAddEnv,
);
},
child: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 15,
),
child: Center(
child: Icon(
CupertinoIcons.add,
size: 24,
color: Colors.white,
),
),
),
),
));
);
} else if (_index == 2) {
actions.add(InkWell(
onTap: () {
@ -96,48 +102,54 @@ class _HomePageState extends ConsumerState<HomePage> {
));
}
return Scaffold(
appBar: QlAppBar(
canBack: false,
title: _title,
actions: actions,
),
body: IndexedStack(
index: _index,
children: [
const Positioned.fill(
child: TaskPage(),
),
const Positioned.fill(
child: EnvPage(),
),
Positioned.fill(
child: ConfigPage(
key: configKey,
return WillPopScope(
onWillPop: () async {
await MoveToBackground.moveTaskToBack();
return false;
},
child: Scaffold(
appBar: QlAppBar(
canBack: false,
title: _title,
actions: actions,
),
body: IndexedStack(
index: _index,
children: [
const Positioned.fill(
child: TaskPage(),
),
),
const Positioned.fill(
child: OtherPage(),
),
],
),
bottomNavigationBar: BottomNavigationBar(
items: titles
.map(
(e) => BottomNavigationBarItem(
icon: Icon(e.icon),
activeIcon: Icon(e.checkedIcon),
label: e.title,
const Positioned.fill(
child: EnvPage(),
),
Positioned.fill(
child: ConfigPage(
key: configKey,
),
)
.toList(),
currentIndex: _index,
onTap: (index) {
_index = index;
_title = titles[index].title;
setState(() {});
},
type: BottomNavigationBarType.fixed,
),
const Positioned.fill(
child: OtherPage(),
),
],
),
bottomNavigationBar: BottomNavigationBar(
items: titles
.map(
(e) => BottomNavigationBarItem(
icon: Icon(e.icon),
activeIcon: Icon(e.checkedIcon),
label: e.title,
),
)
.toList(),
currentIndex: _index,
onTap: (index) {
_index = index;
_title = titles[index].title;
setState(() {});
},
type: BottomNavigationBarType.fixed,
),
),
);
}

View File

@ -7,8 +7,10 @@ import 'package:qinglong_app/base/routes.dart';
import 'package:qinglong_app/base/theme.dart';
import 'package:qinglong_app/base/userinfo_viewmodel.dart';
import 'package:qinglong_app/main.dart';
import 'package:qinglong_app/module/login/user_bean.dart';
import 'package:qinglong_app/utils/extension.dart';
import 'package:qinglong_app/utils/utils.dart';
import 'package:flip_card/flip_card.dart';
import 'login_bean.dart';
@ -20,29 +22,45 @@ class LoginPage extends ConsumerStatefulWidget {
}
class _LoginPageState extends ConsumerState<LoginPage> {
final TextEditingController _hostController =
TextEditingController(text: getIt<UserInfoViewModel>().host);
final TextEditingController _hostController = TextEditingController(text: getIt<UserInfoViewModel>().host);
final TextEditingController _userNameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final TextEditingController _cIdController = TextEditingController();
final TextEditingController _cSecretController = TextEditingController();
GlobalKey<FlipCardState> cardKey = GlobalKey<FlipCardState>();
bool rememberPassword = false;
bool useSecretLogin = false;
@override
void initState() {
super.initState();
useSecretLogin = getIt<UserInfoViewModel>().useSecretLogined;
if (getIt<UserInfoViewModel>().userName != null &&
getIt<UserInfoViewModel>().userName!.isNotEmpty) {
_userNameController.text = getIt<UserInfoViewModel>().userName!;
if (getIt<UserInfoViewModel>().userName != null && getIt<UserInfoViewModel>().userName!.isNotEmpty) {
if (getIt<UserInfoViewModel>().useSecretLogined) {
_cIdController.text = getIt<UserInfoViewModel>().userName!;
} else {
_userNameController.text = getIt<UserInfoViewModel>().userName!;
}
rememberPassword = true;
} else {
rememberPassword = false;
}
if (getIt<UserInfoViewModel>().passWord != null &&
getIt<UserInfoViewModel>().passWord!.isNotEmpty) {
_passwordController.text = getIt<UserInfoViewModel>().passWord!;
if (getIt<UserInfoViewModel>().passWord != null && getIt<UserInfoViewModel>().passWord!.isNotEmpty) {
if (getIt<UserInfoViewModel>().useSecretLogined) {
_cSecretController.text = getIt<UserInfoViewModel>().passWord!;
} else {
_passwordController.text = getIt<UserInfoViewModel>().passWord!;
}
}
getIt<UserInfoViewModel>().updateToken("");
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
if (useSecretLogin) {
cardKey.currentState?.toggleCard();
}
});
}
@override
@ -63,120 +81,203 @@ class _LoginPageState extends ConsumerState<LoginPage> {
Scaffold(
backgroundColor: Colors.transparent,
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 40,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: MediaQuery.of(context).size.height / 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 40,
),
SizedBox(
height: 50,
width: MediaQuery.of(context).size.width,
child: Stack(
children: [
Positioned(
top: 0,
left: 0,
child: Image.asset(
"assets/images/login_tip.png",
height: 45,
),
),
const Positioned(
top: 10,
left: 0,
child: Text(
"登录",
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: MediaQuery.of(context).size.height / 10,
),
SizedBox(
height: 50,
width: MediaQuery.of(context).size.width,
child: Stack(
children: [
Positioned(
top: 0,
left: 0,
child: Image.asset(
"assets/images/login_tip.png",
height: 45,
),
),
const Positioned(
top: 10,
left: 0,
child: Text(
"登录",
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
),
),
),
Positioned(
top: 5,
right: 0,
child: Image.asset(
"assets/images/ql.png",
height: 45,
),
),
],
),
),
SizedBox(
height: MediaQuery.of(context).size.height / 15,
),
const Text(
"域名:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
TextField(
onChanged: (_) {
setState(() {});
},
controller: _hostController,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "http://1.1.1.1:5700",
),
autofocus: false,
),
FlipCard(
key: cardKey,
onFlipDone: (back) {
useSecretLogin = back;
setState(() {});
},
direction: FlipDirection.HORIZONTAL,
front: SizedBox(
height: 200,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 15,
),
const Text(
"用户名:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
TextField(
onChanged: (_) {
setState(() {});
},
controller: _userNameController,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "请输入用户名",
),
autofocus: false,
),
const SizedBox(
height: 15,
),
const Text(
"密码:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
TextField(
onChanged: (_) {
setState(() {});
},
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "请输入密码",
),
autofocus: false,
),
const SizedBox(
height: 10,
),
],
),
),
Positioned(
top: 5,
right: 0,
child: Image.asset(
"assets/images/ql.png",
height: 45,
back: SizedBox(
height: 200,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 15,
),
const Text(
"client_id:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
TextField(
onChanged: (_) {
setState(() {});
},
controller: _cIdController,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "请输入client_id",
),
autofocus: false,
),
const SizedBox(
height: 15,
),
const Text(
"client_secret:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
TextField(
onChanged: (_) {
setState(() {});
},
controller: _cSecretController,
obscureText: true,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "请输入client_secret",
),
autofocus: false,
),
const SizedBox(
height: 10,
),
],
),
),
],
),
),
],
),
SizedBox(
height: MediaQuery.of(context).size.height / 15,
),
const Text(
"域名:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
TextField(
onChanged: (_) {
setState(() {});
},
controller: _hostController,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "http://1.1.1.1:5700",
),
autofocus: false,
),
const SizedBox(
height: 15,
),
const Text(
"用户名:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
TextField(
onChanged: (_) {
setState(() {});
},
controller: _userNameController,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "请输入用户名",
),
autofocus: false,
),
const SizedBox(
height: 15,
),
const Text(
"密码:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
TextField(
onChanged: (_) {
setState(() {});
},
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "请输入密码",
),
autofocus: false,
),
const SizedBox(
height: 10,
),
Row(
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25,),
child: Row(
children: [
Checkbox(
value: rememberPassword,
@ -186,64 +287,69 @@ class _LoginPageState extends ConsumerState<LoginPage> {
},
),
const Text(
"记住密码",
"记住密码/client",
style: TextStyle(
fontSize: 14,
),
),
const Spacer(),
GestureDetector(
onTap: () {
cardKey.currentState?.toggleCard();
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
setState(() {});
});
},
child: Text(
loginByUserName() ? "client_id登录" : "用户名密码登录",
style: const TextStyle(
fontSize: 14,
),
),
),
SizedBox(width: 10,),
],
),
const SizedBox(
height: 30,
),
SizedBox(
),
const SizedBox(
height: 30,
),
Container(
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 40,),
child: SizedBox(
width: MediaQuery.of(context).size.width - 80,
child: IgnorePointer(
ignoring: _hostController.text.isEmpty ||
_userNameController.text.isEmpty ||
_passwordController.text.isEmpty ||
isLoading,
ignoring: !canClickLoginBtn(),
child: CupertinoButton(
padding: const EdgeInsets.symmetric(
vertical: 5,
),
color: (_hostController.text.isNotEmpty &&
_userNameController.text.isNotEmpty &&
_passwordController.text.isNotEmpty &&
!isLoading)
? ref
.watch(themeProvider)
.themeColor
.buttonBgColor()
: Theme.of(context)
.primaryColor
.withOpacity(0.4),
child: isLoading
? const CupertinoActivityIndicator()
: const Text(
"登 录",
style: TextStyle(
fontSize: 16,
),
padding: const EdgeInsets.symmetric(
vertical: 5,
),
color: canClickLoginBtn() ? primaryColor : primaryColor.withOpacity(0.4),
child: isLoading
? const CupertinoActivityIndicator()
: const Text(
"登 录",
style: TextStyle(
fontSize: 16,
),
onPressed: () {
if (rememberPassword) {
getIt<UserInfoViewModel>().updateUserName(
_userNameController.text,
_passwordController.text);
}
Http.pushedLoginPage = false;
Utils.hideKeyBoard(context);
getIt<UserInfoViewModel>()
.updateHost(_hostController.text);
login(_userNameController.text,
_passwordController.text);
}),
),
onPressed: () {
Http.pushedLoginPage = false;
Utils.hideKeyBoard(context);
getIt<UserInfoViewModel>().updateHost(_hostController.text);
Http.clear();
if (loginByUserName()) {
login(_userNameController.text, _passwordController.text);
} else {
login(_cIdController.text, _cSecretController.text);
}
},
),
),
),
],
),
),
],
),
),
),
@ -255,17 +361,118 @@ class _LoginPageState extends ConsumerState<LoginPage> {
bool isLoading = false;
bool loginByUserName() {
return !useSecretLogin;
}
Future<void> login(String userName, String password) async {
isLoading = true;
setState(() {});
HttpResponse<LoginBean> response = await Api.login(userName, password);
if (response.success) {
getIt<UserInfoViewModel>().updateToken(response.bean?.token ?? "");
Navigator.of(context).pushReplacementNamed(Routes.routeHomePage);
HttpResponse<LoginBean> response;
if (loginByUserName()) {
response = await Api.login(userName, password);
} else {
response = await Api.loginByClientId(userName, password);
}
if (response.success) {
loginSuccess(response, userName, password);
} else {
print(response.code);
(response.message ?? "请检查网络情况").toast();
isLoading = false;
setState(() {});
//420代表需要2步验证
if (response.code == 420) {
String twoFact = "";
showCupertinoDialog(
context: context,
builder: (_) => CupertinoAlertDialog(
title: const Text("两步验证"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Material(
color: Colors.transparent,
child: TextField(
onChanged: (value) {
twoFact = value;
},
maxLines: 1,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "请输入code",
),
autofocus: true,
),
),
],
),
actions: [
CupertinoDialogAction(
child: const Text(
"取消",
style: TextStyle(
color: Color(0xff999999),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
CupertinoDialogAction(
child: Text(
"确定",
style: TextStyle(
color: primaryColor,
),
),
onPressed: () async {
Navigator.of(context).pop(true);
HttpResponse<LoginBean> response = await Api.loginTwo(userName, password, twoFact);
if (response.success) {
loginSuccess(response, userName, password);
} else {
loginFailed(response);
}
},
),
],
)).then((value) {
if (value == null) {
isLoading = false;
setState(() {});
}
});
} else {
isLoading = false;
setState(() {});
}
}
}
void loginFailed(HttpResponse<LoginBean> response) {
(response.message ?? "请检查网络情况").toast();
isLoading = false;
setState(() {});
}
void loginSuccess(HttpResponse<LoginBean> response, String userName, String password) {
getIt<UserInfoViewModel>().updateToken(response.bean?.token ?? "");
getIt<UserInfoViewModel>().useSecretLogin(!loginByUserName());
if (rememberPassword) {
getIt<UserInfoViewModel>().updateUserName(userName, password);
}
Navigator.of(context).pushReplacementNamed(Routes.routeHomePage);
}
bool canClickLoginBtn() {
if (isLoading) return false;
if (_hostController.text.isEmpty) return false;
if (!loginByUserName()) {
return _cIdController.text.isNotEmpty && _cSecretController.text.isNotEmpty;
} else {
return _userNameController.text.isNotEmpty && _passwordController.text.isNotEmpty;
}
}
}

View File

@ -0,0 +1,25 @@
import 'package:json_conversion_annotation/json_conversion_annotation.dart';
/// @author NewTab
@JsonConversion()
class UserBean {
String? username;
bool? twoFactorActivated;
UserBean({this.username, this.twoFactorActivated});
UserBean.fromJson(Map<String, dynamic> json) {
username = json['username'];
twoFactorActivated = json['twoFactorActivated'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['username'] = this.username;
data['twoFactorActivated'] = this.twoFactorActivated;
return data;
}
static UserBean jsonConversion(Map<String, dynamic> json) {
return UserBean.fromJson(json);
}
}

View File

@ -0,0 +1,128 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qinglong_app/base/ql_app_bar.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:qinglong_app/base/theme.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../main.dart';
/// @author NewTab
class AboutPage extends ConsumerStatefulWidget {
const AboutPage({Key? key}) : super(key: key);
@override
ConsumerState createState() => _AboutPageState();
}
class _AboutPageState extends ConsumerState<AboutPage> {
String desc = "";
@override
void initState() {
super.initState();
getInfo();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: QlAppBar(
canBack: true,
title: "关于",
),
body: SizedBox(
width: MediaQuery.of(context).size.width,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: MediaQuery.of(context).size.height / 10,
),
Image.asset(
"assets/images/ql.png",
height: 50,
),
const SizedBox(
height: 30,
),
Text(
"青龙",
style: TextStyle(
fontWeight: FontWeight.w600,
color: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 20,
),
),
const SizedBox(
height: 15,
),
Text(
"Version ${desc}",
style: TextStyle(
color: ref.watch(themeProvider).themeColor.descColor(),
fontSize: 16,
),
),
const Spacer(),
Row(
children: [
const Spacer(),
GestureDetector(
onTap: () {
_launchURL("https://github.com/qinglong-app/qinglong_app/releases");
},
child: Text(
"版本更新",
style: TextStyle(
color: primaryColor,
fontSize: 16,
),
),
),
const SizedBox(
width: 15,
),
GestureDetector(
onTap: () {
_launchURL("https://github.com/qinglong-app/qinglong_app");
},
child: Text(
"项目地址",
style: TextStyle(
color: primaryColor,
fontSize: 16,
),
),
),
const Spacer(),
],
),
const SizedBox(
height: 15,
),
],
),
),
);
}
void _launchURL(String _url) async {
try {
await launch(_url);
} catch (e) {
logger.e(e);
}
}
void getInfo() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
String version = packageInfo.version;
desc = version;
setState(() {});
}
}

View File

@ -1,24 +1,22 @@
import 'package:dio_log/dio_log.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/module/others/dependencies/dependency_viewmodel.dart';
import 'package:qinglong_app/module/task/task_bean.dart';
import 'package:qinglong_app/module/task/task_detail/task_detail_bean.dart';
import 'package:qinglong_app/module/task/task_viewmodel.dart';
import 'package:qinglong_app/utils/extension.dart';
class AddDependenyPage extends ConsumerStatefulWidget {
const AddDependenyPage({
class AddDependencyPage extends ConsumerStatefulWidget {
const AddDependencyPage({
Key? key,
}) : super(key: key);
@override
ConsumerState<AddDependenyPage> createState() => _AddDependencyPageState();
ConsumerState<AddDependencyPage> createState() => _AddDependencyPageState();
}
class _AddDependencyPageState extends ConsumerState<AddDependenyPage> {
class _AddDependencyPageState extends ConsumerState<AddDependencyPage> {
final TextEditingController _nameController = TextEditingController();
DepedencyEnum depedencyType = DepedencyEnum.NodeJS;
@ -39,6 +37,10 @@ class _AddDependencyPageState extends ConsumerState<AddDependenyPage> {
title: "新增依赖",
actions: [
InkWell(
onLongPress: () {
showDebugBtn(context, btnColor: Colors.blue);
WidgetsBinding.instance?.endOfFrame;
},
onTap: () {
submit();
},
@ -59,90 +61,88 @@ class _AddDependencyPageState extends ConsumerState<AddDependenyPage> {
)
],
),
body: Container(
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: 10,
),
const Text(
"依赖类型:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(
height: 10,
),
DropdownButtonFormField<DepedencyEnum>(
items: [
DropdownMenuItem(
value: DepedencyEnum.NodeJS,
child: Text(DepedencyEnum.NodeJS.name),
),
DropdownMenuItem(
value: DepedencyEnum.Python3,
child: Text(DepedencyEnum.Python3.name),
),
DropdownMenuItem(
value: DepedencyEnum.Linux,
child: Text(DepedencyEnum.Linux.name),
),
],
value: DepedencyEnum.NodeJS,
onChanged: (value) {
depedencyType = value!;
},
),
],
),
body: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 10,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 10,
),
const Text(
"依赖类型:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
const Text(
"名称:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
const SizedBox(
height: 10,
),
DropdownButtonFormField<DepedencyEnum>(
items: [
DropdownMenuItem(
value: DepedencyEnum.NodeJS,
child: Text(DepedencyEnum.NodeJS.name),
),
),
const SizedBox(
height: 10,
),
TextField(
controller: _nameController,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "请输入名称",
DropdownMenuItem(
value: DepedencyEnum.Python3,
child: Text(DepedencyEnum.Python3.name),
),
autofocus: false,
),
],
),
DropdownMenuItem(
value: DepedencyEnum.Linux,
child: Text(DepedencyEnum.Linux.name),
),
],
value: DepedencyEnum.NodeJS,
onChanged: (value) {
depedencyType = value!;
},
),
],
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 10,
),
const Text(
"名称:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(
height: 10,
),
TextField(
controller: _nameController,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "请输入名称",
),
autofocus: false,
),
],
),
),
],
),
);
}
@ -159,7 +159,7 @@ class _AddDependencyPageState extends ConsumerState<AddDependenyPage> {
);
if (response.success) {
"操作成功".toast();
"新增成功".toast();
ref.read(dependencyProvider).loadData(
depedencyType.name.toLowerCase(),
);

View File

@ -63,7 +63,7 @@ class _DependcyPageState extends State<DependencyPage> with TickerProviderStateM
child: Center(
child: Icon(
CupertinoIcons.add,
size: 20,
size: 24,
color: Colors.white,
),
),
@ -71,68 +71,68 @@ class _DependcyPageState extends State<DependencyPage> with TickerProviderStateM
),
],
),
body: SizedBox(
width: MediaQuery.of(context).size.width,
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
TabBar(
controller: _tabController,
tabs: types
.map((e) => Tab(
text: e.name,
))
.toList(),
isScrollable: true,
indicator: AbsUnderlineTabIndicator(
wantWidth: 20,
borderSide: BorderSide(
color: Theme.of(context).primaryColor,
width: 2,
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
TabBar(
controller: _tabController,
tabs: types
.map(
(e) => Tab(
text: e.name,
),
)
.toList(),
isScrollable: true,
indicator: AbsUnderlineTabIndicator(
wantWidth: 20,
borderSide: BorderSide(
color: Theme.of(context).primaryColor,
width: 2,
),
),
Expanded(
child: BaseStateWidget<DependencyViewModel>(
onReady: (model) {
model.loadData(types[0].name.toLowerCase());
model.loadData(types[1].name.toLowerCase());
model.loadData(types[2].name.toLowerCase());
},
model: dependencyProvider,
builder: (context, model, child) {
return TabBarView(
controller: _tabController,
children: types.map(
(e) {
List<DependencyBean> list;
if (e.index == 0) {
list = model.nodeJsList;
} else if (e.index == 1) {
list = model.python3List;
} else {
list = model.linuxList;
}
),
Expanded(
child: BaseStateWidget<DependencyViewModel>(
onReady: (model) {
model.loadData(types[0].name.toLowerCase());
model.loadData(types[1].name.toLowerCase());
model.loadData(types[2].name.toLowerCase());
},
model: dependencyProvider,
builder: (context, model, child) {
return TabBarView(
controller: _tabController,
children: types.map(
(e) {
List<DependencyBean> list;
if (e.index == 0) {
list = model.nodeJsList;
} else if (e.index == 1) {
list = model.python3List;
} else {
list = model.linuxList;
}
return RefreshIndicator(
child: ListView.builder(
itemBuilder: (context, index) {
return DependencyCell(e, list[index]);
},
itemCount: list.length,
),
onRefresh: () {
return model.loadData(types[_tabController!.index].name.toLowerCase(), false);
return RefreshIndicator(
child: ListView.builder(
itemBuilder: (context, index) {
return DependencyCell(e, list[index]);
},
);
},
).toList(),
);
},
),
itemCount: list.length,
),
onRefresh: () {
return model.loadData(types[_tabController!.index].name.toLowerCase(), false);
},
);
},
).toList(),
);
},
),
],
),
),
],
),
);
}
@ -187,7 +187,7 @@ class DependencyCell extends ConsumerWidget {
maxLines: 1,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color: ref.watch(themeProvider).themeColor.taskTitleColor(),
color: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 18,
),
),
@ -220,7 +220,7 @@ class DependencyCell extends ConsumerWidget {
maxLines: 1,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color: ref.watch(themeProvider).themeColor.taskTitleColor(),
color: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 18,
),
),
@ -334,13 +334,23 @@ class DependencyCell extends ConsumerWidget {
content: Text("确认删除依赖 ${bean.name ?? ""}"),
actions: [
CupertinoDialogAction(
child: const Text("取消"),
child: const Text(
"取消",
style: TextStyle(
color: Color(0xff999999),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
CupertinoDialogAction(
child: const Text("确定"),
child: Text(
"确定",
style: TextStyle(
color: primaryColor,
),
),
onPressed: () {
Navigator.of(context).pop();
ref.read(dependencyProvider).del(type.name.toLowerCase(), sId ?? "");

View File

@ -32,73 +32,73 @@ class _LoginLogPageState extends ConsumerState<LoginLogPage> with LazyLoadState<
backCall: () {
Navigator.of(context).pop();
},
title: "任务日志",
title: "登录日志",
),
body: list.isEmpty
? const Center(
child: CupertinoActivityIndicator(),
)
child: CupertinoActivityIndicator(),
)
: ListView.builder(
itemBuilder: (context, index) {
LoginLogBean item = list[index];
itemBuilder: (context, index) {
LoginLogBean item = list[index];
return ColoredBox(
color: ref.watch(themeProvider).themeColor.settingBgColor(),
child: ListTile(
isThreeLine: true,
title: Text(
Utils.formatMessageTime(item.timestamp ?? 0),
style: TextStyle(
fontSize: 16,
color: ref.watch(themeProvider).themeColor.taskTitleColor(),
),
),
subtitle: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 5,
),
SelectableText(
"${item.address}",
selectionWidthStyle: BoxWidthStyle.max,
selectionHeightStyle: BoxHeightStyle.max,
style: TextStyle(
color: ref.watch(themeProvider).themeColor.descColor(),
fontSize: 14,
return ColoredBox(
color: ref.watch(themeProvider).themeColor.settingBgColor(),
child: ListTile(
isThreeLine: true,
title: Text(
Utils.formatMessageTime(item.timestamp ?? 0),
style: TextStyle(
fontSize: 16,
color: ref.watch(themeProvider).themeColor.titleColor(),
),
),
),
const SizedBox(
height: 5,
),
SelectableText(
"${item.ip}",
selectionWidthStyle: BoxWidthStyle.max,
selectionHeightStyle: BoxHeightStyle.max,
style: TextStyle(
color: ref.watch(themeProvider).themeColor.descColor(),
fontSize: 14,
subtitle: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 5,
),
SelectableText(
"${item.address}",
selectionWidthStyle: BoxWidthStyle.max,
selectionHeightStyle: BoxHeightStyle.max,
style: TextStyle(
color: ref.watch(themeProvider).themeColor.descColor(),
fontSize: 14,
),
),
const SizedBox(
height: 5,
),
SelectableText(
"${item.ip}",
selectionWidthStyle: BoxWidthStyle.max,
selectionHeightStyle: BoxHeightStyle.max,
style: TextStyle(
color: ref.watch(themeProvider).themeColor.descColor(),
fontSize: 14,
),
),
],
),
trailing: item.status == 0
? Icon(
CupertinoIcons.checkmark_circle,
color: primaryColor,
size: 16,
)
: const Icon(
CupertinoIcons.clear_circled,
color: Colors.red,
size: 16,
),
),
],
),
trailing: item.status == 0
? Icon(
CupertinoIcons.checkmark_circle,
color: primaryColor,
size: 16,
)
: const Icon(
CupertinoIcons.clear_circled,
color: Colors.red,
size: 16,
),
);
},
itemCount: list.length,
),
);
},
itemCount: list.length,
),
);
}

View File

@ -1,3 +1,4 @@
import 'package:dio_log/dio_log.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -5,6 +6,7 @@ import 'package:qinglong_app/base/routes.dart';
import 'package:qinglong_app/base/theme.dart';
import 'package:qinglong_app/base/userinfo_viewmodel.dart';
import 'package:qinglong_app/main.dart';
import 'package:qinglong_app/utils/extension.dart';
class OtherPage extends ConsumerStatefulWidget {
const OtherPage({Key? key}) : super(key: key);
@ -26,7 +28,7 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Container(
margin: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 15,
vertical: 30,
),
decoration: BoxDecoration(
color: ref.watch(themeProvider).themeColor.settingBgColor(),
@ -57,7 +59,7 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text(
"脚本管理",
style: TextStyle(
color: ref.watch(themeProvider).themeColor.taskTitleColor(),
color: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 16,
),
),
@ -90,7 +92,7 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text(
"依赖管理",
style: TextStyle(
color: ref.watch(themeProvider).themeColor.taskTitleColor(),
color: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 16,
),
),
@ -123,7 +125,60 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text(
"任务日志",
style: TextStyle(
color: ref.watch(themeProvider).themeColor.taskTitleColor(),
color: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 16,
),
),
const Spacer(),
const Icon(
CupertinoIcons.right_chevron,
size: 16,
),
],
),
),
),
],
),
),
Container(
margin: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 15,
),
decoration: BoxDecoration(
color: ref.watch(themeProvider).themeColor.settingBgColor(),
borderRadius: const BorderRadius.all(
Radius.circular(15),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (getIt<UserInfoViewModel>().useSecretLogined) {
"使用client_id方式登录无法获取登录日志".toast();
} else {
Navigator.of(context).pushNamed(
Routes.routeLoginLog,
);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 15,
),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Text(
"登录日志",
style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 16,
),
),
@ -140,24 +195,29 @@ class _OtherPageState extends ConsumerState<OtherPage> {
indent: 15,
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.of(context).pushNamed(
Routes.routeLoginLog,
);
if (getIt<UserInfoViewModel>().useSecretLogined) {
"使用client_id方式登录无法修改密码".toast();
} else {
Navigator.of(context).pushNamed(
Routes.routeUpdatePassword,
);
}
},
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 15,
),
child: Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"登录日志",
"修改密码",
style: TextStyle(
color: ref.watch(themeProvider).themeColor.taskTitleColor(),
color: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 16,
),
),
@ -190,10 +250,9 @@ class _OtherPageState extends ConsumerState<OtherPage> {
children: [
Padding(
padding: const EdgeInsets.only(
top: 5,
bottom: 5,
left: 15,
right: 15,
top: 5,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
@ -202,22 +261,63 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text(
"夜间模式",
style: TextStyle(
color: ref.watch(themeProvider).themeColor.taskTitleColor(),
color: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 16,
),
),
const Spacer(),
CupertinoSwitch(
value: ref.watch(themeProvider).isInDartMode(),
onChanged: (open) {
ref.watch(themeProvider).changeThemeReal(open);
}),
value: ref.watch(themeProvider).isInDartMode(),
onChanged: (open) {
ref.watch(themeProvider).changeThemeReal(open);
},
),
],
),
),
const Divider(
indent: 15,
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.of(context).pushNamed(
Routes.routeAbout,
);
},
child: Padding(
padding: const EdgeInsets.only(
left: 15,
right: 15,
top: 5,
bottom: 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 SizedBox(
height: 30,
),
Center(
child: SizedBox(
width: MediaQuery.of(context).size.width - 40,
@ -236,16 +336,26 @@ class _OtherPageState extends ConsumerState<OtherPage> {
showCupertinoDialog(
context: context,
builder: (context) => CupertinoAlertDialog(
title: const Text("退出登录吗?"),
title: const Text("退出登录吗?"),
actions: [
CupertinoDialogAction(
child: const Text("取消"),
child: const Text(
"取消",
style: TextStyle(
color: Color(0xff999999),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
CupertinoDialogAction(
child: const Text("确定"),
child: Text(
"确定",
style: TextStyle(
color: primaryColor,
),
),
onPressed: () {
getIt<UserInfoViewModel>().updateToken("");
Navigator.of(context).pushReplacementNamed(Routes.routeLogin);

View File

@ -5,6 +5,7 @@ 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:qinglong_app/base/ui/lazy_load_state.dart';
import 'package:qinglong_app/utils/extension.dart';
@ -27,6 +28,115 @@ class ScriptDetailPage extends ConsumerStatefulWidget {
class _ScriptDetailPageState extends ConsumerState<ScriptDetailPage> with LazyLoadState<ScriptDetailPage> {
String? content;
List<Widget> actions = [];
@override
void initState() {
super.initState();
actions.addAll(
[
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.of(context).pop();
if (content == null || content!.isEmpty) {
"未获取到脚本内容,请稍候重试".toast();
return;
}
Navigator.of(context).pushNamed(
Routes.routeScriptUpdate,
arguments: {
"title": widget.title,
"path": widget.path,
"content": content,
},
).then((value) {
if (value != null && value == true) {
Navigator.of(context).pop(true);
}
});
},
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 15,
),
alignment: Alignment.center,
child: const Material(
color: Colors.transparent,
child: Text(
"编辑",
style: TextStyle(
fontSize: 16,
),
),
),
),
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () async {
Navigator.of(context).pop();
showCupertinoDialog(
context: context,
builder: (context) => CupertinoAlertDialog(
title: const Text("确认删除"),
content: const Text("确认删除该脚本吗"),
actions: [
CupertinoDialogAction(
child: const Text(
"取消",
style: TextStyle(
color: Color(0xff999999),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
CupertinoDialogAction(
child: Text(
"确定",
style: TextStyle(
color: primaryColor,
),
),
onPressed: () async {
Navigator.of(context).pop();
HttpResponse<NullResponse> result = await Api.delScript(widget.title, widget.path ?? "");
if (result.success) {
"删除成功".toast();
Navigator.of(context).pop(true);
} else {
result.message?.toast();
}
},
),
],
),
);
},
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 15,
),
alignment: Alignment.center,
child: const Material(
color: Colors.transparent,
child: Text(
"删除",
style: TextStyle(
color: Colors.red,
fontSize: 16,
),
),
),
),
),
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -36,22 +146,82 @@ class _ScriptDetailPageState extends ConsumerState<ScriptDetailPage> with LazyLo
Navigator.of(context).pop();
},
title: "脚本详情",
),
body:content == null
? const Center(
child: CupertinoActivityIndicator(),
)
: SingleChildScrollView(
child: HighlightView(
content ?? "",
language: getLanguageType(widget.title),
padding: const EdgeInsets.symmetric(
horizontal: 15,
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,
),
),
),
),
theme: ref.watch(themeProvider).themeColor.codeEditorTheme(),
tabSize: 14,
),
],
),
body: content == null
? const Center(
child: CupertinoActivityIndicator(),
)
: SingleChildScrollView(
child: HighlightView(
content ?? "",
language: getLanguageType(widget.title),
padding: const EdgeInsets.symmetric(
horizontal: 15,
),
theme: ref.watch(themeProvider).themeColor.codeEditorTheme(),
tabSize: 14,
),
),
);
}

View File

@ -0,0 +1,93 @@
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/theme.dart';
import 'package:qinglong_app/module/config/config_viewmodel.dart';
import 'package:qinglong_app/utils/extension.dart';
class ScriptEditPage extends ConsumerStatefulWidget {
final String content;
final String title;
final String path;
const ScriptEditPage(this.title, this.path, this.content, {Key? key}) : super(key: key);
@override
_ScriptEditPageState createState() => _ScriptEditPageState();
}
class _ScriptEditPageState extends ConsumerState<ScriptEditPage> {
late TextEditingController _controller;
FocusNode node = FocusNode();
@override
void initState() {
_controller = TextEditingController(text: widget.content);
super.initState();
WidgetsBinding.instance?.addPostFrameCallback(
(timeStamp) {
node.requestFocus();
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: QlAppBar(
canBack: true,
backCall: () {
Navigator.of(context).pop();
},
title: '编辑${widget.title}',
actions: [
InkWell(
onTap: () async {
HttpResponse<NullResponse> response = await Api.updateScript(widget.title, widget.path, _controller.text);
if (response.success) {
"提交成功".toast();
Navigator.of(context).pop(true);
} else {
(response.message ?? "").toast();
}
},
child: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 15,
),
child: Center(
child: Text(
"提交",
style: TextStyle(
color: Colors.white,
fontSize: 16,
),
),
),
),
)
],
),
body: Container(
padding: const EdgeInsets.only(
left: 15,
right: 15,
),
child: SingleChildScrollView(
child: TextField(
focusNode: node,
style: TextStyle(
color: ref.read(themeProvider).themeColor.descColor(),
fontSize: 14,
),
controller: _controller,
minLines: 1,
maxLines: 100,
),
),
),
);
}
}

View File

@ -6,6 +6,7 @@ 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:qinglong_app/base/ui/lazy_load_state.dart';
import 'package:qinglong_app/module/others/scripts/script_bean.dart';
import 'package:qinglong_app/utils/extension.dart';
@ -17,15 +18,9 @@ class ScriptPage extends ConsumerStatefulWidget {
_ScriptPageState createState() => _ScriptPageState();
}
class _ScriptPageState extends ConsumerState<ScriptPage> {
class _ScriptPageState extends ConsumerState<ScriptPage> with LazyLoadState<ScriptPage> {
List<ScriptBean> list = [];
@override
void initState() {
super.initState();
loadData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -51,9 +46,8 @@ class _ScriptPageState extends ConsumerState<ScriptPage> {
title: Text(
item.title ?? "",
style: TextStyle(
color: (item.disabled ?? false)
? ref.watch(themeProvider).themeColor.descColor()
: ref.watch(themeProvider).themeColor.taskTitleColor(),
color:
(item.disabled ?? false) ? ref.watch(themeProvider).themeColor.descColor() : ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 16,
),
),
@ -66,14 +60,18 @@ class _ScriptPageState extends ConsumerState<ScriptPage> {
"title": e.title,
"path": e.parent,
},
);
).then((value) {
if (value != null && value == true) {
loadData();
}
});
},
title: Text(
e.title ?? "",
style: TextStyle(
color: (item.disabled ?? false)
? ref.watch(themeProvider).themeColor.descColor()
: ref.watch(themeProvider).themeColor.taskTitleColor(),
: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 14,
),
),
@ -88,14 +86,19 @@ class _ScriptPageState extends ConsumerState<ScriptPage> {
"title": item.title,
"path": "",
},
).then(
(value) {
if (value != null && value == true) {
loadData();
}
},
);
},
title: Text(
item.title ?? "",
style: TextStyle(
color: (item.disabled ?? false)
? ref.watch(themeProvider).themeColor.descColor()
: ref.watch(themeProvider).themeColor.taskTitleColor(),
color:
(item.disabled ?? false) ? ref.watch(themeProvider).themeColor.descColor() : ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 16,
),
),
@ -118,4 +121,9 @@ class _ScriptPageState extends ConsumerState<ScriptPage> {
response.message?.toast();
}
}
@override
void onLazyLoad() {
loadData();
}
}

View File

@ -46,7 +46,7 @@ class _TaskLogPageState extends ConsumerState<TaskLogPage> with LazyLoadState<Ta
title: Text(
item.name ?? "",
style: TextStyle(
color: ref.watch(themeProvider).themeColor.taskTitleColor(),
color: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 16,
),
),
@ -58,7 +58,7 @@ class _TaskLogPageState extends ConsumerState<TaskLogPage> with LazyLoadState<Ta
title: Text(
e,
style: TextStyle(
color: ref.watch(themeProvider).themeColor.taskTitleColor(),
color: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 14,
),
),
@ -72,7 +72,7 @@ class _TaskLogPageState extends ConsumerState<TaskLogPage> with LazyLoadState<Ta
title: Text(
item.name ?? "",
style: TextStyle(
color: ref.watch(themeProvider).themeColor.taskTitleColor(),
color: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 16,
),
),

View File

@ -0,0 +1,207 @@
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/userinfo_viewmodel.dart';
import 'package:qinglong_app/module/task/task_bean.dart';
import 'package:qinglong_app/module/task/task_viewmodel.dart';
import 'package:qinglong_app/utils/extension.dart';
import '../../main.dart';
class UpdatePasswordPage extends ConsumerStatefulWidget {
const UpdatePasswordPage({
Key? key,
}) : super(key: key);
@override
ConsumerState<UpdatePasswordPage> createState() => _UpdatePasswordPageState();
}
class _UpdatePasswordPageState extends ConsumerState<UpdatePasswordPage> {
final TextEditingController _nameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final TextEditingController _passwordAgainController = TextEditingController();
FocusNode focusNode = FocusNode();
@override
void initState() {
super.initState();
WidgetsBinding.instance?.addPostFrameCallback(
(timeStamp) {
focusNode.requestFocus();
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: QlAppBar(
canBack: true,
backCall: () {
Navigator.of(context).pop();
},
title: "修改用户名密码",
actions: [
InkWell(
onTap: () {
submit();
},
child: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 15,
),
child: Center(
child: Text(
"提交",
style: TextStyle(
color: Colors.white,
fontSize: 16,
),
),
),
),
)
],
),
body: 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(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
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(
obscureText: true,
controller: _passwordController,
maxLines: 1,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
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(
obscureText: true,
maxLines: 1,
controller: _passwordAgainController,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "再次输入新密码",
),
autofocus: false,
),
],
),
),
],
),
);
}
void submit() async {
if (_nameController.text.isEmpty) {
"用户名不能为空".toast();
return;
}
if (_passwordController.text.isEmpty || _passwordAgainController.text.isEmpty) {
"密码不能为空".toast();
return;
}
if (_passwordAgainController.text != _passwordController.text) {
"两次输入的密码不一致".toast();
return;
}
commitReal();
}
void commitReal() async {
String name = _nameController.text;
String password = _passwordController.text;
HttpResponse<NullResponse> response = await Api.updatePassword(name, password);
if (response.success) {
"更新成功".toast();
if (!getIt<UserInfoViewModel>().useSecretLogined) {
getIt<UserInfoViewModel>().updateUserName(name, password);
}
Navigator.of(context).pushNamedAndRemoveUntil(Routes.routeLogin, (route) => false);
} else {
response.message.toast();
}
}
}

View File

@ -4,7 +4,6 @@ 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/module/task/task_bean.dart';
import 'package:qinglong_app/module/task/task_detail/task_detail_bean.dart';
import 'package:qinglong_app/module/task/task_viewmodel.dart';
import 'package:qinglong_app/utils/extension.dart';
@ -24,6 +23,8 @@ class _AddTaskPageState extends ConsumerState<AddTaskPage> {
final TextEditingController _commandController = TextEditingController();
final TextEditingController _cronController = TextEditingController();
FocusNode focusNode = FocusNode();
@override
void initState() {
super.initState();
@ -35,6 +36,11 @@ class _AddTaskPageState extends ConsumerState<AddTaskPage> {
} else {
taskBean = TaskBean();
}
WidgetsBinding.instance?.addPostFrameCallback(
(timeStamp) {
focusNode.requestFocus();
},
);
}
@override
@ -68,112 +74,111 @@ class _AddTaskPageState extends ConsumerState<AddTaskPage> {
)
],
),
body: Container(
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: 10,
),
const Text(
"名称:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(
height: 10,
),
TextField(
controller: _nameController,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "请输入名称",
),
autofocus: false,
),
],
),
body: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 10,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 15,
),
const Text(
"名称:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
const Text(
"命令:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
TextField(
focusNode: focusNode,
controller: _nameController,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "请输入名称",
),
const SizedBox(
height: 10,
),
TextField(
controller: _commandController,
maxLines: 4,
minLines: 1,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "请输入命令",
),
autofocus: false,
),
],
),
autofocus: false,
),
],
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 10,
),
const Text(
"定时:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(
height: 10,
),
TextField(
controller: _cronController,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
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: _commandController,
maxLines: 4,
minLines: 1,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
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: _cronController,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "请输入定时",
),
autofocus: false,
),
const SizedBox(
height: 5,
),
const Text(
"定时的cron不校验是否正确",
style: TextStyle(
fontSize: 12,
),
),
],
),
),
],
),
);
}
@ -192,15 +197,19 @@ class _AddTaskPageState extends ConsumerState<AddTaskPage> {
return;
}
commitReal();
}
void commitReal() async {
taskBean.name = _nameController.text;
taskBean.command = _commandController.text;
taskBean.schedule = _cronController.text;
HttpResponse<TaskDetailBean> response = await Api.addTask(
HttpResponse<NullResponse> response = await Api.addTask(
_nameController.text, _commandController.text, _cronController.text,
id: taskBean.sId);
if (response.success) {
"操作成功".toast();
(widget.taskBean?.sId == null) ? "新增成功" : "修改成功".toast();
ref.read(taskProvider).updateBean(taskBean);
Navigator.of(context).pop();
} else {

View File

@ -1,67 +0,0 @@
import 'package:json_conversion_annotation/json_conversion_annotation.dart';
@JsonConversion()
class TaskDetailBean {
String? name;
String? command;
String? schedule;
bool? saved;
String? sId;
int? created;
int? status;
String? timestamp;
int? isSystem;
int? isDisabled;
String? logPath;
int? isPinned;
TaskDetailBean(
{this.name,
this.command,
this.schedule,
this.saved,
this.sId,
this.created,
this.status,
this.timestamp,
this.isSystem,
this.isDisabled,
this.logPath,
this.isPinned});
TaskDetailBean.fromJson(Map<String, dynamic> json) {
name = json['name'];
command = json['command'];
schedule = json['schedule'];
saved = json['saved'];
sId = json['_id'];
created = json['created'];
status = json['status'];
timestamp = json['timestamp'];
isSystem = json['isSystem'];
isDisabled = json['isDisabled'];
logPath = json['log_path'];
isPinned = json['isPinned'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['name'] = this.name;
data['command'] = this.command;
data['schedule'] = this.schedule;
data['saved'] = this.saved;
data['_id'] = this.sId;
data['created'] = this.created;
data['status'] = this.status;
data['timestamp'] = this.timestamp;
data['isSystem'] = this.isSystem;
data['isDisabled'] = this.isDisabled;
data['log_path'] = this.logPath;
data['isPinned'] = this.isPinned;
return data;
}
static TaskDetailBean jsonConversion(Map<String, dynamic> json) {
return TaskDetailBean.fromJson(json);
}
}

View File

@ -1,36 +1,504 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qinglong_app/base/base_state_widget.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:qinglong_app/module/task/intime_log/intime_log_page.dart';
import 'package:qinglong_app/module/task/task_bean.dart';
import 'package:qinglong_app/module/task/task_detail/task_detail_viewmodel.dart';
import 'package:qinglong_app/utils/utils.dart';
import '../task_viewmodel.dart';
class TaskDetailPage extends ConsumerStatefulWidget {
final TaskBean taskBean;
final bool hideAppbar;
const TaskDetailPage(this.taskBean, {Key? key}) : super(key: key);
const TaskDetailPage(this.taskBean, {Key? key, this.hideAppbar = false}) : super(key: key);
@override
_TaskDetailPageState createState() => _TaskDetailPageState();
}
class _TaskDetailPageState extends ConsumerState<TaskDetailPage> {
List<Widget> actions = [];
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
Widget body = Material(
color: Colors.transparent,
child: Container(
color: Theme.of(context).scaffoldBackgroundColor,
child: SingleChildScrollView(
child: Column(
children: [
Container(
margin: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 15,
),
padding: const EdgeInsets.only(
top: 10,
bottom: 10,
),
decoration: BoxDecoration(
color: ref.watch(themeProvider).themeColor.settingBgColor(),
borderRadius: BorderRadius.circular(15),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TaskDetailCell(
title: "名称",
desc: widget.taskBean.name ?? "",
),
TaskDetailCell(
title: "ID",
desc: widget.taskBean.sId ?? "",
),
TaskDetailCell(
title: "任务",
desc: widget.taskBean.command ?? "",
),
TaskDetailCell(
title: "创建时间",
desc: Utils.formatMessageTime(widget.taskBean.created ?? 0),
),
TaskDetailCell(
title: "更新时间",
desc: Utils.formatGMTTime(widget.taskBean.timestamp ?? ""),
),
TaskDetailCell(
title: "任务定时",
desc: widget.taskBean.schedule ?? "",
),
TaskDetailCell(
title: "最后运行时间",
desc: Utils.formatMessageTime(widget.taskBean.lastExecutionTime ?? 0),
),
TaskDetailCell(
title: "最后运行时长",
desc: widget.taskBean.lastRunningTime == null ? "-" : "${widget.taskBean.lastRunningTime ?? "-"}",
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
showLog();
},
child: TaskDetailCell(
title: "日志路径",
desc: widget.taskBean.logPath ?? "-",
taped: () {
showLog();
},
),
),
TaskDetailCell(
title: "运行状态",
desc: widget.taskBean.status == 0 ? "正在运行" : "空闲",
),
TaskDetailCell(
title: "脚本状态",
desc: widget.taskBean.isDisabled == 1 ? "已禁用" : "已启用",
),
TaskDetailCell(
title: "是否置顶",
desc: widget.taskBean.isPinned == 1 ? "已置顶" : "未置顶",
hideDivide: true,
),
],
),
),
widget.hideAppbar
? const SizedBox.shrink()
: SizedBox(
width: MediaQuery.of(context).size.width - 80,
child: CupertinoButton(
padding: const EdgeInsets.symmetric(
vertical: 5,
),
color: Colors.red,
child: const Text(
"删 除",
style: TextStyle(
fontSize: 16,
),
),
onPressed: () {
delTask(context, ref);
}),
),
const SizedBox(
height: 15,
),
],
),
),
),
);
if (widget.hideAppbar) {
return body;
}
actions.clear();
actions.addAll(
[
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () async{
Navigator.of(context).pop();
await startCron(context, ref);
setState(() {
});
},
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 15,
),
alignment: Alignment.center,
child: Material(
color: Colors.transparent,
child: Text(
widget.taskBean.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,
),
),
),
),
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed(Routes.routeAddTask, arguments: widget.taskBean);
},
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 15,
),
alignment: Alignment.center,
child: const Material(
color: Colors.transparent,
child: Text(
"编辑",
style: TextStyle(
fontSize: 16,
),
),
),
),
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.of(context).pop();
pinTask();
},
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 15,
),
color: Colors.transparent,
alignment: Alignment.center,
child: Material(
color: Colors.transparent,
child: Text(
widget.taskBean.isPinned! == 0 ? "置顶" : "取消置顶",
style: const TextStyle(
fontSize: 16,
),
),
),
),
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.of(context).pop();
enableTask();
},
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 15,
),
alignment: Alignment.center,
child: Material(
color: Colors.transparent,
child: Text(
widget.taskBean.isDisabled! == 0 ? "禁用" : "启用",
style: const TextStyle(
color: Colors.red,
fontSize: 16,
),
),
),
),
),
],
);
return Scaffold(
appBar: QlAppBar(
canBack: true,
backCall: () {
Navigator.of(context).pop();
},
title: widget.taskBean.name ?? "",
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: BaseStateWidget<TaskDetailViewModel>(
model: taskDetailProvider,
builder: (WidgetRef context, TaskDetailViewModel value, Widget? child) {
return Container();
},
onReady: (model) {
model.loadDetail(widget.taskBean.sId!);
},
body: body,
);
}
startCron(BuildContext context, WidgetRef ref) async {
await ref.read(taskProvider).runCrons(widget.taskBean.sId!);
setState(() {});
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
showLog();
});
}
stopCron(BuildContext context, WidgetRef ref) async {
await ref.read(taskProvider).stopCrons(widget.taskBean.sId!);
setState(() {});
}
void enableTask() async {
await ref.read(taskProvider).enableTask(widget.taskBean.sId!, widget.taskBean.isDisabled!);
setState(() {});
}
void pinTask() async {
await ref.read(taskProvider).pinTask(widget.taskBean.sId!, widget.taskBean.isPinned!);
setState(() {});
}
void delTask(BuildContext context, WidgetRef ref) {
showCupertinoDialog(
context: context,
builder: (context) => CupertinoAlertDialog(
title: const Text("确认删除"),
content: Text("确认删除定时任务 ${widget.taskBean.name ?? ""}"),
actions: [
CupertinoDialogAction(
child: const Text(
"取消",
style: TextStyle(
color: Color(0xff999999),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
CupertinoDialogAction(
child: Text(
"确定",
style: TextStyle(
color: primaryColor,
),
),
onPressed: () async {
Navigator.of(context).pop();
await ref.read(taskProvider).delCron(widget.taskBean.sId!);
Navigator.of(context).pop();
},
),
],
),
);
}
void showLog() {
showCupertinoDialog(
builder: (BuildContext context) {
return CupertinoAlertDialog(
title: Text(
"${widget.taskBean.name}运行日志",
maxLines: 1,
style: const TextStyle(overflow: TextOverflow.ellipsis),
),
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);
}
}
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,
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

@ -1,29 +0,0 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qinglong_app/base/base_viewmodel.dart';
import 'package:qinglong_app/base/http/api.dart';
import 'package:qinglong_app/base/http/http.dart';
import 'package:qinglong_app/module/task/task_detail/task_detail_bean.dart';
var taskDetailProvider =
AutoDisposeChangeNotifierProvider<TaskDetailViewModel>((ref) {
return TaskDetailViewModel();
});
class TaskDetailViewModel extends BaseViewModel {
TaskDetailBean? bean;
TaskDetailViewModel();
Future<void> loadDetail(String id) async {
loading(notify: true);
HttpResponse<TaskDetailBean> response = await Api.taskDetail(id);
if (response.success) {
bean = response.bean;
success();
} else {
failed(response.message, notify: true);
}
}
}

View File

@ -9,8 +9,10 @@ import 'package:qinglong_app/base/theme.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/menu.dart';
import 'package:qinglong_app/base/ui/ql_context_menu.dart';
import 'package:qinglong_app/module/task/intime_log/intime_log_page.dart';
import 'package:qinglong_app/module/task/task_bean.dart';
import 'package:qinglong_app/module/task/task_detail/task_detail_page.dart';
import 'package:qinglong_app/module/task/task_viewmodel.dart';
import 'package:qinglong_app/utils/utils.dart';
@ -22,7 +24,6 @@ class TaskPage extends StatefulWidget {
}
class _TaskPageState extends State<TaskPage> {
String? _searchKey;
final TextEditingController _searchController = TextEditingController();
@override
@ -101,10 +102,9 @@ class _TaskPageState extends State<TaskPage> {
TaskBean item = list[index];
if (_searchController.text.isEmpty ||
(item.name?.contains(_searchController.text) ?? false) ||
(item.command?.contains(_searchController.text) ?? false) ||
(item.schedule?.contains(_searchController.text) ??
false)) {
(item.name?.toLowerCase().contains(_searchController.text.toLowerCase()) ?? false) ||
(item.command?.toLowerCase().contains(_searchController.text.toLowerCase()) ?? false) ||
(item.schedule?.contains(_searchController.text.toLowerCase()) ?? false)) {
return TaskItemCell(item, ref);
} else {
return const SizedBox.shrink();
@ -168,15 +168,14 @@ class TaskItemCell extends StatelessWidget {
Widget build(BuildContext context) {
return ColoredBox(
color: ref.watch(themeProvider).themeColor.settingBgColor(),
child: CupertinoContextMenu(
child: QlCupertinoContextMenu(
bean: bean,
actions: [
QLCupertinoContextMenuAction(
child: Text(
bean.status! == 1 ? "运行" : "停止运行",
),
trailingIcon: bean.status! == 1
? CupertinoIcons.memories
: CupertinoIcons.stop_circle,
trailingIcon: bean.status! == 1 ? CupertinoIcons.memories : CupertinoIcons.stop_circle,
onPressed: () {
Navigator.of(context).pop();
startCron(context, ref);
@ -194,8 +193,7 @@ class TaskItemCell extends StatelessWidget {
child: const Text("编辑"),
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context)
.pushNamed(Routes.routeAddTask, arguments: bean);
Navigator.of(context).pushNamed(Routes.routeAddTask, arguments: bean);
},
trailingIcon: CupertinoIcons.pencil_outline,
),
@ -205,9 +203,7 @@ class TaskItemCell extends StatelessWidget {
Navigator.of(context).pop();
pinTask();
},
trailingIcon: bean.isPinned! == 0
? CupertinoIcons.pin
: CupertinoIcons.pin_slash,
trailingIcon: bean.isPinned! == 0 ? CupertinoIcons.pin : CupertinoIcons.pin_slash,
),
QLCupertinoContextMenuAction(
child: Text(bean.isDisabled! == 0 ? "禁用" : "启用"),
@ -216,9 +212,7 @@ class TaskItemCell extends StatelessWidget {
enableTask();
},
isDestructiveAction: true,
trailingIcon: bean.isDisabled! == 0
? Icons.dnd_forwardslash
: Icons.check_circle_outline_sharp,
trailingIcon: bean.isDisabled! == 0 ? Icons.dnd_forwardslash : Icons.check_circle_outline_sharp,
),
QLCupertinoContextMenuAction(
child: const Text("删除"),
@ -231,34 +225,24 @@ class TaskItemCell extends StatelessWidget {
),
],
previewBuilder: (context, anima, child) {
return IntrinsicWidth(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Material(
color: Colors.transparent,
child: Text(
bean.name ?? "",
maxLines: 1,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color:
ref.watch(themeProvider).themeColor.taskTitleColor(),
fontSize: 18,
),
),
bool isDark = ref.watch(themeProvider).darkMode;
return ClipRRect(
borderRadius: BorderRadius.circular(15),
child: Container(
color: isDark ? const Color(0xff333333) : Theme.of(context).scaffoldBackgroundColor,
padding: EdgeInsets.only(
left: 5,
right: 5,
top: 5,
bottom: isDark ? 5 : 20,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(15),
child: TaskDetailPage(
bean,
hideAppbar: true,
),
const SizedBox(
width: 5,
),
bean.isDisabled == 1
? const Icon(
Icons.dnd_forwardslash,
size: 16,
color: Colors.red,
)
: const SizedBox.shrink(),
],
),
),
);
},
@ -267,9 +251,8 @@ class TaskItemCell extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
color: bean.isPinned == 1
? ref.watch(themeProvider).themeColor.pinColor()
: Colors.transparent,
width: MediaQuery.of(context).size.width,
color: bean.isPinned == 1 ? ref.watch(themeProvider).themeColor.pinColor() : Colors.transparent,
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 8,
@ -289,10 +272,7 @@ class TaskItemCell extends StatelessWidget {
maxLines: 1,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color: ref
.watch(themeProvider)
.themeColor
.taskTitleColor(),
color: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 18,
),
),
@ -313,16 +293,11 @@ class TaskItemCell extends StatelessWidget {
Material(
color: Colors.transparent,
child: Text(
(bean.lastExecutionTime == null ||
bean.lastExecutionTime == 0)
? "-"
: Utils.formatMessageTime(
bean.lastExecutionTime!),
(bean.lastExecutionTime == null || bean.lastExecutionTime == 0) ? "-" : Utils.formatMessageTime(bean.lastExecutionTime!),
maxLines: 1,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color:
ref.watch(themeProvider).themeColor.descColor(),
color: ref.watch(themeProvider).themeColor.descColor(),
fontSize: 12,
),
),
@ -333,6 +308,7 @@ class TaskItemCell extends StatelessWidget {
height: 8,
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Material(
color: Colors.transparent,
@ -341,8 +317,7 @@ class TaskItemCell extends StatelessWidget {
maxLines: 1,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color:
ref.watch(themeProvider).themeColor.descColor(),
color: ref.watch(themeProvider).themeColor.descColor(),
fontSize: 12,
),
),
@ -359,6 +334,22 @@ class TaskItemCell extends StatelessWidget {
: const SizedBox.shrink(),
],
),
const SizedBox(
height: 8,
),
Material(
color: Colors.transparent,
child: Text(
bean.command ?? "",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color: ref.watch(themeProvider).themeColor.descColor(),
fontSize: 12,
),
),
),
],
),
),
@ -372,54 +363,15 @@ class TaskItemCell extends StatelessWidget {
);
}
startCron(BuildContext context, WidgetRef ref) {
showCupertinoDialog(
context: context,
builder: (context) => CupertinoAlertDialog(
title: const Text("确认运行"),
content: Text("确认运行定时任务 ${bean.name ?? ""}"),
actions: [
CupertinoDialogAction(
child: const Text("取消"),
onPressed: () {
Navigator.of(context).pop();
},
),
CupertinoDialogAction(
child: const Text("确定"),
onPressed: () {
Navigator.of(context).pop();
ref.read(taskProvider).runCrons(bean.sId!);
},
),
],
),
);
startCron(BuildContext context, WidgetRef ref) async {
await ref.read(taskProvider).runCrons(bean.sId!);
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
logCron(context, ref);
});
}
stopCron(BuildContext context, WidgetRef ref) {
showCupertinoDialog(
context: context,
builder: (context) => CupertinoAlertDialog(
title: const Text("确认停止"),
content: Text("确认停止定时任务 ${bean.name ?? ""}"),
actions: [
CupertinoDialogAction(
child: const Text("取消"),
onPressed: () {
Navigator.of(context).pop();
},
),
CupertinoDialogAction(
child: const Text("确定"),
onPressed: () {
Navigator.of(context).pop();
ref.read(taskProvider).stopCrons(bean.sId!);
},
),
],
),
);
ref.read(taskProvider).stopCrons(bean.sId!);
}
logCron(BuildContext context, WidgetRef ref) {
@ -441,6 +393,7 @@ class TaskItemCell extends StatelessWidget {
),
onPressed: () {
Navigator.of(context).pop();
ref.read(taskProvider).loadData(false);
},
),
],
@ -466,13 +419,23 @@ class TaskItemCell extends StatelessWidget {
content: Text("确认删除定时任务 ${bean.name ?? ""}"),
actions: [
CupertinoDialogAction(
child: const Text("取消"),
child: const Text(
"取消",
style: TextStyle(
color: Color(0xff999999),
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
CupertinoDialogAction(
child: const Text("确定"),
child: Text(
"确定",
style: TextStyle(
color: primaryColor,
),
),
onPressed: () {
Navigator.of(context).pop();
ref.read(taskProvider).delCron(bean.sId!);

View File

@ -3,6 +3,7 @@ import 'package:qinglong_app/base/base_viewmodel.dart';
import 'package:qinglong_app/base/http/api.dart';
import 'package:qinglong_app/base/http/http.dart';
import 'package:qinglong_app/module/task/task_bean.dart';
import 'package:qinglong_app/utils/extension.dart';
var taskProvider = ChangeNotifierProvider((ref) => TaskViewModel());
@ -30,10 +31,6 @@ class TaskViewModel extends BaseViewModel {
}
void sortList() {
list.sort((a, b) {
return b.created!.compareTo(a.created!);
});
for (int i = 0; i < list.length; i++) {
if (list[i].isPinned == 1) {
final TaskBean item = list.removeAt(i);
@ -71,6 +68,7 @@ class TaskViewModel extends BaseViewModel {
HttpResponse<NullResponse> result = await Api.delTask(id);
if (result.success) {
list.removeWhere((element) => element.sId == id);
"删除成功".toast();
notifyListeners();
} else {
failToast(result.message, notify: true);
@ -89,11 +87,12 @@ class TaskViewModel extends BaseViewModel {
notifyListeners();
}
void pinTask(String sId, int isPinned) async {
Future<void> pinTask(String sId, int isPinned) async {
if (isPinned == 1) {
HttpResponse<NullResponse> response = await Api.unpinTask(sId);
if (response.success) {
"取消置顶成功".toast();
list.firstWhere((element) => element.sId == sId).isPinned = 0;
sortList();
success();
@ -104,6 +103,7 @@ class TaskViewModel extends BaseViewModel {
HttpResponse<NullResponse> response = await Api.pinTask(sId);
if (response.success) {
"置顶成功".toast();
list.firstWhere((element) => element.sId == sId).isPinned = 1;
sortList();
success();
@ -113,11 +113,12 @@ class TaskViewModel extends BaseViewModel {
}
}
void enableTask(String sId, int isDisabled) async {
Future<void> enableTask(String sId, int isDisabled) async {
if (isDisabled == 0) {
HttpResponse<NullResponse> response = await Api.disableTask(sId);
if (response.success) {
"禁用成功".toast();
list.firstWhere((element) => element.sId == sId).isDisabled = 1;
sortList();
success();
@ -128,6 +129,7 @@ class TaskViewModel extends BaseViewModel {
HttpResponse<NullResponse> response = await Api.enableTask(sId);
if (response.success) {
"启用成功".toast();
list.firstWhere((element) => element.sId == sId).isDisabled = 0;
sortList();
success();

View File

@ -1,16 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:qinglong_app/base/routes.dart';
class QlNavigatorObserver extends NavigatorObserver {
bool isInLoginPage = false;
@override
void didPush(Route route, Route? previousRoute) {
super.didPush(route, previousRoute);
if (Routes.routeLogin == route.settings.name) {
isInLoginPage = true;
} else {
isInLoginPage = false;
}
}
}

View File

@ -4,11 +4,7 @@ import 'package:fluttertoast/fluttertoast.dart';
extension ContextExt on BuildContext {
T read<T>(ProviderBase<T> provider) {
return (this as WidgetRef).read<T>(provider);
}
T watch<T>(ProviderBase<T> provider) {
return (this as WidgetRef).watch<T>(provider);
return ProviderScope.containerOf(this, listen: false).read(provider);
}
}

View File

@ -6,7 +6,74 @@ class Utils {
FocusScope.of(context).requestFocus(FocusNode());
}
static String formatGMTTime(String gmtTime) {
// "Wed Jan 12 2022 20:33:39 GMT+0800 (中国标准时间)"
try {
if (gmtTime.isEmpty) return "-";
List<String> splitedStr = gmtTime.split(" ");
int year = int.parse(splitedStr[3]);
String time = splitedStr[4];
List<String> splitedTime = time.split(":");
int hour = int.parse(splitedTime[0]);
int min = int.parse(splitedTime[1]);
int second = int.parse(splitedTime[2]);
int day = int.parse(splitedStr[2]);
String month = "01";
switch (splitedStr[1]) {
case "Jan":
month = "01";
break;
case "Feb":
month = "02";
break;
case "Mar":
month = "03";
break;
case "Apr":
month = "04";
break;
case "May":
month = "05";
break;
case "Jun":
month = "06";
break;
case "Jul":
month = "07";
break;
case "Aug":
month = "08";
break;
case "Sep":
month = "09";
break;
case "Oct":
month = "10";
break;
case "Nov":
month = "11";
break;
case "Dec":
month = "12";
break;
}
var date = DateTime(year, int.parse(month), day, hour, min, second);
return formatMessageTime(date.millisecondsSinceEpoch);
} catch (e) {
return "-";
}
}
static String formatMessageTime(int time) {
if(time == 0){
return "-";
}
DateTime current = DateTime.now();
DateTime chatTime;
if (time.toString().length == 10) {

View File

@ -239,6 +239,13 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.0"
flip_card:
dependency: "direct main"
description:
name: flip_card
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.6.0"
flutter:
dependency: "direct main"
description: flutter
@ -345,6 +352,13 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.0"
http:
dependency: transitive
description:
name: http
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.13.4"
http_multi_server:
dependency: transitive
description:
@ -450,6 +464,13 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.1"
move_to_background:
dependency: "direct main"
description:
name: move_to_background
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.2"
package_config:
dependency: transitive
description:
@ -457,6 +478,48 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.2"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.0"
package_info_plus_linux:
dependency: transitive
description:
name: package_info_plus_linux
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.3"
package_info_plus_macos:
dependency: transitive
description:
name: package_info_plus_macos
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.0"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.2"
package_info_plus_web:
dependency: transitive
description:
name: package_info_plus_web
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.4"
package_info_plus_windows:
dependency: transitive
description:
name: package_info_plus_windows
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.4"
path:
dependency: transitive
description:
@ -714,6 +777,62 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.4"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.0.18"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.0.14"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.0.14"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.3"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.2"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.5"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.6"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.2"
vector_math:
dependency: transitive
description:

View File

@ -1,7 +1,7 @@
name: qinglong_app
description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
version: 1.0.2+3
environment:
sdk: ">=2.15.1 <3.0.0"
@ -26,6 +26,10 @@ dependencies:
flutter_highlight: ^0.7.0
drag_and_drop_lists: ^0.3.2+2
fluttertoast: ^8.0.8
move_to_background: ^1.0.2
flip_card: ^0.6.0
package_info_plus: ^1.3.0
url_launcher: ^6.0.18
dev_dependencies:
flutter_native_splash: ^1.3.3

View File

@ -5,13 +5,9 @@
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:qinglong_app/base/http/http.dart';
import 'package:qinglong_app/base/userinfo_viewmodel.dart';
void main() {
String time = "Wed Jan 12 2022 20:33:39 GMT+0800 (中国标准时间)";
import 'package:qinglong_app/main.dart';
import 'package:qinglong_app/module/login/login_bean.dart';
import 'package:qinglong_app/module/login/login_viewmodel.dart';
void main() {}
var result = DateTime.tryParse(time);
print(result);
}