feat: mobile to native

This commit is contained in:
chaos-zhu
2026-06-06 23:52:13 +08:00
parent d02e10c9a3
commit 9cd393b99c
35 changed files with 98 additions and 98 deletions

View File

@@ -1,6 +1,6 @@
# EasyNode Mobile
# EasyNode Native
EasyNode 的 Flutter 移动端 App复用现有后端 (`/api/v1`)在手机上提供服务器列表、SSH 终端、SFTP 文件管理、脚本库、账户安全等能力。App 自身不打包后端地址,登录时由用户填写。
EasyNode 的 Flutter Native App复用现有后端 (`/api/v1`)在手机上提供服务器列表、SSH 终端、SFTP 文件管理、脚本库、账户安全等能力。App 自身不打包后端地址,登录时由用户填写。
## 技术栈
@@ -14,7 +14,7 @@ EasyNode 的 Flutter 移动端 App复用现有后端 (`/api/v1`),在手机
## 目录结构
```
mobile/
native/
├── lib/
│ ├── main.dart # 入口,调用 EasyNodeApp.bootstrap()
│ ├── app.dart # 启动装配、ProviderScope override、登录态路由
@@ -67,9 +67,9 @@ mobile/
### 加密协议
- 登录密码:`core/crypto/rsa_crypto.dart#encryptPassword` → PKCS1 + utf8对应服务端 `node-rsa.decrypt(ct, 'utf8')`
- 移动端 SSH 临时密钥32 字节 AES key → base64 → utf8 → RSA对应 `RSADecryptAsync` + `Buffer.from(text, 'base64')`
- Native 端 SSH 临时密钥32 字节 AES key → base64 → utf8 → RSA对应 `RSADecryptAsync` + `Buffer.from(text, 'base64')`
- `/mobile/ssh-connection` 返回的 `{ iv, tag, ciphertext }``core/crypto/aes_gcm_crypto.dart` AES-GCM 解密。
- **修改加密协议必须 server / mobile 同步升级**,否则破坏现有 App 兼容性。
- **修改加密协议必须 server / native 同步升级**,否则破坏现有 App 兼容性。
### 登录流程
@@ -104,7 +104,7 @@ mobile/
## 本地开发
所有命令在 `mobile/` 目录下执行。
所有命令在 `native/` 目录下执行。
```bash
flutter pub get # 拉依赖
@@ -126,9 +126,9 @@ flutter test # 单元测试(仅在显式需要时执行)
-alias easynode -keyalg RSA -keysize 2048 -validity 10000
```
把生成的 `easynode-release.jks` 放到 `mobile/android/` 下。
把生成的 `easynode-release.jks` 放到 `native/android/` 下。
2. **创建 `mobile/android/key.properties`**
2. **创建 `native/android/key.properties`**
参考 `key.properties.example`
@@ -143,7 +143,7 @@ flutter test # 单元测试(仅在显式需要时执行)
3. **更新版本号**
编辑 `mobile/pubspec.yaml` 顶部的 `version: x.y.z+build``+` 之前是 `versionName`,之后是 `versionCode`。
编辑 `native/pubspec.yaml` 顶部的 `version: x.y.z+build``+` 之前是 `versionName`,之后是 `versionCode`。
4. **构建产物**
@@ -157,8 +157,8 @@ flutter test # 单元测试(仅在显式需要时执行)
产物位置:
- APK: `mobile/build/app/outputs/flutter-apk/`
- AAB: `mobile/build/app/outputs/bundle/release/`
- APK: `native/build/app/outputs/flutter-apk/`
- AAB: `native/build/app/outputs/bundle/release/`
### iOS
@@ -167,13 +167,13 @@ iOS 构建需要 macOS + Xcode。
1. **首次准备**
```bash
cd mobile/ios
cd native/ios
pod install
```
2. **在 Xcode 配置签名**
用 Xcode 打开 `mobile/ios/Runner.xcworkspace`,在 `Runner` → `Signing & Capabilities` 配置 Team / Bundle Identifier / Provisioning Profile。
用 Xcode 打开 `native/ios/Runner.xcworkspace`,在 `Runner` → `Signing & Capabilities` 配置 Team / Bundle Identifier / Provisioning Profile。
3. **构建归档**
@@ -183,7 +183,7 @@ iOS 构建需要 macOS + Xcode。
或在 Xcode 中选择 `Product → Archive`,再通过 Organizer 上传到 App Store Connect / 导出 Ad-hoc IPA。
产物位置:`mobile/build/ios/archive/` 与 `mobile/build/ios/ipa/`。
产物位置:`native/build/ios/archive/` 与 `native/build/ios/ipa/`。
### 发布前自检
@@ -195,6 +195,6 @@ iOS 构建需要 macOS + Xcode。
## 后端约定
- 移动端复用 `/api/v1` 全部接口,鉴权与 Web 端一致:`token` header + `session` cookie。
- 专属端点:`POST /api/v1/mobile/ssh-connection`,返回 AES-GCM 加密后的 SSH 连接参数。修改时同步更新 `server/app/controller/mobile.js` 与 `mobile/lib/features/servers/server_repository.dart`、`mobile/lib/core/crypto/aes_gcm_crypto.dart`。
- Native 端复用 `/api/v1` 全部接口,鉴权与 Web 端一致:`token` header + `session` cookie。
- 专属端点:`POST /api/v1/mobile/ssh-connection`,返回 AES-GCM 加密后的 SSH 连接参数。修改时同步更新 `server/app/controller/mobile.js` 与 `native/lib/features/servers/server_repository.dart`、`native/lib/core/crypto/aes_gcm_crypto.dart`。
- 解密后的 SSH 凭据**不得写入磁盘或日志**。

View File

@@ -2,17 +2,17 @@
## 目标
让 EasyNode Mobile 在 iOS 上架时保持清晰定位:
让 EasyNode Native 在 iOS 上架时保持清晰定位:
> App 不销售数字内容,只访问用户自托管 EasyNode 服务端上的既有 Plus 能力。Plus 授权由用户连接的服务端管理,Mobile 端只展示服务端返回的授权状态,不主动提供购买、获取 key 或外部购买引导。
> App 不销售数字内容,只访问用户自托管 EasyNode 服务端上的既有 Plus 能力。Plus 授权由用户连接的服务端管理,Native 端只展示服务端返回的授权状态,不主动提供购买、获取 key 或外部购买引导。
## 背景
当前 Plus 功能授权在服务端完成:
- Mobile 登录用户自己的 EasyNode 服务端。
- Mobile 调用服务端 `/plus-info` 查看 Plus 运行时状态。
- Mobile 调用服务端 `/plus-conf` 提交 Plus Key。
- Native 登录用户自己的 EasyNode 服务端。
- Native 调用服务端 `/plus-info` 查看 Plus 运行时状态。
- Native 调用服务端 `/plus-conf` 提交 Plus Key。
- EasyNode 服务端再向授权服务器激活 key并在内存中保存解密 key。
因此iOS App 不应表现为「在 App 内销售或引导购买 Plus」。否则容易被 Apple 认为是在绕过 IAP。
@@ -33,7 +33,7 @@
Android / Web 是否保留购买入口可单独决定。为了最大程度降低审核风险,本清单只要求 iOS 隐藏购买引导。
## 需要修改的 mobile 文件
## 需要修改的 native 文件
### 1. `lib/features/settings/plus_subscription_page.dart`
@@ -155,7 +155,7 @@ if (widget.onDiscountTap != null &&
如果需要新增状态说明:
```dart
'plus.iosManagedByServer': 'Plus 授权由当前连接的服务端管理,Mobile 端仅展示服务端返回的授权状态。',
'plus.iosManagedByServer': 'Plus 授权由当前连接的服务端管理,Native 端仅展示服务端返回的授权状态。',
```
### 3. `lib/l10n/strings_en.dart`
@@ -189,7 +189,7 @@ if (widget.onDiscountTap != null &&
如需新增:
```dart
'plus.iosManagedByServer': 'Plus authorization is managed by the connected server. The mobile app only displays the server-reported authorization status.',
'plus.iosManagedByServer': 'Plus authorization is managed by the connected server. The native app only displays the server-reported authorization status.',
```
### 4. `lib/features/settings/models/plus_info.dart`
@@ -329,13 +329,13 @@ final String error;
提交审核时,在 App Review Notes 中说明:
```text
EasyNode Mobile is a client app for connecting to a user self-hosted EasyNode server. Plus capabilities are authorized and managed on the connected server. The iOS app does not sell digital content or provide purchase links; it only displays the authorization status reported by the user's server.
EasyNode Native is a client app for connecting to a user self-hosted EasyNode server. Plus capabilities are authorized and managed on the connected server. The iOS app does not sell digital content or provide purchase links; it only displays the authorization status reported by the user's server.
```
中文含义:
```text
EasyNode Mobile 是连接用户自托管 EasyNode 服务端的客户端。Plus 能力在用户连接的服务端上授权和管理。iOS App 不销售数字内容,也不提供购买链接,只展示用户服务端返回的授权状态。
EasyNode Native 是连接用户自托管 EasyNode 服务端的客户端。Plus 能力在用户连接的服务端上授权和管理。iOS App 不销售数字内容,也不提供购买链接,只展示用户服务端返回的授权状态。
```
## 验收清单

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
/// Shared color tokens for the mobile UI. Matches the warm cream/amber palette
/// defined in the Pencil design (`mobile/design/mobile.pen`).
/// Shared color tokens for the native UI. Matches the warm cream/amber palette
/// defined in the Pencil design (`native/design/native.pen`).
abstract final class AppPalette {
static const canvas = Color(0xFFF7EFE0);
static const card = Color(0xFFFBF5E6);

View File

@@ -38,7 +38,7 @@ class LoginResult {
/// a stub client so the controller can be exercised without real network.
typedef ApiClientFactory = ApiClient Function(String serverAddress, {String? token});
/// Orchestrates the mobile login flow.
/// Orchestrates the native client login flow.
class LoginController {
LoginController({
required ApiClientFactory apiClientFactory,

View File

@@ -1,6 +1,6 @@
/// Server `ssh.js` returns credential records with secrets stripped:
/// `{ id, name, authType, privateKey:'', password:'', openSSHKeyPassword:'', date }`.
/// For mobile we only display the metadata; secrets are write-only via the
/// For the native client we only display the metadata; secrets are write-only via the
/// add/update flow.
class ServerCredentialModel {
const ServerCredentialModel({

View File

@@ -1,6 +1,6 @@
/// Mobile-side projection of `/api/v1/host-list` items.
/// Native-side projection of `/api/v1/host-list` items.
///
/// Only fields needed for the mobile list and connect action are kept;
/// Only fields needed for the native list and connect action are kept;
/// passwords, private keys, etc. are intentionally excluded — the server
/// also clears them in its host-list response.
class ServerModel {

View File

@@ -30,7 +30,7 @@ class LoginSession {
final int createAt;
final int expireAt;
bool get isMobile =>
bool get isNativeClient =>
browser.contains('EasyNode-Mobile') || os.contains('EasyNode-Mobile');
String get location {

View File

@@ -545,7 +545,7 @@ class _SessionCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context);
final mobile = session.isMobile;
final nativeClient = session.isNativeClient;
final bg = isCurrent ? AppPalette.accentSoft : AppPalette.card;
final borderColor = isCurrent ? AppPalette.primary : AppPalette.border;
final borderWidth = isCurrent ? 2.0 : 1.0;
@@ -568,7 +568,7 @@ class _SessionCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
mobile
nativeClient
? Icons.smartphone_rounded
: Icons.monitor_rounded,
size: 18,
@@ -612,7 +612,7 @@ class _SessionCard extends StatelessWidget {
Row(
children: [
Icon(
mobile
nativeClient
? Icons.smartphone_outlined
: Icons.monitor_outlined,
size: 14,
@@ -668,7 +668,7 @@ class _SessionCard extends StatelessWidget {
if (isCurrent) return l.tr('sessions.current');
if (session.revoked) return l.tr('sessions.revoked');
if (_expired) return l.tr('terminal.status.disconnected');
return l.tr('sessions.mobile');
return l.tr('sessions.native');
}
_StatusTone _statusTone() {

View File

@@ -143,7 +143,7 @@ class SshTransportFactory {
}
throw SshTransportException(
'Unsupported mobile proxy type: ${proxy.type}',
'Unsupported native proxy type: ${proxy.type}',
);
}
@@ -211,7 +211,7 @@ class SshTransportFactory {
}
throw SshTransportException(
'Unsupported mobile proxy type: ${config.proxyType}',
'Unsupported native proxy type: ${config.proxyType}',
);
}
}

View File

@@ -3,7 +3,7 @@ import 'package:flutter/widgets.dart';
import 'strings_en.dart';
import 'strings_zh.dart';
/// Lightweight hand-rolled localization layer for the mobile app.
/// Lightweight hand-rolled localization layer for the native app.
///
/// Why not the generated `flutter gen-l10n` flow? Adding the ARB pipeline and
/// keeping it in sync with the Dart codebase is more ceremony than this app

View File

@@ -4,7 +4,7 @@
const Map<String, String> stringsEn = {
// App
'app.title': 'EasyNode',
'app.subtitle': 'Mobile terminal access',
'app.subtitle': 'Native terminal access',
// Common
'common.cancel': 'Cancel',
@@ -91,7 +91,7 @@ const Map<String, String> stringsEn = {
'servers.submit': 'Add / Update',
'servers.secretEditHint': 'Leave blank to keep the existing saved secret.',
'servers.credentialUnsupported':
'Credential selection is reserved for a later mobile credentials screen.',
'Credential selection is reserved for a later native credentials screen.',
'servers.tagsHint': 'Separate tags with commas',
'servers.field.group': 'Group',
'servers.field.name': 'Name',
@@ -333,7 +333,7 @@ const Map<String, String> stringsEn = {
'All login records older than 7 days will be deleted.',
'sessions.purgeDone': 'Old records cleared',
'sessions.current': 'Current session',
'sessions.mobile': 'Mobile',
'sessions.native': 'Native',
'sessions.revoked': 'Revoked',
'sessions.revoke': 'Revoke',
'sessions.revokeConfirmTitle': 'Revoke session?',
@@ -453,7 +453,7 @@ const Map<String, String> stringsEn = {
'terminal.status.disconnected': 'Disconnected',
'terminal.status.error': 'Error',
// Editor (mobile SFTP text file)
// Editor (native SFTP text file)
'editor.tooLarge': 'File exceeds 2 MB. Download to edit.',
'editor.binary': 'Binary file is not editable.',
'editor.readFailed': 'Read failed: {0}',

View File

@@ -315,7 +315,7 @@ const Map<String, String> stringsZh = {
'sessions.purgeConfirmBody': '将删除 7 天前的所有登录记录,确定继续?',
'sessions.purgeDone': '已清理过期记录',
'sessions.current': '当前会话',
'sessions.mobile': '移动',
'sessions.native': '本地',
'sessions.revoked': '已撤销',
'sessions.revoke': '撤销',
'sessions.revokeConfirmTitle': '撤销会话?',
@@ -429,7 +429,7 @@ const Map<String, String> stringsZh = {
'terminal.status.disconnected': '已断开',
'terminal.status.error': '错误',
// Editor (mobile SFTP 文本文件)
// Editor (native SFTP 文本文件)
'editor.tooLarge': '文件超过 2 MB请下载后再编辑',
'editor.binary': '二进制文件不支持编辑',
'editor.readFailed': '读取失败:{0}',

View File

@@ -1,5 +1,5 @@
name: mobile
description: "EasyNode mobile app."
name: easynode_native
description: "EasyNode native app."
publish_to: 'none'
version: 0.1.0-beta.1+2

View File

@@ -2,7 +2,7 @@ import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile/core/crypto/aes_gcm_crypto.dart';
import 'package:easynode_native/core/crypto/aes_gcm_crypto.dart';
import 'package:pointycastle/export.dart';
Uint8List _encryptForTest(Uint8List key, Uint8List iv, Uint8List plaintext) {

View File

@@ -1,5 +1,5 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile/core/storage/device_id.dart';
import 'package:easynode_native/core/storage/device_id.dart';
class _FakeStore implements DeviceIdStore {
String? _value;

View File

@@ -1,5 +1,5 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile/core/utils/jwt_expiry.dart';
import 'package:easynode_native/core/utils/jwt_expiry.dart';
void main() {
test('maps temporary expiry to one hour', () {

View File

@@ -1,5 +1,5 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile/core/utils/validators.dart';
import 'package:easynode_native/core/utils/validators.dart';
void main() {
test('normalizes server address by trimming and removing trailing slash', () {

View File

@@ -1,5 +1,5 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile/features/auth/login_controller.dart';
import 'package:easynode_native/features/auth/login_controller.dart';
void main() {
test('blocks http login until user confirms risk', () async {

View File

@@ -1,10 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile/features/auth/auth_session.dart';
import 'package:mobile/features/auth/login_controller.dart';
import 'package:mobile/features/auth/login_page.dart';
import 'package:mobile/l10n/app_localizations.dart';
import 'package:easynode_native/features/auth/auth_session.dart';
import 'package:easynode_native/features/auth/login_controller.dart';
import 'package:easynode_native/features/auth/login_page.dart';
import 'package:easynode_native/l10n/app_localizations.dart';
void main() {
Widget wrap(Widget child) => MaterialApp(

View File

@@ -2,15 +2,15 @@ import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile/features/servers/server_form_data.dart';
import 'package:mobile/features/servers/server_form_page.dart';
import 'package:mobile/features/servers/server_group_model.dart';
import 'package:mobile/features/servers/server_model.dart';
import 'package:mobile/features/servers/server_repository.dart';
import 'package:mobile/features/shell/sftp_session_manager.dart';
import 'package:mobile/features/terminal/ssh_connection_config.dart';
import 'package:mobile/l10n/app_localizations.dart';
import 'package:mobile/state/api_providers.dart';
import 'package:easynode_native/features/servers/server_form_data.dart';
import 'package:easynode_native/features/servers/server_form_page.dart';
import 'package:easynode_native/features/servers/server_group_model.dart';
import 'package:easynode_native/features/servers/server_model.dart';
import 'package:easynode_native/features/servers/server_repository.dart';
import 'package:easynode_native/features/shell/sftp_session_manager.dart';
import 'package:easynode_native/features/terminal/ssh_connection_config.dart';
import 'package:easynode_native/l10n/app_localizations.dart';
import 'package:easynode_native/state/api_providers.dart';
class _FakeRepository implements ServerRepository {
int createCalls = 0;

View File

@@ -1,5 +1,5 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile/features/servers/server_group_model.dart';
import 'package:easynode_native/features/servers/server_group_model.dart';
void main() {
test('parses group entry into ServerGroupModel', () {

View File

@@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile/features/servers/server_form_data.dart';
import 'package:mobile/features/servers/server_model.dart';
import 'package:easynode_native/features/servers/server_form_data.dart';
import 'package:easynode_native/features/servers/server_model.dart';
void main() {
test('new server form defaults to password auth and no proxy', () {

View File

@@ -2,20 +2,20 @@ import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile/core/api/api_result.dart';
import 'package:mobile/features/servers/server_form_data.dart';
import 'package:mobile/features/servers/server_model.dart';
import 'package:mobile/features/servers/server_group_model.dart';
import 'package:mobile/features/servers/server_repository.dart';
import 'package:mobile/features/shell/sftp_session_manager.dart';
import 'package:mobile/features/servers/servers_tab.dart';
import 'package:mobile/features/terminal/ssh_connection_config.dart';
import 'package:mobile/l10n/app_localizations.dart';
import 'package:mobile/state/auth_notifier.dart';
import 'package:mobile/state/auth_state.dart';
import 'package:mobile/state/api_providers.dart';
import 'package:mobile/state/host_list_notifier.dart';
import 'package:mobile/state/group_list_notifier.dart';
import 'package:easynode_native/core/api/api_result.dart';
import 'package:easynode_native/features/servers/server_form_data.dart';
import 'package:easynode_native/features/servers/server_model.dart';
import 'package:easynode_native/features/servers/server_group_model.dart';
import 'package:easynode_native/features/servers/server_repository.dart';
import 'package:easynode_native/features/shell/sftp_session_manager.dart';
import 'package:easynode_native/features/servers/servers_tab.dart';
import 'package:easynode_native/features/terminal/ssh_connection_config.dart';
import 'package:easynode_native/l10n/app_localizations.dart';
import 'package:easynode_native/state/auth_notifier.dart';
import 'package:easynode_native/state/auth_state.dart';
import 'package:easynode_native/state/api_providers.dart';
import 'package:easynode_native/state/host_list_notifier.dart';
import 'package:easynode_native/state/group_list_notifier.dart';
class _FakeRepository implements ServerRepository {
_FakeRepository({

View File

@@ -1,5 +1,5 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile/features/shell/editor/editor_language.dart';
import 'package:easynode_native/features/shell/editor/editor_language.dart';
void main() {
group('detectFromFileName', () {

View File

@@ -2,7 +2,7 @@ import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile/features/shell/editor/editor_text_sniffer.dart';
import 'package:easynode_native/features/shell/editor/editor_text_sniffer.dart';
void main() {
group('sniffAndDecode', () {

View File

@@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile/features/shell/editor/editor_language.dart';
import 'package:mobile/features/shell/editor/text_editor_controller.dart';
import 'package:easynode_native/features/shell/editor/editor_language.dart';
import 'package:easynode_native/features/shell/editor/text_editor_controller.dart';
class _FakeWriter implements TextEditorWriter {
_FakeWriter({this.shouldThrow = false});

View File

@@ -1,5 +1,5 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile/features/shell/sftp_session_manager.dart';
import 'package:easynode_native/features/shell/sftp_session_manager.dart';
void main() {
group('Sftp text exceptions', () {

View File

@@ -3,7 +3,7 @@ import 'dart:convert';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile/features/terminal/http_proxy_connector.dart';
import 'package:easynode_native/features/terminal/http_proxy_connector.dart';
Future<String> readHttpHeaders(StreamIterator<List<int>> iterator) async {
final bytes = <int>[];

View File

@@ -2,7 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile/features/terminal/socks5_connector.dart';
import 'package:easynode_native/features/terminal/socks5_connector.dart';
Future<List<int>> readExactlyFromIterator(
StreamIterator<List<int>> iterator,

View File

@@ -1,5 +1,5 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile/features/terminal/ssh_connection_config.dart';
import 'package:easynode_native/features/terminal/ssh_connection_config.dart';
void main() {
Map<String, dynamic> basePayload({String passphrase = ''}) => {

View File

@@ -3,9 +3,9 @@ import 'dart:typed_data';
import 'package:dartssh2/dartssh2.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile/features/terminal/http_proxy_connector.dart';
import 'package:mobile/features/terminal/ssh_connection_config.dart';
import 'package:mobile/features/terminal/ssh_transport.dart';
import 'package:easynode_native/features/terminal/http_proxy_connector.dart';
import 'package:easynode_native/features/terminal/ssh_connection_config.dart';
import 'package:easynode_native/features/terminal/ssh_transport.dart';
void main() {
const directConfig = SshConnectionConfig(
@@ -63,7 +63,7 @@ void main() {
isA<SshTransportException>().having(
(error) => error.message,
'message',
'Unsupported mobile proxy type: http',
'Unsupported native proxy type: http',
),
),
);

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile/features/terminal/terminal_toolbar.dart';
import 'package:easynode_native/features/terminal/terminal_toolbar.dart';
void main() {
Widget wrap(Widget child) => MaterialApp(

View File

@@ -29,7 +29,7 @@
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>mobile</title>
<title>EasyNode Native</title>
<link rel="manifest" href="manifest.json">
</head>
<body>

View File

@@ -5,7 +5,7 @@
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "EasyNode mobile app.",
"description": "EasyNode native app.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [