audit responsive workspace and interaction UI
This commit is contained in:
@@ -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, _) {
|
||||
|
||||
@@ -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
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user