From c7b98a04db75144fc2db31f89b8169d21d7d0681 Mon Sep 17 00:00:00 2001 From: jyuesong <425698907@qq.com> Date: Mon, 17 Jan 2022 14:21:41 +0800 Subject: [PATCH] add function --- lib/base/code_editor/EditorModel.dart | 101 +++++++ .../code_editor/EditorModelStyleOptions.dart | 139 +++++++++ lib/base/code_editor/FileEditor.dart | 18 ++ lib/base/code_editor/Theme.dart | 59 ++++ lib/base/code_editor/ToolButton.dart | 14 + lib/base/code_editor/code_editor.dart | 284 ++++++++++++++++++ lib/base/http/http.dart | 3 +- lib/base/http/token_interceptor.dart | 6 +- lib/base/ql_app_bar.dart | 1 + lib/base/theme.dart | 36 ++- lib/base/ui/empty_widget.dart | 26 ++ lib/base/userinfo_viewmodel.dart | 9 +- lib/main.dart | 11 +- lib/module/config/config_edit_page.dart | 56 ++-- lib/module/config/config_page.dart | 25 +- lib/module/home/home_page.dart | 32 +- lib/module/login/login_page.dart | 13 +- lib/module/login/login_viewmodel.dart | 3 +- lib/module/task/task_page.dart | 47 ++- lib/module/task/task_viewmodel.dart | 3 + ...glong_theme.dart => codeeditor_theme.dart} | 0 pubspec.lock | 23 +- pubspec.yaml | 3 +- 23 files changed, 807 insertions(+), 105 deletions(-) create mode 100644 lib/base/code_editor/EditorModel.dart create mode 100644 lib/base/code_editor/EditorModelStyleOptions.dart create mode 100644 lib/base/code_editor/FileEditor.dart create mode 100644 lib/base/code_editor/Theme.dart create mode 100644 lib/base/code_editor/ToolButton.dart create mode 100644 lib/base/code_editor/code_editor.dart create mode 100644 lib/base/ui/empty_widget.dart rename lib/utils/{qinglong_theme.dart => codeeditor_theme.dart} (100%) diff --git a/lib/base/code_editor/EditorModel.dart b/lib/base/code_editor/EditorModel.dart new file mode 100644 index 0000000..1a1a663 --- /dev/null +++ b/lib/base/code_editor/EditorModel.dart @@ -0,0 +1,101 @@ + +import 'package:flutter/material.dart'; +import 'package:qinglong_app/utils/codeeditor_theme.dart'; +import 'FileEditor.dart'; +import 'EditorModelStyleOptions.dart'; +/// Use the EditorModel into CodeEditor in order to control the editor. +/// +/// EditorModel extends ChangeNotifier because we use the provider package +/// to simplify the work. +class EditorModel extends ChangeNotifier { + late int _currentPositionInFiles; + bool _isEditing = false; + EditorModelStyleOptions? styleOptions; + late List _languages; + late List allFiles; + + /// Define the required parameters for the editor to work properly. + /// For that, you need to define [files] wich is a `List`. + /// + /// You can also define your own preferences with [styleOptions]. + EditorModel({required List files, this.styleOptions}) { + if (this.styleOptions == null) { + this.styleOptions = new EditorModelStyleOptions(theme: qinglongLightTheme); + } + this._languages = []; + this._currentPositionInFiles = 0; + if (files.length == 0) { + files.add( + new FileEditor( + name: "index.html", + language: "html", + code: "", + ), + ); + } + files.forEach((FileEditor file) { + this._languages.add(file.language); + }); + this.allFiles = files; + } + + /// Checks in all the given files if [language] is found, + /// then returns a List of the files' content that uses [language]. + List getCodeWithLanguage(String language) { + List listOfCode = []; + this.allFiles.forEach((FileEditor file) { + if (file.language == language) { + listOfCode.add(file.code); + } + }); + return listOfCode; + } + + /// Returns the code of the file where [index] corresponds. + String? getCodeWithIndex(int index) { + return this.allFiles[index].code; + } + + /// Returns the file where [index] corresponds. + FileEditor getFileWithIndex(int index) { + return this.allFiles[index]; + } + + /// Switch the file using [i] as index of the List files. + /// + /// The user can't change the file if he is editing another one. + void changeIndexTo(int i) { + if (this._isEditing) { + return; + } + this._currentPositionInFiles = i; + this.notify(); + } + + /// Toggle the text field. + void toggleEditing() { + this._isEditing = !this._isEditing; + this.notify(); + } + + /// Overwite the previous code of the file where [index] corresponds by [newCode]. + void updateCodeOfIndex(int index, String? newCode) { + this.allFiles[index].code = newCode; + // this.allFiles[index].setCode = newCode; + } + + void notify() => notifyListeners(); + + /// Gets the index of wich file is currently displayed in the editor. + int? get position => this._currentPositionInFiles; + + /// Gets which language is currently shown. + String? get currentLanguage => + this.allFiles[this._currentPositionInFiles].language; + + /// Is the text field shown ? + bool get isEditing => this._isEditing; + + /// Gets the number of files. + int get numberOfFiles => this.allFiles.length; +} diff --git a/lib/base/code_editor/EditorModelStyleOptions.dart b/lib/base/code_editor/EditorModelStyleOptions.dart new file mode 100644 index 0000000..b4ab02b --- /dev/null +++ b/lib/base/code_editor/EditorModelStyleOptions.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; + +/// Set the style of CodeEditor. +/// You have to use it in EditorModel() just like this : +/// ``` +/// EditorModel model = new EditorModel( +/// files, // My files... +/// styleOptions: new EditorModelStyleOptions( +/// fontSize: 13, // Example +/// ), +/// ); +/// ``` +/// An EditorModel instance has the default values of EditorModelStyleOptions. +class EditorModelStyleOptions { + /// Set the padding of the file's content. By default `15.0`. + final EdgeInsets padding; + + /// Set the height of the container. By default `300`. + final double heightOfContainer; + + /// Set the theme of the syntax. code_editor has its own theme. + /// You can create your own or use others themes by looking at : + /// `import 'package:flutter_highlight/themes/'`. + final Map theme; + + /// Set the font family of the entire editor. By default `"monospace"`. + final String fontFamily; + + /// Set the letter spacing property of the text. By default `null`. + final double? letterSpacing; + + /// Set the fontSize of the file's content. By default `15`. + final double fontSize; + + /// Set the height (line-height in CSS) property of the text. By default `1.6`. + final double lineHeight; + + /// Set the size of the tabulation. By default `2`. Do not use a too big number. + final int tabSize; + + /// Set the background color of the editor. By default `Color(0xff2E3152)`. + final Color editorColor; + + /// Set the color of the borders between the navigation bar and the content. + /// By default `Color(0xFF3E416E)`. + final Color editorBorderColor; + + /// Set the color of the file name in the navigation bar. By default `Color(0xFF6CD07A)`. + final Color editorFilenameColor; + final Color editorNormalFilenameColor; + + /// Set the color property of the edit button. By default `Color(0xFF4650c7)`. + final Color editorToolButtonColor; + + /// Set the color of the edit button's text. By default `Color(0xFF4650c7)`. + final Color editorToolButtonTextColor; + + /// Set the font size of the file's name in the navigation bar. + final double? fontSizeOfFilename; + + /// Set the textStyle of the text field. By default : + /// ``` + /// TextStyle( + /// color: Colors.black87, + /// fontSize: 16, + /// letterSpacing: 1.25, + /// fontWeight: FontWeight.w500, + /// ) + /// ``` + final TextStyle textStyleOfTextField; + + /// The background color of the button "Edit". + /// By default `Color(0xFFEEEEEE)`. + final Color editButtonBackgroundColor; + + /// The text color fo the button "Edit". + /// By default `Colors.black`. + final Color editButtonTextColor; + + /// The name of the "Edit" button. + /// By default `Edit`. + final String editButtonName; + + final ToolbarOptions toolbarOptions; + final bool placeCursorAtTheEndOnEdit; + + static const Color defaultColorEditor = Color(0xff2E3152); + static const Color defaultColorBorder = Color(0xFF3E416E); + static const Color defaultColorFileName = Color(0xFF6CD07A); + static const Color defaultToolButtonColor = Color(0xFF4650c7); + static const Color defaultEditBackgroundColor = Color(0xFFEEEEEE); + + EditorModelStyleOptions( + {this.padding = const EdgeInsets.all(15.0), + this.heightOfContainer = 300, + required this.theme, + this.fontFamily = "monospace", + this.letterSpacing, + this.fontSize = 15, + this.lineHeight = 1.6, + this.tabSize = 2, + this.editorColor = defaultColorEditor, + this.editorBorderColor = defaultColorBorder, + this.editorFilenameColor = defaultColorFileName, + this.editorToolButtonColor = defaultToolButtonColor, + this.editorToolButtonTextColor = Colors.white, + this.editButtonBackgroundColor = defaultEditBackgroundColor, + this.editButtonTextColor = Colors.black, + this.editButtonName = "Edit", + this.editorNormalFilenameColor = Colors.black, + this.fontSizeOfFilename, + this.textStyleOfTextField = const TextStyle( + color: Colors.black87, + fontSize: 16, + letterSpacing: 1.25, + fontWeight: FontWeight.w500, + ), + this.toolbarOptions = const ToolbarOptions(), + this.placeCursorAtTheEndOnEdit = true}); + + double? editButtonPosTop; // this will become 50 while editing because of the toolbar's height + double? editButtonPosLeft; + double? editButtonPosBottom = 10; + double? editButtonPosRight = 15; + + /// You can change the position of the button "Edit" / "OK". + /// By default, `bottom: 10`, `right: 15`. + void defineEditButtonPosition({ + required top, + left, + bottom, + right, + }) { + this.editButtonPosTop = top; + this.editButtonPosLeft = left; + this.editButtonPosBottom = bottom; + this.editButtonPosRight = right; + } +} diff --git a/lib/base/code_editor/FileEditor.dart b/lib/base/code_editor/FileEditor.dart new file mode 100644 index 0000000..83ba4ed --- /dev/null +++ b/lib/base/code_editor/FileEditor.dart @@ -0,0 +1,18 @@ +/// A file in the editor, used by EditorModel [model.code]. +/// +/// * [name] is the name of the file shown in the navbar. +/// * [language] is the language used by the theme +/// * [code] is the content of the file, the code +/// +/// Tip: to simplify writing code in a String, +/// write line by line your code in a list List and pass as argument list.join("\n"). +class FileEditor { + late String name; + String? language; + String? code; + FileEditor({String? name, String? language, String? code}) { + this.name = name ?? "file.${language ?? 'txt'}"; + this.language = language ?? "text"; + this.code = code ?? ""; + } +} diff --git a/lib/base/code_editor/Theme.dart b/lib/base/code_editor/Theme.dart new file mode 100644 index 0000000..54c9d4a --- /dev/null +++ b/lib/base/code_editor/Theme.dart @@ -0,0 +1,59 @@ + +import 'package:flutter/material.dart'; + +const FontWeight fontWeight = FontWeight.w400; + +// HTML +const Color tagColor = Color(0xFFF79AA5); // tag +const Color quoteColor = Color(0xFF6CD07A); // "" + +// CSS +const Color attrColor = Color(0xFFCBBA7D); // CSS selectors +const Color propertyColor = Color(0xFF8CDCFE); // property +const Color idColor = Color(0xFFCBBA7D); +const Color classColor = Color(0xFFCBBA7D); + +// JS +const Color keywordColor = Color(0xFF3E9CD6); // keywords (function, ...) +const Color methodsColor = Color(0xFFDCDC9D); // methods built in +const Color titlesColor = Color(0xFFDCDC9D); // titles (function's title) + +/// The theme used by code_editor and created by code_editor. This is the default theme of the editor. +/// +/// You can create your own or use +/// others themes by looking at : +/// +/// `import 'package:flutter_highlight/themes/'`. +const myTheme = { + 'root': TextStyle( + backgroundColor: Color(0xff2E3152), + color: Color(0xffdddddd), + ), + 'keyword': TextStyle(color: keywordColor), + 'params': TextStyle(color: Color(0xffde935f)), + 'selector-tag': TextStyle(color: attrColor), + 'selector-id': TextStyle(color: idColor), + 'selector-class': TextStyle(color: classColor), + 'regexp': TextStyle(color: Color(0xffcc6666)), + 'literal': TextStyle(color: Colors.white), + 'section': TextStyle(color: Colors.white), + 'link': TextStyle(color: Colors.white), + 'subst': TextStyle(color: Color(0xffdddddd)), + 'string': TextStyle(color: quoteColor), + 'title': TextStyle(color: titlesColor), + 'name': TextStyle(color: tagColor), + 'type': TextStyle(color: tagColor), + 'attribute': TextStyle(color: propertyColor), + 'symbol': TextStyle(color: tagColor), + 'bullet': TextStyle(color: tagColor), + 'built_in': TextStyle(color: methodsColor), + 'addition': TextStyle(color: tagColor), + 'variable': TextStyle(color: tagColor), + 'template-tag': TextStyle(color: tagColor), + 'template-variable': TextStyle(color: tagColor), + 'comment': TextStyle(color: Color(0xff777777)), + 'quote': TextStyle(color: Color(0xff777777)), + 'deletion': TextStyle(color: Color(0xff777777)), + 'meta': TextStyle(color: Color(0xff777777)), + 'emphasis': TextStyle(fontStyle: FontStyle.italic), +}; diff --git a/lib/base/code_editor/ToolButton.dart b/lib/base/code_editor/ToolButton.dart new file mode 100644 index 0000000..9347884 --- /dev/null +++ b/lib/base/code_editor/ToolButton.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +/// A button under the navigation bar to help the user write code. +class ToolButton { + Function press; + IconData? icon; + String? symbol; + + ToolButton({ + required this.press, + this.icon, + this.symbol, + }); +} diff --git a/lib/base/code_editor/code_editor.dart b/lib/base/code_editor/code_editor.dart new file mode 100644 index 0000000..94618f0 --- /dev/null +++ b/lib/base/code_editor/code_editor.dart @@ -0,0 +1,284 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_highlight/flutter_highlight.dart'; +import 'package:qinglong_app/utils/codeeditor_theme.dart'; +import 'EditorModel.dart'; +import 'EditorModelStyleOptions.dart'; +import 'FileEditor.dart'; + +/// Creates a code editor that helps users to write and read code. +/// +/// In order to use it, you must define : +/// * [model] an EditorModel, to control the editor, its content and its files +/// * [onSubmit] a Function(String language, String value) executed when the user submits changes in a file. +class CodeEditor extends StatefulWidget { + /// The EditorModel in order to control the editor. + /// + /// This argument is @required. + late final EditorModel? model; + + /// onSubmit function to execute when the user saves changes in a file. + /// This is a function that takes [language] and [value] as arguments. + /// + /// * [language] is the language of the file edited by the user. + /// * [value] is the content of the file. + final Function(String? language, String? value)? onSubmit; + + /// You can disable the edit button (it won't show up at all) just like this : + /// + /// ``` + /// CodeEditor( + /// model: model, // my EditorModel() + /// edit: false, // disable the edit button + /// ) + /// ``` + /// + /// By default, the value is true. + final bool edit; + + /// You can disable the navigation bar like this : + /// + /// ``` + /// CodeEditor( + /// model: model, // my EditorModel() + /// disableNavigationbar: true, // hide the navigation bar + /// ) + /// ``` + /// + /// By default, the value is `false`. + /// + /// WARNING : if you set the value to true, only the first + /// file will be displayed in the editor because + /// it's not possible to switch betweens other files without the navigation bar. + final bool disableNavigationbar; + + /// An optional TextEditingController that can be passed in. + late final TextEditingController? textEditingController; + + CodeEditor({ + Key? key, + this.model, + this.onSubmit, + this.edit = true, + this.disableNavigationbar = false, + this.textEditingController, + }) : super(key: key); + + @override + CodeEditorState createState() => CodeEditorState(); +} + +class CodeEditorState extends State { + /// We need it to control the content of the text field. + late TextEditingController editingController; + + /// The new content of a file when the user is editing one. + String? newValue; + + + /// The text field wants a focus node. + FocusNode focusNode = FocusNode(); + + /// Initialize the formKey for the text field + static final GlobalKey editableTextKey = GlobalKey(); + + @override + void initState() { + super.initState(); + + if (widget.textEditingController != null) { + // Use the user-provide controller + editingController = widget.textEditingController!; + } else { + /// Initialize the controller for the text field. + editingController = TextEditingController(text: ""); + } + newValue = ""; // if there are no changes + } + + @override + void dispose() { + editingController.dispose(); + super.dispose(); + } + + /// Set the cursor at the end of the editableText. + void placeCursorAtTheEnd() { + editingController.selection = TextSelection.fromPosition( + TextPosition(offset: editingController.text.length), + ); + } + + /// Place the cursor where wanted. + /// + /// [pos] places the cursor in the text field + void placeCursor(int pos) { + try { + editingController.selection = TextSelection.fromPosition( + TextPosition(offset: pos), + ); + } catch (e) { + throw Exception("code_editor : placeCursor(int pos), pos is not valid."); + } + } + + + int getCurrentIndex(){ + return widget.model?.position ?? 0; + } + @override + Widget build(BuildContext context) { + /// Gets the model from the parent widget. + EditorModel model = widget.model ??= EditorModel(files: []); + + /// Gets the style options from the parent widget. + EditorModelStyleOptions? opt = model.styleOptions; + + String? language = model.currentLanguage; + + /// Which file in the list of file ? + int? position = model.position; + + /// The content of the file where position corresponds to the list of file. + String? code = model.getCodeWithIndex(position ?? 0); + + bool disableNavigationbar = widget.disableNavigationbar; + + // When we change the file in the navbar, the code in the text field + // isn't updated, so we update it here. + // + // With newValue = code if the user does not change the value + // in the text field + editingController = TextEditingController(text: code); + newValue = code; + + /// The filename in green. + Text showFilename(String name, bool isSelected) { + return Text( + name, + style: TextStyle( + fontWeight: FontWeight.normal, + fontSize: opt?.fontSizeOfFilename, + color: isSelected ? opt?.editorFilenameColor : opt?.editorNormalFilenameColor, + ), + ); + } + + /// Build the navigation bar. + Container buildNavbar() { + return Container( + width: double.infinity, + height: 60, + decoration: BoxDecoration( + color: opt?.editorColor, + border: Border(bottom: BorderSide(color: opt?.editorBorderColor ?? Colors.blue)), + ), + child: ListView.builder( + padding: EdgeInsets.only(left: 15), + itemCount: model.numberOfFiles, + scrollDirection: Axis.horizontal, + itemBuilder: (context, int index) { + final FileEditor file = model.getFileWithIndex(index); + + return Container( + margin: EdgeInsets.only(right: 15), + child: Center( + child: GestureDetector( + // Checks if the position of the navbar is the current file. + child: showFilename(file.name, model.position == index), + onTap: () { + setState(() { + model.changeIndexTo(index); + }); + }, + ), + ), + ); + }, + ), + ); + } + + + /// This button won't appear if `edit = false`. + Widget editButton(String name, Function() press) { + if (widget.edit == true) { + return Positioned( + bottom: opt?.editButtonPosBottom, + right: opt?.editButtonPosRight, + top: (model.isEditing && opt != null && opt.editButtonPosTop != null && opt.editButtonPosTop! < 50) ? 50 : opt?.editButtonPosTop, + left: opt?.editButtonPosLeft, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + primary: opt?.editButtonBackgroundColor, + ), + onPressed: press, + child: Text( + name, + style: TextStyle( + fontSize: 16.0, + fontFamily: "monospace", + fontWeight: FontWeight.normal, + color: opt?.editButtonTextColor, + ), + ), + ), + ); + } else { + return SizedBox.shrink(); + } + } + + // We place the cursor in the end of the text field. + + if (model.isEditing && (model.styleOptions?.placeCursorAtTheEndOnEdit ?? true)) { + placeCursorAtTheEnd(); + } + + /// We toggle the editor and the text field. + Widget buildContentEditor() { + return Stack( + children: [ + Container( + width: double.infinity, + height: opt?.heightOfContainer, + color: opt?.editorColor, + child: SingleChildScrollView( + child: Padding( + padding: opt?.padding ?? const EdgeInsets.all(3.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + HighlightView( + code ?? "code is null", + language: language, + theme: opt?.theme ?? qinglongLightTheme, + tabSize: opt?.tabSize ?? 4, + textStyle: TextStyle( + fontFamily: opt?.fontFamily, + letterSpacing: opt?.letterSpacing, + fontSize: opt?.fontSize, + height: opt?.lineHeight, // line-height + ), + ), + ], + ), + ), + ), + ), + editButton(opt?.editButtonName ?? "Edit", () { + setState(() { + model.toggleEditing(); + }); + }), + ], + ); + } + + return Column( + children: [ + disableNavigationbar ? SizedBox.shrink() : buildNavbar(), + buildContentEditor(), + ], + ); + } +} diff --git a/lib/base/http/http.dart b/lib/base/http/http.dart index ab54db5..76eca18 100644 --- a/lib/base/http/http.dart +++ b/lib/base/http/http.dart @@ -8,6 +8,7 @@ import 'package:qinglong_app/base/http/token_interceptor.dart'; import 'package:qinglong_app/base/userinfo_viewmodel.dart'; import '../../json.jc.dart'; +import '../../main.dart'; class Http { static const int NOT_LOGIN = 1000; @@ -31,7 +32,7 @@ class Http { static void _init() { if (_dio == null) { - initDioConfig(UserInfoViewModel.getInstance().host!); + initDioConfig(getIt().host!); } } diff --git a/lib/base/http/token_interceptor.dart b/lib/base/http/token_interceptor.dart index 9fa3922..eb14cc1 100644 --- a/lib/base/http/token_interceptor.dart +++ b/lib/base/http/token_interceptor.dart @@ -1,11 +1,13 @@ import 'package:dio/dio.dart'; import 'package:qinglong_app/main.dart'; +import '../userinfo_viewmodel.dart'; + class TokenInterceptor extends Interceptor { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { - if (userInfoViewModel.token != null) { - options.headers["Authorization"] = "Bearer " + userInfoViewModel.token!; + if (getIt().token != null) { + options.headers["Authorization"] = "Bearer " + getIt().token!; } options.queryParameters["t"] = (DateTime.now().millisecondsSinceEpoch~/1000).toString(); return handler.next(options); diff --git a/lib/base/ql_app_bar.dart b/lib/base/ql_app_bar.dart index 55fe1da..0c778e1 100644 --- a/lib/base/ql_app_bar.dart +++ b/lib/base/ql_app_bar.dart @@ -35,6 +35,7 @@ class QlAppBar extends StatelessWidget with PreferredSizeWidget { ); return AppBar( + elevation: 0, leading: canBack ? back : null, automaticallyImplyLeading: canBack, title: Text(title), diff --git a/lib/base/theme.dart b/lib/base/theme.dart index 4ba94ff..1055b9f 100644 --- a/lib/base/theme.dart +++ b/lib/base/theme.dart @@ -1,7 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:qinglong_app/utils/qinglong_theme.dart'; +import 'package:qinglong_app/utils/codeeditor_theme.dart'; var themeProvider = ChangeNotifierProvider((ref) => ThemeViewModel()); const Color _primaryColor = Color(0xFF299343); @@ -25,6 +25,25 @@ class ThemeViewModel extends ChangeNotifier { ThemeData darkTheme = ThemeData.dark().copyWith( primaryColor: const Color(0xffffffff), + inputDecorationTheme: const InputDecorationTheme( + labelStyle: TextStyle(color: _primaryColor), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: _primaryColor, + ), + ), + ), + tabBarTheme: const TabBarTheme( + labelStyle: TextStyle( + fontSize: 14, + ), + unselectedLabelStyle: TextStyle( + fontSize: 14, + ), + labelColor: Color(0xffffffff), + unselectedLabelColor: Color(0xff999999), + + ), ); ThemeData lightTheme = ThemeData.light().copyWith( primaryColor: _primaryColor, @@ -51,10 +70,10 @@ ThemeData lightTheme = ThemeData.light().copyWith( ), tabBarTheme: const TabBarTheme( labelStyle: TextStyle( - fontSize: 16, + fontSize: 14, ), unselectedLabelStyle: TextStyle( - fontSize: 16, + fontSize: 14, ), labelColor: _primaryColor, unselectedLabelColor: Color(0xff999999), @@ -66,6 +85,7 @@ ThemeData lightTheme = ThemeData.light().copyWith( abstract class ThemeColors { Color taskTitleColor(); + Color descColor(); Color searchBarBg(); @@ -94,6 +114,11 @@ class LightartThemeColors extends ThemeColors { Map codeEditorTheme() { return qinglongLightTheme; } + + @override + Color descColor() { + return Color(0xff999999); + } } class DartThemeColors extends ThemeColors { @@ -116,4 +141,9 @@ class DartThemeColors extends ThemeColors { Map codeEditorTheme() { return qinglongDarkTheme; } + + @override + Color descColor() { + return Color(0xff999999); + } } diff --git a/lib/base/ui/empty_widget.dart b/lib/base/ui/empty_widget.dart new file mode 100644 index 0000000..00990b6 --- /dev/null +++ b/lib/base/ui/empty_widget.dart @@ -0,0 +1,26 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:qinglong_app/base/theme.dart'; + +class EmptyWidget extends ConsumerWidget { + const EmptyWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context,WidgetRef ref) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "暂无数据", + style: TextStyle( + fontSize: 14, + color:ref.watch(themeProvider).themeColor.descColor(), + ), + ), + ], + ), + ); + } +} diff --git a/lib/base/userinfo_viewmodel.dart b/lib/base/userinfo_viewmodel.dart index ab73b48..338d84f 100644 --- a/lib/base/userinfo_viewmodel.dart +++ b/lib/base/userinfo_viewmodel.dart @@ -6,14 +6,7 @@ class UserInfoViewModel { String? _token; String? _host = ""; - static UserInfoViewModel? _userInfoViewModel; - - static UserInfoViewModel getInstance() { - _userInfoViewModel ??= UserInfoViewModel._(); - return _userInfoViewModel!; - } - - UserInfoViewModel._() { + UserInfoViewModel() { String userInfoJson = SpUtil.getString(sp_UserINfo); _host = SpUtil.getString(sp_Host,defValue: "http://49.234.59.95:5700"); if (userInfoJson.isNotEmpty) { diff --git a/lib/main.dart b/lib/main.dart index 96351a5..626a0cf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:dio_log/overlay_draggable_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:get_it/get_it.dart'; import 'package:logger/logger.dart'; import 'package:qinglong_app/base/theme.dart'; import 'package:qinglong_app/module/login/login_page.dart'; @@ -13,14 +14,16 @@ import 'base/routes.dart'; import 'base/userinfo_viewmodel.dart'; import 'module/home/home_page.dart'; -late UserInfoViewModel userInfoViewModel; +final getIt = GetIt.instance; +var navigatorState = GlobalKey(); var logger = Logger(); void main() async { WidgetsFlutterBinding.ensureInitialized(); await SpUtil.getInstance(); - userInfoViewModel = UserInfoViewModel.getInstance(); + getIt.registerSingleton(UserInfoViewModel()); + runApp( ProviderScope( overrides: [ @@ -36,6 +39,7 @@ void main() async { } class MyApp extends ConsumerWidget { + const MyApp({Key? key}) : super(key: key); @override @@ -46,6 +50,7 @@ class MyApp extends ConsumerWidget { FocusScope.of(context).requestFocus(FocusNode()); }, child: MaterialApp( + navigatorKey: navigatorState, theme: ref.watch(themeProvider).currentTheme, onGenerateRoute: (setting) { return Routes.generateRoute(setting); @@ -53,7 +58,7 @@ class MyApp extends ConsumerWidget { home: Builder( builder: (context) { showDebugBtn(context, btnColor: Colors.blue); - return userInfoViewModel.isLogined() ? const HomePage() : LoginPage(); + return getIt().isLogined() ? const HomePage() : LoginPage(); }, ), // home: LoginPage(), diff --git a/lib/module/config/config_edit_page.dart b/lib/module/config/config_edit_page.dart index 53ed1a1..5a0d8b5 100644 --- a/lib/module/config/config_edit_page.dart +++ b/lib/module/config/config_edit_page.dart @@ -1,12 +1,11 @@ -import 'package:code_editor/code_editor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:qinglong_app/base/common_dialog.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/theme.dart'; import 'package:qinglong_app/module/config/config_viewmodel.dart'; -import 'package:qinglong_app/utils/qinglong_theme.dart'; class ConfigEditPage extends ConsumerStatefulWidget { final String content; @@ -21,34 +20,23 @@ class ConfigEditPage extends ConsumerStatefulWidget { class _ConfigEditPageState extends ConsumerState { String? value; + late TextEditingController _controller; + + @override + void initState() { + _controller = TextEditingController(text: widget.content); + super.initState(); + } + @override Widget build(BuildContext context) { - List files = [ - FileEditor( - name: widget.title, - language: "sh", - code: widget.content, // [code] needs a string - ), - ]; - EditorModel editMode = EditorModel( - files: files, - styleOptions: EditorModelStyleOptions( - fontSize: 13, - heightOfContainer: MediaQuery.of(context).size.height - kToolbarHeight - kBottomNavigationBarHeight - 150, - editorBorderColor: Theme.of(context).scaffoldBackgroundColor, - editButtonBackgroundColor: Theme.of(context).scaffoldBackgroundColor, - editorColor: Theme.of(context).scaffoldBackgroundColor, - theme: qinglongLightTheme, - ), - - ); return Scaffold( appBar: QlAppBar( canBack: true, backCall: () { Navigator.of(context).pop(); }, - title: '编辑文件', + title: '编辑${widget.title}', actions: [ InkWell( onTap: () async { @@ -56,7 +44,7 @@ class _ConfigEditPageState extends ConsumerState { failDialog(context, "请先点击保存"); return; } - HttpResponse response = await Api.saveFile(widget.title, value!); + HttpResponse response = await Api.saveFile(widget.title, _controller.text); if (response.success) { ref.read(configProvider).loadContent(widget.title); Navigator.of(context).pop(); @@ -76,13 +64,21 @@ class _ConfigEditPageState extends ConsumerState { ], ), body: Container( - child: CodeEditor( - model: editMode, - edit: true, - onSubmit: (title, v) { - value = v; - }, - disableNavigationbar: false, + padding: const EdgeInsets.only( + left: 15, + right: 15, + ), + child: SingleChildScrollView( + child: TextField( + focusNode: FocusNode(), + style: TextStyle( + color: ref.read(themeProvider).themeColor.descColor(), + fontSize: 14, + ), + controller: _controller, + minLines: 1, + maxLines: 100, + ), ), ), ); diff --git a/lib/module/config/config_page.dart b/lib/module/config/config_page.dart index 413b7a2..3271238 100644 --- a/lib/module/config/config_page.dart +++ b/lib/module/config/config_page.dart @@ -1,8 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:qinglong_app/base/base_state_widget.dart'; -import 'package:code_editor/code_editor.dart'; +import 'package:qinglong_app/base/code_editor/code_editor.dart'; +import 'package:qinglong_app/base/code_editor/EditorModel.dart'; +import 'package:qinglong_app/base/code_editor/EditorModelStyleOptions.dart'; +import 'package:qinglong_app/base/code_editor/FileEditor.dart'; +import 'package:qinglong_app/base/http/url.dart'; +import 'package:qinglong_app/base/routes.dart'; import 'package:qinglong_app/base/theme.dart'; -import 'package:qinglong_app/utils/qinglong_theme.dart'; +import 'package:qinglong_app/main.dart'; import 'config_viewmodel.dart'; @@ -10,10 +16,12 @@ class ConfigPage extends StatefulWidget { const ConfigPage({Key? key}) : super(key: key); @override - _ConfigPageState createState() => _ConfigPageState(); + ConfigPageState createState() => ConfigPageState(); } -class _ConfigPageState extends State { +class ConfigPageState extends State { + GlobalKey globalKey = GlobalKey(); + @override Widget build(BuildContext context) { return BaseStateWidget( @@ -31,6 +39,7 @@ class _ConfigPageState extends State { files: files, styleOptions: EditorModelStyleOptions( fontSize: 13, + editorNormalFilenameColor: ref.read(themeProvider).themeColor.descColor(), heightOfContainer: MediaQuery.of(context).size.height - kToolbarHeight - kBottomNavigationBarHeight - 90, editorBorderColor: Theme.of(context).scaffoldBackgroundColor, editButtonBackgroundColor: Theme.of(context).scaffoldBackgroundColor, @@ -40,6 +49,7 @@ class _ConfigPageState extends State { ), ); return CodeEditor( + key: globalKey, model: editMode, edit: false, disableNavigationbar: false, @@ -51,4 +61,11 @@ class _ConfigPageState extends State { }, ); } + + void editMe(WidgetRef ref) { + int index = globalKey.currentState?.getCurrentIndex() ?? 0; + + navigatorState.currentState?.pushNamed(Routes.route_ConfigEdit, + arguments: {"title": ref.read(configProvider).list[index].title, "content": ref.read(configProvider).content[ref.read(configProvider).list[index].title]}); + } } diff --git a/lib/module/home/home_page.dart b/lib/module/home/home_page.dart index 5462210..4eb2d37 100644 --- a/lib/module/home/home_page.dart +++ b/lib/module/home/home_page.dart @@ -9,19 +9,21 @@ import 'package:qinglong_app/module/env/env_page.dart'; import 'package:qinglong_app/module/others/other_page.dart'; import 'package:qinglong_app/module/task/task_page.dart'; -class HomePage extends StatefulWidget { +class HomePage extends ConsumerStatefulWidget { const HomePage({Key? key}) : super(key: key); @override _HomePageState createState() => _HomePageState(); } -class _HomePageState extends State { +class _HomePageState extends ConsumerState { int _index = 0; String _title = ""; List titles = []; + GlobalKey configKey = GlobalKey(); + @override void initState() { initTitles(); @@ -52,6 +54,20 @@ class _HomePageState extends State { ), ), )); + } else if (_index == 2) { + actions.add(InkWell( + onTap: () { + configKey.currentState?.editMe(ref); + }, + child: const Padding( + padding: EdgeInsets.symmetric( + horizontal: 15, + ), + child: Center( + child: Text("编辑"), + ), + ), + )); } actions.add( @@ -72,11 +88,13 @@ class _HomePageState extends State { ), body: IndexedStack( index: _index, - children: const [ - TaskPage(), - EnvPage(), - ConfigPage(), - OtherPage(), + children: [ + const TaskPage(), + const EnvPage(), + ConfigPage( + key: configKey, + ), + const OtherPage(), ], ), bottomNavigationBar: BottomNavigationBar( diff --git a/lib/module/login/login_page.dart b/lib/module/login/login_page.dart index 00eb914..0a1e0b2 100644 --- a/lib/module/login/login_page.dart +++ b/lib/module/login/login_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:qinglong_app/base/common_dialog.dart'; import 'package:qinglong_app/base/routes.dart'; +import 'package:qinglong_app/base/userinfo_viewmodel.dart'; import 'package:qinglong_app/main.dart'; import 'package:qinglong_app/module/login/login_viewmodel.dart'; import 'package:qinglong_app/utils/utils.dart'; @@ -16,7 +17,7 @@ class LoginPage extends StatefulWidget { } class _LoginPageState extends State { - final TextEditingController _hostController = TextEditingController(text: userInfoViewModel.host); + final TextEditingController _hostController = TextEditingController(text: getIt().host); final TextEditingController _userNameController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); @@ -63,7 +64,7 @@ class _LoginPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( - height: MediaQuery.of(context).size.height / 6, + height: MediaQuery.of(context).size.height / 10, ), SizedBox( height: 50, @@ -72,7 +73,7 @@ class _LoginPageState extends State { children: [ Positioned( top: 0, - left: 40, + left: 0, child: Image.asset( "assets/images/login_tip.png", height: 45, @@ -80,7 +81,7 @@ class _LoginPageState extends State { ), const Positioned( top: 10, - left: 40, + left: 0, child: Text( "登录", style: TextStyle( @@ -91,7 +92,7 @@ class _LoginPageState extends State { ), Positioned( top: 5, - right: 40, + right: 0, child: Image.asset( "assets/images/ql.png", height: 45, @@ -197,7 +198,7 @@ class _LoginPageState extends State { ), onPressed: () { Utils.hideKeyBoard(context); - userInfoViewModel.updateHost(_hostController.text); + getIt().updateHost(_hostController.text); model.login(_userNameController.text, _passwordController.text); }), ), diff --git a/lib/module/login/login_viewmodel.dart b/lib/module/login/login_viewmodel.dart index a842fae..dd9c4f1 100644 --- a/lib/module/login/login_viewmodel.dart +++ b/lib/module/login/login_viewmodel.dart @@ -3,6 +3,7 @@ import 'package:qinglong_app/base/base_viewmodel.dart'; import 'package:qinglong_app/base/http/api.dart'; import 'package:qinglong_app/base/http/http.dart'; import 'package:qinglong_app/base/http/url.dart'; +import 'package:qinglong_app/base/userinfo_viewmodel.dart'; import 'package:qinglong_app/main.dart'; import 'login_bean.dart'; @@ -23,7 +24,7 @@ class LoginViewModel extends ViewModel { HttpResponse response = await Api.login(userName, password); if (response.success) { - userInfoViewModel.updateToken(response.bean?.token ?? ""); + getIt().updateToken(response.bean?.token ?? ""); loginSuccess = true; isLoading = false; } else { diff --git a/lib/module/task/task_page.dart b/lib/module/task/task_page.dart index 3b6c232..0f4fa4f 100644 --- a/lib/module/task/task_page.dart +++ b/lib/module/task/task_page.dart @@ -6,6 +6,7 @@ import 'package:qinglong_app/base/base_state_widget.dart'; import 'package:qinglong_app/base/routes.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/base/ui/menu.dart'; import 'package:qinglong_app/module/task/intime_log/intime_log_page.dart'; import 'package:qinglong_app/module/task/task_bean.dart'; @@ -83,23 +84,21 @@ class _TaskPageState extends State { onRefresh: () async { return model.loadData(false); }, - child: ListView.builder( - keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, - itemBuilder: (context, index) { - // if (index == 0) { - // return searchCell(ref); - // } + child: list.isEmpty + ? const EmptyWidget() + : ListView.builder( + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + itemBuilder: (context, index) { + TaskBean item = list[index]; - TaskBean item = list[index]; - - if ((item.name == null || item.name!.contains(_searchKey ?? "")) || (item.command == null || item.command!.contains(_searchKey ?? ""))) { - return TaskItemCell(item, ref); - } else { - return const SizedBox.shrink(); - } - }, - itemCount: list.length, - ), + if ((item.name == null || item.name!.contains(_searchKey ?? "")) || (item.command == null || item.command!.contains(_searchKey ?? ""))) { + return TaskItemCell(item, ref); + } else { + return const SizedBox.shrink(); + } + }, + itemCount: list.length, + ), ); } @@ -239,13 +238,13 @@ class TaskItemCell extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, children: [ Material( - color:Colors.transparent, + color: Colors.transparent, child: Text( bean.name ?? "", maxLines: 1, style: TextStyle( overflow: TextOverflow.ellipsis, - color: bean.isDisabled == 1 ? Color(0xffF85152) : ref.watch(themeProvider).themeColor.taskTitleColor(), + color: bean.isDisabled == 1 ? const Color(0xffF85152) : ref.watch(themeProvider).themeColor.taskTitleColor(), fontSize: 18, ), ), @@ -264,13 +263,13 @@ class TaskItemCell extends StatelessWidget { ), const Spacer(), Material( - color:Colors.transparent, + color: Colors.transparent, child: Text( (bean.lastExecutionTime == null || bean.lastExecutionTime == 0) ? "-" : Utils.formatMessageTime(bean.lastExecutionTime!), maxLines: 1, - style: const TextStyle( + style: TextStyle( overflow: TextOverflow.ellipsis, - color: Color(0xff999999), + color: ref.watch(themeProvider).themeColor.descColor(), fontSize: 12, ), ), @@ -281,13 +280,13 @@ class TaskItemCell extends StatelessWidget { height: 8, ), Material( - color:Colors.transparent, + color: Colors.transparent, child: Text( bean.schedule ?? "", maxLines: 1, - style: const TextStyle( + style: TextStyle( overflow: TextOverflow.ellipsis, - color: Color(0xff999999), + color: ref.watch(themeProvider).themeColor.descColor(), fontSize: 12, ), ), diff --git a/lib/module/task/task_viewmodel.dart b/lib/module/task/task_viewmodel.dart index be66a50..94c7ae2 100644 --- a/lib/module/task/task_viewmodel.dart +++ b/lib/module/task/task_viewmodel.dart @@ -33,6 +33,9 @@ class TaskViewModel extends BaseViewModel { list.sort((a, b) { return b.created!.compareTo(a.created!); }); + list.sort((a, b) { + return b.isPinned!.compareTo(a.isPinned!); + }); running.clear(); running.addAll(list.where((element) => element.status == 0)); disabled.clear(); diff --git a/lib/utils/qinglong_theme.dart b/lib/utils/codeeditor_theme.dart similarity index 100% rename from lib/utils/qinglong_theme.dart rename to lib/utils/codeeditor_theme.dart diff --git a/pubspec.lock b/pubspec.lock index 207a330..9f312c5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -155,13 +155,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "4.1.0" - code_editor: - dependency: "direct main" - description: - name: code_editor - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.3.1" collection: dependency: transitive description: @@ -252,7 +245,7 @@ packages: source: hosted version: "0.1.1" flutter_highlight: - dependency: transitive + dependency: "direct main" description: name: flutter_highlight url: "https://pub.flutter-io.cn" @@ -303,13 +296,6 @@ packages: description: flutter source: sdk version: "0.0.0" - font_awesome_flutter: - dependency: transitive - description: - name: font_awesome_flutter - url: "https://pub.flutter-io.cn" - source: hosted - version: "9.2.0" frontend_server_client: dependency: transitive description: @@ -317,6 +303,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" + get_it: + dependency: "direct main" + description: + name: get_it + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.2.0" glob: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e738523..45d2326 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,7 +22,8 @@ dependencies: json_conversion_annotation: ^0.0.4 logger: ^1.1.0 intl: ^0.17.0 - code_editor: ^1.3.1 + get_it: ^7.2.0 + flutter_highlight: ^0.7.0 dev_dependencies: flutter_native_splash: ^1.3.3