Polish media viewer, pull-to-sync, search nav, and add seek bar presets

- Remove peek details sheet from media viewer; scope Activity tab to
  the current file via client-side filtering instead of unreliable
  server-side query params
- Add file rename action (WebDAV MOVE); fix search result taps to
  switch to Files tab and rebuild breadcrumbs from an absolute path
- Add swipe-to-delete confirmation dialog; fix a crash from disposing
  the rename dialog's TextEditingController too early
- Rework pull-to-sync gesture so sync and lock-open thresholds fire
  independently within one gesture, raise thresholds, and add a
  classic Material refresh spinner
- Add "tap active tab to scroll to top" behavior with a settings toggle
- Align selection toolbar padding with the sort/filter controls row
- Replace app icon assets (adaptive icon + splash screen) with
  updated IconKitchen export
- Replace the seek bar style dropdown with a Default/Wavy/Slim/Squiggly
  preset picker, each rendered with a live, perpetually-animated
  preview via a new shared SeekBarPainter/SeekBarPreview

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 10:25:52 -04:00
co-authored by Claude Sonnet 5
parent 1e42fcc0e6
commit d5675d853c
37 changed files with 951 additions and 376 deletions
+176 -28
View File
@@ -4,6 +4,7 @@ import '../models/app_tab.dart';
import '../providers/server_provider.dart';
import '../theme/app_theme.dart';
import '../widgets/frosted_glass_container.dart';
import '../widgets/seek_bar_painter.dart';
class AccountView extends StatelessWidget {
const AccountView({super.key});
@@ -342,6 +343,17 @@ class AccountView extends StatelessWidget {
),
const SizedBox(height: 10),
const _BottomBarAppearanceCard(),
const SizedBox(height: 12),
Card(
child: SwitchListTile(
title: const Text('Tap Tab to Scroll to Top'),
subtitle: const Text(
'Tapping the current bottom bar tab scrolls its list back to the top',
),
value: provider.tapTabToScrollTop,
onChanged: provider.setTapTabToScrollTop,
),
),
const SizedBox(height: 24),
// Bottom Nav Tabs
@@ -742,48 +754,115 @@ class _SwipeActionsCard extends StatelessWidget {
}
}
String _seekBarStyleLabel(MediaProgressBarStyle style) {
switch (style) {
case MediaProgressBarStyle.classic:
return 'Default';
case MediaProgressBarStyle.wavy:
return 'Wavy';
case MediaProgressBarStyle.slim:
return 'Slim';
case MediaProgressBarStyle.squiggly:
return 'Squiggly';
}
}
class _MediaPlayerCard extends StatelessWidget {
const _MediaPlayerCard();
String _label(MediaProgressBarStyle style) {
switch (style) {
case MediaProgressBarStyle.wavy:
return 'Wavy';
case MediaProgressBarStyle.classic:
return 'Classic';
}
Future<void> _openPicker(
BuildContext context,
ServerProvider provider,
) async {
final selected = await showDialog<MediaProgressBarStyle>(
context: context,
builder: (_) =>
_SeekBarStyleDialog(current: provider.mediaProgressBarStyle),
);
if (selected != null) provider.setMediaProgressBarStyle(selected);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final provider = context.watch<ServerProvider>();
return Card(
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
title: Text(
'Seek bar style',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
subtitle: Text(_seekBarStyleLabel(provider.mediaProgressBarStyle)),
trailing: SizedBox(
width: 64,
height: 24,
child: SeekBarPreview(
style: provider.mediaProgressBarStyle,
playedColor: colorScheme.primary,
trackColor: colorScheme.outlineVariant,
),
),
onTap: () => _openPicker(context, provider),
),
);
}
}
/// A grid of the four [MediaProgressBarStyle] presets, each shown as a live
/// preview of the real seek bar painter, matching the preset-picker pattern
/// other Material You media players use for this same setting.
class _SeekBarStyleDialog extends StatelessWidget {
final MediaProgressBarStyle current;
const _SeekBarStyleDialog({required this.current});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Row(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 12),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
'Seek bar style',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
Text(
'Seek Bar Style',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
DropdownButton<MediaProgressBarStyle>(
value: provider.mediaProgressBarStyle,
underline: const SizedBox.shrink(),
borderRadius: BorderRadius.circular(12),
items: MediaProgressBarStyle.values
.map(
(s) => DropdownMenuItem(value: s, child: Text(_label(s))),
)
.toList(),
onChanged: (s) {
if (s != null) provider.setMediaProgressBarStyle(s);
},
const SizedBox(height: 16),
GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 1.35,
children: [
for (final style in MediaProgressBarStyle.values)
_SeekBarStyleOption(
style: style,
isSelected: style == current,
onTap: () => Navigator.pop(context, style),
),
],
),
const SizedBox(height: 4),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
),
],
),
@@ -792,6 +871,75 @@ class _MediaPlayerCard extends StatelessWidget {
}
}
class _SeekBarStyleOption extends StatelessWidget {
final MediaProgressBarStyle style;
final bool isSelected;
final VoidCallback onTap;
const _SeekBarStyleOption({
required this.style,
required this.isSelected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Material(
color: isSelected
? colorScheme.primaryContainer.withValues(alpha: 0.4)
: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isSelected
? colorScheme.primary
: colorScheme.outlineVariant,
width: isSelected ? 2 : 1,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Center(
child: SizedBox(
width: 100,
height: 28,
child: SeekBarPreview(
style: style,
playedColor: colorScheme.primary,
trackColor: colorScheme.outlineVariant,
),
),
),
),
const SizedBox(height: 8),
Text(
_seekBarStyleLabel(style),
style: theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w600,
color: isSelected
? colorScheme.primary
: colorScheme.onSurface,
),
),
],
),
),
),
);
}
}
class _CacheSettingsCard extends StatelessWidget {
const _CacheSettingsCard();
@@ -809,7 +957,7 @@ class _CacheSettingsCard extends StatelessWidget {
String _description(CachePolicy policy) {
switch (policy) {
case CachePolicy.never:
return 'Every visit to a folder fetches it fresh (current behavior)';
return 'Every visit to a folder fetches it fresh';
case CachePolicy.interval:
return 'Reuse a folder\'s listing until it\'s a few minutes old';
case CachePolicy.manual: