From 73e05f8d225caef6d5d519c7381d86ebadac2e39 Mon Sep 17 00:00:00 2001 From: ayushya Date: Wed, 16 Sep 2026 12:28:11 -0400 Subject: [PATCH] Remove scrollbar thumb and restructure Settings profile card - Revert the scrollbar-thumb feature entirely (per-view Scrollbar wrappers and the ScrollbarThemeData override) - back to plain scrollables with no visible thumb - Settings: move the logout button into the profile card (next to the name/email), and move the ONLINE/OFFLINE badge + "Connected to..." text out of the top row into a combined line at the bottom of the card, instead of split between the AppBar and the top of the screen - Update architecture.md/styling.md to drop the stale scrollbar-theme documentation Co-Authored-By: Claude Sonnet 5 --- .claude/context/architecture.md | 4 +- .claude/context/styling.md | 11 +- lib/theme/app_theme.dart | 33 - lib/views/account_view.dart | 761 +++++++++--------- lib/views/search_view.dart | 46 +- lib/views/share_upload_view.dart | 34 +- lib/widgets/details/details_activity_tab.dart | 166 ++-- lib/widgets/details/details_info_tab.dart | 59 +- lib/widgets/details/details_versions_tab.dart | 74 +- lib/widgets/share_sheet.dart | 189 +++-- lib/widgets/synced_header_scaffold.dart | 81 +- 11 files changed, 698 insertions(+), 760 deletions(-) diff --git a/.claude/context/architecture.md b/.claude/context/architecture.md index 60b02d4..b89367b 100644 --- a/.claude/context/architecture.md +++ b/.claude/context/architecture.md @@ -101,8 +101,8 @@ owns the app's share-intent listener (`receive_sharing_intent`): both Five of the six tabs (all but Files) plus each tab's own controls share [`SyncedHeaderScaffold`](../../lib/widgets/synced_header_scaffold.dart) — a `CustomScrollView` with a pull-down "sync status" header (Google -Photos-style), a `Scrollbar`, and a classic Material refresh spinner shown -during a pull-triggered sync. `SearchView`, `AccountView` (Settings), the +Photos-style) and a classic Material refresh spinner shown during a +pull-triggered sync. `SearchView`, `AccountView` (Settings), the file-details sheet, the share sheet, and `ShareUploadView` (the share-to-upload destination picker) are pushed on top via `Navigator`/`showModalBottomSheet`/`showGradualBottomSheet` rather than diff --git a/.claude/context/styling.md b/.claude/context/styling.md index a670fd2..eb4be58 100644 --- a/.claude/context/styling.md +++ b/.claude/context/styling.md @@ -29,13 +29,10 @@ widgets. Key points: - Dark theme supports an `amoled` flag that flattens every surface tone to pure black — extend `colorScheme.copyWith(...)` there if a new surface role needs the same treatment, don't hardcode `Colors.black` at call sites. -- **Scrollbars**: a project-wide `scrollbarTheme` (`AppTheme._scrollbarTheme`) - gives every `Scrollbar` in the app a thick, rounded, always-visible, - draggable thumb (Android fast-scroll style) derived from - `colorScheme.onSurfaceVariant` — don't pass per-instance `thickness`/ - `radius`/`thumbVisibility`/`interactive`, just wrap scrollable content in a - plain `Scrollbar(child: ...)` (pass `controller:` matching the scrollable's - own when one exists) and it picks up the theme automatically. +- **Scrollbars are deliberately not shown** — every scrollable list in the + app is a plain `ListView`/`CustomScrollView` with no `Scrollbar` wrapper + and no `scrollbarTheme` override (tried once, explicitly reverted). Don't + reintroduce one without being asked. ## Reusable chrome diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index 2754cce..fe42c81 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -30,37 +30,6 @@ class AppTheme { Color(0xFF2E7D32), // Emerald Green ]; - // Flutter's default Scrollbar renders a ~6px hairline that's easy to miss - // and hard to grab. This tunes it to read like Android's standard - // draggable fast-scroll thumb: thicker, pill-shaped, and always at least - // faintly visible (not just flashing in on scroll), while still getting - // a touch more visible/opaque while actively hovered or dragged. - static ScrollbarThemeData _scrollbarTheme(ColorScheme colorScheme) { - final thumbColor = colorScheme.onSurfaceVariant; - return ScrollbarThemeData( - thickness: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.dragged) || - states.contains(WidgetState.hovered)) { - return 10.0; - } - return 8.0; - }), - radius: const Radius.circular(8), - thumbColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.dragged)) { - return thumbColor.withValues(alpha: 0.9); - } - if (states.contains(WidgetState.hovered)) { - return thumbColor.withValues(alpha: 0.8); - } - return thumbColor.withValues(alpha: 0.6); - }), - thumbVisibility: const WidgetStatePropertyAll(true), - trackVisibility: const WidgetStatePropertyAll(false), - interactive: true, - ); - } - static ThemeData light( Color seedColor, { ColorScheme? dynamicScheme, @@ -80,7 +49,6 @@ class AppTheme { scaffoldBackgroundColor: colorScheme.surface, pageTransitionsTheme: _pageTransitionsTheme, sliderTheme: _sliderTheme, - scrollbarTheme: _scrollbarTheme(colorScheme), textTheme: GoogleFonts.interTextTheme(ThemeData.light().textTheme) .copyWith( headlineMedium: GoogleFonts.inter( @@ -147,7 +115,6 @@ class AppTheme { scaffoldBackgroundColor: colorScheme.surface, pageTransitionsTheme: _pageTransitionsTheme, sliderTheme: _sliderTheme, - scrollbarTheme: _scrollbarTheme(colorScheme), textTheme: GoogleFonts.interTextTheme(ThemeData.dark().textTheme) .copyWith( headlineMedium: GoogleFonts.inter( diff --git a/lib/views/account_view.dart b/lib/views/account_view.dart index 803bd59..caadbdd 100644 --- a/lib/views/account_view.dart +++ b/lib/views/account_view.dart @@ -25,423 +25,428 @@ class AccountView extends StatelessWidget { final quota = provider.quota; return Scaffold( - appBar: AppBar( - title: const Text('Settings'), - actions: [ - IconButton( - onPressed: () => _showLogoutConfirmation(context, provider), - icon: const Icon(Icons.logout_rounded), - tooltip: 'Log Out', - color: colorScheme.error, - ), - ], - ), + appBar: AppBar(title: const Text('Settings')), body: SafeArea( - child: Scrollbar( - child: ListView( - padding: const EdgeInsets.fromLTRB(20, 8, 20, 32), - physics: const BouncingScrollPhysics(), - children: [ - Text( - 'Connected to ${provider.serverUrl}', - style: theme.textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, + child: ListView( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 32), + physics: const BouncingScrollPhysics(), + children: [ + // User Profile & Storage Card + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: colorScheme.primaryContainer.withValues(alpha: 0.4), + borderRadius: BorderRadius.circular(28), + border: Border.all( + color: colorScheme.primary.withValues(alpha: 0.2), ), ), - const SizedBox(height: 20), - - // User Profile & Storage Card - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: colorScheme.primaryContainer.withValues(alpha: 0.4), - borderRadius: BorderRadius.circular(28), - border: Border.all( - color: colorScheme.primary.withValues(alpha: 0.2), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + CircleAvatar( + radius: 28, + backgroundColor: colorScheme.primary, + child: Text( + (quota?.userName ?? provider.username) + .substring(0, 1) + .toUpperCase(), + style: theme.textTheme.headlineSmall?.copyWith( + color: colorScheme.onPrimary, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + quota?.userName ?? provider.username, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 2), + Text( + quota?.email ?? provider.username, + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + IconButton( + onPressed: () => + _showLogoutConfirmation(context, provider), + icon: const Icon(Icons.logout_rounded), + tooltip: 'Log Out', + color: colorScheme.error, + ), + ], ), - ), + const SizedBox(height: 20), + const Divider(height: 1), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Storage Quota', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + Text( + quota != null + ? (quota.totalBytes > 0 + ? '${_formatBytes(quota.usedBytes)} of ${_formatBytes(quota.totalBytes)}' + : '${_formatBytes(quota.usedBytes)} (Unlimited)') + : 'Live Server Storage', + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular(6), + child: LinearProgressIndicator( + value: quota != null && quota.totalBytes > 0 + ? quota.usagePercentage + : 0.1, + minHeight: 10, + backgroundColor: colorScheme.surfaceContainerHighest, + valueColor: AlwaysStoppedAnimation( + colorScheme.primary, + ), + ), + ), + const SizedBox(height: 8), + Text( + quota != null + ? (quota.totalBytes > 0 + ? '${(quota.usagePercentage * 100).toStringAsFixed(1)}% used' + : 'Unlimited Storage Plan') + : 'WebDAV connection active', + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + fontSize: 11, + ), + ), + const SizedBox(height: 16), + const Divider(height: 1), + const SizedBox(height: 12), + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 4, + ), + decoration: BoxDecoration( + color: provider.isLoggedIn + ? Colors.green.withValues(alpha: 0.2) + : Colors.red.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + provider.isLoggedIn ? 'ONLINE' : 'OFFLINE', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + color: provider.isLoggedIn + ? Colors.green.shade800 + : Colors.red.shade800, + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Connected to ${provider.serverUrl}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 24), + + // Accounts Section + Text( + 'Accounts', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 10), + const _AccountsCard(), + const SizedBox(height: 24), + + // Server Credentials Section + Text( + 'Server Connection Info', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 10), + Card( + child: Column( + children: [ + ListTile( + leading: const Icon(Icons.dns_rounded), + title: const Text('Nextcloud Host'), + subtitle: Text(provider.serverUrl), + ), + const Divider(height: 1, indent: 16, endIndent: 16), + ListTile( + leading: const Icon(Icons.person_rounded), + title: const Text('Logged In User'), + subtitle: Text(provider.username), + ), + const Divider(height: 1, indent: 16, endIndent: 16), + ListTile( + leading: const Icon(Icons.refresh_rounded), + title: const Text('Refresh WebDAV Cache'), + onTap: () async { + await provider.refreshData(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Refreshed WebDAV directory data'), + behavior: SnackBarBehavior.floating, + ), + ); + } + }, + ), + ], + ), + ), + const SizedBox(height: 24), + + // Material You Design Settings + Text( + 'Material You Aesthetics', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 10), + Card( + child: Padding( + padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text( + 'Accent Color', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + provider.useDynamicColor + ? 'Matching your wallpaper' + : 'Custom color', + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 12), Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - CircleAvatar( - radius: 28, - backgroundColor: colorScheme.primary, - child: Text( - (quota?.userName ?? provider.username) - .substring(0, 1) - .toUpperCase(), - style: theme.textTheme.headlineSmall?.copyWith( - color: colorScheme.onPrimary, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - quota?.userName ?? provider.username, - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: 2), - Text( - quota?.email ?? provider.username, - style: theme.textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 4, - ), - decoration: BoxDecoration( - color: provider.isLoggedIn - ? Colors.green.withValues(alpha: 0.2) - : Colors.red.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - provider.isLoggedIn ? 'ONLINE' : 'OFFLINE', - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.bold, - color: provider.isLoggedIn - ? Colors.green.shade800 - : Colors.red.shade800, - ), + _AccentSwatch( + isSelected: provider.useDynamicColor, + onTap: () => provider.setUseDynamicColor(true), + borderColor: colorScheme.onSurface, + background: colorScheme.surfaceContainerHighest, + child: Icon( + Icons.wallpaper_rounded, + color: colorScheme.onSurfaceVariant, + size: 20, ), ), + ...AppTheme.seedColors.map((color) { + final isSelected = + !provider.useDynamicColor && + provider.seedColor == color; + return _AccentSwatch( + isSelected: isSelected, + onTap: () => provider.setSeedColor(color), + borderColor: colorScheme.onSurface, + background: color, + child: isSelected + ? const Icon( + Icons.check_rounded, + color: Colors.white, + size: 20, + ) + : null, + ); + }), ], ), const SizedBox(height: 20), const Divider(height: 1), const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Storage Quota', - style: theme.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - ), - ), - Text( - quota != null - ? (quota.totalBytes > 0 - ? '${_formatBytes(quota.usedBytes)} of ${_formatBytes(quota.totalBytes)}' - : '${_formatBytes(quota.usedBytes)} (Unlimited)') - : 'Live Server Storage', - style: theme.textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - fontWeight: FontWeight.w500, - ), - ), - ], + Text( + 'Theme Mode', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), ), - const SizedBox(height: 10), - ClipRRect( - borderRadius: BorderRadius.circular(6), - child: LinearProgressIndicator( - value: quota != null && quota.totalBytes > 0 - ? quota.usagePercentage - : 0.1, - minHeight: 10, - backgroundColor: colorScheme.surfaceContainerHighest, - valueColor: AlwaysStoppedAnimation( - colorScheme.primary, - ), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: SegmentedButton( + segments: const [ + ButtonSegment( + value: ThemeMode.system, + icon: Icon(Icons.brightness_auto_rounded), + label: Text('System'), + ), + ButtonSegment( + value: ThemeMode.light, + icon: Icon(Icons.light_mode_rounded), + label: Text('Light'), + ), + ButtonSegment( + value: ThemeMode.dark, + icon: Icon(Icons.dark_mode_rounded), + label: Text('Dark'), + ), + ], + selected: {provider.themeMode}, + onSelectionChanged: (set) => + provider.setThemeMode(set.first), ), ), const SizedBox(height: 8), - Text( - quota != null - ? (quota.totalBytes > 0 - ? '${(quota.usagePercentage * 100).toStringAsFixed(1)}% used' - : 'Unlimited Storage Plan') - : 'WebDAV connection active', - style: theme.textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - fontSize: 11, + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: const Text('AMOLED Black'), + subtitle: const Text( + 'Use pure black backgrounds in dark mode', ), + value: provider.amoledDark, + onChanged: provider.setAmoledDark, ), ], ), ), - const SizedBox(height: 24), + ), + const SizedBox(height: 24), - // Accounts Section - Text( - 'Accounts', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w700, - ), + // UI Settings + Text( + 'UI', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, ), - const SizedBox(height: 10), - const _AccountsCard(), - const SizedBox(height: 24), + ), + 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), - // Server Credentials Section - Text( - 'Server Connection Info', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w700, - ), + // Bottom Nav Tabs + Text( + 'Bottom Bar Tabs', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, ), - const SizedBox(height: 10), - Card( - child: Column( - children: [ - ListTile( - leading: const Icon(Icons.dns_rounded), - title: const Text('Nextcloud Host'), - subtitle: Text(provider.serverUrl), - ), - const Divider(height: 1, indent: 16, endIndent: 16), - ListTile( - leading: const Icon(Icons.person_rounded), - title: const Text('Logged In User'), - subtitle: Text(provider.username), - ), - const Divider(height: 1, indent: 16, endIndent: 16), - ListTile( - leading: const Icon(Icons.refresh_rounded), - title: const Text('Refresh WebDAV Cache'), - onTap: () async { - await provider.refreshData(); - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Refreshed WebDAV directory data'), - behavior: SnackBarBehavior.floating, - ), - ); - } - }, - ), - ], - ), + ), + const SizedBox(height: 4), + Text( + 'Drag to reorder, tap the star to set the default, toggle to show or hide', + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, ), - const SizedBox(height: 24), + ), + const SizedBox(height: 10), + const _TabSettingsCard(), + const SizedBox(height: 24), - // Material You Design Settings - Text( - 'Material You Aesthetics', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w700, - ), + // Swipe Actions + Text( + 'Files Swipe Actions', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, ), - const SizedBox(height: 10), - Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Accent Color', - style: theme.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 4), - Text( - provider.useDynamicColor - ? 'Matching your wallpaper' - : 'Custom color', - style: theme.textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _AccentSwatch( - isSelected: provider.useDynamicColor, - onTap: () => provider.setUseDynamicColor(true), - borderColor: colorScheme.onSurface, - background: colorScheme.surfaceContainerHighest, - child: Icon( - Icons.wallpaper_rounded, - color: colorScheme.onSurfaceVariant, - size: 20, - ), - ), - ...AppTheme.seedColors.map((color) { - final isSelected = - !provider.useDynamicColor && - provider.seedColor == color; - return _AccentSwatch( - isSelected: isSelected, - onTap: () => provider.setSeedColor(color), - borderColor: colorScheme.onSurface, - background: color, - child: isSelected - ? const Icon( - Icons.check_rounded, - color: Colors.white, - size: 20, - ) - : null, - ); - }), - ], - ), - const SizedBox(height: 20), - const Divider(height: 1), - const SizedBox(height: 16), - Text( - 'Theme Mode', - style: theme.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 12), - SizedBox( - width: double.infinity, - child: SegmentedButton( - segments: const [ - ButtonSegment( - value: ThemeMode.system, - icon: Icon(Icons.brightness_auto_rounded), - label: Text('System'), - ), - ButtonSegment( - value: ThemeMode.light, - icon: Icon(Icons.light_mode_rounded), - label: Text('Light'), - ), - ButtonSegment( - value: ThemeMode.dark, - icon: Icon(Icons.dark_mode_rounded), - label: Text('Dark'), - ), - ], - selected: {provider.themeMode}, - onSelectionChanged: (set) => - provider.setThemeMode(set.first), - ), - ), - const SizedBox(height: 8), - SwitchListTile( - contentPadding: EdgeInsets.zero, - title: const Text('AMOLED Black'), - subtitle: const Text( - 'Use pure black backgrounds in dark mode', - ), - value: provider.amoledDark, - onChanged: provider.setAmoledDark, - ), - ], - ), - ), + ), + const SizedBox(height: 4), + Text( + 'Choose what swiping a file left or right does in list view', + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, ), - const SizedBox(height: 24), + ), + const SizedBox(height: 10), + const _SwipeActionsCard(), + const SizedBox(height: 24), - // UI Settings - Text( - 'UI', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w700, - ), + // Media Player + Text( + 'Media Player', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, ), - 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: 4), + Text( + 'Choose the seek bar style used when playing videos', + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, ), - const SizedBox(height: 24), + ), + const SizedBox(height: 10), + const _MediaPlayerCard(), + const SizedBox(height: 24), - // Bottom Nav Tabs - Text( - 'Bottom Bar Tabs', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w700, - ), + // Files Cache + Text( + 'Files Cache', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, ), - const SizedBox(height: 4), - Text( - 'Drag to reorder, tap the star to set the default, toggle to show or hide', - style: theme.textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + ), + const SizedBox(height: 4), + Text( + 'Reuse a folder\'s listing instead of refetching it on every visit', + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, ), - const SizedBox(height: 10), - const _TabSettingsCard(), - const SizedBox(height: 24), - - // Swipe Actions - Text( - 'Files Swipe Actions', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: 4), - Text( - 'Choose what swiping a file left or right does in list view', - style: theme.textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 10), - const _SwipeActionsCard(), - const SizedBox(height: 24), - - // Media Player - Text( - 'Media Player', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: 4), - Text( - 'Choose the seek bar style used when playing videos', - style: theme.textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 10), - const _MediaPlayerCard(), - const SizedBox(height: 24), - - // Files Cache - Text( - 'Files Cache', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: 4), - Text( - 'Reuse a folder\'s listing instead of refetching it on every visit', - style: theme.textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 10), - const _CacheSettingsCard(), - ], - ), + ), + const SizedBox(height: 10), + const _CacheSettingsCard(), + ], ), ), ); diff --git a/lib/views/search_view.dart b/lib/views/search_view.dart index da006bc..662c9bc 100644 --- a/lib/views/search_view.dart +++ b/lib/views/search_view.dart @@ -18,7 +18,6 @@ class SearchView extends StatefulWidget { class _SearchViewState extends State { final _controller = TextEditingController(); final _focusNode = FocusNode(); - final _resultsScrollController = ScrollController(); Timer? _debounce; List _results = []; @@ -31,7 +30,6 @@ class _SearchViewState extends State { _debounce?.cancel(); _controller.dispose(); _focusNode.dispose(); - _resultsScrollController.dispose(); super.dispose(); } @@ -187,32 +185,24 @@ class _SearchViewState extends State { ), ); } - return Scrollbar( - controller: _resultsScrollController, - child: ListView.builder( - controller: _resultsScrollController, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - itemCount: _results.length, - itemBuilder: (context, index) { - final item = _results[index]; - return ListTile( - leading: Icon(_iconFor(item.type), color: colorScheme.primary), - title: Text( - item.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text( - item.isFolder - ? item.path - : DateFormat.yMMMd().format(item.lastModified), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - onTap: () => _openResult(item), - ); - }, - ), + return ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + itemCount: _results.length, + itemBuilder: (context, index) { + final item = _results[index]; + return ListTile( + leading: Icon(_iconFor(item.type), color: colorScheme.primary), + title: Text(item.name, maxLines: 1, overflow: TextOverflow.ellipsis), + subtitle: Text( + item.isFolder + ? item.path + : DateFormat.yMMMd().format(item.lastModified), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + onTap: () => _openResult(item), + ); + }, ); } } diff --git a/lib/views/share_upload_view.dart b/lib/views/share_upload_view.dart index 737a6e0..1df4faf 100644 --- a/lib/views/share_upload_view.dart +++ b/lib/views/share_upload_view.dart @@ -142,25 +142,21 @@ class _ShareUploadViewState extends State { ), ), ) - : Scrollbar( - child: ListView.builder( - itemCount: folders.length, - itemBuilder: (context, index) { - final folder = folders[index]; - return ListTile( - leading: Icon( - Icons.folder_rounded, - color: colorScheme.primary, - ), - title: Text(folder.name), - trailing: const Icon( - Icons.chevron_right_rounded, - ), - onTap: () => - provider.navigateToFolder(folder.path), - ); - }, - ), + : ListView.builder( + itemCount: folders.length, + itemBuilder: (context, index) { + final folder = folders[index]; + return ListTile( + leading: Icon( + Icons.folder_rounded, + color: colorScheme.primary, + ), + title: Text(folder.name), + trailing: const Icon(Icons.chevron_right_rounded), + onTap: () => + provider.navigateToFolder(folder.path), + ); + }, ), ), ], diff --git a/lib/widgets/details/details_activity_tab.dart b/lib/widgets/details/details_activity_tab.dart index 02bdcf3..1238752 100644 --- a/lib/widgets/details/details_activity_tab.dart +++ b/lib/widgets/details/details_activity_tab.dart @@ -53,92 +53,90 @@ class _DetailsActivityTabState extends State { ); } - return Scrollbar( - child: ListView.builder( - padding: const EdgeInsets.fromLTRB(20, 12, 20, 24), - itemCount: _activities.length, - itemBuilder: (context, index) { - final act = _activities[index]; - return Padding( - padding: const EdgeInsets.only(bottom: 12), - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: colorScheme.surfaceContainerLow, - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: colorScheme.outlineVariant.withValues(alpha: 0.2), - ), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: colorScheme.primaryContainer, - shape: BoxShape.circle, - ), - child: Icon( - act.icon, - color: colorScheme.onPrimaryContainer, - size: 20, - ), - ), - const SizedBox(width: 14), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Text( - act.title, - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - fontSize: 14, - ), - ), - ), - const SizedBox(width: 8), - Text( - DateFormat.jm().format(act.timestamp), - style: theme.textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - fontSize: 11, - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - act.subject, - style: theme.textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - fontSize: 13, - ), - ), - const SizedBox(height: 6), - Text( - 'By ${act.author} • ${DateFormat.MMMd().format(act.timestamp)}', - style: theme.textTheme.bodySmall?.copyWith( - color: colorScheme.primary, - fontWeight: FontWeight.w500, - fontSize: 11, - ), - ), - ], - ), - ), - ], + return ListView.builder( + padding: const EdgeInsets.fromLTRB(20, 12, 20, 24), + itemCount: _activities.length, + itemBuilder: (context, index) { + final act = _activities[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerLow, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: colorScheme.outlineVariant.withValues(alpha: 0.2), ), ), - ); - }, - ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: colorScheme.primaryContainer, + shape: BoxShape.circle, + ), + child: Icon( + act.icon, + color: colorScheme.onPrimaryContainer, + size: 20, + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + act.title, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + ), + const SizedBox(width: 8), + Text( + DateFormat.jm().format(act.timestamp), + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + fontSize: 11, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + act.subject, + style: theme.textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + fontSize: 13, + ), + ), + const SizedBox(height: 6), + Text( + 'By ${act.author} • ${DateFormat.MMMd().format(act.timestamp)}', + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.primary, + fontWeight: FontWeight.w500, + fontSize: 11, + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, ); } } diff --git a/lib/widgets/details/details_info_tab.dart b/lib/widgets/details/details_info_tab.dart index 64a5562..821441c 100644 --- a/lib/widgets/details/details_info_tab.dart +++ b/lib/widgets/details/details_info_tab.dart @@ -26,42 +26,39 @@ class DetailsInfoTab extends StatelessWidget { ('Favorite', item.isFavorite ? 'Yes' : 'No'), ]; - return Scrollbar( + return ListView.separated( controller: scrollController, - child: ListView.separated( - controller: scrollController, - padding: const EdgeInsets.fromLTRB(20, 12, 20, 24), - itemCount: rows.length, - separatorBuilder: (context, index) => const Divider(height: 1), - itemBuilder: (context, index) { - final (label, value) = rows[index]; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 96, - child: Text( - label, - style: theme.textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + padding: const EdgeInsets.fromLTRB(20, 12, 20, 24), + itemCount: rows.length, + separatorBuilder: (context, index) => const Divider(height: 1), + itemBuilder: (context, index) { + final (label, value) = rows[index]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 96, + child: Text( + label, + style: theme.textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, ), ), - Expanded( - child: Text( - value, - style: theme.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w500, - ), + ), + Expanded( + child: Text( + value, + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, ), ), - ], - ), - ); - }, - ), + ), + ], + ), + ); + }, ); } } diff --git a/lib/widgets/details/details_versions_tab.dart b/lib/widgets/details/details_versions_tab.dart index a38aaf3..c276d82 100644 --- a/lib/widgets/details/details_versions_tab.dart +++ b/lib/widgets/details/details_versions_tab.dart @@ -118,45 +118,43 @@ class _DetailsVersionsTabState extends State { ..._versions, ]; - return Scrollbar( - child: ListView.separated( - padding: const EdgeInsets.fromLTRB(20, 12, 20, 24), - itemCount: items.length, - separatorBuilder: (context, index) => const SizedBox(height: 4), - itemBuilder: (context, index) { - final version = items[index]; - return ListTile( - contentPadding: EdgeInsets.zero, - leading: CircleAvatar( - backgroundColor: colorScheme.primaryContainer, - child: Icon( - Icons.history_rounded, - color: colorScheme.onPrimaryContainer, - size: 20, - ), + return ListView.separated( + padding: const EdgeInsets.fromLTRB(20, 12, 20, 24), + itemCount: items.length, + separatorBuilder: (context, index) => const SizedBox(height: 4), + itemBuilder: (context, index) { + final version = items[index]; + return ListTile( + contentPadding: EdgeInsets.zero, + leading: CircleAvatar( + backgroundColor: colorScheme.primaryContainer, + child: Icon( + Icons.history_rounded, + color: colorScheme.onPrimaryContainer, + size: 20, ), - title: Text( - version.isCurrent - ? 'Current version' - : DateFormat.yMMMd().add_jm().format(version.timestamp), - ), - subtitle: Text( - '${DateFormat.yMMMd().format(version.timestamp)} • ${formatBytes(version.size)}', - ), - trailing: version.isCurrent - ? null - : PopupMenuButton( - onSelected: (value) => value == 'restore' - ? _restore(version) - : _download(version), - itemBuilder: (context) => const [ - PopupMenuItem(value: 'restore', child: Text('Restore')), - PopupMenuItem(value: 'download', child: Text('Download')), - ], - ), - ); - }, - ), + ), + title: Text( + version.isCurrent + ? 'Current version' + : DateFormat.yMMMd().add_jm().format(version.timestamp), + ), + subtitle: Text( + '${DateFormat.yMMMd().format(version.timestamp)} • ${formatBytes(version.size)}', + ), + trailing: version.isCurrent + ? null + : PopupMenuButton( + onSelected: (value) => value == 'restore' + ? _restore(version) + : _download(version), + itemBuilder: (context) => const [ + PopupMenuItem(value: 'restore', child: Text('Restore')), + PopupMenuItem(value: 'download', child: Text('Download')), + ], + ), + ); + }, ); } } diff --git a/lib/widgets/share_sheet.dart b/lib/widgets/share_sheet.dart index c2df25c..a9267a7 100644 --- a/lib/widgets/share_sheet.dart +++ b/lib/widgets/share_sheet.dart @@ -299,115 +299,108 @@ class _ShareSheetState extends State { .toList(); final internalLink = internalLinkFor(provider.serverUrl, widget.item.id); - return Scrollbar( + return ListView( controller: widget.scrollController, - child: ListView( - controller: widget.scrollController, - padding: const EdgeInsets.fromLTRB(20, 12, 20, 24), - children: [ - Text( - widget.item.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w700, - ), + padding: const EdgeInsets.fromLTRB(20, 12, 20, 24), + children: [ + Text( + widget.item.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, ), - const SizedBox(height: 16), - _sectionHeader('Internal shares'), - const SizedBox(height: 8), - TextField( - controller: _searchController, - decoration: InputDecoration( - hintText: 'Type names or teams', - prefixIcon: const Icon(Icons.search_rounded), - suffixIcon: _isSearching - ? const Padding( - padding: EdgeInsets.all(14), - child: SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ), - ) - : null, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - ), - filled: true, - fillColor: colorScheme.surfaceContainerLow, - ), - onChanged: _search, + ), + const SizedBox(height: 16), + _sectionHeader('Internal shares'), + const SizedBox(height: 8), + TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: 'Type names or teams', + prefixIcon: const Icon(Icons.search_rounded), + suffixIcon: _isSearching + ? const Padding( + padding: EdgeInsets.all(14), + child: SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ) + : null, + border: OutlineInputBorder(borderRadius: BorderRadius.circular(16)), + filled: true, + fillColor: colorScheme.surfaceContainerLow, ), - for (final sharee in _searchResults) - ListTile( - contentPadding: EdgeInsets.zero, - leading: Icon( - sharee.type == ShareeType.group - ? Icons.groups_rounded - : Icons.person_rounded, - ), - title: Text(sharee.label), - subtitle: sharee.subtitle != null ? Text(sharee.subtitle!) : null, - onTap: () => _addSharee(sharee), + onChanged: _search, + ), + for (final sharee in _searchResults) + ListTile( + contentPadding: EdgeInsets.zero, + leading: Icon( + sharee.type == ShareeType.group + ? Icons.groups_rounded + : Icons.person_rounded, ), - for (final share in userGroupShares) _shareRow(share), - if (_inherited.isNotEmpty) - ExpansionTile( - tilePadding: EdgeInsets.zero, - leading: const Icon(Icons.more_horiz_rounded), - title: Text('Others with access (${_inherited.length})'), - children: [ - for (final share in _inherited) - _shareRow(share, removable: false), - ], - ), - if (internalLink != null) - ListTile( - contentPadding: EdgeInsets.zero, - leading: const Icon(Icons.link_rounded), - title: const Text('Internal link'), - subtitle: const Text('For people who already have access'), - trailing: IconButton( - icon: const Icon(Icons.copy_rounded), - tooltip: 'Copy link', - onPressed: () => _copy(internalLink, 'internal link'), - ), - ), - const SizedBox(height: 20), - const Divider(), - const SizedBox(height: 12), - _sectionHeader('External shares'), - const SizedBox(height: 8), - TextField( - controller: _emailController, - keyboardType: TextInputType.emailAddress, - decoration: InputDecoration( - hintText: 'Type an email', - prefixIcon: const Icon(Icons.email_outlined), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - ), - filled: true, - fillColor: colorScheme.surfaceContainerLow, - ), - onSubmitted: (_) => _addEmailShare(), + title: Text(sharee.label), + subtitle: sharee.subtitle != null ? Text(sharee.subtitle!) : null, + onTap: () => _addSharee(sharee), ), - for (final share in emailShares) _shareRow(share), - const SizedBox(height: 8), - for (final share in publicLinkShares) _shareRow(share, copyUrl: true), + for (final share in userGroupShares) _shareRow(share), + if (_inherited.isNotEmpty) + ExpansionTile( + tilePadding: EdgeInsets.zero, + leading: const Icon(Icons.more_horiz_rounded), + title: Text('Others with access (${_inherited.length})'), + children: [ + for (final share in _inherited) + _shareRow(share, removable: false), + ], + ), + if (internalLink != null) ListTile( contentPadding: EdgeInsets.zero, leading: const Icon(Icons.link_rounded), - title: const Text('Create public link'), + title: const Text('Internal link'), + subtitle: const Text('For people who already have access'), trailing: IconButton( - icon: const Icon(Icons.add_rounded), - tooltip: 'Create public link', - onPressed: _createPublicLink, + icon: const Icon(Icons.copy_rounded), + tooltip: 'Copy link', + onPressed: () => _copy(internalLink, 'internal link'), ), ), - ], - ), + const SizedBox(height: 20), + const Divider(), + const SizedBox(height: 12), + _sectionHeader('External shares'), + const SizedBox(height: 8), + TextField( + controller: _emailController, + keyboardType: TextInputType.emailAddress, + decoration: InputDecoration( + hintText: 'Type an email', + prefixIcon: const Icon(Icons.email_outlined), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(16)), + filled: true, + fillColor: colorScheme.surfaceContainerLow, + ), + onSubmitted: (_) => _addEmailShare(), + ), + for (final share in emailShares) _shareRow(share), + const SizedBox(height: 8), + for (final share in publicLinkShares) _shareRow(share, copyUrl: true), + ListTile( + contentPadding: EdgeInsets.zero, + leading: const Icon(Icons.link_rounded), + title: const Text('Create public link'), + trailing: IconButton( + icon: const Icon(Icons.add_rounded), + tooltip: 'Create public link', + onPressed: _createPublicLink, + ), + ), + ], ); } } diff --git a/lib/widgets/synced_header_scaffold.dart b/lib/widgets/synced_header_scaffold.dart index c09a78a..0a28e88 100644 --- a/lib/widgets/synced_header_scaffold.dart +++ b/lib/widgets/synced_header_scaffold.dart @@ -161,50 +161,47 @@ class _SyncedHeaderScaffoldState extends State { children: [ NotificationListener( onNotification: _handleScrollNotification, - child: Scrollbar( + child: CustomScrollView( controller: widget.scrollController, - child: CustomScrollView( - controller: widget.scrollController, - // Plain BouncingScrollPhysics only bounces/overscrolls - // reliably once content already fills the viewport — with - // too little content (e.g. a single item) the pull gesture - // can fail to register at all. AlwaysScrollableScrollPhysics - // keeps the pull (and therefore the sync header) working - // regardless of content length. - physics: const AlwaysScrollableScrollPhysics( - parent: BouncingScrollPhysics(), - ), - slivers: [ - SliverAppBar( - pinned: true, - stretch: true, - expandedHeight: _headerLocked ? 190 : kToolbarHeight, - collapsedHeight: kToolbarHeight, - backgroundColor: colorScheme.surfaceContainer, - surfaceTintColor: colorScheme.surfaceContainer, - scrolledUnderElevation: 0, - automaticallyImplyLeading: false, - leadingWidth: _headerLocked ? 56 : 160, - leading: leadingWidget, - actions: widget.actions, - flexibleSpace: FlexibleSpaceBar( - background: _SyncedStretchPanel( - provider: provider, - forceVisible: _headerLocked, - ), - ), - ), - DecoratedSliver( - decoration: BoxDecoration( - color: colorScheme.surface, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(28), - ), - ), - sliver: SliverMainAxisGroup(slivers: widget.contentSlivers), - ), - ], + // Plain BouncingScrollPhysics only bounces/overscrolls + // reliably once content already fills the viewport — with + // too little content (e.g. a single item) the pull gesture + // can fail to register at all. AlwaysScrollableScrollPhysics + // keeps the pull (and therefore the sync header) working + // regardless of content length. + physics: const AlwaysScrollableScrollPhysics( + parent: BouncingScrollPhysics(), ), + slivers: [ + SliverAppBar( + pinned: true, + stretch: true, + expandedHeight: _headerLocked ? 190 : kToolbarHeight, + collapsedHeight: kToolbarHeight, + backgroundColor: colorScheme.surfaceContainer, + surfaceTintColor: colorScheme.surfaceContainer, + scrolledUnderElevation: 0, + automaticallyImplyLeading: false, + leadingWidth: _headerLocked ? 56 : 160, + leading: leadingWidget, + actions: widget.actions, + flexibleSpace: FlexibleSpaceBar( + background: _SyncedStretchPanel( + provider: provider, + forceVisible: _headerLocked, + ), + ), + ), + DecoratedSliver( + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(28), + ), + ), + sliver: SliverMainAxisGroup(slivers: widget.contentSlivers), + ), + ], ), ), // The classic Material pull-to-refresh "bubble" — a floating