Offline tab as Files view, automatic background sync, sync notifications
Build APK / build (push) Successful in 5m27s

- Offline tab is now FilesView(offline: true) over a FolderBrowser interface
  (FilesController / OfflineController) sharing controls, tiles, thumbnails
- Synced files follow the Files Cache rule (default: refresh every 15 min):
  per-account WorkManager jobs, in-app timer, resume and pull triggers;
  root-etag shortcut skips full walks when nothing changed
- Single sync at a time (shared lock); logging out stops that account's sync
- Sync safety: PROPFIND failures skip the path instead of deleting local files
- Mirror empty folders and remove deleted ones; drop synced paths deleted on
  the server; missing local files are re-downloaded
- Notifications: silent per-account sync notifications, "Background sync
  notifications" setting, audible upload/download completion
- Files refresh in place (no spinner flash); retry after network returns
- Fix sync badges not updating (shared native status stream), "Sync off" on
  launch (eager providers), Files Cache section moved under Device Sync

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 18:06:55 -04:00
co-authored by Claude Sonnet 5
parent 49ae712f5e
commit f2bba2914e
38 changed files with 3133 additions and 741 deletions
+20 -6
View File
@@ -1,3 +1,4 @@
import 'dart:io';
import 'package:flutter/material.dart';
import '../models/nextcloud_item.dart';
import '../services/nextcloud_service.dart';
@@ -56,10 +57,16 @@ class ItemThumbnail extends StatelessWidget {
final double borderRadius;
final double iconSize;
/// The on-device copy of [item] (the Offline tab's synced mirror) - when
/// set, images render from this file instead of a server preview, so
/// thumbnails work with no connection.
final File? localFile;
const ItemThumbnail({
super.key,
required this.item,
required this.service,
this.localFile,
required this.size,
required this.borderRadius,
required this.iconSize,
@@ -71,6 +78,7 @@ class ItemThumbnail extends StatelessWidget {
final isMedia =
item.type == NextcloudItemType.image ||
item.type == NextcloudItemType.video;
final useLocal = item.type == NextcloudItemType.image && localFile != null;
// Decode straight to the size this thumbnail is actually painted at —
// the server hands back a 500x500 preview regardless, and decoding that
// in full for a ~40dp tile (times however many are on screen while
@@ -86,16 +94,22 @@ class ItemThumbnail extends StatelessWidget {
color: iconColor.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(borderRadius),
),
child: isMedia && item.previewUrl != null
child: useLocal || (isMedia && item.previewUrl != null)
? Stack(
fit: StackFit.expand,
children: [
Image.network(
item.previewUrl!,
headers: service?.authHeaders,
Image(
image: ResizeImage(
useLocal
? FileImage(localFile!) as ImageProvider
: NetworkImage(
item.previewUrl!,
headers: service?.authHeaders,
),
width: cachePixels,
height: cachePixels,
),
fit: BoxFit.cover,
cacheWidth: cachePixels,
cacheHeight: cachePixels,
filterQuality: FilterQuality.low,
gaplessPlayback: true,
loadingBuilder: (context, child, progress) {