mirror of
https://github.com/Cateners/tiny_computer.git
synced 2026-05-21 00:45:49 +08:00
Adjust data structure and layout
There are a lot of changes compared to the previous version, many features are mixed together and not easy to commit individually. Added a new control panel Added advertisements Moved packets out of assets Adjusted the data structure, so that it will be easier to add multi-container functionality in the future.
This commit is contained in:
368
lib/main.dart
368
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<InfoPage> {
|
||||
final List<bool> _expandState = [true, false, false, false];
|
||||
final List<bool> _expandState = [false, false, false, false];
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ExpansionPanelList(
|
||||
@@ -106,44 +110,95 @@ class _InfoPageState extends State<InfoPage> {
|
||||
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<MyHomePage> {
|
||||
|
||||
//高级设置,全局设置
|
||||
final List<bool> _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<Widget>((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) {
|
||||
|
||||
@@ -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<void> 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<void> 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<String, dynamic> 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<void> applyBonus(Map<String, dynamic> bonus) async {
|
||||
bool flag = false;
|
||||
List<String> ret = G.prefs.getStringList("adsBonus")!.map((e) {
|
||||
Map<String, dynamic> 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<void> grantPermissions() async {
|
||||
Permission.storage.request();
|
||||
Permission.manageExternalStorage.request();
|
||||
}
|
||||
|
||||
static Future<void> initData() async {
|
||||
|
||||
G.prefs = await SharedPreferences.getInstance();
|
||||
|
||||
}
|
||||
|
||||
static Future<void> 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<List<int>>()
|
||||
.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<int, TermPty> termPtys; //为容器<int>存放TermPty数据
|
||||
static late AdManager ads;//广告实例
|
||||
|
||||
|
||||
//看广告可以获得的奖励。
|
||||
//weight抽奖权重,singleUse使用一次花费的数量,amount抽中可以获得的数量
|
||||
static const List<Map<String, dynamic>> bonusTable = [
|
||||
{"name": "开发者的祝福", "subtitle": "支持开发者的证明", "description": "(*'v'*)\n开发者由衷地感谢你!", "weight": 10000, "amount": 1, "singleUse": 0},
|
||||
{"name": "记忆晶片", "subtitle": "看上去像平行四边形", "description": "组成记忆空间的基本元素。\n是从哪里掉下来的呢?", "weight": 50, "amount": 1, "singleUse": 0},
|
||||
{"name": "Wishes Flower Part", "subtitle": "为1个人献上祝福", "description": "希望之花的花瓣。在想好为谁祝福后, 点击使用", "weight": 500, "amount": 1, "singleUse": 1},
|
||||
{"name": "Wishes Flower Part", "subtitle": "为1个人献上祝福", "description": "希望之花的花瓣。在想好为谁祝福后, 点击使用", "weight": 100, "amount": 3, "singleUse": 1},
|
||||
{"name": "Wishes Flower", "subtitle": "为3个人献上祝福", "description": "希望之花。在想好为谁祝福后, 点击使用", "weight": 50, "amount": 1, "singleUse": 1},
|
||||
{"name": "Bonus Reward", "subtitle": "会有极好的事情发生", "description": "来自记忆空间的传说。\n使用后一天内必有极好的事情...\n就是你想象的那种事情...\n就会发生。\n不过, 大概只是个传说吧。", "weight": 10, "amount": 0.01, "singleUse": 1},
|
||||
{"name": "Bonus Reward", "subtitle": "会有极好的事情发生", "description": "来自记忆空间的传说。\n使用后一天内必有极好的事情...\n就是你想象的那种事情...\n就会发生。\n不过, 大概只是个传说吧。", "weight": 1, "amount": 0.1, "singleUse": 1},
|
||||
{"name": "Bonus Reward", "subtitle": "会有极好的事情发生", "description": "来自记忆空间的传说。\n使用后一天内必有极好的事情...\n就是你想象的那种事情...\n就会发生。\n不过, 大概只是个传说吧。", "weight": 1, "amount": 1, "singleUse": 1},
|
||||
];
|
||||
|
||||
//所有key
|
||||
//int defaultContainer = 0: 默认启动第0个容器
|
||||
//String defaultAudioPort = 4713: 默认pulseaudio端口(为了避免和其它软件冲突改成4718了) !!!注意!这个值是String类型
|
||||
//bool autoLaunchVnc = true: 是否自动启动VNC并跳转
|
||||
//String lastDate: 上次启动软件的日期,yyyy-MM-dd
|
||||
//int adsWatchedToday: 今日视频广告观看数量
|
||||
//int adsWatchedTotal: 视频广告观看数量
|
||||
//bool isBannerAdsClosed = false
|
||||
//bool bannerAdsCanBeClosed = false 看一次视频广告永久开启,历史遗留
|
||||
//bool isTerminalWriteEnabled = false
|
||||
//bool terminalWriteCanBeEnabled = false 看一次视频广告永久开启,历史遗留
|
||||
//? int bootstrapVersion: 启动包版本
|
||||
//String[] containersInfo: 所有容器信息(json)
|
||||
//{name, boot:"\$DATA_DIR/bin/proot ...", vnc:"startnovnc", vncUrl:"...", commands:[{name:"更新和升级", command:"apt update -y && apt upgrade -y"}, ...]}
|
||||
//String[] adsBonus: 观看广告获取的奖励(json)
|
||||
//{name: "xxx", amount: xxx}
|
||||
static late SharedPreferences prefs;
|
||||
|
||||
}
|
||||
|
||||
class AdManager {
|
||||
|
||||
static Map<String, bool> placements = {
|
||||
interstitialVideoAdPlacementId: false,
|
||||
rewardedVideoAdPlacementId: false,
|
||||
};
|
||||
|
||||
static void loadAds() {
|
||||
for (var placementId in placements.keys) {
|
||||
loadAd(placementId);
|
||||
}
|
||||
}
|
||||
|
||||
static void loadAd(String placementId) {
|
||||
UnityAds.load(
|
||||
placementId: placementId,
|
||||
onComplete: (placementId) {
|
||||
print('Load Complete $placementId');
|
||||
placements[placementId] = true;
|
||||
},
|
||||
onFailed: (placementId, error, message) => print('Load Failed $placementId: $error $message'),
|
||||
);
|
||||
}
|
||||
|
||||
static void showAd(String placementId, Function completeExtra, Function full) {
|
||||
|
||||
if (G.prefs.getInt("adsWatchedToday")!>=5) {
|
||||
full();
|
||||
return;
|
||||
}
|
||||
|
||||
placements[placementId] = false;
|
||||
UnityAds.showVideoAd(
|
||||
placementId: placementId,
|
||||
onComplete: (placementId) async {
|
||||
print('Video Ad $placementId completed');
|
||||
loadAd(placementId);
|
||||
await G.prefs.setInt("adsWatchedTotal", G.prefs.getInt("adsWatchedTotal")!+1);
|
||||
await G.prefs.setInt("adsWatchedToday", G.prefs.getInt("adsWatchedToday")!+1);
|
||||
completeExtra();
|
||||
},
|
||||
onFailed: (placementId, error, message) {
|
||||
print('Video Ad $placementId failed: $error $message');
|
||||
loadAd(placementId);
|
||||
},
|
||||
onStart: (placementId) => print('Video Ad $placementId started'),
|
||||
onClick: (placementId) => print('Video Ad $placementId click'),
|
||||
onSkipped: (placementId) {
|
||||
print('Video Ad $placementId skipped');
|
||||
loadAd(placementId);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static String get gameId {
|
||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||
return '5403132';
|
||||
}
|
||||
if (defaultTargetPlatform == TargetPlatform.iOS) {
|
||||
return '5403133';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
static String bannerAdPlacementId = 'Banner_Android';
|
||||
|
||||
static String interstitialVideoAdPlacementId = 'Interstitial_Android';
|
||||
|
||||
static String rewardedVideoAdPlacementId = 'Rewarded_Android';
|
||||
}
|
||||
|
||||
class Workflow {
|
||||
|
||||
static Future<void> grantPermissions() async {
|
||||
Permission.storage.request();
|
||||
Permission.manageExternalStorage.request();
|
||||
}
|
||||
|
||||
static Future<void> setupBootstrap() async {
|
||||
//用来共享数据文件的文件夹
|
||||
Util.createDirFromString("${G.dataPath}/share");
|
||||
Util.createDirFromString("${G.dataPath}/debian");
|
||||
//挂载到/dev/shm的文件夹
|
||||
Util.createDirFromString("${G.dataPath}/tmp");
|
||||
//给proot的tmp文件夹,虽然我不知道为什么proot要这个
|
||||
Util.createDirFromString("${G.dataPath}/proot_tmp");
|
||||
//解压后得到bin文件夹和libexec文件夹
|
||||
//bin存放了proot和pulseaudio
|
||||
//libexec存放了proot loader
|
||||
await Util.copyAsset(
|
||||
"assets/assets.zip",
|
||||
"${G.dataPath}/assets.zip",
|
||||
);
|
||||
for (String name in ["xaa", "xab", "xac", "xad", "xae", "xaf", "xag", "xah", "xai", "xaj", "xak", "xal", "xam", "xan", "xao"]) {
|
||||
//for (String name in ["xaa"]) {
|
||||
await Util.copyAsset("assets/$name", "${G.dataPath}/$name");
|
||||
}
|
||||
//dddd
|
||||
await Util.copyAsset(
|
||||
"assets/busybox",
|
||||
"${G.dataPath}/busybox",
|
||||
);
|
||||
await Util.execute(
|
||||
"""
|
||||
cd ${G.dataPath}
|
||||
export DATA_DIR=${G.dataPath}
|
||||
cd \$DATA_DIR
|
||||
chmod +x busybox
|
||||
${G.dataPath}/busybox unzip assets.zip
|
||||
\$DATA_DIR/busybox unzip assets.zip
|
||||
chmod -R +x bin/*
|
||||
chmod -R +x libexec/proot/*
|
||||
cat xa* | ${G.dataPath}/busybox tar x -J -v -C debian
|
||||
${G.dataPath}/busybox rm -rf assets.zip xa*
|
||||
chmod 1777 tmp
|
||||
ln -s \$DATA_DIR/busybox \$DATA_DIR/bin/xz
|
||||
\$DATA_DIR/busybox rm -rf assets.zip
|
||||
""");
|
||||
}
|
||||
|
||||
static Future<void> launchDefaultContainer() async {
|
||||
//初次启动要做的事情
|
||||
static Future<void> initForFirstTime() async {
|
||||
//首先设置bootstrap
|
||||
await setupBootstrap();
|
||||
//存放容器的文件夹0和存放硬链接的文件夹.l2s
|
||||
Util.createDirFromString("${G.dataPath}/containers/0/.l2s");
|
||||
//这个是容器rootfs,被split命令分成了xa*
|
||||
//首次启动,就用这个,别让用户另选了
|
||||
//TODO: 这个字符串列表太丑陋了
|
||||
//for (String name in ["xaa", "xab", "xac", "xad", "xae", "xaf", "xag", "xah", "xai", "xaj", "xak", "xal", "xam", "xan"]) {
|
||||
for (String name in ["xaa", "xab", "xac", "xad", "xae", "xaf", "xag", "xah", "xai", "xaj", "xak", "xal", "xam"]) {
|
||||
await Util.copyAsset("assets/$name", "${G.dataPath}/$name");
|
||||
}
|
||||
//-J
|
||||
await Util.execute(
|
||||
"""
|
||||
export DATA_DIR=${G.dataPath}
|
||||
export CONTAINER_DIR=\$DATA_DIR/containers/0
|
||||
cd \$DATA_DIR
|
||||
export PATH=\$DATA_DIR/bin:\$PATH
|
||||
export PROOT_TMP_DIR=\$DATA_DIR/proot_tmp
|
||||
export PROOT_LOADER=\$DATA_DIR/libexec/proot/loader
|
||||
export PROOT_LOADER_32=\$DATA_DIR/libexec/proot/loader32
|
||||
export PROOT_L2S_DIR=\$CONTAINER_DIR/.l2s
|
||||
\$DATA_DIR/bin/proot --link2symlink -H sh -c "cat xa* | \$DATA_DIR/bin/tar x -J --delay-directory-restore --preserve-permissions -v -C containers/0"
|
||||
#Script from proot-distro
|
||||
chmod u+rw "\$CONTAINER_DIR/etc/passwd" "\$CONTAINER_DIR/etc/shadow" "\$CONTAINER_DIR/etc/group" "\$CONTAINER_DIR/etc/gshadow"
|
||||
echo "aid_\$(id -un):x:\$(id -u):\$(id -g):Termux:/:/sbin/nologin" >> "\$CONTAINER_DIR/etc/passwd"
|
||||
echo "aid_\$(id -un):*:18446:0:99999:7:::" >> "\$CONTAINER_DIR/etc/shadow"
|
||||
id -Gn | tr ' ' '\\n' > tmp1
|
||||
id -G | tr ' ' '\\n' > tmp2
|
||||
\$DATA_DIR/busybox paste tmp1 tmp2 > tmp3
|
||||
local group_name group_id
|
||||
cat tmp3 | while read -r group_name group_id; do
|
||||
echo "aid_\${group_name}:x:\${group_id}:root,aid_\$(id -un)" >> "\$CONTAINER_DIR/etc/group"
|
||||
if [ -f "\$CONTAINER_DIR/etc/gshadow" ]; then
|
||||
echo "aid_\${group_name}:*::root,aid_\$(id -un)" >> "\$CONTAINER_DIR/etc/gshadow"
|
||||
fi
|
||||
done
|
||||
\$DATA_DIR/busybox rm -rf xa* tmp1 tmp2 tmp3
|
||||
""");
|
||||
//一些数据初始化
|
||||
//$DATA_DIR是数据文件夹, $CONTAINER_DIR是容器根目录
|
||||
//容器根目录会有一个fake-proc文件夹存放一些假的proc文件供挂载
|
||||
//"boot":"\$DATA_DIR/bin/proot --link2symlink -H --kill-on-exit --tcsetsf2tcsetsw --root-id --pwd=/root --rootfs=\$CONTAINER_DIR -L --kernel-release=6.2.1-PRoot-Distro --bind=\$DATA_DIR/tmp:/dev/shm --bind=/sys --bind=/proc/self/fd/2:/dev/stderr --bind=/proc/self/fd/1:/dev/stdout --bind=/proc/self/fd/0:/dev/stdin --bind=/proc/self/fd:/dev/fd --bind=/proc --bind=/dev/urandom:/dev/random --bind=/dev --bind=\$CONTAINER_DIR/fake-proc/.loadavg:/proc/loadavg --bind=\$CONTAINER_DIR/fake-proc/.stat:/proc/stat --bind=\$CONTAINER_DIR/fake-proc/.uptime:/proc/uptime --bind=\$CONTAINER_DIR/fake-proc/.version:/proc/version --bind=\$CONTAINER_DIR/fake-proc/.vmstat:/proc/vmstat --bind=\$CONTAINER_DIR/fake-proc/.sysctl_entry_cap_last_cap:/proc/sys/kernel/cap_last_cap /usr/bin/env -i HOME=/root USER=root TERM=xterm-256color SDL_IM_MODULE=fcitx XMODIFIERS=\\\\@im=fcitx QT_IM_MODULE=fcitx GTK_IM_MODULE=fcitx TMPDIR=/tmp 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",
|
||||
await G.prefs.setStringList("containersInfo", ["""{
|
||||
"name":"Ubuntu Jammy",
|
||||
"boot":"\$DATA_DIR/bin/proot --link2symlink --ashmem-memfd --sysvipc -H --kill-on-exit --root-id --pwd=/root --rootfs=\$CONTAINER_DIR -L --kernel-release=6.2.1-PRoot-Distro --mount=\$DATA_DIR/share:/media/share --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:/media/storage/shared --mount=/storage/self/primary/Pictures:/media/storage/Pictures --mount=/storage/self/primary/Music:/media/storage/Music --mount=/storage/self/primary/Movies:/media/storage/Movies --mount=/storage/self/primary/Download:/media/storage/Download --mount=/storage/self/primary/DCIM:/media/storage/DCIM --mount=/storage/self/primary/Documents:/media/storage/Documents --bind=\$DATA_DIR/tmp:/dev/shm --bind=/sys --bind=/proc/self/fd/2:/dev/stderr --bind=/proc/self/fd/1:/dev/stdout --bind=/proc/self/fd/0:/dev/stdin --bind=/proc/self/fd:/dev/fd --bind=/proc --bind=/dev/urandom:/dev/random --bind=/dev --bind=\$CONTAINER_DIR/fake-proc/.loadavg:/proc/loadavg --bind=\$CONTAINER_DIR/fake-proc/.stat:/proc/stat --bind=\$CONTAINER_DIR/fake-proc/.uptime:/proc/uptime --bind=\$CONTAINER_DIR/fake-proc/.version:/proc/version --bind=\$CONTAINER_DIR/fake-proc/.vmstat:/proc/vmstat --bind=\$CONTAINER_DIR/fake-proc/.sysctl_entry_cap_last_cap:/proc/sys/kernel/cap_last_cap /usr/bin/env -i HOME=/root USER=root TERM=xterm-256color SDL_IM_MODULE=fcitx XMODIFIERS=\\\\@im=fcitx QT_IM_MODULE=fcitx GTK_IM_MODULE=fcitx TMPDIR=/tmp 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":"tigervncserver :4 -SecurityTypes none && .local/share/noVNC/utils/novnc_proxy --vnc localhost:5904 --listen localhost:36082 &",
|
||||
"vncUrl":"http://localhost:36082/vnc.html?host=localhost&port=36082&autoconnect=true&resize=remote",
|
||||
"commands":[{"name":"检查更新并升级", "command":"apt update && apt upgrade -y"},
|
||||
{"name":"查看系统信息", "command":"uname -a"},
|
||||
{"name":"清屏", "command":"clear"},
|
||||
{"name":"安装图形处理软件Krita", "command":"apt update && apt install -y krita krita-l10n"},
|
||||
{"name":"卸载图形处理软件Krita", "command":"apt autoremove --purge -y krita krita-l10n"},
|
||||
{"name":"安装视频剪辑软件Kdenlive", "command":"apt update && apt install -y kdenlive"},
|
||||
{"name":"卸载视频剪辑软件Kdenlive", "command":"apt autoremove --purge -y kdenlive"},
|
||||
{"name":"安装科学计算软件Octave", "command":"apt update && apt install -y octave"},
|
||||
{"name":"卸载科学计算软件Octave", "command":"apt autoremove --purge -y octave"},
|
||||
{"name":"???", "command":"timeout 8 /root/.local/bin/cmatrix"}]
|
||||
}"""]);
|
||||
await G.prefs.setStringList("adsBonus", []);
|
||||
await G.prefs.setInt("adsWatchedTotal", 0);
|
||||
//await G.prefs.setBool("terminalWriteCanBeEnabled", false);
|
||||
//G.prefs.setBool("isTerminalWriteEnabled", false);
|
||||
await G.prefs.setBool("isTerminalWriteEnabled", false);
|
||||
//await G.prefs.setBool("bannerAdsCanBeClosed", false);
|
||||
await G.prefs.setBool("isBannerAdsClosed", false);
|
||||
//G.prefs.setBool("autoLaunchVnc", true);
|
||||
await G.prefs.setBool("autoLaunchVnc", true);
|
||||
await G.prefs.setString("defaultAudioPort", "4718");
|
||||
await G.prefs.setInt("defaultContainer", 0);
|
||||
}
|
||||
|
||||
static Future<void> initData() async {
|
||||
|
||||
G.dataPath = (await getApplicationSupportDirectory()).path;
|
||||
|
||||
G.termPtys = {};
|
||||
|
||||
G.prefs = await SharedPreferences.getInstance();
|
||||
|
||||
//限制一天内观看视频广告不超过5次
|
||||
final String currentDate = DateFormat("yyyy-MM-dd").format(DateTime.now());
|
||||
if (currentDate != G.prefs.getString("lastDate")) {
|
||||
await G.prefs.setString("lastDate", currentDate);
|
||||
await G.prefs.setInt("adsWatchedToday", 0);
|
||||
}
|
||||
|
||||
//如果没有这个key,说明是初次启动
|
||||
if (!G.prefs.containsKey("defaultContainer")) {
|
||||
await initForFirstTime();
|
||||
}
|
||||
G.currentContainer = G.prefs.getInt("defaultContainer")!;
|
||||
|
||||
G.controller = WebViewController()..setJavaScriptMode(JavaScriptMode.unrestricted);
|
||||
|
||||
}
|
||||
|
||||
static Future<void> initTerminalForCurrent() async {
|
||||
if (!G.termPtys.containsKey(G.currentContainer)) {
|
||||
G.termPtys[G.currentContainer] = TermPty();
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> initAds() async {
|
||||
UnityAds.init(
|
||||
gameId: AdManager.gameId,
|
||||
testMode: true,
|
||||
onComplete: () {
|
||||
print('Initialization Complete');
|
||||
AdManager.loadAds();
|
||||
},
|
||||
onFailed: (error, message) => print('Initialization Failed: $error $message'),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> setupAudio() async {
|
||||
G.audioPty?.kill();
|
||||
G.audioPty = Pty.start(
|
||||
"/system/bin/sh"
|
||||
);
|
||||
//pulseaudio也需要一个tmp文件夹,这里选择前面的cache,没有什么特别的原因,不行再换
|
||||
//pulseaudio还需要一个文件夹放配置,这里用share
|
||||
G.audioPty!.write(const Utf8Encoder().convert("""
|
||||
export DATA_DIR=${G.dataPath}
|
||||
cd \$DATA_DIR/..
|
||||
export TMPDIR=\$PWD/cache
|
||||
cd \$DATA_DIR
|
||||
export HOME=\$DATA_DIR/share
|
||||
export LD_LIBRARY_PATH=\$DATA_DIR/bin
|
||||
\$DATA_DIR/busybox sed "s/4713/${G.prefs.getString("defaultAudioPort")!}/g" \$DATA_DIR/bin/pulseaudio.conf > \$DATA_DIR/bin/pulseaudio.conf.tmp
|
||||
\$DATA_DIR/bin/pulseaudio -F \$DATA_DIR/bin/pulseaudio.conf.tmp
|
||||
exit
|
||||
"""));
|
||||
await G.audioPty?.exitCode;
|
||||
}
|
||||
|
||||
static Future<void> launchCurrentContainer() async {
|
||||
Util.termWrite(
|
||||
"""
|
||||
cd ${G.dataPath}/..
|
||||
export TMPDIR=\$PWD/cache
|
||||
cd ${G.dataPath}
|
||||
export HOME=\$PWD/share
|
||||
export LD_LIBRARY_PATH=\$PWD/bin
|
||||
\$PWD/bin/pulseaudio -F \$PWD/bin/pulseaudio.conf >/dev/null 2>&1 &
|
||||
export PROOT_TMP_DIR=\$PWD/tmp
|
||||
export PROOT_LOADER=\$PWD/libexec/proot/loader
|
||||
export PROOT_LOADER_32=\$PWD/libexec/proot/loader32
|
||||
${G.dataPath}/bin/proot --mute-setxid --tcsetsf2tcsetsw --root-id --pwd=/root --rootfs=${G.dataPath}/debian --mount=/system --mount=/apex --kill-on-exit --mount=/storage:/storage --mount=${G.dataPath}/share:/media/share -L --link2symlink --mount=/proc:/proc --mount=/dev:/dev --mount=${G.dataPath}/debian/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/Fonts:/usr/share/fonts/wpsm --mount=/storage/self/primary/AppFiles/Fonts:/usr/share/fonts/yozom --mount=/storage/self/primary:/media/storage/shared --mount=/storage/self/primary/Pictures:/media/storage/Pictures --mount=/storage/self/primary/Music:/media/storage/Music --mount=/storage/self/primary/Movies:/media/storage/Movies --mount=/storage/self/primary/Download:/media/storage/Download --mount=/storage/self/primary/DCIM:/media/storage/DCIM --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/.tmoe-container.stat:/proc/stat --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/.tmoe-container.version:/proc/version --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/bus:/proc/bus --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/buddyinfo:/proc/buddyinfo --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/cgroups:/proc/cgroups --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/consoles:/proc/consoles --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/crypto:/proc/crypto --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/devices:/proc/devices --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/diskstats:/proc/diskstats --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/execdomains:/proc/execdomains --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/fb:/proc/fb --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/filesystems:/proc/filesystems --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/interrupts:/proc/interrupts --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/iomem:/proc/iomem --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/ioports:/proc/ioports --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/kallsyms:/proc/kallsyms --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/keys:/proc/keys --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/key-users:/proc/key-users --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/kpageflags:/proc/kpageflags --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/loadavg:/proc/loadavg --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/locks:/proc/locks --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/misc:/proc/misc --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/modules:/proc/modules --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/pagetypeinfo:/proc/pagetypeinfo --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/partitions:/proc/partitions --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/sched_debug:/proc/sched_debug --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/softirqs:/proc/softirqs --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/timer_list:/proc/timer_list --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/uptime:/proc/uptime --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/vmallocinfo:/proc/vmallocinfo --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/vmstat:/proc/vmstat --mount=${G.dataPath}/debian/usr/local/etc/tmoe-linux/proot_proc/zoneinfo:/proc/zoneinfo /usr/bin/env -i HOSTNAME=TINY HOME=/root USER=root 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 DISPLAY=:2 PULSE_SERVER=tcp:127.0.0.1:4713 LANG=zh_CN.UTF-8 SHELL=/bin/zsh PATH=/usr/local/sbin:/usr/local/bin:/bin:/usr/bin:/sbin:/usr/sbin:/usr/games:/usr/local/games /bin/zsh -l
|
||||
startnovnc""");
|
||||
export DATA_DIR=${G.dataPath}
|
||||
export CONTAINER_DIR=\$DATA_DIR/containers/${G.currentContainer}
|
||||
export PROOT_L2S_DIR=\$DATA_DIR/containers/0/.l2s
|
||||
cd \$DATA_DIR
|
||||
export PROOT_TMP_DIR=\$DATA_DIR/proot_tmp
|
||||
export PROOT_LOADER=\$DATA_DIR/libexec/proot/loader
|
||||
export PROOT_LOADER_32=\$DATA_DIR/libexec/proot/loader32
|
||||
${Util.getCurrentProp("boot")}
|
||||
${G.prefs.getBool("autoLaunchVnc")!?Util.getCurrentProp("vnc"):""}
|
||||
clear""");
|
||||
}
|
||||
|
||||
static Future<void> waitForConnection() async {
|
||||
// Future<bool> testConnection(String url) async {
|
||||
// try {
|
||||
// return (await http.get(Uri.parse(url))).statusCode == 200;
|
||||
// } catch (e) {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
// for (;;) {
|
||||
// await Future.delayed(const Duration(milliseconds: 1000), () async {
|
||||
// print("meow");
|
||||
// if (await testConnection(G.vncUrl)) {
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
await retry(
|
||||
// Make a GET request
|
||||
() => http.get(Uri.parse(G.vncUrl)).timeout(const Duration(milliseconds: 250)),
|
||||
() => http.get(Uri.parse(Util.getCurrentProp("vncUrl"))).timeout(const Duration(milliseconds: 250)),
|
||||
// Retry on SocketException or TimeoutException
|
||||
retryIf: (e) => e is SocketException || e is TimeoutException,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> launchBrowser() async {
|
||||
G.controller.loadRequest(Uri.parse(G.vncUrl));
|
||||
G.controller.loadRequest(Uri.parse(Util.getCurrentProp("vncUrl")));
|
||||
Navigator.push(G.homePageStateContext, MaterialPageRoute(builder: (context) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky,overlays: []);
|
||||
SystemChrome.setSystemUIChangeCallback((systemOverlaysAreVisible) async {
|
||||
@@ -244,12 +519,13 @@ startnovnc""");
|
||||
static Future<void> workflow() async {
|
||||
grantPermissions();
|
||||
await initData();
|
||||
await initTerminal();
|
||||
if (Util.isFirstTime()) {
|
||||
await setupBootstrap();
|
||||
await initAds();
|
||||
await initTerminalForCurrent();
|
||||
setupAudio();
|
||||
launchCurrentContainer();
|
||||
if (G.prefs.getBool("autoLaunchVnc")!) {
|
||||
waitForConnection().then((value) => launchBrowser());
|
||||
}
|
||||
launchDefaultContainer();
|
||||
waitForConnection().then((value) => launchBrowser());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user