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>();
}
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
Widget build(BuildContext context) {
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>(
Url.dependencies,
[
{
"name": name,
"type": type,
}
],
list,
);
}

View File

@@ -122,7 +122,8 @@ class Http {
if (!pushedLoginPage) {
"身份已过期,请重新登录".toast();
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) {
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 {
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;
T? bean;
HttpResponse({required this.success, this.message, required this.code, this.bean});
HttpResponse(
{required this.success, this.message, required this.code, this.bean});
}
class DeserializeAction<T> {

View File

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

View File

@@ -14,70 +14,127 @@ class Url {
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) {
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) {
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) {
if (path == login || path == loginByClientId || path == loginTwo || path == loginOld) {
if (path == login ||
path == loginByClientId ||
path == loginTwo ||
path == loginOld) {
return true;
}
return false;

View File

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

View File

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

View File

@@ -5,7 +5,8 @@
import 'dart:math' as math;
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/services.dart';
import 'package:flutter/widgets.dart';
@@ -42,7 +43,8 @@ typedef _ContextMenuPreviewBuilderChildless = Widget Function(
// paintBounds in global coordinates.
Rect _getRect(GlobalKey globalKey) {
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(
renderBoxContainer.paintBounds.topLeft,
);
@@ -194,7 +196,8 @@ class QlCupertinoContextMenu extends StatefulWidget {
State<QlCupertinoContextMenu> createState() => _QlCupertinoContextMenuState();
}
class _QlCupertinoContextMenuState extends State<QlCupertinoContextMenu> with TickerProviderStateMixin {
class _QlCupertinoContextMenuState extends State<QlCupertinoContextMenu>
with TickerProviderStateMixin {
final GlobalKey _childGlobalKey = GlobalKey();
bool _childHidden = false;
@@ -227,7 +230,8 @@ class _QlCupertinoContextMenuState extends State<QlCupertinoContextMenu> with Ti
final double screenWidth = MediaQuery.of(context).size.width;
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();
if (centerDividesChild && distanceFromCenter <= childRect.width / 4) {
return _ContextMenuLocation.center;
@@ -318,7 +322,8 @@ class _QlCupertinoContextMenuState extends State<QlCupertinoContextMenu> with Ti
_openController.reverse();
} else {
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();
}
class _DecoyChildState extends State<_DecoyChild> with TickerProviderStateMixin {
class _DecoyChildState extends State<_DecoyChild>
with TickerProviderStateMixin {
// TODO(justinmc): Dark mode support.
// See https://github.com/flutter/flutter/issues/43211.
static const Color _lightModeMaskColor = Color(0xFF888888);
@@ -481,7 +487,9 @@ class _DecoyChildState extends State<_DecoyChild> with TickerProviderStateMixin
}
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(
rect: _rect.value!,
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
// 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 _ContextMenuPreviewBuilderChildless? _builder;
@@ -562,7 +571,8 @@ class _ContextMenuRoute<T> extends PopupRoute<T> {
static final RectTween _rectTween = RectTween();
static final Animatable<Rect?> _rectAnimatable = _rectTween.chain(_curve);
static final RectTween _rectTweenReverse = RectTween();
static final Animatable<Rect?> _rectAnimatableReverse = _rectTweenReverse.chain(
static final Animatable<Rect?> _rectAnimatableReverse =
_rectTweenReverse.chain(
_curveReverse,
);
static final RectTween _sheetRectTween = RectTween();
@@ -573,10 +583,12 @@ class _ContextMenuRoute<T> extends PopupRoute<T> {
_curveReverse,
);
static final Tween<double> _sheetScaleTween = Tween<double>();
static final Animatable<double> _sheetScaleAnimatable = _sheetScaleTween.chain(
static final Animatable<double> _sheetScaleAnimatable =
_sheetScaleTween.chain(
_curve,
);
static final Animatable<double> _sheetScaleAnimatableReverse = _sheetScaleTween.chain(
static final Animatable<double> _sheetScaleAnimatableReverse =
_sheetScaleTween.chain(
_curveReverse,
);
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
// contextMenuLocation.
static AlignmentDirectional getSheetAlignment(_ContextMenuLocation contextMenuLocation) {
static AlignmentDirectional getSheetAlignment(
_ContextMenuLocation contextMenuLocation) {
switch (contextMenuLocation) {
case _ContextMenuLocation.center:
return AlignmentDirectional.topCenter;
@@ -623,17 +636,27 @@ class _ContextMenuRoute<T> extends PopupRoute<T> {
}
// 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) {
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);
return centered & sheetRect.size;
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;
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;
}
}
@@ -651,7 +674,9 @@ class _ContextMenuRoute<T> extends PopupRoute<T> {
// Take measurements on the child and _ContextMenuSheet and update the
// animation tweens to match.
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.end = childRect;
@@ -725,7 +750,8 @@ class _ContextMenuRoute<T> extends PopupRoute<T> {
}
@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
// buildTransitions as child, the idea being that buildTransitions will
// animate the entire page into the scene. In the case of _ContextMenuRoute,
@@ -735,7 +761,8 @@ class _ContextMenuRoute<T> extends PopupRoute<T> {
}
@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(
builder: (BuildContext context, Orientation orientation) {
_lastOrientation = orientation;
@@ -744,9 +771,15 @@ class _ContextMenuRoute<T> extends PopupRoute<T> {
// they're movable.
if (!animation.isCompleted) {
final bool reverse = animation.status == AnimationStatus.reverse;
final Rect rect = reverse ? _rectAnimatableReverse.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);
final Rect rect = reverse
? _rectAnimatableReverse.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(
children: <Widget>[
Positioned.fromRect(
@@ -818,7 +851,8 @@ class _ContextMenuRouteStatic extends StatefulWidget {
_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
// value.
static const double _kMinScale = 0.8;
@@ -838,7 +872,8 @@ class _ContextMenuRouteStaticState extends State<_ContextMenuRouteStatic> with T
late Animation<double> _sheetOpacityAnimation;
// 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;
return math.max(
_kMinScale,
@@ -859,11 +894,13 @@ class _ContextMenuRouteStaticState extends State<_ContextMenuRouteStatic> with T
// If flung, animate a bit before handling the potential dismiss.
if (details.velocity.pixelsPerSecond.dy.abs() >= kMinFlingVelocity) {
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) {
_sheetController.forward();
} else if (!flingIsAway && _sheetController.status != AnimationStatus.reverse) {
} else if (!flingIsAway &&
_sheetController.status != AnimationStatus.reverse) {
_sheetController.reverse();
}
@@ -918,21 +955,30 @@ class _ContextMenuRouteStaticState extends State<_ContextMenuRouteStatic> with T
widget.onDismiss!(context, _lastScale, _sheetOpacityAnimation.value);
}
Alignment _getChildAlignment(Orientation orientation, _ContextMenuLocation contextMenuLocation) {
Alignment _getChildAlignment(
Orientation orientation, _ContextMenuLocation contextMenuLocation) {
switch (contextMenuLocation) {
case _ContextMenuLocation.center:
return orientation == Orientation.portrait ? Alignment.bottomCenter : Alignment.topRight;
return orientation == Orientation.portrait
? Alignment.bottomCenter
: Alignment.topRight;
case _ContextMenuLocation.right:
return orientation == Orientation.portrait ? Alignment.bottomCenter : Alignment.topLeft;
return orientation == Orientation.portrait
? Alignment.bottomCenter
: Alignment.topLeft;
case _ContextMenuLocation.left:
return orientation == Orientation.portrait ? Alignment.bottomCenter : Alignment.topRight;
return orientation == Orientation.portrait
? Alignment.bottomCenter
: Alignment.topRight;
}
}
void _setDragOffset(Offset dragOffset) {
// Allow horizontal and negative vertical movement, but damp it.
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(() {
_dragOffset = dragOffset;
_moveAnimation = Tween<Offset>(
@@ -949,9 +995,13 @@ class _ContextMenuRouteStaticState extends State<_ContextMenuRouteStatic> with T
);
// 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();
} 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();
}
});
@@ -960,7 +1010,8 @@ class _ContextMenuRouteStaticState extends State<_ContextMenuRouteStatic> with T
// 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
// the original child.
List<Widget> _getChildren(Orientation orientation, _ContextMenuLocation contextMenuLocation) {
List<Widget> _getChildren(
Orientation orientation, _ContextMenuLocation contextMenuLocation) {
final Expanded child = Expanded(
child: Align(
alignment: _getChildAlignment(
@@ -995,7 +1046,9 @@ class _ContextMenuRouteStaticState extends State<_ContextMenuRouteStatic> with T
case _ContextMenuLocation.center:
return <Widget>[child, spacer, sheet];
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:
return <Widget>[child, spacer, sheet];
}
@@ -1004,7 +1057,8 @@ class _ContextMenuRouteStaticState extends State<_ContextMenuRouteStatic> with T
// Build the animation for the _ContextMenuSheet.
Widget _buildSheetAnimation(BuildContext context, Widget? child) {
return Transform.scale(
alignment: _ContextMenuRoute.getSheetAlignment(widget.contextMenuLocation),
alignment:
_ContextMenuRoute.getSheetAlignment(widget.contextMenuLocation),
scale: _sheetScaleAnimation.value,
child: FadeTransition(
opacity: _sheetOpacityAnimation,

View File

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

View File

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

View File

@@ -17,7 +17,10 @@ class SyntaxHighlighterStyle {
this.constantStyle});
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)),
commentStyle: const TextStyle(color: const Color(0xFF9E9E9E)),
keywordStyle: const TextStyle(color: const Color(0xFF9C27B0)),

View File

@@ -26,7 +26,8 @@ class UserInfoViewModel {
_useSecertLogined = SpUtil.getBool(spSecretLogined, defValue: false);
_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) {
for (Map<String, dynamic> value in tempList) {
@@ -40,7 +41,8 @@ class UserInfoViewModel {
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);
_useSecretLogin(secretLogin);
_userName = userName;
@@ -93,7 +95,13 @@ class UserInfoViewModel {
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) {
historyAccounts.removeLast();
@@ -117,7 +125,8 @@ class UserInfoBean {
bool useSecretLogined = false;
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) {
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/task/task_bean.dart';
class JsonConversion$Json {
static M fromJson<M>(dynamic json) {
static M fromJson<M>(dynamic json) {
if (json is List) {
return _getListChildType<M>(json);
} else {
@@ -23,93 +21,104 @@ class JsonConversion$Json {
}
static M _fromJsonSingle<M>(dynamic json) {
String type = M.toString();
if(type == (ConfigBean).toString()){
return ConfigBean.jsonConversion(json) as M;
if (type == (ConfigBean).toString()) {
return ConfigBean.jsonConversion(json) as M;
}
if(type == (EnvBean).toString()){
return EnvBean.jsonConversion(json) as M;
if (type == (EnvBean).toString()) {
return EnvBean.jsonConversion(json) as M;
}
if(type == (SystemBean).toString()){
return SystemBean.jsonConversion(json) as M;
if (type == (SystemBean).toString()) {
return SystemBean.jsonConversion(json) as M;
}
if(type == (LoginBean).toString()){
return LoginBean.jsonConversion(json) as M;
if (type == (LoginBean).toString()) {
return LoginBean.jsonConversion(json) as M;
}
if(type == (UserBean).toString()){
return UserBean.jsonConversion(json) as M;
if (type == (UserBean).toString()) {
return UserBean.jsonConversion(json) as M;
}
if(type == (DependencyBean).toString()){
return DependencyBean.jsonConversion(json) as M;
if (type == (DependencyBean).toString()) {
return DependencyBean.jsonConversion(json) as M;
}
if(type == (LoginLogBean).toString()){
return LoginLogBean.jsonConversion(json) as M;
if (type == (LoginLogBean).toString()) {
return LoginLogBean.jsonConversion(json) as M;
}
if(type == (ScriptBean).toString()){
return ScriptBean.jsonConversion(json) as M;
if (type == (ScriptBean).toString()) {
return ScriptBean.jsonConversion(json) as M;
}
if(type == (TaskLogBean).toString()){
return TaskLogBean.jsonConversion(json) as M;
if (type == (TaskLogBean).toString()) {
return TaskLogBean.jsonConversion(json) as M;
}
if(type == (TaskBean).toString()){
return TaskBean.jsonConversion(json) as M;
if (type == (TaskBean).toString()) {
return TaskBean.jsonConversion(json) as M;
}
throw Exception("not found");
}
static M _getListChildType<M>(List<dynamic> data) {
if(<ConfigBean>[] is M){
return data.map<ConfigBean>((e) => ConfigBean.jsonConversion(e)).toList() as M;
if (<ConfigBean>[] is M) {
return data.map<ConfigBean>((e) => ConfigBean.jsonConversion(e)).toList()
as M;
}
if(<EnvBean>[] is M){
if (<EnvBean>[] is M) {
return data.map<EnvBean>((e) => EnvBean.jsonConversion(e)).toList() as M;
}
if(<SystemBean>[] is M){
return data.map<SystemBean>((e) => SystemBean.jsonConversion(e)).toList() as M;
if (<SystemBean>[] is M) {
return data.map<SystemBean>((e) => SystemBean.jsonConversion(e)).toList()
as M;
}
if(<LoginBean>[] is M){
return data.map<LoginBean>((e) => LoginBean.jsonConversion(e)).toList() as M;
if (<LoginBean>[] is M) {
return data.map<LoginBean>((e) => LoginBean.jsonConversion(e)).toList()
as M;
}
if(<UserBean>[] is M){
return data.map<UserBean>((e) => UserBean.jsonConversion(e)).toList() as M;
if (<UserBean>[] is M) {
return data.map<UserBean>((e) => UserBean.jsonConversion(e)).toList()
as M;
}
if(<DependencyBean>[] is M){
return data.map<DependencyBean>((e) => DependencyBean.jsonConversion(e)).toList() as M;
if (<DependencyBean>[] is M) {
return data
.map<DependencyBean>((e) => DependencyBean.jsonConversion(e))
.toList() as M;
}
if(<LoginLogBean>[] is M){
return data.map<LoginLogBean>((e) => LoginLogBean.jsonConversion(e)).toList() as M;
if (<LoginLogBean>[] is M) {
return data
.map<LoginLogBean>((e) => LoginLogBean.jsonConversion(e))
.toList() as M;
}
if(<ScriptBean>[] is M){
return data.map<ScriptBean>((e) => ScriptBean.jsonConversion(e)).toList() as M;
if (<ScriptBean>[] is M) {
return data.map<ScriptBean>((e) => ScriptBean.jsonConversion(e)).toList()
as M;
}
if(<TaskLogBean>[] is M){
return data.map<TaskLogBean>((e) => TaskLogBean.jsonConversion(e)).toList() as M;
if (<TaskLogBean>[] is M) {
return data
.map<TaskLogBean>((e) => TaskLogBean.jsonConversion(e))
.toList() as M;
}
if(<TaskBean>[] is M){
return data.map<TaskBean>((e) => TaskBean.jsonConversion(e)).toList() as M;
if (<TaskBean>[] is M) {
return data.map<TaskBean>((e) => TaskBean.jsonConversion(e)).toList()
as M;
}
throw Exception("not found");
}
}

View File

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

View File

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

View File

@@ -1,12 +1,16 @@
import 'package:code_text_field/code_text_field.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:highlight/languages/powershell.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/sp_const.dart';
import 'package:qinglong_app/base/theme.dart';
import 'package:qinglong_app/utils/extension.dart';
import 'package:qinglong_app/utils/sp_utils.dart';
class ConfigEditPage extends ConsumerStatefulWidget {
final String content;
@@ -21,47 +25,122 @@ class ConfigEditPage extends ConsumerStatefulWidget {
class _ConfigEditPageState extends ConsumerState<ConfigEditPage> {
CodeController? _codeController;
late String result;
late String preResult;
List<String> operateList = [];
@override
void dispose() {
_codeController?.dispose();
super.dispose();
}
@override
void initState() {
result = widget.content;
preResult = widget.content;
super.initState();
generateOperateList();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
focusNode.requestFocus();
checkClipBoard();
});
}
Future<void> notifyICloud(
BuildContext context, String? title, String? content) async {}
@override
Widget build(BuildContext context) {
_codeController ??= CodeController(
text: widget.content,
text: result,
language: powershell,
onChange: (value) {
result = value;
},
theme: ref.watch(themeProvider).themeColor.codeEditorTheme(),
stringMap: {
"export": const TextStyle(fontWeight: FontWeight.normal, color: Color(0xff6B2375)),
"export": const TextStyle(
fontWeight: FontWeight.normal, color: Color(0xff6B2375)),
},
);
return Scaffold(
appBar: QlAppBar(
canBack: true,
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}',
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(
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) {
"提交成功".toast();
Navigator.of(context).pop(widget.title);
@@ -88,14 +167,177 @@ class _ConfigEditPageState extends ConsumerState<ConfigEditPage> {
),
body: SafeArea(
top: false,
child: CodeField(
controller: _codeController!,
expands: true,
background: ref.watch(themeProvider).themeColor.settingBgColor(),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: SpUtil.getBool(spShowLine, defValue: false) ? 0 : 10,
),
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();
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 'package:code_text_field/code_text_field.dart';
import 'package:flutter/material.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/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/empty_widget.dart';
import 'package:qinglong_app/main.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:qinglong_app/utils/sp_utils.dart';
import '../../base/ui/syntax_highlighter.dart';
import 'config_viewmodel.dart';
@@ -105,7 +110,7 @@ class ConfigPageState extends State<ConfigPage>
bool get wantKeepAlive => true;
}
class CodeWidget extends StatefulWidget {
class CodeWidget extends ConsumerStatefulWidget {
final String content;
const CodeWidget({
@@ -114,30 +119,63 @@ class CodeWidget extends StatefulWidget {
}) : super(key: key);
@override
State<CodeWidget> createState() => _CodeWidgetState();
ConsumerState<CodeWidget> createState() => _CodeWidgetState();
}
class _CodeWidgetState extends State<CodeWidget>
class _CodeWidgetState extends ConsumerState<CodeWidget>
with AutomaticKeepAliveClientMixin {
CodeController? _codeController;
@override
void dispose() {
_codeController?.dispose();
super.dispose();
}
String result = "";
@override
void initState() {
result = widget.content;
super.initState();
}
@override
Widget build(BuildContext context) {
super.build(context);
return SelectableText.rich(
TextSpan(
style: GoogleFonts.droidSansMono(fontSize: 14).apply(
fontSizeFactor: 1,
_codeController ??= CodeController(
text: result,
language: powershell,
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>[
DartSyntaxHighlighter(SyntaxHighlighterStyle.lightThemeStyle())
.format(widget.content)
],
),
style: DefaultTextStyle.of(context).style.apply(
fontSizeFactor: 1,
child: CodeField(
controller: _codeController!,
expands: true,
readOnly: 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,
),
),
selectionWidthStyle: BoxWidthStyle.max,
selectionHeightStyle: BoxHeightStyle.max,
autofocus: true,
),
),
);
}

View File

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

View File

@@ -12,7 +12,14 @@ class EnvBean {
String? name;
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;
@@ -20,7 +27,9 @@ class EnvBean {
value = json['value'];
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());
status = json['status'];
timestamp = json['timestamp'];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,7 +21,8 @@ class DependencyPage extends StatefulWidget {
_DependcyPageState createState() => _DependcyPageState();
}
class _DependcyPageState extends State<DependencyPage> with TickerProviderStateMixin {
class _DependcyPageState extends State<DependencyPage>
with TickerProviderStateMixin {
List<DepedencyEnum> types = [];
TabController? _tabController;
@@ -125,7 +126,9 @@ class _DependcyPageState extends State<DependencyPage> with TickerProviderStateM
itemCount: list.length,
),
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,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color: ref.watch(themeProvider).themeColor.titleColor(),
color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 18,
),
),
@@ -255,11 +261,16 @@ class DependencyCell extends ConsumerWidget {
child: Material(
color: Colors.transparent,
child: Text(
(bean.created == null || bean.created == 0) ? "-" : Utils.formatMessageTime(bean.created!),
(bean.created == null || bean.created == 0)
? "-"
: Utils.formatMessageTime(bean.created!),
maxLines: 1,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color: ref.watch(themeProvider).themeColor.descColor(),
color: ref
.watch(themeProvider)
.themeColor
.descColor(),
fontSize: 12,
),
),
@@ -357,7 +368,9 @@ class DependencyCell extends ConsumerWidget {
),
onPressed: () {
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 = [];
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);
}

View File

@@ -37,99 +37,98 @@ class _LoginLogPageState extends ConsumerState<LoginLogPage>
),
body: list.isEmpty
? const Center(
child: CupertinoActivityIndicator(),
)
child: CupertinoActivityIndicator(),
)
: ListView.separated(
itemBuilder: (context, index) {
LoginLogBean item = list[index];
itemBuilder: (context, index) {
LoginLogBean item = list[index];
return Row(
children: [
const SizedBox(
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,
return Row(
children: [
const SizedBox(
width: 15,
),
),
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,
);
},
),
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,
),
),
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 {
HttpResponse<List<LoginLogBean>> response =
await Api.loginLog();
HttpResponse<List<LoginLogBean>> response = await Api.loginLog();
if (response.success) {
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_riverpod/flutter_riverpod.dart';
import 'package:qinglong_app/base/routes.dart';
import 'package:qinglong_app/base/sp_const.dart';
import 'package:qinglong_app/base/theme.dart';
import 'package:qinglong_app/base/userinfo_viewmodel.dart';
import 'package:qinglong_app/main.dart';
import 'package:qinglong_app/utils/extension.dart';
import 'package:qinglong_app/utils/sp_utils.dart';
class OtherPage extends ConsumerStatefulWidget {
const OtherPage({Key? key}) : super(key: key);
@@ -51,7 +53,10 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text(
"脚本管理",
style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(),
color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16,
),
),
@@ -84,7 +89,10 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text(
"依赖管理",
style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(),
color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16,
),
),
@@ -119,7 +127,10 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text(
"任务日志",
style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(),
color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16,
),
),
@@ -159,7 +170,10 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text(
"登录日志",
style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(),
color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16,
),
),
@@ -200,7 +214,10 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text(
"修改密码",
style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(),
color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16,
),
),
@@ -237,7 +254,8 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text(
"夜间模式",
style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(),
color:
ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 16,
),
),
@@ -251,10 +269,43 @@ class _OtherPageState extends ConsumerState<OtherPage> {
],
),
),
const Divider(
indent: 15,
),
Padding(
padding: const EdgeInsets.only(
left: 15,
right: 15,
bottom: 5,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"查看代码是否显示行号",
style: TextStyle(
color:
ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 16,
),
),
const Spacer(),
CupertinoSwitch(
activeColor: ref.watch(themeProvider).primaryColor,
value: SpUtil.getBool(spShowLine, defValue: false),
onChanged: (open) async {
await SpUtil.putBool(spShowLine, open);
setState(() {});
},
),
],
),
),
const Divider(
indent: 15,
height: 1,
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
@@ -265,7 +316,7 @@ class _OtherPageState extends ConsumerState<OtherPage> {
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 5,
vertical: 10,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
@@ -274,7 +325,10 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text(
"主题设置",
style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(),
color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16,
),
),
@@ -309,7 +363,10 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text(
"切换账号",
style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(),
color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16,
),
),
@@ -346,7 +403,10 @@ class _OtherPageState extends ConsumerState<OtherPage> {
Text(
"关于",
style: TextStyle(
color: ref.watch(themeProvider).themeColor.titleColor(),
color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16,
),
),
@@ -405,7 +465,8 @@ class _OtherPageState extends ConsumerState<OtherPage> {
),
onPressed: () {
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/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/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/sp_const.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/utils/extension.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../base/ui/syntax_highlighter.dart';
import '../../config/config_page.dart';
import 'package:qinglong_app/utils/sp_utils.dart';
/// @author NewTab
class ScriptDetailPage extends ConsumerStatefulWidget {
@@ -34,8 +36,61 @@ class ScriptDetailPage extends ConsumerStatefulWidget {
class _ScriptDetailPageState extends ConsumerState<ScriptDetailPage>
with LazyLoadState<ScriptDetailPage> {
String? content;
CodeController? _codeController;
GlobalKey<CodeFieldState> codeFieldKey = GlobalKey();
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
void initState() {
@@ -85,6 +140,7 @@ class _ScriptDetailPageState extends ConsumerState<ScriptDetailPage>
Navigator.of(context).pop();
showCupertinoDialog(
useRootNavigator: false,
context: context,
builder: (context) => CupertinoAlertDialog(
title: const Text("确认删除"),
@@ -147,18 +203,45 @@ class _ScriptDetailPageState extends ConsumerState<ScriptDetailPage>
@override
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(
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: "脚本详情",
title: widget.title,
actions: [
InkWell(
onTap: () {
showCupertinoModalPopup(
context: context,
useRootNavigator: false,
builder: (context) {
return CupertinoActionSheet(
title: Container(
@@ -219,8 +302,31 @@ class _ScriptDetailPageState extends ConsumerState<ScriptDetailPage>
? const Center(
child: CupertinoActivityIndicator(),
)
: ScriptCodeWidget(
content: content ?? "",
: 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,
),
),
),
);
}
@@ -234,71 +340,23 @@ class _ScriptDetailPageState extends ConsumerState<ScriptDetailPage>
if (response.success) {
content = response.bean;
setState(() {});
Future.delayed(
const Duration(
seconds: 1,
),
() {
codeFieldKey.currentState
?.getCodeScroll()
?.addListener(floatingButtonVisibility);
},
);
} else {
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
void onLazyLoad() {
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:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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/http.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/extension.dart';
import 'package:qinglong_app/utils/sp_utils.dart';
class ScriptEditPage extends ConsumerStatefulWidget {
final String content;
final String title;
final String path;
const ScriptEditPage(this.title, this.path, this.content, {Key? key}) : super(key: key);
const ScriptEditPage(this.title, this.path, this.content, {Key? key})
: super(key: key);
@override
_ScriptEditPageState createState() => _ScriptEditPageState();
@@ -28,6 +32,7 @@ class _ScriptEditPageState extends ConsumerState<ScriptEditPage> {
CodeController? _codeController;
late String result;
FocusNode focusNode = FocusNode();
late String preResult;
@override
void dispose() {
@@ -38,7 +43,7 @@ class _ScriptEditPageState extends ConsumerState<ScriptEditPage> {
@override
void initState() {
result = widget.content;
preResult = widget.content;
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
@@ -77,20 +82,60 @@ class _ScriptEditPageState extends ConsumerState<ScriptEditPage> {
},
theme: ref.watch(themeProvider).themeColor.codeEditorTheme(),
stringMap: {
"export": const TextStyle(fontWeight: FontWeight.normal, color: Color(0xff6B2375)),
"export": const TextStyle(
fontWeight: FontWeight.normal, color: Color(0xff6B2375)),
},
);
return Scaffold(
appBar: QlAppBar(
canBack: true,
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}',
actions: [
InkWell(
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) {
"提交成功".toast();
Navigator.of(context).pop(true);
@@ -117,10 +162,23 @@ class _ScriptEditPageState extends ConsumerState<ScriptEditPage> {
),
body: SafeArea(
top: false,
child: CodeField(
controller: _codeController!,
expands: true,
background: ref.watch(themeProvider).themeColor.tabBarColor(),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: SpUtil.getBool(spShowLine, defValue: false) ? 0 : 10,
),
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/utils/extension.dart';
import 'script_upload_page.dart';
/// @author NewTab
class ScriptPage extends ConsumerStatefulWidget {
const ScriptPage({Key? key}) : super(key: key);
@@ -19,7 +21,8 @@ class ScriptPage extends ConsumerStatefulWidget {
_ScriptPageState createState() => _ScriptPageState();
}
class _ScriptPageState extends ConsumerState<ScriptPage> with LazyLoadState<ScriptPage> {
class _ScriptPageState extends ConsumerState<ScriptPage>
with LazyLoadState<ScriptPage> {
List<ScriptBean> list = [];
final TextEditingController _searchController = TextEditingController();
@@ -75,7 +78,8 @@ class _ScriptPageState extends ConsumerState<ScriptPage> with LazyLoadState<Scri
},
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
keyboardDismissBehavior:
ScrollViewKeyboardDismissBehavior.onDrag,
itemBuilder: (context, index) {
if (index == 0) {
return searchCell(ref);
@@ -87,82 +91,110 @@ class _ScriptPageState extends ConsumerState<ScriptPage> with LazyLoadState<Scri
(item.title?.contains(_searchController.text) ?? false) ||
(item.value?.contains(_searchController.text) ?? false) ||
((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 ??
false))) {
return ColoredBox(
color: ref.watch(themeProvider).themeColor.settingBgColor(),
child: (item.children != null && item.children!.isNotEmpty)
? ExpansionTile(
title: Text(
item.title ?? "",
style: TextStyle(
color: (item.disabled ?? false)
? ref.watch(themeProvider).themeColor.descColor()
: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 16,
),
),
children: item.children!
.where((element) {
if (_searchController.text.isEmpty) {
return true;
}
return (element.title?.contains(_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,
color:
ref.watch(themeProvider).themeColor.settingBgColor(),
child:
(item.children != null && item.children!.isNotEmpty)
? ExpansionTile(
title: Text(
item.title ?? "",
style: TextStyle(
color: (item.disabled ?? false)
? ref
.watch(themeProvider)
.themeColor
.descColor()
: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16,
),
),
children: item.children!
.where((element) {
if (_searchController.text.isEmpty) {
return true;
}
return (element.title?.contains(
_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) {
if (value != null && value == true) {
loadData();
}
});
},
title: Text(
e.title ?? "",
style: TextStyle(
color: (item.disabled ?? false)
? ref.watch(themeProvider).themeColor.descColor()
: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 14,
),
),
))
.toList(),
)
: ListTile(
onTap: () {
Navigator.of(context).pushNamed(
Routes.routeScriptDetail,
arguments: {
"title": item.title,
"path": "",
title: Text(
e.title ?? "",
style: TextStyle(
color: (item.disabled ?? false)
? ref
.watch(themeProvider)
.themeColor
.descColor()
: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 14,
),
),
))
.toList(),
)
: ListTile(
onTap: () {
Navigator.of(context).pushNamed(
Routes.routeScriptDetail,
arguments: {
"title": item.title,
"path": "",
},
).then(
(value) {
if (value != null && value == true) {
loadData();
}
},
);
},
).then(
(value) {
if (value != null && value == true) {
loadData();
}
},
);
},
title: Text(
item.title ?? "",
style: TextStyle(
color: (item.disabled ?? false)
? ref.watch(themeProvider).themeColor.descColor()
: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 16,
title: Text(
item.title ?? "",
style: TextStyle(
color: (item.disabled ?? false)
? ref
.watch(themeProvider)
.themeColor
.descColor()
: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16,
),
),
),
),
),
);
} else {
return const SizedBox.shrink();
@@ -209,144 +241,12 @@ class _ScriptPageState extends ConsumerState<ScriptPage> with LazyLoadState<Scri
String scriptPath = "";
void addScript() {
showCupertinoDialog(
useRootNavigator: false,
context: context,
builder: (context) => CupertinoAlertDialog(
title: const Text("新增脚本"),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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();
}
},
),
],
),
);
List<String?> paths = list
.where((element) => element.children?.isNotEmpty ?? false)
.map((e) => e.title)
.toList();
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ScriptUploadPage(paths: paths)));
}
}

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,20 +26,20 @@ class TaskBean {
TaskBean(
{this.name,
this.command,
this.schedule,
this.saved,
this.sId,
this.created,
this.status,
this.timestamp,
this.isSystem,
this.isDisabled,
this.logPath,
this.isPinned,
this.lastExecutionTime,
this.lastRunningTime,
this.pid});
this.command,
this.schedule,
this.saved,
this.sId,
this.created,
this.status,
this.timestamp,
this.isSystem,
this.isDisabled,
this.logPath,
this.isPinned,
this.lastExecutionTime,
this.lastRunningTime,
this.pid});
get nId => _id;
@@ -51,7 +51,9 @@ class TaskBean {
saved = json['saved'];
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());
status = json['status'];
timestamp = json['timestamp'].toString();

View File

@@ -17,7 +17,8 @@ class TaskDetailPage extends ConsumerStatefulWidget {
final TaskBean taskBean;
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
_TaskDetailPageState createState() => _TaskDetailPageState();
@@ -71,11 +72,13 @@ class _TaskDetailPageState extends ConsumerState<TaskDetailPage> {
),
TaskDetailCell(
title: "创建时间",
desc: Utils.formatMessageTime(widget.taskBean.created ?? 0),
desc:
Utils.formatMessageTime(widget.taskBean.created ?? 0),
),
TaskDetailCell(
title: "更新时间",
desc: Utils.formatGMTTime(widget.taskBean.timestamp ?? ""),
desc:
Utils.formatGMTTime(widget.taskBean.timestamp ?? ""),
),
TaskDetailCell(
title: "任务定时",
@@ -83,11 +86,14 @@ class _TaskDetailPageState extends ConsumerState<TaskDetailPage> {
),
TaskDetailCell(
title: "最后运行时间",
desc: Utils.formatMessageTime(widget.taskBean.lastExecutionTime ?? 0),
desc: Utils.formatMessageTime(
widget.taskBean.lastExecutionTime ?? 0),
),
TaskDetailCell(
title: "最后运行时长",
desc: widget.taskBean.lastRunningTime == null ? "-" : "${widget.taskBean.lastRunningTime ?? "-"}",
desc: widget.taskBean.lastRunningTime == null
? "-"
: "${widget.taskBean.lastRunningTime ?? "-"}",
),
GestureDetector(
behavior: HitTestBehavior.opaque,
@@ -205,7 +211,8 @@ class _TaskDetailPageState extends ConsumerState<TaskDetailPage> {
behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed(Routes.routeAddTask, arguments: widget.taskBean);
Navigator.of(context)
.pushNamed(Routes.routeAddTask, arguments: widget.taskBean);
},
child: Container(
padding: const EdgeInsets.symmetric(
@@ -357,12 +364,16 @@ class _TaskDetailPageState extends ConsumerState<TaskDetailPage> {
}
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(() {});
}
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(() {});
}
@@ -472,14 +483,16 @@ class TaskDetailCell extends ConsumerWidget {
}
},
style: TextStyle(
color: ref.watch(themeProvider).themeColor.descColor(),
color:
ref.watch(themeProvider).themeColor.descColor(),
fontSize: 14,
),
),
),
)
: 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];
if (_searchController.text.isEmpty ||
(item.name?.toLowerCase().contains(_searchController.text.toLowerCase()) ?? false) ||
(item.command?.toLowerCase().contains(_searchController.text.toLowerCase()) ?? false) ||
(item.schedule?.contains(_searchController.text.toLowerCase()) ?? false)) {
(item.name
?.toLowerCase()
.contains(_searchController.text.toLowerCase()) ??
false) ||
(item.command
?.toLowerCase()
.contains(_searchController.text.toLowerCase()) ??
false) ||
(item.schedule
?.contains(_searchController.text.toLowerCase()) ??
false)) {
return TaskItemCell(item, ref);
} else {
return const SizedBox.shrink();
@@ -80,9 +88,17 @@ class _TaskPageState extends ConsumerState<TaskPage> {
if (index == 0) return const SizedBox.shrink();
TaskBean item = list[index - 1];
if (_searchController.text.isEmpty ||
(item.name?.toLowerCase().contains(_searchController.text.toLowerCase()) ?? false) ||
(item.command?.toLowerCase().contains(_searchController.text.toLowerCase()) ?? false) ||
(item.schedule?.contains(_searchController.text.toLowerCase()) ?? false)) {
(item.name
?.toLowerCase()
.contains(_searchController.text.toLowerCase()) ??
false) ||
(item.command
?.toLowerCase()
.contains(_searchController.text.toLowerCase()) ??
false) ||
(item.schedule
?.contains(_searchController.text.toLowerCase()) ??
false)) {
return Container(
color: ref.watch(themeProvider).themeColor.settingBgColor(),
child: const Divider(
@@ -130,7 +146,9 @@ class _TaskPageState extends ConsumerState<TaskPage> {
child: Text(
TaskViewModel.allStr,
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,
),
),
@@ -140,8 +158,9 @@ class _TaskPageState extends ConsumerState<TaskPage> {
child: Text(
TaskViewModel.runningStr,
style: TextStyle(
color:
currentState == TaskViewModel.runningStr ? ref.watch(themeProvider).primaryColor : ref.watch(themeProvider).themeColor.titleColor(),
color: currentState == TaskViewModel.runningStr
? ref.watch(themeProvider).primaryColor
: ref.watch(themeProvider).themeColor.titleColor(),
fontSize: 14,
),
),
@@ -151,7 +170,9 @@ class _TaskPageState extends ConsumerState<TaskPage> {
child: Text(
TaskViewModel.neverStr,
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,
),
),
@@ -165,7 +186,10 @@ class _TaskPageState extends ConsumerState<TaskPage> {
style: TextStyle(
color: currentState == TaskViewModel.notScriptStr
? ref.watch(themeProvider).primaryColor
: ref.watch(themeProvider).themeColor.titleColor(),
: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 14,
),
),
@@ -178,8 +202,9 @@ class _TaskPageState extends ConsumerState<TaskPage> {
TaskViewModel.disableStr,
style: TextStyle(
fontSize: 14,
color:
currentState == TaskViewModel.disableStr ? ref.watch(themeProvider).primaryColor : ref.watch(themeProvider).themeColor.titleColor(),
color: currentState == TaskViewModel.disableStr
? ref.watch(themeProvider).primaryColor
: ref.watch(themeProvider).themeColor.titleColor(),
),
),
value: TaskViewModel.disableStr,
@@ -236,7 +261,9 @@ class TaskItemCell extends StatelessWidget {
@override
Widget build(BuildContext context) {
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(
key: ValueKey(bean.sId),
endActionPane: ActionPane(
@@ -247,7 +274,8 @@ class TaskItemCell extends StatelessWidget {
backgroundColor: const Color(0xff5D5E70),
onPressed: (_) {
WidgetsBinding.instance.endOfFrame.then((timeStamp) {
Navigator.of(context).pushNamed(Routes.routeAddTask, arguments: bean);
Navigator.of(context)
.pushNamed(Routes.routeAddTask, arguments: bean);
});
},
foregroundColor: Colors.white,
@@ -259,7 +287,9 @@ class TaskItemCell extends StatelessWidget {
pinTask(context);
},
foregroundColor: Colors.white,
icon: (bean.isPinned ?? 0) == 0 ? CupertinoIcons.pin : CupertinoIcons.pin_slash,
icon: (bean.isPinned ?? 0) == 0
? CupertinoIcons.pin
: CupertinoIcons.pin_slash,
),
SlidableAction(
backgroundColor: const Color(0xffA356D6),
@@ -267,7 +297,9 @@ class TaskItemCell extends StatelessWidget {
enableTask(context);
},
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(
backgroundColor: const Color(0xffEA4D3E),
@@ -299,7 +331,9 @@ class TaskItemCell extends StatelessWidget {
}
},
foregroundColor: Colors.white,
icon: bean.status! == 1 ? CupertinoIcons.memories : CupertinoIcons.stop_circle,
icon: bean.status! == 1
? CupertinoIcons.memories
: CupertinoIcons.stop_circle,
),
SlidableAction(
backgroundColor: const Color(0xff606467),
@@ -317,10 +351,13 @@ class TaskItemCell extends StatelessWidget {
],
),
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(
onTap: () {
Navigator.of(context).pushNamed(Routes.routeTaskDetail, arguments: bean);
Navigator.of(context)
.pushNamed(Routes.routeTaskDetail, arguments: bean);
},
child: Container(
width: MediaQuery.of(context).size.width,
@@ -356,7 +393,10 @@ class TaskItemCell extends StatelessWidget {
overflow: TextOverflow.ellipsis,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color: ref.watch(themeProvider).themeColor.titleColor(),
color: ref
.watch(themeProvider)
.themeColor
.titleColor(),
fontSize: 16,
),
),
@@ -392,11 +432,16 @@ class TaskItemCell extends StatelessWidget {
Material(
color: Colors.transparent,
child: Text(
(bean.lastExecutionTime == null || bean.lastExecutionTime == 0) ? "-" : Utils.formatMessageTime(bean.lastExecutionTime!),
(bean.lastExecutionTime == null ||
bean.lastExecutionTime == 0)
? "-"
: Utils.formatMessageTime(
bean.lastExecutionTime!),
maxLines: 1,
style: TextStyle(
overflow: TextOverflow.ellipsis,
color: ref.watch(themeProvider).themeColor.descColor(),
color:
ref.watch(themeProvider).themeColor.descColor(),
fontSize: 12,
),
),

View File

@@ -62,7 +62,8 @@ class TaskViewModel extends BaseViewModel {
}
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) {
return 1;
}
@@ -75,7 +76,8 @@ class TaskViewModel extends BaseViewModel {
p.sort((a, b) {
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) {
return 1;
}
@@ -86,7 +88,8 @@ class TaskViewModel extends BaseViewModel {
});
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) {
return 1;
}
@@ -94,7 +97,8 @@ class TaskViewModel extends BaseViewModel {
});
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) {
return 1;
}
@@ -102,7 +106,8 @@ class TaskViewModel extends BaseViewModel {
});
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) {
return 1;
}
@@ -116,9 +121,12 @@ class TaskViewModel extends BaseViewModel {
running.clear();
running.addAll(list.where((element) => element.status == 0));
neverRunning.clear();
neverRunning.addAll(list.where((element) => element.lastRunningTime == null));
neverRunning
.addAll(list.where((element) => element.lastRunningTime == null));
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.addAll(list.where((element) => element.isDisabled == 1));
}

View File

@@ -9,6 +9,7 @@ class Utils {
static bool isUpperVersion() {
return systemBean.isUpperVersion();
}
static bool 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