mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-09-03 06:24:12 +08:00
feat: mobile to native
This commit is contained in:
@@ -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 凭据**不得写入磁盘或日志**。
|
||||
|
||||
@@ -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 不销售数字内容,也不提供购买链接,只展示用户服务端返回的授权状态。
|
||||
```
|
||||
|
||||
## 验收清单
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}',
|
||||
|
||||
@@ -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}',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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', () {
|
||||
|
||||
@@ -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', () {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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', () {
|
||||
|
||||
@@ -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', () {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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', () {
|
||||
|
||||
@@ -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', () {
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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', () {
|
||||
|
||||
@@ -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>[];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = ''}) => {
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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": [
|
||||
|
||||
Reference in New Issue
Block a user