audit responsive workspace and interaction UI
This commit is contained in:
+14
-4
@@ -9,17 +9,27 @@ import '../pages/login_page.dart';
|
||||
import '../pages/workspace_page.dart';
|
||||
import 'app_theme.dart';
|
||||
|
||||
class GuangyaApp extends ConsumerWidget {
|
||||
class GuangyaApp extends ConsumerStatefulWidget {
|
||||
const GuangyaApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<GuangyaApp> createState() => _GuangyaAppState();
|
||||
}
|
||||
|
||||
class _GuangyaAppState extends ConsumerState<GuangyaApp> {
|
||||
var _sessionInitialized = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final themeState = ref.watch(themeProvider);
|
||||
final auth = ref.watch(authProvider);
|
||||
|
||||
// Auto-load files when signed in
|
||||
if (auth.isSignedIn) {
|
||||
if (!auth.isSignedIn) {
|
||||
_sessionInitialized = false;
|
||||
} else if (!_sessionInitialized) {
|
||||
_sessionInitialized = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted || !ref.read(authProvider).isSignedIn) return;
|
||||
final fp = ref.read(fileProvider.notifier);
|
||||
fp.api = ref.read(authProvider.notifier).api;
|
||||
final media = ref.read(mediaLibraryProvider.notifier);
|
||||
|
||||
@@ -37,6 +37,7 @@ class AppLogEntry {
|
||||
'Auth' => '认证',
|
||||
'Media' => '媒体库',
|
||||
'CloudIndex' => '全盘索引',
|
||||
'Player' => '播放器',
|
||||
'App' => '应用',
|
||||
'Flutter' => '界面',
|
||||
'Dart' => '运行时',
|
||||
|
||||
+36
-40
@@ -27,23 +27,14 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
final auth = ref.watch(authProvider);
|
||||
final compact = MediaQuery.sizeOf(context).width < 480;
|
||||
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
theme.colorScheme.primary.withAlpha(30),
|
||||
theme.colorScheme.background,
|
||||
theme.colorScheme.primary.withAlpha(15),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: ColoredBox(
|
||||
color: theme.colorScheme.background,
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(32),
|
||||
padding: EdgeInsets.all(compact ? 16 : 32),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 420),
|
||||
child: Column(
|
||||
@@ -93,7 +84,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
// Login card
|
||||
ShadCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
padding: EdgeInsets.all(compact ? 18 : 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
@@ -297,33 +288,38 @@ class _TabButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? theme.colorScheme.background : Colors.transparent,
|
||||
return Semantics(
|
||||
button: true,
|
||||
selected: isActive,
|
||||
label: label,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
boxShadow: isActive
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.black.withAlpha(20),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
|
||||
color: isActive
|
||||
? theme.colorScheme.foreground
|
||||
: theme.colorScheme.mutedForeground,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 160),
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive
|
||||
? theme.colorScheme.background
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: isActive
|
||||
? Border.all(color: theme.colorScheme.border)
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
|
||||
color: isActive
|
||||
? theme.colorScheme.foreground
|
||||
: theme.colorScheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -252,10 +252,12 @@ class _FileSearchResultsPageState extends ConsumerState<FileSearchResultsPage> {
|
||||
if (selected.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 4),
|
||||
child: Row(
|
||||
child: Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
Text('已选择 ${selected.length} 项'),
|
||||
const Spacer(),
|
||||
ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => notifier.copyToClipboard(selected),
|
||||
@@ -432,10 +434,12 @@ class _MediaSearchResultsPageState
|
||||
if (selected.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 6,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
Text('已选择 ${selected.length} 个资源'),
|
||||
const Spacer(),
|
||||
ShadButton(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: _recognizing
|
||||
@@ -638,30 +642,56 @@ class _SearchPageFrame extends StatelessWidget {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
height: 54,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
constraints: const BoxConstraints(minHeight: 54),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: cs.border)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
ShadButton.ghost(
|
||||
onPressed: onClose,
|
||||
leading: const Icon(Icons.arrow_back_rounded, size: 16),
|
||||
child: const Text('返回'),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: cs.foreground,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ShadBadge(child: Text(query)),
|
||||
],
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final compact = constraints.maxWidth < 500;
|
||||
return Row(
|
||||
children: [
|
||||
ShadTooltip(
|
||||
builder: (_) => const Text('返回'),
|
||||
child: ShadButton.ghost(
|
||||
size: compact
|
||||
? ShadButtonSize.sm
|
||||
: ShadButtonSize.regular,
|
||||
onPressed: onClose,
|
||||
leading: const Icon(Icons.arrow_back_rounded, size: 16),
|
||||
child: compact
|
||||
? const SizedBox.shrink()
|
||||
: const Text('返回'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: cs.foreground,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: compact ? 120 : 260),
|
||||
child: ShadBadge(
|
||||
child: Text(
|
||||
query,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(child: child),
|
||||
|
||||
@@ -72,6 +72,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||
final cs = ShadTheme.of(context).colorScheme;
|
||||
final themeState = ref.watch(themeProvider);
|
||||
final mediaState = ref.watch(mediaLibraryProvider);
|
||||
final compact = MediaQuery.sizeOf(context).width < 760;
|
||||
|
||||
return ShadDialog(
|
||||
title: Row(
|
||||
@@ -97,10 +98,11 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||
),
|
||||
],
|
||||
child: SizedBox(
|
||||
width: 720,
|
||||
width: compact ? MediaQuery.sizeOf(context).width - 32 : 720,
|
||||
child: ShadTabs<_SettingsTab>(
|
||||
value: _activeTab,
|
||||
onChanged: (value) => setState(() => _activeTab = value),
|
||||
scrollable: compact,
|
||||
tabBarConstraints: const BoxConstraints(maxWidth: 720),
|
||||
contentConstraints: const BoxConstraints(maxWidth: 720),
|
||||
tabs: [
|
||||
@@ -327,13 +329,16 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tabScroll(Widget child) => SizedBox(
|
||||
height: 360,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
Widget _tabScroll(Widget child) {
|
||||
final availableHeight = MediaQuery.sizeOf(context).height - 270;
|
||||
return SizedBox(
|
||||
height: availableHeight.clamp(260.0, 420.0),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _textInput(
|
||||
TextEditingController controller, {
|
||||
@@ -356,46 +361,48 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||
);
|
||||
|
||||
Future<void> _saveSettings() async {
|
||||
StorageManager.set(
|
||||
StorageKeys.tmdbApiKey,
|
||||
_tmdbApiKeyController.text.trim(),
|
||||
);
|
||||
StorageManager.set(
|
||||
StorageKeys.tmdbImageProxy,
|
||||
_tmdbImageProxyController.text.trim(),
|
||||
);
|
||||
await StorageManager.set(
|
||||
StorageKeys.httpProxyHost,
|
||||
_httpProxyHostController.text.trim(),
|
||||
);
|
||||
await StorageManager.set(
|
||||
StorageKeys.httpProxyPort,
|
||||
_httpProxyPortController.text.trim(),
|
||||
);
|
||||
await StorageManager.delete(StorageKeys.tmdbProxyHost);
|
||||
await StorageManager.delete(StorageKeys.tmdbProxyPort);
|
||||
await Future.wait([
|
||||
StorageManager.set(
|
||||
StorageKeys.tmdbApiKey,
|
||||
_tmdbApiKeyController.text.trim(),
|
||||
),
|
||||
StorageManager.set(
|
||||
StorageKeys.tmdbImageProxy,
|
||||
_tmdbImageProxyController.text.trim(),
|
||||
),
|
||||
StorageManager.set(
|
||||
StorageKeys.httpProxyHost,
|
||||
_httpProxyHostController.text.trim(),
|
||||
),
|
||||
StorageManager.set(
|
||||
StorageKeys.httpProxyPort,
|
||||
_httpProxyPortController.text.trim(),
|
||||
),
|
||||
StorageManager.delete(StorageKeys.tmdbProxyHost),
|
||||
StorageManager.delete(StorageKeys.tmdbProxyPort),
|
||||
StorageManager.set(
|
||||
StorageKeys.mediaScanConcurrency,
|
||||
_scanConcurrencyController.text.trim(),
|
||||
),
|
||||
StorageManager.set(
|
||||
StorageKeys.fastTransferConcurrency,
|
||||
_transferConcurrencyController.text.trim(),
|
||||
),
|
||||
StorageManager.set(
|
||||
StorageKeys.fileCacheTTLMinutes,
|
||||
_cacheTTLController.text.trim(),
|
||||
),
|
||||
StorageManager.set(
|
||||
StorageKeys.cloudIndexRefreshMinutes,
|
||||
_cloudIndexRefreshController.text.trim(),
|
||||
),
|
||||
StorageManager.set(
|
||||
StorageKeys.defaultFilePageSize,
|
||||
_pageSizeController.text.trim(),
|
||||
),
|
||||
]);
|
||||
DioClient.updateNetworkProxy();
|
||||
StorageManager.set(
|
||||
StorageKeys.mediaScanConcurrency,
|
||||
_scanConcurrencyController.text.trim(),
|
||||
);
|
||||
StorageManager.set(
|
||||
StorageKeys.fastTransferConcurrency,
|
||||
_transferConcurrencyController.text.trim(),
|
||||
);
|
||||
StorageManager.set(
|
||||
StorageKeys.fileCacheTTLMinutes,
|
||||
_cacheTTLController.text.trim(),
|
||||
);
|
||||
await StorageManager.set(
|
||||
StorageKeys.cloudIndexRefreshMinutes,
|
||||
_cloudIndexRefreshController.text.trim(),
|
||||
);
|
||||
ref.read(mediaLibraryProvider.notifier).updateCloudIndexRefreshSchedule();
|
||||
StorageManager.set(
|
||||
StorageKeys.defaultFilePageSize,
|
||||
_pageSizeController.text.trim(),
|
||||
);
|
||||
}
|
||||
|
||||
String _themeModeToString(ThemeMode mode) {
|
||||
|
||||
@@ -383,7 +383,10 @@ class _WorkspacePageState extends ConsumerState<WorkspacePage> {
|
||||
}
|
||||
|
||||
void _showSettings(BuildContext context) {
|
||||
showDialog(context: context, builder: (_) => const SettingsDialog());
|
||||
showShadDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const SettingsDialog(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCloudContent(FileState state) {
|
||||
|
||||
+314
-261
@@ -94,25 +94,38 @@ class _ToolHeader extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: cs.border)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
ShadButton.ghost(
|
||||
onPressed: onClose,
|
||||
leading: const Icon(Icons.arrow_back_rounded, size: 16),
|
||||
child: const Text('返回'),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Icon(tool.icon, size: 20, color: cs.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
tool.title,
|
||||
style: TextStyle(
|
||||
color: cs.foreground,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final compact = constraints.maxWidth < 420;
|
||||
return Row(
|
||||
children: [
|
||||
ShadTooltip(
|
||||
builder: (_) => const Text('返回'),
|
||||
child: ShadButton.ghost(
|
||||
size: compact ? ShadButtonSize.sm : ShadButtonSize.regular,
|
||||
onPressed: onClose,
|
||||
leading: const Icon(Icons.arrow_back_rounded, size: 16),
|
||||
child: compact ? const SizedBox.shrink() : const Text('返回'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Icon(tool.icon, size: 20, color: cs.primary),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
tool.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: cs.foreground,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1145,54 +1158,61 @@ class _BatchRenameToolState extends ConsumerState<_BatchRenameTool> {
|
||||
description: '规则按从上到下的顺序应用。',
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
for (var index = 0; index < _rules.length; index++)
|
||||
_BatchRenameRuleRow(
|
||||
key: ValueKey(_rules[index].id),
|
||||
rule: _rules[index],
|
||||
isFirst: index == 0,
|
||||
isLast: index == _rules.length - 1,
|
||||
onChanged: (rule) => _updateRule(index, rule),
|
||||
onMoveUp: () => _moveRule(index, -1),
|
||||
onMoveDown: () => _moveRule(index, 1),
|
||||
onRemove: () => setState(() {
|
||||
_rules.removeAt(index);
|
||||
if (_rules.isEmpty) {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: _BatchRenameRuleRow(
|
||||
key: ValueKey(_rules[index].id),
|
||||
rule: _rules[index],
|
||||
isFirst: index == 0,
|
||||
isLast: index == _rules.length - 1,
|
||||
onChanged: (rule) => _updateRule(index, rule),
|
||||
onMoveUp: () => _moveRule(index, -1),
|
||||
onMoveDown: () => _moveRule(index, 1),
|
||||
onRemove: () => setState(() {
|
||||
_rules.removeAt(index);
|
||||
if (_rules.isEmpty) {
|
||||
_rules = const [
|
||||
BatchRenameRule(
|
||||
id: 'rule-0',
|
||||
kind: BatchRenameRuleKind.replace,
|
||||
),
|
||||
];
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
ShadButton.outline(
|
||||
onPressed: () => setState(() {
|
||||
_rules.add(
|
||||
BatchRenameRule(
|
||||
id: 'rule-${DateTime.now().microsecondsSinceEpoch}',
|
||||
kind: BatchRenameRuleKind.replace,
|
||||
),
|
||||
);
|
||||
}),
|
||||
leading: const Icon(Icons.add_rounded, size: 16),
|
||||
child: const Text('添加规则'),
|
||||
),
|
||||
ShadButton.ghost(
|
||||
onPressed: () => setState(() {
|
||||
_rules = const [
|
||||
BatchRenameRule(
|
||||
id: 'rule-0',
|
||||
kind: BatchRenameRuleKind.replace,
|
||||
),
|
||||
];
|
||||
}
|
||||
}),
|
||||
),
|
||||
ShadButton.outline(
|
||||
onPressed: () => setState(() {
|
||||
_rules.add(
|
||||
BatchRenameRule(
|
||||
id: 'rule-${DateTime.now().microsecondsSinceEpoch}',
|
||||
kind: BatchRenameRuleKind.replace,
|
||||
),
|
||||
);
|
||||
}),
|
||||
leading: const Icon(Icons.add_rounded, size: 16),
|
||||
child: const Text('添加规则'),
|
||||
),
|
||||
ShadButton.ghost(
|
||||
onPressed: () => setState(() {
|
||||
_rules = const [
|
||||
BatchRenameRule(
|
||||
id: 'rule-0',
|
||||
kind: BatchRenameRuleKind.replace,
|
||||
),
|
||||
];
|
||||
}),
|
||||
child: const Text('清空规则'),
|
||||
}),
|
||||
child: const Text('清空规则'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1216,43 +1236,51 @@ class _BatchRenameToolState extends ConsumerState<_BatchRenameTool> {
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
ShadButton.outline(
|
||||
onPressed: _running
|
||||
? null
|
||||
: () => setState(() {
|
||||
_selectedIDs
|
||||
..clear()
|
||||
..addAll(
|
||||
applicable.map((item) => item.file.id),
|
||||
);
|
||||
}),
|
||||
child: const Text('全选可应用项'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ShadButton.ghost(
|
||||
onPressed: _running
|
||||
? null
|
||||
: () => setState(_selectedIDs.clear),
|
||||
child: const Text('全不选'),
|
||||
),
|
||||
const Spacer(),
|
||||
if (_status.isNotEmpty)
|
||||
Expanded(
|
||||
child: Text(
|
||||
_status,
|
||||
textAlign: TextAlign.right,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: ShadTheme.of(
|
||||
context,
|
||||
).colorScheme.mutedForeground,
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) => Wrap(
|
||||
alignment: WrapAlignment.spaceBetween,
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
ShadButton.outline(
|
||||
onPressed: _running
|
||||
? null
|
||||
: () => setState(() {
|
||||
_selectedIDs
|
||||
..clear()
|
||||
..addAll(
|
||||
applicable.map((item) => item.file.id),
|
||||
);
|
||||
}),
|
||||
child: const Text('全选可应用项'),
|
||||
),
|
||||
ShadButton.ghost(
|
||||
onPressed: _running
|
||||
? null
|
||||
: () => setState(_selectedIDs.clear),
|
||||
child: const Text('全不选'),
|
||||
),
|
||||
if (_status.isNotEmpty)
|
||||
SizedBox(
|
||||
width: constraints.maxWidth < 560
|
||||
? constraints.maxWidth
|
||||
: 260,
|
||||
child: Text(
|
||||
_status,
|
||||
textAlign: constraints.maxWidth < 560
|
||||
? TextAlign.left
|
||||
: TextAlign.right,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: ShadTheme.of(
|
||||
context,
|
||||
).colorScheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
@@ -1274,36 +1302,58 @@ class _BatchRenameToolState extends ConsumerState<_BatchRenameTool> {
|
||||
),
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
const _BatchRenamePreviewHeader(),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemCount: previews.length,
|
||||
separatorBuilder: (_, _) =>
|
||||
const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final preview = previews[index];
|
||||
final canSelect = preview.applicable;
|
||||
return _BatchRenamePreviewRow(
|
||||
preview: preview,
|
||||
selected: _selectedIDs.contains(
|
||||
preview.file.id,
|
||||
),
|
||||
enabled: canSelect && !_running,
|
||||
onChanged: (selected) => setState(() {
|
||||
if (selected == true && canSelect) {
|
||||
_selectedIDs.add(preview.file.id);
|
||||
} else {
|
||||
_selectedIDs.remove(preview.file.id);
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final tableWidth = constraints.maxWidth < 920
|
||||
? 920.0
|
||||
: constraints.maxWidth;
|
||||
return Scrollbar(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: SizedBox(
|
||||
width: tableWidth,
|
||||
child: Column(
|
||||
children: [
|
||||
const _BatchRenamePreviewHeader(),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemCount: previews.length,
|
||||
separatorBuilder: (_, _) =>
|
||||
const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final preview = previews[index];
|
||||
final canSelect =
|
||||
preview.applicable;
|
||||
return _BatchRenamePreviewRow(
|
||||
preview: preview,
|
||||
selected: _selectedIDs.contains(
|
||||
preview.file.id,
|
||||
),
|
||||
enabled: canSelect && !_running,
|
||||
onChanged: (selected) =>
|
||||
setState(() {
|
||||
if (selected == true &&
|
||||
canSelect) {
|
||||
_selectedIDs.add(
|
||||
preview.file.id,
|
||||
);
|
||||
} else {
|
||||
_selectedIDs.remove(
|
||||
preview.file.id,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1448,72 +1498,82 @@ class _BatchRenameFolderPickerState
|
||||
child: const Text('使用此目录'),
|
||||
),
|
||||
],
|
||||
child: SizedBox(
|
||||
width: 540,
|
||||
height: 360,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final narrow = constraints.maxWidth < 560;
|
||||
return SizedBox(
|
||||
width: narrow ? constraints.maxWidth : 540,
|
||||
height: narrow ? 420 : 360,
|
||||
child: Column(
|
||||
children: [
|
||||
ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: _path.isEmpty
|
||||
? null
|
||||
: () {
|
||||
setState(() => _path.removeLast());
|
||||
_load();
|
||||
},
|
||||
leading: const Icon(Icons.arrow_back_rounded, size: 16),
|
||||
child: const Text('返回上级'),
|
||||
Row(
|
||||
children: [
|
||||
ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: _path.isEmpty
|
||||
? null
|
||||
: () {
|
||||
setState(() => _path.removeLast());
|
||||
_load();
|
||||
},
|
||||
leading: const Icon(Icons.arrow_back_rounded, size: 16),
|
||||
child: narrow
|
||||
? const SizedBox.shrink()
|
||||
: const Text('返回上级'),
|
||||
),
|
||||
const Spacer(),
|
||||
ShadTooltip(
|
||||
builder: (_) => const Text('刷新文件夹'),
|
||||
child: ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: _loading ? null : _load,
|
||||
child: const Icon(Icons.refresh_rounded, size: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: _loading ? null : _load,
|
||||
child: const Icon(Icons.refresh_rounded, size: 16),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: _loading
|
||||
? const Center(child: ShadProgress())
|
||||
: _error != null
|
||||
? Center(
|
||||
child: Text(
|
||||
_error!,
|
||||
style: TextStyle(color: cs.destructive),
|
||||
),
|
||||
)
|
||||
: _folders.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
'没有子文件夹',
|
||||
style: TextStyle(color: cs.mutedForeground),
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
itemCount: _folders.length,
|
||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final folder = _folders[index];
|
||||
return ListTile(
|
||||
leading: Icon(
|
||||
Icons.folder_rounded,
|
||||
color: cs.primary,
|
||||
),
|
||||
title: Text(folder.name),
|
||||
trailing: const Icon(Icons.chevron_right_rounded),
|
||||
onTap: () {
|
||||
setState(() => _path.add(folder));
|
||||
_load();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: _loading
|
||||
? const Center(child: ShadProgress())
|
||||
: _error != null
|
||||
? Center(
|
||||
child: Text(
|
||||
_error!,
|
||||
style: TextStyle(color: cs.destructive),
|
||||
),
|
||||
)
|
||||
: _folders.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
'没有子文件夹',
|
||||
style: TextStyle(color: cs.mutedForeground),
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
itemCount: _folders.length,
|
||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final folder = _folders[index];
|
||||
return ListTile(
|
||||
leading: Icon(
|
||||
Icons.folder_rounded,
|
||||
color: cs.primary,
|
||||
),
|
||||
title: Text(folder.name),
|
||||
trailing: const Icon(Icons.chevron_right_rounded),
|
||||
onTap: () {
|
||||
setState(() => _path.add(folder));
|
||||
_load();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1547,91 +1607,84 @@ class _BatchRenameRuleRow extends StatelessWidget {
|
||||
rule.kind == BatchRenameRuleKind.regex;
|
||||
final supportsCase =
|
||||
hasReplacement || rule.kind == BatchRenameRuleKind.remove;
|
||||
return IntrinsicWidth(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.muted.withValues(alpha: 0.46),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: cs.border.withValues(alpha: 0.65)),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
ShadCheckbox(
|
||||
value: rule.enabled,
|
||||
label: const Text('启用'),
|
||||
onChanged: (value) => onChanged(rule.copyWith(enabled: value)),
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.muted.withValues(alpha: 0.46),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: cs.border.withValues(alpha: 0.65)),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
ShadCheckbox(
|
||||
value: rule.enabled,
|
||||
label: const Text('启用'),
|
||||
onChanged: (value) => onChanged(rule.copyWith(enabled: value)),
|
||||
),
|
||||
SizedBox(
|
||||
width: 132,
|
||||
child: ShadSelect<BatchRenameRuleKind>(
|
||||
key: ValueKey('${rule.id}-${rule.kind}'),
|
||||
initialValue: rule.kind,
|
||||
selectedOptionBuilder: (_, value) => Text(_ruleKindLabel(value)),
|
||||
options: [
|
||||
for (final value in BatchRenameRuleKind.values)
|
||||
ShadOption(value: value, child: Text(_ruleKindLabel(value))),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) onChanged(rule.copyWith(kind: value));
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
width: 132,
|
||||
child: ShadSelect<BatchRenameRuleKind>(
|
||||
key: ValueKey('${rule.id}-${rule.kind}'),
|
||||
initialValue: rule.kind,
|
||||
selectedOptionBuilder: (_, value) =>
|
||||
Text(_ruleKindLabel(value)),
|
||||
options: [
|
||||
for (final value in BatchRenameRuleKind.values)
|
||||
ShadOption(
|
||||
value: value,
|
||||
child: Text(_ruleKindLabel(value)),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) onChanged(rule.copyWith(kind: value));
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 190,
|
||||
child: ShadInput(
|
||||
initialValue: rule.pattern,
|
||||
placeholder: Text(_rulePatternPlaceholder(rule.kind)),
|
||||
onChanged: (value) => onChanged(rule.copyWith(pattern: value)),
|
||||
),
|
||||
),
|
||||
if (hasReplacement) ...[
|
||||
const Icon(Icons.arrow_right_alt_rounded, size: 18),
|
||||
SizedBox(
|
||||
width: 190,
|
||||
width: 160,
|
||||
child: ShadInput(
|
||||
initialValue: rule.pattern,
|
||||
placeholder: Text(_rulePatternPlaceholder(rule.kind)),
|
||||
onChanged: (value) => onChanged(rule.copyWith(pattern: value)),
|
||||
),
|
||||
),
|
||||
if (hasReplacement) ...[
|
||||
const Icon(Icons.arrow_right_alt_rounded, size: 18),
|
||||
SizedBox(
|
||||
width: 160,
|
||||
child: ShadInput(
|
||||
initialValue: rule.replacement,
|
||||
placeholder: const Text('替换为'),
|
||||
onChanged: (value) =>
|
||||
onChanged(rule.copyWith(replacement: value)),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (supportsCase)
|
||||
ShadCheckbox(
|
||||
value: rule.ignoreCase,
|
||||
label: const Text('忽略大小写'),
|
||||
initialValue: rule.replacement,
|
||||
placeholder: const Text('替换为'),
|
||||
onChanged: (value) =>
|
||||
onChanged(rule.copyWith(ignoreCase: value)),
|
||||
onChanged(rule.copyWith(replacement: value)),
|
||||
),
|
||||
_RenameRuleIconButton(
|
||||
icon: Icons.arrow_upward_rounded,
|
||||
tooltip: '上移规则',
|
||||
enabled: !isFirst,
|
||||
onPressed: onMoveUp,
|
||||
),
|
||||
_RenameRuleIconButton(
|
||||
icon: Icons.arrow_downward_rounded,
|
||||
tooltip: '下移规则',
|
||||
enabled: !isLast,
|
||||
onPressed: onMoveDown,
|
||||
),
|
||||
_RenameRuleIconButton(
|
||||
icon: Icons.delete_outline_rounded,
|
||||
tooltip: '删除规则',
|
||||
enabled: true,
|
||||
destructive: true,
|
||||
onPressed: onRemove,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (supportsCase)
|
||||
ShadCheckbox(
|
||||
value: rule.ignoreCase,
|
||||
label: const Text('忽略大小写'),
|
||||
onChanged: (value) => onChanged(rule.copyWith(ignoreCase: value)),
|
||||
),
|
||||
_RenameRuleIconButton(
|
||||
icon: Icons.arrow_upward_rounded,
|
||||
tooltip: '上移规则',
|
||||
enabled: !isFirst,
|
||||
onPressed: onMoveUp,
|
||||
),
|
||||
_RenameRuleIconButton(
|
||||
icon: Icons.arrow_downward_rounded,
|
||||
tooltip: '下移规则',
|
||||
enabled: !isLast,
|
||||
onPressed: onMoveDown,
|
||||
),
|
||||
_RenameRuleIconButton(
|
||||
icon: Icons.delete_outline_rounded,
|
||||
tooltip: '删除规则',
|
||||
enabled: true,
|
||||
destructive: true,
|
||||
onPressed: onRemove,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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