Dedupe native transfer/notification code and split ServerProvider into focused controllers
Build APK / build (push) Successful in 5m20s
Build APK / build (push) Successful in 5m20s
Consolidates duplicated GET/PUT/notification-channel logic across DownloadService/ShareUploadService/SyncEngine into shared Kotlin helpers, gives upload/download real batch queueing instead of dropping a second concurrent batch, and dedupes repeated Dart channel-argument boilerplate. Replaces the 2300+ line ServerProvider god object with ten focused ChangeNotifiers (SessionController, SettingsController, FilesController, PhotosController, FavoritesController, TrashController, SharesController, RecentController, SyncStatusController, PickController) plus ItemOperations, a plain coordinator for cross-domain item mutations - fixing the coupling where device-sync status, per-tab data, and global UI prefs all lived in one object. Updates every view/widget call site accordingly and refreshes the architecture/server/standards/styling docs to match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+112
-103
@@ -2,7 +2,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/app_tab.dart';
|
||||
import '../models/saved_account.dart';
|
||||
import '../providers/server_provider.dart';
|
||||
import '../providers/files_controller.dart';
|
||||
import '../providers/session_controller.dart';
|
||||
import '../providers/settings_controller.dart';
|
||||
import '../providers/sync_status_controller.dart';
|
||||
import '../services/sync_service.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import '../widgets/frosted_glass_container.dart';
|
||||
@@ -17,8 +20,10 @@ class AccountView extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final provider = context.watch<ServerProvider>();
|
||||
final quota = provider.quota;
|
||||
final session = context.watch<SessionController>();
|
||||
final settings = context.watch<SettingsController>();
|
||||
final files = context.watch<FilesController>();
|
||||
final quota = files.quota;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Settings')),
|
||||
@@ -46,7 +51,7 @@ class AccountView extends StatelessWidget {
|
||||
radius: 28,
|
||||
backgroundColor: colorScheme.primary,
|
||||
child: Text(
|
||||
(quota?.userName ?? provider.username)
|
||||
(quota?.userName ?? session.username)
|
||||
.substring(0, 1)
|
||||
.toUpperCase(),
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
@@ -61,14 +66,14 @@ class AccountView extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
quota?.userName ?? provider.username,
|
||||
quota?.userName ?? session.username,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
quota?.email ?? provider.username,
|
||||
quota?.email ?? session.username,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
@@ -119,8 +124,8 @@ class AccountView extends StatelessWidget {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
Uri.tryParse(provider.serverUrl)?.host ??
|
||||
provider.serverUrl,
|
||||
Uri.tryParse(session.serverUrl)?.host ??
|
||||
session.serverUrl,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
@@ -185,7 +190,7 @@ class AccountView extends StatelessWidget {
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
tooltip: 'Refresh cached data',
|
||||
onPressed: () async {
|
||||
await provider.refreshData();
|
||||
await files.refreshData();
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
@@ -200,14 +205,14 @@ class AccountView extends StatelessWidget {
|
||||
icon: const Icon(Icons.logout_rounded),
|
||||
tooltip: 'Logout',
|
||||
color: Colors.orange.shade700,
|
||||
onPressed: () => _handleLogout(context, provider),
|
||||
onPressed: () => _handleLogout(context, session),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outline_rounded),
|
||||
tooltip: 'Remove account',
|
||||
color: colorScheme.error,
|
||||
onPressed: () =>
|
||||
_confirmRemoveActive(context, provider),
|
||||
_confirmRemoveActive(context, session),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -271,7 +276,7 @@ class AccountView extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
provider.useDynamicColor
|
||||
settings.useDynamicColor
|
||||
? 'Matching your wallpaper'
|
||||
: 'Custom color',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
@@ -283,8 +288,8 @@ class AccountView extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_AccentSwatch(
|
||||
isSelected: provider.useDynamicColor,
|
||||
onTap: () => provider.setUseDynamicColor(true),
|
||||
isSelected: settings.useDynamicColor,
|
||||
onTap: () => settings.setUseDynamicColor(true),
|
||||
borderColor: colorScheme.onSurface,
|
||||
background: colorScheme.surfaceContainerHighest,
|
||||
child: Icon(
|
||||
@@ -295,11 +300,11 @@ class AccountView extends StatelessWidget {
|
||||
),
|
||||
...AppTheme.seedColors.map((color) {
|
||||
final isSelected =
|
||||
!provider.useDynamicColor &&
|
||||
provider.seedColor == color;
|
||||
!settings.useDynamicColor &&
|
||||
settings.seedColor == color;
|
||||
return _AccentSwatch(
|
||||
isSelected: isSelected,
|
||||
onTap: () => provider.setSeedColor(color),
|
||||
onTap: () => settings.setSeedColor(color),
|
||||
borderColor: colorScheme.onSurface,
|
||||
background: color,
|
||||
child: isSelected
|
||||
@@ -343,9 +348,9 @@ class AccountView extends StatelessWidget {
|
||||
label: Text('Dark'),
|
||||
),
|
||||
],
|
||||
selected: {provider.themeMode},
|
||||
selected: {settings.themeMode},
|
||||
onSelectionChanged: (set) =>
|
||||
provider.setThemeMode(set.first),
|
||||
settings.setThemeMode(set.first),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -355,8 +360,8 @@ class AccountView extends StatelessWidget {
|
||||
subtitle: const Text(
|
||||
'Use pure black backgrounds in dark mode',
|
||||
),
|
||||
value: provider.amoledDark,
|
||||
onChanged: provider.setAmoledDark,
|
||||
value: settings.amoledDark,
|
||||
onChanged: settings.setAmoledDark,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -380,8 +385,8 @@ class AccountView extends StatelessWidget {
|
||||
subtitle: const Text(
|
||||
'Tapping the current bottom bar tab scrolls its list back to the top',
|
||||
),
|
||||
value: provider.tapTabToScrollTop,
|
||||
onChanged: provider.setTapTabToScrollTop,
|
||||
value: settings.tapTabToScrollTop,
|
||||
onChanged: settings.setTapTabToScrollTop,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
@@ -462,24 +467,24 @@ class AccountView extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// Logout keeps the account saved (see [ServerProvider.logout]) so it's
|
||||
/// harmless/reversible from a one-tap resume on the login screen -
|
||||
/// Logout keeps the account saved (see [SessionController.logout]) so
|
||||
/// it's harmless/reversible from a one-tap resume on the login screen -
|
||||
/// doesn't need a confirmation dialog the way [_confirmRemoveActive] does.
|
||||
Future<void> _handleLogout(
|
||||
BuildContext context,
|
||||
ServerProvider provider,
|
||||
SessionController session,
|
||||
) async {
|
||||
final navigator = Navigator.of(context);
|
||||
await provider.logout();
|
||||
await session.logout();
|
||||
navigator.popUntil((route) => route.isFirst);
|
||||
}
|
||||
|
||||
Future<void> _confirmRemoveActive(
|
||||
BuildContext context,
|
||||
ServerProvider provider,
|
||||
SessionController session,
|
||||
) async {
|
||||
final host = Uri.tryParse(provider.serverUrl)?.host ?? provider.serverUrl;
|
||||
final hasOtherAccounts = provider.accounts.length > 1;
|
||||
final host = Uri.tryParse(session.serverUrl)?.host ?? session.serverUrl;
|
||||
final hasOtherAccounts = session.accounts.length > 1;
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
@@ -487,8 +492,8 @@ class AccountView extends StatelessWidget {
|
||||
title: const Text('Remove Account'),
|
||||
content: Text(
|
||||
hasOtherAccounts
|
||||
? 'Remove ${provider.username} ($host)? Another saved account will become active.'
|
||||
: 'Remove ${provider.username} ($host)? You can add it again later.',
|
||||
? 'Remove ${session.username} ($host)? Another saved account will become active.'
|
||||
: 'Remove ${session.username} ($host)? You can add it again later.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -507,15 +512,15 @@ class AccountView extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
final id = provider.activeAccountId;
|
||||
final id = session.activeAccountId;
|
||||
if (id == null) return;
|
||||
if (!context.mounted) return;
|
||||
final navigator = Navigator.of(context);
|
||||
await provider.removeAccount(id);
|
||||
await session.removeAccount(id);
|
||||
// Only pop back to the root route if that was the last account and the
|
||||
// session actually ended - if it fell back to another saved account,
|
||||
// Settings just keeps showing (now for that account) instead.
|
||||
if (!provider.isLoggedIn) {
|
||||
if (!session.isLoggedIn) {
|
||||
navigator.popUntil((route) => route.isFirst);
|
||||
}
|
||||
}
|
||||
@@ -529,10 +534,10 @@ class _AccountsCard extends StatelessWidget {
|
||||
|
||||
Future<void> _confirmRemove(
|
||||
BuildContext context,
|
||||
ServerProvider provider,
|
||||
SessionController session,
|
||||
SavedAccount account,
|
||||
) async {
|
||||
final isActive = account.id == provider.activeAccountId;
|
||||
final isActive = account.id == session.activeAccountId;
|
||||
final host = Uri.tryParse(account.serverUrl)?.host ?? account.serverUrl;
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
@@ -540,7 +545,7 @@ class _AccountsCard extends StatelessWidget {
|
||||
return AlertDialog(
|
||||
title: const Text('Remove Account'),
|
||||
content: Text(
|
||||
isActive && provider.accounts.length > 1
|
||||
isActive && session.accounts.length > 1
|
||||
? 'Remove ${account.username} ($host)? Another saved account will become active.'
|
||||
: 'Remove ${account.username} ($host)? You can add it again later.',
|
||||
),
|
||||
@@ -563,13 +568,13 @@ class _AccountsCard extends StatelessWidget {
|
||||
if (confirmed == true) {
|
||||
if (!context.mounted) return;
|
||||
final navigator = Navigator.of(context);
|
||||
await provider.removeAccount(account.id);
|
||||
await session.removeAccount(account.id);
|
||||
// Only when this was the last saved account does isLoggedIn drop to
|
||||
// false and main.dart swap the root route to LoginView underneath -
|
||||
// pop back to it then, rather than leaving Settings stranded on top.
|
||||
// Removing a non-active account, or falling back to another one,
|
||||
// both keep the user logged in, so Settings should just stay put.
|
||||
if (!provider.isLoggedIn) {
|
||||
if (!session.isLoggedIn) {
|
||||
navigator.popUntil((route) => route.isFirst);
|
||||
}
|
||||
}
|
||||
@@ -577,8 +582,8 @@ class _AccountsCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<ServerProvider>();
|
||||
final accounts = provider.accounts;
|
||||
final session = context.watch<SessionController>();
|
||||
final accounts = session.accounts;
|
||||
|
||||
return Card(
|
||||
child: Column(
|
||||
@@ -586,11 +591,11 @@ class _AccountsCard extends StatelessWidget {
|
||||
for (final account in accounts) ...[
|
||||
_AccountRow(
|
||||
account: account,
|
||||
isActive: account.id == provider.activeAccountId,
|
||||
onTap: account.id == provider.activeAccountId
|
||||
isActive: account.id == session.activeAccountId,
|
||||
onTap: account.id == session.activeAccountId
|
||||
? null
|
||||
: () => provider.switchAccount(account.id),
|
||||
onRemove: () => _confirmRemove(context, provider, account),
|
||||
: () => session.switchAccount(account.id),
|
||||
onRemove: () => _confirmRemove(context, session, account),
|
||||
),
|
||||
const Divider(height: 1, indent: 16, endIndent: 16),
|
||||
],
|
||||
@@ -671,12 +676,12 @@ class _SecurityCard extends StatelessWidget {
|
||||
|
||||
Future<void> _handleLoginLockChanged(
|
||||
BuildContext context,
|
||||
ServerProvider provider,
|
||||
SessionController session,
|
||||
bool value,
|
||||
) async {
|
||||
final success = value
|
||||
? await provider.setupLoginLock()
|
||||
: await provider.disableLoginLock();
|
||||
? await session.setupLoginLock()
|
||||
: await session.disableLoginLock();
|
||||
if (!success && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
@@ -693,7 +698,7 @@ class _SecurityCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<ServerProvider>();
|
||||
final session = context.watch<SessionController>();
|
||||
|
||||
return Card(
|
||||
child: Column(
|
||||
@@ -704,18 +709,18 @@ class _SecurityCard extends StatelessWidget {
|
||||
subtitle: const Text(
|
||||
"Require this device's PIN or biometric to open Noo",
|
||||
),
|
||||
value: provider.loginLockEnabled,
|
||||
value: session.loginLockEnabled,
|
||||
onChanged: (value) =>
|
||||
_handleLoginLockChanged(context, provider, value),
|
||||
_handleLoginLockChanged(context, session, value),
|
||||
),
|
||||
const Divider(height: 1, indent: 16, endIndent: 16),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.swap_horiz_rounded),
|
||||
title: const Text('Lock account switching'),
|
||||
subtitle: const Text('Unlock to switch between saved accounts'),
|
||||
value: provider.lockAccountSwitching,
|
||||
onChanged: provider.loginLockEnabled
|
||||
? provider.setLockAccountSwitching
|
||||
value: session.lockAccountSwitching,
|
||||
onChanged: session.loginLockEnabled
|
||||
? session.setLockAccountSwitching
|
||||
: null,
|
||||
),
|
||||
const Divider(height: 1, indent: 16, endIndent: 16),
|
||||
@@ -723,9 +728,9 @@ class _SecurityCard extends StatelessWidget {
|
||||
secondary: const Icon(Icons.visibility_off_rounded),
|
||||
title: const Text('Lock hidden files'),
|
||||
subtitle: const Text('Unlock to reveal hidden files and folders'),
|
||||
value: provider.lockHiddenFiles,
|
||||
onChanged: provider.loginLockEnabled
|
||||
? provider.setLockHiddenFiles
|
||||
value: session.lockHiddenFiles,
|
||||
onChanged: session.loginLockEnabled
|
||||
? session.setLockHiddenFiles
|
||||
: null,
|
||||
),
|
||||
],
|
||||
@@ -744,10 +749,13 @@ class _DeviceSyncCard extends StatefulWidget {
|
||||
class _DeviceSyncCardState extends State<_DeviceSyncCard> {
|
||||
bool _syncingNow = false;
|
||||
|
||||
Future<void> _syncNow(ServerProvider provider) async {
|
||||
Future<void> _syncNow(
|
||||
SessionController session,
|
||||
SyncStatusController sync,
|
||||
) async {
|
||||
setState(() => _syncingNow = true);
|
||||
try {
|
||||
await SyncService.syncNow(provider);
|
||||
await SyncService.syncNow(session, sync);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -766,9 +774,10 @@ class _DeviceSyncCardState extends State<_DeviceSyncCard> {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final provider = context.watch<ServerProvider>();
|
||||
final folders = provider.syncedPaths;
|
||||
final everything = provider.syncEverything;
|
||||
final session = context.watch<SessionController>();
|
||||
final sync = context.watch<SyncStatusController>();
|
||||
final folders = sync.syncedPaths;
|
||||
final everything = sync.syncEverything;
|
||||
|
||||
return Card(
|
||||
child: Column(
|
||||
@@ -781,7 +790,7 @@ class _DeviceSyncCardState extends State<_DeviceSyncCard> {
|
||||
'Mirror the whole account instead of picking folders',
|
||||
),
|
||||
value: everything,
|
||||
onChanged: provider.setSyncEverything,
|
||||
onChanged: sync.setSyncEverything,
|
||||
),
|
||||
const Divider(height: 1, indent: 16, endIndent: 16),
|
||||
if (everything)
|
||||
@@ -814,7 +823,7 @@ class _DeviceSyncCardState extends State<_DeviceSyncCard> {
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
tooltip: 'Stop syncing',
|
||||
onPressed: () => provider.removeSyncedPath(folder),
|
||||
onPressed: () => sync.removeSyncedPath(folder),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1, indent: 16, endIndent: 16),
|
||||
@@ -823,8 +832,8 @@ class _DeviceSyncCardState extends State<_DeviceSyncCard> {
|
||||
secondary: const Icon(Icons.signal_cellular_alt_rounded),
|
||||
title: const Text('Sync on cellular'),
|
||||
subtitle: const Text('Off = background sync only runs on Wi-Fi'),
|
||||
value: provider.syncOnCellular,
|
||||
onChanged: provider.setSyncOnCellular,
|
||||
value: sync.syncOnCellular,
|
||||
onChanged: sync.setSyncOnCellular,
|
||||
),
|
||||
const Divider(height: 1, indent: 16, endIndent: 16),
|
||||
ListTile(
|
||||
@@ -840,7 +849,7 @@ class _DeviceSyncCardState extends State<_DeviceSyncCard> {
|
||||
: const Icon(Icons.sync_rounded),
|
||||
title: const Text('Sync now'),
|
||||
enabled: (everything || folders.isNotEmpty) && !_syncingNow,
|
||||
onTap: () => _syncNow(provider),
|
||||
onTap: () => _syncNow(session, sync),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -857,9 +866,9 @@ class _BottomBarAppearanceCard extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _BottomBarAppearanceCardState extends State<_BottomBarAppearanceCard> {
|
||||
// Mirrors the provider values locally so the slider thumb and the live
|
||||
// Mirrors the controller values locally so the slider thumb and the live
|
||||
// preview track the drag gesture on every frame. Driving them straight off
|
||||
// `provider.bottomBar*` instead would tie the slider's own responsiveness
|
||||
// `settings.bottomBar*` instead would tie the slider's own responsiveness
|
||||
// to a full Provider-wide rebuild — including the expensive BackdropFilter
|
||||
// blur in the preview — which can't keep up with fast drag ticks and makes
|
||||
// the thumb appear stuck until the next unrelated rebuild.
|
||||
@@ -870,9 +879,9 @@ class _BottomBarAppearanceCardState extends State<_BottomBarAppearanceCard> {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final provider = context.watch<ServerProvider>();
|
||||
final opacity = _opacity ?? provider.bottomBarOpacity;
|
||||
final blur = _blur ?? provider.bottomBarBlur;
|
||||
final settings = context.watch<SettingsController>();
|
||||
final opacity = _opacity ?? settings.bottomBarOpacity;
|
||||
final blur = _blur ?? settings.bottomBarBlur;
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
@@ -974,7 +983,7 @@ class _BottomBarAppearanceCardState extends State<_BottomBarAppearanceCard> {
|
||||
min: 0.1,
|
||||
max: 1.0,
|
||||
onChanged: (value) => setState(() => _opacity = value),
|
||||
onChangeEnd: provider.setBottomBarOpacity,
|
||||
onChangeEnd: settings.setBottomBarOpacity,
|
||||
),
|
||||
|
||||
Row(
|
||||
@@ -994,7 +1003,7 @@ class _BottomBarAppearanceCardState extends State<_BottomBarAppearanceCard> {
|
||||
min: 0,
|
||||
max: 40,
|
||||
onChanged: (value) => setState(() => _blur = value),
|
||||
onChangeEnd: provider.setBottomBarBlur,
|
||||
onChangeEnd: settings.setBottomBarBlur,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1075,7 +1084,7 @@ class _SwipeActionsCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final provider = context.watch<ServerProvider>();
|
||||
final settings = context.watch<SettingsController>();
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
@@ -1086,16 +1095,16 @@ class _SwipeActionsCard extends StatelessWidget {
|
||||
context,
|
||||
theme,
|
||||
label: 'Swipe right',
|
||||
value: provider.swipeRightAction,
|
||||
onChanged: provider.setSwipeRightAction,
|
||||
value: settings.swipeRightAction,
|
||||
onChanged: settings.setSwipeRightAction,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
_buildRow(
|
||||
context,
|
||||
theme,
|
||||
label: 'Swipe left',
|
||||
value: provider.swipeLeftAction,
|
||||
onChanged: provider.setSwipeLeftAction,
|
||||
value: settings.swipeLeftAction,
|
||||
onChanged: settings.setSwipeLeftAction,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1157,21 +1166,21 @@ class _MediaPlayerCard extends StatelessWidget {
|
||||
|
||||
Future<void> _openPicker(
|
||||
BuildContext context,
|
||||
ServerProvider provider,
|
||||
SettingsController settings,
|
||||
) async {
|
||||
final selected = await showDialog<MediaProgressBarStyle>(
|
||||
context: context,
|
||||
builder: (_) =>
|
||||
_SeekBarStyleDialog(current: provider.mediaProgressBarStyle),
|
||||
_SeekBarStyleDialog(current: settings.mediaProgressBarStyle),
|
||||
);
|
||||
if (selected != null) provider.setMediaProgressBarStyle(selected);
|
||||
if (selected != null) settings.setMediaProgressBarStyle(selected);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final provider = context.watch<ServerProvider>();
|
||||
final settings = context.watch<SettingsController>();
|
||||
|
||||
return Card(
|
||||
child: ListTile(
|
||||
@@ -1182,17 +1191,17 @@ class _MediaPlayerCard extends StatelessWidget {
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
subtitle: Text(_seekBarStyleLabel(provider.mediaProgressBarStyle)),
|
||||
subtitle: Text(_seekBarStyleLabel(settings.mediaProgressBarStyle)),
|
||||
trailing: SizedBox(
|
||||
width: 64,
|
||||
height: 24,
|
||||
child: SeekBarPreview(
|
||||
style: provider.mediaProgressBarStyle,
|
||||
style: settings.mediaProgressBarStyle,
|
||||
playedColor: colorScheme.primary,
|
||||
trackColor: colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
onTap: () => _openPicker(context, provider),
|
||||
onTap: () => _openPicker(context, settings),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1354,7 +1363,7 @@ class _CacheSettingsCard extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final provider = context.watch<ServerProvider>();
|
||||
final files = context.watch<FilesController>();
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
@@ -1362,9 +1371,9 @@ class _CacheSettingsCard extends StatelessWidget {
|
||||
child: Column(
|
||||
children: [
|
||||
RadioGroup<CachePolicy>(
|
||||
groupValue: provider.cachePolicy,
|
||||
groupValue: files.cachePolicy,
|
||||
onChanged: (value) {
|
||||
if (value != null) provider.setCachePolicy(value);
|
||||
if (value != null) files.setCachePolicy(value);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -1377,7 +1386,7 @@ class _CacheSettingsCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (provider.cachePolicy == CachePolicy.interval)
|
||||
if (files.cachePolicy == CachePolicy.interval)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||
child: Column(
|
||||
@@ -1392,7 +1401,7 @@ class _CacheSettingsCard extends StatelessWidget {
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
Text(
|
||||
'${provider.cacheIntervalMinutes} min',
|
||||
'${files.cacheIntervalMinutes} min',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
@@ -1400,13 +1409,13 @@ class _CacheSettingsCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
Slider(
|
||||
value: provider.cacheIntervalMinutes.toDouble(),
|
||||
value: files.cacheIntervalMinutes.toDouble(),
|
||||
min: 1,
|
||||
max: 60,
|
||||
divisions: 59,
|
||||
label: '${provider.cacheIntervalMinutes} min',
|
||||
label: '${files.cacheIntervalMinutes} min',
|
||||
onChanged: (value) =>
|
||||
provider.setCacheIntervalMinutes(value.round()),
|
||||
files.setCacheIntervalMinutes(value.round()),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1423,7 +1432,7 @@ class _TabSettingsCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<ServerProvider>();
|
||||
final settings = context.watch<SettingsController>();
|
||||
|
||||
return Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
@@ -1431,20 +1440,20 @@ class _TabSettingsCard extends StatelessWidget {
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
onReorderItem: (oldIndex, newIndex) {
|
||||
final order = List<AppTab>.from(provider.tabOrder);
|
||||
final order = List<AppTab>.from(settings.tabOrder);
|
||||
final tab = order.removeAt(oldIndex);
|
||||
order.insert(newIndex, tab);
|
||||
provider.setTabOrder(order);
|
||||
settings.setTabOrder(order);
|
||||
},
|
||||
children: [
|
||||
for (final tab in provider.tabOrder)
|
||||
for (final tab in settings.tabOrder)
|
||||
_TabConfigRow(
|
||||
key: ValueKey(tab),
|
||||
tab: tab,
|
||||
isVisible: !provider.hiddenTabs.contains(tab),
|
||||
isDefault: provider.defaultTab == tab,
|
||||
isVisible: !settings.hiddenTabs.contains(tab),
|
||||
isDefault: settings.defaultTab == tab,
|
||||
onVisibilityChanged: (value) {
|
||||
final error = provider.setTabHidden(tab, !value);
|
||||
final error = settings.setTabHidden(tab, !value);
|
||||
if (error != null && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
@@ -1454,7 +1463,7 @@ class _TabSettingsCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
},
|
||||
onSetDefault: () => provider.setDefaultTab(tab),
|
||||
onSetDefault: () => settings.setDefaultTab(tab),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/server_provider.dart';
|
||||
import '../providers/files_controller.dart';
|
||||
import '../widgets/more_tabs_button.dart';
|
||||
import '../widgets/profile_avatar_button.dart';
|
||||
import '../widgets/synced_header_scaffold.dart';
|
||||
@@ -15,13 +15,13 @@ class ActivityView extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final provider = context.watch<ServerProvider>();
|
||||
final activities = provider.activities;
|
||||
final files = context.watch<FilesController>();
|
||||
final activities = files.activities;
|
||||
|
||||
final List<Widget> contentSlivers = [
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 16)),
|
||||
|
||||
if (provider.isLoading && activities.isEmpty)
|
||||
if (files.isLoading && activities.isEmpty)
|
||||
const SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
@@ -134,7 +134,6 @@ class ActivityView extends StatelessWidget {
|
||||
|
||||
return SyncedHeaderScaffold(
|
||||
scrollController: scrollController,
|
||||
provider: provider,
|
||||
actions: const [MoreTabsButton(), ProfileAvatarButton()],
|
||||
contentSlivers: contentSlivers,
|
||||
);
|
||||
|
||||
@@ -7,7 +7,11 @@ import 'package:share_plus/share_plus.dart';
|
||||
import '../models/app_tab.dart';
|
||||
import '../models/nextcloud_item.dart';
|
||||
import '../models/selection_action.dart';
|
||||
import '../providers/server_provider.dart';
|
||||
import '../providers/favorites_controller.dart';
|
||||
import '../providers/files_controller.dart';
|
||||
import '../providers/item_operations.dart';
|
||||
import '../providers/session_controller.dart';
|
||||
import '../providers/settings_controller.dart';
|
||||
import '../services/download_service.dart';
|
||||
import '../widgets/details/details_sheet.dart';
|
||||
import '../widgets/item_icon.dart';
|
||||
@@ -24,7 +28,7 @@ import 'move_copy_destination_picker.dart';
|
||||
|
||||
/// Every favorited file/folder across the whole account, account-wide - a
|
||||
/// real tab rather than a filter toggle scoped to whatever folder the
|
||||
/// Files tab happens to be browsing (see `ServerProvider.fetchAllFavorites`
|
||||
/// Files tab happens to be browsing (see `FavoritesController.fetchAll`
|
||||
/// for why: a favorited item several folders deep needs to show up
|
||||
/// regardless of whether its parent folders are themselves favorited,
|
||||
/// which a current-folder-only filter can never do, and a flat account-
|
||||
@@ -32,7 +36,7 @@ import 'move_copy_destination_picker.dart';
|
||||
/// toggle had - the trail showing wherever Files was last browsing while
|
||||
/// the content showed something else entirely - simply can't happen).
|
||||
/// Shares Files' own sort/hidden/storage-scope/grid-list display prefs
|
||||
/// (via `ServerProvider.applyFilesDisplayPrefs`) rather than a separate
|
||||
/// (via `FilesController.applyFilesDisplayPrefs`) rather than a separate
|
||||
/// parallel settings dimension.
|
||||
class FavoritesView extends StatefulWidget {
|
||||
final ScrollController scrollController;
|
||||
@@ -128,13 +132,12 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
/// back on this tab either way.
|
||||
void _openFavorite(
|
||||
BuildContext context,
|
||||
ServerProvider provider,
|
||||
NextcloudItem item,
|
||||
List<NextcloudItem> siblings,
|
||||
) {
|
||||
if (item.isFolder) {
|
||||
provider.navigateToAbsoluteFolder(item.path);
|
||||
provider.requestTab(AppTab.files);
|
||||
context.read<FilesController>().navigateToAbsoluteFolder(item.path);
|
||||
context.read<SettingsController>().requestTab(AppTab.files);
|
||||
} else {
|
||||
Navigator.push(
|
||||
context,
|
||||
@@ -145,42 +148,39 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
|
||||
/// The bulk actions shown in the sticky selection toolbar for the
|
||||
/// currently-selected items - same set Files/Photos offer.
|
||||
List<SelectionAction> _buildSelectionActions(
|
||||
ServerProvider provider,
|
||||
List<NextcloudItem> selected,
|
||||
) {
|
||||
List<SelectionAction> _buildSelectionActions(List<NextcloudItem> selected) {
|
||||
return [
|
||||
SelectionAction(
|
||||
icon: Icons.favorite_border_rounded,
|
||||
label: 'Remove from favorites',
|
||||
onTap: () => _unfavoriteSelected(provider, selected),
|
||||
onTap: () => _unfavoriteSelected(selected),
|
||||
),
|
||||
SelectionAction(
|
||||
icon: Icons.share_rounded,
|
||||
label: 'Share',
|
||||
onTap: () => selected.length == 1
|
||||
? ShareSheet.show(context, selected.single)
|
||||
: _shareSelected(context, provider, selected),
|
||||
: _shareSelected(context, selected),
|
||||
),
|
||||
SelectionAction(
|
||||
icon: Icons.download_rounded,
|
||||
label: 'Download',
|
||||
onTap: () => _downloadSelected(context, provider, selected),
|
||||
onTap: () => _downloadSelected(context, selected),
|
||||
),
|
||||
SelectionAction(
|
||||
icon: Icons.delete_outline_rounded,
|
||||
label: 'Delete',
|
||||
onTap: () => _confirmDeleteSelected(context, provider, selected),
|
||||
onTap: () => _confirmDeleteSelected(context, selected),
|
||||
),
|
||||
SelectionAction(
|
||||
icon: Icons.copy_rounded,
|
||||
label: 'Copy',
|
||||
onTap: () => _moveOrCopySelected(provider, selected, copy: true),
|
||||
onTap: () => _moveOrCopySelected(selected, copy: true),
|
||||
),
|
||||
SelectionAction(
|
||||
icon: Icons.drive_file_move_rounded,
|
||||
label: 'Move',
|
||||
onTap: () => _moveOrCopySelected(provider, selected, copy: false),
|
||||
onTap: () => _moveOrCopySelected(selected, copy: false),
|
||||
),
|
||||
if (selected.length == 1)
|
||||
SelectionAction(
|
||||
@@ -191,21 +191,19 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> _unfavoriteSelected(
|
||||
ServerProvider provider,
|
||||
List<NextcloudItem> items,
|
||||
) async {
|
||||
Future<void> _unfavoriteSelected(List<NextcloudItem> items) async {
|
||||
final ops = context.read<ItemOperations>();
|
||||
for (final item in items) {
|
||||
await provider.toggleItemFavorite(item);
|
||||
await ops.toggleItemFavorite(item);
|
||||
}
|
||||
_clearSelection();
|
||||
}
|
||||
|
||||
Future<void> _shareSelected(
|
||||
BuildContext context,
|
||||
ServerProvider provider,
|
||||
List<NextcloudItem> items,
|
||||
) async {
|
||||
final ops = context.read<ItemOperations>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(
|
||||
@@ -216,7 +214,7 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
|
||||
final lines = <String>[];
|
||||
for (final item in items) {
|
||||
final link = await provider.createShareLink(item);
|
||||
final link = await ops.createShareLink(item);
|
||||
if (link != null) {
|
||||
lines.add(items.length > 1 ? '${item.name}: $link' : link);
|
||||
}
|
||||
@@ -252,16 +250,16 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
/// `FilesView._downloadSelected`'s identical doc comment for why.
|
||||
Future<void> _downloadSelected(
|
||||
BuildContext context,
|
||||
ServerProvider provider,
|
||||
List<NextcloudItem> items,
|
||||
) async {
|
||||
final session = context.read<SessionController>();
|
||||
final files = items.where((i) => !i.isFolder).toList();
|
||||
_clearSelection();
|
||||
if (files.isEmpty) return;
|
||||
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
try {
|
||||
await DownloadService.startDownload(provider, files);
|
||||
await DownloadService.startDownload(session, files);
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
@@ -284,7 +282,6 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
|
||||
Future<void> _confirmDeleteSelected(
|
||||
BuildContext context,
|
||||
ServerProvider provider,
|
||||
List<NextcloudItem> items,
|
||||
) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
@@ -313,10 +310,11 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
);
|
||||
if (confirmed != true || !context.mounted) return;
|
||||
|
||||
final ops = context.read<ItemOperations>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
var succeeded = 0;
|
||||
for (final item in items) {
|
||||
final success = await provider.deleteItem(item.path);
|
||||
final success = await ops.deleteItem(item.path);
|
||||
if (success) succeeded++;
|
||||
}
|
||||
|
||||
@@ -330,7 +328,6 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
}
|
||||
|
||||
Future<void> _moveOrCopySelected(
|
||||
ServerProvider provider,
|
||||
List<NextcloudItem> items, {
|
||||
required bool copy,
|
||||
}) async {
|
||||
@@ -351,7 +348,7 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildControlsRow(ServerProvider provider) {
|
||||
Widget _buildControlsRow(FilesController files) {
|
||||
return SizedBox(
|
||||
height: 44,
|
||||
child: SingleChildScrollView(
|
||||
@@ -360,28 +357,28 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
provider.filesSortAscending
|
||||
files.filesSortAscending
|
||||
? Icons.arrow_upward_rounded
|
||||
: Icons.arrow_downward_rounded,
|
||||
size: 20,
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
tooltip: provider.filesSortAscending ? 'Ascending' : 'Descending',
|
||||
onPressed: provider.toggleFilesSortOrder,
|
||||
tooltip: files.filesSortAscending ? 'Ascending' : 'Descending',
|
||||
onPressed: files.toggleFilesSortOrder,
|
||||
),
|
||||
SizedBox(
|
||||
width: 130,
|
||||
child: SortMenuButton(
|
||||
field: provider.filesSortField,
|
||||
onChanged: provider.setFilesSortField,
|
||||
field: files.filesSortField,
|
||||
onChanged: files.setFilesSortField,
|
||||
),
|
||||
),
|
||||
ToggleIconButton(
|
||||
icon: provider.showHiddenFiles
|
||||
icon: files.showHiddenFiles
|
||||
? Icons.visibility_rounded
|
||||
: Icons.visibility_off_rounded,
|
||||
isSelected: provider.showHiddenFiles,
|
||||
onTap: () => provider.toggleShowHiddenFiles(),
|
||||
isSelected: files.showHiddenFiles,
|
||||
onTap: () => files.toggleShowHiddenFiles(),
|
||||
tooltip: 'Show hidden files',
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
@@ -389,14 +386,14 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
children: [
|
||||
ToggleIconButton(
|
||||
icon: Symbols.circles_rounded,
|
||||
isSelected: provider.storageScope == StorageScope.cloud,
|
||||
onTap: () => provider.setStorageScope(StorageScope.cloud),
|
||||
isSelected: files.storageScope == StorageScope.cloud,
|
||||
onTap: () => files.setStorageScope(StorageScope.cloud),
|
||||
tooltip: 'Cloud storage',
|
||||
),
|
||||
ToggleIconButton(
|
||||
icon: Symbols.hard_drive_rounded,
|
||||
isSelected: provider.storageScope == StorageScope.external,
|
||||
onTap: () => provider.setStorageScope(StorageScope.external),
|
||||
isSelected: files.storageScope == StorageScope.external,
|
||||
onTap: () => files.setStorageScope(StorageScope.external),
|
||||
tooltip: 'External storage',
|
||||
),
|
||||
],
|
||||
@@ -406,14 +403,14 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
children: [
|
||||
ToggleIconButton(
|
||||
icon: Icons.view_list_rounded,
|
||||
isSelected: !provider.isGridView,
|
||||
onTap: () => provider.setGridView(false),
|
||||
isSelected: !files.isGridView,
|
||||
onTap: () => files.setGridView(false),
|
||||
tooltip: 'List view',
|
||||
),
|
||||
ToggleIconButton(
|
||||
icon: Icons.grid_view_rounded,
|
||||
isSelected: provider.isGridView,
|
||||
onTap: () => provider.setGridView(true),
|
||||
isSelected: files.isGridView,
|
||||
onTap: () => files.setGridView(true),
|
||||
tooltip: 'Grid view',
|
||||
),
|
||||
],
|
||||
@@ -427,11 +424,11 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
Widget _buildListTile(
|
||||
BuildContext context,
|
||||
NextcloudItem item,
|
||||
ServerProvider provider,
|
||||
List<NextcloudItem> siblings,
|
||||
) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final session = context.watch<SessionController>();
|
||||
final isSelected = _selectedIds.contains(item.id);
|
||||
|
||||
return Padding(
|
||||
@@ -447,7 +444,7 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
if (_isSelecting) {
|
||||
_toggleSelection(item);
|
||||
} else {
|
||||
_openFavorite(context, provider, item, siblings);
|
||||
_openFavorite(context, item, siblings);
|
||||
}
|
||||
},
|
||||
onLongPress: () => _toggleSelection(item),
|
||||
@@ -461,7 +458,7 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
checkmarkSize: 24,
|
||||
child: ItemThumbnail(
|
||||
item: item,
|
||||
provider: provider,
|
||||
service: session.service,
|
||||
size: 44,
|
||||
borderRadius: 12,
|
||||
iconSize: 22,
|
||||
@@ -503,10 +500,10 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
Widget _buildGridCard(
|
||||
BuildContext context,
|
||||
NextcloudItem item,
|
||||
ServerProvider provider,
|
||||
List<NextcloudItem> siblings,
|
||||
) {
|
||||
final theme = Theme.of(context);
|
||||
final session = context.watch<SessionController>();
|
||||
final isSelected = _selectedIds.contains(item.id);
|
||||
final isMedia =
|
||||
(item.type == NextcloudItemType.image ||
|
||||
@@ -523,7 +520,7 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
if (_isSelecting) {
|
||||
_toggleSelection(item);
|
||||
} else {
|
||||
_openFavorite(context, provider, item, siblings);
|
||||
_openFavorite(context, item, siblings);
|
||||
}
|
||||
},
|
||||
onLongPress: () => _toggleSelection(item),
|
||||
@@ -533,7 +530,7 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
child: isMedia
|
||||
? Image.network(
|
||||
item.previewUrl!,
|
||||
headers: provider.service?.authHeaders,
|
||||
headers: session.service?.authHeaders,
|
||||
fit: BoxFit.cover,
|
||||
filterQuality: FilterQuality.low,
|
||||
gaplessPlayback: true,
|
||||
@@ -587,16 +584,17 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final provider = context.watch<ServerProvider>();
|
||||
final favoritesController = context.watch<FavoritesController>();
|
||||
final filesController = context.watch<FilesController>();
|
||||
|
||||
if (!_requested) {
|
||||
_requested = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => provider.fetchAllFavorites(),
|
||||
(_) => favoritesController.fetchAll(),
|
||||
);
|
||||
}
|
||||
|
||||
final favorites = provider.favoriteItems;
|
||||
final favorites = favoritesController.items;
|
||||
final siblings = favorites.where((i) => !i.isFolder).toList();
|
||||
final selectedItems = favorites
|
||||
.where((i) => _selectedIds.contains(i.id))
|
||||
@@ -609,17 +607,17 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
height: 60,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||
child: _buildControlsRow(provider),
|
||||
child: _buildControlsRow(filesController),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 8)),
|
||||
if (provider.isFavoritesLoading && favorites.isEmpty)
|
||||
if (favoritesController.isLoading && favorites.isEmpty)
|
||||
const SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
else if (provider.favoritesErrorMessage != null)
|
||||
else if (favoritesController.errorMessage != null)
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(
|
||||
@@ -643,7 +641,7 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
provider.favoritesErrorMessage!,
|
||||
favoritesController.errorMessage!,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
@@ -651,7 +649,7 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.icon(
|
||||
onPressed: provider.fetchAllFavorites,
|
||||
onPressed: favoritesController.fetchAll,
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
label: const Text('Retry'),
|
||||
),
|
||||
@@ -683,7 +681,7 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (provider.isGridView)
|
||||
else if (filesController.isGridView)
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
sliver: SliverGrid(
|
||||
@@ -694,12 +692,7 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return _buildGridCard(
|
||||
context,
|
||||
favorites[index],
|
||||
provider,
|
||||
siblings,
|
||||
);
|
||||
return _buildGridCard(context, favorites[index], siblings);
|
||||
}, childCount: favorites.length),
|
||||
),
|
||||
)
|
||||
@@ -708,12 +701,7 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return _buildListTile(
|
||||
context,
|
||||
favorites[index],
|
||||
provider,
|
||||
siblings,
|
||||
);
|
||||
return _buildListTile(context, favorites[index], siblings);
|
||||
}, childCount: favorites.length),
|
||||
),
|
||||
),
|
||||
@@ -728,11 +716,10 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
},
|
||||
child: SyncedHeaderScaffold(
|
||||
scrollController: widget.scrollController,
|
||||
provider: provider,
|
||||
actions: const [MoreTabsButton(), ProfileAvatarButton()],
|
||||
onRefresh: provider.fetchAllFavorites,
|
||||
onRefresh: favoritesController.fetchAll,
|
||||
selectionBar: _isSelecting
|
||||
? _buildSelectionBar(context, theme, provider, selectedItems)
|
||||
? _buildSelectionBar(context, theme, selectedItems)
|
||||
: null,
|
||||
contentSlivers: contentSlivers,
|
||||
),
|
||||
@@ -745,7 +732,6 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
Widget _buildSelectionBar(
|
||||
BuildContext context,
|
||||
ThemeData theme,
|
||||
ServerProvider provider,
|
||||
List<NextcloudItem> selectedItems,
|
||||
) {
|
||||
return Padding(
|
||||
@@ -783,10 +769,7 @@ class _FavoritesViewState extends State<FavoritesView>
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final action in _buildSelectionActions(
|
||||
provider,
|
||||
selectedItems,
|
||||
))
|
||||
for (final action in _buildSelectionActions(selectedItems))
|
||||
IconButton(
|
||||
icon: Icon(action.icon, size: 20),
|
||||
tooltip: action.label,
|
||||
|
||||
@@ -9,7 +9,9 @@ import 'package:pdfx/pdfx.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import '../models/nextcloud_item.dart';
|
||||
import '../providers/server_provider.dart';
|
||||
import '../providers/item_operations.dart';
|
||||
import '../providers/session_controller.dart';
|
||||
import '../providers/settings_controller.dart';
|
||||
import '../services/download_service.dart';
|
||||
import '../widgets/details/details_sheet.dart';
|
||||
import '../widgets/frosted_glass_container.dart';
|
||||
@@ -148,13 +150,13 @@ class _FileViewerScreenState extends State<FileViewerScreen> {
|
||||
}
|
||||
|
||||
Future<String> _downloadToTemp(
|
||||
ServerProvider provider,
|
||||
SessionController session,
|
||||
NextcloudItem item,
|
||||
) async {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final savePath = p.join(tempDir.path, item.name);
|
||||
setState(() => _downloadProgress = 0);
|
||||
await provider.service!.downloadToFile(
|
||||
await session.service!.downloadToFile(
|
||||
item.path,
|
||||
savePath,
|
||||
onProgress: (received, total) {
|
||||
@@ -167,11 +169,11 @@ class _FileViewerScreenState extends State<FileViewerScreen> {
|
||||
return savePath;
|
||||
}
|
||||
|
||||
Future<void> _openExternally(ServerProvider provider) async {
|
||||
Future<void> _openExternally(SessionController session) async {
|
||||
final item = _currentItem;
|
||||
setState(() => _isBusy = true);
|
||||
try {
|
||||
final path = await _downloadToTemp(provider, item);
|
||||
final path = await _downloadToTemp(session, item);
|
||||
final result = await OpenFile.open(path);
|
||||
if (mounted && result.type != ResultType.done) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -194,10 +196,10 @@ class _FileViewerScreenState extends State<FileViewerScreen> {
|
||||
/// `UploadService`/`ShareUploadService.kt` on the upload side: a real
|
||||
/// Android Service survives the app being closed mid-download, and one
|
||||
/// notification covers progress/cancel instead of blocking this screen.
|
||||
Future<void> _downloadToDevice(ServerProvider provider) async {
|
||||
Future<void> _downloadToDevice(SessionController session) async {
|
||||
final item = _currentItem;
|
||||
try {
|
||||
await DownloadService.startDownload(provider, [item]);
|
||||
await DownloadService.startDownload(session, [item]);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
@@ -216,16 +218,16 @@ class _FileViewerScreenState extends State<FileViewerScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _toggleFavorite(ServerProvider provider) async {
|
||||
Future<void> _toggleFavorite(ItemOperations ops) async {
|
||||
final item = _currentItem;
|
||||
await provider.toggleItemFavorite(item);
|
||||
await ops.toggleItemFavorite(item);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_mediaItems[_currentIndex] = item.copyWith(isFavorite: !item.isFavorite);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _deleteCurrentItem(ServerProvider provider) async {
|
||||
Future<void> _deleteCurrentItem(ItemOperations ops) async {
|
||||
final item = _currentItem;
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
@@ -254,7 +256,7 @@ class _FileViewerScreenState extends State<FileViewerScreen> {
|
||||
if (confirmed != true || !mounted) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
final success = await provider.deleteItem(item.path);
|
||||
final success = await ops.deleteItem(item.path);
|
||||
if (!mounted) return;
|
||||
setState(() => _isBusy = false);
|
||||
|
||||
@@ -283,7 +285,9 @@ class _FileViewerScreenState extends State<FileViewerScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.read<ServerProvider>();
|
||||
final session = context.read<SessionController>();
|
||||
final settings = context.read<SettingsController>();
|
||||
final ops = context.read<ItemOperations>();
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
@@ -296,7 +300,7 @@ class _FileViewerScreenState extends State<FileViewerScreen> {
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: colorScheme.surface,
|
||||
child: _buildBody(context, provider),
|
||||
child: _buildBody(context, session),
|
||||
),
|
||||
),
|
||||
if (_downloadProgress != null)
|
||||
@@ -322,8 +326,8 @@ class _FileViewerScreenState extends State<FileViewerScreen> {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 0),
|
||||
child: FrostedGlassContainer(
|
||||
opacity: provider.bottomBarOpacity,
|
||||
blurSigma: provider.bottomBarBlur,
|
||||
opacity: settings.bottomBarOpacity,
|
||||
blurSigma: settings.bottomBarBlur,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
@@ -367,13 +371,13 @@ class _FileViewerScreenState extends State<FileViewerScreen> {
|
||||
child: _MediaActionBar(
|
||||
isFavorite: _currentItem.isFavorite,
|
||||
isBusy: _isBusy,
|
||||
opacity: provider.bottomBarOpacity,
|
||||
blurSigma: provider.bottomBarBlur,
|
||||
opacity: settings.bottomBarOpacity,
|
||||
blurSigma: settings.bottomBarBlur,
|
||||
onShare: () => ShareSheet.show(context, _currentItem),
|
||||
onFavorite: () => _toggleFavorite(provider),
|
||||
onDelete: () => _deleteCurrentItem(provider),
|
||||
onOpenExternally: () => _openExternally(provider),
|
||||
onDownload: () => _downloadToDevice(provider),
|
||||
onFavorite: () => _toggleFavorite(ops),
|
||||
onDelete: () => _deleteCurrentItem(ops),
|
||||
onOpenExternally: () => _openExternally(session),
|
||||
onDownload: () => _downloadToDevice(session),
|
||||
onDetails: () => DetailsSheet.show(context, _currentItem),
|
||||
),
|
||||
),
|
||||
@@ -385,10 +389,11 @@ class _FileViewerScreenState extends State<FileViewerScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(BuildContext context, ServerProvider provider) {
|
||||
Widget _buildBody(BuildContext context, SessionController session) {
|
||||
if (!_isSwipeable) {
|
||||
return _buildStaticPreview(context, provider);
|
||||
return _buildStaticPreview(context, session);
|
||||
}
|
||||
final settings = context.read<SettingsController>();
|
||||
|
||||
return PageView.builder(
|
||||
controller: _pageController,
|
||||
@@ -399,24 +404,24 @@ class _FileViewerScreenState extends State<FileViewerScreen> {
|
||||
if (mediaItem.type == NextcloudItemType.image) {
|
||||
return _ImagePreview(
|
||||
key: ValueKey(mediaItem.id),
|
||||
url: provider.service!.fileUrl(mediaItem.path),
|
||||
headers: provider.service!.authHeaders,
|
||||
url: session.service!.fileUrl(mediaItem.path),
|
||||
headers: session.service!.authHeaders,
|
||||
);
|
||||
}
|
||||
return _VideoPreview(
|
||||
key: ValueKey(mediaItem.id),
|
||||
url: provider.service!.fileUrl(mediaItem.path),
|
||||
headers: provider.service!.authHeaders,
|
||||
url: session.service!.fileUrl(mediaItem.path),
|
||||
headers: session.service!.authHeaders,
|
||||
isActive: index == _currentIndex,
|
||||
controlsBottomOffset: _controlsVisible ? 108 : 24,
|
||||
opacity: provider.bottomBarOpacity,
|
||||
blurSigma: provider.bottomBarBlur,
|
||||
opacity: settings.bottomBarOpacity,
|
||||
blurSigma: settings.bottomBarBlur,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStaticPreview(BuildContext context, ServerProvider provider) {
|
||||
Widget _buildStaticPreview(BuildContext context, SessionController session) {
|
||||
switch (widget.item.type) {
|
||||
case NextcloudItemType.image:
|
||||
case NextcloudItemType.video:
|
||||
@@ -424,15 +429,15 @@ class _FileViewerScreenState extends State<FileViewerScreen> {
|
||||
return const SizedBox.shrink();
|
||||
default:
|
||||
if (_isPdf) {
|
||||
return _PdfPreview(item: widget.item, provider: provider);
|
||||
return _PdfPreview(item: widget.item, session: session);
|
||||
}
|
||||
if (_isText) {
|
||||
return _TextPreview(item: widget.item, provider: provider);
|
||||
return _TextPreview(item: widget.item, session: session);
|
||||
}
|
||||
return _UnsupportedPreview(
|
||||
item: widget.item,
|
||||
isBusy: _isBusy,
|
||||
onOpenExternally: () => _openExternally(provider),
|
||||
onOpenExternally: () => _openExternally(session),
|
||||
onOpenDetails: () => DetailsSheet.show(context, widget.item),
|
||||
);
|
||||
}
|
||||
@@ -752,7 +757,7 @@ class _VideoPreviewState extends State<_VideoPreview> {
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final progressBarStyle = context
|
||||
.watch<ServerProvider>()
|
||||
.watch<SettingsController>()
|
||||
.mediaProgressBarStyle;
|
||||
if (_error != null) {
|
||||
return Center(
|
||||
@@ -966,9 +971,9 @@ class _WavySeekBarState extends State<_WavySeekBar>
|
||||
|
||||
class _PdfPreview extends StatefulWidget {
|
||||
final NextcloudItem item;
|
||||
final ServerProvider provider;
|
||||
final SessionController session;
|
||||
|
||||
const _PdfPreview({required this.item, required this.provider});
|
||||
const _PdfPreview({required this.item, required this.session});
|
||||
|
||||
@override
|
||||
State<_PdfPreview> createState() => _PdfPreviewState();
|
||||
@@ -986,7 +991,7 @@ class _PdfPreviewState extends State<_PdfPreview> {
|
||||
|
||||
Future<void> _load() async {
|
||||
try {
|
||||
final bytes = await widget.provider.service!.fetchBytes(widget.item.path);
|
||||
final bytes = await widget.session.service!.fetchBytes(widget.item.path);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_controller = PdfControllerPinch(
|
||||
@@ -1033,9 +1038,9 @@ class _PdfPreviewState extends State<_PdfPreview> {
|
||||
|
||||
class _TextPreview extends StatefulWidget {
|
||||
final NextcloudItem item;
|
||||
final ServerProvider provider;
|
||||
final SessionController session;
|
||||
|
||||
const _TextPreview({required this.item, required this.provider});
|
||||
const _TextPreview({required this.item, required this.session});
|
||||
|
||||
@override
|
||||
State<_TextPreview> createState() => _TextPreviewState();
|
||||
@@ -1053,7 +1058,7 @@ class _TextPreviewState extends State<_TextPreview> {
|
||||
|
||||
Future<void> _load() async {
|
||||
try {
|
||||
final bytes = await widget.provider.service!.fetchBytes(widget.item.path);
|
||||
final bytes = await widget.session.service!.fetchBytes(widget.item.path);
|
||||
if (mounted) {
|
||||
setState(() => _content = utf8.decode(bytes, allowMalformed: true));
|
||||
}
|
||||
|
||||
+137
-137
@@ -9,7 +9,12 @@ import 'package:provider/provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import '../models/nextcloud_item.dart';
|
||||
import '../models/selection_action.dart';
|
||||
import '../providers/server_provider.dart';
|
||||
import '../providers/files_controller.dart';
|
||||
import '../providers/item_operations.dart';
|
||||
import '../providers/pick_controller.dart';
|
||||
import '../providers/session_controller.dart';
|
||||
import '../providers/settings_controller.dart';
|
||||
import '../providers/sync_status_controller.dart';
|
||||
import '../services/download_service.dart';
|
||||
import '../services/share_intent_service.dart';
|
||||
import '../widgets/breadcrumbs.dart';
|
||||
@@ -204,16 +209,14 @@ class _FilesViewState extends State<FilesView>
|
||||
/// folders are still browsable, a matching file either toggles selection
|
||||
/// (multi-select requests) or immediately finishes the pick, and a
|
||||
/// non-matching file (wrong mime type for the caller) is rejected.
|
||||
void _handlePickTap(
|
||||
BuildContext context,
|
||||
ServerProvider provider,
|
||||
NextcloudItem item,
|
||||
) {
|
||||
void _handlePickTap(BuildContext context, NextcloudItem item) {
|
||||
final files = context.read<FilesController>();
|
||||
final pick = context.read<PickController>();
|
||||
if (item.isFolder) {
|
||||
provider.navigateToFolder(item.path);
|
||||
files.navigateToFolder(item.path);
|
||||
return;
|
||||
}
|
||||
if (!provider.itemMatchesPickFilter(item)) {
|
||||
if (!pick.itemMatchesPickFilter(item)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("This app can't accept this file type"),
|
||||
@@ -222,25 +225,27 @@ class _FilesViewState extends State<FilesView>
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (provider.pickRequest!.allowMultiple) {
|
||||
if (pick.pickRequest!.allowMultiple) {
|
||||
_toggleSelection(item);
|
||||
} else {
|
||||
provider.confirmPick([item]);
|
||||
pick.confirmPick([item]);
|
||||
}
|
||||
}
|
||||
|
||||
/// The bulk actions shown in the sticky selection toolbar for the
|
||||
/// currently-selected items.
|
||||
List<SelectionAction> _buildSelectionActions(
|
||||
ServerProvider provider,
|
||||
BuildContext context,
|
||||
List<NextcloudItem> selected,
|
||||
) {
|
||||
if (provider.isPicking) {
|
||||
final pick = context.watch<PickController>();
|
||||
final sync = context.watch<SyncStatusController>();
|
||||
if (pick.isPicking) {
|
||||
return [
|
||||
SelectionAction(
|
||||
icon: Icons.check_rounded,
|
||||
label: 'Use ${selected.length} item(s)',
|
||||
onTap: () => provider.confirmPick(selected),
|
||||
onTap: () => pick.confirmPick(selected),
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -252,52 +257,52 @@ class _FilesViewState extends State<FilesView>
|
||||
label: selected.every((i) => i.isFavorite)
|
||||
? 'Remove from favorites'
|
||||
: 'Favorite',
|
||||
onTap: () => _favoriteSelected(provider, selected),
|
||||
onTap: () => _favoriteSelected(context, selected),
|
||||
),
|
||||
SelectionAction(
|
||||
icon: Icons.share_rounded,
|
||||
label: 'Share',
|
||||
onTap: () => selected.length == 1
|
||||
? ShareSheet.show(context, selected.single)
|
||||
: _shareSelected(context, provider, selected),
|
||||
: _shareSelected(context, selected),
|
||||
),
|
||||
SelectionAction(
|
||||
icon: Icons.download_rounded,
|
||||
label: 'Download',
|
||||
onTap: () => _downloadSelected(context, provider, selected),
|
||||
onTap: () => _downloadSelected(context, selected),
|
||||
),
|
||||
SelectionAction(
|
||||
icon: Icons.delete_outline_rounded,
|
||||
label: 'Delete',
|
||||
onTap: () => _confirmDeleteSelected(context, provider, selected),
|
||||
onTap: () => _confirmDeleteSelected(context, selected),
|
||||
),
|
||||
SelectionAction(
|
||||
icon: Icons.copy_rounded,
|
||||
label: 'Copy',
|
||||
onTap: () => _moveOrCopySelected(provider, selected, copy: true),
|
||||
onTap: () => _moveOrCopySelected(context, selected, copy: true),
|
||||
),
|
||||
SelectionAction(
|
||||
icon: Icons.drive_file_move_rounded,
|
||||
label: 'Move',
|
||||
onTap: () => _moveOrCopySelected(provider, selected, copy: false),
|
||||
onTap: () => _moveOrCopySelected(context, selected, copy: false),
|
||||
),
|
||||
if (selected.length == 1)
|
||||
SelectionAction(
|
||||
icon: Icons.drive_file_rename_outline_rounded,
|
||||
label: 'Rename',
|
||||
onTap: () => _renameItem(provider, selected.single),
|
||||
onTap: () => _renameItem(selected.single),
|
||||
),
|
||||
if (selected.length == 1)
|
||||
SelectionAction(
|
||||
icon: provider.isPathSynced(selected.single.path)
|
||||
icon: sync.isPathSynced(selected.single.path)
|
||||
? Icons.sync_rounded
|
||||
: Icons.sync_outlined,
|
||||
label: provider.isPathSynced(selected.single.path)
|
||||
label: sync.isPathSynced(selected.single.path)
|
||||
? 'Stop syncing to device'
|
||||
: 'Sync to device',
|
||||
onTap: () => provider.isPathSynced(selected.single.path)
|
||||
? provider.removeSyncedPath(selected.single.path)
|
||||
: provider.addSyncedPath(selected.single.path),
|
||||
onTap: () => sync.isPathSynced(selected.single.path)
|
||||
? sync.removeSyncedPath(selected.single.path)
|
||||
: sync.addSyncedPath(selected.single.path),
|
||||
),
|
||||
if (selected.length == 1)
|
||||
SelectionAction(
|
||||
@@ -308,7 +313,7 @@ class _FilesViewState extends State<FilesView>
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> _renameItem(ServerProvider provider, NextcloudItem item) async {
|
||||
Future<void> _renameItem(NextcloudItem item) async {
|
||||
final controller = TextEditingController(text: item.name);
|
||||
final newName = await showDialog<String>(
|
||||
context: context,
|
||||
@@ -345,7 +350,8 @@ class _FilesViewState extends State<FilesView>
|
||||
if (!mounted) return;
|
||||
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final success = await provider.renameItem(item, newName);
|
||||
final ops = context.read<ItemOperations>();
|
||||
final success = await ops.renameItem(item, newName);
|
||||
_clearSelection();
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
@@ -359,9 +365,9 @@ class _FilesViewState extends State<FilesView>
|
||||
|
||||
Future<void> _shareSelected(
|
||||
BuildContext context,
|
||||
ServerProvider provider,
|
||||
List<NextcloudItem> items,
|
||||
) async {
|
||||
final ops = context.read<ItemOperations>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(
|
||||
@@ -372,7 +378,7 @@ class _FilesViewState extends State<FilesView>
|
||||
|
||||
final lines = <String>[];
|
||||
for (final item in items) {
|
||||
final link = await provider.createShareLink(item);
|
||||
final link = await ops.createShareLink(item);
|
||||
if (link != null) {
|
||||
lines.add(items.length > 1 ? '${item.name}: $link' : link);
|
||||
}
|
||||
@@ -408,14 +414,14 @@ class _FilesViewState extends State<FilesView>
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final provider = context.watch<ServerProvider>();
|
||||
final files = context.watch<FilesController>();
|
||||
|
||||
final pathDepth = provider.pathStack.length;
|
||||
final pathDepth = files.pathStack.length;
|
||||
final navigatingDeeper = pathDepth > _lastPathDepth;
|
||||
_lastPathDepth = pathDepth;
|
||||
|
||||
final hasBreadcrumbs = provider.pathStack.length > 1;
|
||||
final selectedItems = provider.items
|
||||
final hasBreadcrumbs = files.pathStack.length > 1;
|
||||
final selectedItems = files.items
|
||||
.where((i) => _selectedIds.contains(i.id))
|
||||
.toList();
|
||||
|
||||
@@ -445,16 +451,16 @@ class _FilesViewState extends State<FilesView>
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
provider.filesSortAscending
|
||||
files.filesSortAscending
|
||||
? Icons.arrow_upward_rounded
|
||||
: Icons.arrow_downward_rounded,
|
||||
size: 20,
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
tooltip: provider.filesSortAscending
|
||||
tooltip: files.filesSortAscending
|
||||
? 'Ascending'
|
||||
: 'Descending',
|
||||
onPressed: provider.toggleFilesSortOrder,
|
||||
onPressed: files.toggleFilesSortOrder,
|
||||
),
|
||||
// A plain width, not Expanded - this row scrolls
|
||||
// horizontally now, which gives every control unbounded
|
||||
@@ -462,16 +468,16 @@ class _FilesViewState extends State<FilesView>
|
||||
SizedBox(
|
||||
width: 130,
|
||||
child: SortMenuButton(
|
||||
field: provider.filesSortField,
|
||||
onChanged: provider.setFilesSortField,
|
||||
field: files.filesSortField,
|
||||
onChanged: files.setFilesSortField,
|
||||
),
|
||||
),
|
||||
ToggleIconButton(
|
||||
icon: provider.showHiddenFiles
|
||||
icon: files.showHiddenFiles
|
||||
? Icons.visibility_rounded
|
||||
: Icons.visibility_off_rounded,
|
||||
isSelected: provider.showHiddenFiles,
|
||||
onTap: () => provider.toggleShowHiddenFiles(),
|
||||
isSelected: files.showHiddenFiles,
|
||||
onTap: () => files.toggleShowHiddenFiles(),
|
||||
tooltip: 'Show hidden files',
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
@@ -479,17 +485,17 @@ class _FilesViewState extends State<FilesView>
|
||||
children: [
|
||||
ToggleIconButton(
|
||||
icon: Symbols.circles_rounded,
|
||||
isSelected: provider.storageScope == StorageScope.cloud,
|
||||
isSelected: files.storageScope == StorageScope.cloud,
|
||||
onTap: () =>
|
||||
provider.setStorageScope(StorageScope.cloud),
|
||||
files.setStorageScope(StorageScope.cloud),
|
||||
tooltip: 'Cloud storage',
|
||||
),
|
||||
ToggleIconButton(
|
||||
icon: Symbols.hard_drive_rounded,
|
||||
isSelected:
|
||||
provider.storageScope == StorageScope.external,
|
||||
files.storageScope == StorageScope.external,
|
||||
onTap: () =>
|
||||
provider.setStorageScope(StorageScope.external),
|
||||
files.setStorageScope(StorageScope.external),
|
||||
tooltip: 'External storage',
|
||||
),
|
||||
],
|
||||
@@ -500,17 +506,17 @@ class _FilesViewState extends State<FilesView>
|
||||
ToggleIconButton(
|
||||
icon: Icons.select_all_rounded,
|
||||
isSelected:
|
||||
provider.filesTypeFilter == FilesTypeFilter.all,
|
||||
files.filesTypeFilter == FilesTypeFilter.all,
|
||||
onTap: () =>
|
||||
provider.setFilesTypeFilter(FilesTypeFilter.all),
|
||||
files.setFilesTypeFilter(FilesTypeFilter.all),
|
||||
tooltip: 'Files & folders',
|
||||
),
|
||||
ToggleIconButton(
|
||||
icon: Icons.insert_drive_file_outlined,
|
||||
isSelected:
|
||||
provider.filesTypeFilter ==
|
||||
files.filesTypeFilter ==
|
||||
FilesTypeFilter.filesOnly,
|
||||
onTap: () => provider.setFilesTypeFilter(
|
||||
onTap: () => files.setFilesTypeFilter(
|
||||
FilesTypeFilter.filesOnly,
|
||||
),
|
||||
tooltip: 'Files only',
|
||||
@@ -518,9 +524,9 @@ class _FilesViewState extends State<FilesView>
|
||||
ToggleIconButton(
|
||||
icon: Icons.folder_outlined,
|
||||
isSelected:
|
||||
provider.filesTypeFilter ==
|
||||
files.filesTypeFilter ==
|
||||
FilesTypeFilter.foldersOnly,
|
||||
onTap: () => provider.setFilesTypeFilter(
|
||||
onTap: () => files.setFilesTypeFilter(
|
||||
FilesTypeFilter.foldersOnly,
|
||||
),
|
||||
tooltip: 'Folders only',
|
||||
@@ -532,14 +538,14 @@ class _FilesViewState extends State<FilesView>
|
||||
children: [
|
||||
ToggleIconButton(
|
||||
icon: Icons.view_list_rounded,
|
||||
isSelected: !provider.isGridView,
|
||||
onTap: () => provider.setGridView(false),
|
||||
isSelected: !files.isGridView,
|
||||
onTap: () => files.setGridView(false),
|
||||
tooltip: 'List view',
|
||||
),
|
||||
ToggleIconButton(
|
||||
icon: Icons.grid_view_rounded,
|
||||
isSelected: provider.isGridView,
|
||||
onTap: () => provider.setGridView(true),
|
||||
isSelected: files.isGridView,
|
||||
onTap: () => files.setGridView(true),
|
||||
tooltip: 'Grid view',
|
||||
),
|
||||
],
|
||||
@@ -553,8 +559,8 @@ class _FilesViewState extends State<FilesView>
|
||||
SizedBox(
|
||||
height: 32,
|
||||
child: Breadcrumbs(
|
||||
pathStack: provider.pathStack,
|
||||
onTap: (index) => provider.navigateToPathIndex(index),
|
||||
pathStack: files.pathStack,
|
||||
onTap: (index) => files.navigateToPathIndex(index),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -574,12 +580,12 @@ class _FilesViewState extends State<FilesView>
|
||||
),
|
||||
),
|
||||
// Files List / Grid
|
||||
if (provider.isLoading)
|
||||
if (files.isLoading)
|
||||
const SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
else if (provider.errorMessage != null)
|
||||
else if (files.errorMessage != null)
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(
|
||||
@@ -603,7 +609,7 @@ class _FilesViewState extends State<FilesView>
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
provider.errorMessage!,
|
||||
files.errorMessage!,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
@@ -611,7 +617,7 @@ class _FilesViewState extends State<FilesView>
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.icon(
|
||||
onPressed: provider.refreshData,
|
||||
onPressed: files.refreshData,
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
label: const Text('Retry Connection'),
|
||||
),
|
||||
@@ -620,7 +626,7 @@ class _FilesViewState extends State<FilesView>
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (provider.items.isEmpty)
|
||||
else if (files.items.isEmpty)
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(
|
||||
@@ -643,7 +649,7 @@ class _FilesViewState extends State<FilesView>
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (provider.isGridView)
|
||||
else if (files.isGridView)
|
||||
SliverPadding(
|
||||
key: const ValueKey('files-grid'),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
@@ -655,14 +661,14 @@ class _FilesViewState extends State<FilesView>
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final item = provider.items[index];
|
||||
final item = files.items[index];
|
||||
return _FolderEnterAnimation(
|
||||
key: ValueKey('${provider.currentFolderPath}::${item.id}'),
|
||||
key: ValueKey('${files.currentFolderPath}::${item.id}'),
|
||||
index: index,
|
||||
fromRight: navigatingDeeper,
|
||||
child: _buildGridCard(context, item, provider),
|
||||
child: _buildGridCard(context, item),
|
||||
);
|
||||
}, childCount: provider.items.length),
|
||||
}, childCount: files.items.length),
|
||||
),
|
||||
)
|
||||
else
|
||||
@@ -671,14 +677,14 @@ class _FilesViewState extends State<FilesView>
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final item = provider.items[index];
|
||||
final item = files.items[index];
|
||||
return _FolderEnterAnimation(
|
||||
key: ValueKey('${provider.currentFolderPath}::${item.id}'),
|
||||
key: ValueKey('${files.currentFolderPath}::${item.id}'),
|
||||
index: index,
|
||||
fromRight: navigatingDeeper,
|
||||
child: _buildListTile(context, item, provider),
|
||||
child: _buildListTile(context, item),
|
||||
);
|
||||
}, childCount: provider.items.length),
|
||||
}, childCount: files.items.length),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -693,29 +699,28 @@ class _FilesViewState extends State<FilesView>
|
||||
];
|
||||
|
||||
return PopScope(
|
||||
canPop: !_isSelecting && provider.pathStack.length <= 1,
|
||||
canPop: !_isSelecting && files.pathStack.length <= 1,
|
||||
onPopInvokedWithResult: (didPop, result) {
|
||||
if (didPop) return;
|
||||
if (_isSelecting) {
|
||||
_clearSelection();
|
||||
} else {
|
||||
provider.navigateUp();
|
||||
files.navigateUp();
|
||||
}
|
||||
},
|
||||
child: SyncedHeaderScaffold(
|
||||
scrollController: widget.scrollController,
|
||||
provider: provider,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
tooltip: 'New',
|
||||
onPressed: () => _showCreateMenu(context, provider),
|
||||
onPressed: () => _showCreateMenu(context),
|
||||
),
|
||||
const MoreTabsButton(),
|
||||
const ProfileAvatarButton(),
|
||||
],
|
||||
selectionBar: _isSelecting
|
||||
? _buildSelectionBar(context, theme, provider, selectedItems)
|
||||
? _buildSelectionBar(context, theme, selectedItems)
|
||||
: null,
|
||||
contentSlivers: contentSlivers,
|
||||
),
|
||||
@@ -728,7 +733,6 @@ class _FilesViewState extends State<FilesView>
|
||||
Widget _buildSelectionBar(
|
||||
BuildContext context,
|
||||
ThemeData theme,
|
||||
ServerProvider provider,
|
||||
List<NextcloudItem> selectedItems,
|
||||
) {
|
||||
return Padding(
|
||||
@@ -771,7 +775,7 @@ class _FilesViewState extends State<FilesView>
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final action in _buildSelectionActions(
|
||||
provider,
|
||||
context,
|
||||
selectedItems,
|
||||
))
|
||||
IconButton(
|
||||
@@ -790,13 +794,14 @@ class _FilesViewState extends State<FilesView>
|
||||
}
|
||||
|
||||
Future<void> _favoriteSelected(
|
||||
ServerProvider provider,
|
||||
BuildContext context,
|
||||
List<NextcloudItem> items,
|
||||
) async {
|
||||
final ops = context.read<ItemOperations>();
|
||||
final allFavorited = items.every((i) => i.isFavorite);
|
||||
for (final item in items) {
|
||||
if (item.isFavorite == allFavorited) {
|
||||
await provider.toggleItemFavorite(item);
|
||||
await ops.toggleItemFavorite(item);
|
||||
}
|
||||
}
|
||||
_clearSelection();
|
||||
@@ -810,9 +815,10 @@ class _FilesViewState extends State<FilesView>
|
||||
/// notification for the whole selection instead of blocking here.
|
||||
Future<void> _downloadSelected(
|
||||
BuildContext context,
|
||||
ServerProvider provider,
|
||||
List<NextcloudItem> items,
|
||||
) async {
|
||||
final session = context.read<SessionController>();
|
||||
final sync = context.read<SyncStatusController>();
|
||||
final files = items.where((i) => !i.isFolder).toList();
|
||||
_clearSelection();
|
||||
if (files.isEmpty) return;
|
||||
@@ -822,9 +828,9 @@ class _FilesViewState extends State<FilesView>
|
||||
// Already mirrored locally by device sync (every selected file, not
|
||||
// just some)? Save straight from the local copy instead of a fresh
|
||||
// network fetch through DownloadService - see
|
||||
// ServerProvider.localSyncedFilePath.
|
||||
// SyncStatusController.localSyncedFilePath.
|
||||
final localPaths = await Future.wait(
|
||||
files.map(provider.localSyncedFilePath),
|
||||
files.map(sync.localSyncedFilePath),
|
||||
);
|
||||
if (localPaths.every((path) => path != null)) {
|
||||
try {
|
||||
@@ -859,7 +865,7 @@ class _FilesViewState extends State<FilesView>
|
||||
}
|
||||
|
||||
try {
|
||||
await DownloadService.startDownload(provider, files);
|
||||
await DownloadService.startDownload(session, files);
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
@@ -882,7 +888,6 @@ class _FilesViewState extends State<FilesView>
|
||||
|
||||
Future<void> _confirmDeleteSelected(
|
||||
BuildContext context,
|
||||
ServerProvider provider,
|
||||
List<NextcloudItem> items,
|
||||
) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
@@ -912,9 +917,10 @@ class _FilesViewState extends State<FilesView>
|
||||
if (confirmed != true || !context.mounted) return;
|
||||
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final ops = context.read<ItemOperations>();
|
||||
var succeeded = 0;
|
||||
for (final item in items) {
|
||||
final success = await provider.deleteItem(item.path);
|
||||
final success = await ops.deleteItem(item.path);
|
||||
if (success) succeeded++;
|
||||
}
|
||||
|
||||
@@ -928,7 +934,7 @@ class _FilesViewState extends State<FilesView>
|
||||
}
|
||||
|
||||
Future<void> _moveOrCopySelected(
|
||||
ServerProvider provider,
|
||||
BuildContext context,
|
||||
List<NextcloudItem> items, {
|
||||
required bool copy,
|
||||
}) async {
|
||||
@@ -937,7 +943,7 @@ class _FilesViewState extends State<FilesView>
|
||||
items,
|
||||
copy: copy,
|
||||
);
|
||||
if (!mounted || result == null) return;
|
||||
if (!context.mounted || result == null) return;
|
||||
_clearSelection();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
@@ -949,13 +955,15 @@ class _FilesViewState extends State<FilesView>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildListTile(
|
||||
BuildContext context,
|
||||
NextcloudItem item,
|
||||
ServerProvider provider,
|
||||
) {
|
||||
Widget _buildListTile(BuildContext context, NextcloudItem item) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final files = context.watch<FilesController>();
|
||||
final pick = context.watch<PickController>();
|
||||
final sync = context.watch<SyncStatusController>();
|
||||
final session = context.watch<SessionController>();
|
||||
final settings = context.watch<SettingsController>();
|
||||
final ops = context.read<ItemOperations>();
|
||||
final isSelected = _selectedIds.contains(item.id);
|
||||
|
||||
// No border radius here — the outer ClipRRect below is the only place
|
||||
@@ -968,17 +976,17 @@ class _FilesViewState extends State<FilesView>
|
||||
: colorScheme.surfaceContainerLow,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (provider.isPicking) {
|
||||
_handlePickTap(context, provider, item);
|
||||
if (pick.isPicking) {
|
||||
_handlePickTap(context, item);
|
||||
} else if (_isSelecting) {
|
||||
_toggleSelection(item);
|
||||
} else if (item.isFolder) {
|
||||
provider.navigateToFolder(item.path);
|
||||
files.navigateToFolder(item.path);
|
||||
} else {
|
||||
_openFile(context, item, provider);
|
||||
_openFile(context, item);
|
||||
}
|
||||
},
|
||||
onLongPress: provider.isPicking && !provider.pickRequest!.allowMultiple
|
||||
onLongPress: pick.isPicking && !pick.pickRequest!.allowMultiple
|
||||
? null
|
||||
: () => _toggleSelection(item),
|
||||
child: Padding(
|
||||
@@ -994,7 +1002,7 @@ class _FilesViewState extends State<FilesView>
|
||||
checkmarkSize: 24,
|
||||
child: ItemThumbnail(
|
||||
item: item,
|
||||
provider: provider,
|
||||
service: session.service,
|
||||
size: 44,
|
||||
borderRadius: 12,
|
||||
iconSize: 22,
|
||||
@@ -1004,7 +1012,7 @@ class _FilesViewState extends State<FilesView>
|
||||
right: -2,
|
||||
bottom: -2,
|
||||
child: SyncStatusBadge(
|
||||
status: provider.syncStatusFor(item),
|
||||
status: sync.syncStatusFor(item),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1039,15 +1047,15 @@ class _FilesViewState extends State<FilesView>
|
||||
),
|
||||
);
|
||||
|
||||
final content = _isSelecting || provider.isPicking
|
||||
final content = _isSelecting || pick.isPicking
|
||||
? card
|
||||
: SwipeableItem(
|
||||
itemKey: ValueKey('file-${item.id}'),
|
||||
itemName: item.name,
|
||||
provider: provider,
|
||||
onFavorite: () => provider.toggleItemFavorite(item),
|
||||
settings: settings,
|
||||
onFavorite: () => ops.toggleItemFavorite(item),
|
||||
onShare: () => ShareSheet.show(context, item),
|
||||
onDelete: () => _deleteViaSwipe(provider, item),
|
||||
onDelete: () => _deleteViaSwipe(item),
|
||||
child: card,
|
||||
);
|
||||
|
||||
@@ -1057,11 +1065,9 @@ class _FilesViewState extends State<FilesView>
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _deleteViaSwipe(
|
||||
ServerProvider provider,
|
||||
NextcloudItem item,
|
||||
) async {
|
||||
final success = await provider.deleteItem(item.path);
|
||||
Future<void> _deleteViaSwipe(NextcloudItem item) async {
|
||||
final ops = context.read<ItemOperations>();
|
||||
final success = await ops.deleteItem(item.path);
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
@@ -1073,11 +1079,10 @@ class _FilesViewState extends State<FilesView>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGridCard(
|
||||
BuildContext context,
|
||||
NextcloudItem item,
|
||||
ServerProvider provider,
|
||||
) {
|
||||
Widget _buildGridCard(BuildContext context, NextcloudItem item) {
|
||||
final files = context.watch<FilesController>();
|
||||
final pick = context.watch<PickController>();
|
||||
final sync = context.watch<SyncStatusController>();
|
||||
final isSelected = _selectedIds.contains(item.id);
|
||||
final isMedia =
|
||||
(item.type == NextcloudItemType.image ||
|
||||
@@ -1090,17 +1095,17 @@ class _FilesViewState extends State<FilesView>
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (provider.isPicking) {
|
||||
_handlePickTap(context, provider, item);
|
||||
if (pick.isPicking) {
|
||||
_handlePickTap(context, item);
|
||||
} else if (_isSelecting) {
|
||||
_toggleSelection(item);
|
||||
} else if (item.isFolder) {
|
||||
provider.navigateToFolder(item.path);
|
||||
files.navigateToFolder(item.path);
|
||||
} else {
|
||||
_openFile(context, item, provider);
|
||||
_openFile(context, item);
|
||||
}
|
||||
},
|
||||
onLongPress: provider.isPicking && !provider.pickRequest!.allowMultiple
|
||||
onLongPress: pick.isPicking && !pick.pickRequest!.allowMultiple
|
||||
? null
|
||||
: () => _toggleSelection(item),
|
||||
child: Stack(
|
||||
@@ -1109,13 +1114,13 @@ class _FilesViewState extends State<FilesView>
|
||||
isSelected: isSelected,
|
||||
checkmarkSize: 32,
|
||||
child: isMedia
|
||||
? _buildMediaGridContent(context, item, provider)
|
||||
? _buildMediaGridContent(context, item)
|
||||
: _buildPlainGridContent(context, item),
|
||||
),
|
||||
Positioned(
|
||||
right: 6,
|
||||
bottom: 6,
|
||||
child: SyncStatusBadge(status: provider.syncStatusFor(item)),
|
||||
child: SyncStatusBadge(status: sync.syncStatusFor(item)),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1126,11 +1131,8 @@ class _FilesViewState extends State<FilesView>
|
||||
/// Grid content for images/videos: the actual preview fills the whole
|
||||
/// card as a background, with the name/size legible over a bottom scrim
|
||||
/// — matching a Google Photos-style grid instead of a small icon badge.
|
||||
Widget _buildMediaGridContent(
|
||||
BuildContext context,
|
||||
NextcloudItem item,
|
||||
ServerProvider provider,
|
||||
) {
|
||||
Widget _buildMediaGridContent(BuildContext context, NextcloudItem item) {
|
||||
final session = context.watch<SessionController>();
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return LayoutBuilder(
|
||||
@@ -1143,7 +1145,7 @@ class _FilesViewState extends State<FilesView>
|
||||
children: [
|
||||
Image.network(
|
||||
item.previewUrl!,
|
||||
headers: provider.service?.authHeaders,
|
||||
headers: session.service?.authHeaders,
|
||||
fit: BoxFit.cover,
|
||||
cacheWidth: cachePixels,
|
||||
cacheHeight: cachePixels,
|
||||
@@ -1253,18 +1255,15 @@ class _FilesViewState extends State<FilesView>
|
||||
);
|
||||
}
|
||||
|
||||
void _openFile(
|
||||
BuildContext context,
|
||||
NextcloudItem item,
|
||||
ServerProvider provider,
|
||||
) {
|
||||
void _openFile(BuildContext context, NextcloudItem item) {
|
||||
final files = context.read<FilesController>();
|
||||
Navigator.push(
|
||||
context,
|
||||
FileViewerScreen.route(item: item, siblings: provider.items),
|
||||
FileViewerScreen.route(item: item, siblings: files.items),
|
||||
);
|
||||
}
|
||||
|
||||
void _showCreateMenu(BuildContext context, ServerProvider provider) {
|
||||
void _showCreateMenu(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
@@ -1293,7 +1292,7 @@ class _FilesViewState extends State<FilesView>
|
||||
title: const Text('New Folder'),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
_showCreateFolderDialog(context, provider);
|
||||
_showCreateFolderDialog(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -1304,7 +1303,8 @@ class _FilesViewState extends State<FilesView>
|
||||
);
|
||||
}
|
||||
|
||||
void _showCreateFolderDialog(BuildContext context, ServerProvider provider) {
|
||||
void _showCreateFolderDialog(BuildContext context) {
|
||||
final files = context.read<FilesController>();
|
||||
final controller = TextEditingController();
|
||||
|
||||
showDialog(
|
||||
@@ -1329,7 +1329,7 @@ class _FilesViewState extends State<FilesView>
|
||||
final name = controller.text.trim();
|
||||
if (name.isNotEmpty) {
|
||||
Navigator.pop(context);
|
||||
final success = await provider.createFolder(name);
|
||||
final success = await files.createFolder(name);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/server_provider.dart';
|
||||
import '../providers/session_controller.dart';
|
||||
|
||||
/// Shown in place of the main shell whenever [ServerProvider.needsUnlock] is
|
||||
/// Shown in place of the main shell whenever [SessionController.needsUnlock] is
|
||||
/// true - a fresh app launch with login lock configured, or returning to the
|
||||
/// foreground after being backgrounded. Prompts automatically on first show
|
||||
/// and again whenever the app resumes while still locked (e.g. the user
|
||||
@@ -36,13 +36,13 @@ class _LockScreenViewState extends State<LockScreenView>
|
||||
if (state != AppLifecycleState.resumed || !mounted || _authenticating) {
|
||||
return;
|
||||
}
|
||||
if (context.read<ServerProvider>().needsUnlock) _attemptUnlock();
|
||||
if (context.read<SessionController>().needsUnlock) _attemptUnlock();
|
||||
}
|
||||
|
||||
Future<void> _attemptUnlock() async {
|
||||
if (_authenticating || !mounted) return;
|
||||
setState(() => _authenticating = true);
|
||||
await context.read<ServerProvider>().attemptUnlock();
|
||||
await context.read<SessionController>().attemptUnlock();
|
||||
if (mounted) setState(() => _authenticating = false);
|
||||
}
|
||||
|
||||
|
||||
+23
-23
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/saved_account.dart';
|
||||
import '../providers/server_provider.dart';
|
||||
import '../providers/session_controller.dart';
|
||||
import 'login_webview_view.dart';
|
||||
|
||||
class LoginView extends StatefulWidget {
|
||||
@@ -28,7 +28,7 @@ class _LoginViewState extends State<LoginView> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.isAddingAccount) {
|
||||
_originalActiveAccountId = context.read<ServerProvider>().activeAccountId;
|
||||
_originalActiveAccountId = context.read<SessionController>().activeAccountId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,13 +41,13 @@ class _LoginViewState extends State<LoginView> {
|
||||
void _handleContinue() {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
FocusScope.of(context).unfocus();
|
||||
context.read<ServerProvider>().startLoginFlow(
|
||||
context.read<SessionController>().startLoginFlow(
|
||||
_urlController.text.trim(),
|
||||
addAccount: widget.isAddingAccount,
|
||||
);
|
||||
}
|
||||
|
||||
/// Resumes a saved account with one tap (see [ServerProvider.switchAccount]).
|
||||
/// Resumes a saved account with one tap (see [SessionController.switchAccount]).
|
||||
/// That can fail silently from the account's own perspective - most often
|
||||
/// a stored app password that no longer works (revoked server-side, or
|
||||
/// left over from before a since-fixed bug that deleted it too eagerly on
|
||||
@@ -55,8 +55,8 @@ class _LoginViewState extends State<LoginView> {
|
||||
/// silently failing every time - a delete button on the row itself (see
|
||||
/// [_SavedAccountRow]) is the way out.
|
||||
Future<void> _continueAsAccount(SavedAccount account) async {
|
||||
final provider = context.read<ServerProvider>();
|
||||
final success = await provider.switchAccount(account.id);
|
||||
final session = context.read<SessionController>();
|
||||
final success = await session.switchAccount(account.id);
|
||||
if (success || !mounted) return;
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
@@ -98,25 +98,25 @@ class _LoginViewState extends State<LoginView> {
|
||||
},
|
||||
);
|
||||
if (confirmed != true || !mounted) return;
|
||||
await context.read<ServerProvider>().removeAccount(account.id);
|
||||
await context.read<SessionController>().removeAccount(account.id);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final provider = context.watch<ServerProvider>();
|
||||
final session = context.watch<SessionController>();
|
||||
|
||||
final isAwaitingBrowser =
|
||||
provider.loginFlowStatus == LoginFlowStatus.awaitingBrowser;
|
||||
final isInitiating = provider.loginFlowStatus == LoginFlowStatus.initiating;
|
||||
session.loginFlowStatus == LoginFlowStatus.awaitingBrowser;
|
||||
final isInitiating = session.loginFlowStatus == LoginFlowStatus.initiating;
|
||||
|
||||
// A new/refreshed account has just become active - pop back to
|
||||
// Settings rather than leaving this form sitting on top of it.
|
||||
if (widget.isAddingAccount &&
|
||||
!_popped &&
|
||||
provider.loginFlowStatus == LoginFlowStatus.idle &&
|
||||
provider.activeAccountId != _originalActiveAccountId) {
|
||||
session.loginFlowStatus == LoginFlowStatus.idle &&
|
||||
session.activeAccountId != _originalActiveAccountId) {
|
||||
_popped = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
@@ -125,14 +125,14 @@ class _LoginViewState extends State<LoginView> {
|
||||
|
||||
// Every login (first account or an additional one) shows its login
|
||||
// page in LoginWebViewView, a real screen this app owns - see
|
||||
// ServerProvider.startLoginFlow's doc comment for why. Push it the
|
||||
// SessionController.startLoginFlow's doc comment for why. Push it the
|
||||
// moment there's a URL to show; _webViewPushed resets once that route
|
||||
// pops (cancelled or done) so a retry after cancelling pushes it again.
|
||||
if (!_webViewPushed &&
|
||||
isAwaitingBrowser &&
|
||||
provider.pendingLoginUrl != null) {
|
||||
session.pendingLoginUrl != null) {
|
||||
_webViewPushed = true;
|
||||
final url = provider.pendingLoginUrl!;
|
||||
final url = session.pendingLoginUrl!;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
if (!mounted) return;
|
||||
// A plain MaterialPageRoute's animated slide/fade transition can
|
||||
@@ -173,9 +173,9 @@ class _LoginViewState extends State<LoginView> {
|
||||
// from here with one tap - no need to repeat
|
||||
// Login Flow v2.
|
||||
if (!widget.isAddingAccount &&
|
||||
provider.accounts.isNotEmpty) ...[
|
||||
session.accounts.isNotEmpty) ...[
|
||||
_SavedAccountsSection(
|
||||
accounts: provider.accounts,
|
||||
accounts: session.accounts,
|
||||
onSelect: (account) => _continueAsAccount(account),
|
||||
onRemove: (account) => _confirmRemoveAccount(account),
|
||||
),
|
||||
@@ -201,10 +201,10 @@ class _LoginViewState extends State<LoginView> {
|
||||
formKey: _formKey,
|
||||
urlController: _urlController,
|
||||
isLoading:
|
||||
isInitiating || provider.isLoading || isAwaitingBrowser,
|
||||
isInitiating || session.isLoading || isAwaitingBrowser,
|
||||
errorMessage:
|
||||
provider.loginFlowStatus == LoginFlowStatus.error
|
||||
? provider.errorMessage
|
||||
session.loginFlowStatus == LoginFlowStatus.error
|
||||
? session.errorMessage
|
||||
: null,
|
||||
onContinue: _handleContinue,
|
||||
theme: theme,
|
||||
@@ -226,9 +226,9 @@ class _LoginViewState extends State<LoginView> {
|
||||
// gone - this view could never be popped before "add account" existed,
|
||||
// so that case wasn't reachable until now.
|
||||
return PopScope(
|
||||
canPop: provider.loginFlowStatus != LoginFlowStatus.awaitingBrowser,
|
||||
canPop: session.loginFlowStatus != LoginFlowStatus.awaitingBrowser,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (!didPop) provider.cancelLoginFlow();
|
||||
if (!didPop) session.cancelLoginFlow();
|
||||
},
|
||||
child: body,
|
||||
);
|
||||
@@ -392,7 +392,7 @@ class _ServerForm extends StatelessWidget {
|
||||
}
|
||||
|
||||
/// Saved accounts a logout left recoverable - tapping one resumes it via
|
||||
/// [ServerProvider.switchAccount] instead of repeating Login Flow v2.
|
||||
/// [SessionController.switchAccount] instead of repeating Login Flow v2.
|
||||
class _SavedAccountsSection extends StatelessWidget {
|
||||
final List<SavedAccount> accounts;
|
||||
final ValueChanged<SavedAccount> onSelect;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import '../providers/server_provider.dart';
|
||||
import '../providers/session_controller.dart';
|
||||
|
||||
/// Every Login Flow v2 page - first login or an additional account -
|
||||
/// rendered in Flutter's own WebView rather than a Chrome Custom Tab or
|
||||
@@ -13,7 +13,7 @@ import '../providers/server_provider.dart';
|
||||
/// exactly like every other screen in the app - and unlike a Custom Tab,
|
||||
/// this screen can close itself automatically once login succeeds.
|
||||
///
|
||||
/// See ServerProvider.startLoginFlow's doc comment for the full rationale.
|
||||
/// See SessionController.startLoginFlow's doc comment for the full rationale.
|
||||
class LoginWebViewView extends StatefulWidget {
|
||||
final Uri url;
|
||||
|
||||
@@ -56,19 +56,19 @@ class _LoginWebViewViewState extends State<LoginWebViewView> {
|
||||
}
|
||||
|
||||
void _cancel() {
|
||||
context.read<ServerProvider>().cancelLoginFlow();
|
||||
context.read<SessionController>().cancelLoginFlow();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<ServerProvider>();
|
||||
final session = context.watch<SessionController>();
|
||||
|
||||
// The login flow finished (success or error) or was cancelled
|
||||
// elsewhere - close this screen automatically rather than leaving it
|
||||
// sitting open over a flow that's no longer awaiting the browser.
|
||||
if (!_popped &&
|
||||
provider.loginFlowStatus != LoginFlowStatus.awaitingBrowser) {
|
||||
session.loginFlowStatus != LoginFlowStatus.awaitingBrowser) {
|
||||
_popped = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
|
||||
@@ -4,7 +4,9 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/move_copy_result.dart';
|
||||
import '../models/nextcloud_item.dart';
|
||||
import '../providers/server_provider.dart';
|
||||
import '../providers/files_controller.dart';
|
||||
import '../providers/item_operations.dart';
|
||||
import '../providers/session_controller.dart';
|
||||
import '../widgets/breadcrumbs.dart';
|
||||
import '../widgets/item_icon.dart';
|
||||
import '../widgets/more_tabs_button.dart';
|
||||
@@ -18,13 +20,13 @@ import '../widgets/synced_header_scaffold.dart';
|
||||
/// Destination-folder browser for moving/copying [items] (a multi-select
|
||||
/// batch from Files or Photos). Visually mirrors `ShareUploadView`'s
|
||||
/// browser (same chrome/controls/listing), but deliberately does **not**
|
||||
/// reuse `ServerProvider`'s shared `currentFolderPath`/`pathStack`/`items`
|
||||
/// reuse `FilesController`'s shared `currentFolderPath`/`pathStack`/`items`
|
||||
/// navigation state the way that screen does - this is pushed mid-browsing
|
||||
/// session (the user was already looking at a specific Files-tab folder
|
||||
/// when they selected items and tapped Move/Copy), so clobbering that
|
||||
/// shared state here would strand the Files tab in whatever folder this
|
||||
/// picker last visited. Instead it owns its own local navigation state and
|
||||
/// fetches through `ServerProvider.fetchFolderListing`, a stateless
|
||||
/// fetches through `FilesController.fetchFolderListing`, a stateless
|
||||
/// pass-through that never touches shared fields - `ShareUploadView` gets
|
||||
/// away with the shared state precisely because it always resets to root
|
||||
/// and pops all the way to the app root afterward (a cold share-intent
|
||||
@@ -83,7 +85,9 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
|
||||
Future<void> _fetch(String path) async {
|
||||
setState(() => _isLoading = true);
|
||||
final raw = await context.read<ServerProvider>().fetchFolderListing(path);
|
||||
final raw = await context.read<FilesController>().fetchFolderListing(
|
||||
path,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_rawItems = raw;
|
||||
@@ -109,7 +113,7 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Client-side mirror of `ServerProvider`'s own authoritative check (see
|
||||
/// Client-side mirror of `ItemOperations`' own authoritative check (see
|
||||
/// `_isSelfOrDescendant`) - just for disabling the confirm button with an
|
||||
/// explanation up front instead of letting the request round-trip and
|
||||
/// fail.
|
||||
@@ -123,11 +127,11 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> _confirm(ServerProvider provider) async {
|
||||
Future<void> _confirm(ItemOperations ops) async {
|
||||
setState(() => _isSubmitting = true);
|
||||
final result = widget.copy
|
||||
? await provider.copyItems(widget.items, _currentPath)
|
||||
: await provider.moveItems(widget.items, _currentPath);
|
||||
? await ops.copyItems(widget.items, _currentPath)
|
||||
: await ops.moveItems(widget.items, _currentPath);
|
||||
if (!mounted) return;
|
||||
|
||||
if (result.blockedReason != null) {
|
||||
@@ -151,7 +155,7 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (choices != null) {
|
||||
final resolved = await provider.resolveConflicts(
|
||||
final resolved = await ops.resolveConflicts(
|
||||
result.conflicts,
|
||||
_currentPath,
|
||||
copy: widget.copy,
|
||||
@@ -171,7 +175,7 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
).pop(MoveCopyResult(succeeded: succeeded, failed: failed));
|
||||
}
|
||||
|
||||
Widget _buildControlsRow(ServerProvider provider) {
|
||||
Widget _buildControlsRow(FilesController files) {
|
||||
return SizedBox(
|
||||
height: 44,
|
||||
child: SingleChildScrollView(
|
||||
@@ -180,28 +184,28 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
provider.filesSortAscending
|
||||
files.filesSortAscending
|
||||
? Icons.arrow_upward_rounded
|
||||
: Icons.arrow_downward_rounded,
|
||||
size: 20,
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
tooltip: provider.filesSortAscending ? 'Ascending' : 'Descending',
|
||||
onPressed: provider.toggleFilesSortOrder,
|
||||
tooltip: files.filesSortAscending ? 'Ascending' : 'Descending',
|
||||
onPressed: files.toggleFilesSortOrder,
|
||||
),
|
||||
SizedBox(
|
||||
width: 130,
|
||||
child: SortMenuButton(
|
||||
field: provider.filesSortField,
|
||||
onChanged: provider.setFilesSortField,
|
||||
field: files.filesSortField,
|
||||
onChanged: files.setFilesSortField,
|
||||
),
|
||||
),
|
||||
ToggleIconButton(
|
||||
icon: provider.showHiddenFiles
|
||||
icon: files.showHiddenFiles
|
||||
? Icons.visibility_rounded
|
||||
: Icons.visibility_off_rounded,
|
||||
isSelected: provider.showHiddenFiles,
|
||||
onTap: () => provider.toggleShowHiddenFiles(),
|
||||
isSelected: files.showHiddenFiles,
|
||||
onTap: () => files.toggleShowHiddenFiles(),
|
||||
tooltip: 'Show hidden files',
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
@@ -209,14 +213,14 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
children: [
|
||||
ToggleIconButton(
|
||||
icon: Symbols.circles_rounded,
|
||||
isSelected: provider.storageScope == StorageScope.cloud,
|
||||
onTap: () => provider.setStorageScope(StorageScope.cloud),
|
||||
isSelected: files.storageScope == StorageScope.cloud,
|
||||
onTap: () => files.setStorageScope(StorageScope.cloud),
|
||||
tooltip: 'Cloud storage',
|
||||
),
|
||||
ToggleIconButton(
|
||||
icon: Symbols.hard_drive_rounded,
|
||||
isSelected: provider.storageScope == StorageScope.external,
|
||||
onTap: () => provider.setStorageScope(StorageScope.external),
|
||||
isSelected: files.storageScope == StorageScope.external,
|
||||
onTap: () => files.setStorageScope(StorageScope.external),
|
||||
tooltip: 'External storage',
|
||||
),
|
||||
],
|
||||
@@ -226,14 +230,14 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
children: [
|
||||
ToggleIconButton(
|
||||
icon: Icons.view_list_rounded,
|
||||
isSelected: !provider.isGridView,
|
||||
onTap: () => provider.setGridView(false),
|
||||
isSelected: !files.isGridView,
|
||||
onTap: () => files.setGridView(false),
|
||||
tooltip: 'List view',
|
||||
),
|
||||
ToggleIconButton(
|
||||
icon: Icons.grid_view_rounded,
|
||||
isSelected: provider.isGridView,
|
||||
onTap: () => provider.setGridView(true),
|
||||
isSelected: files.isGridView,
|
||||
onTap: () => files.setGridView(true),
|
||||
tooltip: 'Grid view',
|
||||
),
|
||||
],
|
||||
@@ -248,9 +252,10 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
// the listing matches what the Files tab itself would show for this
|
||||
// folder) but are visually dimmed and inert, same treatment as
|
||||
// ShareUploadView's own destination browser.
|
||||
Widget _buildListTile(NextcloudItem item, ServerProvider provider) {
|
||||
Widget _buildListTile(NextcloudItem item) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final session = context.watch<SessionController>();
|
||||
final isFolder = item.isFolder;
|
||||
|
||||
return Padding(
|
||||
@@ -272,7 +277,7 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
children: [
|
||||
ItemThumbnail(
|
||||
item: item,
|
||||
provider: provider,
|
||||
service: session.service,
|
||||
size: 44,
|
||||
borderRadius: 12,
|
||||
iconSize: 22,
|
||||
@@ -312,7 +317,7 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGridCard(NextcloudItem item, ServerProvider provider) {
|
||||
Widget _buildGridCard(NextcloudItem item) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final isFolder = item.isFolder;
|
||||
@@ -379,12 +384,13 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final provider = context.watch<ServerProvider>();
|
||||
final files = context.watch<FilesController>();
|
||||
final ops = context.read<ItemOperations>();
|
||||
final hasBreadcrumbs = _pathStack.length > 1;
|
||||
final currentLabel = _currentPath == '/'
|
||||
? 'Home'
|
||||
: _currentPath.split('/').where((s) => s.isNotEmpty).last;
|
||||
final items = provider.applyFilesDisplayPrefs(_rawItems);
|
||||
final items = files.applyFilesDisplayPrefs(_rawItems);
|
||||
final invalidDestination = _destinationIsInvalid();
|
||||
|
||||
final controlsColumn = Padding(
|
||||
@@ -392,7 +398,7 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildControlsRow(provider),
|
||||
_buildControlsRow(files),
|
||||
if (hasBreadcrumbs) ...[
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
@@ -430,7 +436,7 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (provider.isGridView)
|
||||
else if (files.isGridView)
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
sliver: SliverGrid(
|
||||
@@ -441,7 +447,7 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return _buildGridCard(items[index], provider);
|
||||
return _buildGridCard(items[index]);
|
||||
}, childCount: items.length),
|
||||
),
|
||||
)
|
||||
@@ -450,7 +456,7 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return _buildListTile(items[index], provider);
|
||||
return _buildListTile(items[index]);
|
||||
}, childCount: items.length),
|
||||
),
|
||||
),
|
||||
@@ -465,7 +471,6 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
child: Scaffold(
|
||||
body: SyncedHeaderScaffold(
|
||||
scrollController: _scrollController,
|
||||
provider: provider,
|
||||
actions: const [MoreTabsButton(), ProfileAvatarButton()],
|
||||
contentSlivers: contentSlivers,
|
||||
),
|
||||
@@ -499,7 +504,7 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
child: FilledButton.icon(
|
||||
onPressed: invalidDestination || _isSubmitting
|
||||
? null
|
||||
: () => _confirm(provider),
|
||||
: () => _confirm(ops),
|
||||
icon: _isSubmitting
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
|
||||
+70
-70
@@ -5,7 +5,11 @@ import 'package:provider/provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import '../models/nextcloud_item.dart';
|
||||
import '../models/selection_action.dart';
|
||||
import '../providers/server_provider.dart';
|
||||
import '../providers/files_controller.dart';
|
||||
import '../providers/item_operations.dart';
|
||||
import '../providers/photos_controller.dart';
|
||||
import '../providers/pick_controller.dart';
|
||||
import '../providers/session_controller.dart';
|
||||
import '../services/download_service.dart';
|
||||
import '../widgets/details/details_sheet.dart';
|
||||
import '../widgets/more_tabs_button.dart';
|
||||
@@ -111,10 +115,10 @@ class _PhotosViewState extends State<PhotosView>
|
||||
/// just the matching/toggle/immediate-confirm branch.
|
||||
void _handlePickTap(
|
||||
BuildContext context,
|
||||
ServerProvider provider,
|
||||
PickController pick,
|
||||
NextcloudItem item,
|
||||
) {
|
||||
if (!provider.itemMatchesPickFilter(item)) {
|
||||
if (!pick.itemMatchesPickFilter(item)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("This app can't accept this file type"),
|
||||
@@ -123,25 +127,25 @@ class _PhotosViewState extends State<PhotosView>
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (provider.pickRequest!.allowMultiple) {
|
||||
if (pick.pickRequest!.allowMultiple) {
|
||||
_toggleSelection(item);
|
||||
} else {
|
||||
provider.confirmPick([item]);
|
||||
pick.confirmPick([item]);
|
||||
}
|
||||
}
|
||||
|
||||
/// The bulk actions shown in the sticky selection toolbar for the
|
||||
/// currently-selected items.
|
||||
List<SelectionAction> _buildSelectionActions(
|
||||
ServerProvider provider,
|
||||
PickController pick,
|
||||
List<NextcloudItem> selected,
|
||||
) {
|
||||
if (provider.isPicking) {
|
||||
if (pick.isPicking) {
|
||||
return [
|
||||
SelectionAction(
|
||||
icon: Icons.check_rounded,
|
||||
label: 'Use ${selected.length} item(s)',
|
||||
onTap: () => provider.confirmPick(selected),
|
||||
onTap: () => pick.confirmPick(selected),
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -153,34 +157,34 @@ class _PhotosViewState extends State<PhotosView>
|
||||
label: selected.every((i) => i.isFavorite)
|
||||
? 'Remove from favorites'
|
||||
: 'Favorite',
|
||||
onTap: () => _favoriteSelected(provider, selected),
|
||||
onTap: () => _favoriteSelected(selected),
|
||||
),
|
||||
SelectionAction(
|
||||
icon: Icons.share_rounded,
|
||||
label: 'Share',
|
||||
onTap: () => selected.length == 1
|
||||
? ShareSheet.show(context, selected.single)
|
||||
: _shareSelected(context, provider, selected),
|
||||
: _shareSelected(context, selected),
|
||||
),
|
||||
SelectionAction(
|
||||
icon: Icons.download_rounded,
|
||||
label: 'Download',
|
||||
onTap: () => _downloadSelected(context, provider, selected),
|
||||
onTap: () => _downloadSelected(context, selected),
|
||||
),
|
||||
SelectionAction(
|
||||
icon: Icons.delete_outline_rounded,
|
||||
label: 'Delete',
|
||||
onTap: () => _confirmDeleteSelected(context, provider, selected),
|
||||
onTap: () => _confirmDeleteSelected(context, selected),
|
||||
),
|
||||
SelectionAction(
|
||||
icon: Icons.copy_rounded,
|
||||
label: 'Copy',
|
||||
onTap: () => _moveOrCopySelected(provider, selected, copy: true),
|
||||
onTap: () => _moveOrCopySelected(selected, copy: true),
|
||||
),
|
||||
SelectionAction(
|
||||
icon: Icons.drive_file_move_rounded,
|
||||
label: 'Move',
|
||||
onTap: () => _moveOrCopySelected(provider, selected, copy: false),
|
||||
onTap: () => _moveOrCopySelected(selected, copy: false),
|
||||
),
|
||||
if (selected.length == 1)
|
||||
SelectionAction(
|
||||
@@ -195,16 +199,18 @@ class _PhotosViewState extends State<PhotosView>
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final provider = context.watch<ServerProvider>();
|
||||
final photosController = context.watch<PhotosController>();
|
||||
final filesController = context.watch<FilesController>();
|
||||
final pick = context.watch<PickController>();
|
||||
|
||||
if (!_requested) {
|
||||
_requested = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => provider.fetchAllMedia(),
|
||||
(_) => photosController.fetchAllMedia(),
|
||||
);
|
||||
}
|
||||
|
||||
final photos = provider.photoItems;
|
||||
final photos = photosController.items;
|
||||
final selectedItems = photos
|
||||
.where((i) => _selectedIds.contains(i.id))
|
||||
.toList();
|
||||
@@ -217,38 +223,38 @@ class _PhotosViewState extends State<PhotosView>
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
provider.photosSortAscending
|
||||
photosController.sortAscending
|
||||
? Icons.arrow_upward_rounded
|
||||
: Icons.arrow_downward_rounded,
|
||||
size: 20,
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
tooltip: provider.photosSortAscending
|
||||
tooltip: photosController.sortAscending
|
||||
? 'Ascending'
|
||||
: 'Descending',
|
||||
onPressed: provider.togglePhotosSortOrder,
|
||||
onPressed: photosController.toggleSortOrder,
|
||||
),
|
||||
Expanded(
|
||||
child: SortMenuButton(
|
||||
field: provider.photosSortField,
|
||||
onChanged: provider.setPhotosSortField,
|
||||
field: photosController.sortField,
|
||||
onChanged: photosController.setSortField,
|
||||
),
|
||||
),
|
||||
ToggleIconButton(
|
||||
icon: provider.showFavoritesOnlyPhotos
|
||||
icon: photosController.showFavoritesOnly
|
||||
? Icons.favorite_rounded
|
||||
: Icons.favorite_border_rounded,
|
||||
isSelected: provider.showFavoritesOnlyPhotos,
|
||||
onTap: provider.toggleFavoritesFilterPhotos,
|
||||
isSelected: photosController.showFavoritesOnly,
|
||||
onTap: photosController.toggleFavoritesFilter,
|
||||
tooltip: 'Favorites only',
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
ToggleIconButton(
|
||||
icon: provider.showHiddenPhotos
|
||||
icon: photosController.showHidden
|
||||
? Icons.visibility_rounded
|
||||
: Icons.visibility_off_rounded,
|
||||
isSelected: provider.showHiddenPhotos,
|
||||
onTap: () => provider.toggleShowHiddenPhotos(),
|
||||
isSelected: photosController.showHidden,
|
||||
onTap: () => photosController.toggleShowHidden(),
|
||||
tooltip: 'Show hidden files',
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
@@ -256,14 +262,17 @@ class _PhotosViewState extends State<PhotosView>
|
||||
children: [
|
||||
ToggleIconButton(
|
||||
icon: Symbols.circles_rounded,
|
||||
isSelected: provider.storageScope == StorageScope.cloud,
|
||||
onTap: () => provider.setStorageScope(StorageScope.cloud),
|
||||
isSelected: filesController.storageScope == StorageScope.cloud,
|
||||
onTap: () =>
|
||||
filesController.setStorageScope(StorageScope.cloud),
|
||||
tooltip: 'Cloud storage',
|
||||
),
|
||||
ToggleIconButton(
|
||||
icon: Symbols.hard_drive_rounded,
|
||||
isSelected: provider.storageScope == StorageScope.external,
|
||||
onTap: () => provider.setStorageScope(StorageScope.external),
|
||||
isSelected:
|
||||
filesController.storageScope == StorageScope.external,
|
||||
onTap: () =>
|
||||
filesController.setStorageScope(StorageScope.external),
|
||||
tooltip: 'External storage',
|
||||
),
|
||||
],
|
||||
@@ -283,12 +292,12 @@ class _PhotosViewState extends State<PhotosView>
|
||||
),
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 8)),
|
||||
|
||||
if (provider.isMediaLoading && photos.isEmpty)
|
||||
if (photosController.isLoading && photos.isEmpty)
|
||||
const SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
else if (provider.mediaErrorMessage != null)
|
||||
else if (photosController.errorMessage != null)
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(
|
||||
@@ -312,7 +321,7 @@ class _PhotosViewState extends State<PhotosView>
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
provider.mediaErrorMessage!,
|
||||
photosController.errorMessage!,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
@@ -320,7 +329,7 @@ class _PhotosViewState extends State<PhotosView>
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.icon(
|
||||
onPressed: provider.fetchAllMedia,
|
||||
onPressed: photosController.fetchAllMedia,
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
label: const Text('Retry'),
|
||||
),
|
||||
@@ -364,7 +373,7 @@ class _PhotosViewState extends State<PhotosView>
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final photo = photos[index];
|
||||
return _buildPhotoTile(context, photo, provider);
|
||||
return _buildPhotoTile(context, photo);
|
||||
}, childCount: photos.length),
|
||||
),
|
||||
),
|
||||
@@ -385,11 +394,10 @@ class _PhotosViewState extends State<PhotosView>
|
||||
},
|
||||
child: SyncedHeaderScaffold(
|
||||
scrollController: widget.scrollController,
|
||||
provider: provider,
|
||||
actions: const [MoreTabsButton(), ProfileAvatarButton()],
|
||||
onRefresh: provider.fetchAllMedia,
|
||||
onRefresh: photosController.fetchAllMedia,
|
||||
selectionBar: _isSelecting
|
||||
? _buildSelectionBar(context, theme, provider, selectedItems)
|
||||
? _buildSelectionBar(context, theme, pick, selectedItems)
|
||||
: null,
|
||||
contentSlivers: contentSlivers,
|
||||
),
|
||||
@@ -402,7 +410,7 @@ class _PhotosViewState extends State<PhotosView>
|
||||
Widget _buildSelectionBar(
|
||||
BuildContext context,
|
||||
ThemeData theme,
|
||||
ServerProvider provider,
|
||||
PickController pick,
|
||||
List<NextcloudItem> selectedItems,
|
||||
) {
|
||||
return Padding(
|
||||
@@ -445,7 +453,7 @@ class _PhotosViewState extends State<PhotosView>
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final action in _buildSelectionActions(
|
||||
provider,
|
||||
pick,
|
||||
selectedItems,
|
||||
))
|
||||
IconButton(
|
||||
@@ -463,12 +471,10 @@ class _PhotosViewState extends State<PhotosView>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPhotoTile(
|
||||
BuildContext context,
|
||||
NextcloudItem photo,
|
||||
ServerProvider provider,
|
||||
) {
|
||||
Widget _buildPhotoTile(BuildContext context, NextcloudItem photo) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final pick = context.watch<PickController>();
|
||||
final session = context.watch<SessionController>();
|
||||
final isSelected = _selectedIds.contains(photo.id);
|
||||
|
||||
// No border radius on the tile itself — the outer ClipRRect below is
|
||||
@@ -476,15 +482,15 @@ class _PhotosViewState extends State<PhotosView>
|
||||
// floating rounded corner mid-drag (see the same fix in files_view).
|
||||
final tile = GestureDetector(
|
||||
onTap: () {
|
||||
if (provider.isPicking) {
|
||||
_handlePickTap(context, provider, photo);
|
||||
if (pick.isPicking) {
|
||||
_handlePickTap(context, pick, photo);
|
||||
} else if (_isSelecting) {
|
||||
_toggleSelection(photo);
|
||||
} else {
|
||||
_openLightbox(context, photo, provider);
|
||||
_openLightbox(context, photo);
|
||||
}
|
||||
},
|
||||
onLongPress: provider.isPicking && !provider.pickRequest!.allowMultiple
|
||||
onLongPress: pick.isPicking && !pick.pickRequest!.allowMultiple
|
||||
? null
|
||||
: () => _toggleSelection(photo),
|
||||
child: ColoredBox(
|
||||
@@ -507,7 +513,7 @@ class _PhotosViewState extends State<PhotosView>
|
||||
.round();
|
||||
return Image.network(
|
||||
photo.previewUrl!,
|
||||
headers: provider.service?.authHeaders,
|
||||
headers: session.service?.authHeaders,
|
||||
fit: BoxFit.cover,
|
||||
cacheWidth: cachePixels,
|
||||
cacheHeight: cachePixels,
|
||||
@@ -571,25 +577,20 @@ class _PhotosViewState extends State<PhotosView>
|
||||
);
|
||||
}
|
||||
|
||||
void _openLightbox(
|
||||
BuildContext context,
|
||||
NextcloudItem photo,
|
||||
ServerProvider provider,
|
||||
) {
|
||||
void _openLightbox(BuildContext context, NextcloudItem photo) {
|
||||
final photos = context.read<PhotosController>().items;
|
||||
Navigator.push(
|
||||
context,
|
||||
FileViewerScreen.route(item: photo, siblings: provider.photoItems),
|
||||
FileViewerScreen.route(item: photo, siblings: photos),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _favoriteSelected(
|
||||
ServerProvider provider,
|
||||
List<NextcloudItem> items,
|
||||
) async {
|
||||
Future<void> _favoriteSelected(List<NextcloudItem> items) async {
|
||||
final ops = context.read<ItemOperations>();
|
||||
final allFavorited = items.every((i) => i.isFavorite);
|
||||
for (final item in items) {
|
||||
if (item.isFavorite == allFavorited) {
|
||||
await provider.toggleItemFavorite(item);
|
||||
await ops.toggleItemFavorite(item);
|
||||
}
|
||||
}
|
||||
_clearSelection();
|
||||
@@ -597,9 +598,9 @@ class _PhotosViewState extends State<PhotosView>
|
||||
|
||||
Future<void> _shareSelected(
|
||||
BuildContext context,
|
||||
ServerProvider provider,
|
||||
List<NextcloudItem> items,
|
||||
) async {
|
||||
final ops = context.read<ItemOperations>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(
|
||||
@@ -610,7 +611,7 @@ class _PhotosViewState extends State<PhotosView>
|
||||
|
||||
final lines = <String>[];
|
||||
for (final item in items) {
|
||||
final link = await provider.createShareLink(item);
|
||||
final link = await ops.createShareLink(item);
|
||||
if (link != null) {
|
||||
lines.add(items.length > 1 ? '${item.name}: $link' : link);
|
||||
}
|
||||
@@ -646,13 +647,13 @@ class _PhotosViewState extends State<PhotosView>
|
||||
/// `FilesView._downloadSelected`'s identical doc comment for why.
|
||||
Future<void> _downloadSelected(
|
||||
BuildContext context,
|
||||
ServerProvider provider,
|
||||
List<NextcloudItem> items,
|
||||
) async {
|
||||
final session = context.read<SessionController>();
|
||||
_clearSelection();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
try {
|
||||
await DownloadService.startDownload(provider, items);
|
||||
await DownloadService.startDownload(session, items);
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
@@ -675,7 +676,6 @@ class _PhotosViewState extends State<PhotosView>
|
||||
|
||||
Future<void> _confirmDeleteSelected(
|
||||
BuildContext context,
|
||||
ServerProvider provider,
|
||||
List<NextcloudItem> items,
|
||||
) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
@@ -704,10 +704,11 @@ class _PhotosViewState extends State<PhotosView>
|
||||
);
|
||||
if (confirmed != true || !context.mounted) return;
|
||||
|
||||
final ops = context.read<ItemOperations>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
var succeeded = 0;
|
||||
for (final item in items) {
|
||||
final success = await provider.deleteItem(item.path);
|
||||
final success = await ops.deleteItem(item.path);
|
||||
if (success) succeeded++;
|
||||
}
|
||||
|
||||
@@ -721,7 +722,6 @@ class _PhotosViewState extends State<PhotosView>
|
||||
}
|
||||
|
||||
Future<void> _moveOrCopySelected(
|
||||
ServerProvider provider,
|
||||
List<NextcloudItem> items, {
|
||||
required bool copy,
|
||||
}) async {
|
||||
|
||||
+16
-26
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/nextcloud_item.dart';
|
||||
import '../providers/server_provider.dart';
|
||||
import '../providers/recent_controller.dart';
|
||||
import '../widgets/more_tabs_button.dart';
|
||||
import '../widgets/profile_avatar_button.dart';
|
||||
import '../widgets/synced_header_scaffold.dart';
|
||||
@@ -48,7 +48,7 @@ Color _recentItemIconColor(BuildContext context, NextcloudItemType type) {
|
||||
}
|
||||
|
||||
/// Recently modified files across the whole account (not folders), newest
|
||||
/// first — see [ServerProvider.fetchRecent].
|
||||
/// first — see [RecentController.fetchAll].
|
||||
class RecentView extends StatefulWidget {
|
||||
final ScrollController scrollController;
|
||||
|
||||
@@ -65,26 +65,24 @@ class _RecentViewState extends State<RecentView> {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final provider = context.watch<ServerProvider>();
|
||||
final recent = context.watch<RecentController>();
|
||||
|
||||
if (!_requested) {
|
||||
_requested = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => provider.fetchRecent(),
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => recent.fetchAll());
|
||||
}
|
||||
|
||||
final items = provider.recentItems;
|
||||
final items = recent.items;
|
||||
|
||||
final List<Widget> contentSlivers = [
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 16)),
|
||||
|
||||
if (provider.isRecentLoading && items.isEmpty)
|
||||
if (recent.isLoading && items.isEmpty)
|
||||
const SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
else if (provider.recentErrorMessage != null)
|
||||
else if (recent.errorMessage != null)
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(
|
||||
@@ -108,7 +106,7 @@ class _RecentViewState extends State<RecentView> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
provider.recentErrorMessage!,
|
||||
recent.errorMessage!,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
@@ -116,7 +114,7 @@ class _RecentViewState extends State<RecentView> {
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.icon(
|
||||
onPressed: provider.fetchRecent,
|
||||
onPressed: recent.fetchAll,
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
label: const Text('Retry'),
|
||||
),
|
||||
@@ -154,7 +152,7 @@ class _RecentViewState extends State<RecentView> {
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final item = items[index];
|
||||
return _buildTile(context, item, provider);
|
||||
return _buildTile(context, item);
|
||||
}, childCount: items.length),
|
||||
),
|
||||
),
|
||||
@@ -165,18 +163,13 @@ class _RecentViewState extends State<RecentView> {
|
||||
|
||||
return SyncedHeaderScaffold(
|
||||
scrollController: widget.scrollController,
|
||||
provider: provider,
|
||||
actions: const [MoreTabsButton(), ProfileAvatarButton()],
|
||||
onRefresh: provider.fetchRecent,
|
||||
onRefresh: recent.fetchAll,
|
||||
contentSlivers: contentSlivers,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTile(
|
||||
BuildContext context,
|
||||
NextcloudItem item,
|
||||
ServerProvider provider,
|
||||
) {
|
||||
Widget _buildTile(BuildContext context, NextcloudItem item) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final iconColor = _recentItemIconColor(context, item.type);
|
||||
@@ -192,7 +185,7 @@ class _RecentViewState extends State<RecentView> {
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () => _openFile(context, item, provider),
|
||||
onTap: () => _openFile(context, item),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
@@ -248,12 +241,9 @@ class _RecentViewState extends State<RecentView> {
|
||||
);
|
||||
}
|
||||
|
||||
void _openFile(
|
||||
BuildContext context,
|
||||
NextcloudItem item,
|
||||
ServerProvider provider,
|
||||
) {
|
||||
final siblings = provider.recentItems.where((i) => i.isMedia).toList();
|
||||
void _openFile(BuildContext context, NextcloudItem item) {
|
||||
final recent = context.read<RecentController>();
|
||||
final siblings = recent.items.where((i) => i.isMedia).toList();
|
||||
Navigator.push(
|
||||
context,
|
||||
FileViewerScreen.route(item: item, siblings: siblings),
|
||||
|
||||
@@ -5,7 +5,8 @@ import 'package:path/path.dart' as p;
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/app_tab.dart';
|
||||
import '../models/nextcloud_item.dart';
|
||||
import '../providers/server_provider.dart';
|
||||
import '../providers/files_controller.dart';
|
||||
import '../providers/settings_controller.dart';
|
||||
import 'file_viewer_screen.dart';
|
||||
|
||||
class SearchView extends StatefulWidget {
|
||||
@@ -50,13 +51,13 @@ class _SearchViewState extends State<SearchView> {
|
||||
}
|
||||
|
||||
Future<void> _runSearch(String query) async {
|
||||
final provider = context.read<ServerProvider>();
|
||||
final files = context.read<FilesController>();
|
||||
setState(() {
|
||||
_isSearching = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
final results = await provider.searchFiles(query);
|
||||
final results = await files.searchFiles(query);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_results = results;
|
||||
@@ -98,12 +99,13 @@ class _SearchViewState extends State<SearchView> {
|
||||
/// browsing before), switches the shell to the Files tab, and closes
|
||||
/// search. For a file, also opens it once back on Files.
|
||||
Future<void> _openResult(NextcloudItem item) async {
|
||||
final provider = context.read<ServerProvider>();
|
||||
final files = context.read<FilesController>();
|
||||
final settings = context.read<SettingsController>();
|
||||
final navigator = Navigator.of(context);
|
||||
|
||||
final folderPath = item.isFolder ? item.path : p.dirname(item.path);
|
||||
await provider.navigateToAbsoluteFolder(folderPath);
|
||||
provider.requestTab(AppTab.files);
|
||||
await files.navigateToAbsoluteFolder(folderPath);
|
||||
settings.requestTab(AppTab.files);
|
||||
navigator.popUntil((route) => route.isFirst);
|
||||
|
||||
if (!item.isFolder) {
|
||||
|
||||
@@ -4,7 +4,9 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/app_tab.dart';
|
||||
import '../models/nextcloud_item.dart';
|
||||
import '../providers/server_provider.dart';
|
||||
import '../providers/files_controller.dart';
|
||||
import '../providers/session_controller.dart';
|
||||
import '../providers/settings_controller.dart';
|
||||
import '../services/share_intent_service.dart';
|
||||
import '../services/upload_service.dart';
|
||||
import '../widgets/breadcrumbs.dart';
|
||||
@@ -43,7 +45,7 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
// Shared files have no relationship to wherever the user was last
|
||||
// browsing, so start the destination picker fresh at the root.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) context.read<ServerProvider>().navigateToAbsoluteFolder('/');
|
||||
if (mounted) context.read<FilesController>().navigateToAbsoluteFolder('/');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -53,11 +55,13 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _uploadHere(ServerProvider provider) async {
|
||||
Future<void> _uploadHere(SessionController session) async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final navigator = Navigator.of(context);
|
||||
final files = context.read<FilesController>();
|
||||
final settings = context.read<SettingsController>();
|
||||
try {
|
||||
await UploadService.startUpload(provider, widget.files);
|
||||
await UploadService.startUpload(session, files, widget.files);
|
||||
} catch (e) {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
@@ -68,7 +72,7 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
return;
|
||||
}
|
||||
|
||||
provider.requestTab(AppTab.files);
|
||||
settings.requestTab(AppTab.files);
|
||||
navigator.popUntil((route) => route.isFirst);
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
@@ -82,7 +86,7 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildControlsRow(ServerProvider provider) {
|
||||
Widget _buildControlsRow(FilesController files) {
|
||||
return SizedBox(
|
||||
height: 44,
|
||||
child: SingleChildScrollView(
|
||||
@@ -91,28 +95,28 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
provider.filesSortAscending
|
||||
files.filesSortAscending
|
||||
? Icons.arrow_upward_rounded
|
||||
: Icons.arrow_downward_rounded,
|
||||
size: 20,
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
tooltip: provider.filesSortAscending ? 'Ascending' : 'Descending',
|
||||
onPressed: provider.toggleFilesSortOrder,
|
||||
tooltip: files.filesSortAscending ? 'Ascending' : 'Descending',
|
||||
onPressed: files.toggleFilesSortOrder,
|
||||
),
|
||||
SizedBox(
|
||||
width: 130,
|
||||
child: SortMenuButton(
|
||||
field: provider.filesSortField,
|
||||
onChanged: provider.setFilesSortField,
|
||||
field: files.filesSortField,
|
||||
onChanged: files.setFilesSortField,
|
||||
),
|
||||
),
|
||||
ToggleIconButton(
|
||||
icon: provider.showHiddenFiles
|
||||
icon: files.showHiddenFiles
|
||||
? Icons.visibility_rounded
|
||||
: Icons.visibility_off_rounded,
|
||||
isSelected: provider.showHiddenFiles,
|
||||
onTap: () => provider.toggleShowHiddenFiles(),
|
||||
isSelected: files.showHiddenFiles,
|
||||
onTap: () => files.toggleShowHiddenFiles(),
|
||||
tooltip: 'Show hidden files',
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
@@ -120,14 +124,14 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
children: [
|
||||
ToggleIconButton(
|
||||
icon: Symbols.circles_rounded,
|
||||
isSelected: provider.storageScope == StorageScope.cloud,
|
||||
onTap: () => provider.setStorageScope(StorageScope.cloud),
|
||||
isSelected: files.storageScope == StorageScope.cloud,
|
||||
onTap: () => files.setStorageScope(StorageScope.cloud),
|
||||
tooltip: 'Cloud storage',
|
||||
),
|
||||
ToggleIconButton(
|
||||
icon: Symbols.hard_drive_rounded,
|
||||
isSelected: provider.storageScope == StorageScope.external,
|
||||
onTap: () => provider.setStorageScope(StorageScope.external),
|
||||
isSelected: files.storageScope == StorageScope.external,
|
||||
onTap: () => files.setStorageScope(StorageScope.external),
|
||||
tooltip: 'External storage',
|
||||
),
|
||||
],
|
||||
@@ -137,24 +141,24 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
children: [
|
||||
ToggleIconButton(
|
||||
icon: Icons.select_all_rounded,
|
||||
isSelected: provider.filesTypeFilter == FilesTypeFilter.all,
|
||||
onTap: () => provider.setFilesTypeFilter(FilesTypeFilter.all),
|
||||
isSelected: files.filesTypeFilter == FilesTypeFilter.all,
|
||||
onTap: () => files.setFilesTypeFilter(FilesTypeFilter.all),
|
||||
tooltip: 'Files & folders',
|
||||
),
|
||||
ToggleIconButton(
|
||||
icon: Icons.insert_drive_file_outlined,
|
||||
isSelected:
|
||||
provider.filesTypeFilter == FilesTypeFilter.filesOnly,
|
||||
files.filesTypeFilter == FilesTypeFilter.filesOnly,
|
||||
onTap: () =>
|
||||
provider.setFilesTypeFilter(FilesTypeFilter.filesOnly),
|
||||
files.setFilesTypeFilter(FilesTypeFilter.filesOnly),
|
||||
tooltip: 'Files only',
|
||||
),
|
||||
ToggleIconButton(
|
||||
icon: Icons.folder_outlined,
|
||||
isSelected:
|
||||
provider.filesTypeFilter == FilesTypeFilter.foldersOnly,
|
||||
files.filesTypeFilter == FilesTypeFilter.foldersOnly,
|
||||
onTap: () =>
|
||||
provider.setFilesTypeFilter(FilesTypeFilter.foldersOnly),
|
||||
files.setFilesTypeFilter(FilesTypeFilter.foldersOnly),
|
||||
tooltip: 'Folders only',
|
||||
),
|
||||
],
|
||||
@@ -164,14 +168,14 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
children: [
|
||||
ToggleIconButton(
|
||||
icon: Icons.view_list_rounded,
|
||||
isSelected: !provider.isGridView,
|
||||
onTap: () => provider.setGridView(false),
|
||||
isSelected: !files.isGridView,
|
||||
onTap: () => files.setGridView(false),
|
||||
tooltip: 'List view',
|
||||
),
|
||||
ToggleIconButton(
|
||||
icon: Icons.grid_view_rounded,
|
||||
isSelected: provider.isGridView,
|
||||
onTap: () => provider.setGridView(true),
|
||||
isSelected: files.isGridView,
|
||||
onTap: () => files.setGridView(true),
|
||||
tooltip: 'Grid view',
|
||||
),
|
||||
],
|
||||
@@ -185,13 +189,11 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
// Only folders are valid upload destinations - files still show (so the
|
||||
// listing matches what the Files tab itself would show for this folder)
|
||||
// but are visually dimmed and inert rather than hidden outright.
|
||||
Widget _buildListTile(
|
||||
BuildContext context,
|
||||
NextcloudItem item,
|
||||
ServerProvider provider,
|
||||
) {
|
||||
Widget _buildListTile(BuildContext context, NextcloudItem item) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final files = context.read<FilesController>();
|
||||
final session = context.watch<SessionController>();
|
||||
final isFolder = item.isFolder;
|
||||
|
||||
return Padding(
|
||||
@@ -203,9 +205,7 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
child: Material(
|
||||
color: colorScheme.surfaceContainerLow,
|
||||
child: InkWell(
|
||||
onTap: isFolder
|
||||
? () => provider.navigateToFolder(item.path)
|
||||
: null,
|
||||
onTap: isFolder ? () => files.navigateToFolder(item.path) : null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
@@ -215,7 +215,7 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
children: [
|
||||
ItemThumbnail(
|
||||
item: item,
|
||||
provider: provider,
|
||||
service: session.service,
|
||||
size: 44,
|
||||
borderRadius: 12,
|
||||
iconSize: 22,
|
||||
@@ -255,13 +255,10 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGridCard(
|
||||
BuildContext context,
|
||||
NextcloudItem item,
|
||||
ServerProvider provider,
|
||||
) {
|
||||
Widget _buildGridCard(BuildContext context, NextcloudItem item) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final files = context.read<FilesController>();
|
||||
final isFolder = item.isFolder;
|
||||
final iconColor = getIconColor(context, item.type);
|
||||
|
||||
@@ -272,7 +269,7 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: isFolder ? () => provider.navigateToFolder(item.path) : null,
|
||||
onTap: isFolder ? () => files.navigateToFolder(item.path) : null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Column(
|
||||
@@ -326,13 +323,14 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final provider = context.watch<ServerProvider>();
|
||||
final hasBreadcrumbs = provider.pathStack.length > 1;
|
||||
final currentPath = provider.pathStack.last;
|
||||
final files = context.watch<FilesController>();
|
||||
final session = context.watch<SessionController>();
|
||||
final hasBreadcrumbs = files.pathStack.length > 1;
|
||||
final currentPath = files.pathStack.last;
|
||||
final currentLabel = currentPath == '/'
|
||||
? 'Home'
|
||||
: currentPath.split('/').where((s) => s.isNotEmpty).last;
|
||||
final items = provider.items;
|
||||
final items = files.items;
|
||||
|
||||
// Mirrors FilesView's own controls-row + breadcrumbs sticky header
|
||||
// exactly (padding, heights) so this reads as the same browser, just
|
||||
@@ -342,14 +340,14 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildControlsRow(provider),
|
||||
_buildControlsRow(files),
|
||||
if (hasBreadcrumbs) ...[
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
height: 32,
|
||||
child: Breadcrumbs(
|
||||
pathStack: provider.pathStack,
|
||||
onTap: (index) => provider.navigateToPathIndex(index),
|
||||
pathStack: files.pathStack,
|
||||
onTap: (index) => files.navigateToPathIndex(index),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -365,7 +363,7 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
child: controlsColumn,
|
||||
),
|
||||
),
|
||||
if (provider.isLoading)
|
||||
if (files.isLoading)
|
||||
const SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
@@ -380,7 +378,7 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (provider.isGridView)
|
||||
else if (files.isGridView)
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
sliver: SliverGrid(
|
||||
@@ -391,7 +389,7 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return _buildGridCard(context, items[index], provider);
|
||||
return _buildGridCard(context, items[index]);
|
||||
}, childCount: items.length),
|
||||
),
|
||||
)
|
||||
@@ -400,7 +398,7 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return _buildListTile(context, items[index], provider);
|
||||
return _buildListTile(context, items[index]);
|
||||
}, childCount: items.length),
|
||||
),
|
||||
),
|
||||
@@ -411,7 +409,6 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
return Scaffold(
|
||||
body: SyncedHeaderScaffold(
|
||||
scrollController: _scrollController,
|
||||
provider: provider,
|
||||
// Same trailing actions as every other tab except "More tabs" -
|
||||
// there's nowhere useful for it to go while mid-upload (jumping to
|
||||
// Trash/Shares/etc. would abandon this destination picker), so the
|
||||
@@ -494,7 +491,7 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: () => _uploadHere(provider),
|
||||
onPressed: () => _uploadHere(session),
|
||||
icon: const Icon(Icons.upload_rounded),
|
||||
label: Text('Upload to $currentLabel'),
|
||||
),
|
||||
|
||||
+19
-22
@@ -3,7 +3,7 @@ import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/nextcloud_item.dart';
|
||||
import '../models/nextcloud_share.dart';
|
||||
import '../providers/server_provider.dart';
|
||||
import '../providers/shares_controller.dart';
|
||||
import '../widgets/more_tabs_button.dart';
|
||||
import '../widgets/profile_avatar_button.dart';
|
||||
import '../widgets/synced_header_scaffold.dart';
|
||||
@@ -60,16 +60,16 @@ class _SharesViewState extends State<SharesView> {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final provider = context.watch<ServerProvider>();
|
||||
final sharesController = context.watch<SharesController>();
|
||||
|
||||
if (!_requested) {
|
||||
_requested = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => provider.fetchShares(),
|
||||
(_) => sharesController.fetchAll(),
|
||||
);
|
||||
}
|
||||
|
||||
final shares = provider.shares;
|
||||
final shares = sharesController.shares;
|
||||
|
||||
final List<Widget> contentSlivers = [
|
||||
SliverToBoxAdapter(
|
||||
@@ -80,19 +80,20 @@ class _SharesViewState extends State<SharesView> {
|
||||
ButtonSegment(value: false, label: Text('Shared by me')),
|
||||
ButtonSegment(value: true, label: Text('Shared with me')),
|
||||
],
|
||||
selected: {provider.sharesWithMe},
|
||||
onSelectionChanged: (set) => provider.setSharesWithMe(set.first),
|
||||
selected: {sharesController.sharedWithMe},
|
||||
onSelectionChanged: (set) =>
|
||||
sharesController.setSharedWithMe(set.first),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 8)),
|
||||
|
||||
if (provider.isSharesLoading && shares.isEmpty)
|
||||
if (sharesController.isLoading && shares.isEmpty)
|
||||
const SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
else if (provider.sharesErrorMessage != null)
|
||||
else if (sharesController.errorMessage != null)
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(
|
||||
@@ -116,7 +117,7 @@ class _SharesViewState extends State<SharesView> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
provider.sharesErrorMessage!,
|
||||
sharesController.errorMessage!,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
@@ -124,7 +125,7 @@ class _SharesViewState extends State<SharesView> {
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.icon(
|
||||
onPressed: provider.fetchShares,
|
||||
onPressed: sharesController.fetchAll,
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
label: const Text('Retry'),
|
||||
),
|
||||
@@ -147,7 +148,7 @@ class _SharesViewState extends State<SharesView> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
provider.sharesWithMe
|
||||
sharesController.sharedWithMe
|
||||
? 'Nothing has been shared with you'
|
||||
: 'You haven\'t shared anything yet',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
@@ -164,7 +165,7 @@ class _SharesViewState extends State<SharesView> {
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final share = shares[index];
|
||||
return _buildShareTile(context, share, provider);
|
||||
return _buildShareTile(context, share);
|
||||
}, childCount: shares.length),
|
||||
),
|
||||
),
|
||||
@@ -175,18 +176,13 @@ class _SharesViewState extends State<SharesView> {
|
||||
|
||||
return SyncedHeaderScaffold(
|
||||
scrollController: widget.scrollController,
|
||||
provider: provider,
|
||||
actions: const [MoreTabsButton(), ProfileAvatarButton()],
|
||||
onRefresh: provider.fetchShares,
|
||||
onRefresh: sharesController.fetchAll,
|
||||
contentSlivers: contentSlivers,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildShareTile(
|
||||
BuildContext context,
|
||||
NextcloudShare share,
|
||||
ServerProvider provider,
|
||||
) {
|
||||
Widget _buildShareTile(BuildContext context, NextcloudShare share) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final iconColor = colorScheme.primary;
|
||||
@@ -260,7 +256,7 @@ class _SharesViewState extends State<SharesView> {
|
||||
IconButton(
|
||||
icon: Icon(Icons.link_off_rounded, color: colorScheme.error),
|
||||
tooltip: share.sharedWithMe ? 'Remove' : 'Unshare',
|
||||
onPressed: () => _confirmUnshare(context, provider, share),
|
||||
onPressed: () => _confirmUnshare(context, share),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -271,7 +267,6 @@ class _SharesViewState extends State<SharesView> {
|
||||
|
||||
Future<void> _confirmUnshare(
|
||||
BuildContext context,
|
||||
ServerProvider provider,
|
||||
NextcloudShare share,
|
||||
) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
@@ -303,7 +298,9 @@ class _SharesViewState extends State<SharesView> {
|
||||
if (confirmed != true || !context.mounted) return;
|
||||
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final success = await provider.deleteShare(share);
|
||||
final success = await context.read<SharesController>().deleteShare(
|
||||
share,
|
||||
);
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
|
||||
+16
-26
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/nextcloud_item.dart';
|
||||
import '../providers/server_provider.dart';
|
||||
import '../providers/trash_controller.dart';
|
||||
import '../widgets/more_tabs_button.dart';
|
||||
import '../widgets/profile_avatar_button.dart';
|
||||
import '../widgets/synced_header_scaffold.dart';
|
||||
@@ -42,26 +42,26 @@ class _TrashViewState extends State<TrashView> {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final provider = context.watch<ServerProvider>();
|
||||
final trashController = context.watch<TrashController>();
|
||||
|
||||
if (!_requested) {
|
||||
_requested = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => provider.fetchTrash(),
|
||||
(_) => trashController.fetchAll(),
|
||||
);
|
||||
}
|
||||
|
||||
final trash = provider.trashItems;
|
||||
final trash = trashController.items;
|
||||
|
||||
final List<Widget> contentSlivers = [
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 16)),
|
||||
|
||||
if (provider.isTrashLoading && trash.isEmpty)
|
||||
if (trashController.isLoading && trash.isEmpty)
|
||||
const SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
else if (provider.trashErrorMessage != null)
|
||||
else if (trashController.errorMessage != null)
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(
|
||||
@@ -85,7 +85,7 @@ class _TrashViewState extends State<TrashView> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
provider.trashErrorMessage!,
|
||||
trashController.errorMessage!,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
@@ -93,7 +93,7 @@ class _TrashViewState extends State<TrashView> {
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.icon(
|
||||
onPressed: provider.fetchTrash,
|
||||
onPressed: trashController.fetchAll,
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
label: const Text('Retry'),
|
||||
),
|
||||
@@ -131,7 +131,7 @@ class _TrashViewState extends State<TrashView> {
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final item = trash[index];
|
||||
return _buildTrashTile(context, item, provider);
|
||||
return _buildTrashTile(context, item);
|
||||
}, childCount: trash.length),
|
||||
),
|
||||
),
|
||||
@@ -142,18 +142,13 @@ class _TrashViewState extends State<TrashView> {
|
||||
|
||||
return SyncedHeaderScaffold(
|
||||
scrollController: widget.scrollController,
|
||||
provider: provider,
|
||||
actions: const [MoreTabsButton(), ProfileAvatarButton()],
|
||||
onRefresh: provider.fetchTrash,
|
||||
onRefresh: trashController.fetchAll,
|
||||
contentSlivers: contentSlivers,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTrashTile(
|
||||
BuildContext context,
|
||||
NextcloudItem item,
|
||||
ServerProvider provider,
|
||||
) {
|
||||
Widget _buildTrashTile(BuildContext context, NextcloudItem item) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final iconColor = colorScheme.outline;
|
||||
@@ -210,7 +205,7 @@ class _TrashViewState extends State<TrashView> {
|
||||
icon: const Icon(Icons.restore_rounded),
|
||||
tooltip: 'Restore',
|
||||
color: colorScheme.primary,
|
||||
onPressed: () => _restore(context, provider, item),
|
||||
onPressed: () => _restore(context, item),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
@@ -218,7 +213,7 @@ class _TrashViewState extends State<TrashView> {
|
||||
color: colorScheme.error,
|
||||
),
|
||||
tooltip: 'Delete forever',
|
||||
onPressed: () => _confirmDeleteForever(context, provider, item),
|
||||
onPressed: () => _confirmDeleteForever(context, item),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -227,13 +222,9 @@ class _TrashViewState extends State<TrashView> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _restore(
|
||||
BuildContext context,
|
||||
ServerProvider provider,
|
||||
NextcloudItem item,
|
||||
) async {
|
||||
Future<void> _restore(BuildContext context, NextcloudItem item) async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final success = await provider.restoreTrashItem(item);
|
||||
final success = await context.read<TrashController>().restore(item);
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
@@ -246,7 +237,6 @@ class _TrashViewState extends State<TrashView> {
|
||||
|
||||
Future<void> _confirmDeleteForever(
|
||||
BuildContext context,
|
||||
ServerProvider provider,
|
||||
NextcloudItem item,
|
||||
) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
@@ -276,7 +266,7 @@ class _TrashViewState extends State<TrashView> {
|
||||
if (confirmed != true || !context.mounted) return;
|
||||
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final success = await provider.deleteTrashItemForever(item);
|
||||
final success = await context.read<TrashController>().deleteForever(item);
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
|
||||
Reference in New Issue
Block a user