diff --git a/.gitignore b/.gitignore
index 829fff8..07061bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,4 +44,5 @@ app.*.map.json
/android/app/release
/backup
-#assets/xa*
\ No newline at end of file
+
+assets/xa*
diff --git a/README.md b/README.md
index 4286fd4..f12ab9e 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,14 @@
# 小小电脑
-
+
-点开软件就是电脑
+即开即用的类PC环境,内置WPS、火狐浏览器、VS Code等常用软件
-Click-to-run debian 12 xfce on android for Chinese users, with fcitx pinyin input method and wps office preinstalled. No termux required.
+Click-to-run ubuntu jammy xfce on android for Chinese users, with fcitx pinyin input method and wps office preinstalled. No termux required.
## 原理
-使用proot运行debian环境
+使用proot运行ubuntu环境
内置[noVNC](https://github.com/novnc/noVNC)显示图形界面
@@ -17,49 +17,47 @@ Click-to-run debian 12 xfce on android for Chinese users, with fcitx pinyin inpu
只支持arm64安卓
-**目前新安装的软件无法读写文件,但可以访问手机存储,原因未知**
-
-(我接下来可能会排查一下是proot还是容器的问题
-顺便学习一下容器是怎么做的
-毕竟我的修改可能出了问题)
-
## 项目结构
assets的文件来源如下:
-- [build-proot-android, proot二进制文件](https://github.com/green-green-avk/build-proot-android)
+- [proot](https://github.com/termux/proot/), 使用[build-proot-android](https://github.com/green-green-avk/build-proot-android)脚本编译
- [busybox](https://github.com/meefik/busybox)
- [Xserver XSDL, pulseaudio相关文件](https://github.com/pelya/commandergenius/tree/sdl_android/project/jni/application/xserver)
-- [Tmoe Linux, debian包来源](https://github.com/2moe/tmoe)
+- [proot-distro, ubuntu包来源](https://github.com/termux/proot-distro)
-其中proot、busybox和pulseaudio相关文件都是直接用了二进制文件。
+其中busybox和pulseaudio相关文件都是直接用了二进制文件。
(pulseaudio我真的编译不来,如果你会的话请教教我吧)
-对debian容器进行了如下修改:
-- 使用tmoe工具安装了xfce环境和全套VNC;
+对ubuntu容器进行了如下修改:
+- 安装了xfce环境、tigerVNC、noVNC;
+- 使用kali-undercover提供的Win10主题美化xfce;
- 安装了wps office, 对wps office进行了如下修改:
- 界面改成了多组件,避免无法打开wps;
- - 根据[这篇文章](https://forums.debiancn.org/t/topic/4015/8)创建了libtiff软链,避免无法打开wpspdf
- 补上了缺失的字体;
- 安装了VS Code和中文插件;
- 安装了fcitx输入法和云拼音组件。按切换输入法。
- 强烈建议**不要**使用安卓中文输入法直接输入中文,而是使用英文键盘通过容器的输入法输入中文,避免丢字错字。
-- 对VNC启动脚本进行修改,删除了tigerVNC密码验证;
- - 虽然不太可能,但如果还是被问到密码的话输12345678
-- 对noVNC脚本(/usr/local/etc/tmoe-linux/novnc/core/rfb.js)进行修改,添加了userScale变量控制缩放
- - 默认显示太大了,很多窗口点开都超出了屏幕范围,目前我使显示缩小了userScale=1.5倍
-- 改掉了一些容器里的Termux硬链接,有一些.git文件夹里的没改,应该无伤大雅吧=v=
+- 对noVNC进行[修改](https://github.com/Cateners/noVNC) (scale_factor分支),添加了scale factor滑块控制缩放
+- 在主目录下提供storage文件夹,通过此文件夹可以方便地访问手机存储(如果提供了存储权限的话)
+- 启动时会尝试挂载手机的一些字体目录(AppFiles/Fonts、Fonts和/system/fonts), 如果这些目录下有字体文件的话会一并加载到系统中,无需额外安装
- 最后采用tar.xz压缩,用split命令分成了xa*等多个文件
+数据包不再在assets中更新,而是随releases提供,主要是为了避免git越来越大
+
lib目录:
-- main.dart文件,页面布局,目前只有一个页面,非常简单
+- main.dart文件,页面布局,老实说已经有点乱了
- workflow.dart文件,逻辑部分,目前也还算简单
- Util 工具类
- G 全局变量类
- Workflow 从软件点开到容器启动的所有步骤
+## 目前已知bug
+
+多用户/分身情形无法使用apt, 其它见issue
+
## 一些链接
这是我的第一个flutter软件,感谢这些项目为我指路
diff --git a/android/gradle.properties b/android/gradle.properties
index 710351d..e45b3ce 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -1,4 +1,4 @@
#org.gradle.jvmargs=-Xmx1536M
-org.gradle.jvmargs=-Xmx12288m
+org.gradle.jvmargs=-Xmx16384m
android.useAndroidX=true
android.enableJetifier=true
diff --git a/assets/assets.zip b/assets/assets.zip
index d6a1645..c198b22 100644
Binary files a/assets/assets.zip and b/assets/assets.zip differ
diff --git a/assets/xaa b/assets/xaa
deleted file mode 100644
index 75cc9fb..0000000
Binary files a/assets/xaa and /dev/null differ
diff --git a/assets/xab b/assets/xab
deleted file mode 100644
index 217b419..0000000
Binary files a/assets/xab and /dev/null differ
diff --git a/assets/xac b/assets/xac
deleted file mode 100644
index e5af911..0000000
Binary files a/assets/xac and /dev/null differ
diff --git a/assets/xad b/assets/xad
deleted file mode 100644
index 477a56e..0000000
Binary files a/assets/xad and /dev/null differ
diff --git a/assets/xae b/assets/xae
deleted file mode 100644
index 2a848da..0000000
Binary files a/assets/xae and /dev/null differ
diff --git a/assets/xaf b/assets/xaf
deleted file mode 100644
index 66c47b0..0000000
Binary files a/assets/xaf and /dev/null differ
diff --git a/assets/xag b/assets/xag
deleted file mode 100644
index 288f2a7..0000000
Binary files a/assets/xag and /dev/null differ
diff --git a/assets/xah b/assets/xah
deleted file mode 100644
index d876b22..0000000
Binary files a/assets/xah and /dev/null differ
diff --git a/assets/xai b/assets/xai
deleted file mode 100644
index a2bc6a0..0000000
Binary files a/assets/xai and /dev/null differ
diff --git a/assets/xaj b/assets/xaj
deleted file mode 100644
index 6c53cf6..0000000
Binary files a/assets/xaj and /dev/null differ
diff --git a/assets/xak b/assets/xak
deleted file mode 100644
index f908fed..0000000
Binary files a/assets/xak and /dev/null differ
diff --git a/assets/xal b/assets/xal
deleted file mode 100644
index 8074a2f..0000000
Binary files a/assets/xal and /dev/null differ
diff --git a/assets/xam b/assets/xam
deleted file mode 100644
index 05f56f9..0000000
Binary files a/assets/xam and /dev/null differ
diff --git a/assets/xan b/assets/xan
deleted file mode 100644
index 1f9a2a9..0000000
Binary files a/assets/xan and /dev/null differ
diff --git a/assets/xao b/assets/xao
deleted file mode 100644
index 771ad94..0000000
Binary files a/assets/xao and /dev/null differ
diff --git a/lib/main.dart b/lib/main.dart
index 9de6457..397d93d 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -17,7 +17,9 @@
import 'dart:async';
+import 'dart:convert';
import 'dart:math';
+//import 'dart:convert';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter/material.dart';
@@ -25,6 +27,8 @@ import 'package:xterm/xterm.dart';
//import 'package:xterm/flutter.dart';
import 'package:tiny_computer/workflow.dart';
+import 'package:unity_ads_plugin/unity_ads_plugin.dart';
+
void main() {
runApp(const MyApp());
}
@@ -89,7 +93,7 @@ class InfoPage extends StatefulWidget {
}
class _InfoPageState extends State {
- final List _expandState = [true, false, false, false];
+ final List _expandState = [false, false, false, false];
@override
Widget build(BuildContext context) {
return ExpansionPanelList(
@@ -106,44 +110,95 @@ class _InfoPageState extends State {
return const ListTile(title: Text("使用说明"));
},
body: const Padding(padding: EdgeInsets.all(8), child: Text("""
-Loading... Please wait about 5 to 10 minutes for first time
-第一次加载,大概需要几分钟...
+第一次加载, 大概需要5到10分钟...
请不要在安装时退出软件
-你可以在等待的同时阅读“隐私政策”和“服务条款”,
-阅读完后可以关闭广告
-...不过我还没写(广告也没放)
一些注意事项:
-此软件免费开源
-项目地址: https://github.com/Cateners/tiny_computer
-如果是买的就是被骗了, 请举报!
-((然后请我喝水!!!!!!)(不是))
+此软件以GPL协议免费开源
+如果是买的就是被骗了, 请举报
+源代码在这里: https://github.com/Cateners/tiny_computer
+软件也会第一时间在这里更新
+请尽可能在这里下载软件, 确保是正版
-如果遇到android 12的signal 9问题
-请自行查找教程修复
+常见问题:
+如果你的系统版本大于等于android 12
+可能会在使用过程中异常退出(返回错误码9)
+届时本软件会提供方案指引你修复
并不难
此软件因为没有权限
所以不能帮你修复
-一般只要你以前修复过(Tmoe脚本、Vmos助手、全手动adb等等)
-现在就不用再次修复
+
+如果你给了存储权限
+那么可以从主目录下的
+storage目录访问手机存储
+
+如果认为界面大小比例不合适
+可以通过调整左栏设置-高级设置里的scale
+快捷调整界面缩放
+这个功能是原本的noVNC里没有的哦!
+具体的改动可以在这里看到:
+https://github.com/Cateners/noVNC/tree/scale_factor
+
+其余两个选项是
+quality(图像质量)和compression(压缩等级)
+...是noVNC中本来就有的选项。
+如果感觉界面卡卡的
+可以适当调低
+
+如果你想安装其他软件
+可以在网上搜索
+"ubuntu安装xxx教程"
+"linux安装xxx教程"等等
+本软件也提供一些基本软件安装按钮
+包括图形处理, 视频剪辑, 科学计算相关的软件
+稍后你就会看到
+
+如果你想安装更多字体
+在给了存储权限的情况下
+直接将字体复制到手机存储的Fonts文件夹即可
+一些常用的办公字体
+可以在Windows电脑的C:\\Windows\\Fonts文件夹找到
+由于可能的版权问题
+软件不能帮你做
+
+关于中文输入的问题
+强烈建议不要使用安卓中文输入法直接输入中文
+而是使用英文键盘通过容器的输入法(Ctrl+空格切换)输入中文
+避免丢字错字
+
+在之前的版本中有网友反馈过这些问题
+还请注意:
+三星Galaxy S21 Ultra, 安卓13, 黑屏
+红米Note 12, 安卓13(miui14), 黑屏
+红米Note 11T Pro+, miui13.0.4,“无法连接”
+Vivo Pad,安卓13,看不见鼠标移动
+关于这个
+我目前没有什么好的解决办法
+(毕竟我没有这些设备
+也不方便定位原因)
+如果你遇到了类似问题
+不管解没解决
+都可以去https://github.com/Cateners/tiny_computer/issues/1留个言
+
+感谢使用!
+
+项目原理:
+项目采用proot运行ubuntu虚拟容器系统
+图形界面是经过kali-undercover提供的Win10主题美化的xfce
+系统预装了WPS, VSCode、火狐浏览器和fcitx输入法
这个项目没有使用Termux
因为我不太喜欢Termux的路径硬编码
路径硬编码会导致软件在多用户/分身等场景无法使用
当然这样一来就用不了Termux的软件生态了
-项目采用proot运行tmoe的debian12(xfce)
-debian系统里预装了WPS, VSCode和fcitx输入法
-界面是webview+noVNC
-
-目前不能安装其他软件
-安装的其他软件无法读写容器
-但可以访问手机存储
-我也不太清楚原因
-
-如果你给了存储权限
-那么可以从storage目录访问手机目录
-所以任何时候都不要尝试rm -rf /*
+...如果你不知道什么是Termux
+那也无所谓
+即使完全不懂原理也不影响使用本软件
+但假如有一天你有了其他高级需求
+比如想换系统、换架构等等
+那么请学习并使用Termux
+届时本软件的使命已经达成...
(顺带一提, 全部解压完大概需要7GB空间
解压途中占用空间可能达到9GB
@@ -170,7 +225,26 @@ debian系统里预装了WPS, VSCode和fcitx输入法
return const ListTile(title: Text("支持作者"));
}), body: Column(
children: [
- const Padding(padding: EdgeInsets.all(8), child: Text("请我喝一杯水吧")),
+ const Padding(padding: EdgeInsets.all(8), child: Text("""
+这个软件预计会有一些广告
+之前的版本中说过
+如果完整地看了"隐私政策"与"服务条款"的话
+就可以选择关闭广告
+但因为那两个玩意一直都不知道怎么写
+想想还是算了
+但软件里的广告还是可以关闭的
+
+本软件的广告分为横幅广告和视频广告
+横幅广告在终端和控制页面的顶端出现
+只需完整观看一次视频广告即可永久关闭
+视频广告目前只在"关闭横幅广告"和"启用终端"两个功能中出现
+看一个视频即可永久启用对应功能
+我认为还是比较良心的...吧?
+
+总之为了良好的体验
+在图形界面是不会出现广告的
+这点还请放心
+""")),
const FractionallySizedBox(
widthFactor: 0.8,
child: Image(image: AssetImage("images/alipay.png"))
@@ -237,32 +311,254 @@ class MyHomePage extends StatefulWidget {
class _MyHomePageState extends State {
+ //高级设置,全局设置
+ final List _expandState = [false, false, false];
+
+ bool bannerAdsFailedToLoad = false;
+
//安装完成了吗?
//完成后从加载界面切换到主界面
bool isLoadingComplete = false;
//主界面索引
int pageIndex = 0;
+ final ButtonStyle buttonStyle = OutlinedButton.styleFrom(
+ tapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ minimumSize: const Size(0, 0), padding: const EdgeInsets.fromLTRB(4, 2, 4, 2)
+ );
+
@override
Widget build(BuildContext context) {
- G.homePageStateContext = context;
+ G.homePageStateContext = context;
- if (!isLoadingComplete) {
- Workflow.workflow().then((value) {
- setState(() {
- isLoadingComplete = true;
+ if (!isLoadingComplete) {
+ Workflow.workflow().then((value) {
+ setState(() {
+ isLoadingComplete = true;
+ });
});
- });
- }
+ }
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
- title: Text(widget.title),
+ title: Text(isLoadingComplete?Util.getCurrentProp("name"):widget.title),
),
- body: isLoadingComplete?TerminalView(G.terminal):const LoadingPage(),
+ body: isLoadingComplete?Column(mainAxisSize: MainAxisSize.min, children: [
+ G.prefs.getBool("isBannerAdsClosed")!||bannerAdsFailedToLoad?SizedBox.fromSize(size: const Size.square(0),):UnityBannerAd(
+ placementId: AdManager.bannerAdPlacementId,
+ onLoad: (placementId) => print('Banner loaded: $placementId'),
+ onClick: (placementId) => print('Banner clicked: $placementId'),
+ onFailed: (placementId, error, message) {
+ print('Banner Ad $placementId failed: $error $message');
+ setState(() {
+ bannerAdsFailedToLoad = true;
+ });
+ },
+ ),Expanded(flex: 1, child: AnimatedSwitcher(
+ duration: const Duration(milliseconds: 256),
+ child: [TerminalView(G.termPtys[G.currentContainer]!.terminal), Padding(
+ padding: const EdgeInsets.all(8),
+ child: Scrollbar(child: SingleChildScrollView(child: Column(
+ children: [
+ const Padding(
+ padding: EdgeInsets.all(16),
+ child: FractionallySizedBox(
+ widthFactor: 0.4,
+ child: Image(
+ image: AssetImage("images/icon.png")
+ )
+ ),
+ ),
+ /*Padding(
+ padding: const EdgeInsets.fromLTRB(0, 0, 0, 8),
+ child: Text(Util.getCurrentProp("name"), textScaleFactor: 2),
+ ),*/
+ Wrap(alignment: WrapAlignment.center, spacing: 4.0, runSpacing: 4.0, children: Util.getCurrentProp("commands")
+ .asMap().entries.map((e) {
+ return OutlinedButton(style: buttonStyle, child: Text(e.value["name"]!), onPressed: () {
+ setState(() {
+ Util.termWrite(e.value["command"]!);
+ pageIndex = 0;
+ });
+ }, onLongPress: () {
+ String name = e.value["name"]!;
+ String command = e.value["command"]!;
+ showDialog(context: context, builder: (context) {
+ return AlertDialog(title: const Text("指令编辑"), content: SingleChildScrollView(child: Column(children: [
+ TextFormField(initialValue: name, decoration: const InputDecoration(border: OutlineInputBorder(), labelText: "指令名称"), onChanged: (value) {
+ name = value;
+ }),
+ SizedBox.fromSize(size: const Size.square(8)),
+ TextFormField(maxLines: null, initialValue: command, decoration: const InputDecoration(border: OutlineInputBorder(), labelText: "指令内容"), onChanged: (value) {
+ command = value;
+ }),
+ ])), actions: [
+ TextButton(onPressed:() async {
+ await Util.setCurrentProp("commands", Util.getCurrentProp("commands")
+ ..removeAt(e.key));
+ setState(() {});
+ if (!context.mounted) return;
+ Navigator.of(context).pop();
+ }, child: const Text("删除该项")),
+ TextButton(onPressed:() {
+ Navigator.of(context).pop();
+ }, child: const Text("取消")),
+ TextButton(onPressed:() async {
+ await Util.setCurrentProp("commands", Util.getCurrentProp("commands")
+ ..setAll(e.key, [{"name": name, "command": command}]));
+ setState(() {});
+ if (!context.mounted) return;
+ Navigator.of(context).pop();
+ }, child: const Text("保存")),
+ ]);
+ },);
+ },);
+ }).toList()..add(OutlinedButton(style: buttonStyle, onPressed:() {
+ String name = "";
+ String command = "";
+ showDialog(context: context, builder: (context) {
+ return AlertDialog(title: const Text("指令编辑"), content: SingleChildScrollView(child: Column(children: [
+ TextFormField(initialValue: name, decoration: const InputDecoration(border: OutlineInputBorder(), labelText: "指令名称"), onChanged: (value) {
+ name = value;
+ }),
+ SizedBox.fromSize(size: const Size.square(8)),
+ TextFormField(maxLines: null, initialValue: command, decoration: const InputDecoration(border: OutlineInputBorder(), labelText: "指令内容"), onChanged: (value) {
+ command = value;
+ }),
+ ])), actions: [
+ TextButton(onPressed:() {
+ Navigator.of(context).pop();
+ }, child: const Text("取消")),
+ TextButton(onPressed:() async {
+ await Util.setCurrentProp("commands", Util.getCurrentProp("commands")
+ ..add({"name": name, "command": command}));
+ 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: [
+ ExpansionPanelList(
+ elevation: 1,
+ expandedHeaderPadding: const EdgeInsets.all(0),
+ expansionCallback: (panelIndex, isExpanded) {
+ setState(() {
+ _expandState[panelIndex] = isExpanded;
+ });
+ },children: [
+ ExpansionPanel(
+ isExpanded: _expandState[0],
+ headerBuilder: ((context, isExpanded) {
+ return const ListTile(title: Text("高级设置"), subtitle: Text("修改后重启生效"));
+ }), body: Padding(padding: const EdgeInsets.all(12), child: Column(children: [
+ TextFormField(maxLines: null, initialValue: Util.getCurrentProp("name"), decoration: const InputDecoration(border: OutlineInputBorder(), labelText: "容器名称"), onChanged: (value) async {
+ await Util.setCurrentProp("name", value);
+ setState(() {});
+ }),
+ SizedBox.fromSize(size: const Size.square(8)),
+ TextFormField(maxLines: null, initialValue: Util.getCurrentProp("boot"), decoration: const InputDecoration(border: OutlineInputBorder(), labelText: "启动命令"), onChanged: (value) async {
+ await Util.setCurrentProp("boot", value);
+ }),
+ SizedBox.fromSize(size: const Size.square(8)),
+ TextFormField(maxLines: null, initialValue: Util.getCurrentProp("vnc"), decoration: const InputDecoration(border: OutlineInputBorder(), labelText: "vnc启动命令"), onChanged: (value) async {
+ await Util.setCurrentProp("vnc", value);
+ }),
+ SizedBox.fromSize(size: const Size.square(8)),
+ TextFormField(maxLines: null, initialValue: Util.getCurrentProp("vncUrl"), decoration: const InputDecoration(border: OutlineInputBorder(), labelText: "网页跳转地址"), onChanged: (value) async {
+ await Util.setCurrentProp("vncUrl", value);
+ }),
+ ],))),
+ ExpansionPanel(
+ isExpanded: _expandState[1],
+ headerBuilder: ((context, isExpanded) {
+ return const ListTile(title: Text("全局设置"), subtitle: Text("在这里关广告、开启终端编辑"));
+ }), body: Padding(padding: const EdgeInsets.all(12), child: Column(children: [
+ TextFormField(maxLines: null, initialValue: G.prefs.getString("defaultAudioPort"), decoration: const InputDecoration(border: OutlineInputBorder(), labelText: "pulseaudio接收端口"), onChanged: (value) async {
+ await G.prefs.setString("defaultAudioPort", value);
+ }),
+ SizedBox.fromSize(size: const Size.square(8)),
+ SwitchListTile(title: const Text("关闭横幅广告"), value: G.prefs.getBool("isBannerAdsClosed")!, onChanged:(value) {
+ if (value && (G.prefs.getInt("adsWatchedTotal")! == 0)) {
+ ScaffoldMessenger.of(context).hideCurrentSnackBar();
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text("观看一个视频广告解锁><"))
+ );
+ return;
+ }
+ G.prefs.setBool("isBannerAdsClosed", value);
+ setState(() {});
+ },),
+ SizedBox.fromSize(size: const Size.square(8)),
+ SwitchListTile(title: const Text("启用终端"), value: G.prefs.getBool("isTerminalWriteEnabled")!, onChanged:(value) {
+ if (value && (G.prefs.getInt("adsWatchedTotal")! == 0)) {
+ ScaffoldMessenger.of(context).hideCurrentSnackBar();
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: const Text("观看一个视频广告解锁><"), action: SnackBarAction(label: "啊?", onPressed: () {
+ G.prefs.setBool("isTerminalWriteEnabled", value);
+ setState(() {});
+ },))
+ );
+ return;
+ }
+ G.prefs.setBool("isTerminalWriteEnabled", value);
+ setState(() {});
+ },),
+ SizedBox.fromSize(size: const Size.square(8)),
+ SwitchListTile(title: const Text("开启时启动图形界面"), value: G.prefs.getBool("autoLaunchVnc")!, onChanged:(value) {
+ G.prefs.setBool("autoLaunchVnc", value);
+ setState(() {});
+ },),
+ ],))),
+ ExpansionPanel(
+ isExpanded: _expandState[2],
+ headerBuilder: ((context, isExpanded) {
+ return const ListTile(title: Text("广告记录"));
+ }), body: Padding(padding: const EdgeInsets.all(12), child: Column(children: [
+ OutlinedButton(child: const Text("看一个广告"), onPressed: () {
+ if (AdManager.placements[AdManager.rewardedVideoAdPlacementId]!) {
+ AdManager.showAd(AdManager.rewardedVideoAdPlacementId, () {
+ final bonus = Util.getRandomBonus();
+ Util.applyBonus(bonus);
+ ScaffoldMessenger.of(context).hideCurrentSnackBar();
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text("你获得了 ${bonus["name"]}*${bonus["amount"]}"))
+ );
+ setState(() {
+
+ });
+ }, () {
+ ScaffoldMessenger.of(context).hideCurrentSnackBar();
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text("已经看5个广告了, 今天也非常感谢><"))
+ );
+ });
+ }
+ }),
+ const SizedBox.square(dimension: 8),
+ Text(G.prefs.getStringList("adsBonus")!.map((element) {
+ final e = jsonDecode(element);
+ return e["amount"]==0?"":"${e["name"]}*${e["amount"]}\n";
+ }).join())
+ ],))),
+ ],),
+ SizedBox.fromSize(size: const Size.square(8)),
+ const InfoPage()
+ ]
+ )
+ ))
+ ,)
+ ]
+ )))
+ )][pageIndex],
+ transitionBuilder: (child, animation) {
+ return FadeTransition(opacity: animation, child: child);
+ },))]):const LoadingPage(),
bottomNavigationBar: Visibility(visible: isLoadingComplete,
child: BottomNavigationBar(currentIndex: pageIndex,
onTap: (index) {
diff --git a/lib/workflow.dart b/lib/workflow.dart
index 0eca640..cd5da18 100644
--- a/lib/workflow.dart
+++ b/lib/workflow.dart
@@ -18,6 +18,9 @@
import 'dart:io';
import 'dart:async';
import 'dart:convert';
+import 'dart:math';
+
+import 'package:intl/intl.dart';
import 'package:http/http.dart' as http;
import 'package:retry/retry.dart';
@@ -35,10 +38,9 @@ import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
+import 'package:unity_ads_plugin/unity_ads_plugin.dart';
+
class Util {
- static bool isFirstTime() {
- return (! Directory("${G.dataPath}/bin").existsSync()) || File("${G.dataPath}/xao").existsSync();
- }
static Future copyAsset(String src, String dst) async {
await File(dst).writeAsBytes((await rootBundle.load(src)).buffer.asUint8List());
@@ -60,58 +62,81 @@ class Util {
}
static void termWrite(String str) {
- G.pty.write(const Utf8Encoder().convert("$str\n"));
+ G.termPtys[G.currentContainer]!.pty.write(const Utf8Encoder().convert("$str\n"));
+ }
+
+ static dynamic getCurrentProp(String key) {
+ return jsonDecode(G.prefs.getStringList("containersInfo")![G.currentContainer])[key];
+ }
+
+ //用来设置name, boot, vnc, vncUrl等
+ static Future setCurrentProp(String key, dynamic value) async {
+ await G.prefs.setStringList("containersInfo",
+ G.prefs.getStringList("containersInfo")!..setAll(G.currentContainer,
+ [jsonEncode((jsonDecode(
+ G.prefs.getStringList("containersInfo")![G.currentContainer]
+ ))..update(key, (v) => value))]
+ )
+ );
+ }
+
+ //返回单个G.bonusTable定义的item
+ static Map getRandomBonus() {
+ final random = Random();
+ final totalWeight = G.bonusTable.fold(0.0, (sum, item) => sum + item['weight']);
+ final randomIndex = random.nextDouble() * totalWeight;
+ var cumulativeWeight = 0.0;
+ for (final item in G.bonusTable) {
+ cumulativeWeight += item['weight'];
+ if (randomIndex <= cumulativeWeight) {
+ return item;
+ }
+ }
+ return G.bonusTable[0];
+ }
+
+ //由getRandomBonus返回的数据
+ static Future applyBonus(Map bonus) async {
+ bool flag = false;
+ List ret = G.prefs.getStringList("adsBonus")!.map((e) {
+ Map item = jsonDecode(e);
+ return (item["name"] == bonus["name"])?
+ jsonEncode(item..update("amount", (v) {
+ flag = true;
+ return v + bonus["amount"];
+ })):e;
+ }).toList();
+ if (!flag) {
+ ret.add("""{"name": "${bonus["name"]}", "amount": ${bonus["amount"]}}""");
+ }
+ await G.prefs.setStringList("adsBonus", ret);
+ print(G.prefs.getStringList("adsBonus")!);
}
}
-// Global variables
-class G {
- static late final String dataPath;
- static late Terminal terminal;
- static late Pty pty;
- static late WebViewController controller;
- static late BuildContext homePageStateContext;
+//一个结合terminal和pty的类
+class TermPty {
+ late final Terminal terminal;
+ late final Pty pty;
- static late SharedPreferences prefs;
-
-
- static const String vncUrl = "http://localhost:36080/vnc.html?host=localhost&port=36080&autoconnect=true&resize=remote";
-}
-
-class Workflow {
-
- static Future grantPermissions() async {
- Permission.storage.request();
- Permission.manageExternalStorage.request();
- }
-
- static Future initData() async {
-
- G.prefs = await SharedPreferences.getInstance();
-
- }
-
- static Future initTerminal() async {
-
- G.dataPath = (await getApplicationSupportDirectory()).path;
-
- G.controller = WebViewController()..setJavaScriptMode(JavaScriptMode.unrestricted);
-
- G.terminal = Terminal();
-
- G.pty = Pty.start(
+ TermPty() {
+ terminal = Terminal();
+ pty = Pty.start(
"/system/bin/sh",
workingDirectory: G.dataPath,
- columns: G.terminal.viewWidth,
- rows: G.terminal.viewHeight,
+ columns: terminal.viewWidth,
+ rows: terminal.viewHeight,
);
- G.pty.output
+ pty.output
.cast>()
.transform(const Utf8Decoder())
- .listen(G.terminal.write);
- G.pty.exitCode.then((code) {
- G.terminal.write('the process exited with exit code $code');
- //TO_DO: Singal 9 hint
+ .listen(terminal.write);
+ pty.exitCode.then((code) {
+ terminal.write('the process exited with exit code $code');
+ if (code == 0) {
+ SystemChannels.platform.invokeMethod("SystemNavigator.pop");
+ }
+ //TODO: Singal 9 hint, 改成对话框
if (code == -9) {
Navigator.push(G.homePageStateContext, MaterialPageRoute(builder: (context) {
const TextStyle ts = TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.normal);
@@ -131,87 +156,337 @@ class Workflow {
}));
}
});
- G.terminal.onOutput = (data) {
- G.pty.write(const Utf8Encoder().convert(data));
+ terminal.onOutput = (data) {
+ if (G.prefs.getBool("isTerminalWriteEnabled")!) {
+ pty.write(const Utf8Encoder().convert(data));
+ }
};
- G.terminal.onResize = (w, h, pw, ph) {
- G.pty.resize(h, w);
+ terminal.onResize = (w, h, pw, ph) {
+ pty.resize(h, w);
};
-
}
+}
+
+// Global variables
+class G {
+ static late final String dataPath;
+ static Pty? audioPty;
+ static late WebViewController controller;
+ static late BuildContext homePageStateContext;
+ static late int currentContainer; //目前运行第几个容器
+ static late Map termPtys; //为容器存放TermPty数据
+ static late AdManager ads;//广告实例
+
+
+ //看广告可以获得的奖励。
+ //weight抽奖权重,singleUse使用一次花费的数量,amount抽中可以获得的数量
+ static const List