/* * Copyright 2023 Hongen Wang All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import 'dart:convert'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_code_editor/flutter_code_editor.dart'; import 'package:http/http.dart' as http; import 'package:get/get.dart'; import 'package:proxypin/l10n/app_localizations.dart'; import 'package:flutter_highlight/themes/monokai-sublime.dart'; import 'package:flutter_toastr/flutter_toastr.dart'; import 'package:highlight/languages/javascript.dart'; import 'package:proxypin/network/components/manager/script_manager.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/ui/component/utils.dart'; import 'package:proxypin/ui/component/widgets.dart'; import 'package:proxypin/ui/mobile/widgets/floating_window.dart'; import 'package:proxypin/utils/lang.dart'; import 'package:proxypin/utils/platform.dart'; import 'package:share_plus/share_plus.dart'; import 'package:url_launcher/url_launcher.dart'; /// @author wanghongen /// 2023/10/19 /// js脚本 class MobileScript extends StatefulWidget { const MobileScript({super.key}); @override State createState() => _MobileScriptState(); } bool _refresh = false; /// 刷新脚本 void _refreshScript({bool force = false}) { if (_refresh && !force) { return; } _refresh = true; Future.delayed(const Duration(milliseconds: 1500), () async { _refresh = false; (await ScriptManager.instance).flushConfig(); }); } class _MobileScriptState extends State { AppLocalizations get localizations => AppLocalizations.of(context)!; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(localizations.script, style: const TextStyle(fontSize: 16))), body: Padding( padding: const EdgeInsets.only(left: 15, right: 10), child: futureWidget( ScriptManager.instance, loading: true, (data) => Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ SizedBox( child: ListTile( title: Text(localizations.enableScript), subtitle: Text(localizations.scriptUseDescribe), trailing: SwitchWidget( value: data.enabled, onChanged: (value) { data.enabled = value; _refreshScript(); }, ))), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton.icon( icon: const Icon(Icons.add, size: 18), onPressed: scriptEdit, label: Text(localizations.add)), const SizedBox(width: 5), TextButton.icon( icon: const Icon(Icons.input_rounded, size: 18), onPressed: import, label: Text(localizations.import), ), const SizedBox(width: 5), TextButton.icon( icon: const Icon(Icons.terminal, size: 18), onPressed: consoleLog, label: Text(localizations.logger), ), ], ), const SizedBox(height: 5), Expanded(child: ScriptList(scripts: data.list)), ])))); } void consoleLog() { // FloatingWindowManager().show(context); Navigator.of(context).push(MaterialPageRoute(builder: (context) => const ScriptConsoleLog())); } //导入js Future import() async { FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.any); if (result == null || result.files.isEmpty) { return; } var file = result.files.single.xFile; try { var scriptManager = (await ScriptManager.instance); var json = jsonDecode(utf8.decode(await file.readAsBytes())); if (json is List) { for (var item in json) { var scriptItem = ScriptItem.fromJson(item); await scriptManager.addScript(scriptItem, item['script']); } } else { var scriptItem = ScriptItem.fromJson(json); await scriptManager.addScript(scriptItem, json['script']); } _refreshScript(); if (mounted) { FlutterToastr.show(localizations.importSuccess, context); } setState(() {}); } catch (e, t) { logger.e('导入失败 $file', error: e, stackTrace: t); if (mounted) { FlutterToastr.show("${localizations.importFailed} $e", context); } } } /// 添加脚本 Future scriptEdit() async { Navigator.of(context).push(MaterialPageRoute(builder: (context) => const ScriptEdit())).then((value) { if (value != null) { setState(() {}); } }); } } ///控制台日志 class ScriptConsoleLog extends StatefulWidget { const ScriptConsoleLog({super.key}); @override State createState() => _ScriptConsoleLogState(); } class _ScriptConsoleLogState extends State { int channelId = "ScriptConsoleLog".hashCode; static final List logs = []; static FloatingWindowManager floatingWindowManager = FloatingWindowManager(); final ScrollController _scrollController = ScrollController(); AppLocalizations get localizations => AppLocalizations.of(context)!; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((d) { if (_scrollController.hasClients) { _scrollController.jumpTo(_scrollController.position.maxScrollExtent); } }); LogHandler logHandler = LogHandler( channelId: channelId, handle: (log) { logs.add(log); if (!mounted && !floatingWindowManager.isShow) { logs.clear(); ScriptManager.removeLogHandler(channelId); return; } if (mounted) { setState(() {}); } }); ScriptManager.registerLogHandler(logHandler); } @override void dispose() { super.dispose(); if (!floatingWindowManager.isShow) { logs.clear(); ScriptManager.removeLogHandler(channelId); } _scrollController.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(localizations.logger, style: const TextStyle(fontSize: 16)), actions: [ IconButton( tooltip: localizations.windowMode, onPressed: () { if (floatingWindowManager.isShow) { floatingWindowManager.hide(); return; } floatingWindowManager.show(context, widget: ScriptLogSmallWindow(floatingWindowManager: floatingWindowManager)); }, icon: const Icon(Icons.picture_in_picture_alt_rounded)), const SizedBox(width: 5), IconButton( tooltip: localizations.clear, onPressed: () => setState(() { logs.clear(); }), icon: const Icon(Icons.delete)), const SizedBox(width: 10) ]), body: Container( padding: const EdgeInsets.only(top: 10, bottom: 10, right: 3), decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))), child: Scrollbar( controller: _scrollController, thumbVisibility: true, thickness: 6, interactive: true, child: loggerContent()), )); } Widget loggerContent() { return ListView.builder( controller: _scrollController, itemCount: logs.length, itemBuilder: (context, index) { var log = logs[index]; Color? color; if (log.level == 'error') { color = Colors.red; } else if (log.level == 'warn') { color = Colors.orange; } return Padding( padding: const EdgeInsets.only(bottom: 5, left: 3, right: 3), child: Row( children: [ Text(log.time.timeFormat(), style: const TextStyle(fontSize: 13, color: Colors.grey)), const SizedBox(width: 8), Text(log.level, style: TextStyle(fontSize: 13, color: color)), const SizedBox(width: 8), Expanded(child: SelectableText(log.output, style: TextStyle(fontSize: 13, color: color))), ], )); }); } } class ScriptLogSmallWindow extends StatefulWidget { final FloatingWindowManager floatingWindowManager; const ScriptLogSmallWindow({super.key, required this.floatingWindowManager}); @override State createState() => _ScriptLogSmallWindowState(); } class _ScriptLogSmallWindowState extends State { final List logs = []; final ScrollController _scrollController = ScrollController(); @override void initState() { super.initState(); LogHandler logHandler = LogHandler( channelId: hashCode, handle: (log) { logs.add(log); if (!mounted) { ScriptManager.removeLogHandler(hashCode); return; } setState(() {}); _scrollController.jumpTo(_scrollController.position.maxScrollExtent); }); ScriptManager.registerLogHandler(logHandler); } @override void dispose() { _scrollController.dispose(); logger.d("dispose small window log handler $hashCode"); ScriptManager.removeLogHandler(hashCode); super.dispose(); } @override Widget build(BuildContext context) { return FloatingWindow( top: 320, right: 8, child: Material( child: Container( height: 320, width: 180, decoration: BoxDecoration( color: Colors.teal.withOpacity(0.3), border: Border.all(color: Colors.grey.withOpacity(0.8)), borderRadius: const BorderRadius.all(Radius.circular(10))), child: Stack( children: [ Positioned( top: -12, left: -5, child: IconButton( onPressed: () { Navigator.of(context) .push(MaterialPageRoute(builder: (context) => const ScriptConsoleLog())); }, icon: const Icon(Icons.picture_in_picture, size: 20))), Positioned( top: -12, right: -8, child: IconButton( onPressed: () => widget.floatingWindowManager.hide(), icon: const Icon(Icons.close, size: 20))), list() ], )))); } Widget list() { return Padding( padding: const EdgeInsets.only(bottom: 5, top: 18), child: Scrollbar( controller: _scrollController, thumbVisibility: true, thickness: 2, child: ListView.builder( controller: _scrollController, itemCount: logs.length, itemBuilder: (context, index) { var log = logs[index]; return Padding( padding: const EdgeInsets.only(bottom: 3, left: 3, right: 3), child: Text(log.output, maxLines: 3, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 13, color: log.level == 'error' ? Colors.red : null))); }))); } } /// 编辑脚本 class ScriptEdit extends StatefulWidget { final ScriptItem? scriptItem; final String? script; final String? url; final List? urls; final String? title; final bool fromRemoteUrl; const ScriptEdit({ super.key, this.scriptItem, this.script, this.url, this.urls, this.title, this.fromRemoteUrl = false, }); @override State createState() => _ScriptEditState(); } class _ScriptEditState extends State { late CodeController script; late TextEditingController nameController; late List urlControllers; late TextEditingController remoteUrlController; late bool _useRemote; final RxBool _fetchingRemoteScript = false.obs; AppLocalizations get localizations => AppLocalizations.of(context)!; @override void initState() { super.initState(); final urls = widget.scriptItem?.urls ?? (widget.urls != null && widget.urls!.isNotEmpty ? widget.urls! : (widget.url != null && widget.url!.isNotEmpty ? [widget.url!] : [])); urlControllers = urls.isNotEmpty ? urls.map((u) => TextEditingController(text: u)).toList() : [TextEditingController()]; script = CodeController(language: javascript, text: widget.script ?? ScriptManager.template); nameController = TextEditingController(text: widget.scriptItem?.name ?? widget.title ?? ''); remoteUrlController = TextEditingController(text: widget.scriptItem?.remoteUrl ?? ''); _useRemote = widget.fromRemoteUrl || ((widget.scriptItem?.remoteUrl ?? '').trim().isNotEmpty); } @override void dispose() { for (final c in urlControllers) { c.dispose(); } script.dispose(); nameController.dispose(); remoteUrlController.dispose(); _fetchingRemoteScript.close(); super.dispose(); } Future _fetchRemoteScript() async { if (_fetchingRemoteScript.value) return; final remoteUrl = remoteUrlController.text.trim(); if (remoteUrl.isEmpty) { FlutterToastr.show("${localizations.remoteUrl} ${localizations.cannotBeEmpty}", context, position: FlutterToastr.top); return; } final uri = Uri.tryParse(remoteUrl); if (uri == null || !(uri.scheme == 'http' || uri.scheme == 'https')) { FlutterToastr.show("${localizations.remoteUrl} ${localizations.fail}", context, position: FlutterToastr.top); return; } try { _fetchingRemoteScript.value = true; final resp = await http.get(uri); if (resp.statusCode < 200 || resp.statusCode >= 300) { FlutterToastr.show("Fetch failed: HTTP ${resp.statusCode}", context, position: FlutterToastr.top); return; } final content = utf8.decode(resp.bodyBytes); script.text = content; if (mounted) { setState(() {}); } } catch (e) { if (mounted) { FlutterToastr.show("Fetch failed: $e", context, position: FlutterToastr.top); } } finally { _fetchingRemoteScript.value = false; } } void _resetScript() { script.text = ScriptManager.template; } @override Widget build(BuildContext context) { GlobalKey formKey = GlobalKey(); bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh'); return Scaffold( appBar: AppBar( title: Row(children: [ Text(localizations.scriptEdit, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), const SizedBox(width: 10), Text.rich(TextSpan( text: localizations.useGuide, style: const TextStyle(color: Colors.blue, fontSize: 14), recognizer: TapGestureRecognizer() ..onTap = () => launchUrl( mode: LaunchMode.externalApplication, Uri.parse(isCN ? 'https://gitee.com/wanghongenpin/proxypin/wikis/%E8%84%9A%E6%9C%AC' : 'https://github.com/wanghongenpin/proxypin/wiki/Script')))), ]), actions: [ TextButton( onPressed: () async { if (!(formKey.currentState as FormState).validate()) { FlutterToastr.show("${localizations.name} URL ${localizations.cannotBeEmpty}", context, position: FlutterToastr.top); return; } // 收集所有非空、去重的 url final urls = urlControllers.map((c) => c.text.trim()).where((u) => u.isNotEmpty).toSet().toList(); if (urls.isEmpty) { FlutterToastr.show("URL ${localizations.cannotBeEmpty}", context, position: FlutterToastr.top); return; } // Only persist remoteUrl when remote mode is enabled. final remoteUrl = _useRemote ? remoteUrlController.text.trim() : ''; final hasRemote = remoteUrl.isNotEmpty; if (_useRemote && !hasRemote) { FlutterToastr.show("Remote URL ${localizations.cannotBeEmpty}", context, position: FlutterToastr.top); return; } var scriptManager = await ScriptManager.instance; if (widget.scriptItem == null) { var scriptItem = ScriptItem(true, nameController.text, urls); scriptItem.remoteUrl = _useRemote ? remoteUrl : null; await scriptManager.addScript(scriptItem, script.text); } else { widget.scriptItem?.name = nameController.text; widget.scriptItem?.urls = urls; widget.scriptItem?.urlRegs = null; widget.scriptItem?.remoteUrl = _useRemote ? remoteUrl : null; await scriptManager.updateScript(widget.scriptItem!, script.text); } _refreshScript(force: true); if (context.mounted) { FlutterToastr.show(localizations.saveSuccess, context); Navigator.of(context).maybePop(true); } }, child: Text(localizations.save)), ]), body: Form( key: formKey, child: ListView( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8), children: [ // Name section Card( color: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.5), elevation: 0, shape: RoundedRectangleBorder( side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.4)), borderRadius: BorderRadius.circular(8)), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), child: textField("${localizations.name}:", nameController, localizations.pleaseEnter))), // URLs section Card( color: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.5), elevation: 0, shape: RoundedRectangleBorder( side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.4)), borderRadius: BorderRadius.circular(8)), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ const Text("URL(s):"), const SizedBox(width: 8), IconButton( icon: const Icon(Icons.add_outlined, size: 20), tooltip: localizations.add, onPressed: () => setState(() => urlControllers.add(TextEditingController()))), const Spacer(), Text("${urlControllers.length}", style: const TextStyle(fontSize: 12, color: Colors.grey)) ]), const SizedBox(height: 6), ...List.generate( urlControllers.length, (i) => Padding( padding: const EdgeInsets.only(bottom: 8), child: Row(children: [ Expanded( child: TextFormField( controller: urlControllers[i], validator: (val) => val?.isNotEmpty == true ? null : "", keyboardType: TextInputType.url, decoration: InputDecoration( hintText: "github.com/api/*", hintStyle: const TextStyle(fontSize: 14, color: Colors.grey), contentPadding: const EdgeInsets.all(10), errorStyle: const TextStyle(height: 0, fontSize: 0), focusedBorder: focusedBorder(), isDense: true, border: const OutlineInputBorder(), ), )), if (urlControllers.length > 1) IconButton( icon: const Icon(Icons.remove_circle_outline, color: Colors.red), tooltip: localizations.delete, onPressed: () { setState(() { urlControllers[i].dispose(); urlControllers.removeAt(i); }); }), ]))) ]))), // Source section Card( color: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.5), elevation: 0, shape: RoundedRectangleBorder( side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.4)), borderRadius: BorderRadius.circular(8)), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), child: Row(children: [ SizedBox(width: 55, child: Text('${localizations.type}:')), Expanded( child: DropdownButtonFormField( initialValue: _useRemote, items: [ DropdownMenuItem(value: false, child: Text(localizations.local)), DropdownMenuItem(value: true, child: Text(localizations.remoteUrl)), ], onChanged: (val) { if (val == null) return; setState(() { _useRemote = val; }); }, decoration: InputDecoration( contentPadding: const EdgeInsets.all(10), focusedBorder: focusedBorder(), isDense: true, border: const OutlineInputBorder(), ), )) ]))), // Remote URL section if (_useRemote) Card( color: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.5), elevation: 0, shape: RoundedRectangleBorder( side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.4)), borderRadius: BorderRadius.circular(8)), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), child: Row(children: [ SizedBox(width: 65, child: Text('${localizations.remoteUrl}:')), Expanded( child: SizedBox( height: 34, child: TextFormField( controller: remoteUrlController, keyboardType: TextInputType.url, decoration: InputDecoration( hintText: 'https://example.com/script.js', hintStyle: const TextStyle(fontSize: 14, color: Colors.grey), contentPadding: const EdgeInsets.all(10), focusedBorder: focusedBorder(), isDense: true, border: const OutlineInputBorder(), ), onFieldSubmitted: (_) => _fetchRemoteScript(), ), ), ), const SizedBox(width: 3), Obx(() { // Keep the button visually aligned with the text field by fixing the height // and using a compact FilledButton (with icon when idle and spinner when fetching). return SizedBox( height: 34, child: Tooltip( message: localizations.view, child: FilledButton.tonal( onPressed: _fetchRemoteScript, style: FilledButton.styleFrom( minimumSize: const Size(44, 34), padding: const EdgeInsets.symmetric(horizontal: 8), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6))), child: _fetchingRemoteScript.value ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.cloud_download, size: 18), ), ), ); }), ]))), // Script section Card( color: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.5), elevation: 0, shape: RoundedRectangleBorder( side: BorderSide(color: Theme.of(context).dividerColor.withOpacity(0.4)), borderRadius: BorderRadius.circular(8)), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Text("${localizations.script}:", style: const TextStyle(fontWeight: FontWeight.w500)), const Spacer(), Tooltip( message: localizations.copy, child: IconButton( icon: const Icon(Icons.copy_all_outlined, size: 20), onPressed: () { Clipboard.setData(ClipboardData(text: script.text)); FlutterToastr.show(localizations.copied, context, position: FlutterToastr.top); })), Tooltip( message: 'Reset', child: IconButton( icon: const Icon(Icons.settings_backup_restore, size: 22), onPressed: _resetScript)), Tooltip( message: localizations.clear, child: IconButton( icon: const Icon(Icons.delete_sweep_outlined, size: 22), onPressed: () { script.text = ''; setState(() {}); })) ]), CodeTheme( data: CodeThemeData(styles: monokaiSublimeTheme), child: ClipRRect( borderRadius: BorderRadius.circular(6), child: Container( decoration: BoxDecoration( color: Colors.grey.shade900, border: Border.all(color: Colors.grey.withOpacity(0.2))), child: SingleChildScrollView( child: CodeField( readOnly: _useRemote, enableSuggestions: true, textStyle: const TextStyle(fontSize: 13, color: Colors.white), controller: script, gutterStyle: const GutterStyle(width: 50, margin: 0), ))))), ]))), ], ))); } Widget textField(String label, TextEditingController controller, String hint, {TextInputType? keyboardType}) { return Row(children: [ SizedBox(width: 65, child: Text(label)), Expanded( child: TextFormField( controller: controller, validator: (val) => val?.isNotEmpty == true ? null : "", keyboardType: keyboardType, decoration: InputDecoration( hintText: hint, contentPadding: const EdgeInsets.all(10), hintStyle: const TextStyle(fontSize: 14, color: Colors.grey), errorStyle: const TextStyle(height: 0, fontSize: 0), focusedBorder: focusedBorder(), isDense: true, border: const OutlineInputBorder()), )) ]); } InputBorder focusedBorder() { return OutlineInputBorder(borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2)); } } /// 脚本列表 class ScriptList extends StatefulWidget { final List scripts; const ScriptList({super.key, required this.scripts}); @override State createState() => _ScriptListState(); } class _ScriptListState extends State { Set selected = {}; bool multiple = false; AppLocalizations get localizations => AppLocalizations.of(context)!; @override Widget build(BuildContext context) { return Scaffold( persistentFooterButtons: multiple ? [globalMenu()] : null, body: Container( padding: const EdgeInsets.only(top: 10, bottom: 30), decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))), child: Scrollbar( child: ListView(children: [ Row( mainAxisAlignment: MainAxisAlignment.start, children: [ Container(width: 100, padding: const EdgeInsets.only(left: 10), child: Text(localizations.name)), SizedBox(width: 50, child: Text(localizations.enable, textAlign: TextAlign.center)), const VerticalDivider(), const Expanded(child: Text("URL")), ], ), const Divider(thickness: 0.5), Column(children: rows(widget.scripts)) ])))); } Stack globalMenu() { return Stack(children: [ Container( height: 50, width: double.infinity, margin: const EdgeInsets.only(top: 10), decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2)))), Positioned( top: 0, left: 0, right: 0, child: Center( child: TextButton( onPressed: () {}, child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ TextButton.icon( onPressed: () { export(context, selected.toList()); setState(() { selected.clear(); multiple = false; }); }, icon: const Icon(Icons.share, size: 18), label: Text(localizations.export, style: const TextStyle(fontSize: 14))), TextButton.icon( onPressed: () => removeScripts(selected.toList()), icon: const Icon(Icons.delete, size: 18), label: Text(localizations.delete, style: const TextStyle(fontSize: 14))), TextButton.icon( onPressed: () { setState(() { multiple = false; selected.clear(); }); }, icon: const Icon(Icons.cancel, size: 18), label: Text(localizations.cancel, style: const TextStyle(fontSize: 14))), ])))) ]); } List rows(List list) { var primaryColor = Theme.of(context).colorScheme.primary; return List.generate(list.length, (index) { final item = list[index]; final isRemote = item.remoteUrl != null && item.remoteUrl!.trim().isNotEmpty; return InkWell( splashColor: primaryColor.withOpacity(0.3), onTap: () async { if (multiple) { setState(() { if (!selected.add(index)) { selected.remove(index); } }); return; } showEdit(index); }, onLongPress: () => showMenus(index), child: Container( color: selected.contains(index) ? primaryColor.withOpacity(0.8) : index.isEven ? Colors.grey.withOpacity(0.1) : null, height: 45, padding: const EdgeInsets.all(5), child: Row( children: [ SizedBox( width: 100, child: Row(children: [ Expanded(child: Text(list[index].name ?? '', style: const TextStyle(fontSize: 13))), if (isRemote) const Padding( padding: EdgeInsets.only(left: 6), child: Text('R', style: TextStyle(fontSize: 11, color: Colors.blue))), ])), SizedBox( width: 50, child: Transform.scale( scale: 0.65, child: SwitchWidget( value: list[index].enabled, onChanged: (val) { list[index].enabled = val; _refreshScript(); }))), const SizedBox(width: 10), Expanded( child: Text(list[index].urls.join(', ').fixAutoLines(), style: const TextStyle(fontSize: 13))), ], ))); }); } //点击菜单 void showMenus(int index) { setState(() { selected.add(index); }); showModalBottomSheet( context: context, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(10))), enableDrag: true, builder: (context) { return Wrap( alignment: WrapAlignment.center, children: [ BottomSheetItem( text: localizations.multiple, onPressed: () { setState(() => multiple = true); }), const Divider(thickness: 0.5, height: 1), BottomSheetItem(text: localizations.edit, onPressed: () => showEdit(index)), const Divider(thickness: 0.5, height: 1), BottomSheetItem(text: localizations.share, onPressed: () => export(context, [index])), const Divider(thickness: 0.5, height: 1), BottomSheetItem( text: widget.scripts[index].enabled ? localizations.disabled : localizations.enable, onPressed: () { widget.scripts[index].enabled = !widget.scripts[index].enabled; _refreshScript(); }), const Divider(thickness: 0.5, height: 1), BottomSheetItem( text: localizations.delete, onPressed: () async { await (await ScriptManager.instance).removeScript(index); _refreshScript(force: true); if (context.mounted) FlutterToastr.show(localizations.importSuccess, context); }), Container(color: Theme.of(context).hoverColor, height: 8), TextButton( child: Container( height: 45, width: double.infinity, padding: const EdgeInsets.only(top: 10), child: Text(localizations.cancel, textAlign: TextAlign.center)), onPressed: () { Navigator.of(context).pop(); }, ), ], ); }).then((value) { if (multiple) { return; } setState(() { selected.remove(index); }); }); } Future showEdit([int? index]) async { String? script; if (index != null) { var scriptManager = await ScriptManager.instance; var scriptItem = widget.scripts[index]; if (scriptItem.remoteUrl == null || scriptItem.remoteUrl?.isEmpty == true) { script = await scriptManager.getScript(scriptItem); } } if (!mounted) { return; } Navigator.of(context) .push(MaterialPageRoute( builder: (context) => ScriptEdit(scriptItem: index == null ? null : widget.scripts[index], script: script))) .then((value) { if (value != null) { setState(() {}); } }); } //导出js Future export(BuildContext context, List indexes) async { if (indexes.isEmpty) return; //文件名称 String fileName = 'proxypin-scripts.json'; var scriptManager = await ScriptManager.instance; List json = []; for (var idx in indexes) { var item = widget.scripts[idx]; var map = item.toJson(); map.remove("scriptPath"); if (item.remoteUrl == null || item.remoteUrl!.trim().isEmpty) { map['script'] = await scriptManager.getScript(item); } json.add(map); } RenderBox? box; if (await Platforms.isIpad() && context.mounted) { box = context.findRenderObject() as RenderBox?; } final XFile file = XFile.fromData(utf8.encode(jsonEncode(json)), mimeType: 'json'); Share.shareXFiles([file], fileNameOverrides: [fileName], sharePositionOrigin: box?.paintBounds); } void enableStatus(bool enable) { for (var idx in selected) { widget.scripts[idx].enabled = enable; } setState(() {}); _refreshScript(); } Future removeScripts(List indexes) async { if (indexes.isEmpty) return; showConfirmDialog(context, content: localizations.confirmContent, onConfirm: () async { var scriptManager = await ScriptManager.instance; for (var idx in indexes) { await scriptManager.removeScript(idx); } setState(() { selected.clear(); }); _refreshScript(force: true); if (mounted) FlutterToastr.show(localizations.deleteSuccess, context); }); } }