mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-10-06 15:13:18 +08:00
feat: 常见配置文件高亮&收藏路径文件展开
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -54,6 +54,13 @@ String? _fileExtension(String name) {
|
||||
return name.substring(dot + 1).toLowerCase();
|
||||
}
|
||||
|
||||
String _basenameLower(String name) {
|
||||
final normalized = name.replaceAll('\\', '/');
|
||||
final slash = normalized.lastIndexOf('/');
|
||||
final base = slash < 0 ? normalized : normalized.substring(slash + 1);
|
||||
return base.toLowerCase();
|
||||
}
|
||||
|
||||
bool isKnownBinaryExtension(String name) {
|
||||
final ext = _fileExtension(name);
|
||||
if (ext == null) return false;
|
||||
@@ -63,6 +70,27 @@ bool isKnownBinaryExtension(String name) {
|
||||
bool hasFileExtension(String name) => _fileExtension(name) != null;
|
||||
|
||||
EditorLanguage detectFromFileName(String name) {
|
||||
final base = _basenameLower(name);
|
||||
switch (base) {
|
||||
case 'nginx.conf':
|
||||
return EditorLanguage(
|
||||
id: 'Nginx',
|
||||
highlightMode: langNginx,
|
||||
defaultIndent: 2,
|
||||
);
|
||||
case 'caddyfile':
|
||||
return EditorLanguage(
|
||||
id: 'Caddyfile',
|
||||
highlightMode: langNginx,
|
||||
defaultIndent: 2,
|
||||
);
|
||||
case 'dockerfile':
|
||||
return EditorLanguage(
|
||||
id: 'Dockerfile',
|
||||
highlightMode: langDockerfile,
|
||||
defaultIndent: 2,
|
||||
);
|
||||
}
|
||||
final ext = _fileExtension(name);
|
||||
if (ext == null) {
|
||||
return _plainText;
|
||||
|
||||
@@ -71,6 +71,7 @@ class SftpSessionState extends ChangeNotifier {
|
||||
bool loadingDirectory = false;
|
||||
bool showHidden = false;
|
||||
String? lastError;
|
||||
String? connectPhase;
|
||||
|
||||
bool get isConnected => status == SftpConnectionStatus.connected;
|
||||
bool get hasSelection => selectedNames.isNotEmpty;
|
||||
@@ -176,6 +177,7 @@ class SftpSessionManager extends ChangeNotifier {
|
||||
lastError: null,
|
||||
selectedNames: <String>{},
|
||||
);
|
||||
state.connectPhase = 'transport';
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
@@ -192,6 +194,8 @@ class SftpSessionManager extends ChangeNotifier {
|
||||
: null,
|
||||
identities: identities,
|
||||
);
|
||||
state.connectPhase = 'channel';
|
||||
notifyListeners();
|
||||
final sftp = await client.sftp();
|
||||
_connections[server.id] = _SftpConnection(
|
||||
client: client,
|
||||
@@ -199,13 +203,19 @@ class SftpSessionManager extends ChangeNotifier {
|
||||
transport: transport,
|
||||
);
|
||||
|
||||
final initial = await _resolveInitialPath(sftp, config.username);
|
||||
final entries = await _listDirectory(sftp, initial, state.showHidden);
|
||||
final favorites = await loadFavorites?.call(server.id) ?? const [];
|
||||
state.connectPhase = 'listing';
|
||||
notifyListeners();
|
||||
final results = await Future.wait<Object?>([
|
||||
_resolveInitialPathAndList(sftp, config.username, state.showHidden),
|
||||
_safeLoadFavorites(loadFavorites, server.id),
|
||||
]);
|
||||
final listing = results[0] as _InitialListing;
|
||||
final favorites = results[1] as List<SftpFavorite>;
|
||||
state.connectPhase = null;
|
||||
state.update(
|
||||
status: SftpConnectionStatus.connected,
|
||||
currentPath: initial,
|
||||
entries: entries,
|
||||
currentPath: listing.path,
|
||||
entries: listing.entries,
|
||||
favorites: favorites,
|
||||
loadingDirectory: false,
|
||||
lastError: null,
|
||||
@@ -215,6 +225,7 @@ class SftpSessionManager extends ChangeNotifier {
|
||||
return state;
|
||||
} catch (error) {
|
||||
await _connections.remove(server.id)?.close();
|
||||
state.connectPhase = null;
|
||||
state.update(
|
||||
status: SftpConnectionStatus.error,
|
||||
loadingDirectory: false,
|
||||
@@ -608,33 +619,68 @@ class SftpSessionManager extends ChangeNotifier {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<String> _resolveInitialPath(SftpClient sftp, String username) async {
|
||||
Future<_InitialListing> _resolveInitialPathAndList(
|
||||
SftpClient sftp,
|
||||
String username,
|
||||
bool showHidden,
|
||||
) async {
|
||||
if (username == 'root') {
|
||||
try {
|
||||
await sftp.listdir('/root');
|
||||
return '/root';
|
||||
final names = await sftp.listdir('/root');
|
||||
return _InitialListing(
|
||||
path: '/root',
|
||||
entries: _filterAndSort(names, showHidden),
|
||||
);
|
||||
} catch (_) {
|
||||
return '/';
|
||||
// fall through to '/'
|
||||
}
|
||||
}
|
||||
try {
|
||||
await sftp.listdir('/');
|
||||
return '/';
|
||||
final names = await sftp.listdir('/');
|
||||
return _InitialListing(
|
||||
path: '/',
|
||||
entries: _filterAndSort(names, showHidden),
|
||||
);
|
||||
} catch (_) {
|
||||
try {
|
||||
return await sftp.absolute('.');
|
||||
final absolute = await sftp.absolute('.');
|
||||
final names = await sftp.listdir(absolute);
|
||||
return _InitialListing(
|
||||
path: absolute,
|
||||
entries: _filterAndSort(names, showHidden),
|
||||
);
|
||||
} catch (_) {
|
||||
return '~';
|
||||
final names = await sftp.listdir('.');
|
||||
return _InitialListing(
|
||||
path: '~',
|
||||
entries: _filterAndSort(names, showHidden),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<SftpFavorite>> _safeLoadFavorites(
|
||||
Future<List<SftpFavorite>> Function(String hostId)? loader,
|
||||
String hostId,
|
||||
) async {
|
||||
if (loader == null) return const [];
|
||||
try {
|
||||
return await loader(hostId);
|
||||
} catch (_) {
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<SftpFileEntry>> _listDirectory(
|
||||
SftpClient sftp,
|
||||
String path,
|
||||
bool showHidden,
|
||||
) async {
|
||||
final names = await sftp.listdir(path == '~' ? '.' : path);
|
||||
return _filterAndSort(names, showHidden);
|
||||
}
|
||||
|
||||
List<SftpFileEntry> _filterAndSort(List<SftpName> names, bool showHidden) {
|
||||
final entries = names
|
||||
.where((item) => item.filename != '.' && item.filename != '..')
|
||||
.where((item) => showHidden || !item.filename.startsWith('.'))
|
||||
@@ -834,6 +880,13 @@ class _SftpConnection {
|
||||
}
|
||||
}
|
||||
|
||||
class _InitialListing {
|
||||
const _InitialListing({required this.path, required this.entries});
|
||||
|
||||
final String path;
|
||||
final List<SftpFileEntry> entries;
|
||||
}
|
||||
|
||||
extension on String {
|
||||
String skip(int count) {
|
||||
if (length <= count) return '';
|
||||
|
||||
@@ -128,7 +128,10 @@ class _SftpTabState extends ConsumerState<SftpTab> {
|
||||
final session = manager.activeSession;
|
||||
if (_connecting ||
|
||||
session?.status == SftpConnectionStatus.connecting) {
|
||||
return _SftpConnectingView(server: session?.server);
|
||||
return _SftpConnectingView(
|
||||
server: session?.server,
|
||||
phase: session?.connectPhase,
|
||||
);
|
||||
}
|
||||
if (session == null) {
|
||||
return ListView(
|
||||
@@ -758,16 +761,45 @@ class _SftpToolbar extends StatelessWidget {
|
||||
? Icons.folder_outlined
|
||||
: Icons.insert_drive_file_outlined,
|
||||
favorite.path,
|
||||
() {
|
||||
final target = favorite.type == 'folder'
|
||||
? favorite.path
|
||||
: SftpSessionManager.parentPath(favorite.path);
|
||||
manager.openPath(target);
|
||||
},
|
||||
() => _openFavorite(context, favorite),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> _openFavorite(
|
||||
BuildContext context,
|
||||
SftpFavorite favorite,
|
||||
) async {
|
||||
if (favorite.type == 'folder') {
|
||||
await manager.openPath(favorite.path);
|
||||
return;
|
||||
}
|
||||
final parent = SftpSessionManager.parentPath(favorite.path);
|
||||
final fileName = _favoriteBasename(favorite.path);
|
||||
await manager.openPath(parent);
|
||||
if (!context.mounted) return;
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => TextEditorPage(
|
||||
manager: manager,
|
||||
remotePath: favorite.path,
|
||||
fileName: fileName,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
await manager.refreshActive();
|
||||
}
|
||||
|
||||
String _favoriteBasename(String path) {
|
||||
final normalized = path.replaceAll('\\', '/');
|
||||
final trimmed = normalized.endsWith('/') && normalized.length > 1
|
||||
? normalized.substring(0, normalized.length - 1)
|
||||
: normalized;
|
||||
final index = trimmed.lastIndexOf('/');
|
||||
return index < 0 ? trimmed : trimmed.substring(index + 1);
|
||||
}
|
||||
|
||||
Future<void> _showNameDialog(
|
||||
BuildContext context, {
|
||||
required String type,
|
||||
@@ -1469,13 +1501,20 @@ class _SftpFileRow extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _SftpConnectingView extends StatelessWidget {
|
||||
const _SftpConnectingView({required this.server});
|
||||
const _SftpConnectingView({required this.server, this.phase});
|
||||
|
||||
final ServerModel? server;
|
||||
final String? phase;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l = AppLocalizations.of(context);
|
||||
final phaseLabel = switch (phase) {
|
||||
'transport' => l.tr('sftp.phase.transport'),
|
||||
'channel' => l.tr('sftp.phase.channel'),
|
||||
'listing' => l.tr('sftp.phase.listing'),
|
||||
_ => l.tr('sftp.connecting'),
|
||||
};
|
||||
return ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(16, 120, 16, 24),
|
||||
@@ -1494,7 +1533,7 @@ class _SftpConnectingView extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Text(
|
||||
l.tr('sftp.connecting'),
|
||||
phaseLabel,
|
||||
style: const TextStyle(
|
||||
color: _SftpPalette.text,
|
||||
fontSize: 18,
|
||||
|
||||
@@ -148,6 +148,9 @@ const Map<String, String> stringsEn = {
|
||||
'sftp.statusConnected': 'Connected',
|
||||
'sftp.statusDisconnected': 'Not connected',
|
||||
'sftp.connecting': 'Connecting SFTP...',
|
||||
'sftp.phase.transport': 'Connecting to server...',
|
||||
'sftp.phase.channel': 'Opening SFTP channel...',
|
||||
'sftp.phase.listing': 'Reading directory...',
|
||||
'sftp.emptyDirectory': 'This directory is empty.',
|
||||
'sftp.upload': 'Upload',
|
||||
'sftp.newItem': 'New',
|
||||
|
||||
@@ -138,6 +138,9 @@ const Map<String, String> stringsZh = {
|
||||
'sftp.statusConnected': '已连接',
|
||||
'sftp.statusDisconnected': '未连接',
|
||||
'sftp.connecting': '正在连接 SFTP...',
|
||||
'sftp.phase.transport': '正在连接服务器...',
|
||||
'sftp.phase.channel': '正在打开 SFTP 通道...',
|
||||
'sftp.phase.listing': '正在读取目录...',
|
||||
'sftp.emptyDirectory': '当前目录为空',
|
||||
'sftp.upload': '上传',
|
||||
'sftp.newItem': '新建',
|
||||
|
||||
Reference in New Issue
Block a user