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:
Caten
2023-09-12 15:07:53 +08:00
parent 76420edd8a
commit 95d6f662de
25 changed files with 830 additions and 156 deletions

View File

@@ -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, 安卓13miui14, 黑屏
红米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) {