mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 16:56:19 +01:00
Merge branch 'viewer-rewrite'
This commit is contained in:
commit
0c1b2ba979
39 changed files with 3446 additions and 1083 deletions
|
@ -7,6 +7,7 @@ targets:
|
|||
double: "${$?.toStringAsFixed(3)}"
|
||||
List: "[length: ${$?.length}]"
|
||||
Set: "{length: ${$?.length}}"
|
||||
Map: "{length: ${$?.length}}"
|
||||
File: "${$?.path}"
|
||||
FileDescriptor: "${$?.fdPath}"
|
||||
useEnumName: true
|
||||
|
|
|
@ -11,6 +11,7 @@ import 'package:nc_photos/di_container.dart';
|
|||
import 'package:nc_photos/entity/file.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/progress_util.dart';
|
||||
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
|
||||
import 'package:nc_photos/rx_extension.dart';
|
||||
|
@ -90,6 +91,8 @@ class FilesController {
|
|||
/// callers must sort it by themselves if the ordering is important
|
||||
ValueStream<FilesStreamEvent> get stream => _dataStreamController.stream;
|
||||
|
||||
Stream<ExceptionEvent> get errorStream => _dataErrorStreamController.stream;
|
||||
|
||||
/// Return a stream of file summaries associated with [account]
|
||||
///
|
||||
/// File summary contains the number of files grouped by their dates
|
||||
|
@ -107,6 +110,9 @@ class FilesController {
|
|||
ValueStream<TimelineStreamEvent> get timelineStream =>
|
||||
_timelineStreamController.stream;
|
||||
|
||||
Stream<ExceptionEvent> get timelineErrorStream =>
|
||||
_timelineErrorStreamController.stream;
|
||||
|
||||
Future<void> syncRemote({
|
||||
void Function(Progress progress)? onProgressUpdate,
|
||||
}) async {
|
||||
|
@ -166,7 +172,7 @@ class FilesController {
|
|||
OrNull<DateTime>? overrideDateTime,
|
||||
bool? isFavorite,
|
||||
OrNull<ImageLocation>? location,
|
||||
Exception? Function(List<int> fileIds) errorBuilder =
|
||||
Exception? Function(List<FileDescriptor> files) errorBuilder =
|
||||
UpdatePropertyFailureError.new,
|
||||
}) async {
|
||||
final dataBackups = <int, FileDescriptor>{};
|
||||
|
@ -287,8 +293,8 @@ class FilesController {
|
|||
}
|
||||
return value.copyWith(data: next);
|
||||
});
|
||||
errorBuilder(failures.map((e) => e.fdId).toList())
|
||||
?.let(_dataStreamController.addError);
|
||||
errorBuilder(failures)
|
||||
?.let((e) => _dataErrorStreamController.add(ExceptionEvent(e)));
|
||||
}
|
||||
|
||||
// TODO query outdated
|
||||
|
@ -299,7 +305,7 @@ class FilesController {
|
|||
|
||||
Future<void> remove(
|
||||
List<FileDescriptor> files, {
|
||||
Exception? Function(List<int> fileIds) errorBuilder =
|
||||
Exception? Function(List<FileDescriptor> files) errorBuilder =
|
||||
RemoveFailureError.new,
|
||||
}) async {
|
||||
final dataBackups = <int, FileDescriptor>{};
|
||||
|
@ -316,7 +322,7 @@ class FilesController {
|
|||
_summaryStreamController.addWithValue((value) {
|
||||
final next = Map.of(value.summary.items);
|
||||
for (final f in files) {
|
||||
final key = f.fdDateTime.toDate();
|
||||
final key = f.fdDateTime.toLocal().toDate();
|
||||
final original = next[key];
|
||||
if (original == null) {
|
||||
continue;
|
||||
|
@ -391,8 +397,8 @@ class FilesController {
|
|||
}
|
||||
return value.copyWith(data: next);
|
||||
});
|
||||
errorBuilder(failures.map((e) => e.fdId).toList())
|
||||
?.let(_dataStreamController.addError);
|
||||
errorBuilder(failures)
|
||||
?.let((e) => _dataErrorStreamController.add(ExceptionEvent(e)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -447,7 +453,7 @@ class FilesController {
|
|||
files: v.files.addedAll(data),
|
||||
));
|
||||
} catch (e, stackTrace) {
|
||||
_dataStreamController.addError(e, stackTrace);
|
||||
_dataErrorStreamController.add(ExceptionEvent(e, stackTrace));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -462,7 +468,7 @@ class FilesController {
|
|||
files: v.files.addedAll(data),
|
||||
));
|
||||
} catch (e, stackTrace) {
|
||||
_dataStreamController.addError(e, stackTrace);
|
||||
_dataErrorStreamController.add(ExceptionEvent(e, stackTrace));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -483,7 +489,7 @@ class FilesController {
|
|||
));
|
||||
_addTimelineDateRange(dateRange);
|
||||
} catch (e, stackTrace) {
|
||||
_timelineStreamController.addError(e, stackTrace);
|
||||
_timelineErrorStreamController.add(ExceptionEvent(e, stackTrace));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -663,6 +669,8 @@ class FilesController {
|
|||
hasNext: true,
|
||||
),
|
||||
);
|
||||
final _dataErrorStreamController =
|
||||
StreamController<ExceptionEvent>.broadcast();
|
||||
|
||||
var _isSummaryStreamInited = false;
|
||||
final _summaryStreamController = BehaviorSubject<FilesSummaryStreamEvent>();
|
||||
|
@ -670,6 +678,8 @@ class FilesController {
|
|||
final _timelineStreamController = BehaviorSubject.seeded(
|
||||
const TimelineStreamEvent(data: {}, isDummy: true),
|
||||
);
|
||||
final _timelineErrorStreamController =
|
||||
StreamController<ExceptionEvent>.broadcast();
|
||||
// sorted in descending order
|
||||
var _timelineQueriedRanges = <DateRange>[];
|
||||
|
||||
|
@ -680,22 +690,22 @@ class FilesController {
|
|||
|
||||
@toString
|
||||
class UpdatePropertyFailureError implements Exception {
|
||||
const UpdatePropertyFailureError(this.fileIds);
|
||||
const UpdatePropertyFailureError(this.files);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final List<int> fileIds;
|
||||
final List<FileDescriptor> files;
|
||||
}
|
||||
|
||||
@toString
|
||||
class RemoveFailureError implements Exception {
|
||||
const RemoveFailureError(this.fileIds);
|
||||
const RemoveFailureError(this.files);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final List<int> fileIds;
|
||||
final List<FileDescriptor> files;
|
||||
}
|
||||
|
||||
class _MockResult {
|
||||
|
|
|
@ -77,13 +77,13 @@ extension _$FilesControllerNpLog on FilesController {
|
|||
extension _$UpdatePropertyFailureErrorToString on UpdatePropertyFailureError {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "UpdatePropertyFailureError {fileIds: [length: ${fileIds.length}]}";
|
||||
return "UpdatePropertyFailureError {files: [length: ${files.length}]}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$RemoveFailureErrorToString on RemoveFailureError {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "RemoveFailureError {fileIds: [length: ${fileIds.length}]}";
|
||||
return "RemoveFailureError {files: [length: ${files.length}]}";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import 'package:nc_photos/k.dart' as k;
|
|||
import 'package:nc_photos/language_util.dart';
|
||||
import 'package:nc_photos/protected_page_handler.dart';
|
||||
import 'package:nc_photos/size.dart';
|
||||
import 'package:nc_photos/widget/viewer.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:np_common/object_util.dart';
|
||||
import 'package:np_gps_map/np_gps_map.dart';
|
||||
|
@ -210,6 +211,45 @@ class PrefController {
|
|||
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> setViewerAppBarButtons(List<ViewerAppBarButtonType> value) =>
|
||||
_set<List<ViewerAppBarButtonType>>(
|
||||
controller: _viewerAppBarButtonsController,
|
||||
setter: (pref, value) => pref.setViewerAppBarButtons(value),
|
||||
value: value,
|
||||
);
|
||||
|
||||
Future<bool> setViewerBottomAppBarButtons(
|
||||
List<ViewerAppBarButtonType> value) =>
|
||||
_set<List<ViewerAppBarButtonType>>(
|
||||
controller: _viewerBottomAppBarButtonsController,
|
||||
setter: (pref, value) => pref.setViewerBottomAppBarButtons(value),
|
||||
value: value,
|
||||
);
|
||||
|
||||
Future<bool> _set<T>({
|
||||
required BehaviorSubject<T> controller,
|
||||
required Future<bool> Function(Pref pref, T value) setter,
|
||||
|
@ -337,6 +377,35 @@ class PrefController {
|
|||
@npSubjectAccessor
|
||||
late final _mapDefaultCustomRangeController = BehaviorSubject.seeded(
|
||||
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);
|
||||
@npSubjectAccessor
|
||||
late final _viewerAppBarButtonsController =
|
||||
BehaviorSubject.seeded(pref.getViewerAppBarButtons() ??
|
||||
const [
|
||||
ViewerAppBarButtonType.livePhoto,
|
||||
ViewerAppBarButtonType.favorite,
|
||||
]);
|
||||
@npSubjectAccessor
|
||||
late final _viewerBottomAppBarButtonsController =
|
||||
BehaviorSubject.seeded(pref.getViewerBottomAppBarButtons() ??
|
||||
const [
|
||||
ViewerAppBarButtonType.share,
|
||||
ViewerAppBarButtonType.edit,
|
||||
ViewerAppBarButtonType.enhance,
|
||||
ViewerAppBarButtonType.download,
|
||||
ViewerAppBarButtonType.delete,
|
||||
]);
|
||||
}
|
||||
|
||||
extension PrefControllerExtension on PrefController {
|
||||
|
|
|
@ -206,6 +206,52 @@ extension $PrefControllerNpSubjectAccessor on PrefController {
|
|||
mapDefaultCustomRange.distinct().skip(1);
|
||||
Duration get mapDefaultCustomRangeValue =>
|
||||
_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;
|
||||
// _viewerAppBarButtonsController
|
||||
ValueStream<List<ViewerAppBarButtonType>> get viewerAppBarButtons =>
|
||||
_viewerAppBarButtonsController.stream;
|
||||
Stream<List<ViewerAppBarButtonType>> get viewerAppBarButtonsNew =>
|
||||
viewerAppBarButtons.skip(1);
|
||||
Stream<List<ViewerAppBarButtonType>> get viewerAppBarButtonsChange =>
|
||||
viewerAppBarButtons.distinct().skip(1);
|
||||
List<ViewerAppBarButtonType> get viewerAppBarButtonsValue =>
|
||||
_viewerAppBarButtonsController.value;
|
||||
// _viewerBottomAppBarButtonsController
|
||||
ValueStream<List<ViewerAppBarButtonType>> get viewerBottomAppBarButtons =>
|
||||
_viewerBottomAppBarButtonsController.stream;
|
||||
Stream<List<ViewerAppBarButtonType>> get viewerBottomAppBarButtonsNew =>
|
||||
viewerBottomAppBarButtons.skip(1);
|
||||
Stream<List<ViewerAppBarButtonType>> get viewerBottomAppBarButtonsChange =>
|
||||
viewerBottomAppBarButtons.distinct().skip(1);
|
||||
List<ViewerAppBarButtonType> get viewerBottomAppBarButtonsValue =>
|
||||
_viewerBottomAppBarButtonsController.value;
|
||||
}
|
||||
|
||||
extension $SecurePrefControllerNpSubjectAccessor on SecurePrefController {
|
||||
|
|
|
@ -131,6 +131,41 @@ extension on Pref {
|
|||
?.let((v) => Duration(days: v));
|
||||
Future<bool> setMapDefaultCustomRange(Duration value) =>
|
||||
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);
|
||||
|
||||
List<ViewerAppBarButtonType>? getViewerAppBarButtons() => provider
|
||||
.getIntList(PrefKey.viewerAppBarButtons)
|
||||
?.map(ViewerAppBarButtonType.fromValue)
|
||||
.toList();
|
||||
Future<bool> setViewerAppBarButtons(List<ViewerAppBarButtonType> value) =>
|
||||
provider.setIntList(
|
||||
PrefKey.viewerAppBarButtons, value.map((e) => e.index).toList());
|
||||
|
||||
List<ViewerAppBarButtonType>? getViewerBottomAppBarButtons() => provider
|
||||
.getIntList(PrefKey.viewerBottomAppBarButtons)
|
||||
?.map(ViewerAppBarButtonType.fromValue)
|
||||
.toList();
|
||||
Future<bool> setViewerBottomAppBarButtons(
|
||||
List<ViewerAppBarButtonType> value) =>
|
||||
provider.setIntList(PrefKey.viewerBottomAppBarButtons,
|
||||
value.map((e) => e.index).toList());
|
||||
}
|
||||
|
||||
MapCoord? _tryMapCoordFromJson(dynamic json) {
|
||||
|
|
|
@ -117,6 +117,8 @@ enum PrefKey implements PrefKeyInterface {
|
|||
isNewHttpEngine,
|
||||
mapDefaultRangeType,
|
||||
mapDefaultCustomRange,
|
||||
viewerAppBarButtons,
|
||||
viewerBottomAppBarButtons,
|
||||
;
|
||||
|
||||
@override
|
||||
|
@ -211,6 +213,10 @@ enum PrefKey implements PrefKeyInterface {
|
|||
return "mapDefaultRangeType";
|
||||
case PrefKey.mapDefaultCustomRange:
|
||||
return "mapDefaultCustomRange";
|
||||
case PrefKey.viewerAppBarButtons:
|
||||
return "viewerAppBarButtons";
|
||||
case PrefKey.viewerBottomAppBarButtons:
|
||||
return "viewerBottomAppBarButtons";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -260,6 +266,9 @@ abstract class PrefProvider {
|
|||
List<String>? getStringList(PrefKeyInterface key);
|
||||
Future<bool> setStringList(PrefKeyInterface key, List<String> value);
|
||||
|
||||
List<int>? getIntList(PrefKeyInterface key);
|
||||
Future<bool> setIntList(PrefKeyInterface key, List<int> value);
|
||||
|
||||
Future<bool> remove(PrefKeyInterface key);
|
||||
Future<bool> clear();
|
||||
}
|
||||
|
|
|
@ -82,27 +82,6 @@ extension PrefExtension on Pref {
|
|||
Future<bool> setLanguage(int value) => _set<int>(
|
||||
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() =>
|
||||
provider.getBool(PrefKey.isAlbumBrowserShowDate);
|
||||
bool isAlbumBrowserShowDateOr([bool def = false]) =>
|
||||
|
@ -179,13 +158,6 @@ extension PrefExtension on Pref {
|
|||
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 isVideoPlayerMuteOr([bool def = false]) => isVideoPlayerMute() ?? def;
|
||||
Future<bool> setVideoPlayerMute(bool value) => _set<bool>(
|
||||
|
|
|
@ -31,6 +31,12 @@ class PrefMemoryProvider extends PrefProvider {
|
|||
Future<bool> setStringList(PrefKeyInterface key, List<String> value) =>
|
||||
_set(key, value);
|
||||
|
||||
@override
|
||||
List<int>? getIntList(PrefKeyInterface key) => _get<List<int>>(key);
|
||||
@override
|
||||
Future<bool> setIntList(PrefKeyInterface key, List<int> value) =>
|
||||
_set(key, value);
|
||||
|
||||
@override
|
||||
Future<bool> remove(PrefKeyInterface key) async {
|
||||
_data.remove(key.toStringKey());
|
||||
|
|
|
@ -71,6 +71,16 @@ class PrefSecureStorageProvider implements PrefProvider {
|
|||
Future<bool> setStringList(PrefKeyInterface key, List<String> value) =>
|
||||
setString(key, jsonEncode(value));
|
||||
|
||||
@override
|
||||
List<int>? getIntList(PrefKeyInterface key) {
|
||||
final value = _rawData[key.toStringKey()];
|
||||
return (value?.let(jsonDecode) as List).cast<int>();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> setIntList(PrefKeyInterface key, List<int> value) =>
|
||||
setString(key, jsonEncode(value));
|
||||
|
||||
@override
|
||||
Future<bool> remove(PrefKeyInterface key) async {
|
||||
try {
|
||||
|
|
|
@ -47,6 +47,15 @@ class PrefSharedPreferencesProvider extends PrefProvider {
|
|||
Future<bool> setStringList(PrefKeyInterface key, List<String> value) =>
|
||||
_pref.setStringList(key.toStringKey(), value);
|
||||
|
||||
@override
|
||||
List<int>? getIntList(PrefKeyInterface key) =>
|
||||
_pref.getStringList(key.toStringKey())?.map(int.parse).toList();
|
||||
|
||||
@override
|
||||
Future<bool> setIntList(PrefKeyInterface key, List<int> value) =>
|
||||
_pref.setStringList(
|
||||
key.toStringKey(), value.map((e) => e.toString()).toList());
|
||||
|
||||
@override
|
||||
Future<bool> remove(PrefKeyInterface key) => _pref.remove(key.toStringKey());
|
||||
|
||||
|
|
|
@ -36,6 +36,12 @@ class PrefUniversalStorageProvider extends PrefProvider {
|
|||
Future<bool> setStringList(PrefKeyInterface key, List<String> value) =>
|
||||
_set(key, value);
|
||||
|
||||
@override
|
||||
List<int>? getIntList(PrefKeyInterface key) => _get<List<int>>(key);
|
||||
@override
|
||||
Future<bool> setIntList(PrefKeyInterface key, List<int> value) =>
|
||||
_set(key, value);
|
||||
|
||||
@override
|
||||
Future<bool> remove(PrefKeyInterface key) async {
|
||||
final newData = Map.of(_data)..remove(key.toStringKey());
|
||||
|
|
|
@ -2,6 +2,8 @@ import 'dart:io';
|
|||
|
||||
import 'package:flutter/material.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/navigation_manager.dart';
|
||||
import 'package:nc_photos/widget/trusted_cert_manager.dart';
|
||||
|
@ -58,6 +60,11 @@ String toUserString(Object? exception) {
|
|||
?.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);
|
||||
}
|
||||
|
|
|
@ -59,21 +59,24 @@ class _Bloc extends Bloc<_Event, _State>
|
|||
Future<void> _onLoad(_LoadItems ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
unawaited(filesController.queryByArchived());
|
||||
return forEach(
|
||||
return Future.wait([
|
||||
forEach(
|
||||
emit,
|
||||
filesController.stream,
|
||||
onData: (data) => state.copyWith(
|
||||
files: data.data,
|
||||
isLoading: data.hasNext || _itemTransformerQueue.isProcessing,
|
||||
),
|
||||
onError: (e, stackTrace) {
|
||||
_log.severe("[_onLoad] Uncaught exception", e, stackTrace);
|
||||
return state.copyWith(
|
||||
),
|
||||
forEach(
|
||||
emit,
|
||||
filesController.errorStream,
|
||||
onData: (data) => state.copyWith(
|
||||
isLoading: _itemTransformerQueue.isProcessing,
|
||||
error: ExceptionEvent(e, stackTrace),
|
||||
);
|
||||
},
|
||||
);
|
||||
error: ExceptionEvent(data.error, data.stackTrace),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
void _onTransformItems(_TransformItems ev, Emitter<_State> emit) {
|
||||
|
|
|
@ -73,10 +73,9 @@ class _ContentListBody extends StatelessWidget {
|
|||
Navigator.of(context).pushNamed(
|
||||
Viewer.routeName,
|
||||
arguments: ViewerArguments(
|
||||
context.bloc.account,
|
||||
state.transformedItems
|
||||
.whereType<_FileItem>()
|
||||
.map((e) => e.file)
|
||||
.map((e) => e.file.fdId)
|
||||
.toList(),
|
||||
actualIndex,
|
||||
),
|
||||
|
|
|
@ -149,13 +149,14 @@ class _Bloc extends Bloc<_Event, _State>
|
|||
itemsWhitelist: whitelist,
|
||||
);
|
||||
},
|
||||
onError: (e, stackTrace) {
|
||||
_log.severe("[_onLoad] Uncaught exception", e, stackTrace);
|
||||
return state.copyWith(
|
||||
),
|
||||
forEach(
|
||||
emit,
|
||||
filesController.errorStream,
|
||||
onData: (data) => state.copyWith(
|
||||
isLoading: false,
|
||||
error: ExceptionEvent(e, stackTrace),
|
||||
);
|
||||
},
|
||||
error: ExceptionEvent(data.error, data.stackTrace),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -73,19 +73,12 @@ class _ContentListBody extends StatelessWidget {
|
|||
Navigator.of(context).pushNamed(
|
||||
Viewer.routeName,
|
||||
arguments: ViewerArguments(
|
||||
context.bloc.account,
|
||||
state.transformedItems
|
||||
.whereType<_FileItem>()
|
||||
.map((e) => e.file)
|
||||
.map((e) => e.file.fdId)
|
||||
.toList(),
|
||||
actualIndex,
|
||||
fromCollection: ViewerCollectionData(
|
||||
state.collection,
|
||||
state.transformedItems
|
||||
.whereType<_ActualItem>()
|
||||
.map((e) => e.original)
|
||||
.toList(),
|
||||
),
|
||||
collectionId: state.collection.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
185
app/lib/widget/file_content_view.dart
Normal file
185
app/lib/widget/file_content_view.dart
Normal 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);
|
||||
}
|
191
app/lib/widget/file_content_view.g.dart
Normal file
191
app/lib/widget/file_content_view.g.dart
Normal 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}";
|
||||
}
|
||||
}
|
135
app/lib/widget/file_content_view/bloc.dart
Normal file
135
app/lib/widget/file_content_view/bloc.dart
Normal 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;
|
||||
}
|
169
app/lib/widget/file_content_view/state_event.dart
Normal file
169
app/lib/widget/file_content_view/state_event.dart
Normal 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;
|
||||
}
|
107
app/lib/widget/file_content_view/view.dart
Normal file
107
app/lib/widget/file_content_view/view.dart
Normal 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());
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -99,7 +99,7 @@ extension _$_ItemNpLog on _Item {
|
|||
extension _$_StateToString on _State {
|
||||
String _$toString() {
|
||||
// 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}";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -87,10 +87,9 @@ class _ContentListBody extends StatelessWidget {
|
|||
Navigator.of(context).pushNamed(
|
||||
Viewer.routeName,
|
||||
arguments: ViewerArguments(
|
||||
context.bloc.account,
|
||||
state.transformedItems
|
||||
.whereType<_FileItem>()
|
||||
.map((e) => e.file)
|
||||
.map((e) => e.file.fdId)
|
||||
.toList(),
|
||||
actualIndex,
|
||||
),
|
||||
|
|
|
@ -87,11 +87,12 @@ class _HomeSearchState extends State<HomeSearch>
|
|||
@override
|
||||
onItemTap(SelectableItem item, int index) {
|
||||
item.as<PhotoListFileItem>()?.run((fileItem) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
Navigator.of(context).pushNamed(
|
||||
Viewer.routeName,
|
||||
arguments:
|
||||
ViewerArguments(widget.account, _backingFiles, fileItem.fileIndex),
|
||||
arguments: ViewerArguments(
|
||||
_backingFiles.map((e) => e.fdId).toList(),
|
||||
fileItem.fileIndex,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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:flutter/widgets.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
@ -152,40 +151,17 @@ class _RemoteImageViewerState extends State<RemoteImageViewer> {
|
|||
toHeroContext,
|
||||
);
|
||||
},
|
||||
child: CachedNetworkImage(
|
||||
fit: BoxFit.contain,
|
||||
cacheManager: ThumbnailCacheManager.inst,
|
||||
imageUrl: NetworkRectThumbnail.imageUrlForFile(
|
||||
widget.account, widget.file),
|
||||
httpHeaders: {
|
||||
"Authorization":
|
||||
AuthUtil.fromAccount(widget.account).toHeaderValue(),
|
||||
},
|
||||
fadeInDuration: const Duration(),
|
||||
filterQuality: FilterQuality.high,
|
||||
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
|
||||
child: _PreviewImage(
|
||||
account: widget.account,
|
||||
file: widget.file,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_isHeroDone)
|
||||
mod.CachedNetworkImage(
|
||||
fit: BoxFit.contain,
|
||||
cacheManager: LargeImageCacheManager.inst,
|
||||
imageUrl: _getImageUrl(widget.account, widget.file),
|
||||
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;
|
||||
},
|
||||
_FullSizedImage(
|
||||
account: widget.account,
|
||||
file: widget.file,
|
||||
onItemLoaded: _onItemLoaded,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -76,8 +76,7 @@ class _ResultViewerState extends State<ResultViewer> {
|
|||
);
|
||||
} else {
|
||||
return Viewer(
|
||||
account: _account!,
|
||||
streamFiles: [_file!],
|
||||
fileIds: [_file!.fdId],
|
||||
startIndex: 0,
|
||||
);
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -2,13 +2,444 @@
|
|||
|
||||
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? isInitialLoad,
|
||||
bool? isShowAppBar,
|
||||
List<ViewerAppBarButtonType>? appBarButtons,
|
||||
List<ViewerAppBarButtonType>? bottomAppBarButtons,
|
||||
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 isInitialLoad,
|
||||
dynamic isShowAppBar,
|
||||
dynamic appBarButtons,
|
||||
dynamic bottomAppBarButtons,
|
||||
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,
|
||||
isInitialLoad: isInitialLoad as bool? ?? that.isInitialLoad,
|
||||
isShowAppBar: isShowAppBar as bool? ?? that.isShowAppBar,
|
||||
appBarButtons: appBarButtons as List<ViewerAppBarButtonType>? ??
|
||||
that.appBarButtons,
|
||||
bottomAppBarButtons:
|
||||
bottomAppBarButtons as List<ViewerAppBarButtonType>? ??
|
||||
that.bottomAppBarButtons,
|
||||
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
|
||||
// **************************************************************************
|
||||
|
||||
extension _$_ViewerStateNpLog on _ViewerState {
|
||||
extension _$_WrappedViewerStateNpLog on _WrappedViewerState {
|
||||
// ignore: unused_element
|
||||
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, isInitialLoad: $isInitialLoad, isShowAppBar: $isShowAppBar, appBarButtons: [length: ${appBarButtons.length}], bottomAppBarButtons: [length: ${bottomAppBarButtons.length}], 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 _$_SetAppBarButtonsToString on _SetAppBarButtons {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_SetAppBarButtons {value: [length: ${value.length}]}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_SetBottomAppBarButtonsToString on _SetBottomAppBarButtons {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_SetBottomAppBarButtons {value: [length: ${value.length}]}";
|
||||
}
|
||||
}
|
||||
|
||||
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}";
|
||||
}
|
||||
}
|
||||
|
|
165
app/lib/widget/viewer/app_bar.dart
Normal file
165
app/lib/widget/viewer/app_bar.dart
Normal file
|
@ -0,0 +1,165 @@
|
|||
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 ||
|
||||
previous.currentFile != current.currentFile ||
|
||||
previous.collection != current.collection ||
|
||||
previous.appBarButtons != current.appBarButtons,
|
||||
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
|
||||
? [
|
||||
...state.appBarButtons
|
||||
.map((e) => _buildAppBarButton(
|
||||
e,
|
||||
currentFile: state.currentFile,
|
||||
collection: state.collection,
|
||||
))
|
||||
.nonNulls,
|
||||
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 ||
|
||||
previous.bottomAppBarButtons != current.bottomAppBarButtons,
|
||||
builder: (context, state) => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: state.bottomAppBarButtons
|
||||
.map((e) => _buildAppBarButton(
|
||||
e,
|
||||
currentFile: state.currentFile,
|
||||
collection: state.collection,
|
||||
))
|
||||
.nonNulls
|
||||
.map((e) => Expanded(
|
||||
flex: 1,
|
||||
child: e,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Build app bar buttons based on [type]. May return null if this button type
|
||||
/// is not supported in the current context
|
||||
Widget? _buildAppBarButton(
|
||||
ViewerAppBarButtonType type, {
|
||||
required FileDescriptor? currentFile,
|
||||
required Collection? collection,
|
||||
}) {
|
||||
switch (type) {
|
||||
case ViewerAppBarButtonType.livePhoto:
|
||||
return currentFile?.let(getLivePhotoTypeFromFile) != null
|
||||
? const _AppBarLivePhotoButton()
|
||||
: null;
|
||||
case ViewerAppBarButtonType.favorite:
|
||||
return const _AppBarFavoriteButton();
|
||||
case ViewerAppBarButtonType.share:
|
||||
return const _AppBarShareButton();
|
||||
case ViewerAppBarButtonType.edit:
|
||||
return features.isSupportEnhancement &&
|
||||
currentFile?.let(ImageEnhancer.isSupportedFormat) == true
|
||||
? const _AppBarEditButton()
|
||||
: null;
|
||||
case ViewerAppBarButtonType.enhance:
|
||||
return features.isSupportEnhancement &&
|
||||
currentFile?.let(ImageEnhancer.isSupportedFormat) == true
|
||||
? const _AppBarEnhanceButton()
|
||||
: null;
|
||||
case ViewerAppBarButtonType.download:
|
||||
return const _AppBarDownloadButton();
|
||||
case ViewerAppBarButtonType.delete:
|
||||
return collection == null ? const _AppBarDeleteButton() : null;
|
||||
}
|
||||
}
|
160
app/lib/widget/viewer/app_bar_buttons.dart
Normal file
160
app/lib/widget/viewer/app_bar_buttons.dart
Normal file
|
@ -0,0 +1,160 @@
|
|||
part of '../viewer.dart';
|
||||
|
||||
enum ViewerAppBarButtonType {
|
||||
// the order must not be changed
|
||||
livePhoto,
|
||||
favorite,
|
||||
share,
|
||||
edit,
|
||||
enhance,
|
||||
download,
|
||||
delete,
|
||||
;
|
||||
|
||||
static ViewerAppBarButtonType fromValue(int value) =>
|
||||
ViewerAppBarButtonType.values[value];
|
||||
}
|
||||
|
||||
class _AppBarLivePhotoButton extends StatelessWidget {
|
||||
const _AppBarLivePhotoButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _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();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AppBarFavoriteButton extends StatelessWidget {
|
||||
const _AppBarFavoriteButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _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)));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AppBarShareButton extends StatelessWidget {
|
||||
const _AppBarShareButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.share_outlined),
|
||||
tooltip: L10n.global().shareTooltip,
|
||||
onPressed: () {
|
||||
context.state.currentFile?.fdId
|
||||
.let((id) => context.addEvent(_Share(id)));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AppBarEditButton extends StatelessWidget {
|
||||
const _AppBarEditButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.tune_outlined),
|
||||
tooltip: L10n.global().editTooltip,
|
||||
onPressed: () {
|
||||
context.state.currentFile?.fdId
|
||||
.let((id) => context.addEvent(_Edit(id)));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AppBarEnhanceButton extends StatelessWidget {
|
||||
const _AppBarEnhanceButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.auto_fix_high_outlined),
|
||||
tooltip: L10n.global().enhanceTooltip,
|
||||
onPressed: () {
|
||||
context.state.currentFile?.fdId
|
||||
.let((id) => context.addEvent(_Enhance(id)));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AppBarDownloadButton extends StatelessWidget {
|
||||
const _AppBarDownloadButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.download_outlined),
|
||||
tooltip: L10n.global().downloadTooltip,
|
||||
onPressed: () {
|
||||
context.state.currentFile?.fdId
|
||||
.let((id) => context.addEvent(_Download(id)));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AppBarDeleteButton extends StatelessWidget {
|
||||
const _AppBarDeleteButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.delete_outlined),
|
||||
tooltip: L10n.global().deleteTooltip,
|
||||
onPressed: () {
|
||||
context.state.currentFile?.fdId
|
||||
.let((id) => context.addEvent(_Delete(id)));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
462
app/lib/widget/viewer/bloc.dart
Normal file
462
app/lib/widget/viewer/bloc.dart
Normal file
|
@ -0,0 +1,462 @@
|
|||
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]]!,
|
||||
appBarButtons: prefController.viewerAppBarButtonsValue,
|
||||
bottomAppBarButtons: prefController.viewerBottomAppBarButtonsValue,
|
||||
)) {
|
||||
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<_SetAppBarButtons>(_onAppBarButtons);
|
||||
on<_SetBottomAppBarButtons>(_onBottomAppBarButtons);
|
||||
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();
|
||||
}));
|
||||
}
|
||||
_subscriptions.add(prefController.viewerAppBarButtonsChange.listen((event) {
|
||||
add(_SetAppBarButtons(event));
|
||||
}));
|
||||
_subscriptions
|
||||
.add(prefController.viewerBottomAppBarButtonsChange.listen((event) {
|
||||
add(_SetBottomAppBarButtons(event));
|
||||
}));
|
||||
|
||||
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 _onAppBarButtons(_SetAppBarButtons ev, _Emitter emit) {
|
||||
_log.info(ev);
|
||||
emit(state.copyWith(appBarButtons: ev.value));
|
||||
}
|
||||
|
||||
void _onBottomAppBarButtons(_SetBottomAppBarButtons ev, _Emitter emit) {
|
||||
_log.info(ev);
|
||||
emit(state.copyWith(bottomAppBarButtons: ev.value));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
111
app/lib/widget/viewer/detail_pane.dart
Normal file
111
app/lib/widget/viewer/detail_pane.dart
Normal 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;
|
||||
}
|
441
app/lib/widget/viewer/state_event.dart
Normal file
441
app/lib/widget/viewer/state_event.dart
Normal file
|
@ -0,0 +1,441 @@
|
|||
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.isInitialLoad,
|
||||
required this.isShowAppBar,
|
||||
required this.appBarButtons,
|
||||
required this.bottomAppBarButtons,
|
||||
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,
|
||||
required List<ViewerAppBarButtonType> appBarButtons,
|
||||
required List<ViewerAppBarButtonType> bottomAppBarButtons,
|
||||
}) =>
|
||||
_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,
|
||||
isInitialLoad: true,
|
||||
isShowAppBar: true,
|
||||
appBarButtons: appBarButtons,
|
||||
bottomAppBarButtons: bottomAppBarButtons,
|
||||
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 isInitialLoad;
|
||||
|
||||
final bool isShowAppBar;
|
||||
final List<ViewerAppBarButtonType> appBarButtons;
|
||||
final List<ViewerAppBarButtonType> bottomAppBarButtons;
|
||||
|
||||
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 _SetAppBarButtons implements _Event {
|
||||
const _SetAppBarButtons(this.value);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final List<ViewerAppBarButtonType> value;
|
||||
}
|
||||
|
||||
@toString
|
||||
class _SetBottomAppBarButtons implements _Event {
|
||||
const _SetBottomAppBarButtons(this.value);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final List<ViewerAppBarButtonType> value;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
25
app/lib/widget/viewer/type.dart
Normal file
25
app/lib/widget/viewer/type.dart
Normal 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;
|
||||
}
|
372
app/lib/widget/viewer/view.dart
Normal file
372
app/lib/widget/viewer/view.dart
Normal 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.hasClients) {
|
||||
_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;
|
|
@ -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;
|
||||
}
|
|
@ -88,6 +88,9 @@ class _ZoomableViewerState extends State<ZoomableViewer>
|
|||
|
||||
void _setIsZooming(bool flag) {
|
||||
_isZooming = flag;
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
final next = _isZoomed;
|
||||
if (next != _wasZoomed) {
|
||||
_wasZoomed = next;
|
||||
|
|
|
@ -86,4 +86,6 @@ extension ListExtension<T> on List<T> {
|
|||
List<T> added(T value) => toList()..add(value);
|
||||
|
||||
List<T> removed(T value) => toList()..remove(value);
|
||||
|
||||
List<T> removedAt(int index) => toList()..removeAt(index);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue