4 Commits

Author SHA1 Message Date
jyuesong
7e3a63169e 依赖管理支持批量添加 2022-06-16 15:54:19 +08:00
jyuesong
1298dba590 支持上传脚本 2022-06-16 14:37:47 +08:00
jyuesong
2005083d2e 新增代码文件支持行号显示 2022-06-15 11:05:49 +08:00
jyuesong
4e3f8a0df9 1.1.0 release 2022-06-08 10:42:26 +08:00
165 changed files with 6233 additions and 810 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
assets/images/js.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
assets/images/json.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
assets/images/other.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
assets/images/py.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
assets/images/shell.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
assets/images/ts.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -24,7 +24,9 @@ class BaseStateWidget<T extends BaseViewModel> extends ConsumerStatefulWidget {
_BaseStateWidgetState<T> createState() => _BaseStateWidgetState<T>(); _BaseStateWidgetState<T> createState() => _BaseStateWidgetState<T>();
} }
class _BaseStateWidgetState<T extends BaseViewModel> extends ConsumerState<BaseStateWidget<T>> with LazyLoadState<BaseStateWidget<T>> { class _BaseStateWidgetState<T extends BaseViewModel>
extends ConsumerState<BaseStateWidget<T>>
with LazyLoadState<BaseStateWidget<T>> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var viewModel = ref.watch<T>(widget.model); var viewModel = ref.watch<T>(widget.model);

View File

@@ -353,15 +353,10 @@ class Api {
); );
} }
static Future<HttpResponse<NullResponse>> addDependency(String name, int type) async { static Future<HttpResponse<NullResponse>> addDependency(List<Map<String, dynamic>> list) async {
return await Http.post<NullResponse>( return await Http.post<NullResponse>(
Url.dependencies, Url.dependencies,
[ list,
{
"name": name,
"type": type,
}
],
); );
} }

View File

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

View File

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

View File

@@ -14,70 +14,127 @@ class Url {
static const updatePassword = "/api/user"; static const updatePassword = "/api/user";
static get tasks => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons" : "/api/crons"; static get tasks => getIt<UserInfoViewModel>().useSecretLogined
? "/open/crons"
: "/api/crons";
static get runTasks => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/run" : "/api/crons/run"; static get runTasks => getIt<UserInfoViewModel>().useSecretLogined
? "/open/crons/run"
: "/api/crons/run";
static get stopTasks => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/stop" : "/api/crons/stop"; static get stopTasks => getIt<UserInfoViewModel>().useSecretLogined
? "/open/crons/stop"
: "/api/crons/stop";
static get taskDetail => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/" : "/api/crons/"; static get taskDetail => getIt<UserInfoViewModel>().useSecretLogined
? "/open/crons/"
: "/api/crons/";
static get addTask => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons" : "/api/crons"; static get addTask => getIt<UserInfoViewModel>().useSecretLogined
? "/open/crons"
: "/api/crons";
static get pinTask => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/pin" : "/api/crons/pin"; static get pinTask => getIt<UserInfoViewModel>().useSecretLogined
? "/open/crons/pin"
: "/api/crons/pin";
static get unpinTask => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/unpin" : "/api/crons/unpin"; static get unpinTask => getIt<UserInfoViewModel>().useSecretLogined
? "/open/crons/unpin"
: "/api/crons/unpin";
static get enableTask => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/enable" : "/api/crons/enable"; static get enableTask => getIt<UserInfoViewModel>().useSecretLogined
? "/open/crons/enable"
: "/api/crons/enable";
static get disableTask => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/disable" : "/api/crons/disable"; static get disableTask => getIt<UserInfoViewModel>().useSecretLogined
? "/open/crons/disable"
: "/api/crons/disable";
static get files => getIt<UserInfoViewModel>().useSecretLogined ? "/open/configs/files" : "/api/configs/files"; static get files => getIt<UserInfoViewModel>().useSecretLogined
? "/open/configs/files"
: "/api/configs/files";
static get configContent => getIt<UserInfoViewModel>().useSecretLogined ? "/open/configs/" : "/api/configs/"; static get configContent => getIt<UserInfoViewModel>().useSecretLogined
? "/open/configs/"
: "/api/configs/";
static get saveFile => getIt<UserInfoViewModel>().useSecretLogined ? "/open/configs/save" : "/api/configs/save"; static get saveFile => getIt<UserInfoViewModel>().useSecretLogined
? "/open/configs/save"
: "/api/configs/save";
static get envs => getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs" : "/api/envs"; static get envs =>
getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs" : "/api/envs";
static get addEnv => getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs" : "/api/envs"; static get addEnv =>
getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs" : "/api/envs";
static get delEnv => getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs" : "/api/envs"; static get delEnv =>
getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs" : "/api/envs";
static get disableEnvs => getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs/disable" : "/api/envs/disable"; static get disableEnvs => getIt<UserInfoViewModel>().useSecretLogined
? "/open/envs/disable"
: "/api/envs/disable";
static get enableEnvs => getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs/enable" : "/api/envs/enable"; static get enableEnvs => getIt<UserInfoViewModel>().useSecretLogined
? "/open/envs/enable"
: "/api/envs/enable";
static get loginLog => getIt<UserInfoViewModel>().useSecretLogined ? "/open/user/login-log" : "/api/user/login-log"; static get loginLog => getIt<UserInfoViewModel>().useSecretLogined
? "/open/user/login-log"
: "/api/user/login-log";
static get taskLog => getIt<UserInfoViewModel>().useSecretLogined ? "/open/logs" : "/api/logs"; static get taskLog =>
getIt<UserInfoViewModel>().useSecretLogined ? "/open/logs" : "/api/logs";
static get taskLogDetail => getIt<UserInfoViewModel>().useSecretLogined ? "/open/logs/" : "/api/logs/"; static get taskLogDetail => getIt<UserInfoViewModel>().useSecretLogined
? "/open/logs/"
: "/api/logs/";
static get scripts => getIt<UserInfoViewModel>().useSecretLogined ? "/open/scripts/files" : "/api/scripts/files"; static get scripts => getIt<UserInfoViewModel>().useSecretLogined
? "/open/scripts/files"
: "/api/scripts/files";
static get scripts2 => getIt<UserInfoViewModel>().useSecretLogined ? "/open/scripts" : "/api/scripts"; static get scripts2 => getIt<UserInfoViewModel>().useSecretLogined
? "/open/scripts"
: "/api/scripts";
static get scriptUpdate => getIt<UserInfoViewModel>().useSecretLogined ? "/open/scripts" : "/api/scripts"; static get scriptUpdate => getIt<UserInfoViewModel>().useSecretLogined
? "/open/scripts"
: "/api/scripts";
static get scriptDetail => getIt<UserInfoViewModel>().useSecretLogined ? "/open/scripts/" : "/api/scripts/"; static get scriptDetail => getIt<UserInfoViewModel>().useSecretLogined
? "/open/scripts/"
: "/api/scripts/";
static get dependencies => getIt<UserInfoViewModel>().useSecretLogined ? "/open/dependencies" : "/api/dependencies"; static get dependencies => getIt<UserInfoViewModel>().useSecretLogined
? "/open/dependencies"
: "/api/dependencies";
static get addScript => getIt<UserInfoViewModel>().useSecretLogined ? "/open/scripts" : "/api/scripts"; static get addScript => getIt<UserInfoViewModel>().useSecretLogined
? "/open/scripts"
: "/api/scripts";
static get dependencyReinstall => getIt<UserInfoViewModel>().useSecretLogined ? "/open/dependencies/reinstall" : "/api/dependencies/reinstall"; static get dependencyReinstall => getIt<UserInfoViewModel>().useSecretLogined
? "/open/dependencies/reinstall"
: "/api/dependencies/reinstall";
static intimeLog(String cronId) { static intimeLog(String cronId) {
return getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/$cronId/log" : "/api/crons/$cronId/log"; return getIt<UserInfoViewModel>().useSecretLogined
? "/open/crons/$cronId/log"
: "/api/crons/$cronId/log";
} }
static envMove(String envId) { static envMove(String envId) {
return getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs/$envId/move" : "/api/envs/$envId/move"; return getIt<UserInfoViewModel>().useSecretLogined
? "/open/envs/$envId/move"
: "/api/envs/$envId/move";
} }
static bool inWhiteList(String path) { static bool inWhiteList(String path) {
if (path == login || path == loginByClientId || path == loginTwo || path == loginOld) { if (path == login ||
path == loginByClientId ||
path == loginTwo ||
path == loginOld) {
return true; return true;
} }
return false; return false;

View File

@@ -71,7 +71,8 @@ class Routes {
return MaterialPageRoute(builder: (context) => const AddTaskPage()); return MaterialPageRoute(builder: (context) => const AddTaskPage());
} }
case routeAddDependency: case routeAddDependency:
return MaterialPageRoute(builder: (context) => const AddDependencyPage()); return MaterialPageRoute(
builder: (context) => const AddDependencyPage());
case routeAddEnv: case routeAddEnv:
if (settings.arguments != null) { if (settings.arguments != null) {
return MaterialPageRoute( return MaterialPageRoute(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,10 +11,8 @@ import 'package:qinglong_app/module/others/scripts/script_bean.dart';
import 'package:qinglong_app/module/others/task_log/task_log_bean.dart'; import 'package:qinglong_app/module/others/task_log/task_log_bean.dart';
import 'package:qinglong_app/module/task/task_bean.dart'; import 'package:qinglong_app/module/task/task_bean.dart';
class JsonConversion$Json { class JsonConversion$Json {
static M fromJson<M>(dynamic json) {
static M fromJson<M>(dynamic json) {
if (json is List) { if (json is List) {
return _getListChildType<M>(json); return _getListChildType<M>(json);
} else { } else {
@@ -23,93 +21,104 @@ class JsonConversion$Json {
} }
static M _fromJsonSingle<M>(dynamic json) { static M _fromJsonSingle<M>(dynamic json) {
String type = M.toString(); String type = M.toString();
if(type == (ConfigBean).toString()){ if (type == (ConfigBean).toString()) {
return ConfigBean.jsonConversion(json) as M; return ConfigBean.jsonConversion(json) as M;
} }
if(type == (EnvBean).toString()){ if (type == (EnvBean).toString()) {
return EnvBean.jsonConversion(json) as M; return EnvBean.jsonConversion(json) as M;
} }
if(type == (SystemBean).toString()){ if (type == (SystemBean).toString()) {
return SystemBean.jsonConversion(json) as M; return SystemBean.jsonConversion(json) as M;
} }
if(type == (LoginBean).toString()){ if (type == (LoginBean).toString()) {
return LoginBean.jsonConversion(json) as M; return LoginBean.jsonConversion(json) as M;
} }
if(type == (UserBean).toString()){ if (type == (UserBean).toString()) {
return UserBean.jsonConversion(json) as M; return UserBean.jsonConversion(json) as M;
} }
if(type == (DependencyBean).toString()){ if (type == (DependencyBean).toString()) {
return DependencyBean.jsonConversion(json) as M; return DependencyBean.jsonConversion(json) as M;
} }
if(type == (LoginLogBean).toString()){ if (type == (LoginLogBean).toString()) {
return LoginLogBean.jsonConversion(json) as M; return LoginLogBean.jsonConversion(json) as M;
} }
if(type == (ScriptBean).toString()){ if (type == (ScriptBean).toString()) {
return ScriptBean.jsonConversion(json) as M; return ScriptBean.jsonConversion(json) as M;
} }
if(type == (TaskLogBean).toString()){ if (type == (TaskLogBean).toString()) {
return TaskLogBean.jsonConversion(json) as M; return TaskLogBean.jsonConversion(json) as M;
} }
if(type == (TaskBean).toString()){ if (type == (TaskBean).toString()) {
return TaskBean.jsonConversion(json) as M; return TaskBean.jsonConversion(json) as M;
} }
throw Exception("not found"); throw Exception("not found");
} }
static M _getListChildType<M>(List<dynamic> data) { static M _getListChildType<M>(List<dynamic> data) {
if(<ConfigBean>[] is M){ 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){ if (<EnvBean>[] is M) {
return data.map<EnvBean>((e) => EnvBean.jsonConversion(e)).toList() as M; return data.map<EnvBean>((e) => EnvBean.jsonConversion(e)).toList() as M;
} }
if(<SystemBean>[] is M){ 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){ 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){ 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){ 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){ 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){ 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){ 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){ 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;
} }
throw Exception("not found"); throw Exception("not found");
} }
} }

View File

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

View File

@@ -64,13 +64,17 @@ class _ChangeAccountPageState extends ConsumerState<ChangeAccountPage> {
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == getIt<UserInfoViewModel>().historyAccounts.length) { if (index ==
getIt<UserInfoViewModel>().historyAccounts.length) {
return addAccount(); return addAccount();
} }
return ClipRRect( return ClipRRect(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
child: Container( child: Container(
color: ref.watch(themeProvider).themeColor.settingBordorColor(), color: ref
.watch(themeProvider)
.themeColor
.settingBordorColor(),
child: buildCell(index), child: buildCell(index),
), ),
); );
@@ -80,7 +84,8 @@ class _ChangeAccountPageState extends ConsumerState<ChangeAccountPage> {
height: 10, height: 10,
); );
}, },
itemCount: getIt<UserInfoViewModel>().historyAccounts.length + 1), itemCount:
getIt<UserInfoViewModel>().historyAccounts.length + 1),
), ),
], ],
), ),
@@ -108,31 +113,34 @@ class _ChangeAccountPageState extends ConsumerState<ChangeAccountPage> {
), ),
trailing: index == 0 trailing: index == 0
? Container( ? Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 5, horizontal: 5,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
border: Border.all(color: ref.watch(themeProvider).primaryColor, width: 1), border: Border.all(
), color: ref.watch(themeProvider).primaryColor, width: 1),
child: Text( ),
"已登录", child: Text(
style: TextStyle(color: ref.watch(themeProvider).primaryColor, fontSize: 12), "已登录",
), style: TextStyle(
) color: ref.watch(themeProvider).primaryColor, fontSize: 12),
),
)
: (isLoginingHost.isNotEmpty : (isLoginingHost.isNotEmpty
? SizedBox( ? SizedBox(
width: 15, width: 15,
height: 15, height: 15,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2, strokeWidth: 2,
color: ref.watch(themeProvider).primaryColor, color: ref.watch(themeProvider).primaryColor,
), ),
) )
: const SizedBox.shrink()), : const SizedBox.shrink()),
); );
if (getIt<UserInfoViewModel>().historyAccounts[index].host == getIt<UserInfoViewModel>().host) { if (getIt<UserInfoViewModel>().historyAccounts[index].host ==
getIt<UserInfoViewModel>().host) {
return child; return child;
} }
@@ -146,7 +154,8 @@ class _ChangeAccountPageState extends ConsumerState<ChangeAccountPage> {
backgroundColor: Colors.red, backgroundColor: Colors.red,
flex: 1, flex: 1,
onPressed: (_) { onPressed: (_) {
getIt<UserInfoViewModel>().removeHistoryAccount(getIt<UserInfoViewModel>().historyAccounts[index].host); getIt<UserInfoViewModel>().removeHistoryAccount(
getIt<UserInfoViewModel>().historyAccounts[index].host);
setState(() {}); setState(() {});
}, },
foregroundColor: Colors.white, foregroundColor: Colors.white,
@@ -181,7 +190,8 @@ class _ChangeAccountPageState extends ConsumerState<ChangeAccountPage> {
void dealLoginResponse(int response) { void dealLoginResponse(int response) {
if (response == LoginHelper.success) { if (response == LoginHelper.success) {
Navigator.of(context).pushNamedAndRemoveUntil(Routes.routeHomePage, (_) => false); Navigator.of(context)
.pushNamedAndRemoveUntil(Routes.routeHomePage, (_) => false);
} else if (response == LoginHelper.failed) { } else if (response == LoginHelper.failed) {
loginFailed(); loginFailed();
} else { } else {
@@ -201,58 +211,58 @@ class _ChangeAccountPageState extends ConsumerState<ChangeAccountPage> {
showCupertinoDialog( showCupertinoDialog(
context: context, context: context,
builder: (_) => CupertinoAlertDialog( builder: (_) => CupertinoAlertDialog(
title: const Text("两步验证"), title: const Text("两步验证"),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Material( Material(
color: Colors.transparent, color: Colors.transparent,
child: TextField( child: TextField(
onChanged: (value) { onChanged: (value) {
twoFact = value; twoFact = value;
}, },
maxLines: 1, maxLines: 1,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
decoration: const InputDecoration( decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5), contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "请输入code", hintText: "请输入code",
),
autofocus: true,
),
), ),
autofocus: true, ],
),
), ),
], actions: [
), CupertinoDialogAction(
actions: [ child: const Text(
CupertinoDialogAction( "取消",
child: const Text( style: TextStyle(
"取消", color: Color(0xff999999),
style: TextStyle( ),
color: Color(0xff999999), ),
onPressed: () {
Navigator.of(context).pop();
},
), ),
), CupertinoDialogAction(
onPressed: () { child: Text(
Navigator.of(context).pop(); "确定",
}, style: TextStyle(
), color: ref.watch(themeProvider).primaryColor,
CupertinoDialogAction( ),
child: Text( ),
"确定", onPressed: () async {
style: TextStyle( Navigator.of(context).pop(true);
color: ref.watch(themeProvider).primaryColor, if (helper != null) {
var response = await helper!.loginTwice(twoFact);
dealLoginResponse(response);
} else {
"状态异常".toast();
}
},
), ),
), ],
onPressed: () async { )).then((value) {
Navigator.of(context).pop(true);
if (helper != null) {
var response = await helper!.loginTwice(twoFact);
dealLoginResponse(response);
} else {
"状态异常".toast();
}
},
),
],
)).then((value) {
if (value == null) { if (value == null) {
isLoginingHost = ""; isLoginingHost = "";
setState(() {}); setState(() {});
@@ -266,7 +276,7 @@ class _ChangeAccountPageState extends ConsumerState<ChangeAccountPage> {
onTap: () { onTap: () {
Navigator.of(context).pushNamedAndRemoveUntil( Navigator.of(context).pushNamedAndRemoveUntil(
Routes.routeLogin, Routes.routeLogin,
(_) => false, (_) => false,
arguments: true, arguments: true,
); );
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -44,7 +44,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
if (!widget.doNotLoadLocalData) { if (!widget.doNotLoadLocalData) {
_hostController.text = getIt<UserInfoViewModel>().host ?? ""; _hostController.text = getIt<UserInfoViewModel>().host ?? "";
useSecretLogin = getIt<UserInfoViewModel>().useSecretLogined; useSecretLogin = getIt<UserInfoViewModel>().useSecretLogined;
if (getIt<UserInfoViewModel>().userName != null && getIt<UserInfoViewModel>().userName!.isNotEmpty) { if (getIt<UserInfoViewModel>().userName != null &&
getIt<UserInfoViewModel>().userName!.isNotEmpty) {
if (getIt<UserInfoViewModel>().useSecretLogined) { if (getIt<UserInfoViewModel>().useSecretLogined) {
_cIdController.text = getIt<UserInfoViewModel>().userName!; _cIdController.text = getIt<UserInfoViewModel>().userName!;
} else { } else {
@@ -54,7 +55,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
} else { } else {
rememberPassword = false; rememberPassword = false;
} }
if (getIt<UserInfoViewModel>().passWord != null && getIt<UserInfoViewModel>().passWord!.isNotEmpty) { if (getIt<UserInfoViewModel>().passWord != null &&
getIt<UserInfoViewModel>().passWord!.isNotEmpty) {
if (getIt<UserInfoViewModel>().useSecretLogined) { if (getIt<UserInfoViewModel>().useSecretLogined) {
_cSecretController.text = getIt<UserInfoViewModel>().passWord!; _cSecretController.text = getIt<UserInfoViewModel>().passWord!;
} else { } else {
@@ -76,7 +78,9 @@ class _LoginPageState extends ConsumerState<LoginPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: AnnotatedRegion<SystemUiOverlayStyle>( body: AnnotatedRegion<SystemUiOverlayStyle>(
value: ref.watch(themeProvider).darkMode == true ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark, value: ref.watch(themeProvider).darkMode == true
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
child: ColoredBox( child: ColoredBox(
color: ref.watch(themeProvider).themeColor.settingBgColor(), color: ref.watch(themeProvider).themeColor.settingBgColor(),
child: SingleChildScrollView( child: SingleChildScrollView(
@@ -105,7 +109,10 @@ class _LoginPageState extends ConsumerState<LoginPage> {
style: TextStyle( style: TextStyle(
fontSize: 26, fontSize: 26,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
), ),
), ),
), ),
@@ -115,7 +122,9 @@ class _LoginPageState extends ConsumerState<LoginPage> {
if (debugBtnIsShow()) { if (debugBtnIsShow()) {
dismissDebugBtn(); dismissDebugBtn();
} else { } else {
showDebugBtn(context, btnColor: ref.watch(themeProvider).primaryColor); showDebugBtn(context,
btnColor:
ref.watch(themeProvider).primaryColor);
} }
WidgetsBinding.instance.endOfFrame; WidgetsBinding.instance.endOfFrame;
}, },
@@ -158,7 +167,10 @@ class _LoginPageState extends ConsumerState<LoginPage> {
hintText: "http://1.1.1.1:5700", hintText: "http://1.1.1.1:5700",
hintStyle: TextStyle( hintStyle: TextStyle(
fontSize: 16, fontSize: 16,
color: ref.watch(themeProvider).themeColor.descColor(), color: ref
.watch(themeProvider)
.themeColor
.descColor(),
), ),
), ),
autofocus: false, autofocus: false,
@@ -216,7 +228,10 @@ class _LoginPageState extends ConsumerState<LoginPage> {
hintText: "请输入账户", hintText: "请输入账户",
hintStyle: TextStyle( hintStyle: TextStyle(
fontSize: 16, fontSize: 16,
color: ref.watch(themeProvider).themeColor.descColor(), color: ref
.watch(themeProvider)
.themeColor
.descColor(),
), ),
), ),
autofocus: false, autofocus: false,
@@ -260,7 +275,10 @@ class _LoginPageState extends ConsumerState<LoginPage> {
hintText: "请输入密码", hintText: "请输入密码",
hintStyle: TextStyle( hintStyle: TextStyle(
fontSize: 16, fontSize: 16,
color: ref.watch(themeProvider).themeColor.descColor(), color: ref
.watch(themeProvider)
.themeColor
.descColor(),
), ),
), ),
autofocus: false, autofocus: false,
@@ -310,7 +328,10 @@ class _LoginPageState extends ConsumerState<LoginPage> {
hintText: "请输入client_id", hintText: "请输入client_id",
hintStyle: TextStyle( hintStyle: TextStyle(
fontSize: 16, fontSize: 16,
color: ref.watch(themeProvider).themeColor.descColor(), color: ref
.watch(themeProvider)
.themeColor
.descColor(),
), ),
), ),
autofocus: false, autofocus: false,
@@ -354,7 +375,10 @@ class _LoginPageState extends ConsumerState<LoginPage> {
hintText: "请输入client_secret", hintText: "请输入client_secret",
hintStyle: TextStyle( hintStyle: TextStyle(
fontSize: 16, fontSize: 16,
color: ref.watch(themeProvider).themeColor.descColor(), color: ref
.watch(themeProvider)
.themeColor
.descColor(),
), ),
), ),
autofocus: false, autofocus: false,
@@ -399,7 +423,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
GestureDetector( GestureDetector(
onTap: () { onTap: () {
cardKey.currentState?.toggleCard(); cardKey.currentState?.toggleCard();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance
.addPostFrameCallback((timeStamp) {
setState(() {}); setState(() {});
}); });
}, },
@@ -421,7 +446,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
height: 30, height: 30,
), ),
Shake( Shake(
preferences: const AnimationPreferences(autoPlay: AnimationPlayStates.None), preferences: const AnimationPreferences(
autoPlay: AnimationPlayStates.None),
key: loginKey, key: loginKey,
child: Center( child: Center(
child: Container( child: Container(
@@ -469,9 +495,11 @@ class _LoginPageState extends ConsumerState<LoginPage> {
Http.pushedLoginPage = false; Http.pushedLoginPage = false;
Utils.hideKeyBoard(context); Utils.hideKeyBoard(context);
if (loginByUserName()) { if (loginByUserName()) {
login(_userNameController.text, _passwordController.text); login(_userNameController.text,
_passwordController.text);
} else { } else {
login(_cIdController.text, _cSecretController.text); login(_cIdController.text,
_cSecretController.text);
} }
}, },
); );
@@ -498,7 +526,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
onSelected: (UserInfoBean result) { onSelected: (UserInfoBean result) {
selected(result); selected(result);
}, },
itemBuilder: (BuildContext context) => <PopupMenuEntry<UserInfoBean>>[ itemBuilder: (BuildContext context) =>
<PopupMenuEntry<UserInfoBean>>[
...getIt<UserInfoViewModel>() ...getIt<UserInfoViewModel>()
.historyAccounts .historyAccounts
.map( .map(
@@ -554,7 +583,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
isLoading = true; isLoading = true;
setState(() {}); setState(() {});
helper = LoginHelper(useSecretLogin, _hostController.text, userName, password, rememberPassword); helper = LoginHelper(useSecretLogin, _hostController.text, userName,
password, rememberPassword);
var response = await helper!.login(); var response = await helper!.login();
dealLoginResponse(response); dealLoginResponse(response);
} }
@@ -581,9 +611,11 @@ class _LoginPageState extends ConsumerState<LoginPage> {
if (_hostController.text.isEmpty) return false; if (_hostController.text.isEmpty) return false;
if (!loginByUserName()) { if (!loginByUserName()) {
return _cIdController.text.isNotEmpty && _cSecretController.text.isNotEmpty; return _cIdController.text.isNotEmpty &&
_cSecretController.text.isNotEmpty;
} else { } else {
return _userNameController.text.isNotEmpty && _passwordController.text.isNotEmpty; return _userNameController.text.isNotEmpty &&
_passwordController.text.isNotEmpty;
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -37,99 +37,98 @@ class _LoginLogPageState extends ConsumerState<LoginLogPage>
), ),
body: list.isEmpty body: list.isEmpty
? const Center( ? const Center(
child: CupertinoActivityIndicator(), child: CupertinoActivityIndicator(),
) )
: ListView.separated( : ListView.separated(
itemBuilder: (context, index) { itemBuilder: (context, index) {
LoginLogBean item = list[index]; LoginLogBean item = list[index];
return Row( return Row(
children: [ children: [
const SizedBox( const SizedBox(
width: 15, width: 15,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 10,
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${item.address}",
style: TextStyle(
color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 14,
),
),
const SizedBox(
width: 5,
),
Padding(
padding: const EdgeInsets.only(
top: 2,
),
child: Image.asset(
item.status == 0
? "assets/images/icon_success.png"
: "assets/images/icon_fail.png",
width: 30,
fit: BoxFit.cover,
),
),
],
),
const SizedBox(
height: 10,
),
SelectableText(
"${item.ip}",
selectionWidthStyle: BoxWidthStyle.max,
selectionHeightStyle: BoxHeightStyle.max,
style: TextStyle(
color:
ref.watch(themeProvider).themeColor.descColor(),
fontSize: 14,
), ),
), Column(
const SizedBox( crossAxisAlignment: CrossAxisAlignment.start,
height: 10, children: [
), const SizedBox(
], height: 10,
), ),
const Spacer(), Row(
Text( crossAxisAlignment: CrossAxisAlignment.start,
Utils.formatMessageTime(item.timestamp ?? 0), children: [
style: TextStyle( Text(
fontSize: 12, "${item.address}",
color: ref.watch(themeProvider).themeColor.descColor(), style: TextStyle(
), color: ref
), .watch(themeProvider)
const SizedBox( .themeColor
width: 15, .titleColor(),
), fontSize: 14,
], ),
); ),
}, const SizedBox(
itemCount: list.length, width: 5,
separatorBuilder: (BuildContext context, int index) { ),
return const Divider( Padding(
indent: 15, padding: const EdgeInsets.only(
height: 1, top: 2,
); ),
}, child: Image.asset(
), item.status == 0
? "assets/images/icon_success.png"
: "assets/images/icon_fail.png",
width: 30,
fit: BoxFit.cover,
),
),
],
),
const SizedBox(
height: 10,
),
SelectableText(
"${item.ip}",
selectionWidthStyle: BoxWidthStyle.max,
selectionHeightStyle: BoxHeightStyle.max,
style: TextStyle(
color:
ref.watch(themeProvider).themeColor.descColor(),
fontSize: 14,
),
),
const SizedBox(
height: 10,
),
],
),
const Spacer(),
Text(
Utils.formatMessageTime(item.timestamp ?? 0),
style: TextStyle(
fontSize: 12,
color: ref.watch(themeProvider).themeColor.descColor(),
),
),
const SizedBox(
width: 15,
),
],
);
},
itemCount: list.length,
separatorBuilder: (BuildContext context, int index) {
return const Divider(
indent: 15,
height: 1,
);
},
),
); );
} }
Future<void> loadData() async { Future<void> loadData() async {
HttpResponse<List<LoginLogBean>> response = HttpResponse<List<LoginLogBean>> response = await Api.loginLog();
await Api.loginLog();
if (response.success) { if (response.success) {
if (response.bean == null || response.bean!.isEmpty) { if (response.bean == null || response.bean!.isEmpty) {

View File

@@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qinglong_app/base/routes.dart'; import 'package:qinglong_app/base/routes.dart';
import 'package:qinglong_app/base/sp_const.dart';
import 'package:qinglong_app/base/theme.dart'; import 'package:qinglong_app/base/theme.dart';
import 'package:qinglong_app/base/userinfo_viewmodel.dart'; import 'package:qinglong_app/base/userinfo_viewmodel.dart';
import 'package:qinglong_app/main.dart'; import 'package:qinglong_app/main.dart';
import 'package:qinglong_app/utils/extension.dart'; import 'package:qinglong_app/utils/extension.dart';
import 'package:qinglong_app/utils/sp_utils.dart';
class OtherPage extends ConsumerStatefulWidget { class OtherPage extends ConsumerStatefulWidget {
const OtherPage({Key? key}) : super(key: key); const OtherPage({Key? key}) : super(key: key);
@@ -51,7 +53,10 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text( Text(
"脚本管理", "脚本管理",
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -84,7 +89,10 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text( Text(
"依赖管理", "依赖管理",
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -119,7 +127,10 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text( Text(
"任务日志", "任务日志",
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -159,7 +170,10 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text( Text(
"登录日志", "登录日志",
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -200,7 +214,10 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text( Text(
"修改密码", "修改密码",
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -237,7 +254,8 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text( Text(
"夜间模式", "夜间模式",
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(), color:
ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -251,10 +269,43 @@ class _OtherPageState extends ConsumerState<OtherPage> {
], ],
), ),
), ),
const Divider( const Divider(
indent: 15, 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( GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () { onTap: () {
@@ -265,7 +316,7 @@ class _OtherPageState extends ConsumerState<OtherPage> {
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 15, horizontal: 15,
vertical: 5, vertical: 10,
), ),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@@ -274,7 +325,10 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text( Text(
"主题设置", "主题设置",
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -309,7 +363,10 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text( Text(
"切换账号", "切换账号",
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -346,7 +403,10 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text( Text(
"关于", "关于",
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -405,7 +465,8 @@ class _OtherPageState extends ConsumerState<OtherPage> {
), ),
onPressed: () { onPressed: () {
getIt<UserInfoViewModel>().updateToken(""); getIt<UserInfoViewModel>().updateToken("");
Navigator.of(context).pushReplacementNamed(Routes.routeLogin); Navigator.of(context)
.pushReplacementNamed(Routes.routeLogin);
}, },
), ),
], ],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,8 @@ class TaskLogDetailPage extends ConsumerStatefulWidget {
_TaskLogDetailPageState createState() => _TaskLogDetailPageState(); _TaskLogDetailPageState createState() => _TaskLogDetailPageState();
} }
class _TaskLogDetailPageState extends ConsumerState<TaskLogDetailPage> with LazyLoadState<TaskLogDetailPage> { class _TaskLogDetailPageState extends ConsumerState<TaskLogDetailPage>
with LazyLoadState<TaskLogDetailPage> {
String? content; String? content;
@override @override
@@ -40,15 +41,15 @@ class _TaskLogDetailPageState extends ConsumerState<TaskLogDetailPage> with Lazy
body: content == null body: content == null
? const Center(child: CupertinoActivityIndicator()) ? const Center(child: CupertinoActivityIndicator())
: Padding( : Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 15, horizontal: 15,
), ),
child: SelectableText( child: SelectableText(
(content == null || content!.isEmpty) ? "暂无数据" : content!, (content == null || content!.isEmpty) ? "暂无数据" : content!,
selectionHeightStyle: BoxHeightStyle.max, selectionHeightStyle: BoxHeightStyle.max,
selectionWidthStyle: BoxWidthStyle.max, selectionWidthStyle: BoxWidthStyle.max,
), ),
), ),
); );
} }

View File

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

View File

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

View File

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

View File

@@ -39,8 +39,7 @@ class _InTimeLogPageState extends State<InTimeLogPage>
if (!canRequest) return; if (!canRequest) return;
if (isRequest) return; if (isRequest) return;
isRequest = true; isRequest = true;
HttpResponse<String> response = HttpResponse<String> response = await Api.inTimeLog(widget.cronId);
await Api.inTimeLog(widget.cronId);
if (response.success) { if (response.success) {
content = response.bean; content = response.bean;
setState(() {}); setState(() {});
@@ -161,7 +160,7 @@ class _InTimeLogPageState extends State<InTimeLogPage>
if (widget.needTimer) { if (widget.needTimer) {
_timer = Timer.periodic( _timer = Timer.periodic(
const Duration(seconds: 2), const Duration(seconds: 2),
(timer) { (timer) {
getLogData(); getLogData();
}, },
); );

View File

@@ -26,20 +26,20 @@ class TaskBean {
TaskBean( TaskBean(
{this.name, {this.name,
this.command, this.command,
this.schedule, this.schedule,
this.saved, this.saved,
this.sId, this.sId,
this.created, this.created,
this.status, this.status,
this.timestamp, this.timestamp,
this.isSystem, this.isSystem,
this.isDisabled, this.isDisabled,
this.logPath, this.logPath,
this.isPinned, this.isPinned,
this.lastExecutionTime, this.lastExecutionTime,
this.lastRunningTime, this.lastRunningTime,
this.pid}); this.pid});
get nId => _id; get nId => _id;
@@ -51,7 +51,9 @@ class TaskBean {
saved = json['saved']; saved = json['saved'];
id = json['id']; id = json['id'];
_id = json['_id']; _id = json['_id'];
sId = json.containsKey('_id') ? json['_id'].toString() : (json.containsKey('id') ? json['id'].toString() : ""); sId = json.containsKey('_id')
? json['_id'].toString()
: (json.containsKey('id') ? json['id'].toString() : "");
created = int.tryParse(json['created'].toString()); created = int.tryParse(json['created'].toString());
status = json['status']; status = json['status'];
timestamp = json['timestamp'].toString(); timestamp = json['timestamp'].toString();

View File

@@ -17,7 +17,8 @@ class TaskDetailPage extends ConsumerStatefulWidget {
final TaskBean taskBean; final TaskBean taskBean;
final bool hideAppbar; final bool hideAppbar;
const TaskDetailPage(this.taskBean, {Key? key, this.hideAppbar = false}) : super(key: key); const TaskDetailPage(this.taskBean, {Key? key, this.hideAppbar = false})
: super(key: key);
@override @override
_TaskDetailPageState createState() => _TaskDetailPageState(); _TaskDetailPageState createState() => _TaskDetailPageState();
@@ -71,11 +72,13 @@ class _TaskDetailPageState extends ConsumerState<TaskDetailPage> {
), ),
TaskDetailCell( TaskDetailCell(
title: "创建时间", title: "创建时间",
desc: Utils.formatMessageTime(widget.taskBean.created ?? 0), desc:
Utils.formatMessageTime(widget.taskBean.created ?? 0),
), ),
TaskDetailCell( TaskDetailCell(
title: "更新时间", title: "更新时间",
desc: Utils.formatGMTTime(widget.taskBean.timestamp ?? ""), desc:
Utils.formatGMTTime(widget.taskBean.timestamp ?? ""),
), ),
TaskDetailCell( TaskDetailCell(
title: "任务定时", title: "任务定时",
@@ -83,11 +86,14 @@ class _TaskDetailPageState extends ConsumerState<TaskDetailPage> {
), ),
TaskDetailCell( TaskDetailCell(
title: "最后运行时间", title: "最后运行时间",
desc: Utils.formatMessageTime(widget.taskBean.lastExecutionTime ?? 0), desc: Utils.formatMessageTime(
widget.taskBean.lastExecutionTime ?? 0),
), ),
TaskDetailCell( TaskDetailCell(
title: "最后运行时长", title: "最后运行时长",
desc: widget.taskBean.lastRunningTime == null ? "-" : "${widget.taskBean.lastRunningTime ?? "-"}", desc: widget.taskBean.lastRunningTime == null
? "-"
: "${widget.taskBean.lastRunningTime ?? "-"}",
), ),
GestureDetector( GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
@@ -205,7 +211,8 @@ class _TaskDetailPageState extends ConsumerState<TaskDetailPage> {
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
Navigator.of(context).pushNamed(Routes.routeAddTask, arguments: widget.taskBean); Navigator.of(context)
.pushNamed(Routes.routeAddTask, arguments: widget.taskBean);
}, },
child: Container( child: Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@@ -357,12 +364,16 @@ class _TaskDetailPageState extends ConsumerState<TaskDetailPage> {
} }
void enableTask() async { void enableTask() async {
await ref.read(taskProvider).enableTask(widget.taskBean.sId!, widget.taskBean.isDisabled!); await ref
.read(taskProvider)
.enableTask(widget.taskBean.sId!, widget.taskBean.isDisabled!);
setState(() {}); setState(() {});
} }
void pinTask() async { void pinTask() async {
await ref.read(taskProvider).pinTask(widget.taskBean.sId!, widget.taskBean.isPinned!); await ref
.read(taskProvider)
.pinTask(widget.taskBean.sId!, widget.taskBean.isPinned!);
setState(() {}); setState(() {});
} }
@@ -472,14 +483,16 @@ class TaskDetailCell extends ConsumerWidget {
} }
}, },
style: TextStyle( style: TextStyle(
color: ref.watch(themeProvider).themeColor.descColor(), color:
ref.watch(themeProvider).themeColor.descColor(),
fontSize: 14, fontSize: 14,
), ),
), ),
), ),
) )
: Expanded( : Expanded(
child: Align(alignment: Alignment.centerRight, child: icon!), child:
Align(alignment: Alignment.centerRight, child: icon!),
), ),
], ],
), ),

View File

@@ -67,9 +67,17 @@ class _TaskPageState extends ConsumerState<TaskPage> {
} }
TaskBean item = list[index - 1]; TaskBean item = list[index - 1];
if (_searchController.text.isEmpty || if (_searchController.text.isEmpty ||
(item.name?.toLowerCase().contains(_searchController.text.toLowerCase()) ?? false) || (item.name
(item.command?.toLowerCase().contains(_searchController.text.toLowerCase()) ?? false) || ?.toLowerCase()
(item.schedule?.contains(_searchController.text.toLowerCase()) ?? false)) { .contains(_searchController.text.toLowerCase()) ??
false) ||
(item.command
?.toLowerCase()
.contains(_searchController.text.toLowerCase()) ??
false) ||
(item.schedule
?.contains(_searchController.text.toLowerCase()) ??
false)) {
return TaskItemCell(item, ref); return TaskItemCell(item, ref);
} else { } else {
return const SizedBox.shrink(); return const SizedBox.shrink();
@@ -80,9 +88,17 @@ class _TaskPageState extends ConsumerState<TaskPage> {
if (index == 0) return const SizedBox.shrink(); if (index == 0) return const SizedBox.shrink();
TaskBean item = list[index - 1]; TaskBean item = list[index - 1];
if (_searchController.text.isEmpty || if (_searchController.text.isEmpty ||
(item.name?.toLowerCase().contains(_searchController.text.toLowerCase()) ?? false) || (item.name
(item.command?.toLowerCase().contains(_searchController.text.toLowerCase()) ?? false) || ?.toLowerCase()
(item.schedule?.contains(_searchController.text.toLowerCase()) ?? false)) { .contains(_searchController.text.toLowerCase()) ??
false) ||
(item.command
?.toLowerCase()
.contains(_searchController.text.toLowerCase()) ??
false) ||
(item.schedule
?.contains(_searchController.text.toLowerCase()) ??
false)) {
return Container( return Container(
color: ref.watch(themeProvider).themeColor.settingBgColor(), color: ref.watch(themeProvider).themeColor.settingBgColor(),
child: const Divider( child: const Divider(
@@ -130,7 +146,9 @@ class _TaskPageState extends ConsumerState<TaskPage> {
child: Text( child: Text(
TaskViewModel.allStr, TaskViewModel.allStr,
style: TextStyle( style: TextStyle(
color: currentState == TaskViewModel.allStr ? ref.watch(themeProvider).primaryColor : ref.watch(themeProvider).themeColor.titleColor(), color: currentState == TaskViewModel.allStr
? ref.watch(themeProvider).primaryColor
: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 14, fontSize: 14,
), ),
), ),
@@ -140,8 +158,9 @@ class _TaskPageState extends ConsumerState<TaskPage> {
child: Text( child: Text(
TaskViewModel.runningStr, TaskViewModel.runningStr,
style: TextStyle( style: TextStyle(
color: color: currentState == TaskViewModel.runningStr
currentState == TaskViewModel.runningStr ? ref.watch(themeProvider).primaryColor : ref.watch(themeProvider).themeColor.titleColor(), ? ref.watch(themeProvider).primaryColor
: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 14, fontSize: 14,
), ),
), ),
@@ -151,7 +170,9 @@ class _TaskPageState extends ConsumerState<TaskPage> {
child: Text( child: Text(
TaskViewModel.neverStr, TaskViewModel.neverStr,
style: TextStyle( style: TextStyle(
color: currentState == TaskViewModel.neverStr ? ref.watch(themeProvider).primaryColor : ref.watch(themeProvider).themeColor.titleColor(), color: currentState == TaskViewModel.neverStr
? ref.watch(themeProvider).primaryColor
: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 14, fontSize: 14,
), ),
), ),
@@ -165,7 +186,10 @@ class _TaskPageState extends ConsumerState<TaskPage> {
style: TextStyle( style: TextStyle(
color: currentState == TaskViewModel.notScriptStr color: currentState == TaskViewModel.notScriptStr
? ref.watch(themeProvider).primaryColor ? ref.watch(themeProvider).primaryColor
: ref.watch(themeProvider).themeColor.titleColor(), : ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 14, fontSize: 14,
), ),
), ),
@@ -178,8 +202,9 @@ class _TaskPageState extends ConsumerState<TaskPage> {
TaskViewModel.disableStr, TaskViewModel.disableStr,
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: color: currentState == TaskViewModel.disableStr
currentState == TaskViewModel.disableStr ? ref.watch(themeProvider).primaryColor : ref.watch(themeProvider).themeColor.titleColor(), ? ref.watch(themeProvider).primaryColor
: ref.watch(themeProvider).themeColor.titleColor(),
), ),
), ),
value: TaskViewModel.disableStr, value: TaskViewModel.disableStr,
@@ -236,7 +261,9 @@ class TaskItemCell extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ColoredBox( return ColoredBox(
color: bean.isPinned == 1 ? ref.watch(themeProvider).themeColor.pinColor() : ref.watch(themeProvider).themeColor.settingBgColor(), color: bean.isPinned == 1
? ref.watch(themeProvider).themeColor.pinColor()
: ref.watch(themeProvider).themeColor.settingBgColor(),
child: Slidable( child: Slidable(
key: ValueKey(bean.sId), key: ValueKey(bean.sId),
endActionPane: ActionPane( endActionPane: ActionPane(
@@ -247,7 +274,8 @@ class TaskItemCell extends StatelessWidget {
backgroundColor: const Color(0xff5D5E70), backgroundColor: const Color(0xff5D5E70),
onPressed: (_) { onPressed: (_) {
WidgetsBinding.instance.endOfFrame.then((timeStamp) { WidgetsBinding.instance.endOfFrame.then((timeStamp) {
Navigator.of(context).pushNamed(Routes.routeAddTask, arguments: bean); Navigator.of(context)
.pushNamed(Routes.routeAddTask, arguments: bean);
}); });
}, },
foregroundColor: Colors.white, foregroundColor: Colors.white,
@@ -259,7 +287,9 @@ class TaskItemCell extends StatelessWidget {
pinTask(context); pinTask(context);
}, },
foregroundColor: Colors.white, foregroundColor: Colors.white,
icon: (bean.isPinned ?? 0) == 0 ? CupertinoIcons.pin : CupertinoIcons.pin_slash, icon: (bean.isPinned ?? 0) == 0
? CupertinoIcons.pin
: CupertinoIcons.pin_slash,
), ),
SlidableAction( SlidableAction(
backgroundColor: const Color(0xffA356D6), backgroundColor: const Color(0xffA356D6),
@@ -267,7 +297,9 @@ class TaskItemCell extends StatelessWidget {
enableTask(context); enableTask(context);
}, },
foregroundColor: Colors.white, foregroundColor: Colors.white,
icon: bean.isDisabled! == 0 ? Icons.dnd_forwardslash : Icons.check_circle_outline_sharp, icon: bean.isDisabled! == 0
? Icons.dnd_forwardslash
: Icons.check_circle_outline_sharp,
), ),
SlidableAction( SlidableAction(
backgroundColor: const Color(0xffEA4D3E), backgroundColor: const Color(0xffEA4D3E),
@@ -299,7 +331,9 @@ class TaskItemCell extends StatelessWidget {
} }
}, },
foregroundColor: Colors.white, foregroundColor: Colors.white,
icon: bean.status! == 1 ? CupertinoIcons.memories : CupertinoIcons.stop_circle, icon: bean.status! == 1
? CupertinoIcons.memories
: CupertinoIcons.stop_circle,
), ),
SlidableAction( SlidableAction(
backgroundColor: const Color(0xff606467), backgroundColor: const Color(0xff606467),
@@ -317,10 +351,13 @@ class TaskItemCell extends StatelessWidget {
], ],
), ),
child: Material( child: Material(
color: bean.isPinned == 1 ? ref.watch(themeProvider).themeColor.pinColor() : ref.watch(themeProvider).themeColor.settingBgColor(), color: bean.isPinned == 1
? ref.watch(themeProvider).themeColor.pinColor()
: ref.watch(themeProvider).themeColor.settingBgColor(),
child: InkWell( child: InkWell(
onTap: () { onTap: () {
Navigator.of(context).pushNamed(Routes.routeTaskDetail, arguments: bean); Navigator.of(context)
.pushNamed(Routes.routeTaskDetail, arguments: bean);
}, },
child: Container( child: Container(
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
@@ -356,7 +393,10 @@ class TaskItemCell extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
color: ref.watch(themeProvider).themeColor.titleColor(), color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -392,11 +432,16 @@ class TaskItemCell extends StatelessWidget {
Material( Material(
color: Colors.transparent, color: Colors.transparent,
child: Text( child: Text(
(bean.lastExecutionTime == null || bean.lastExecutionTime == 0) ? "-" : Utils.formatMessageTime(bean.lastExecutionTime!), (bean.lastExecutionTime == null ||
bean.lastExecutionTime == 0)
? "-"
: Utils.formatMessageTime(
bean.lastExecutionTime!),
maxLines: 1, maxLines: 1,
style: TextStyle( style: TextStyle(
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
color: ref.watch(themeProvider).themeColor.descColor(), color:
ref.watch(themeProvider).themeColor.descColor(),
fontSize: 12, fontSize: 12,
), ),
), ),

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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