新增订阅管理

新增环境变量导入和导出
This commit is contained in:
3343780376
2022-06-23 16:30:39 +08:00
parent 7e3a63169e
commit 1cd3724805
15 changed files with 2604 additions and 298 deletions

View File

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

View File

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

View File

@@ -130,6 +130,38 @@ class Url {
: "/api/envs/$envId/move";
}
// 运行订阅
static get runSubscriptions => getIt<UserInfoViewModel>().useSecretLogined
? "/open/subscriptions/run"
: "/api/subscriptions/run";
// 停止订阅
static get stopSubscriptions => getIt<UserInfoViewModel>().useSecretLogined
? "/open/subscriptions/stop"
: "/api/subscriptions/stop";
// 启用订阅
static get enableSubscriptions => getIt<UserInfoViewModel>().useSecretLogined
? "/open/subscriptions/enable"
: "/api/subscriptions/enable";
// 禁用订阅
static get disableSubscriptions => getIt<UserInfoViewModel>().useSecretLogined
? "/open/subscriptions/disable"
: "/api/subscriptions/disable";
// GET 获取订阅 POST 提交订阅 PUT 修改订阅 DELETE 删除订阅
static get subscriptions => getIt<UserInfoViewModel>().useSecretLogined
? "/open/subscriptions"
: "/api/subscriptions";
// 获取订阅日志
static subtimeLog(int cronId) {
return getIt<UserInfoViewModel>().useSecretLogined
? "/open/subscriptions/$cronId/log"
: "/api/subscriptions/$cronId/log";
}
static bool inWhiteList(String path) {
if (path == login ||
path == loginByClientId ||

View File

@@ -8,12 +8,18 @@ import 'package:qinglong_app/module/env/env_detail_page.dart';
import 'package:qinglong_app/module/home/home_page.dart';
import 'package:qinglong_app/module/login/login_page.dart';
import 'package:qinglong_app/module/others/about_page.dart';
import 'package:qinglong_app/module/others/backup/backup_page.dart';
import 'package:qinglong_app/module/others/dependencies/add_dependency_page.dart';
import 'package:qinglong_app/module/others/dependencies/dependency_page.dart';
import 'package:qinglong_app/module/others/login_log/login_log_page.dart';
import 'package:qinglong_app/module/others/scripts/acript_add_page.dart';
import 'package:qinglong_app/module/others/scripts/script_detail_page.dart';
import 'package:qinglong_app/module/others/scripts/script_edit_page.dart';
import 'package:qinglong_app/module/others/scripts/script_page.dart';
import 'package:qinglong_app/module/others/subscription/subscription_add_page.dart';
import 'package:qinglong_app/module/others/subscription/subscription_bean.dart';
import 'package:qinglong_app/module/others/subscription/subscription_detail_page.dart';
import 'package:qinglong_app/module/others/subscription/subscription_page.dart';
import 'package:qinglong_app/module/others/task_log/task_log_detail_page.dart';
import 'package:qinglong_app/module/others/task_log/task_log_page.dart';
import 'package:qinglong_app/module/others/theme_page.dart';
@@ -43,6 +49,12 @@ class Routes {
static const String routeAbout = "/about";
static const String routeTheme = "/theme";
static const String routeChangeAccount = "/changeAccount";
static const String routerAddScript = "/script/add";
static const String routerSubscription = "/subscription";
static const String routerSubscriptionDetail = "/subscription/detail";
static const String routerSubscriptionAdd = "/subscription/add";
static const String routerBackup = "/backup";
static Route<dynamic>? generateRoute(RouteSettings settings) {
switch (settings.name) {
@@ -151,7 +163,35 @@ class Routes {
(settings.arguments as Map)["content"],
),
);
case routeScriptAdd:
return CupertinoPageRoute(
builder: (context) => const ScriptAddPage(),
);
case routerSubscription:
return CupertinoPageRoute(
builder: (context) => const SubscriptionPage(),
);
case routerSubscriptionDetail:
return CupertinoPageRoute(
builder: (context) => SubscriptionDetailPage(
settings.arguments as Subscription,
),
);
case routerSubscriptionAdd:
if (settings.arguments != null) {
return MaterialPageRoute(
builder: (context) => SubscriptionAddPage(
settings.arguments as Subscription,
));
} else {
return MaterialPageRoute(builder: (context) => const SubscriptionAddPage(null));
}
case routerBackup:
return CupertinoPageRoute(
builder: (context) => const BackUpPage(),
);
}
return null;
}

View File

@@ -89,7 +89,7 @@ enum _ContextMenuLocation {
/// See also:
///
/// * [Apple's HIG for Context Menus](https://developer.apple.com/design/human-interface-guidelines/ios/controls/context-menus/)
class QlCupertinoContextMenu extends StatefulWidget {
class QlCupertinoContextMenu<T> extends StatefulWidget {
/// Create a context menu.
///
/// [actions] is required and cannot be null or empty.
@@ -105,7 +105,7 @@ class QlCupertinoContextMenu extends StatefulWidget {
assert(child != null),
super(key: key);
final TaskBean bean;
final T bean;
/// The widget that can be "opened" with the [QlCupertinoContextMenu].
///
@@ -322,8 +322,14 @@ class _QlCupertinoContextMenuState extends State<QlCupertinoContextMenu>
_openController.reverse();
} else {
if (_openController.isDismissed) {
if (widget.bean is TaskBean){
Navigator.of(context)
.pushNamed(Routes.routeTaskDetail, arguments: widget.bean);
}else{
Navigator.of(context)
.pushNamed(Routes.routerSubscriptionDetail, arguments: widget.bean);
}
}
}
}

View File

@@ -8,10 +8,13 @@ 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/subscription/subscription_bean.dart';
import 'package:qinglong_app/module/others/task_log/task_log_bean.dart';
import 'package:qinglong_app/module/task/task_bean.dart';
class JsonConversion$Json {
static M fromJson<M>(dynamic json) {
if (json is List) {
return _getListChildType<M>(json);
@@ -21,6 +24,7 @@ class JsonConversion$Json {
}
static M _fromJsonSingle<M>(dynamic json) {
String type = M.toString();
if(type == (ConfigBean).toString()){
@@ -63,13 +67,16 @@ class JsonConversion$Json {
return TaskBean.jsonConversion(json) as M;
}
if (type == (Subscription).toString()){
return Subscription.fromJson(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;
return data.map<ConfigBean>((e) => ConfigBean.jsonConversion(e)).toList() as M;
}
if(<EnvBean>[] is M){
@@ -77,46 +84,39 @@ class JsonConversion$Json {
}
if(<SystemBean>[] is M){
return data.map<SystemBean>((e) => SystemBean.jsonConversion(e)).toList()
as M;
return data.map<SystemBean>((e) => SystemBean.jsonConversion(e)).toList() as M;
}
if(<LoginBean>[] is M){
return data.map<LoginBean>((e) => LoginBean.jsonConversion(e)).toList()
as M;
return data.map<LoginBean>((e) => LoginBean.jsonConversion(e)).toList() as M;
}
if(<UserBean>[] is M){
return data.map<UserBean>((e) => UserBean.jsonConversion(e)).toList()
as M;
return data.map<UserBean>((e) => UserBean.jsonConversion(e)).toList() as M;
}
if(<DependencyBean>[] is M){
return data
.map<DependencyBean>((e) => DependencyBean.jsonConversion(e))
.toList() as M;
return data.map<DependencyBean>((e) => DependencyBean.jsonConversion(e)).toList() as M;
}
if(<LoginLogBean>[] is M){
return data
.map<LoginLogBean>((e) => LoginLogBean.jsonConversion(e))
.toList() as M;
return data.map<LoginLogBean>((e) => LoginLogBean.jsonConversion(e)).toList() as M;
}
if(<ScriptBean>[] is M){
return data.map<ScriptBean>((e) => ScriptBean.jsonConversion(e)).toList()
as M;
return data.map<ScriptBean>((e) => ScriptBean.jsonConversion(e)).toList() as M;
}
if(<TaskLogBean>[] is M){
return data
.map<TaskLogBean>((e) => TaskLogBean.jsonConversion(e))
.toList() as M;
return data.map<TaskLogBean>((e) => TaskLogBean.jsonConversion(e)).toList() as M;
}
if(<TaskBean>[] is M){
return data.map<TaskBean>((e) => TaskBean.jsonConversion(e)).toList()
as M;
return data.map<TaskBean>((e) => TaskBean.jsonConversion(e)).toList() as M;
}
if (<Subscription>[] is M){
return data.map<Subscription>((e) => Subscription.jsonConversion(e)).toList() as M;
}
throw Exception("not found");

View File

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

View File

@@ -1,13 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qinglong_app/base/routes.dart';
import 'package:qinglong_app/base/sp_const.dart';
import 'package:qinglong_app/base/theme.dart';
import 'package:qinglong_app/base/userinfo_viewmodel.dart';
import 'package:qinglong_app/main.dart';
import 'package:qinglong_app/utils/extension.dart';
import 'package:qinglong_app/utils/sp_utils.dart';
class OtherPage extends ConsumerStatefulWidget {
const OtherPage({Key? key}) : super(key: key);
@@ -28,8 +26,15 @@ class _OtherPageState extends ConsumerState<OtherPage> {
children: [
Container(
margin: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 15,
),
decoration: BoxDecoration(
color: ref.watch(themeProvider).themeColor.settingBordorColor(),
borderRadius: const BorderRadius.all(
Radius.circular(15),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -230,13 +235,103 @@ class _OtherPageState extends ConsumerState<OtherPage> {
),
),
),
const Divider(
indent: 15,
),
GestureDetector(
onTap: () {
Navigator.of(context).pushNamed(
Routes.routerSubscription,
);
},
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.only(
top: 5,
bottom: 10,
left: 15,
right: 15,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"订阅管理",
style: TextStyle(
color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16,
),
),
const Spacer(),
const Icon(
CupertinoIcons.right_chevron,
size: 16,
),
],
),
),
),
const Divider(
indent: 15,
),
GestureDetector(
onTap: () {
Navigator.of(context).pushNamed(
Routes.routerBackup,
);
},
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.only(
top: 5,
bottom: 10,
left: 15,
right: 15,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"数据备份",
style: TextStyle(
color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16,
),
),
const Spacer(),
const Icon(
CupertinoIcons.right_chevron,
size: 16,
),
],
),
),
),
const Divider(
indent: 15,
),
],
),
),
Container(
margin: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 15,
),
decoration: BoxDecoration(
color: ref.watch(themeProvider).themeColor.settingBordorColor(),
borderRadius: const BorderRadius.all(
Radius.circular(15),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -272,40 +367,6 @@ class _OtherPageState extends ConsumerState<OtherPage> {
const Divider(
indent: 15,
),
Padding(
padding: const EdgeInsets.only(
left: 15,
right: 15,
bottom: 5,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"查看代码是否显示行号",
style: TextStyle(
color:
ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 16,
),
),
const Spacer(),
CupertinoSwitch(
activeColor: ref.watch(themeProvider).primaryColor,
value: SpUtil.getBool(spShowLine, defValue: false),
onChanged: (open) async {
await SpUtil.putBool(spShowLine, open);
setState(() {});
},
),
],
),
),
const Divider(
indent: 15,
height: 1,
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
@@ -316,7 +377,7 @@ class _OtherPageState extends ConsumerState<OtherPage> {
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 10,
vertical: 5,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -40,6 +40,7 @@ dependencies:
file_picker: ^4.6.1
code_text_field:
path: ./pub/code_field-master
path_provider: ^2.0.11
# flutter pub run build_runner build --delete-conflicting-outputs
# flutter pub run change_app_package_name:main work.master.qinglongapp