新增订阅管理

新增环境变量导入和导出
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

@@ -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();
});
}
}
}