新增订阅管理

新增环境变量导入和导出
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,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();
});
}
}
}