audit responsive workspace and interaction UI

This commit is contained in:
ngfchl
2026-07-19 00:14:00 +08:00
parent be64bd59c4
commit 80f0db6996
11 changed files with 696 additions and 544 deletions
+4 -2
View File
@@ -9,6 +9,8 @@ class AppLogDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cs = ShadTheme.of(context).colorScheme;
final screen = MediaQuery.sizeOf(context);
final compact = screen.width < 720;
return ShadDialog(
title: const Text('运行日志'),
description: const Text('实时显示当前会话日志;RELEASE 同时写入本地日志文件。'),
@@ -26,8 +28,8 @@ class AppLogDialog extends StatelessWidget {
),
],
child: SizedBox(
width: 840,
height: 520,
width: compact ? screen.width - 32 : 840,
height: compact ? (screen.height * 0.56).clamp(280, 440) : 520,
child: ValueListenableBuilder<List<AppLogEntry>>(
valueListenable: AppLogger.entries,
builder: (_, entries, _) {
+61 -42
View File
@@ -17,27 +17,12 @@ class BreadcrumbBar extends StatelessWidget {
final cs = ShadTheme.of(context).colorScheme;
final items = <Widget>[
// Home button
GestureDetector(
_BreadcrumbItem(
label: '云盘根目录',
text: '云盘',
selected: path.isEmpty,
enabled: true,
onTap: () => onNavigate(-1),
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: path.isEmpty ? cs.primary.withAlpha(15) : null,
borderRadius: BorderRadius.circular(6),
),
child: Text(
'云盘',
style: TextStyle(
fontSize: 13,
fontWeight: path.isEmpty ? FontWeight.w600 : FontWeight.normal,
color: path.isEmpty ? cs.primary : cs.mutedForeground,
),
),
),
),
),
];
@@ -51,28 +36,12 @@ class BreadcrumbBar extends StatelessWidget {
),
);
items.add(
GestureDetector(
onTap: isLast ? null : () => onNavigate(i),
child: MouseRegion(
cursor: isLast
? SystemMouseCursors.basic
: SystemMouseCursors.click,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isLast ? cs.primary.withAlpha(15) : null,
borderRadius: BorderRadius.circular(6),
),
child: Text(
path[i].name,
style: TextStyle(
fontSize: 13,
fontWeight: isLast ? FontWeight.w600 : FontWeight.normal,
color: isLast ? cs.primary : cs.mutedForeground,
),
),
),
),
_BreadcrumbItem(
label: path[i].name,
text: path[i].name,
selected: isLast,
enabled: !isLast,
onTap: () => onNavigate(i),
),
);
}
@@ -90,3 +59,53 @@ class BreadcrumbBar extends StatelessWidget {
);
}
}
class _BreadcrumbItem extends StatelessWidget {
final String label;
final String text;
final bool selected;
final bool enabled;
final VoidCallback onTap;
const _BreadcrumbItem({
required this.label,
required this.text,
required this.selected,
required this.enabled,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final cs = ShadTheme.of(context).colorScheme;
return Semantics(
button: enabled,
selected: selected,
label: label,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: enabled ? onTap : null,
borderRadius: BorderRadius.circular(6),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: selected ? cs.primary.withAlpha(15) : null,
borderRadius: BorderRadius.circular(6),
),
child: Text(
text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
fontWeight: selected ? FontWeight.w600 : FontWeight.normal,
color: selected ? cs.primary : cs.mutedForeground,
),
),
),
),
),
);
}
}
+118 -110
View File
@@ -206,124 +206,132 @@ class _FileListTileState extends State<FileListTile> {
),
],
],
child: GestureDetector(
onTap: _isRenaming ? null : widget.onSelect,
onDoubleTap: _isRenaming ? null : widget.onOpen,
child: Container(
height: compact ? 74 : 62,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: widget.isSelected
? cs.primary.withValues(alpha: 0.14)
: cs.card,
border: Border(bottom: BorderSide(color: cs.border, width: 0.5)),
),
child: compact
? _buildCompactContent(theme)
: Row(
children: [
if (widget.isSelected)
Padding(
padding: const EdgeInsets.only(right: 8),
child: Icon(
LucideIcons.checkCircle,
size: 18,
color: theme.colorScheme.primary,
child: Semantics(
button: !_isRenaming,
selected: widget.isSelected,
label:
'${widget.file.isDirectory ? '文件夹' : '文件'} ${widget.file.name}${widget.file.isDirectory ? '' : '${widget.file.formattedSize}'}',
hint: _isRenaming ? '正在重命名' : '点按选择,双击打开',
child: GestureDetector(
onTap: _isRenaming ? null : widget.onSelect,
onDoubleTap: _isRenaming ? null : widget.onOpen,
child: Container(
height: compact ? 74 : 62,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: widget.isSelected
? cs.primary.withValues(alpha: 0.14)
: cs.card,
border: Border(bottom: BorderSide(color: cs.border, width: 0.5)),
),
child: compact
? _buildCompactContent(theme)
: Row(
children: [
if (widget.isSelected)
Padding(
padding: const EdgeInsets.only(right: 8),
child: Icon(
LucideIcons.checkCircle,
size: 18,
color: theme.colorScheme.primary,
),
),
SizedBox(
width: 32,
height: 32,
child: FileIcon(file: widget.file),
),
SizedBox(
width: 32,
height: 32,
child: FileIcon(file: widget.file),
),
const SizedBox(width: 12),
Expanded(
child: _isRenaming
? Row(
children: [
Expanded(
child: ShadInput(
controller: _renameController,
focusNode: _renameFocusNode,
autofocus: true,
enabled: !_isSubmittingRename,
onSubmitted: (_) => _confirmRename(),
),
),
const SizedBox(width: 4),
ShadButton.ghost(
size: ShadButtonSize.sm,
onPressed: _isSubmittingRename
? null
: _confirmRename,
child: const Icon(
Icons.check_rounded,
size: 17,
),
),
ShadButton.ghost(
size: ShadButtonSize.sm,
onPressed: _isSubmittingRename
? null
: _cancelRename,
child: const Icon(
Icons.close_rounded,
size: 17,
),
),
],
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.file.name,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: theme.colorScheme.foreground,
),
overflow: TextOverflow.ellipsis,
),
if (widget.file.directoryContentSummary
case final String summary)
Text(
summary,
style: TextStyle(
fontSize: 11,
color: theme.colorScheme.mutedForeground,
const SizedBox(width: 12),
Expanded(
child: _isRenaming
? Row(
children: [
Expanded(
child: ShadInput(
controller: _renameController,
focusNode: _renameFocusNode,
autofocus: true,
enabled: !_isSubmittingRename,
onSubmitted: (_) => _confirmRename(),
),
),
],
),
),
SizedBox(
width: 80,
child: Text(
widget.file.formattedSize,
textAlign: TextAlign.right,
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.mutedForeground,
const SizedBox(width: 4),
ShadButton.ghost(
size: ShadButtonSize.sm,
onPressed: _isSubmittingRename
? null
: _confirmRename,
child: const Icon(
Icons.check_rounded,
size: 17,
),
),
ShadButton.ghost(
size: ShadButtonSize.sm,
onPressed: _isSubmittingRename
? null
: _cancelRename,
child: const Icon(
Icons.close_rounded,
size: 17,
),
),
],
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.file.name,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: theme.colorScheme.foreground,
),
overflow: TextOverflow.ellipsis,
),
if (widget.file.directoryContentSummary
case final String summary)
Text(
summary,
style: TextStyle(
fontSize: 11,
color:
theme.colorScheme.mutedForeground,
),
),
],
),
),
SizedBox(
width: 80,
child: Text(
widget.file.formattedSize,
textAlign: TextAlign.right,
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.mutedForeground,
),
),
),
),
const SizedBox(width: 12),
SizedBox(
width: 120,
child: Text(
widget.file.modifiedAt,
textAlign: TextAlign.right,
style: TextStyle(
fontSize: 11,
color: theme.colorScheme.mutedForeground,
const SizedBox(width: 12),
SizedBox(
width: 120,
child: Text(
widget.file.modifiedAt,
textAlign: TextAlign.right,
style: TextStyle(
fontSize: 11,
color: theme.colorScheme.mutedForeground,
),
overflow: TextOverflow.ellipsis,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
),
),
);
+36 -13
View File
@@ -12,6 +12,7 @@ import 'package:shadcn_ui/shadcn_ui.dart';
import '../models/cloud_file.dart';
import '../models/media_library.dart';
import '../core/logging/app_logger.dart';
import '../providers/file_provider.dart';
Future<void> showMediaPlayerDialog(
@@ -39,7 +40,9 @@ Future<void> showMediaPlayerDialog(
onPlaybackFailure: openExternalPlayer,
),
);
} catch (_) {
} catch (error, stackTrace) {
AppLogger.warning('Player', '内置播放器窗口异常,正在打开外部播放器:$error');
AppLogger.debug('Player', stackTrace.toString());
await openExternalPlayer();
}
}
@@ -168,6 +171,7 @@ class _MediaPlayerDialogState extends ConsumerState<MediaPlayerDialog> {
}
void _handlePlaybackFailure(Object error, String message) {
AppLogger.warning('Player', '$message,准备切换外部播放器:$error');
if (!mounted) return;
setState(() {
_error = '$message$error';
@@ -282,28 +286,32 @@ class _MediaPlayerDialogState extends ConsumerState<MediaPlayerDialog> {
Widget build(BuildContext context) {
final cs = ShadTheme.of(context).colorScheme;
final screen = MediaQuery.sizeOf(context);
final sideWidth = _showEpisodes ? 250.0 : 0.0;
const dialogPadding = 40.0;
final maxDialogWidth = math.max(400.0, screen.width - 32);
final maxVideoWidth = math.max(
360.0,
maxDialogWidth - sideWidth - dialogPadding,
final compact = screen.width < 600;
final sideWidth = _showEpisodes && !compact ? 250.0 : 0.0;
final maxDialogWidth = math.max(1.0, screen.width - 24);
final maxVideoWidth = math.max(1.0, maxDialogWidth - sideWidth - 24);
final minVideoWidth = math.min(compact ? 240.0 : 360.0, maxVideoWidth);
final minVideoHeight = compact ? 160.0 : 240.0;
final maxVideoHeight = math.max(
minVideoHeight,
screen.height - (compact ? 230 : 290),
);
final maxVideoHeight = math.max(260.0, screen.height - 290);
final preferredWidth = _hasVideoDimensions
? math.min(900.0, _videoAspectRatio * maxVideoHeight)
: 600.0;
final videoWidth = preferredWidth.clamp(360.0, maxVideoWidth).toDouble();
final videoWidth = preferredWidth
.clamp(minVideoWidth, maxVideoWidth)
.toDouble();
final videoHeight = (videoWidth / _videoAspectRatio)
.clamp(240.0, maxVideoHeight)
.clamp(minVideoHeight, maxVideoHeight)
.toDouble();
final contentWidth = videoWidth + sideWidth;
return ShadDialog(
constraints: BoxConstraints(
maxWidth: math.min(maxDialogWidth, contentWidth + dialogPadding),
maxHeight: math.max(360, screen.height - 24),
maxWidth: math.min(maxDialogWidth, contentWidth + 24),
maxHeight: math.max(260, screen.height - 16),
),
padding: const EdgeInsets.all(20),
padding: EdgeInsets.all(compact ? 12 : 20),
scrollable: false,
title: Text(
_currentFile.name,
@@ -315,6 +323,8 @@ class _MediaPlayerDialogState extends ConsumerState<MediaPlayerDialog> {
ShadButton.outline(
onPressed: _episodes.length < 2
? null
: compact
? _showEpisodesSheet
: () => setState(() => _showEpisodes = !_showEpisodes),
leading: const Icon(Icons.format_list_bulleted_rounded, size: 16),
child: const Text('同目录剧集'),
@@ -381,6 +391,19 @@ class _MediaPlayerDialogState extends ConsumerState<MediaPlayerDialog> {
);
}
Future<void> _showEpisodesSheet() => showShadSheet<void>(
context: context,
side: ShadSheetSide.bottom,
builder: (_) => ShadSheet(
constraints: const BoxConstraints(maxHeight: 520),
title: const Text('同目录剧集'),
child: SizedBox(
height: 360,
child: _episodeList(ShadTheme.of(context).colorScheme),
),
),
);
Widget _episodeList(ShadColorScheme cs) {
return DecoratedBox(
decoration: BoxDecoration(