diff --git a/lib/main.dart b/lib/main.dart index eafdb1f..324ca3e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -25,9 +25,9 @@ import 'dart:math'; import 'package:clipboard/clipboard.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_pty/flutter_pty.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:saf/saf.dart'; //import 'package:flutter/services.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:flutter/material.dart'; @@ -1011,6 +1011,20 @@ class _MyHomePageState extends State { }, child: const Text("添加")), ]); },); + }, onLongPress: () { + showDialog(context: context, builder: (context) { + return AlertDialog(content: const Text("是否重置所有快捷指令?"), actions: [ + TextButton(onPressed:() { + Navigator.of(context).pop(); + }, child: const Text("取消")), + TextButton(onPressed:() async { + await Util.setCurrentProp("commands", D.commands); + setState(() {}); + if (!context.mounted) return; + Navigator.of(context).pop(); + }, child: const Text("是")), + ]); + }); }, child: const Text("添加快捷指令")))), Padding(padding: const EdgeInsets.all(8), child: Card(child: Padding(padding: const EdgeInsets.all(8), child: Column(children: [ @@ -1234,6 +1248,120 @@ class _MyHomePageState extends State { Permission.manageExternalStorage.request(); }), ]), + const Text("这里可以将设备上的文件夹与软件容器内的文件夹绑定,在下次启动软件时生效。"), + ListView.builder(itemBuilder: (context, index) { + final Map e = jsonDecode(Util.getGlobal("customMounts")[index]); + return GestureDetector(onLongPress: () { + String name = e["name"]!; + bool isNameValid = false; + Saf pathSaf = Saf(e["path"]!); + bool isPathValid = false; + showDialog(context: context, builder: (context) { + return AlertDialog(title: const Text("选项编辑"), content: SingleChildScrollView(child: Column(children: [ + TextFormField(initialValue: name, decoration: const InputDecoration(border: OutlineInputBorder(), labelText: "挂载到主文件夹的名称"), validator: (value) { + if (!RegExp("[^\\s\\\\/:\\*\\?\\\"<>\\|](\\x20|[^\\s\\\\/:\\*\\?\\\"<>\\|])*[^\\s\\\\/:\\*\\?\\\"<>\\|\\.]\$").hasMatch(value!)) { + return "非法文件名"; + } + isNameValid = true; + return null; + }), + SizedBox.fromSize(size: const Size.square(8)), + TextFormField(maxLines: null, initialValue: pathSaf.directory, decoration: const InputDecoration(border: OutlineInputBorder(), labelText: "在本机的路径"), readOnly: true, onTap: () async { + isPathValid = (await pathSaf.getDirectoryPermission(isDynamic: true))??false; + }), + ])), actions: [ + TextButton(onPressed:() async { + await G.prefs.setStringList("customMounts", Util.getGlobal("customMounts") + ..removeAt(index)); + setState(() {}); + if (!context.mounted) return; + Navigator.of(context).pop(); + }, child: const Text("删除该项")), + TextButton(onPressed:() { + Navigator.of(context).pop(); + }, child: const Text("取消")), + TextButton(onPressed:() async { + if (!isNameValid || !isPathValid) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("名称非法或路径无效")) + ); + return; + } + await G.prefs.setStringList("customMounts", Util.getGlobal("customMounts") + ..setAll(index, [{"name": name, "path": pathSaf.directory, "isEnabled": e["isEnabled"]}])); + setState(() {}); + if (!context.mounted) return; + Navigator.of(context).pop(); + }, child: const Text("保存")), + ]); + },); + + }, child: CheckboxListTile(title: Text(e["name"]), subtitle: Text(e["path"]), value: e["isEnabled"], onChanged: (value) async { + await G.prefs.setStringList("customMounts", Util.getGlobal("customMounts")..setAll(index, [jsonEncode(e..update("isEnabled", (v) { + return value!; + }))])); + setState(() {}); + })); + }, shrinkWrap: true, itemCount: Util.getGlobal("customMounts").length), + ListTile(title: const Text("添加路径"), onTap: () async { + Saf pathSaf = Saf("/storage/self/primary/Download"); + bool hasPermission = (await pathSaf.getDirectoryPermission(isDynamic: true))??false; + if (!hasPermission) { + return; + } + String name = "新路径"; + bool isNameValid = false; + bool isPathValid = false; + if (!context.mounted) return; + showDialog(context: context, builder: (context) { + return AlertDialog(title: const Text("选项编辑"), content: SingleChildScrollView(child: Column(children: [ + TextFormField(initialValue: name, decoration: const InputDecoration(border: OutlineInputBorder(), labelText: "挂载到主文件夹的名称"), validator: (value) { + if (!RegExp("[^\\s\\\\/:\\*\\?\\\"<>\\|](\\x20|[^\\s\\\\/:\\*\\?\\\"<>\\|])*[^\\s\\\\/:\\*\\?\\\"<>\\|\\.]\$").hasMatch(value!)) { + return "非法文件名"; + } + isNameValid = true; + return null; + }), + SizedBox.fromSize(size: const Size.square(8)), + TextFormField(maxLines: null, initialValue: pathSaf.directory, decoration: const InputDecoration(border: OutlineInputBorder(), labelText: "在本机的路径"), readOnly: true, onTap: () async { + isPathValid = (await pathSaf.getDirectoryPermission(isDynamic: true))??false; + }), + ])), actions: [ + TextButton(onPressed:() { + Navigator.of(context).pop(); + }, child: const Text("取消")), + TextButton(onPressed:() async { + if (!isNameValid || !isPathValid) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("名称非法或路径无效")) + ); + return; + } + await G.prefs.setStringList("customMounts", Util.getGlobal("customMounts") + ..add({"name": name, "path": pathSaf.directory, "isEnabled": true})); + setState(() {}); + if (!context.mounted) return; + Navigator.of(context).pop(); + }, child: const Text("保存")), + ]); + }); + }, onLongPress: () { + showDialog(context: context, builder: (context) { + return AlertDialog(content: const Text("是否清空所有路径?"), actions: [ + TextButton(onPressed:() { + Navigator.of(context).pop(); + }, child: const Text("取消")), + TextButton(onPressed:() async { + await G.prefs.setStringList("customMounts", []); + setState(() {}); + if (!context.mounted) return; + Navigator.of(context).pop(); + }, child: const Text("是")), + ]); + }); + }) ],))), ExpansionPanel( isExpanded: _expandState[4], diff --git a/lib/workflow.dart b/lib/workflow.dart index d1f2727..4250706 100644 --- a/lib/workflow.dart +++ b/lib/workflow.dart @@ -87,9 +87,13 @@ class Util { //String defaultFFmpegCommand 默认推流命令 //? int bootstrapVersion: 启动包版本 //String[] containersInfo: 所有容器信息(json) - //{name, boot:"\$DATA_DIR/bin/proot ...", vnc:"startnovnc", vncUrl:"...", commands:[{name:"更新和升级", command:"apt update -y && apt upgrade -y"}, ...]} + //{name, boot:"\$DATA_DIR/bin/proot ...", vnc:"startnovnc", vncUrl:"...", commands:[{name:"更新和升级", command:"apt update -y && apt upgrade -y"}, + // bind:[{name:"U盘", src:"/storage/xxxx", dst:"/media/meow"}]...]} //String[] adsBonus: 观看广告获取的奖励(json) //{name: "xxx", amount: xxx} + //String[] customMounts: 自定义挂载地址(json) + //{name: "xxx", path: "xxx", isEnabled: true} + //TODO: 这么写还是不对劲,有空改成类试试? static dynamic getGlobal(String key) { bool b = G.prefs.containsKey(key); switch (key) { @@ -109,6 +113,7 @@ class Util { case "defaultFFmpegCommand" : return b ? G.prefs.getString(key)! : (value){G.prefs.setString(key, value); return value;}("-hide_banner -an -max_delay 1000000 -r 30 -f android_camera -i 0:0 -vf scale=iw/2:-1 -rtsp_transport udp -f rtsp rtsp://127.0.0.1:8554/stream"); case "containersInfo" : return G.prefs.getStringList(key)!; case "adsBonus" : return b ? G.prefs.getStringList(key)! : (value){G.prefs.setStringList(key, value); return value;}([].cast()); + case "customMounts" : return b ? G.prefs.getStringList(key)! : (value){G.prefs.setStringList(key, value); return value;}([].cast()); } } @@ -312,6 +317,34 @@ class TermPty { } +//default values +class D { + //默认快捷指令 + static const commands = [{"name":"检查更新并升级", "command":"sudo apt update && sudo apt upgrade -y"}, +{"name":"查看系统信息", "command":"neofetch -L && neofetch --off"}, +{"name":"清屏", "command":"clear"}, +{"name":"安装图形处理软件Krita", "command":"sudo apt update && sudo apt install -y krita krita-l10n"}, +{"name":"卸载图形处理软件Krita", "command":"sudo apt autoremove --purge -y krita krita-l10n"}, +{"name":"安装视频剪辑软件Kdenlive", "command":"sudo apt update && sudo apt install -y kdenlive"}, +{"name":"卸载视频剪辑软件Kdenlive", "command":"sudo apt autoremove --purge -y kdenlive"}, +{"name":"安装科学计算软件Octave", "command":"sudo apt update && sudo apt install -y octave"}, +{"name":"卸载科学计算软件Octave", "command":"sudo apt autoremove --purge -y octave"}, +{"name":"安装WPS", "command":"wget https://wps-linux-personal.wpscdn.cn/wps/download/ep/Linux2019/11704/wps-office_11.1.0.11704_arm64.deb -O /tmp/wps.deb && sudo apt update && sudo apt install -y /tmp/wps.deb; rm /tmp/wps.deb"}, +{"name":"卸载WPS", "command":"sudo apt autoremove --purge -y wps-office"}, +{"name":"安装CAJViewer", "command":"wget https://download.cnki.net/net.cnki.cajviewer_1.3.20-1_arm64.deb -O /tmp/caj.deb && sudo apt update && sudo apt install -y /tmp/caj.deb && bash /home/tiny/.local/share/tiny/caj/postinst; rm /tmp/caj.deb"}, +{"name":"卸载CAJViewer", "command":"sudo apt autoremove --purge -y net.cnki.cajviewer && bash /home/tiny/.local/share/tiny/caj/postrm"}, +{"name":"安装亿图图示", "command":"wget https://www.edrawsoft.cn/2download/aarch64/edrawmax_11.5.6-3_arm64.deb -O /tmp/edraw.deb && sudo apt update && sudo apt install -y /tmp/edraw.deb && bash /home/tiny/.local/share/tiny/edraw/postinst; rm /tmp/edraw.deb"}, +{"name":"卸载亿图图示", "command":"sudo apt autoremove --purge -y edrawmax libldap-2.4-2"}, +{"name":"安装QQ", "command":"wget https://dldir1.qq.com/qqfile/qq/QQNT/b69de82d/linuxqq_3.2.1-17153_arm64.deb -O /tmp/qq.deb && sudo apt update && sudo apt install -y /tmp/qq.deb && sed -i 's#Exec=/opt/QQ/qq %U#Exec=/opt/QQ/qq --no-sandbox %U#g' /usr/share/applications/qq.desktop; rm /tmp/qq.deb"}, +{"name":"卸载QQ", "command":"sudo apt autoremove --purge -y linuxqq"}, +{"name":"修复无法编译C语言程序", "command":"sudo apt update && sudo apt reinstall -y libc6-dev"}, +{"name":"修复系统语言到中文", "command":"sudo localedef -c -i zh_CN -f UTF-8 zh_CN.UTF-8"}, +{"name":"启用回收站", "command":"sudo apt update && sudo apt install -y gvfs && echo '安装完成, 重启软件即可使用回收站。'"}, +{"name":"拉流测试", "command":"ffplay rtsp://127.0.0.1:8554/stream &"}, +{"name":"关机", "command":"stopvnc\nexit\nexit"}, +{"name":"???", "command":"timeout 8 cmatrix"}]; +} + // Global variables class G { static late final String dataPath; @@ -521,29 +554,7 @@ done "boot":"\$DATA_DIR/bin/proot -H --change-id=1000:1000 --pwd=/home/tiny --rootfs=\$CONTAINER_DIR --mount=/system --mount=/apex --kill-on-exit --mount=/storage:/storage --sysvipc -L --link2symlink --mount=/proc:/proc --mount=/dev:/dev --mount=\$CONTAINER_DIR/tmp:/dev/shm --mount=/dev/urandom:/dev/random --mount=/proc/self/fd:/dev/fd --mount=/proc/self/fd/0:/dev/stdin --mount=/proc/self/fd/1:/dev/stdout --mount=/proc/self/fd/2:/dev/stderr --mount=/dev/null:/dev/tty0 --mount=/dev/null:/proc/sys/kernel/cap_last_cap --mount=/storage/self/primary:/media/sd --mount=\$DATA_DIR/share:/home/tiny/公共 --mount=/storage/self/primary/Fonts:/usr/share/fonts/wpsm --mount=/storage/self/primary/AppFiles/Fonts:/usr/share/fonts/yozom --mount=/system/fonts:/usr/share/fonts/androidm --mount=/storage/self/primary/Pictures:/home/tiny/图片 --mount=/storage/self/primary/Music:/home/tiny/音乐 --mount=/storage/self/primary/Movies:/home/tiny/视频 --mount=/storage/self/primary/Download:/home/tiny/下载 --mount=/storage/self/primary/DCIM:/home/tiny/照片 --mount=/storage/self/primary/Documents:/home/tiny/文档 --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/.tmoe-container.stat:/proc/stat --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/.tmoe-container.version:/proc/version --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/bus:/proc/bus --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/buddyinfo:/proc/buddyinfo --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/cgroups:/proc/cgroups --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/consoles:/proc/consoles --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/crypto:/proc/crypto --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/devices:/proc/devices --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/diskstats:/proc/diskstats --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/execdomains:/proc/execdomains --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/fb:/proc/fb --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/filesystems:/proc/filesystems --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/interrupts:/proc/interrupts --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/iomem:/proc/iomem --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/ioports:/proc/ioports --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/kallsyms:/proc/kallsyms --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/keys:/proc/keys --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/key-users:/proc/key-users --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/kpageflags:/proc/kpageflags --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/loadavg:/proc/loadavg --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/locks:/proc/locks --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/misc:/proc/misc --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/modules:/proc/modules --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/pagetypeinfo:/proc/pagetypeinfo --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/partitions:/proc/partitions --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/sched_debug:/proc/sched_debug --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/softirqs:/proc/softirqs --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/timer_list:/proc/timer_list --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/uptime:/proc/uptime --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/vmallocinfo:/proc/vmallocinfo --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/vmstat:/proc/vmstat --mount=\$CONTAINER_DIR/usr/local/etc/tmoe-linux/proot_proc/zoneinfo:/proc/zoneinfo /usr/bin/env -i HOSTNAME=TINY HOME=/home/tiny USER=tiny TERM=xterm-256color SDL_IM_MODULE=fcitx XMODIFIERS=@im=fcitx QT_IM_MODULE=fcitx GTK_IM_MODULE=fcitx TMOE_CHROOT=false TMOE_PROOT=true TMPDIR=/tmp MOZ_FAKE_NO_SANDBOX=1 DISPLAY=:4 PULSE_SERVER=tcp:127.0.0.1:4718 LANG=zh_CN.UTF-8 SHELL=/bin/bash PATH=/usr/local/sbin:/usr/local/bin:/bin:/usr/bin:/sbin:/usr/sbin:/usr/games:/usr/local/games /bin/bash -l", "vnc":"startnovnc &", "vncUrl":"http://localhost:36082/vnc.html?host=localhost&port=36082&autoconnect=true&resize=remote&password=12345678", -"commands":[{"name":"检查更新并升级", "command":"sudo apt update && sudo apt upgrade -y"}, -{"name":"查看系统信息", "command":"neofetch -L && neofetch --off"}, -{"name":"清屏", "command":"clear"}, -{"name":"安装图形处理软件Krita", "command":"sudo apt update && sudo apt install -y krita krita-l10n"}, -{"name":"卸载图形处理软件Krita", "command":"sudo apt autoremove --purge -y krita krita-l10n"}, -{"name":"安装视频剪辑软件Kdenlive", "command":"sudo apt update && sudo apt install -y kdenlive"}, -{"name":"卸载视频剪辑软件Kdenlive", "command":"sudo apt autoremove --purge -y kdenlive"}, -{"name":"安装科学计算软件Octave", "command":"sudo apt update && sudo apt install -y octave"}, -{"name":"卸载科学计算软件Octave", "command":"sudo apt autoremove --purge -y octave"}, -{"name":"安装WPS", "command":"wget https://wps-linux-personal.wpscdn.cn/wps/download/ep/Linux2019/11704/wps-office_11.1.0.11704_arm64.deb -O /tmp/wps.deb && sudo apt update && sudo apt install -y /tmp/wps.deb; rm /tmp/wps.deb"}, -{"name":"卸载WPS", "command":"sudo apt autoremove --purge -y wps-office"}, -{"name":"安装CAJViewer", "command":"wget https://download.cnki.net/net.cnki.cajviewer_1.3.20-1_arm64.deb -O /tmp/caj.deb && sudo apt update && sudo apt install -y /tmp/caj.deb && bash /home/tiny/.local/share/tiny/caj/postinst; rm /tmp/caj.deb"}, -{"name":"卸载CAJViewer", "command":"sudo apt autoremove --purge -y net.cnki.cajviewer && bash /home/tiny/.local/share/tiny/caj/postrm"}, -{"name":"安装亿图图示", "command":"wget https://www.edrawsoft.cn/2download/aarch64/edrawmax_11.5.6-3_arm64.deb -O /tmp/edraw.deb && sudo apt update && sudo apt install -y /tmp/edraw.deb && bash /home/tiny/.local/share/tiny/edraw/postinst; rm /tmp/edraw.deb"}, -{"name":"卸载亿图图示", "command":"sudo apt autoremove --purge -y edrawmax libldap-2.4-2"}, -{"name":"安装QQ", "command":"wget https://dldir1.qq.com/qqfile/qq/QQNT/b69de82d/linuxqq_3.2.1-17153_arm64.deb -O /tmp/qq.deb && sudo apt update && sudo apt install -y /tmp/qq.deb && sed -i 's#Exec=/opt/QQ/qq %U#Exec=/opt/QQ/qq --no-sandbox %U#g' /usr/share/applications/qq.desktop; rm /tmp/qq.deb"}, -{"name":"卸载QQ", "command":"sudo apt autoremove --purge -y linuxqq"}, -{"name":"修复无法编译C语言程序", "command":"sudo apt update && sudo apt reinstall -y libc6-dev"}, -{"name":"修复系统语言到中文", "command":"sudo localedef -c -i zh_CN -f UTF-8 zh_CN.UTF-8"}, -{"name":"启用回收站", "command":"sudo apt update && sudo apt install -y gvfs && echo '安装完成, 重启软件即可使用回收站。'"}, -{"name":"拉流测试", "command":"ffplay rtsp://127.0.0.1:8554/stream &"}, -{"name":"关机", "command":"stopvnc\\nexit\\nexit"}, -{"name":"???", "command":"timeout 8 cmatrix"}] +"commands":${jsonEncode(D.commands)} }"""]); // await G.prefs.setStringList("adsBonus", []); // await G.prefs.setInt("adsWatchedTotal", 0); diff --git a/pubspec.lock b/pubspec.lock index 5541128..8744d96 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -328,6 +328,15 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + saf: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: e3aa3fcbd031f2645688e97ebd5a4f14a014bd42 + url: "https://github.com/Cateners/saf" + source: git + version: "1.0.3+3" shared_preferences: dependency: "direct main" description: @@ -537,18 +546,18 @@ packages: dependency: "direct main" description: name: webview_flutter - sha256: "82f6787d5df55907aa01e49bd9644f4ed1cc82af7a8257dd9947815959d2e755" + sha256: c1ab9b81090705c6069197d9fdc1625e587b52b8d70cdde2339d177ad0dbb98e url: "https://pub.dev" source: hosted - version: "4.2.4" + version: "4.4.1" webview_flutter_android: dependency: transitive description: name: webview_flutter_android - sha256: ddc167c6676f57c8b367d19fcbee267d6dc6adf81bd6c3cb87981d30746e0a6d + sha256: b0cd33dd7d3dd8e5f664e11a19e17ba12c352647269921a3b568406b001f1dff url: "https://pub.dev" source: hosted - version: "3.10.1" + version: "3.12.0" webview_flutter_platform_interface: dependency: transitive description: @@ -561,10 +570,10 @@ packages: dependency: transitive description: name: webview_flutter_wkwebview - sha256: "485af05f2c5f83c7f78c20e236b170ad02df7153b299ae9917345be43871d29f" + sha256: "30b9af6bdd457b44c08748b9190d23208b5165357cc2eb57914fee1366c42974" url: "https://pub.dev" source: hosted - version: "3.8.0" + version: "3.9.1" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9def63a..c062bf0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,6 +43,8 @@ dependencies: unity_ads_plugin: ^0.3.8 clipboard: ^0.1.3 ffmpeg_kit_flutter_full_gpl: ^6.0.3 + saf: + git: https://github.com/Cateners/saf # The following adds the Cupertino Icons font to your application.