Completely rewrite Viewer

This commit is contained in:
Ming Ming 2024-10-07 01:15:45 +08:00
parent 0d56f10243
commit 3576f7e397
33 changed files with 3151 additions and 1083 deletions

View file

@ -7,6 +7,7 @@ targets:
double: "${$?.toStringAsFixed(3)}" double: "${$?.toStringAsFixed(3)}"
List: "[length: ${$?.length}]" List: "[length: ${$?.length}]"
Set: "{length: ${$?.length}}" Set: "{length: ${$?.length}}"
Map: "{length: ${$?.length}}"
File: "${$?.path}" File: "${$?.path}"
FileDescriptor: "${$?.fdPath}" FileDescriptor: "${$?.fdPath}"
useEnumName: true useEnumName: true

View file

@ -11,6 +11,7 @@ import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/exception_event.dart';
import 'package:nc_photos/progress_util.dart'; import 'package:nc_photos/progress_util.dart';
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util; import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
import 'package:nc_photos/rx_extension.dart'; import 'package:nc_photos/rx_extension.dart';
@ -90,6 +91,8 @@ class FilesController {
/// callers must sort it by themselves if the ordering is important /// callers must sort it by themselves if the ordering is important
ValueStream<FilesStreamEvent> get stream => _dataStreamController.stream; ValueStream<FilesStreamEvent> get stream => _dataStreamController.stream;
Stream<ExceptionEvent> get errorStream => _dataErrorStreamController.stream;
/// Return a stream of file summaries associated with [account] /// Return a stream of file summaries associated with [account]
/// ///
/// File summary contains the number of files grouped by their dates /// File summary contains the number of files grouped by their dates
@ -107,6 +110,9 @@ class FilesController {
ValueStream<TimelineStreamEvent> get timelineStream => ValueStream<TimelineStreamEvent> get timelineStream =>
_timelineStreamController.stream; _timelineStreamController.stream;
Stream<ExceptionEvent> get timelineErrorStream =>
_timelineErrorStreamController.stream;
Future<void> syncRemote({ Future<void> syncRemote({
void Function(Progress progress)? onProgressUpdate, void Function(Progress progress)? onProgressUpdate,
}) async { }) async {
@ -166,7 +172,7 @@ class FilesController {
OrNull<DateTime>? overrideDateTime, OrNull<DateTime>? overrideDateTime,
bool? isFavorite, bool? isFavorite,
OrNull<ImageLocation>? location, OrNull<ImageLocation>? location,
Exception? Function(List<int> fileIds) errorBuilder = Exception? Function(List<FileDescriptor> files) errorBuilder =
UpdatePropertyFailureError.new, UpdatePropertyFailureError.new,
}) async { }) async {
final dataBackups = <int, FileDescriptor>{}; final dataBackups = <int, FileDescriptor>{};
@ -287,8 +293,8 @@ class FilesController {
} }
return value.copyWith(data: next); return value.copyWith(data: next);
}); });
errorBuilder(failures.map((e) => e.fdId).toList()) errorBuilder(failures)
?.let(_dataStreamController.addError); ?.let((e) => _dataErrorStreamController.add(ExceptionEvent(e)));
} }
// TODO query outdated // TODO query outdated
@ -299,7 +305,7 @@ class FilesController {
Future<void> remove( Future<void> remove(
List<FileDescriptor> files, { List<FileDescriptor> files, {
Exception? Function(List<int> fileIds) errorBuilder = Exception? Function(List<FileDescriptor> files) errorBuilder =
RemoveFailureError.new, RemoveFailureError.new,
}) async { }) async {
final dataBackups = <int, FileDescriptor>{}; final dataBackups = <int, FileDescriptor>{};
@ -391,8 +397,8 @@ class FilesController {
} }
return value.copyWith(data: next); return value.copyWith(data: next);
}); });
errorBuilder(failures.map((e) => e.fdId).toList()) errorBuilder(failures)
?.let(_dataStreamController.addError); ?.let((e) => _dataErrorStreamController.add(ExceptionEvent(e)));
} }
} }
@ -447,7 +453,7 @@ class FilesController {
files: v.files.addedAll(data), files: v.files.addedAll(data),
)); ));
} catch (e, stackTrace) { } catch (e, stackTrace) {
_dataStreamController.addError(e, stackTrace); _dataErrorStreamController.add(ExceptionEvent(e, stackTrace));
} }
} }
@ -462,7 +468,7 @@ class FilesController {
files: v.files.addedAll(data), files: v.files.addedAll(data),
)); ));
} catch (e, stackTrace) { } catch (e, stackTrace) {
_dataStreamController.addError(e, stackTrace); _dataErrorStreamController.add(ExceptionEvent(e, stackTrace));
} }
} }
@ -483,7 +489,7 @@ class FilesController {
)); ));
_addTimelineDateRange(dateRange); _addTimelineDateRange(dateRange);
} catch (e, stackTrace) { } catch (e, stackTrace) {
_timelineStreamController.addError(e, stackTrace); _timelineErrorStreamController.add(ExceptionEvent(e, stackTrace));
} }
} }
@ -663,6 +669,8 @@ class FilesController {
hasNext: true, hasNext: true,
), ),
); );
final _dataErrorStreamController =
StreamController<ExceptionEvent>.broadcast();
var _isSummaryStreamInited = false; var _isSummaryStreamInited = false;
final _summaryStreamController = BehaviorSubject<FilesSummaryStreamEvent>(); final _summaryStreamController = BehaviorSubject<FilesSummaryStreamEvent>();
@ -670,6 +678,8 @@ class FilesController {
final _timelineStreamController = BehaviorSubject.seeded( final _timelineStreamController = BehaviorSubject.seeded(
const TimelineStreamEvent(data: {}, isDummy: true), const TimelineStreamEvent(data: {}, isDummy: true),
); );
final _timelineErrorStreamController =
StreamController<ExceptionEvent>.broadcast();
// sorted in descending order // sorted in descending order
var _timelineQueriedRanges = <DateRange>[]; var _timelineQueriedRanges = <DateRange>[];
@ -680,22 +690,22 @@ class FilesController {
@toString @toString
class UpdatePropertyFailureError implements Exception { class UpdatePropertyFailureError implements Exception {
const UpdatePropertyFailureError(this.fileIds); const UpdatePropertyFailureError(this.files);
@override @override
String toString() => _$toString(); String toString() => _$toString();
final List<int> fileIds; final List<FileDescriptor> files;
} }
@toString @toString
class RemoveFailureError implements Exception { class RemoveFailureError implements Exception {
const RemoveFailureError(this.fileIds); const RemoveFailureError(this.files);
@override @override
String toString() => _$toString(); String toString() => _$toString();
final List<int> fileIds; final List<FileDescriptor> files;
} }
class _MockResult { class _MockResult {

View file

@ -77,13 +77,13 @@ extension _$FilesControllerNpLog on FilesController {
extension _$UpdatePropertyFailureErrorToString on UpdatePropertyFailureError { extension _$UpdatePropertyFailureErrorToString on UpdatePropertyFailureError {
String _$toString() { String _$toString() {
// ignore: unnecessary_string_interpolations // ignore: unnecessary_string_interpolations
return "UpdatePropertyFailureError {fileIds: [length: ${fileIds.length}]}"; return "UpdatePropertyFailureError {files: [length: ${files.length}]}";
} }
} }
extension _$RemoveFailureErrorToString on RemoveFailureError { extension _$RemoveFailureErrorToString on RemoveFailureError {
String _$toString() { String _$toString() {
// ignore: unnecessary_string_interpolations // ignore: unnecessary_string_interpolations
return "RemoveFailureError {fileIds: [length: ${fileIds.length}]}"; return "RemoveFailureError {files: [length: ${files.length}]}";
} }
} }

View file

@ -210,6 +210,30 @@ class PrefController {
value: value, value: value,
); );
Future<bool> setSlideshowDuration(Duration value) => _set<Duration>(
controller: _slideshowDurationController,
setter: (pref, value) => pref.setSlideshowDuration(value),
value: value,
);
Future<bool> setSlideshowShuffle(bool value) => _set<bool>(
controller: _isSlideshowShuffleController,
setter: (pref, value) => pref.setSlideshowShuffle(value),
value: value,
);
Future<bool> setSlideshowRepeat(bool value) => _set<bool>(
controller: _isSlideshowRepeatController,
setter: (pref, value) => pref.setSlideshowRepeat(value),
value: value,
);
Future<bool> setSlideshowReverse(bool value) => _set<bool>(
controller: _isSlideshowReverseController,
setter: (pref, value) => pref.setSlideshowReverse(value),
value: value,
);
Future<bool> _set<T>({ Future<bool> _set<T>({
required BehaviorSubject<T> controller, required BehaviorSubject<T> controller,
required Future<bool> Function(Pref pref, T value) setter, required Future<bool> Function(Pref pref, T value) setter,
@ -337,6 +361,18 @@ class PrefController {
@npSubjectAccessor @npSubjectAccessor
late final _mapDefaultCustomRangeController = BehaviorSubject.seeded( late final _mapDefaultCustomRangeController = BehaviorSubject.seeded(
pref.getMapDefaultCustomRange() ?? const Duration(days: 30)); pref.getMapDefaultCustomRange() ?? const Duration(days: 30));
@npSubjectAccessor
late final _slideshowDurationController = BehaviorSubject.seeded(
pref.getSlideshowDuration() ?? const Duration(seconds: 5));
@npSubjectAccessor
late final _isSlideshowShuffleController =
BehaviorSubject.seeded(pref.isSlideshowShuffle() ?? false);
@npSubjectAccessor
late final _isSlideshowRepeatController =
BehaviorSubject.seeded(pref.isSlideshowRepeat() ?? false);
@npSubjectAccessor
late final _isSlideshowReverseController =
BehaviorSubject.seeded(pref.isSlideshowReverse() ?? false);
} }
extension PrefControllerExtension on PrefController { extension PrefControllerExtension on PrefController {

View file

@ -206,6 +206,34 @@ extension $PrefControllerNpSubjectAccessor on PrefController {
mapDefaultCustomRange.distinct().skip(1); mapDefaultCustomRange.distinct().skip(1);
Duration get mapDefaultCustomRangeValue => Duration get mapDefaultCustomRangeValue =>
_mapDefaultCustomRangeController.value; _mapDefaultCustomRangeController.value;
// _slideshowDurationController
ValueStream<Duration> get slideshowDuration =>
_slideshowDurationController.stream;
Stream<Duration> get slideshowDurationNew => slideshowDuration.skip(1);
Stream<Duration> get slideshowDurationChange =>
slideshowDuration.distinct().skip(1);
Duration get slideshowDurationValue => _slideshowDurationController.value;
// _isSlideshowShuffleController
ValueStream<bool> get isSlideshowShuffle =>
_isSlideshowShuffleController.stream;
Stream<bool> get isSlideshowShuffleNew => isSlideshowShuffle.skip(1);
Stream<bool> get isSlideshowShuffleChange =>
isSlideshowShuffle.distinct().skip(1);
bool get isSlideshowShuffleValue => _isSlideshowShuffleController.value;
// _isSlideshowRepeatController
ValueStream<bool> get isSlideshowRepeat =>
_isSlideshowRepeatController.stream;
Stream<bool> get isSlideshowRepeatNew => isSlideshowRepeat.skip(1);
Stream<bool> get isSlideshowRepeatChange =>
isSlideshowRepeat.distinct().skip(1);
bool get isSlideshowRepeatValue => _isSlideshowRepeatController.value;
// _isSlideshowReverseController
ValueStream<bool> get isSlideshowReverse =>
_isSlideshowReverseController.stream;
Stream<bool> get isSlideshowReverseNew => isSlideshowReverse.skip(1);
Stream<bool> get isSlideshowReverseChange =>
isSlideshowReverse.distinct().skip(1);
bool get isSlideshowReverseValue => _isSlideshowReverseController.value;
} }
extension $SecurePrefControllerNpSubjectAccessor on SecurePrefController { extension $SecurePrefControllerNpSubjectAccessor on SecurePrefController {

View file

@ -131,6 +131,24 @@ extension on Pref {
?.let((v) => Duration(days: v)); ?.let((v) => Duration(days: v));
Future<bool> setMapDefaultCustomRange(Duration value) => Future<bool> setMapDefaultCustomRange(Duration value) =>
provider.setInt(PrefKey.mapDefaultCustomRange, value.inDays); provider.setInt(PrefKey.mapDefaultCustomRange, value.inDays);
Duration? getSlideshowDuration() => provider
.getInt(PrefKey.slideshowDuration)
?.let((v) => Duration(seconds: v));
Future<bool> setSlideshowDuration(Duration value) =>
provider.setInt(PrefKey.slideshowDuration, value.inSeconds);
bool? isSlideshowShuffle() => provider.getBool(PrefKey.isSlideshowShuffle);
Future<bool> setSlideshowShuffle(bool value) =>
provider.setBool(PrefKey.isSlideshowShuffle, value);
bool? isSlideshowRepeat() => provider.getBool(PrefKey.isSlideshowRepeat);
Future<bool> setSlideshowRepeat(bool value) =>
provider.setBool(PrefKey.isSlideshowRepeat, value);
bool? isSlideshowReverse() => provider.getBool(PrefKey.isSlideshowReverse);
Future<bool> setSlideshowReverse(bool value) =>
provider.setBool(PrefKey.isSlideshowReverse, value);
} }
MapCoord? _tryMapCoordFromJson(dynamic json) { MapCoord? _tryMapCoordFromJson(dynamic json) {

View file

@ -82,27 +82,6 @@ extension PrefExtension on Pref {
Future<bool> setLanguage(int value) => _set<int>( Future<bool> setLanguage(int value) => _set<int>(
PrefKey.language, value, (key, value) => provider.setInt(key, value)); PrefKey.language, value, (key, value) => provider.setInt(key, value));
int? getSlideshowDuration() => provider.getInt(PrefKey.slideshowDuration);
int getSlideshowDurationOr(int def) => getSlideshowDuration() ?? def;
Future<bool> setSlideshowDuration(int value) => _set<int>(
PrefKey.slideshowDuration,
value,
(key, value) => provider.setInt(key, value));
bool? isSlideshowShuffle() => provider.getBool(PrefKey.isSlideshowShuffle);
bool isSlideshowShuffleOr(bool def) => isSlideshowShuffle() ?? def;
Future<bool> setSlideshowShuffle(bool value) => _set<bool>(
PrefKey.isSlideshowShuffle,
value,
(key, value) => provider.setBool(key, value));
bool? isSlideshowRepeat() => provider.getBool(PrefKey.isSlideshowRepeat);
bool isSlideshowRepeatOr(bool def) => isSlideshowRepeat() ?? def;
Future<bool> setSlideshowRepeat(bool value) => _set<bool>(
PrefKey.isSlideshowRepeat,
value,
(key, value) => provider.setBool(key, value));
bool? isAlbumBrowserShowDate() => bool? isAlbumBrowserShowDate() =>
provider.getBool(PrefKey.isAlbumBrowserShowDate); provider.getBool(PrefKey.isAlbumBrowserShowDate);
bool isAlbumBrowserShowDateOr([bool def = false]) => bool isAlbumBrowserShowDateOr([bool def = false]) =>
@ -179,13 +158,6 @@ extension PrefExtension on Pref {
value, value,
(key, value) => provider.setBool(key, value)); (key, value) => provider.setBool(key, value));
bool? isSlideshowReverse() => provider.getBool(PrefKey.isSlideshowReverse);
bool isSlideshowReverseOr(bool def) => isSlideshowReverse() ?? def;
Future<bool> setSlideshowReverse(bool value) => _set<bool>(
PrefKey.isSlideshowReverse,
value,
(key, value) => provider.setBool(key, value));
bool? isVideoPlayerMute() => provider.getBool(PrefKey.isVideoPlayerMute); bool? isVideoPlayerMute() => provider.getBool(PrefKey.isVideoPlayerMute);
bool isVideoPlayerMuteOr([bool def = false]) => isVideoPlayerMute() ?? def; bool isVideoPlayerMuteOr([bool def = false]) => isVideoPlayerMute() ?? def;
Future<bool> setVideoPlayerMute(bool value) => _set<bool>( Future<bool> setVideoPlayerMute(bool value) => _set<bool>(

View file

@ -2,6 +2,8 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/controller/files_controller.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/exception.dart'; import 'package:nc_photos/exception.dart';
import 'package:nc_photos/navigation_manager.dart'; import 'package:nc_photos/navigation_manager.dart';
import 'package:nc_photos/widget/trusted_cert_manager.dart'; import 'package:nc_photos/widget/trusted_cert_manager.dart';
@ -58,6 +60,11 @@ String toUserString(Object? exception) {
?.pushNamed(TrustedCertManager.routeName), ?.pushNamed(TrustedCertManager.routeName),
), ),
); );
} else if (exception is UpdatePropertyFailureError) {
return (
"Failed to update files: ${exception.files.map((f) => f.filename).join(", ")}",
null
);
} }
return (exception?.toString() ?? "Unknown error", null); return (exception?.toString() ?? "Unknown error", null);
} }

View file

@ -59,21 +59,24 @@ class _Bloc extends Bloc<_Event, _State>
Future<void> _onLoad(_LoadItems ev, Emitter<_State> emit) { Future<void> _onLoad(_LoadItems ev, Emitter<_State> emit) {
_log.info(ev); _log.info(ev);
unawaited(filesController.queryByArchived()); unawaited(filesController.queryByArchived());
return forEach( return Future.wait([
emit, forEach(
filesController.stream, emit,
onData: (data) => state.copyWith( filesController.stream,
files: data.data, onData: (data) => state.copyWith(
isLoading: data.hasNext || _itemTransformerQueue.isProcessing, files: data.data,
isLoading: data.hasNext || _itemTransformerQueue.isProcessing,
),
), ),
onError: (e, stackTrace) { forEach(
_log.severe("[_onLoad] Uncaught exception", e, stackTrace); emit,
return state.copyWith( filesController.errorStream,
onData: (data) => state.copyWith(
isLoading: _itemTransformerQueue.isProcessing, isLoading: _itemTransformerQueue.isProcessing,
error: ExceptionEvent(e, stackTrace), error: ExceptionEvent(data.error, data.stackTrace),
); ),
}, ),
); ]);
} }
void _onTransformItems(_TransformItems ev, Emitter<_State> emit) { void _onTransformItems(_TransformItems ev, Emitter<_State> emit) {

View file

@ -73,10 +73,9 @@ class _ContentListBody extends StatelessWidget {
Navigator.of(context).pushNamed( Navigator.of(context).pushNamed(
Viewer.routeName, Viewer.routeName,
arguments: ViewerArguments( arguments: ViewerArguments(
context.bloc.account,
state.transformedItems state.transformedItems
.whereType<_FileItem>() .whereType<_FileItem>()
.map((e) => e.file) .map((e) => e.file.fdId)
.toList(), .toList(),
actualIndex, actualIndex,
), ),

View file

@ -149,13 +149,14 @@ class _Bloc extends Bloc<_Event, _State>
itemsWhitelist: whitelist, itemsWhitelist: whitelist,
); );
}, },
onError: (e, stackTrace) { ),
_log.severe("[_onLoad] Uncaught exception", e, stackTrace); forEach(
return state.copyWith( emit,
isLoading: false, filesController.errorStream,
error: ExceptionEvent(e, stackTrace), onData: (data) => state.copyWith(
); isLoading: false,
}, error: ExceptionEvent(data.error, data.stackTrace),
),
), ),
]); ]);
} }

View file

@ -73,19 +73,12 @@ class _ContentListBody extends StatelessWidget {
Navigator.of(context).pushNamed( Navigator.of(context).pushNamed(
Viewer.routeName, Viewer.routeName,
arguments: ViewerArguments( arguments: ViewerArguments(
context.bloc.account,
state.transformedItems state.transformedItems
.whereType<_FileItem>() .whereType<_FileItem>()
.map((e) => e.file) .map((e) => e.file.fdId)
.toList(), .toList(),
actualIndex, actualIndex,
fromCollection: ViewerCollectionData( collectionId: state.collection.id,
state.collection,
state.transformedItems
.whereType<_ActualItem>()
.map((e) => e.original)
.toList(),
),
), ),
); );
}, },

View file

@ -0,0 +1,185 @@
import 'dart:async';
import 'package:copy_with/copy_with.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/bloc_util.dart';
import 'package:nc_photos/controller/account_controller.dart';
import 'package:nc_photos/controller/files_controller.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/exception_event.dart';
import 'package:nc_photos/live_photo_util.dart';
import 'package:nc_photos/widget/image_viewer.dart';
import 'package:nc_photos/widget/live_photo_viewer.dart';
import 'package:nc_photos/widget/video_viewer.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/unique.dart';
import 'package:to_string/to_string.dart';
import 'package:video_player_platform_interface/video_player_platform_interface.dart';
part 'file_content_view.g.dart';
part 'file_content_view/bloc.dart';
part 'file_content_view/state_event.dart';
part 'file_content_view/view.dart';
@npLog
class FileContentView extends StatefulWidget {
const FileContentView({
super.key,
required this.fileId,
required this.shouldPlayLivePhoto,
required this.canZoom,
required this.canPlay,
required this.isPlayControlVisible,
this.onContentHeightChanged,
this.onZoomChanged,
this.onVideoPlayingChanged,
this.onLivePhotoLoadFailue,
});
@override
State<StatefulWidget> createState() => _FileContentViewState();
final int fileId;
final bool shouldPlayLivePhoto;
final bool canZoom;
final bool canPlay;
final bool isPlayControlVisible;
final void Function(double height)? onContentHeightChanged;
final void Function(bool isZoomed)? onZoomChanged;
final void Function(bool isPlaying)? onVideoPlayingChanged;
final void Function()? onLivePhotoLoadFailue;
}
class _FileContentViewState extends State<FileContentView> {
@override
void initState() {
super.initState();
_bloc = _Bloc(
account: context.read<AccountController>().account,
filesController: context.read<AccountController>().filesController,
fileId: widget.fileId,
shouldPlayLivePhoto: widget.shouldPlayLivePhoto,
canZoom: widget.canZoom,
canPlay: widget.canPlay,
isPlayControlVisible: widget.isPlayControlVisible,
);
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _bloc,
child: MultiBlocListener(
listeners: [
_BlocListenerT(
selector: (state) => state.contentHeight,
listener: (context, contentHeight) {
if (contentHeight != null) {
widget.onContentHeightChanged?.call(contentHeight);
}
},
),
_BlocListenerT(
selector: (state) => state.isZoomed,
listener: (context, isZoomed) {
widget.onZoomChanged?.call(isZoomed);
},
),
_BlocListenerT(
selector: (state) => state.isPlaying,
listener: (context, isPlaying) {
widget.onVideoPlayingChanged?.call(isPlaying);
},
),
_BlocListenerT(
selector: (state) => state.isLivePhotoLoadFailed,
listener: (context, isLivePhotoLoadFailed) {
if (isLivePhotoLoadFailed.value) {
widget.onLivePhotoLoadFailue?.call();
}
},
),
],
child: const _WrappedFileContentView(),
),
);
}
@override
void didUpdateWidget(covariant FileContentView oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.shouldPlayLivePhoto != oldWidget.shouldPlayLivePhoto) {
_bloc.add(_SetShouldPlayLivePhoto(widget.shouldPlayLivePhoto));
}
if (widget.canZoom != oldWidget.canZoom) {
_bloc.add(_SetCanZoom(widget.canZoom));
}
if (widget.canPlay != oldWidget.canPlay) {
_bloc.add(_SetCanPlay(widget.canPlay));
}
if (widget.isPlayControlVisible != oldWidget.isPlayControlVisible) {
_bloc.add(_SetIsPlayControlVisible(widget.isPlayControlVisible));
}
}
late final _Bloc _bloc;
}
@npLog
class _WrappedFileContentView extends StatelessWidget {
const _WrappedFileContentView();
@override
Widget build(BuildContext context) {
return _BlocSelector(
selector: (state) => state.file,
builder: (context, file) {
if (file == null) {
_log.severe("[build] File is null");
return Container();
} else if (file_util.isSupportedImageFormat(file)) {
return _BlocSelector(
selector: (state) => state.shouldPlayLivePhoto,
builder: (context, shouldPlayLivePhoto) {
if (shouldPlayLivePhoto) {
final livePhotoType = getLivePhotoTypeFromFile(file);
if (livePhotoType != null) {
return _LivePhotoPageContentView(
livePhotoType: livePhotoType,
);
} else {
_log.warning("[build] Not a live photo");
return const _PhotoPageContentView();
}
} else {
return const _PhotoPageContentView();
}
},
);
} else if (file_util.isSupportedVideoFormat(file)) {
return const _VideoPageContentView();
} else {
_log.shout("[build] Unknown file format: ${file.fdMime}");
// _pageStates[index]!.itemHeight = 0;
return Container();
}
},
);
}
}
typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
// typedef _BlocListener = BlocListener<_Bloc, _State>;
typedef _BlocListenerT<T> = BlocListenerT<_Bloc, _State, T>;
typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
typedef _Emitter = Emitter<_State>;
extension on BuildContext {
_Bloc get bloc => read<_Bloc>();
// _State get state => bloc.state;
void addEvent(_Event event) => bloc.add(event);
}

View file

@ -0,0 +1,191 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'file_content_view.dart';
// **************************************************************************
// CopyWithLintRuleGenerator
// **************************************************************************
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
// **************************************************************************
// CopyWithGenerator
// **************************************************************************
abstract class $_StateCopyWithWorker {
_State call(
{FileDescriptor? file,
bool? shouldPlayLivePhoto,
bool? canZoom,
bool? canPlay,
bool? isPlayControlVisible,
bool? isLoaded,
bool? isZoomed,
bool? isPlaying,
Unique<bool>? isLivePhotoLoadFailed,
double? contentHeight,
ExceptionEvent? error});
}
class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
_$_StateCopyWithWorkerImpl(this.that);
@override
_State call(
{dynamic file = copyWithNull,
dynamic shouldPlayLivePhoto,
dynamic canZoom,
dynamic canPlay,
dynamic isPlayControlVisible,
dynamic isLoaded,
dynamic isZoomed,
dynamic isPlaying,
dynamic isLivePhotoLoadFailed,
dynamic contentHeight = copyWithNull,
dynamic error = copyWithNull}) {
return _State(
file: file == copyWithNull ? that.file : file as FileDescriptor?,
shouldPlayLivePhoto:
shouldPlayLivePhoto as bool? ?? that.shouldPlayLivePhoto,
canZoom: canZoom as bool? ?? that.canZoom,
canPlay: canPlay as bool? ?? that.canPlay,
isPlayControlVisible:
isPlayControlVisible as bool? ?? that.isPlayControlVisible,
isLoaded: isLoaded as bool? ?? that.isLoaded,
isZoomed: isZoomed as bool? ?? that.isZoomed,
isPlaying: isPlaying as bool? ?? that.isPlaying,
isLivePhotoLoadFailed: isLivePhotoLoadFailed as Unique<bool>? ??
that.isLivePhotoLoadFailed,
contentHeight: contentHeight == copyWithNull
? that.contentHeight
: contentHeight as double?,
error: error == copyWithNull ? that.error : error as ExceptionEvent?);
}
final _State that;
}
extension $_StateCopyWith on _State {
$_StateCopyWithWorker get copyWith => _$copyWith;
$_StateCopyWithWorker get _$copyWith => _$_StateCopyWithWorkerImpl(this);
}
// **************************************************************************
// NpLogGenerator
// **************************************************************************
extension _$FileContentViewNpLog on FileContentView {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("widget.file_content_view.FileContentView");
}
extension _$_WrappedFileContentViewNpLog on _WrappedFileContentView {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("widget.file_content_view._WrappedFileContentView");
}
extension _$_BlocNpLog on _Bloc {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("widget.file_content_view._Bloc");
}
// **************************************************************************
// ToStringGenerator
// **************************************************************************
extension _$_StateToString on _State {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_State {file: ${file == null ? null : "${file!.fdPath}"}, shouldPlayLivePhoto: $shouldPlayLivePhoto, canZoom: $canZoom, canPlay: $canPlay, isPlayControlVisible: $isPlayControlVisible, isLoaded: $isLoaded, isZoomed: $isZoomed, isPlaying: $isPlaying, isLivePhotoLoadFailed: $isLivePhotoLoadFailed, contentHeight: ${contentHeight == null ? null : "${contentHeight!.toStringAsFixed(3)}"}, error: $error}";
}
}
extension _$_SetFileToString on _SetFile {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetFile {value: ${value == null ? null : "${value!.fdPath}"}}";
}
}
extension _$_SetShouldPlayLivePhotoToString on _SetShouldPlayLivePhoto {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetShouldPlayLivePhoto {value: $value}";
}
}
extension _$_SetCanZoomToString on _SetCanZoom {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetCanZoom {value: $value}";
}
}
extension _$_SetCanPlayToString on _SetCanPlay {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetCanPlay {value: $value}";
}
}
extension _$_SetIsPlayControlVisibleToString on _SetIsPlayControlVisible {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetIsPlayControlVisible {value: $value}";
}
}
extension _$_SetLoadedToString on _SetLoaded {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetLoaded {}";
}
}
extension _$_SetContentHeightToString on _SetContentHeight {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetContentHeight {value: ${value.toStringAsFixed(3)}}";
}
}
extension _$_SetIsZoomedToString on _SetIsZoomed {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetIsZoomed {value: $value}";
}
}
extension _$_SetPlayingToString on _SetPlaying {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetPlaying {}";
}
}
extension _$_SetPauseToString on _SetPause {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetPause {}";
}
}
extension _$_SetLivePhotoLoadFailedToString on _SetLivePhotoLoadFailed {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetLivePhotoLoadFailed {}";
}
}
extension _$_SetErrorToString on _SetError {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetError {error: $error, stackTrace: $stackTrace}";
}
}

View file

@ -0,0 +1,135 @@
part of '../file_content_view.dart';
@npLog
class _Bloc extends Bloc<_Event, _State> with BlocLogger {
_Bloc({
required this.account,
required this.filesController,
required this.fileId,
required bool shouldPlayLivePhoto,
required bool canZoom,
required bool canPlay,
required bool isPlayControlVisible,
}) : super(_State.init(
file: filesController.stream.value.dataMap[fileId],
shouldPlayLivePhoto: shouldPlayLivePhoto,
canZoom: canZoom,
canPlay: canPlay,
isPlayControlVisible: isPlayControlVisible,
)) {
on<_SetFile>(_onSetFile);
on<_SetShouldPlayLivePhoto>(_onSetShouldPlayLivePhoto);
on<_SetCanZoom>(_onSetCanZoom);
on<_SetCanPlay>(_onSetCanPlay);
on<_SetIsPlayControlVisible>(_onSetIsPlayControlVisible);
on<_SetLoaded>(_onSetLoaded);
on<_SetIsZoomed>(_onSetIsZoomed);
on<_SetContentHeight>(_onSetContentHeight);
on<_SetPlaying>(_onSetPlaying);
on<_SetPause>(_onSetPause);
on<_SetLivePhotoLoadFailed>(_onSetLivePhotoLoadFailed);
on<_SetError>(_onSetError);
_subscriptions.add(filesController.stream.listen((ev) {
add(_SetFile(ev.dataMap[fileId]));
}));
_subscriptions.add(filesController.errorStream.listen((ev) {
add(_SetError(ev.error, ev.stackTrace));
}));
}
@override
Future<void> close() {
for (final s in _subscriptions) {
s.cancel();
}
return super.close();
}
@override
String get tag => _log.fullName;
@override
void onError(Object error, StackTrace stackTrace) {
// we need this to prevent onError being triggered recursively
if (!isClosed && !_isHandlingError) {
_isHandlingError = true;
try {
add(_SetError(error, stackTrace));
} catch (_) {}
_isHandlingError = false;
}
super.onError(error, stackTrace);
}
void _onSetFile(_SetFile ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(file: ev.value));
}
void _onSetShouldPlayLivePhoto(_SetShouldPlayLivePhoto ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(shouldPlayLivePhoto: ev.value));
}
void _onSetCanZoom(_SetCanZoom ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(canZoom: ev.value));
}
void _onSetCanPlay(_SetCanPlay ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(canPlay: ev.value));
}
void _onSetIsPlayControlVisible(_SetIsPlayControlVisible ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(isPlayControlVisible: ev.value));
}
void _onSetLoaded(_SetLoaded ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(isLoaded: true));
}
void _onSetIsZoomed(_SetIsZoomed ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(isZoomed: ev.value));
}
void _onSetContentHeight(_SetContentHeight ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(contentHeight: ev.value));
}
void _onSetPlaying(_SetPlaying ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(isPlaying: true));
}
void _onSetPause(_SetPause ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(isPlaying: false));
}
void _onSetLivePhotoLoadFailed(_SetLivePhotoLoadFailed ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(
shouldPlayLivePhoto: false,
isLivePhotoLoadFailed: Unique(true),
));
}
void _onSetError(_SetError ev, Emitter<_State> emit) {
_log.info(ev);
emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace)));
}
final Account account;
final FilesController filesController;
final int fileId;
final _subscriptions = <StreamSubscription>[];
var _isHandlingError = false;
}

View file

@ -0,0 +1,169 @@
part of '../file_content_view.dart';
@genCopyWith
@toString
class _State {
const _State({
required this.file,
required this.shouldPlayLivePhoto,
required this.canZoom,
required this.canPlay,
required this.isPlayControlVisible,
required this.isLoaded,
required this.isZoomed,
required this.isPlaying,
required this.isLivePhotoLoadFailed,
this.contentHeight,
this.error,
});
factory _State.init({
required FileDescriptor? file,
required bool shouldPlayLivePhoto,
required bool canZoom,
required bool canPlay,
required bool isPlayControlVisible,
}) =>
_State(
file: file,
shouldPlayLivePhoto: shouldPlayLivePhoto,
canZoom: canZoom,
canPlay: canPlay,
isPlayControlVisible: isPlayControlVisible,
isLoaded: false,
isZoomed: false,
isPlaying: false,
isLivePhotoLoadFailed: Unique(false),
);
@override
String toString() => _$toString();
final FileDescriptor? file;
final bool shouldPlayLivePhoto;
final bool canZoom;
final bool canPlay;
final bool isPlayControlVisible;
final bool isLoaded;
final bool isZoomed;
final bool isPlaying;
final Unique<bool> isLivePhotoLoadFailed;
final double? contentHeight;
final ExceptionEvent? error;
}
abstract class _Event {}
@toString
class _SetFile implements _Event {
const _SetFile(this.value);
@override
String toString() => _$toString();
final FileDescriptor? value;
}
@toString
class _SetShouldPlayLivePhoto implements _Event {
const _SetShouldPlayLivePhoto(this.value);
@override
String toString() => _$toString();
final bool value;
}
@toString
class _SetCanZoom implements _Event {
const _SetCanZoom(this.value);
@override
String toString() => _$toString();
final bool value;
}
@toString
class _SetCanPlay implements _Event {
const _SetCanPlay(this.value);
@override
String toString() => _$toString();
final bool value;
}
@toString
class _SetIsPlayControlVisible implements _Event {
const _SetIsPlayControlVisible(this.value);
@override
String toString() => _$toString();
final bool value;
}
@toString
class _SetLoaded implements _Event {
const _SetLoaded();
@override
String toString() => _$toString();
}
@toString
class _SetContentHeight implements _Event {
const _SetContentHeight(this.value);
@override
String toString() => _$toString();
final double value;
}
@toString
class _SetIsZoomed implements _Event {
const _SetIsZoomed(this.value);
@override
String toString() => _$toString();
final bool value;
}
@toString
class _SetPlaying implements _Event {
const _SetPlaying();
@override
String toString() => _$toString();
}
@toString
class _SetPause implements _Event {
const _SetPause();
@override
String toString() => _$toString();
}
@toString
class _SetLivePhotoLoadFailed implements _Event {
const _SetLivePhotoLoadFailed();
@override
String toString() => _$toString();
}
@toString
class _SetError implements _Event {
const _SetError(this.error, [this.stackTrace]);
@override
String toString() => _$toString();
final Object error;
final StackTrace? stackTrace;
}

View file

@ -0,0 +1,107 @@
part of '../file_content_view.dart';
class _LivePhotoPageContentView extends StatelessWidget {
const _LivePhotoPageContentView({
required this.livePhotoType,
});
@override
Widget build(BuildContext context) {
return _BlocBuilder(
buildWhen: (previous, current) =>
previous.file != current.file || previous.canPlay != current.canPlay,
builder: (context, state) => state.file == null
? Container()
: LivePhotoViewer(
account: context.bloc.account,
file: state.file!,
livePhotoType: livePhotoType,
canPlay: state.canPlay,
onLoaded: () {
context.addEvent(const _SetLoaded());
},
onHeightChanged: (height) {
context.addEvent(_SetContentHeight(height));
},
onLoadFailure: () {
context.addEvent(const _SetLivePhotoLoadFailed());
},
),
);
}
final LivePhotoType livePhotoType;
}
class _PhotoPageContentView extends StatelessWidget {
const _PhotoPageContentView();
@override
Widget build(BuildContext context) {
return _BlocBuilder(
buildWhen: (previous, current) =>
previous.file != current.file || previous.canZoom != current.canZoom,
builder: (context, state) => state.file == null
? Container()
: RemoteImageViewer(
account: context.bloc.account,
file: state.file!,
canZoom: state.canZoom,
onLoaded: () {
context.addEvent(const _SetLoaded());
},
onHeightChanged: (height) {
context.addEvent(_SetContentHeight(height));
},
onZoomStarted: () {
context.addEvent(const _SetIsZoomed(true));
},
onZoomEnded: () {
context.addEvent(const _SetIsZoomed(false));
},
),
);
}
}
class _VideoPageContentView extends StatelessWidget {
const _VideoPageContentView();
@override
Widget build(BuildContext context) {
return _BlocBuilder(
buildWhen: (previous, current) =>
previous.file != current.file ||
previous.canZoom != current.canZoom ||
previous.isPlayControlVisible != current.isPlayControlVisible ||
previous.canPlay != current.canPlay,
builder: (context, state) => state.file == null
? Container()
: VideoViewer(
account: context.bloc.account,
file: state.file!,
canZoom: state.canZoom,
canPlay: state.canPlay,
isControlVisible: state.isPlayControlVisible,
onLoaded: () {
context.addEvent(const _SetLoaded());
},
onHeightChanged: (height) {
context.addEvent(_SetContentHeight(height));
},
onZoomStarted: () {
context.addEvent(const _SetIsZoomed(true));
},
onZoomEnded: () {
context.addEvent(const _SetIsZoomed(false));
},
onPlay: () {
context.addEvent(const _SetPlaying());
},
onPause: () {
context.addEvent(const _SetPause());
},
),
);
}
}

View file

@ -99,7 +99,7 @@ extension _$_ItemNpLog on _Item {
extension _$_StateToString on _State { extension _$_StateToString on _State {
String _$toString() { String _$toString() {
// ignore: unnecessary_string_interpolations // ignore: unnecessary_string_interpolations
return "_State {collections: [length: ${collections.length}], sort: ${sort.name}, isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, itemCounts: $itemCounts, error: $error, removeError: $removeError}"; return "_State {collections: [length: ${collections.length}], sort: ${sort.name}, isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, itemCounts: {length: ${itemCounts.length}}, error: $error, removeError: $removeError}";
} }
} }

View file

@ -87,10 +87,9 @@ class _ContentListBody extends StatelessWidget {
Navigator.of(context).pushNamed( Navigator.of(context).pushNamed(
Viewer.routeName, Viewer.routeName,
arguments: ViewerArguments( arguments: ViewerArguments(
context.bloc.account,
state.transformedItems state.transformedItems
.whereType<_FileItem>() .whereType<_FileItem>()
.map((e) => e.file) .map((e) => e.file.fdId)
.toList(), .toList(),
actualIndex, actualIndex,
), ),

View file

@ -87,11 +87,12 @@ class _HomeSearchState extends State<HomeSearch>
@override @override
onItemTap(SelectableItem item, int index) { onItemTap(SelectableItem item, int index) {
item.as<PhotoListFileItem>()?.run((fileItem) { item.as<PhotoListFileItem>()?.run((fileItem) {
Navigator.pushNamed( Navigator.of(context).pushNamed(
context,
Viewer.routeName, Viewer.routeName,
arguments: arguments: ViewerArguments(
ViewerArguments(widget.account, _backingFiles, fileItem.fileIndex), _backingFiles.map((e) => e.fdId).toList(),
fileItem.fileIndex,
),
); );
}); });
} }

View file

@ -1,4 +1,3 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart'; import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@ -152,40 +151,17 @@ class _RemoteImageViewerState extends State<RemoteImageViewer> {
toHeroContext, toHeroContext,
); );
}, },
child: CachedNetworkImage( child: _PreviewImage(
fit: BoxFit.contain, account: widget.account,
cacheManager: ThumbnailCacheManager.inst, file: widget.file,
imageUrl: NetworkRectThumbnail.imageUrlForFile(
widget.account, widget.file),
httpHeaders: {
"Authorization":
AuthUtil.fromAccount(widget.account).toHeaderValue(),
},
fadeInDuration: const Duration(),
filterQuality: FilterQuality.high,
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
), ),
), ),
), ),
if (_isHeroDone) if (_isHeroDone)
mod.CachedNetworkImage( _FullSizedImage(
fit: BoxFit.contain, account: widget.account,
cacheManager: LargeImageCacheManager.inst, file: widget.file,
imageUrl: _getImageUrl(widget.account, widget.file), onItemLoaded: _onItemLoaded,
httpHeaders: {
"Authorization":
AuthUtil.fromAccount(widget.account).toHeaderValue(),
},
fadeInDuration: const Duration(),
filterQuality: FilterQuality.high,
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
imageBuilder: (context, child, imageProvider) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_onItemLoaded();
});
const SizeChangedLayoutNotification().dispatch(context);
return child;
},
), ),
], ],
), ),
@ -286,3 +262,66 @@ String _getImageUrl(Account account, FileDescriptor file) {
); );
} }
} }
class _PreviewImage extends StatelessWidget {
const _PreviewImage({
required this.account,
required this.file,
});
@override
Widget build(BuildContext context) {
return mod.CachedNetworkImage(
fit: BoxFit.contain,
cacheManager: ThumbnailCacheManager.inst,
imageUrl: NetworkRectThumbnail.imageUrlForFile(account, file),
httpHeaders: {
"Authorization": AuthUtil.fromAccount(account).toHeaderValue(),
},
fadeInDuration: const Duration(),
filterQuality: FilterQuality.high,
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
imageBuilder: (context, child, imageProvider) {
const SizeChangedLayoutNotification().dispatch(context);
return child;
},
);
}
final Account account;
final FileDescriptor file;
}
class _FullSizedImage extends StatelessWidget {
const _FullSizedImage({
required this.account,
required this.file,
this.onItemLoaded,
});
@override
Widget build(BuildContext context) {
return mod.CachedNetworkImage(
fit: BoxFit.contain,
cacheManager: LargeImageCacheManager.inst,
imageUrl: _getImageUrl(account, file),
httpHeaders: {
"Authorization": AuthUtil.fromAccount(account).toHeaderValue(),
},
fadeInDuration: const Duration(),
filterQuality: FilterQuality.high,
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
imageBuilder: (context, child, imageProvider) {
WidgetsBinding.instance.addPostFrameCallback((_) {
onItemLoaded?.call();
});
const SizeChangedLayoutNotification().dispatch(context);
return child;
},
);
}
final Account account;
final FileDescriptor file;
final VoidCallback? onItemLoaded;
}

View file

@ -76,8 +76,7 @@ class _ResultViewerState extends State<ResultViewer> {
); );
} else { } else {
return Viewer( return Viewer(
account: _account!, fileIds: [_file!.fdId],
streamFiles: [_file!],
startIndex: 0, startIndex: 0,
); );
} }

File diff suppressed because it is too large Load diff

View file

@ -2,13 +2,421 @@
part of 'viewer.dart'; part of 'viewer.dart';
// **************************************************************************
// CopyWithLintRuleGenerator
// **************************************************************************
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
// **************************************************************************
// CopyWithGenerator
// **************************************************************************
abstract class $_StateCopyWithWorker {
_State call(
{List<int>? fileIdOrders,
Map<int, FileDescriptor>? files,
Map<int, _PageState>? fileStates,
int? index,
FileDescriptor? currentFile,
_PageState? currentFileState,
Collection? collection,
CollectionItemsController? collectionItemsController,
Map<int, CollectionItem>? collectionItems,
bool? isShowDetailPane,
bool? isClosingDetailPane,
bool? isDetailPaneActive,
Unique<_OpenDetailPaneRequest>? openDetailPaneRequest,
Unique<bool>? closeDetailPane,
bool? isZoomed,
bool? isShowAppBar,
bool? isInitialLoad,
Unique<int?>? pendingRemovePage,
Unique<ImageEditorArguments?>? imageEditorRequest,
Unique<ImageEnhancerArguments?>? imageEnhancerRequest,
Unique<_ShareRequest?>? shareRequest,
Unique<_SlideshowRequest?>? slideshowRequest,
ExceptionEvent? error});
}
class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
_$_StateCopyWithWorkerImpl(this.that);
@override
_State call(
{dynamic fileIdOrders,
dynamic files,
dynamic fileStates,
dynamic index,
dynamic currentFile = copyWithNull,
dynamic currentFileState = copyWithNull,
dynamic collection = copyWithNull,
dynamic collectionItemsController = copyWithNull,
dynamic collectionItems = copyWithNull,
dynamic isShowDetailPane,
dynamic isClosingDetailPane,
dynamic isDetailPaneActive,
dynamic openDetailPaneRequest,
dynamic closeDetailPane,
dynamic isZoomed,
dynamic isShowAppBar,
dynamic isInitialLoad,
dynamic pendingRemovePage,
dynamic imageEditorRequest,
dynamic imageEnhancerRequest,
dynamic shareRequest,
dynamic slideshowRequest,
dynamic error = copyWithNull}) {
return _State(
fileIdOrders: fileIdOrders as List<int>? ?? that.fileIdOrders,
files: files as Map<int, FileDescriptor>? ?? that.files,
fileStates: fileStates as Map<int, _PageState>? ?? that.fileStates,
index: index as int? ?? that.index,
currentFile: currentFile == copyWithNull
? that.currentFile
: currentFile as FileDescriptor?,
currentFileState: currentFileState == copyWithNull
? that.currentFileState
: currentFileState as _PageState?,
collection: collection == copyWithNull
? that.collection
: collection as Collection?,
collectionItemsController: collectionItemsController == copyWithNull
? that.collectionItemsController
: collectionItemsController as CollectionItemsController?,
collectionItems: collectionItems == copyWithNull
? that.collectionItems
: collectionItems as Map<int, CollectionItem>?,
isShowDetailPane: isShowDetailPane as bool? ?? that.isShowDetailPane,
isClosingDetailPane:
isClosingDetailPane as bool? ?? that.isClosingDetailPane,
isDetailPaneActive:
isDetailPaneActive as bool? ?? that.isDetailPaneActive,
openDetailPaneRequest:
openDetailPaneRequest as Unique<_OpenDetailPaneRequest>? ??
that.openDetailPaneRequest,
closeDetailPane:
closeDetailPane as Unique<bool>? ?? that.closeDetailPane,
isZoomed: isZoomed as bool? ?? that.isZoomed,
isShowAppBar: isShowAppBar as bool? ?? that.isShowAppBar,
isInitialLoad: isInitialLoad as bool? ?? that.isInitialLoad,
pendingRemovePage:
pendingRemovePage as Unique<int?>? ?? that.pendingRemovePage,
imageEditorRequest:
imageEditorRequest as Unique<ImageEditorArguments?>? ??
that.imageEditorRequest,
imageEnhancerRequest:
imageEnhancerRequest as Unique<ImageEnhancerArguments?>? ??
that.imageEnhancerRequest,
shareRequest:
shareRequest as Unique<_ShareRequest?>? ?? that.shareRequest,
slideshowRequest: slideshowRequest as Unique<_SlideshowRequest?>? ??
that.slideshowRequest,
error: error == copyWithNull ? that.error : error as ExceptionEvent?);
}
final _State that;
}
extension $_StateCopyWith on _State {
$_StateCopyWithWorker get copyWith => _$copyWith;
$_StateCopyWithWorker get _$copyWith => _$_StateCopyWithWorkerImpl(this);
}
abstract class $_PageStateCopyWithWorker {
_PageState call(
{double? itemHeight, bool? hasLoaded, bool? shouldPlayLivePhoto});
}
class _$_PageStateCopyWithWorkerImpl implements $_PageStateCopyWithWorker {
_$_PageStateCopyWithWorkerImpl(this.that);
@override
_PageState call(
{dynamic itemHeight = copyWithNull,
dynamic hasLoaded,
dynamic shouldPlayLivePhoto}) {
return _PageState(
itemHeight: itemHeight == copyWithNull
? that.itemHeight
: itemHeight as double?,
hasLoaded: hasLoaded as bool? ?? that.hasLoaded,
shouldPlayLivePhoto:
shouldPlayLivePhoto as bool? ?? that.shouldPlayLivePhoto);
}
final _PageState that;
}
extension $_PageStateCopyWith on _PageState {
$_PageStateCopyWithWorker get copyWith => _$copyWith;
$_PageStateCopyWithWorker get _$copyWith =>
_$_PageStateCopyWithWorkerImpl(this);
}
// ************************************************************************** // **************************************************************************
// NpLogGenerator // NpLogGenerator
// ************************************************************************** // **************************************************************************
extension _$_ViewerStateNpLog on _ViewerState { extension _$_WrappedViewerStateNpLog on _WrappedViewerState {
// ignore: unused_element // ignore: unused_element
Logger get _log => log; Logger get _log => log;
static final log = Logger("widget.viewer._ViewerState"); static final log = Logger("widget.viewer._WrappedViewerState");
}
extension _$_BlocNpLog on _Bloc {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("widget.viewer._Bloc");
}
extension _$_ContentBodyStateNpLog on _ContentBodyState {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("widget.viewer._ContentBodyState");
}
extension _$_PageViewStateNpLog on _PageViewState {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("widget.viewer._PageViewState");
}
// **************************************************************************
// ToStringGenerator
// **************************************************************************
extension _$_StateToString on _State {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_State {fileIdOrders: $fileIdOrders, files: {length: ${files.length}}, fileStates: {length: ${fileStates.length}}, index: $index, currentFile: ${currentFile == null ? null : "${currentFile!.fdPath}"}, currentFileState: $currentFileState, collection: $collection, collectionItemsController: $collectionItemsController, collectionItems: ${collectionItems == null ? null : "{length: ${collectionItems!.length}}"}, isShowDetailPane: $isShowDetailPane, isClosingDetailPane: $isClosingDetailPane, isDetailPaneActive: $isDetailPaneActive, openDetailPaneRequest: $openDetailPaneRequest, closeDetailPane: $closeDetailPane, isZoomed: $isZoomed, isShowAppBar: $isShowAppBar, isInitialLoad: $isInitialLoad, pendingRemovePage: $pendingRemovePage, imageEditorRequest: $imageEditorRequest, imageEnhancerRequest: $imageEnhancerRequest, shareRequest: $shareRequest, slideshowRequest: $slideshowRequest, error: $error}";
}
}
extension _$_PageStateToString on _PageState {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_PageState {itemHeight: ${itemHeight == null ? null : "${itemHeight!.toStringAsFixed(3)}"}, hasLoaded: $hasLoaded, shouldPlayLivePhoto: $shouldPlayLivePhoto}";
}
}
extension _$_InitToString on _Init {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_Init {}";
}
}
extension _$_SetIndexToString on _SetIndex {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetIndex {index: $index}";
}
}
extension _$_RequestPageToString on _RequestPage {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_RequestPage {index: $index}";
}
}
extension _$_SetCollectionToString on _SetCollection {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetCollection {collection: $collection, itemsController: $itemsController}";
}
}
extension _$_SetCollectionItemsToString on _SetCollectionItems {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetCollectionItems {value: ${value == null ? null : "[length: ${value!.length}]"}}";
}
}
extension _$_ToggleAppBarToString on _ToggleAppBar {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_ToggleAppBar {}";
}
}
extension _$_ShowAppBarToString on _ShowAppBar {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_ShowAppBar {}";
}
}
extension _$_HideAppBarToString on _HideAppBar {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_HideAppBar {}";
}
}
extension _$_PauseLivePhotoToString on _PauseLivePhoto {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_PauseLivePhoto {fileId: $fileId}";
}
}
extension _$_PlayLivePhotoToString on _PlayLivePhoto {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_PlayLivePhoto {fileId: $fileId}";
}
}
extension _$_UnfavoriteToString on _Unfavorite {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_Unfavorite {fileId: $fileId}";
}
}
extension _$_FavoriteToString on _Favorite {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_Favorite {fileId: $fileId}";
}
}
extension _$_UnarchiveToString on _Unarchive {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_Unarchive {fileId: $fileId}";
}
}
extension _$_ArchiveToString on _Archive {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_Archive {fileId: $fileId}";
}
}
extension _$_ShareToString on _Share {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_Share {fileId: $fileId}";
}
}
extension _$_EditToString on _Edit {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_Edit {fileId: $fileId}";
}
}
extension _$_EnhanceToString on _Enhance {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_Enhance {fileId: $fileId}";
}
}
extension _$_DownloadToString on _Download {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_Download {fileId: $fileId}";
}
}
extension _$_DeleteToString on _Delete {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_Delete {fileId: $fileId}";
}
}
extension _$_RemoveFromCollectionToString on _RemoveFromCollection {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_RemoveFromCollection {value: $value}";
}
}
extension _$_StartSlideshowToString on _StartSlideshow {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_StartSlideshow {fileId: $fileId}";
}
}
extension _$_OpenDetailPaneToString on _OpenDetailPane {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_OpenDetailPane {shouldAnimate: $shouldAnimate}";
}
}
extension _$_CloseDetailPaneToString on _CloseDetailPane {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_CloseDetailPane {}";
}
}
extension _$_DetailPaneClosedToString on _DetailPaneClosed {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_DetailPaneClosed {}";
}
}
extension _$_ShowDetailPaneToString on _ShowDetailPane {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_ShowDetailPane {}";
}
}
extension _$_SetDetailPaneInactiveToString on _SetDetailPaneInactive {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetDetailPaneInactive {}";
}
}
extension _$_SetDetailPaneActiveToString on _SetDetailPaneActive {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetDetailPaneActive {}";
}
}
extension _$_SetFileContentHeightToString on _SetFileContentHeight {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetFileContentHeight {fileId: $fileId, value: ${value.toStringAsFixed(3)}}";
}
}
extension _$_SetIsZoomedToString on _SetIsZoomed {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetIsZoomed {value: $value}";
}
}
extension _$_RemovePageToString on _RemovePage {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_RemovePage {value: $value}";
}
}
extension _$_SetErrorToString on _SetError {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetError {error: $error, stackTrace: $stackTrace}";
}
} }

View file

@ -0,0 +1,213 @@
part of '../viewer.dart';
class _AppBar extends StatelessWidget {
const _AppBar();
@override
Widget build(BuildContext context) {
final isTitleCentered = getRawPlatform() == NpPlatform.iOs;
return _BlocBuilder(
buildWhen: (previous, current) =>
previous.isDetailPaneActive != current.isDetailPaneActive ||
previous.isZoomed != current.isZoomed,
builder: (context, state) => AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: _BlocBuilder(
buildWhen: (previous, current) =>
previous.isDetailPaneActive != current.isDetailPaneActive ||
previous.currentFile != current.currentFile,
builder: (context, state) =>
!state.isDetailPaneActive && state.currentFile != null
? _AppBarTitle(
file: state.currentFile!,
isCentered: isTitleCentered,
)
: const SizedBox.shrink(),
),
titleSpacing: 0,
centerTitle: isTitleCentered,
actions: !state.isDetailPaneActive && !state.isZoomed
? [
_BlocBuilder(
buildWhen: (previous, current) =>
previous.currentFile != current.currentFile ||
previous.currentFileState != current.currentFileState,
builder: (context, state) {
if (state.currentFile?.let(getLivePhotoTypeFromFile) !=
null) {
if (state.currentFileState?.shouldPlayLivePhoto ??
false) {
return IconButton(
icon: const Icon(Icons.motion_photos_pause_outlined),
onPressed: () {
context.state.currentFile?.fdId.let(
(id) => context.addEvent(_PauseLivePhoto(id)));
},
);
} else {
return IconButton(
icon: const PngIcon(icMotionPhotosPlay24dp),
onPressed: () {
context.state.currentFile?.fdId.let(
(id) => context.addEvent(_PlayLivePhoto(id)));
},
);
}
} else {
return const SizedBox.shrink();
}
},
),
_BlocSelector(
selector: (state) => state.currentFile,
builder: (context, currentFile) => currentFile
?.fdIsFavorite ==
true
? IconButton(
icon: const Icon(Icons.star),
tooltip: L10n.global().unfavoriteTooltip,
onPressed: () {
context.state.currentFile?.fdId
.let((id) => context.addEvent(_Unfavorite(id)));
},
)
: IconButton(
icon: const Icon(Icons.star_border),
tooltip: L10n.global().favoriteTooltip,
onPressed: () {
context.state.currentFile?.fdId
.let((id) => context.addEvent(_Favorite(id)));
},
),
),
IconButton(
icon: const Icon(Icons.more_vert),
tooltip: L10n.global().detailsTooltip,
onPressed: () {
context.addEvent(const _OpenDetailPane(true));
},
),
]
: null,
),
);
}
}
class _AppBarTitle extends StatelessWidget {
const _AppBarTitle({
required this.file,
required this.isCentered,
});
@override
Widget build(BuildContext context) {
final locale = Localizations.localeOf(context).languageCode;
final localTime = file.fdDateTime.toLocal();
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
isCentered ? CrossAxisAlignment.center : CrossAxisAlignment.start,
children: [
Text(
(localTime.year == DateTime.now().year
? DateFormat.MMMd(locale)
: DateFormat.yMMMd(locale))
.format(localTime),
style: Theme.of(context).textTheme.titleMedium,
),
Text(
DateFormat.jm(locale).format(localTime),
style: Theme.of(context).textTheme.bodySmall,
),
],
);
}
final FileDescriptor file;
final bool isCentered;
}
class _BottomAppBar extends StatelessWidget {
const _BottomAppBar();
@override
Widget build(BuildContext context) {
return Container(
height: kToolbarHeight,
alignment: Alignment.center,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment(0, -1),
end: Alignment(0, 1),
colors: [
Color.fromARGB(0, 0, 0, 0),
Color.fromARGB(192, 0, 0, 0),
],
),
),
child: _BlocBuilder(
buildWhen: (previous, current) =>
previous.currentFile != current.currentFile ||
previous.collection != current.collection,
builder: (context, state) => Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
IconButton(
icon: const Icon(Icons.share_outlined),
tooltip: L10n.global().shareTooltip,
onPressed: () {
context.state.currentFile?.fdId
.let((id) => context.addEvent(_Share(id)));
},
),
if (features.isSupportEnhancement &&
state.currentFile?.let(ImageEnhancer.isSupportedFormat) ==
true) ...[
IconButton(
icon: const Icon(Icons.tune_outlined),
tooltip: L10n.global().editTooltip,
onPressed: () {
context.state.currentFile?.fdId
.let((id) => context.addEvent(_Edit(id)));
},
),
IconButton(
icon: const Icon(Icons.auto_fix_high_outlined),
tooltip: L10n.global().enhanceTooltip,
onPressed: () {
context.state.currentFile?.fdId
.let((id) => context.addEvent(_Enhance(id)));
},
),
],
IconButton(
icon: const Icon(Icons.download_outlined),
tooltip: L10n.global().downloadTooltip,
onPressed: () {
context.state.currentFile?.fdId
.let((id) => context.addEvent(_Download(id)));
},
),
if (state.collection == null)
IconButton(
icon: const Icon(Icons.delete_outlined),
tooltip: L10n.global().deleteTooltip,
onPressed: () {
context.state.currentFile?.fdId
.let((id) => context.addEvent(_Delete(id)));
},
),
]
.map((e) => Expanded(
flex: 1,
child: e,
))
.toList(),
),
),
);
}
}

View file

@ -0,0 +1,441 @@
part of '../viewer.dart';
@npLog
class _Bloc extends Bloc<_Event, _State>
with BlocLogger, BlocForEachMixin<_Event, _State> {
_Bloc(
this._c, {
required this.account,
required this.filesController,
required this.collectionsController,
required this.prefController,
required this.brightness,
required List<int> fileIds,
required int startIndex,
this.collectionId,
}) : super(_State.init(
fileIds: fileIds,
index: startIndex,
currentFile:
filesController.stream.value.dataMap[fileIds[startIndex]]!,
)) {
on<_Init>(_onInit);
on<_SetIndex>(_onSetIndex);
on<_RequestPage>(_onRequestPage);
on<_SetCollection>(_onSetCollection);
on<_SetCollectionItems>(_onSetCollectionItems);
on<_ToggleAppBar>(_onToggleAppBar);
on<_ShowAppBar>(_onShowAppBar);
on<_HideAppBar>(_onHideAppBar);
on<_PauseLivePhoto>(_onPauseLivePhoto);
on<_PlayLivePhoto>(_onPlayLivePhoto);
on<_Unfavorite>(_onUnfavorite);
on<_Favorite>(_onFavorite);
on<_Unarchive>(_onUnarchive);
on<_Archive>(_onArchive);
on<_Share>(_onShare);
on<_Edit>(_onEdit);
on<_Enhance>(_onEnhance);
on<_Download>(_onDownload);
on<_Delete>(_onDelete);
on<_RemoveFromCollection>(_onRemoveFromCollection);
on<_StartSlideshow>(_onStartSlideshow);
on<_OpenDetailPane>(_onOpenDetailPane);
on<_CloseDetailPane>(_onCloseDetailPane);
on<_DetailPaneClosed>(_onDetailPaneClosed);
on<_ShowDetailPane>(_onShowDetailPane);
on<_SetDetailPaneInactive>(_onSetDetailPaneInactive);
on<_SetDetailPaneActive>(_onSetDetailPaneActive);
on<_SetFileContentHeight>(_onSetFileContentHeight);
on<_SetIsZoomed>(_onSetIsZoomed);
on<_RemovePage>(_onRemovePage);
on<_SetError>(_onSetError);
if (collectionId != null) {
_subscriptions.add(collectionsController.stream.listen((event) {
for (final c in event.data) {
if (c.collection.id == collectionId) {
add(_SetCollection(c.collection, c.controller));
_collectionItemsSubscription?.cancel();
_collectionItemsSubscription = c.controller.stream.listen((event) {
add(_SetCollectionItems(event.items));
});
return;
}
}
_log.warning("[_Bloc] Collection not found: $collectionId");
add(const _SetCollection(null, null));
add(const _SetCollectionItems(null));
_collectionItemsSubscription?.cancel();
}));
}
add(_SetIndex(startIndex));
}
@override
Future<void> close() {
_collectionItemsSubscription?.cancel();
for (final s in _subscriptions) {
s.cancel();
}
return super.close();
}
@override
String get tag => _log.fullName;
@override
void onError(Object error, StackTrace stackTrace) {
// we need this to prevent onError being triggered recursively
if (!isClosed && !_isHandlingError) {
_isHandlingError = true;
try {
add(_SetError(error, stackTrace));
} catch (_) {}
_isHandlingError = false;
}
super.onError(error, stackTrace);
}
Future<void> _onInit(_Init ev, _Emitter emit) async {
await Future.wait([
forEach(
emit,
filesController.stream,
onData: (data) => state.copyWith(
files: data.dataMap,
currentFile: data.dataMap[state.fileIdOrders[state.index]],
),
),
forEach(
emit,
filesController.errorStream,
onData: (data) => state.copyWith(
error: ExceptionEvent(data.error, data.stackTrace),
),
),
]);
}
void _onSetIndex(_SetIndex ev, _Emitter emit) {
_log.info(ev);
final fileId = state.fileIdOrders[ev.index];
final fileState = state.fileStates[fileId] ?? _PageState.create();
emit(state.copyWith(
index: ev.index,
currentFile: state.files[fileId],
fileStates: state.fileStates[fileId] == null
? state.fileStates.addedAll({fileId: fileState})
: null,
currentFileState: fileState,
isInitialLoad: false,
));
}
void _onRequestPage(_RequestPage ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(index: ev.index));
}
void _onSetCollection(_SetCollection ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(
collection: ev.collection,
collectionItemsController: ev.itemsController,
));
}
void _onSetCollectionItems(_SetCollectionItems ev, _Emitter emit) {
_log.info(ev);
final itemMap = ev.value
?.whereType<CollectionFileItem>()
.map((e) => MapEntry(e.file.fdId, e))
.toMap();
emit(state.copyWith(collectionItems: itemMap));
}
void _onToggleAppBar(_ToggleAppBar ev, _Emitter emit) {
_log.info(ev);
final to = !state.isShowAppBar;
emit(state.copyWith(isShowAppBar: to));
if (to) {
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: SystemUiOverlay.values,
);
} else {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
}
}
void _onShowAppBar(_ShowAppBar ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(isShowAppBar: true));
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: SystemUiOverlay.values,
);
}
void _onHideAppBar(_HideAppBar ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(isShowAppBar: false));
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
}
void _onPauseLivePhoto(_PauseLivePhoto ev, _Emitter emit) {
_log.info(ev);
_updateFileState(ev.fileId, emit, shouldPlayLivePhoto: false);
}
void _onPlayLivePhoto(_PlayLivePhoto ev, _Emitter emit) {
_log.info(ev);
_updateFileState(ev.fileId, emit, shouldPlayLivePhoto: true);
}
void _onUnfavorite(_Unfavorite ev, _Emitter emit) {
_log.info(ev);
final f = state.files[ev.fileId];
if (f == null) {
_log.severe("[_onUnfavorite] file is null: ${ev.fileId}");
return;
}
filesController.updateProperty([f], isFavorite: false);
}
void _onFavorite(_Favorite ev, _Emitter emit) {
_log.info(ev);
final f = state.files[ev.fileId];
if (f == null) {
_log.severe("[_onFavorite] file is null: ${ev.fileId}");
return;
}
filesController.updateProperty([f], isFavorite: true);
}
void _onUnarchive(_Unarchive ev, _Emitter emit) {
_log.info(ev);
final f = state.files[ev.fileId];
if (f == null) {
_log.severe("[_onUnarchive] file is null: ${ev.fileId}");
return;
}
filesController.updateProperty([f], isArchived: const OrNull(false));
_removeFileFromStream(ev.fileId, emit);
}
void _onArchive(_Archive ev, _Emitter emit) {
_log.info(ev);
final f = state.files[ev.fileId];
if (f == null) {
_log.severe("[_onArchive] file is null: ${ev.fileId}");
return;
}
filesController.updateProperty([f], isArchived: const OrNull(true));
_removeFileFromStream(ev.fileId, emit);
}
void _onShare(_Share ev, _Emitter emit) {
_log.info(ev);
final f = state.files[ev.fileId];
if (f == null) {
_log.severe("[_onShare] file is null: ${ev.fileId}");
return;
}
emit(state.copyWith(shareRequest: Unique(_ShareRequest(f))));
}
void _onEdit(_Edit ev, _Emitter emit) {
_log.info(ev);
final f = state.files[ev.fileId];
if (f == null) {
_log.severe("[_onEdit] file is null: ${ev.fileId}");
return;
}
emit(state.copyWith(
imageEditorRequest: Unique(ImageEditorArguments(account, f)),
));
}
void _onEnhance(_Enhance ev, _Emitter emit) {
_log.info(ev);
final f = state.files[ev.fileId];
if (f == null) {
_log.severe("[_onEnhance] file is null: ${ev.fileId}");
return;
}
emit(state.copyWith(
imageEnhancerRequest: Unique(ImageEnhancerArguments(
account,
f,
prefController.isSaveEditResultToServerValue,
)),
));
}
void _onDownload(_Download ev, _Emitter emit) {
_log.info(ev);
final f = state.files[ev.fileId];
if (f == null) {
_log.severe("[_onDownload] file is null: ${ev.fileId}");
return;
}
DownloadHandler(_c).downloadFiles(account, [f]);
}
void _onDelete(_Delete ev, _Emitter emit) {
_log.info(ev);
final f = state.files[ev.fileId];
if (f == null) {
_log.severe("[_onDelete] file is null: ${ev.fileId}");
return;
}
RemoveSelectionHandler(filesController: filesController)(
account: account,
selection: [f],
isRemoveOpened: true,
isMoveToTrash: true,
);
_removeFileFromStream(f.fdId, emit);
}
void _onRemoveFromCollection(_RemoveFromCollection ev, _Emitter emit) {
_log.info(ev);
if (!CollectionAdapter.of(_c, account, state.collection!)
.isPermitted(CollectionCapability.manualItem)) {
throw UnsupportedError("Operation not supported by this collection");
}
state.collectionItemsController!.removeItems([ev.value]);
_removeFileFromStream((ev.value as CollectionFileItem).file.fdId, emit);
}
void _onStartSlideshow(_StartSlideshow ev, _Emitter emit) {
_log.info(ev);
final files =
state.fileIdOrders.map((id) => state.files[id]).nonNulls.toList();
final req = _SlideshowRequest(
account: account,
files: files,
startIndex: files
.indexWhere((e) => e.fdId == ev.fileId)
.let((i) => i == -1 ? 0 : i),
);
emit(state.copyWith(slideshowRequest: Unique(req)));
}
void _onOpenDetailPane(_OpenDetailPane ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(
openDetailPaneRequest: Unique(_OpenDetailPaneRequest(ev.shouldAnimate)),
));
}
void _onCloseDetailPane(_CloseDetailPane ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(
closeDetailPane: Unique(true),
isClosingDetailPane: true,
));
}
void _onDetailPaneClosed(_DetailPaneClosed ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(
isShowDetailPane: false,
isClosingDetailPane: false,
));
}
void _onShowDetailPane(_ShowDetailPane ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(
isShowDetailPane: true,
isDetailPaneActive: true,
));
}
void _onSetDetailPaneInactive(_SetDetailPaneInactive ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(isDetailPaneActive: false));
}
void _onSetDetailPaneActive(_SetDetailPaneActive ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(isDetailPaneActive: true));
}
void _onSetFileContentHeight(_SetFileContentHeight ev, _Emitter emit) {
_log.info(ev);
_updateFileState(ev.fileId, emit, itemHeight: ev.value);
}
void _onSetIsZoomed(_SetIsZoomed ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(isZoomed: ev.value));
}
void _onRemovePage(_RemovePage ev, _Emitter emit) {
_log.info(ev);
final newFileIds = state.fileIdOrders.removedAt(ev.value);
emit(state.copyWith(
fileIdOrders: newFileIds,
index: ev.value <= state.index ? state.index - 1 : state.index,
));
}
void _onSetError(_SetError ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace)));
}
void _updateFileState(
int fileId,
_Emitter emit, {
double? itemHeight,
bool? hasLoaded,
bool? shouldPlayLivePhoto,
}) {
final newStates = Map.of(state.fileStates);
var newState = state.fileStates[fileId] ?? _PageState.create();
newState = newState.copyWith(
hasLoaded: hasLoaded,
shouldPlayLivePhoto: shouldPlayLivePhoto,
);
if (itemHeight != null) {
// we don't support resetting itemHeight to null
newState = newState.copyWith(itemHeight: itemHeight);
}
newStates[fileId] = newState;
if (fileId == state.currentFile?.fdId) {
emit(state.copyWith(
fileStates: newStates,
currentFileState: newState,
));
} else {
emit(state.copyWith(fileStates: newStates));
}
}
void _removeFileFromStream(int fileId, _Emitter emit) {
final index = state.fileIdOrders.indexOf(fileId);
emit(state.copyWith(pendingRemovePage: Unique(index)));
}
final DiContainer _c;
final Account account;
final FilesController filesController;
final CollectionsController collectionsController;
final PrefController prefController;
final Brightness brightness;
final String? collectionId;
final _subscriptions = <StreamSubscription>[];
StreamSubscription? _collectionItemsSubscription;
var _isHandlingError = false;
}

View file

@ -0,0 +1,111 @@
part of '../viewer.dart';
class _DetailPaneContainer extends StatelessWidget {
const _DetailPaneContainer({
required this.fileId,
});
final int fileId;
@override
Widget build(BuildContext context) {
return _BlocBuilder(
buildWhen: (previous, current) =>
previous.isShowDetailPane != current.isShowDetailPane ||
previous.isZoomed != current.isZoomed ||
previous.fileStates[fileId] != current.fileStates[fileId],
builder: (context, state) => IgnorePointer(
ignoring: !state.isShowDetailPane,
child: Visibility(
visible: !state.isZoomed,
child: AnimatedOpacity(
opacity: state.isShowDetailPane ? 1 : 0,
duration: k.animationDurationNormal,
onEnd: () {
if (!state.isShowDetailPane) {
context.addEvent(const _SetDetailPaneInactive());
}
},
child: Theme(
data: buildTheme(context, context.bloc.brightness),
child: Builder(
builder: (context) => Container(
alignment: Alignment.topLeft,
constraints: BoxConstraints(
minHeight: MediaQuery.of(context).size.height,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(4),
),
),
margin: EdgeInsets.only(
top: _calcDetailPaneOffset(
state.fileStates[fileId],
MediaQuery.of(context).size.height,
),
),
// this visibility widget avoids loading the detail pane
// until it's actually opened, otherwise swiping between
// photos will slow down severely
child: Visibility(
visible: state.isShowDetailPane,
child: _DetailPane(fileId: fileId),
),
),
),
),
),
),
),
);
}
}
class _DetailPane extends StatelessWidget {
const _DetailPane({
required this.fileId,
});
@override
Widget build(BuildContext context) {
return _BlocBuilder(
buildWhen: (previous, current) =>
previous.files[fileId] != current.files[fileId] ||
previous.collection != current.collection ||
previous.collectionItems?[fileId] != current.collectionItems?[fileId],
builder: (context, state) {
final file = state.files[fileId];
final collection = state.collection;
final collectionItem = state.collectionItems?[fileId];
return file == null
? const SizedBox.shrink()
: ViewerDetailPane(
account: context.bloc.account,
fd: file,
fromCollection: collection != null && collectionItem != null
? ViewerSingleCollectionData(collection, collectionItem)
: null,
onRemoveFromCollectionPressed: (_) {
context.addEvent(_RemoveFromCollection(collectionItem!));
},
onArchivePressed: (_) {
context.addEvent(_Archive(fileId));
},
onUnarchivePressed: (_) {
context.addEvent(_Unarchive(fileId));
},
onSlideshowPressed: () {
context.addEvent(_StartSlideshow(fileId));
},
onDeletePressed: (_) {
context.addEvent(_Delete(fileId));
},
);
},
);
}
final int fileId;
}

View file

@ -0,0 +1,412 @@
part of '../viewer.dart';
@genCopyWith
@toString
class _State {
const _State({
required this.fileIdOrders,
required this.files,
required this.fileStates,
required this.index,
required this.currentFile,
this.currentFileState,
this.collection,
this.collectionItemsController,
this.collectionItems,
required this.isShowDetailPane,
required this.isClosingDetailPane,
required this.isDetailPaneActive,
required this.openDetailPaneRequest,
required this.closeDetailPane,
required this.isZoomed,
required this.isShowAppBar,
required this.isInitialLoad,
required this.pendingRemovePage,
required this.imageEditorRequest,
required this.imageEnhancerRequest,
required this.shareRequest,
required this.slideshowRequest,
this.error,
});
factory _State.init({
required List<int> fileIds,
required int index,
required FileDescriptor currentFile,
}) =>
_State(
fileIdOrders: fileIds,
files: const {},
fileStates: const {},
index: index,
currentFile: currentFile,
isShowDetailPane: false,
isClosingDetailPane: false,
isDetailPaneActive: false,
openDetailPaneRequest: Unique(const _OpenDetailPaneRequest(false)),
closeDetailPane: Unique(false),
isZoomed: false,
isShowAppBar: true,
isInitialLoad: true,
pendingRemovePage: Unique(null),
imageEditorRequest: Unique(null),
imageEnhancerRequest: Unique(null),
shareRequest: Unique(null),
slideshowRequest: Unique(null),
);
@override
String toString() => _$toString();
bool get canOpenDetailPane => !isZoomed;
@Format(r"$$?")
final List<int> fileIdOrders;
final Map<int, FileDescriptor> files;
final Map<int, _PageState> fileStates;
final int index;
final FileDescriptor? currentFile;
final _PageState? currentFileState;
final Collection? collection;
final CollectionItemsController? collectionItemsController;
final Map<int, CollectionItem>? collectionItems;
final bool isShowDetailPane;
final bool isClosingDetailPane;
final bool isDetailPaneActive;
final Unique<_OpenDetailPaneRequest> openDetailPaneRequest;
final Unique<bool> closeDetailPane;
final bool isZoomed;
final bool isShowAppBar;
final bool isInitialLoad;
final Unique<int?> pendingRemovePage;
final Unique<ImageEditorArguments?> imageEditorRequest;
final Unique<ImageEnhancerArguments?> imageEnhancerRequest;
final Unique<_ShareRequest?> shareRequest;
final Unique<_SlideshowRequest?> slideshowRequest;
final ExceptionEvent? error;
}
@genCopyWith
@toString
class _PageState {
const _PageState({
required this.itemHeight,
required this.hasLoaded,
required this.shouldPlayLivePhoto,
});
factory _PageState.create() {
return const _PageState(
itemHeight: null,
hasLoaded: false,
shouldPlayLivePhoto: false,
);
}
@override
String toString() => _$toString();
final double? itemHeight;
final bool hasLoaded;
final bool shouldPlayLivePhoto;
}
abstract class _Event {}
@toString
class _Init implements _Event {
const _Init();
@override
String toString() => _$toString();
}
@toString
class _SetIndex implements _Event {
const _SetIndex(this.index);
@override
String toString() => _$toString();
final int index;
}
@toString
class _RequestPage implements _Event {
const _RequestPage(this.index);
@override
String toString() => _$toString();
final int index;
}
@toString
class _SetCollection implements _Event {
const _SetCollection(this.collection, this.itemsController);
@override
String toString() => _$toString();
final Collection? collection;
final CollectionItemsController? itemsController;
}
@toString
class _SetCollectionItems implements _Event {
const _SetCollectionItems(this.value);
@override
String toString() => _$toString();
final List<CollectionItem>? value;
}
@toString
class _ToggleAppBar implements _Event {
const _ToggleAppBar();
@override
String toString() => _$toString();
}
@toString
class _ShowAppBar implements _Event {
const _ShowAppBar();
@override
String toString() => _$toString();
}
@toString
class _HideAppBar implements _Event {
const _HideAppBar();
@override
String toString() => _$toString();
}
@toString
class _PauseLivePhoto implements _Event {
const _PauseLivePhoto(this.fileId);
@override
String toString() => _$toString();
final int fileId;
}
@toString
class _PlayLivePhoto implements _Event {
const _PlayLivePhoto(this.fileId);
@override
String toString() => _$toString();
final int fileId;
}
@toString
class _Unfavorite implements _Event {
const _Unfavorite(this.fileId);
@override
String toString() => _$toString();
final int fileId;
}
@toString
class _Favorite implements _Event {
const _Favorite(this.fileId);
@override
String toString() => _$toString();
final int fileId;
}
@toString
class _Unarchive implements _Event {
const _Unarchive(this.fileId);
@override
String toString() => _$toString();
final int fileId;
}
@toString
class _Archive implements _Event {
const _Archive(this.fileId);
@override
String toString() => _$toString();
final int fileId;
}
@toString
class _Share implements _Event {
const _Share(this.fileId);
@override
String toString() => _$toString();
final int fileId;
}
@toString
class _Edit implements _Event {
const _Edit(this.fileId);
@override
String toString() => _$toString();
final int fileId;
}
@toString
class _Enhance implements _Event {
const _Enhance(this.fileId);
@override
String toString() => _$toString();
final int fileId;
}
@toString
class _Download implements _Event {
const _Download(this.fileId);
@override
String toString() => _$toString();
final int fileId;
}
@toString
class _Delete implements _Event {
const _Delete(this.fileId);
@override
String toString() => _$toString();
final int fileId;
}
@toString
class _RemoveFromCollection implements _Event {
const _RemoveFromCollection(this.value);
@override
String toString() => _$toString();
final CollectionItem value;
}
@toString
class _StartSlideshow implements _Event {
const _StartSlideshow(this.fileId);
@override
String toString() => _$toString();
final int fileId;
}
@toString
class _OpenDetailPane implements _Event {
const _OpenDetailPane(this.shouldAnimate);
@override
String toString() => _$toString();
final bool shouldAnimate;
}
@toString
class _CloseDetailPane implements _Event {
const _CloseDetailPane();
@override
String toString() => _$toString();
}
@toString
class _DetailPaneClosed implements _Event {
const _DetailPaneClosed();
@override
String toString() => _$toString();
}
@toString
class _ShowDetailPane implements _Event {
const _ShowDetailPane();
@override
String toString() => _$toString();
}
@toString
class _SetDetailPaneInactive implements _Event {
const _SetDetailPaneInactive();
@override
String toString() => _$toString();
}
@toString
class _SetDetailPaneActive implements _Event {
const _SetDetailPaneActive();
@override
String toString() => _$toString();
}
@toString
class _SetFileContentHeight implements _Event {
const _SetFileContentHeight(this.fileId, this.value);
@override
String toString() => _$toString();
final int fileId;
final double value;
}
@toString
class _SetIsZoomed implements _Event {
const _SetIsZoomed(this.value);
@override
String toString() => _$toString();
final bool value;
}
@toString
class _RemovePage implements _Event {
const _RemovePage(this.value);
@override
String toString() => _$toString();
final int value;
}
@toString
class _SetError implements _Event {
const _SetError(this.error, [this.stackTrace]);
@override
String toString() => _$toString();
final Object error;
final StackTrace? stackTrace;
}

View file

@ -0,0 +1,25 @@
part of '../viewer.dart';
class _OpenDetailPaneRequest {
const _OpenDetailPaneRequest(this.shouldAnimate);
final bool shouldAnimate;
}
class _ShareRequest {
const _ShareRequest(this.file);
final FileDescriptor file;
}
class _SlideshowRequest {
const _SlideshowRequest({
required this.account,
required this.files,
required this.startIndex,
});
final Account account;
final List<FileDescriptor> files;
final int startIndex;
}

View file

@ -0,0 +1,372 @@
part of '../viewer.dart';
class _ContentBody extends StatefulWidget {
const _ContentBody();
@override
State<StatefulWidget> createState() => _ContentBodyState();
}
@npLog
class _ContentBodyState extends State<_ContentBody> {
@override
Widget build(BuildContext context) {
return MultiBlocListener(
listeners: [
_BlocListener(
listenWhen: (previous, current) =>
previous.pendingRemovePage != current.pendingRemovePage,
listener: (context, state) {
final index = state.pendingRemovePage.value;
if (index == null) {
return;
}
if (state.fileIdOrders.length <= 1) {
// removing the only item, pop view
Navigator.of(context).pop();
} else if (index == state.index) {
// removing current page
if (index >= state.fileIdOrders.length - 1) {
// removing the last item, go back
_pageViewController
.previousPage(
duration: k.animationDurationNormal,
curve: Curves.easeInOut,
)
.then((_) {
if (mounted) {
context.addEvent(_RemovePage(index));
}
});
} else {
_pageViewController
.nextPage(
duration: k.animationDurationNormal,
curve: Curves.easeInOut,
)
.then((_) {
if (mounted) {
context.addEvent(_RemovePage(index));
}
});
}
} else {
context.addEvent(_RemovePage(index));
}
},
),
_BlocListenerT(
selector: (state) => state.index,
listener: (context, index) {
if (index != _pageViewController.currentPage) {
_log.info(
"[build] Page out sync, correcting: ${_pageViewController.currentPage} -> $index");
_pageViewController.jumpToPage(index);
}
},
),
],
child: GestureDetector(
onTap: () {
context.addEvent(const _ToggleAppBar());
},
child: Stack(
children: [
const Positioned.fill(
child: ColoredBox(color: Colors.black),
),
_BlocBuilder(
buildWhen: (previous, current) =>
previous.fileIdOrders != current.fileIdOrders ||
previous.isZoomed != current.isZoomed,
builder: (context, state) => HorizontalPageViewer(
key: _key,
pageCount: state.fileIdOrders.length,
pageBuilder: (context, i) => _PageView(
key: Key("FileContentView-${state.fileIdOrders[i]}"),
fileId: state.fileIdOrders[i],
pageHeight: MediaQuery.of(context).size.height,
),
initialPage: state.index,
controller: _pageViewController,
viewportFraction: _viewportFraction,
canSwitchPage: !state.isZoomed,
onPageChanged: (from, to) {
context.addEvent(_SetIndex(to));
},
),
),
_BlocSelector<bool>(
selector: (state) => state.isShowAppBar,
builder: (context, isShowAppBar) => isShowAppBar
? Container(
// + status bar height
height:
kToolbarHeight + MediaQuery.of(context).padding.top,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment(0, -1),
end: Alignment(0, 1),
colors: [
Color.fromARGB(192, 0, 0, 0),
Color.fromARGB(0, 0, 0, 0),
],
),
),
)
: const SizedBox.shrink(),
),
],
),
),
);
}
// prevent view getting disposed
final _key = GlobalKey();
final _pageViewController = HorizontalPageViewerController();
}
class _PageView extends StatefulWidget {
const _PageView({
super.key,
required this.fileId,
required this.pageHeight,
});
@override
State<StatefulWidget> createState() => _PageViewState();
final int fileId;
final double pageHeight;
}
@npLog
class _PageViewState extends State<_PageView> {
@override
void initState() {
super.initState();
_scrollController = ScrollController(
initialScrollOffset:
context.state.isShowDetailPane && !context.state.isClosingDetailPane
? _calcDetailPaneOpenedScrollPosition(
context.state.fileStates[widget.fileId], widget.pageHeight)
: 0,
);
if (context.state.isShowDetailPane && !context.state.isClosingDetailPane) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final pageState = context.state.fileStates[widget.fileId];
if (mounted && pageState?.itemHeight != null) {
_hasInitDetailPane = true;
context.addEvent(const _OpenDetailPane(false));
}
});
} else {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_scrollController.jumpTo(0);
}
});
}
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MultiBlocListener(
listeners: [
_BlocListener(
listenWhen: (previous, current) =>
previous.openDetailPaneRequest != current.openDetailPaneRequest,
listener: (context, state) {
if (!state.canOpenDetailPane) {
_log.warning("[build] Can't open detail pane right now");
return;
}
if (state.openDetailPaneRequest.value.shouldAnimate) {
_scrollController.animateTo(
_calcDetailPaneOpenedScrollPosition(
context.state.fileStates[widget.fileId], widget.pageHeight),
duration: k.animationDurationNormal,
curve: Curves.easeOut,
);
} else {
_scrollController.jumpTo(_calcDetailPaneOpenedScrollPosition(
context.state.fileStates[widget.fileId], widget.pageHeight));
}
},
),
_BlocListenerT(
selector: (state) => state.closeDetailPane,
listener: (context, state) {
_scrollController.animateTo(
0,
duration: k.animationDurationNormal,
curve: Curves.easeOut,
);
},
),
_BlocListenerT(
selector: (state) => state.fileStates[widget.fileId]?.itemHeight,
listener: (context, itemHeight) {
if (itemHeight != null && !_hasInitDetailPane) {
if (context.state.isShowDetailPane &&
!context.state.isClosingDetailPane) {
_hasInitDetailPane = true;
context.addEvent(const _OpenDetailPane(false));
}
}
},
),
],
child: _BlocSelector(
selector: (state) => state.files[widget.fileId],
builder: (context, file) {
if (file == null) {
return const Center(
child: Text("File not found"),
);
} else {
return FractionallySizedBox(
widthFactor: 1 / _viewportFraction,
child: NotificationListener<ScrollNotification>(
onNotification: _onPageContentScrolled,
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context)
.copyWith(scrollbars: false),
child: _BlocBuilder(
buildWhen: (previous, current) =>
previous.isZoomed != current.isZoomed,
builder: (context, state) => SingleChildScrollView(
controller: _scrollController,
physics: !state.isZoomed
? null
: const NeverScrollableScrollPhysics(),
child: Stack(
children: [
_BlocBuilder(
buildWhen: (previous, current) =>
previous.fileStates[widget.fileId] !=
current.fileStates[widget.fileId] ||
previous.isShowAppBar != current.isShowAppBar ||
previous.isDetailPaneActive !=
current.isDetailPaneActive,
builder: (context, state) => FileContentView(
fileId: file.fdId,
shouldPlayLivePhoto: state
.fileStates[widget.fileId]
?.shouldPlayLivePhoto ??
false,
canZoom: !state.isDetailPaneActive,
canPlay: !state.isDetailPaneActive,
isPlayControlVisible: state.isShowAppBar &&
!state.isDetailPaneActive,
onContentHeightChanged: (contentHeight) {
context.addEvent(_SetFileContentHeight(
widget.fileId, contentHeight));
},
onZoomChanged: (isZoomed) {
context.addEvent(_SetIsZoomed(isZoomed));
},
onVideoPlayingChanged: (isPlaying) {
if (isPlaying) {
context.addEvent(const _HideAppBar());
} else {
context.addEvent(const _ShowAppBar());
}
},
onLivePhotoLoadFailue: () {
context
.addEvent(_PauseLivePhoto(widget.fileId));
},
),
),
_DetailPaneContainer(fileId: widget.fileId),
],
),
),
),
),
),
);
}
},
),
);
}
bool _onPageContentScrolled(ScrollNotification notification) {
if (!context.state.canOpenDetailPane) {
return false;
}
if (notification is ScrollStartNotification) {
_scrollStartPosition = _scrollController.position.pixels;
}
if (notification is ScrollEndNotification) {
_scrollStartPosition = null;
final scrollPos = _scrollController.position;
if (scrollPos.pixels == 0) {
context.addEvent(const _DetailPaneClosed());
} else if (scrollPos.pixels <
_calcDetailPaneOpenedScrollPosition(
context.state.fileStates[widget.fileId], widget.pageHeight) -
1) {
if (scrollPos.userScrollDirection == ScrollDirection.reverse) {
// upward, open the pane to its minimal size
context.addEvent(const _OpenDetailPane(true));
} else if (scrollPos.userScrollDirection == ScrollDirection.forward) {
// downward, close the pane
context.addEvent(const _CloseDetailPane());
}
}
} else if (notification is ScrollUpdateNotification) {
if (!context.state.isShowDetailPane) {
context.addEvent(const _ShowDetailPane());
}
}
if (notification is OverscrollNotification) {
if (_scrollStartPosition == 0) {
// start at top
_overscrollSum += notification.overscroll;
if (_overscrollSum < -144) {
// and scroll downwards
Navigator.of(context).pop();
}
}
} else {
_overscrollSum = 0;
}
return false;
}
late final ScrollController _scrollController;
double? _scrollStartPosition;
var _overscrollSum = 0.0;
var _hasInitDetailPane = false;
}
double _calcDetailPaneOpenedScrollPosition(
_PageState? pageState, double pageHeight) {
// distance of the detail pane from the top edge
const distanceFromTop = 196;
return max(_calcDetailPaneOffset(pageState, pageHeight) - distanceFromTop, 0);
}
double _calcDetailPaneOffset(_PageState? pageState, double pageHeight) {
if (pageState?.itemHeight == null) {
return pageHeight;
} else {
return pageState!.itemHeight! +
(pageHeight - pageState.itemHeight!) / 2 -
4;
}
}
const _viewportFraction = 1.05;

View file

@ -1,41 +0,0 @@
import 'package:flutter/material.dart';
/// Button bar near the bottom of viewer
///
/// Buttons are spread evenly across the horizontal axis
class ViewerBottomAppBar extends StatelessWidget {
const ViewerBottomAppBar({
super.key,
required this.children,
});
@override
build(BuildContext context) {
return Container(
height: kToolbarHeight,
alignment: Alignment.center,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment(0, -1),
end: Alignment(0, 1),
colors: [
Color.fromARGB(0, 0, 0, 0),
Color.fromARGB(192, 0, 0, 0),
],
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: children
.map((e) => Expanded(
flex: 1,
child: e,
))
.toList(),
),
);
}
final List<Widget> children;
}

View file

@ -87,7 +87,9 @@ class _ZoomableViewerState extends State<ZoomableViewer>
} }
void _setIsZooming(bool flag) { void _setIsZooming(bool flag) {
_isZooming = flag; setState(() {
_isZooming = flag;
});
final next = _isZoomed; final next = _isZoomed;
if (next != _wasZoomed) { if (next != _wasZoomed) {
_wasZoomed = next; _wasZoomed = next;

View file

@ -86,4 +86,6 @@ extension ListExtension<T> on List<T> {
List<T> added(T value) => toList()..add(value); List<T> added(T value) => toList()..add(value);
List<T> removed(T value) => toList()..remove(value); List<T> removed(T value) => toList()..remove(value);
List<T> removedAt(int index) => toList()..removeAt(index);
} }