Files
noo/lib/widgets/media_grid_tile.dart
T
ayushyaandClaude Sonnet 5 f2bba2914e
Build APK / build (push) Successful in 5m27s
Offline tab as Files view, automatic background sync, sync notifications
- 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>
2026-09-21 18:06:55 -04:00

99 lines
3.4 KiB
Dart

import 'package:flutter/material.dart';
import '../models/nextcloud_item.dart';
import 'synced_header_scaffold.dart' show formatBytes;
/// Grid content for an image/video: 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.
/// Shared by the Files and Offline tabs; they differ only in where the
/// pixels come from ([imageBuilder] - a server preview vs. the local
/// mirror file) and what to show if that fails ([fallbackBuilder]).
class MediaGridTile extends StatelessWidget {
final NextcloudItem item;
/// Given the pixel width to decode at, returns the image to paint.
/// Decoding straight to the painted size (rather than the full 500x500
/// server preview / full-resolution local file) avoids a real source of
/// scrolling jank.
final ImageProvider Function(int cachePixels) imageBuilder;
final WidgetBuilder fallbackBuilder;
const MediaGridTile({
super.key,
required this.item,
required this.imageBuilder,
required this.fallbackBuilder,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return LayoutBuilder(
builder: (context, constraints) {
final cachePixels =
(constraints.maxWidth * MediaQuery.of(context).devicePixelRatio)
.round();
return Stack(
fit: StackFit.expand,
children: [
Image(
image: imageBuilder(cachePixels),
fit: BoxFit.cover,
filterQuality: FilterQuality.low,
gaplessPlayback: true,
errorBuilder: (ctx, err, stack) => fallbackBuilder(ctx),
),
if (item.type == NextcloudItemType.video)
const Center(
child: Icon(
Icons.play_circle_fill_rounded,
color: Colors.white,
size: 36,
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
padding: const EdgeInsets.fromLTRB(10, 20, 10, 8),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.transparent, Colors.black87],
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleSmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
Text(
formatBytes(item.size),
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.white70,
fontSize: 10,
),
),
],
),
),
),
],
);
},
);
}
}