diff --git a/lib/controllers/settings_controller.dart b/lib/controllers/settings_controller.dart index 6b3d9cd6..c592ad0e 100644 --- a/lib/controllers/settings_controller.dart +++ b/lib/controllers/settings_controller.dart @@ -3,7 +3,9 @@ import 'dart:convert'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:dio/dio.dart'; +import 'package:flutter_js/flutter_js.dart'; import 'package:get/get.dart'; +import 'package:miru_app/utils/extension.dart'; class SettingsController extends GetxController { final contributors = [].obs; @@ -25,12 +27,12 @@ class SettingsController extends GetxController { void toggleExtensionLogWindow(bool open) async { if (open && extensionLogWindowId.value == -1) { final window = await DesktopMultiWindow.createWindow(jsonEncode({ - "name": 'log', + "name": 'debug', })); extensionLogWindowId.value = window.windowId; window ..center() - ..setTitle("miru extension log") + ..setTitle("miru extension debug") ..show(); // 用于检测窗口是否关闭 @@ -45,12 +47,83 @@ class SettingsController extends GetxController { timer.cancel(); } }); + // 轮询带执行的方法并执行方法 + Timer.periodic(const Duration(milliseconds: 500), (timer) async { + if (extensionLogWindowId.value == -1) { + timer.cancel(); + return; + } + await _handleMethods(); + }); + return; } WindowController.fromWindowId(extensionLogWindowId.value).close(); extensionLogWindowId.value = -1; } + // 返回执行结果 + _invokeMethodResult(String methodKey, dynamic result) async { + await DesktopMultiWindow.invokeMethod( + extensionLogWindowId.value, + "result", + { + "key": methodKey, + "result": result, + }, + ); + } + + // 获取方法列表 + Future>> _getMethods() async { + final methods = await DesktopMultiWindow.invokeMethod( + extensionLogWindowId.value, + "getMethods", + ); + + return List.from(methods) + .map((e) => Map.from(e)) + .toList(); + } + + // 处理待执行的方法 + Future _handleMethods() async { + final methods = await _getMethods(); + for (final call in methods) { + if (call["method"] == "getInstalledExtensions") { + _invokeMethodResult( + call["key"], + ExtensionUtils.runtimes.values + .toList() + .map((e) => e.extension.toJson()) + .toList(), + ); + } + + if (call["method"] == "debugExecute") { + final arguments = call["arguments"]; + final extension = ExtensionUtils.runtimes[arguments["package"]]; + final method = arguments["method"]; + final runtime = extension!.runtime; + try { + final jsResult = await runtime.handlePromise( + await runtime.evaluateAsync('stringify(()=>{return $method})'), + ); + final result = jsResult.stringResult; + _invokeMethodResult( + call["key"], + result, + ); + } catch (e) { + _invokeMethodResult( + call["key"], + e.toString(), + ); + } + } + } + } + _getContributors() async { final res = await Dio() .get("https://api.github.com/repos/miru-project/miru-app/contributors"); diff --git a/lib/data/services/extension_service.dart b/lib/data/services/extension_service.dart index 26d4128d..e38303f9 100644 --- a/lib/data/services/extension_service.dart +++ b/lib/data/services/extension_service.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:html/dom.dart'; import 'package:html/parser.dart'; @@ -53,26 +54,70 @@ class ExtensionService { }); // 请求 runtime.onMessage('request', (dynamic args) async { - ExtensionUtils.addLog( - extension, - ExtensionLogLevel.info, - "Request: ${args[0]} , ${args[1]}", - ); _cuurentRequestUrl = args[0]; final headers = args[1]['headers'] ?? {}; if (headers['User-Agent'] == null) { headers['User-Agent'] = MiruStorage.getUASetting(); } - return (await _dio.request( - args[0], - data: args[1]['data'], - queryParameters: args[1]['queryParameters'] ?? {}, - options: Options( - headers: headers, - method: args[1]['method'] ?? 'get', - ), - )) - .data; + + final url = args[0]; + final method = args[1]['method'] ?? 'get'; + final requestBody = args[1]['data']; + + final log = ExtensionNetworkLog( + extension: extension, + url: args[0], + method: method, + requestHeaders: headers, + ); + final key = UniqueKey().toString(); + ExtensionUtils.addNetworkLog( + key, + log, + ); + + try { + final res = await _dio.request( + url, + data: requestBody, + queryParameters: args[1]['queryParameters'] ?? {}, + options: Options( + headers: headers, + method: method, + ), + ); + log.requestHeaders = res.requestOptions.headers; + log.responseBody = res.data; + log.responseHeaders = res.headers.map.map( + (key, value) => MapEntry( + key, + value.join(';'), + ), + ); + log.statusCode = res.statusCode; + + ExtensionUtils.addNetworkLog( + key, + log, + ); + return res.data; + } on DioException catch (e) { + log.url = e.requestOptions.uri.toString(); + log.requestHeaders = e.requestOptions.headers; + log.responseBody = e.response?.data; + log.responseHeaders = e.response?.headers.map.map( + (key, value) => MapEntry( + key, + value.join(';'), + ), + ); + log.statusCode = e.response?.statusCode; + ExtensionUtils.addNetworkLog( + key, + log, + ); + rethrow; + } }); // 设置 @@ -355,7 +400,7 @@ class ExtensionService { async function stringify(callback) { const data = await callback(); - return typeof data === "object" ? JSON.stringify(data) : data; + return typeof data === "object" ? JSON.stringify(data,0,2) : data; } '''); @@ -365,9 +410,9 @@ class ExtensionService { JsEvalResult jsResult = await runtime.evaluateAsync(''' $ext - var extenstion = new Ext(); - extenstion.load().then(()=>{ - sendMessage("cleanSettings", JSON.stringify([extenstion.settingKeys])); + var extension = new Ext(); + extension.load().then(()=>{ + sendMessage("cleanSettings", JSON.stringify([extension.settingKeys])); }); '''); await runtime.handlePromise(jsResult); @@ -397,7 +442,7 @@ class ExtensionService { return cookies.map((e) => e.toString()).join(';'); } - Future _runExtension(Future Function() fun) async { + Future runExtension(Future Function() fun) async { try { return await fun(); } catch (e) { @@ -419,9 +464,9 @@ class ExtensionService { } Future> latest(int page) async { - return _runExtension(() async { + return runExtension(() async { final jsResult = await runtime.handlePromise( - await runtime.evaluateAsync('stringify(()=>extenstion.latest($page))'), + await runtime.evaluateAsync('stringify(()=>extension.latest($page))'), ); List result = jsonDecode(jsResult.stringResult).map((e) { @@ -439,10 +484,10 @@ class ExtensionService { int page, { Map>? filter, }) async { - return _runExtension(() async { + return runExtension(() async { final jsResult = await runtime.handlePromise( await runtime.evaluateAsync( - 'stringify(()=>extenstion.search("$kw",$page,${filter == null ? null : jsonEncode(filter)}))'), + 'stringify(()=>extension.search("$kw",$page,${filter == null ? null : jsonEncode(filter)}))'), ); List result = jsonDecode(jsResult.stringResult).map((e) { @@ -460,12 +505,12 @@ class ExtensionService { }) async { late String eval; if (filter == null) { - eval = 'stringify(()=>extenstion.createFilter())'; + eval = 'stringify(()=>extension.createFilter())'; } else { eval = - 'stringify(()=>extenstion.createFilter(JSON.parse(\'${jsonEncode(filter)}\')))'; + 'stringify(()=>extension.createFilter(JSON.parse(\'${jsonEncode(filter)}\')))'; } - return _runExtension(() async { + return runExtension(() async { final jsResult = await runtime.handlePromise( await runtime.evaluateAsync(eval), ); @@ -480,9 +525,9 @@ class ExtensionService { } Future detail(String url) async { - return _runExtension(() async { + return runExtension(() async { final jsResult = await runtime.handlePromise( - await runtime.evaluateAsync('stringify(()=>extenstion.detail("$url"))'), + await runtime.evaluateAsync('stringify(()=>extension.detail("$url"))'), ); final result = ExtensionDetail.fromJson(jsonDecode(jsResult.stringResult)); @@ -492,9 +537,9 @@ class ExtensionService { } Future watch(String url) async { - return _runExtension(() async { + return runExtension(() async { final jsResult = await runtime.handlePromise( - await runtime.evaluateAsync('stringify(()=>extenstion.watch("$url"))'), + await runtime.evaluateAsync('stringify(()=>extension.watch("$url"))'), ); final data = jsonDecode(jsResult.stringResult); @@ -514,10 +559,10 @@ class ExtensionService { } Future checkUpdate(url) async { - return _runExtension(() async { + return runExtension(() async { final jsResult = await runtime.handlePromise( await runtime - .evaluateAsync('stringify(()=>extenstion.checkUpdate("$url"))'), + .evaluateAsync('stringify(()=>extension.checkUpdate("$url"))'), ); return jsResult.stringResult; }); diff --git a/lib/main.dart b/lib/main.dart index 257242a6..b427496c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,7 +7,7 @@ import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:media_kit/media_kit.dart'; import 'package:miru_app/controllers/application_controller.dart'; -import 'package:miru_app/views/pages/log_page.dart'; +import 'package:miru_app/views/pages/debug_page.dart'; import 'package:miru_app/views/pages/main_page.dart'; import 'package:miru_app/router/router.dart'; import 'package:miru_app/utils/extension.dart'; @@ -28,7 +28,7 @@ void main(List args) async { : jsonDecode(args[2]) as Map; Map windows = { - "log": ExtensionLogWindow( + "debug": ExtensionDebugWindow( windowController: WindowController.fromWindowId(windowId), ), }; diff --git a/lib/models/extension.dart b/lib/models/extension.dart index 1a959364..037c9bc6 100644 --- a/lib/models/extension.dart +++ b/lib/models/extension.dart @@ -233,3 +233,31 @@ class ExtensionLog { Map toJson() => _$ExtensionLogToJson(this); } + +@JsonSerializable() +class ExtensionNetworkLog { + final Extension extension; + String? responseBody; + String? requestBody; + Map? requestHeaders; + Map? responseHeaders; + String url; + String method; + int? statusCode; + + ExtensionNetworkLog({ + required this.extension, + required this.url, + required this.method, + this.statusCode, + this.responseBody, + this.requestBody, + this.requestHeaders, + this.responseHeaders, + }); + + factory ExtensionNetworkLog.fromJson(Map json) => + _$ExtensionNetworkLogFromJson(json); + + Map toJson() => _$ExtensionNetworkLogToJson(this); +} diff --git a/lib/models/extension.g.dart b/lib/models/extension.g.dart index 98ba3920..7e00852b 100644 --- a/lib/models/extension.g.dart +++ b/lib/models/extension.g.dart @@ -229,3 +229,28 @@ const _$ExtensionLogLevelEnumMap = { ExtensionLogLevel.info: 'info', ExtensionLogLevel.error: 'error', }; + +ExtensionNetworkLog _$ExtensionNetworkLogFromJson(Map json) => + ExtensionNetworkLog( + extension: Extension.fromJson(json['extension'] as Map), + url: json['url'] as String, + method: json['method'] as String, + statusCode: json['statusCode'] as int?, + responseBody: json['responseBody'] as String?, + requestBody: json['requestBody'] as String?, + requestHeaders: json['requestHeaders'] as Map?, + responseHeaders: json['responseHeaders'] as Map?, + ); + +Map _$ExtensionNetworkLogToJson( + ExtensionNetworkLog instance) => + { + 'extension': instance.extension, + 'responseBody': instance.responseBody, + 'requestBody': instance.requestBody, + 'requestHeaders': instance.requestHeaders, + 'responseHeaders': instance.responseHeaders, + 'url': instance.url, + 'method': instance.method, + 'statusCode': instance.statusCode, + }; diff --git a/lib/utils/extension.dart b/lib/utils/extension.dart index e6c48ed7..a6438402 100644 --- a/lib/utils/extension.dart +++ b/lib/utils/extension.dart @@ -133,7 +133,6 @@ class ExtensionUtils { return; } final windowId = Get.find().extensionLogWindowId.value; - if (windowId == -1) { return; } @@ -155,6 +154,31 @@ class ExtensionUtils { } } + static addNetworkLog( + String key, + ExtensionNetworkLog log, + ) { + if (!Get.isRegistered()) { + return; + } + final windowId = Get.find().extensionLogWindowId.value; + if (windowId == -1) { + return; + } + try { + DesktopMultiWindow.invokeMethod( + windowId, + "addNetworkLog", + jsonEncode({ + 'key': key, + 'log': log.toJson(), + }), + ); + } catch (e) { + debugPrint(e.toString()); + } + } + // ==MiruExtension== // @name Enime // @version v0.0.1 diff --git a/lib/views/pages/debug_page.dart b/lib/views/pages/debug_page.dart new file mode 100644 index 00000000..482c98d7 --- /dev/null +++ b/lib/views/pages/debug_page.dart @@ -0,0 +1,670 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/material.dart' as material; +import 'package:flutter_code_editor/flutter_code_editor.dart'; +import 'package:flutter_highlight/themes/monokai-sublime.dart'; +import 'package:miru_app/models/extension.dart'; +import 'package:miru_app/views/widgets/debug/extension_log_tile.dart'; +import 'package:highlight/languages/json.dart'; + +// 待执行的方法 +final List> _methodList = []; +// 执行结果 key 和 completer +final Map _resultMap = {}; + +// 调用方法 往方法列表里添加方法 +Future callMethod(String method, [dynamic arguments]) async { + final key = UniqueKey().toString(); + _methodList.add({ + "key": key, + "method": method, + "arguments": arguments, + }); + // 等待结果 + final completer = Completer(); + _resultMap[key] = completer; + final result = await completer.future; + _resultMap.remove(key); + return result; +} + +class ExtensionDebugWindow extends StatefulWidget { + const ExtensionDebugWindow({ + super.key, + required this.windowController, + }); + final WindowController windowController; + + @override + State createState() => _ExtensionDebugWindowState(); +} + +class _ExtensionDebugWindowState extends State { + final List _logs = []; + final Map _networkLogs = {}; + + // tab 列表 + final List _tabs = [ + "Log", + "Network", + "Debug", + ]; + // 当前选中的 tab + String _currentTab = "Log"; + + // 扩展列表 + final List _extensions = []; + + // 选择的扩展 + Extension? _selectedExtension; + + @override + void initState() { + DesktopMultiWindow.setMethodHandler((call, fromWindowId) async { + if (call.method == "addLog") { + final log = ExtensionLog.fromJson(jsonDecode(call.arguments)); + if (_selectedExtension == null) { + setState(() { + _logs.add(log); + }); + return null; + } + if (_selectedExtension!.package == log.extension.package) { + setState(() { + _logs.add(log); + }); + } + } + + if (call.method == "addNetworkLog") { + final args = jsonDecode(call.arguments); + final key = args["key"]; + final log = ExtensionNetworkLog.fromJson(args["log"]); + if (_selectedExtension == null) { + setState(() { + _networkLogs[key] = log; + }); + return null; + } + if (_selectedExtension!.package == log.extension.package) { + setState(() { + _networkLogs[key] = log; + }); + } + } + + if (call.method == "state") { + return "yes"; + } + // 主窗口轮询,返回方法列表里的方法 + if (call.method == "getMethods") { + final methods = [..._methodList]; + _methodList.clear(); + return methods; + } + + // 主窗口返回执行结果 + if (call.method == "result") { + final arg = call.arguments; + final key = arg["key"]; + final result = arg["result"]; + final completer = _resultMap[key]; + if (completer != null) { + completer.complete( + result, + ); + } + } + }); + _getInstalledExtensions(); + super.initState(); + } + + // 获取已安装扩展列表 + _getInstalledExtensions() async { + final extensions = await callMethod("getInstalledExtensions"); + debugPrint(extensions.toString()); + List list = List.from(extensions); + final extList = list.map((e) => Map.from(e)).toList(); + setState(() { + _extensions.clear(); + _extensions.addAll( + extList.map((e) => Extension.fromJson(e)).toList(), + ); + }); + } + + @override + Widget build(BuildContext context) { + final views = [ + ConsoleView( + logs: _logs, + onClear: () { + setState(() { + _logs.clear(); + }); + }, + ), + NetworkView( + logs: _networkLogs, + onClear: () { + setState(() { + _networkLogs.clear(); + }); + }, + ), + DebugView( + selectedExtension: _selectedExtension, + ), + ]; + + return FluentApp( + debugShowCheckedModeBanner: false, + theme: FluentThemeData.dark(), + home: Container( + color: FluentThemeData.dark().acrylicBackgroundColor, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Card( + child: Row( + children: [ + Expanded( + child: Wrap( + spacing: 10, + runSpacing: 10, + children: [ + for (var tab in _tabs) + ToggleButton( + checked: _currentTab == tab, + onChanged: (value) { + if (!value) { + return; + } + setState(() { + _currentTab = tab; + }); + }, + child: Text(tab), + ), + ], + ), + ), + // 获取扩展列表 + Button( + onPressed: _getInstalledExtensions, + child: const Text("Refresh"), + ), + const SizedBox(width: 8), + // 选择扩展 + ComboBox( + onChanged: (value) { + setState(() { + _selectedExtension = value; + }); + }, + items: [ + for (var ext in _extensions) + ComboBoxItem( + value: ext, + child: Row( + children: [ + Text(ext.name), + const SizedBox(width: 8), + Text( + ext.package, + style: const TextStyle( + fontSize: 10, + ), + ) + ], + ), + ), + ], + value: _selectedExtension, + ), + const SizedBox(width: 8), + // 清空选择 + IconButton( + onPressed: () { + setState(() { + _selectedExtension = null; + }); + }, + icon: const Icon( + FluentIcons.clear, + size: 10, + ), + ), + ], + ), + ), + const SizedBox(height: 10), + Expanded( + child: IndexedStack( + index: _tabs.indexOf(_currentTab), + children: views, + ), + ), + ], + ), + ), + ); + } +} + +class ConsoleView extends StatefulWidget { + const ConsoleView({ + super.key, + required this.logs, + this.onClear, + }); + final List logs; + final VoidCallback? onClear; + + @override + State createState() => _ConsoleViewState(); +} + +class _ConsoleViewState extends State { + final ScrollController _controller = ScrollController(); + + List get logs => widget.logs; + // 是否滚动到底部 + bool _isScrollToBottom = true; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + scrollToBottom(); + }); + } + + @override + void didUpdateWidget(covariant ConsoleView oldWidget) { + super.didUpdateWidget(oldWidget); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + if (!_isScrollToBottom) { + return; + } + scrollToBottom(); + }); + } + + // 滚动到底部 + void scrollToBottom() { + if (!_controller.hasClients) { + return; + } + _controller.animateTo( + _controller.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.ease, + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (logs.isEmpty) { + return const Center( + child: Text("No log"), + ); + } + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Button( + onPressed: widget.onClear, + child: const Text("Clear"), + ), + const SizedBox(width: 8), + // 自动滚动到底部 + ToggleButton( + checked: _isScrollToBottom, + onChanged: (value) { + setState(() { + _isScrollToBottom = value; + }); + }, + child: const Text("Auto Scroll"), + ), + ], + ), + ), + const SizedBox(height: 10), + Expanded( + child: ListView( + controller: _controller, + padding: const EdgeInsets.all(10), + children: [ + for (var log in logs) ExtensionLogTile(log: log), + ], + ), + ) + ], + ); + } +} + +class NetworkView extends StatefulWidget { + const NetworkView({ + super.key, + required this.logs, + required this.onClear, + }); + final Map logs; + final VoidCallback? onClear; + + @override + State createState() => _NetworkViewState(); +} + +class _NetworkViewState extends State { + String _selectLogKey = ""; + ExtensionNetworkLog? get _selectLog => widget.logs[_selectLogKey]; + + @override + Widget build(BuildContext context) { + if (widget.logs.isEmpty) { + return const Center( + child: Text("No log"), + ); + } + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Button( + onPressed: widget.onClear, + child: const Text("Clear"), + ), + ], + ), + ), + const SizedBox(height: 10), + Expanded( + child: Row( + children: [ + Expanded( + child: ListView( + padding: const EdgeInsets.all(10), + children: [ + for (var log in widget.logs.entries) + ListTile.selectable( + leading: Text( + log.value.statusCode == null + ? "waiting" + : log.value.statusCode.toString(), + style: TextStyle( + color: log.value.statusCode != null + ? (log.value.statusCode! >= 200 && + log.value.statusCode! < 300 + ? Colors.green + : Colors.red) + : null, + fontWeight: FontWeight.bold, + ), + ), + selectionMode: ListTileSelectionMode.none, + subtitle: Text( + log.value.url, + overflow: TextOverflow.ellipsis, + ), + selected: _selectLogKey == log.key, + title: Text( + log.value.url.split('/').last, + overflow: TextOverflow.ellipsis, + ), + onPressed: () { + setState(() { + if (_selectLogKey == log.key) { + _selectLogKey = ""; + return; + } + _selectLogKey = log.key; + }); + }, + ), + ], + ), + ), + if (_selectLogKey.isNotEmpty && _selectLog != null) + Expanded( + flex: 2, + child: ListView( + children: [ + ListTile( + title: const Text("URL"), + subtitle: SelectableText( + _selectLog!.url, + ), + ), + ListTile( + title: const Text("Method"), + subtitle: SelectableText( + _selectLog!.method, + ), + ), + ListTile( + title: const Text("Status Code"), + subtitle: SelectableText( + _selectLog!.statusCode.toString(), + ), + ), + if (_selectLog!.requestHeaders != null) + ListTile( + title: const Text("Request Headers"), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (var header + in _selectLog!.requestHeaders!.entries) + SelectableText( + "${header.key}: ${header.value}", + ), + ], + ), + ), + ListTile( + title: const Text("Request Body"), + subtitle: SelectableText( + _selectLog!.requestBody.toString(), + ), + ), + if (_selectLog!.responseHeaders != null) + ListTile( + title: const Text("Response Headers"), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (var header + in _selectLog!.responseHeaders!.entries) + SelectableText( + "${header.key}: ${header.value}", + ), + ], + ), + ), + ListTile( + title: const Text("Response Body"), + subtitle: TextBox( + readOnly: true, + maxLines: 100, + controller: TextEditingController( + text: _selectLog!.responseBody, + ), + ), + ), + ], + ), + ) + ], + ), + ) + ], + ); + } +} + +class DebugView extends StatefulWidget { + const DebugView({ + super.key, + required this.selectedExtension, + }); + final Extension? selectedExtension; + + @override + State createState() => _DebugViewState(); +} + +class _DebugViewState extends State { + // 方法列表 + final Map _methods = { + "latest(page: number)": "Get latest data form search page", + "search(keyword: string, page: number, filter: map)": + "Search data by keyword", + "detail(url: string)": "Get detail data by url", + "createFilter(filter: map)": "Create filter by url", + "watch(url: string)": "Watch data by url", + }; + + final _controller = TextEditingController(); + + final _resultController = CodeController( + language: json, + ); + + // 是否等待接收数据 + bool _isLoading = false; + + // 执行方法 + void execute() async { + _isLoading = true; + final method = _controller.text; + if (method.isEmpty) { + return; + } + final result = await callMethod("debugExecute", { + "method": method, + "package": widget.selectedExtension!.package, + }); + debugPrint(result.toString()); + _resultController.text = result.toString(); + _isLoading = false; + setState(() {}); + } + + @override + void dispose() { + _controller.dispose(); + _resultController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (widget.selectedExtension == null) { + return const Center( + child: Text("No extension selected, please select an extension first"), + ); + } + + return Row( + children: [ + Expanded( + child: ListView( + children: [ + for (var method in _methods.entries) + ListTile.selectable( + title: Text( + method.key, + ), + subtitle: Text(method.value), + onSelectionChange: (value) { + if (!value) { + return; + } + setState(() { + _controller.text = 'extension.${method.key}'; + }); + }, + ) + ], + ), + ), + const SizedBox(width: 10), + Expanded( + flex: 3, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: TextBox( + placeholder: "call method", + controller: _controller, + ), + ), + const SizedBox(width: 10), + Button( + onPressed: _isLoading + ? null + : () { + execute(); + }, + child: const Text("Execute"), + ), + ], + ), + const SizedBox(height: 10), + const Text("Result"), + const SizedBox(height: 10), + Expanded( + child: material.MaterialApp( + debugShowCheckedModeBanner: false, + home: material.Scaffold( + backgroundColor: Colors.transparent, + body: CodeTheme( + data: CodeThemeData( + styles: monokaiSublimeTheme, + ), + child: SingleChildScrollView( + child: CodeField( + controller: _resultController, + textStyle: const TextStyle( + fontSize: 14, + ), + ), + ), + ), + ), + ), + ) + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/views/pages/log_page.dart b/lib/views/pages/log_page.dart deleted file mode 100644 index 9604c087..00000000 --- a/lib/views/pages/log_page.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'dart:convert'; - -import 'package:desktop_multi_window/desktop_multi_window.dart'; -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:miru_app/models/extension.dart'; -import 'package:miru_app/views/widgets/log/extension_log_tile.dart'; - -class ExtensionLogWindow extends StatefulWidget { - const ExtensionLogWindow({ - super.key, - required this.windowController, - }); - final WindowController windowController; - - @override - State createState() => _ExtensionLogWindowState(); -} - -class _ExtensionLogWindowState extends State { - List logs = []; - final ScrollController _controller = ScrollController(); - - @override - void initState() { - DesktopMultiWindow.setMethodHandler((call, fromWindowId) async { - if (call.method == "addLog") { - final log = ExtensionLog.fromJson(jsonDecode(call.arguments)); - setState(() { - logs.add(log); - }); - // 延时 - await Future.delayed(const Duration(milliseconds: 100), () { - _controller.animateTo( - _controller.position.maxScrollExtent, - duration: const Duration(milliseconds: 300), - curve: Curves.ease, - ); - }); - } - if (call.method == "state") { - return "yes"; - } - }); - super.initState(); - } - - @override - Widget build(BuildContext context) { - return FluentApp( - debugShowCheckedModeBanner: false, - home: Container( - color: Colors.white, - child: ListView( - controller: _controller, - padding: const EdgeInsets.all(16), - children: [ - if (logs.isEmpty) - const Center( - child: Text("No log"), - ) - else ...[ - for (var log in logs) ExtensionLogTile(log: log), - ] - ], - ), - ), - ); - } -} diff --git a/lib/views/widgets/log/extension_log_tile.dart b/lib/views/widgets/debug/extension_log_tile.dart similarity index 80% rename from lib/views/widgets/log/extension_log_tile.dart rename to lib/views/widgets/debug/extension_log_tile.dart index 8a473e24..2b25cb10 100644 --- a/lib/views/widgets/log/extension_log_tile.dart +++ b/lib/views/widgets/debug/extension_log_tile.dart @@ -8,19 +8,18 @@ class ExtensionLogTile extends StatelessWidget { @override Widget build(BuildContext context) { + Color? color; + + if (log.level == ExtensionLogLevel.error) { + color = Colors.red; + } + return Container( - padding: const EdgeInsets.all(3), + padding: const EdgeInsets.all(10), + color: color?.withAlpha(50), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - padding: const EdgeInsets.all(5), - color: log.level == ExtensionLogLevel.error - ? Colors.red - : Colors.green, - child: Text(log.time.toString()), - ), - const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start,