feat: 常见配置文件高亮&收藏路径文件展开

This commit is contained in:
chaos-zhu
2026-05-23 23:00:47 +08:00
parent b84a436917
commit db0d6c9f43
6 changed files with 148 additions and 6458 deletions
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 '';
+48 -9
View File
@@ -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,
+3
View File
@@ -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',
+3
View File
@@ -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': '新建',