added a swipeable media gallery for chat and folder images and videos with server previews, on-demand originals and zoom, and marked document previews in chat so they no longer pass for photos
This commit is contained in:
1 parent
952f916231
commit
aa176c90e3
22 files changed
+1658
-103
No files matched your search
@@ -0,0 +1,165 @@
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user