Files
Client/lib/widget/media_gallery/gallery_cache.dart
T

166 lines
5.6 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import '../../api/marianumcloud/webdav/webdav_api.dart';
import '../../model/endpoint_data.dart';
import '../../session/session_manager.dart';
import '../../utils/downloads/download_manager.dart';
import '../../utils/screen_bound_image.dart';
import 'gallery_item.dart';
/// Server-rendered preview edge for the gallery; Nextcloud derives it from the
/// max preview it already keeps for the thumbnails.
const int _previewSize = 2048;
/// Originals the gallery loaded on its own — outside [DownloadManager], so no
/// notification or tray entry. Wiped whenever an account stops being active.
class GalleryOriginals extends ChangeNotifier {
GalleryOriginals._();
static final GalleryOriginals instance = GalleryOriginals._();
static final CacheManager _images = CacheManager(
Config(
'galleryOriginals',
stalePeriod: const Duration(days: 14),
maxNrOfCacheObjects: 150,
),
);
// Videos are large; keep only a handful around.
static final CacheManager _videos = CacheManager(
Config(
'galleryVideos',
stalePeriod: const Duration(days: 3),
maxNrOfCacheObjects: 10,
),
);
final Map<String, _Flight> _flights = {};
static CacheManager _managerFor(GalleryItem item) =>
item.isVideo ? _videos : _images;
static String _cacheKey(GalleryItem item) =>
'${SessionManager().requireNextcloud().username}:${item.file.path}'
':${item.eTag ?? ''}';
static String _webdavUrl(String remotePath) =>
'${WebdavApi.buildWebdavUrl()}'
'${Uri.encodeComponent(remotePath).replaceAll('%2F', '/')}';
/// Download progress (0–1, null while the size is unknown) of a running
/// original download, or null when none runs.
ValueListenable<double?>? progressOf(GalleryItem item) =>
_flights[_cacheKey(item)]?.progress;
Future<File?> cached(GalleryItem item) async {
final info = await _managerFor(item).getFileFromCache(_cacheKey(item));
final file = info?.file;
return file != null && file.existsSync() ? file : null;
}
/// Loads the original once; parallel callers share the same download.
Future<File> load(GalleryItem item) {
final key = _cacheKey(item);
final running = _flights[key];
if (running != null) return running.completer.future;
final flight = _Flight();
_flights[key] = flight;
notifyListeners();
final epoch = SessionManager().sessionEpoch;
final manager = _managerFor(item);
flight.subscription = manager
.getFileStream(
_webdavUrl(item.file.path),
key: key,
headers: SessionManager().requireNextcloud().authHeaders,
withProgress: true,
)
.listen(
(response) {
if (response is DownloadProgress) {
flight.progress.value = response.progress;
} else if (response is FileInfo) {
if (!SessionManager().isCurrentSession(epoch)) {
unawaited(manager.removeFile(key));
_finish(key, error: StateError('Konto gewechselt'));
return;
}
_finish(key, file: response.file);
}
},
onError: (Object e) => _finish(key, error: e),
cancelOnError: true,
);
return flight.completer.future;
}
void _finish(String key, {File? file, Object? error}) {
final flight = _flights.remove(key);
if (flight == null) return;
unawaited(flight.cancel());
if (file != null) {
flight.completer.complete(file);
} else {
flight.completer.completeError(
error ?? StateError('Download fehlgeschlagen'),
);
}
notifyListeners();
scheduleMicrotask(flight.progress.dispose);
}
/// Copies the original under its real file name (into the downloads folder,
/// which [SessionWipe] clears) — share sheets and "Öffnen mit" need the name
/// and extension, the cache file has neither.
Future<String> namedCopy(GalleryItem item) async {
final source = await cached(item) ?? await load(item);
final dir = await DownloadManager.downloadsDirectory();
await dir.create(recursive: true);
final target = File('${dir.path}/${item.file.name}');
await source.copy(target.path);
return target.path;
}
Future<void> clear() async {
for (final key in _flights.keys.toList()) {
_finish(key, error: StateError('Abgebrochen'));
}
await _images.emptyCache();
await _videos.emptyCache();
}
}
class _Flight {
final Completer<File> completer = Completer<File>();
final ValueNotifier<double?> progress = ValueNotifier(null);
StreamSubscription<FileResponse>? subscription;
Future<void> cancel() async => subscription?.cancel();
}
/// Screen-sized server preview. One instance per item and size, so the
/// viewer's precache and the page's image hit the same cache entry.
ImageProvider galleryPreviewProvider(BuildContext context, GalleryItem item) {
final fileId = item.file.fileId;
final url = fileId != null
? '${EndpointData().nextcloud().origin()}/index.php/core/preview'
'?fileId=$fileId&x=$_previewSize&y=$_previewSize&a=1'
: '${EndpointData().nextcloud().origin()}/index.php/core/preview.png'
'?file=${Uri.encodeQueryComponent(item.file.path)}'
'&x=$_previewSize&y=$_previewSize&a=1';
return screenBoundImage(
context,
CachedNetworkImageProvider(
url,
headers: SessionManager().requireNextcloud().authHeaders,
),
);
}