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:
@@ -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/item_operations.dart';
|
||||
|
||||
/// Per-file activity feed — same list-item look as the account-wide
|
||||
/// Activity tab (activity_view.dart), scoped to a single item.
|
||||
@@ -21,8 +21,8 @@ class _DetailsActivityTabState extends State<DetailsActivityTab> {
|
||||
List<NextcloudActivity> _activities = [];
|
||||
|
||||
Future<void> _load() async {
|
||||
final provider = context.read<ServerProvider>();
|
||||
final result = await provider.fetchFileActivity(widget.item);
|
||||
final ops = context.read<ItemOperations>();
|
||||
final result = await ops.fetchFileActivity(widget.item);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_activities = result;
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:path_provider/path_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../models/nextcloud_file_version.dart';
|
||||
import '../../models/nextcloud_item.dart';
|
||||
import '../../providers/server_provider.dart';
|
||||
import '../../providers/item_operations.dart';
|
||||
import '../synced_header_scaffold.dart' show formatBytes;
|
||||
|
||||
/// File version history — a synthetic "Current version" row (from the
|
||||
@@ -27,8 +27,8 @@ class _DetailsVersionsTabState extends State<DetailsVersionsTab> {
|
||||
List<NextcloudFileVersion> _versions = [];
|
||||
|
||||
Future<void> _load() async {
|
||||
final provider = context.read<ServerProvider>();
|
||||
final result = await provider.fetchFileVersions(widget.item);
|
||||
final ops = context.read<ItemOperations>();
|
||||
final result = await ops.fetchFileVersions(widget.item);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_versions = result;
|
||||
@@ -37,9 +37,9 @@ class _DetailsVersionsTabState extends State<DetailsVersionsTab> {
|
||||
}
|
||||
|
||||
Future<void> _restore(NextcloudFileVersion version) async {
|
||||
final provider = context.read<ServerProvider>();
|
||||
final ops = context.read<ItemOperations>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final success = await provider.restoreFileVersion(
|
||||
final success = await ops.restoreFileVersion(
|
||||
widget.item,
|
||||
version.versionLabel,
|
||||
);
|
||||
@@ -56,7 +56,7 @@ class _DetailsVersionsTabState extends State<DetailsVersionsTab> {
|
||||
}
|
||||
|
||||
Future<void> _download(NextcloudFileVersion version) async {
|
||||
final provider = context.read<ServerProvider>();
|
||||
final ops = context.read<ItemOperations>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
try {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
@@ -64,7 +64,7 @@ class _DetailsVersionsTabState extends State<DetailsVersionsTab> {
|
||||
tempDir.path,
|
||||
'${version.versionLabel}_${widget.item.name}',
|
||||
);
|
||||
final success = await provider.downloadVersion(
|
||||
final success = await ops.downloadVersion(
|
||||
widget.item,
|
||||
version.versionLabel,
|
||||
tempPath,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/nextcloud_item.dart';
|
||||
import '../providers/server_provider.dart';
|
||||
import '../services/nextcloud_service.dart';
|
||||
|
||||
/// The icon/color/thumbnail treatment for a file or folder, shared by any
|
||||
/// screen that lists [NextcloudItem]s the same way the Files tab does (see
|
||||
@@ -51,7 +51,7 @@ Color getIconColor(BuildContext context, NextcloudItemType type) {
|
||||
/// the Photos tab.
|
||||
class ItemThumbnail extends StatelessWidget {
|
||||
final NextcloudItem item;
|
||||
final ServerProvider provider;
|
||||
final NextcloudService? service;
|
||||
final double size;
|
||||
final double borderRadius;
|
||||
final double iconSize;
|
||||
@@ -59,7 +59,7 @@ class ItemThumbnail extends StatelessWidget {
|
||||
const ItemThumbnail({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.provider,
|
||||
required this.service,
|
||||
required this.size,
|
||||
required this.borderRadius,
|
||||
required this.iconSize,
|
||||
@@ -92,7 +92,7 @@ class ItemThumbnail extends StatelessWidget {
|
||||
children: [
|
||||
Image.network(
|
||||
item.previewUrl!,
|
||||
headers: provider.service?.authHeaders,
|
||||
headers: service?.authHeaders,
|
||||
fit: BoxFit.cover,
|
||||
cacheWidth: cachePixels,
|
||||
cacheHeight: cachePixels,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/app_tab.dart';
|
||||
import '../providers/server_provider.dart';
|
||||
import '../providers/pick_controller.dart';
|
||||
import '../providers/settings_controller.dart';
|
||||
import 'app_tab_view_builder.dart';
|
||||
|
||||
/// Opens a tab that's currently hidden from the bottom nav bar as its own
|
||||
@@ -13,23 +14,24 @@ class MoreTabsButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<ServerProvider>();
|
||||
final pick = context.watch<PickController>();
|
||||
final settings = context.watch<SettingsController>();
|
||||
|
||||
// While picking (another app's GET_CONTENT request), MainShellView
|
||||
// already restricts the bottom nav to Files/Photos - this button would
|
||||
// otherwise be the one remaining way to reach a tab that isn't a valid
|
||||
// pick source (Trash, Shares, ...), so it hides entirely rather than
|
||||
// just disabling.
|
||||
if (provider.isPicking) return const SizedBox.shrink();
|
||||
if (pick.isPicking) return const SizedBox.shrink();
|
||||
|
||||
return PopupMenuButton<AppTab>(
|
||||
icon: const Icon(Icons.apps_rounded),
|
||||
tooltip: 'More tabs',
|
||||
enabled: provider.hiddenTabs.isNotEmpty,
|
||||
enabled: settings.hiddenTabs.isNotEmpty,
|
||||
onSelected: (tab) => _openHiddenTab(context, tab),
|
||||
itemBuilder: (context) => [
|
||||
for (final tab in provider.tabOrder.where(
|
||||
(t) => provider.hiddenTabs.contains(t),
|
||||
for (final tab in settings.tabOrder.where(
|
||||
(t) => settings.hiddenTabs.contains(t),
|
||||
))
|
||||
PopupMenuItem(
|
||||
value: tab,
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'gradual_bottom_sheet.dart';
|
||||
/// destination) - summarizes every conflict at once rather than prompting
|
||||
/// per item as they're hit, and lets the user resolve them all the same
|
||||
/// way (overwrite/keep both) or open a per-item breakdown. Returns the
|
||||
/// `ConflictChoice` map `ServerProvider.resolveConflicts` expects, or null
|
||||
/// `ConflictChoice` map `ItemOperations.resolveConflicts` expects, or null
|
||||
/// if dismissed without choosing.
|
||||
class MoveCopyConflictSheet extends StatefulWidget {
|
||||
final List<MoveCopyConflict> conflicts;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/server_provider.dart';
|
||||
import '../providers/session_controller.dart';
|
||||
import '../views/account_view.dart';
|
||||
|
||||
/// Top-bar avatar button that opens the account screen. Replaces the old
|
||||
@@ -20,7 +20,7 @@ class ProfileAvatarButton extends StatelessWidget {
|
||||
final velocity = details.primaryVelocity ?? 0;
|
||||
if (velocity.abs() < 250) return;
|
||||
|
||||
final provider = context.read<ServerProvider>();
|
||||
final provider = context.read<SessionController>();
|
||||
final target = velocity < 0
|
||||
? await provider.cycleToNextAccount()
|
||||
: await provider.cycleToPreviousAccount();
|
||||
@@ -37,7 +37,7 @@ class ProfileAvatarButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<ServerProvider>();
|
||||
final provider = context.watch<SessionController>();
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final initial = provider.username.isNotEmpty
|
||||
? provider.username[0].toUpperCase()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
import '../providers/server_provider.dart';
|
||||
import '../providers/settings_controller.dart';
|
||||
|
||||
/// Draws a video seek bar in one of the four [MediaProgressBarStyle]
|
||||
/// presets. Shared by the actual in-player seek bar and the small previews
|
||||
|
||||
@@ -7,7 +7,9 @@ import 'package:share_plus/share_plus.dart';
|
||||
import '../models/nextcloud_item.dart';
|
||||
import '../models/nextcloud_share.dart';
|
||||
import '../models/nextcloud_sharee.dart';
|
||||
import '../providers/server_provider.dart';
|
||||
import '../providers/item_operations.dart';
|
||||
import '../providers/session_controller.dart';
|
||||
import '../providers/sync_status_controller.dart';
|
||||
import 'details/details_sheet.dart' show DetailsHeader;
|
||||
import 'gradual_bottom_sheet.dart';
|
||||
|
||||
@@ -83,10 +85,10 @@ class _ShareSheetState extends State<ShareSheet> {
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
final provider = context.read<ServerProvider>();
|
||||
final ops = context.read<ItemOperations>();
|
||||
final results = await Future.wait([
|
||||
provider.fetchItemShares(widget.item),
|
||||
provider.fetchInheritedShares(widget.item),
|
||||
ops.fetchItemShares(widget.item),
|
||||
ops.fetchInheritedShares(widget.item),
|
||||
]);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
@@ -103,8 +105,8 @@ class _ShareSheetState extends State<ShareSheet> {
|
||||
return;
|
||||
}
|
||||
setState(() => _isSearching = true);
|
||||
final provider = context.read<ServerProvider>();
|
||||
final results = await provider.searchSharees(trimmed);
|
||||
final ops = context.read<ItemOperations>();
|
||||
final results = await ops.searchSharees(trimmed);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_searchResults = results;
|
||||
@@ -113,9 +115,9 @@ class _ShareSheetState extends State<ShareSheet> {
|
||||
}
|
||||
|
||||
Future<void> _addSharee(NextcloudSharee sharee) async {
|
||||
final provider = context.read<ServerProvider>();
|
||||
final ops = context.read<ItemOperations>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final share = await provider.createShare(
|
||||
final share = await ops.createShare(
|
||||
path: widget.item.path,
|
||||
shareType: sharee.shareTypeValue,
|
||||
shareWith: sharee.shareWith,
|
||||
@@ -147,9 +149,9 @@ class _ShareSheetState extends State<ShareSheet> {
|
||||
final email = _emailController.text.trim();
|
||||
if (email.isEmpty) return;
|
||||
setState(() => _isAddingEmail = true);
|
||||
final provider = context.read<ServerProvider>();
|
||||
final ops = context.read<ItemOperations>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final share = await provider.createShare(
|
||||
final share = await ops.createShare(
|
||||
path: widget.item.path,
|
||||
shareType: 4,
|
||||
shareWith: email,
|
||||
@@ -179,9 +181,9 @@ class _ShareSheetState extends State<ShareSheet> {
|
||||
}
|
||||
|
||||
Future<void> _createPublicLink() async {
|
||||
final provider = context.read<ServerProvider>();
|
||||
final ops = context.read<ItemOperations>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final share = await provider.createShare(
|
||||
final share = await ops.createShare(
|
||||
path: widget.item.path,
|
||||
shareType: 3,
|
||||
);
|
||||
@@ -199,9 +201,9 @@ class _ShareSheetState extends State<ShareSheet> {
|
||||
}
|
||||
|
||||
Future<void> _removeShare(NextcloudShare share) async {
|
||||
final provider = context.read<ServerProvider>();
|
||||
final ops = context.read<ItemOperations>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final success = await provider.deleteShare(share);
|
||||
final success = await ops.deleteShare(share);
|
||||
if (!mounted) return;
|
||||
if (success) {
|
||||
setState(() => _shares = _shares.where((s) => s.id != share.id).toList());
|
||||
@@ -231,20 +233,21 @@ class _ShareSheetState extends State<ShareSheet> {
|
||||
/// users/links) and the only one here that reads the file's actual
|
||||
/// bytes.
|
||||
Future<void> _shareFileDirectly() async {
|
||||
final provider = context.read<ServerProvider>();
|
||||
if (provider.service == null) return;
|
||||
final session = context.read<SessionController>();
|
||||
final sync = context.read<SyncStatusController>();
|
||||
if (session.service == null) return;
|
||||
setState(() => _isSharingFile = true);
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
try {
|
||||
// Already mirrored locally by device sync? Share that copy straight
|
||||
// away instead of a fresh WebDAV fetch - see
|
||||
// ServerProvider.localSyncedFilePath.
|
||||
// SyncStatusController.localSyncedFilePath.
|
||||
final tempPath =
|
||||
await provider.localSyncedFilePath(widget.item) ??
|
||||
await sync.localSyncedFilePath(widget.item) ??
|
||||
await () async {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final path = p.join(tempDir.path, widget.item.name);
|
||||
await provider.service!.downloadToFile(widget.item.path, path);
|
||||
await session.service!.downloadToFile(widget.item.path, path);
|
||||
return path;
|
||||
}();
|
||||
if (!mounted) return;
|
||||
@@ -334,7 +337,7 @@ class _ShareSheetState extends State<ShareSheet> {
|
||||
}
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final provider = context.watch<ServerProvider>();
|
||||
final session = context.watch<SessionController>();
|
||||
|
||||
if (_isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
@@ -352,7 +355,7 @@ class _ShareSheetState extends State<ShareSheet> {
|
||||
final emailShares = _shares
|
||||
.where((s) => s.shareType == ShareType.email)
|
||||
.toList();
|
||||
final internalLink = internalLinkFor(provider.serverUrl, widget.item.id);
|
||||
final internalLink = internalLinkFor(session.serverUrl, widget.item.id);
|
||||
|
||||
return ListView(
|
||||
controller: widget.scrollController,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../providers/server_provider.dart';
|
||||
import '../providers/files_controller.dart';
|
||||
|
||||
String sortFieldLabel(FileSortField field) {
|
||||
switch (field) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../providers/server_provider.dart';
|
||||
import '../providers/settings_controller.dart';
|
||||
|
||||
/// Wraps [child] with the user's configured swipe-left/swipe-right actions
|
||||
/// (favorite, delete, share, or none) — shared between the Files list view
|
||||
@@ -10,7 +10,7 @@ import '../providers/server_provider.dart';
|
||||
class SwipeableItem extends StatelessWidget {
|
||||
final Key itemKey;
|
||||
final String itemName;
|
||||
final ServerProvider provider;
|
||||
final SettingsController settings;
|
||||
final Future<void> Function() onFavorite;
|
||||
final Future<void> Function() onShare;
|
||||
final Future<void> Function() onDelete;
|
||||
@@ -20,7 +20,7 @@ class SwipeableItem extends StatelessWidget {
|
||||
super.key,
|
||||
required this.itemKey,
|
||||
required this.itemName,
|
||||
required this.provider,
|
||||
required this.settings,
|
||||
required this.onFavorite,
|
||||
required this.onShare,
|
||||
required this.onDelete,
|
||||
@@ -69,8 +69,8 @@ class SwipeableItem extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final rightAction = provider.swipeRightAction;
|
||||
final leftAction = provider.swipeLeftAction;
|
||||
final rightAction = settings.swipeRightAction;
|
||||
final leftAction = settings.swipeLeftAction;
|
||||
final direction = _directionFor(leftAction, rightAction);
|
||||
if (direction == DismissDirection.none) return child;
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/nextcloud_item.dart';
|
||||
import '../models/sync_status.dart';
|
||||
import '../providers/server_provider.dart';
|
||||
import '../providers/files_controller.dart';
|
||||
import '../providers/session_controller.dart';
|
||||
import '../providers/sync_status_controller.dart';
|
||||
|
||||
/// Icon/label for the persistent header chip/panel - reflects device-sync
|
||||
/// status (see `ServerProvider.syncHeaderStatus`), not the WebDAV
|
||||
/// status (see `SyncStatusController.syncHeaderStatus`), not the WebDAV
|
||||
/// directory-listing refresh the pull gesture itself triggers (that has
|
||||
/// its own, separate floating spinner bubble - see
|
||||
/// `_SyncedHeaderScaffoldState`'s `_isRefreshing`).
|
||||
@@ -42,7 +45,6 @@ String formatQuota(NextcloudUserQuota quota) {
|
||||
/// holds the tab's own content slivers.
|
||||
class SyncedHeaderScaffold extends StatefulWidget {
|
||||
final ScrollController scrollController;
|
||||
final ServerProvider provider;
|
||||
|
||||
/// Trailing icons in the app bar (e.g. an upload button, the avatar).
|
||||
final List<Widget> actions;
|
||||
@@ -53,8 +55,8 @@ class SyncedHeaderScaffold extends StatefulWidget {
|
||||
final List<Widget> contentSlivers;
|
||||
|
||||
/// What a deliberate pull-to-sync should trigger. Defaults to
|
||||
/// [ServerProvider.refreshData] (the current folder's listing); pass e.g.
|
||||
/// `provider.fetchTrash` for a tab backed by different data.
|
||||
/// [FilesController.refreshData] (the current folder's listing); pass
|
||||
/// e.g. `trashController.fetchAll` for a tab backed by different data.
|
||||
final Future<void> Function()? onRefresh;
|
||||
|
||||
/// When non-null (a tab is mid-selection), this takes over the pinned top
|
||||
@@ -69,7 +71,6 @@ class SyncedHeaderScaffold extends StatefulWidget {
|
||||
const SyncedHeaderScaffold({
|
||||
super.key,
|
||||
required this.scrollController,
|
||||
required this.provider,
|
||||
required this.actions,
|
||||
required this.contentSlivers,
|
||||
this.onRefresh,
|
||||
@@ -118,7 +119,8 @@ class _SyncedHeaderScaffoldState extends State<SyncedHeaderScaffold> {
|
||||
}
|
||||
|
||||
void _startRefresh() {
|
||||
final refresh = widget.onRefresh ?? widget.provider.refreshData;
|
||||
final refresh =
|
||||
widget.onRefresh ?? context.read<FilesController>().refreshData;
|
||||
setState(() => _isRefreshing = true);
|
||||
refresh().whenComplete(() {
|
||||
if (mounted) setState(() => _isRefreshing = false);
|
||||
@@ -163,7 +165,7 @@ class _SyncedHeaderScaffoldState extends State<SyncedHeaderScaffold> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final provider = widget.provider;
|
||||
final syncStatus = context.watch<SyncStatusController>();
|
||||
|
||||
final Widget leadingWidget;
|
||||
if (_headerLocked) {
|
||||
@@ -175,7 +177,7 @@ class _SyncedHeaderScaffoldState extends State<SyncedHeaderScaffold> {
|
||||
} else if (_isPulling) {
|
||||
leadingWidget = const SizedBox.shrink();
|
||||
} else {
|
||||
leadingWidget = _SyncStatusChip(provider: provider, onTap: _lockOpen);
|
||||
leadingWidget = _SyncStatusChip(onTap: _lockOpen);
|
||||
}
|
||||
|
||||
final topInset = MediaQuery.of(context).padding.top;
|
||||
@@ -205,7 +207,7 @@ class _SyncedHeaderScaffoldState extends State<SyncedHeaderScaffold> {
|
||||
expandedHeight: widget.selectionBar != null
|
||||
? kToolbarHeight
|
||||
: (_headerLocked
|
||||
? 190.0 + provider.syncConflicts.length * 52.0
|
||||
? 190.0 + syncStatus.syncConflicts.length * 52.0
|
||||
: kToolbarHeight),
|
||||
collapsedHeight: kToolbarHeight,
|
||||
backgroundColor: colorScheme.surfaceContainer,
|
||||
@@ -225,7 +227,6 @@ class _SyncedHeaderScaffoldState extends State<SyncedHeaderScaffold> {
|
||||
? null
|
||||
: FlexibleSpaceBar(
|
||||
background: _SyncedStretchPanel(
|
||||
provider: provider,
|
||||
forceVisible: _headerLocked,
|
||||
),
|
||||
),
|
||||
@@ -287,16 +288,15 @@ class _SyncedHeaderScaffoldState extends State<SyncedHeaderScaffold> {
|
||||
/// [_SyncedStretchPanel] — tapping it is a discoverable shortcut for pulling
|
||||
/// the header down to expand that same "Synced" view.
|
||||
class _SyncStatusChip extends StatelessWidget {
|
||||
final ServerProvider provider;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _SyncStatusChip({required this.provider, required this.onTap});
|
||||
const _SyncStatusChip({required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final status = provider.syncHeaderStatus;
|
||||
final status = context.watch<SyncStatusController>().syncHeaderStatus;
|
||||
final (icon, label) = _syncHeaderDisplay(status);
|
||||
final iconColor = status == SyncHeaderStatus.alert
|
||||
? colorScheme.error
|
||||
@@ -337,36 +337,33 @@ class _SyncStatusChip extends StatelessWidget {
|
||||
/// [_SyncedHeaderScaffoldState] decides via [forceVisible] whether it stays
|
||||
/// locked open.
|
||||
class _SyncedStretchPanel extends StatelessWidget {
|
||||
final ServerProvider provider;
|
||||
|
||||
/// Keeps the panel fully shown even without live overscroll — used once
|
||||
/// the header has "locked" open after a deliberate pull-down.
|
||||
final bool forceVisible;
|
||||
|
||||
const _SyncedStretchPanel({
|
||||
required this.provider,
|
||||
this.forceVisible = false,
|
||||
});
|
||||
const _SyncedStretchPanel({this.forceVisible = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final topInset = MediaQuery.of(context).padding.top;
|
||||
final quota = context.watch<FilesController>().quota;
|
||||
final serverUrl = context.watch<SessionController>().serverUrl;
|
||||
final syncStatus = context.watch<SyncStatusController>();
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final settings = context
|
||||
final spacerSettings = context
|
||||
.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>();
|
||||
final maxExtent = settings?.maxExtent ?? constraints.maxHeight;
|
||||
final maxExtent = spacerSettings?.maxExtent ?? constraints.maxHeight;
|
||||
final stretch = (constraints.maxHeight - maxExtent).clamp(0.0, 80.0);
|
||||
// Reaches 1.0 (title fully hidden) at 50px of pull — comfortably
|
||||
// before the 100px lock threshold.
|
||||
final progress = forceVisible ? 1.0 : (stretch / 50).clamp(0.0, 1.0);
|
||||
final quota = provider.quota;
|
||||
final status = provider.syncHeaderStatus;
|
||||
final status = syncStatus.syncHeaderStatus;
|
||||
final (_, statusLabel) = _syncHeaderDisplay(status);
|
||||
final conflicts = provider.syncConflicts;
|
||||
final conflicts = syncStatus.syncConflicts;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
@@ -389,13 +386,10 @@ class _SyncedStretchPanel extends StatelessWidget {
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
if (provider.serverUrl.isNotEmpty) ...[
|
||||
if (serverUrl.isNotEmpty) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
provider.serverUrl.replaceFirst(
|
||||
RegExp(r'^https?://'),
|
||||
'',
|
||||
),
|
||||
serverUrl.replaceFirst(RegExp(r'^https?://'), ''),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
@@ -438,10 +432,7 @@ class _SyncedStretchPanel extends StatelessWidget {
|
||||
for (final conflict in conflicts)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: _SyncConflictRow(
|
||||
conflict: conflict,
|
||||
provider: provider,
|
||||
),
|
||||
child: _SyncConflictRow(conflict: conflict),
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -462,9 +453,8 @@ class _SyncedStretchPanel extends StatelessWidget {
|
||||
/// side), just triggered in-app instead.
|
||||
class _SyncConflictRow extends StatefulWidget {
|
||||
final SyncConflictInfo conflict;
|
||||
final ServerProvider provider;
|
||||
|
||||
const _SyncConflictRow({required this.conflict, required this.provider});
|
||||
const _SyncConflictRow({required this.conflict});
|
||||
|
||||
@override
|
||||
State<_SyncConflictRow> createState() => _SyncConflictRowState();
|
||||
@@ -476,7 +466,7 @@ class _SyncConflictRowState extends State<_SyncConflictRow> {
|
||||
Future<void> _resolve(bool useLocal) async {
|
||||
setState(() => _resolving = true);
|
||||
try {
|
||||
await widget.provider.resolveSyncConflict(
|
||||
await context.read<SyncStatusController>().resolveSyncConflict(
|
||||
widget.conflict,
|
||||
useLocal: useLocal,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user