Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion clients/app/lib/core/i18n.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ const Map<String, Map<AppLang, String>> _dict = {
'settings.nav.aiModel': {AppLang.zh: 'AI 模型', AppLang.en: 'AI Model'},
'settings.nav.common': {AppLang.zh: '通用', AppLang.en: 'Common'},
'settings.nav.terminal': {AppLang.zh: '终端', AppLang.en: 'Terminal'},
'settings.nav.theme': {AppLang.zh: '终端主题', AppLang.en: 'Terminal Theme'},
'settings.nav.theme': {AppLang.zh: '外观', AppLang.en: 'Appearance'},
'settings.theme.scheme': {AppLang.zh: '配色方案', AppLang.en: 'Color Scheme'},
'settings.theme.hint': {
AppLang.zh: '选择应用的整体配色,立即生效。',
AppLang.en: 'Pick the app color scheme. Applies instantly.'
},
'settings.nav.security': {AppLang.zh: '安全', AppLang.en: 'Security'},
'settings.nav.shortcuts': {AppLang.zh: '快捷键', AppLang.en: 'Shortcuts'},

// 通用页
Expand Down Expand Up @@ -76,6 +82,7 @@ const Map<String, Map<AppLang, String>> _dict = {
'top.connect': {AppLang.zh: '连接', AppLang.en: 'Connect'},
'top.newHost': {AppLang.zh: '新建主机', AppLang.en: 'New Host'},
'top.split': {AppLang.zh: '分屏', AppLang.en: 'Split'},
'top.resetLayout': {AppLang.zh: '重置布局', AppLang.en: 'Reset Layout'},

// ========== 面板标题 ==========
'panel.hosts': {AppLang.zh: '主机', AppLang.en: 'Hosts'},
Expand Down
80 changes: 80 additions & 0 deletions clients/app/lib/core/lock_store.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/// 本地主密码锁 —— 启动时校验,保护本地主机簿/密钥不被随手打开。
///
/// 安全设计:
/// - 不存明文,不存可逆密文,只存「加盐多轮 SHA-256」校验哈希。
/// - 随机盐(16B)抗彩虹表;多轮迭代(stretch)抬高暴力成本。
/// - 存 ~/.lowenssh/lock.json,权限 600。
/// - 未设置主密码时无锁,直接进应用(不强制)。
library;

import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:pointycastle/export.dart';

const int _saltLen = 16;
const int _rounds = 100000; // 迭代轮数,抬高暴力破解成本

String get _lockFile =>
'${Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'] ?? '.'}/.lowenssh/lock.json';

/// 加盐多轮 SHA-256:hash = SHA256^rounds(salt + pwd)
String _hash(String pwd, Uint8List salt) {
final digest = SHA256Digest();
var cur = Uint8List.fromList([...salt, ...utf8.encode(pwd)]);
for (var i = 0; i < _rounds; i++) {
cur = digest.process(cur);
}
return base64.encode(cur);
}

Uint8List _randomSalt() {
final rnd = Random.secure();
final b = Uint8List(_saltLen);
for (var i = 0; i < _saltLen; i++) {
b[i] = rnd.nextInt(256);
}
return b;
}

/// 是否已设置主密码
bool hasMasterPassword() => File(_lockFile).existsSync();

/// 设置/修改主密码(覆盖写)
void setMasterPassword(String pwd) {
final salt = _randomSalt();
final data = {
'salt': base64.encode(salt),
'hash': _hash(pwd, salt),
'rounds': _rounds,
};
final dir = Directory(
'${Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'] ?? '.'}/.lowenssh');
if (!dir.existsSync()) dir.createSync(recursive: true);
final f = File(_lockFile);
f.writeAsStringSync(const JsonEncoder.withIndent(' ').convert(data));
// 权限 600(仅本人可读写)
try {
Process.runSync('chmod', ['600', _lockFile]);
} catch (_) {/* Windows 无 chmod,忽略 */}
}

/// 取消主密码(删除锁文件)
void clearMasterPassword() {
final f = File(_lockFile);
if (f.existsSync()) f.deleteSync();
}

/// 校验主密码是否正确
bool verifyMasterPassword(String pwd) {
final f = File(_lockFile);
if (!f.existsSync()) return true; // 没设锁视为通过
try {
final j = jsonDecode(f.readAsStringSync()) as Map<String, dynamic>;
final salt = base64.decode(j['salt'] as String);
return _hash(pwd, Uint8List.fromList(salt)) == j['hash'] as String;
} catch (_) {
return false;
}
}
225 changes: 225 additions & 0 deletions clients/app/lib/core/palette.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import 'package:flutter/material.dart';

/// 一套配色方案的数据载体。字段与 theme.dart 的 AppColors 一一对应。
/// 新增配色只需 new 一个 AppPalette 实例,不必改调用点。
class AppPalette {
final String id; // 持久化用的稳定标识
final String nameZh; // 显示名(中)
final String nameEn; // 显示名(英)
final Brightness brightness; // 亮/暗,给 Flutter ThemeData 用

final Color base; // 主背景
final Color mantle; // 次背景(侧栏)
final Color crust; // 最深(终端/状态栏)
final Color surface0; // 边框/分隔
final Color surface1; // 高亮边框
final Color surface2;
final Color text; // 主文字
final Color subtext; // 次文字
final Color overlay; // 暗淡文字
final Color blue;
final Color lavender;
final Color sapphire;
final Color green;
final Color yellow;
final Color peach;
final Color red;
final Color mauve;
final Color teal;
final Color pink;

const AppPalette({
required this.id,
required this.nameZh,
required this.nameEn,
required this.brightness,
required this.base,
required this.mantle,
required this.crust,
required this.surface0,
required this.surface1,
required this.surface2,
required this.text,
required this.subtext,
required this.overlay,
required this.blue,
required this.lavender,
required this.sapphire,
required this.green,
required this.yellow,
required this.peach,
required this.red,
required this.mauve,
required this.teal,
required this.pink,
});
}

/// 内置配色方案。均来自 Catppuccin 官方色板,结构一致只是色号不同。
/// Mocha = 当前默认(暗);Macchiato/Frappe 是偏暖的暗色;Latte 是亮色。
class Palettes {
/// Catppuccin Mocha(深暗,原默认)
static const mocha = AppPalette(
id: 'mocha',
nameZh: 'Mocha 深暗', nameEn: 'Mocha Dark',
brightness: Brightness.dark,
base: Color(0xFF1E1E2E),
mantle: Color(0xFF181825),
crust: Color(0xFF11111B),
surface0: Color(0xFF313244),
surface1: Color(0xFF45475A),
surface2: Color(0xFF585B70),
text: Color(0xFFCDD6F4),
subtext: Color(0xFFA6ADC8),
overlay: Color(0xFF6C7086),
blue: Color(0xFF89B4FA),
lavender: Color(0xFFB4BEFE),
sapphire: Color(0xFF74C7EC),
green: Color(0xFFA6E3A1),
yellow: Color(0xFFF9E2AF),
peach: Color(0xFFFAB387),
red: Color(0xFFF38BA8),
mauve: Color(0xFFCBA6F7),
teal: Color(0xFF94E2D5),
pink: Color(0xFFF5C2E7),
);

/// Catppuccin Macchiato(暗,比 Mocha 略亮偏暖)
static const macchiato = AppPalette(
id: 'macchiato',
nameZh: 'Macchiato 暖暗', nameEn: 'Macchiato Warm',
brightness: Brightness.dark,
base: Color(0xFF24273A),
mantle: Color(0xFF1E2030),
crust: Color(0xFF181926),
surface0: Color(0xFF363A4F),
surface1: Color(0xFF494D64),
surface2: Color(0xFF5B6078),
text: Color(0xFFCAD3F5),
subtext: Color(0xFFA5ADCB),
overlay: Color(0xFF6E738D),
blue: Color(0xFF8AADF4),
lavender: Color(0xFFB7BDF8),
sapphire: Color(0xFF7DC4E4),
green: Color(0xFFA6DA95),
yellow: Color(0xFFEED49F),
peach: Color(0xFFF5A97F),
red: Color(0xFFED8796),
mauve: Color(0xFFC6A0F6),
teal: Color(0xFF8BD5CA),
pink: Color(0xFFF5BDE6),
);

/// Catppuccin Frappé(暗,更柔和的中间调)
static const frappe = AppPalette(
id: 'frappe',
nameZh: 'Frappé 柔暗', nameEn: 'Frappé Soft',
brightness: Brightness.dark,
base: Color(0xFF303446),
mantle: Color(0xFF292C3C),
crust: Color(0xFF232634),
surface0: Color(0xFF414559),
surface1: Color(0xFF51576D),
surface2: Color(0xFF626880),
text: Color(0xFFC6D0F5),
subtext: Color(0xFFA5ADCE),
overlay: Color(0xFF737994),
blue: Color(0xFF8CAAEE),
lavender: Color(0xFFBABBF1),
sapphire: Color(0xFF85C1DC),
green: Color(0xFFA6D189),
yellow: Color(0xFFE5C890),
peach: Color(0xFFEF9F76),
red: Color(0xFFE78284),
mauve: Color(0xFFCA9EE6),
teal: Color(0xFF81C8BE),
pink: Color(0xFFF4B8E4),
);

/// Catppuccin Latte(亮色)
static const latte = AppPalette(
id: 'latte',
nameZh: 'Latte 亮色', nameEn: 'Latte Light',
brightness: Brightness.light,
base: Color(0xFFEFF1F5),
mantle: Color(0xFFE6E9EF),
crust: Color(0xFFDCE0E8),
surface0: Color(0xFFCCD0DA),
surface1: Color(0xFFBCC0CC),
surface2: Color(0xFFACB0BE),
text: Color(0xFF4C4F69),
subtext: Color(0xFF5C5F77),
overlay: Color(0xFF8C8FA1),
blue: Color(0xFF1E66F5),
lavender: Color(0xFF7287FD),
sapphire: Color(0xFF209FB5),
green: Color(0xFF40A02B),
yellow: Color(0xFFDF8E1D),
peach: Color(0xFFFE640B),
red: Color(0xFFD20F39),
mauve: Color(0xFF8839EF),
teal: Color(0xFF179299),
pink: Color(0xFFEA76CB),
);

/// 霓虹夜(Tokyo Night 风,高饱和彩色,深蓝底 + 鲜亮强调色)
static const neon = AppPalette(
id: 'neon',
nameZh: '霓虹夜',
nameEn: 'Neon Night',
brightness: Brightness.dark,
base: Color(0xFF1A1B26),
mantle: Color(0xFF16161E),
crust: Color(0xFF0F0F17),
surface0: Color(0xFF2A2E45),
surface1: Color(0xFF3B4261),
surface2: Color(0xFF545C7E),
text: Color(0xFFC0CAF5),
subtext: Color(0xFF9AA5CE),
overlay: Color(0xFF565F89),
blue: Color(0xFF7AA2F7),
lavender: Color(0xFFBB9AF7),
sapphire: Color(0xFF2AC3DE),
green: Color(0xFF9ECE6A),
yellow: Color(0xFFE0AF68),
peach: Color(0xFFFF9E64),
red: Color(0xFFF7768E),
mauve: Color(0xFFBB9AF7),
teal: Color(0xFF73DACA),
pink: Color(0xFFFF75A0),
);

/// 樱桃亮(高对比亮色,白底 + 鲜艳强调色,彩色边框感强)
static const cherry = AppPalette(
id: 'cherry',
nameZh: '樱桃亮',
nameEn: 'Cherry Light',
brightness: Brightness.light,
base: Color(0xFFFFFFFF),
mantle: Color(0xFFF6F2F8),
crust: Color(0xFFEDE7F0),
surface0: Color(0xFFE0D4E7),
surface1: Color(0xFFCBB8D6),
surface2: Color(0xFFB199C2),
text: Color(0xFF2D1B33),
subtext: Color(0xFF5C476B),
overlay: Color(0xFF8A7397),
blue: Color(0xFF2563EB),
lavender: Color(0xFF7C3AED),
sapphire: Color(0xFF0891B2),
green: Color(0xFF16A34A),
yellow: Color(0xFFCA8A04),
peach: Color(0xFFEA580C),
red: Color(0xFFDC2626),
mauve: Color(0xFF9333EA),
teal: Color(0xFF0D9488),
pink: Color(0xFFDB2777),
);

/// 全部内置方案,按显示顺序
static const all = [mocha, macchiato, frappe, latte, neon, cherry];

/// 按 id 取,找不到回退 Mocha
static AppPalette byId(String? id) =>
all.firstWhere((p) => p.id == id, orElse: () => mocha);
}
8 changes: 8 additions & 0 deletions clients/app/lib/core/settings_store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ enum CursorStyle { block, underline, bar }
class AppSettings {
final AppLang lang;

// 外观
final String themeId; // 配色方案 id(见 palette.dart)

// 终端设置(xterm 真实支持项)
final double termFontSize; // 字号
final bool selectToCopy; // 选中即复制
Expand All @@ -23,6 +26,7 @@ class AppSettings {

const AppSettings({
this.lang = AppLang.zh,
this.themeId = 'mocha',
this.termFontSize = 12.5,
this.selectToCopy = true,
this.rightClickPaste = true,
Expand All @@ -32,6 +36,7 @@ class AppSettings {

factory AppSettings.fromJson(Map<String, dynamic> j) => AppSettings(
lang: (j['lang'] as String?) == 'en' ? AppLang.en : AppLang.zh,
themeId: j['themeId'] as String? ?? 'mocha',
termFontSize: (j['termFontSize'] as num?)?.toDouble() ?? 12.5,
selectToCopy: j['selectToCopy'] as bool? ?? true,
rightClickPaste: j['rightClickPaste'] as bool? ?? true,
Expand All @@ -45,6 +50,7 @@ class AppSettings {

Map<String, dynamic> toJson() => {
'lang': lang == AppLang.en ? 'en' : 'zh',
'themeId': themeId,
'termFontSize': termFontSize,
'selectToCopy': selectToCopy,
'rightClickPaste': rightClickPaste,
Expand All @@ -54,6 +60,7 @@ class AppSettings {

AppSettings copyWith({
AppLang? lang,
String? themeId,
double? termFontSize,
bool? selectToCopy,
bool? rightClickPaste,
Expand All @@ -62,6 +69,7 @@ class AppSettings {
}) =>
AppSettings(
lang: lang ?? this.lang,
themeId: themeId ?? this.themeId,
termFontSize: termFontSize ?? this.termFontSize,
selectToCopy: selectToCopy ?? this.selectToCopy,
rightClickPaste: rightClickPaste ?? this.rightClickPaste,
Expand Down
Loading
Loading