nc-photos/app/lib/widget/home_photos.dart

1180 lines
37 KiB
Dart
Raw Normal View History

2022-07-28 18:59:26 +02:00
import 'dart:async';
2022-06-12 07:45:51 +02:00
import 'dart:collection';
import 'dart:math' as math;
2022-04-09 12:45:17 +02:00
import 'dart:ui';
2022-06-12 07:45:51 +02:00
import 'package:collection/collection.dart';
2021-05-19 21:11:06 +02:00
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
2021-04-10 06:28:12 +02:00
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
2022-06-12 07:45:51 +02:00
import 'package:intl/intl.dart';
2021-04-10 06:28:12 +02:00
import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
2021-07-25 07:00:38 +02:00
import 'package:nc_photos/app_localizations.dart';
2022-01-27 11:02:45 +01:00
import 'package:nc_photos/bloc/bloc_util.dart' as bloc_util;
import 'package:nc_photos/bloc/progress.dart';
import 'package:nc_photos/bloc/scan_account_dir.dart';
2022-06-06 19:37:46 +02:00
import 'package:nc_photos/compute_queue.dart';
2023-07-03 19:23:42 +02:00
import 'package:nc_photos/controller/account_controller.dart';
2022-01-22 13:02:24 +01:00
import 'package:nc_photos/di_container.dart';
2021-09-28 22:56:44 +02:00
import 'package:nc_photos/download_handler.dart';
import 'package:nc_photos/entity/collection.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
2023-07-17 09:35:45 +02:00
import 'package:nc_photos/entity/pref.dart';
2023-02-20 15:21:35 +01:00
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/event/native_event.dart';
2021-04-10 06:28:12 +02:00
import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/k.dart' as k;
2022-06-06 19:37:46 +02:00
import 'package:nc_photos/language_util.dart' as language_util;
2021-04-11 11:57:08 +02:00
import 'package:nc_photos/metadata_task_manager.dart';
2022-06-06 19:37:46 +02:00
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/platform/k.dart' as platform_k;
import 'package:nc_photos/primitive.dart';
import 'package:nc_photos/service.dart' as service;
import 'package:nc_photos/share_handler.dart';
2021-04-10 06:28:12 +02:00
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart';
2022-06-12 07:45:51 +02:00
import 'package:nc_photos/throttler.dart';
2022-06-06 19:37:46 +02:00
import 'package:nc_photos/widget/builder/photo_list_item_builder.dart';
import 'package:nc_photos/widget/collection_browser.dart';
import 'package:nc_photos/widget/handler/add_selection_to_collection_handler.dart';
2022-01-22 13:02:24 +01:00
import 'package:nc_photos/widget/handler/archive_selection_handler.dart';
2022-07-11 09:06:23 +02:00
import 'package:nc_photos/widget/handler/double_tap_exit_handler.dart';
import 'package:nc_photos/widget/handler/remove_selection_handler.dart';
2021-04-10 06:28:12 +02:00
import 'package:nc_photos/widget/home_app_bar.dart';
import 'package:nc_photos/widget/page_visibility_mixin.dart';
2021-05-06 13:29:20 +02:00
import 'package:nc_photos/widget/photo_list_item.dart';
2021-12-19 12:30:56 +01:00
import 'package:nc_photos/widget/photo_list_util.dart' as photo_list_util;
2021-04-23 09:55:53 +02:00
import 'package:nc_photos/widget/selectable_item_stream_list_mixin.dart';
2021-07-25 16:27:19 +02:00
import 'package:nc_photos/widget/selection_app_bar.dart';
import 'package:nc_photos/widget/settings.dart';
2021-04-10 06:28:12 +02:00
import 'package:nc_photos/widget/viewer.dart';
2021-07-25 10:52:24 +02:00
import 'package:nc_photos/widget/zoom_menu_button.dart';
2022-12-16 16:01:04 +01:00
import 'package:np_codegen/np_codegen.dart';
2022-06-12 07:45:51 +02:00
import 'package:visibility_detector/visibility_detector.dart';
2021-04-10 06:28:12 +02:00
2022-12-16 16:01:04 +01:00
part 'home_photos.g.dart';
class HomePhotosBackToTopEvent {
const HomePhotosBackToTopEvent();
}
2021-04-10 06:28:12 +02:00
class HomePhotos extends StatefulWidget {
2021-09-15 08:58:06 +02:00
const HomePhotos({
2021-07-23 22:05:57 +02:00
Key? key,
required this.account,
2021-04-10 06:28:12 +02:00
}) : super(key: key);
@override
createState() => _HomePhotosState();
final Account account;
}
2022-12-16 16:01:04 +01:00
@npLog
2021-04-23 09:55:53 +02:00
class _HomePhotosState extends State<HomePhotos>
with
SelectableItemStreamListMixin<HomePhotos>,
RouteAware,
PageVisibilityMixin,
TickerProviderStateMixin {
2021-04-10 06:28:12 +02:00
@override
initState() {
super.initState();
2021-10-27 22:40:54 +02:00
_thumbZoomLevel = Pref().getHomePhotosZoomLevelOr(0);
_initBloc();
_web?.onInitState();
_prefUpdatedListener.begin();
_imageProcessorUploadSuccessListener?.begin();
_onBackToTopListener.begin();
}
@override
dispose() {
_onBackToTopListener.end();
_prefUpdatedListener.end();
_imageProcessorUploadSuccessListener?.end();
_web?.onDispose();
super.dispose();
2021-04-10 06:28:12 +02:00
}
@override
build(BuildContext context) {
return BlocListener<ScanAccountDirBloc, ScanAccountDirBlocState>(
2021-04-10 06:28:12 +02:00
bloc: _bloc,
listener: (context, state) => _onStateChange(context, state),
child: _buildContent(context),
2021-04-10 06:28:12 +02:00
);
}
2022-06-06 19:37:46 +02:00
@override
onItemTap(SelectableItem item, int index) {
item.as<PhotoListFileItem>()?.run((fileItem) {
Navigator.pushNamed(
context,
Viewer.routeName,
arguments:
ViewerArguments(widget.account, _backingFiles, fileItem.fileIndex),
);
});
}
2022-06-12 07:45:51 +02:00
@override
onVisibilityChanged(VisibilityInfo info, int index, SelectableItem item) {
if (info.visibleFraction >= 0.2) {
_visibleItems.add(_VisibleItem(index, item));
} else {
_visibleItems.remove(_VisibleItem(index, item));
}
_visibilityThrottler.trigger(
maxResponceTime: const Duration(milliseconds: 500));
}
2022-07-11 09:06:23 +02:00
@override
onBackButtonPressed() async {
if (isSelectionMode) {
return super.onBackButtonPressed();
} else {
return DoubleTapExitHandler()();
}
}
2021-04-10 06:28:12 +02:00
void _initBloc() {
if (_bloc.state is ScanAccountDirBlocInit) {
2021-04-10 06:28:12 +02:00
_log.info("[_initBloc] Initialize bloc");
2021-04-11 11:52:39 +02:00
_reqQuery();
2021-04-10 06:28:12 +02:00
} else {
// process the current state
2022-06-20 13:49:58 +02:00
WidgetsBinding.instance.addPostFrameCallback((_) {
2022-12-11 06:01:13 +01:00
if (mounted) {
setState(() {
_onStateChange(context, _bloc.state);
});
}
2021-07-03 13:32:23 +02:00
});
2021-04-10 06:28:12 +02:00
}
}
Widget _buildContent(BuildContext context) {
2021-05-19 21:11:06 +02:00
return LayoutBuilder(builder: (context, constraints) {
2022-04-09 12:43:50 +02:00
final scrollExtent = _getScrollViewExtent(context, constraints);
2021-05-19 21:11:06 +02:00
return Stack(
children: [
buildItemStreamListOuter(
context,
2022-11-12 10:55:33 +01:00
child: DraggableScrollbar.semicircle(
controller: _scrollController,
overrideMaxScrollExtent: scrollExtent,
// status bar + app bar
topOffset: _calcAppBarExtent(context),
bottomOffset: _calcBottomAppBarExtent(context),
labelTextBuilder: (_) => _buildScrollLabel(context),
labelPadding: const EdgeInsets.symmetric(horizontal: 40),
backgroundColor: Theme.of(context)
.elevate(Theme.of(context).colorScheme.inverseSurface, 3),
2022-11-12 10:55:33 +01:00
enabled: _isScrollbarVisible,
heightScrollThumb: 60,
child: ScrollConfiguration(
behavior:
ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: RefreshIndicator(
onRefresh: () async {
_onRefreshSelected();
await _waitRefresh();
},
child: CustomScrollView(
controller: _scrollController,
slivers: [
_buildAppBar(context),
_web?.buildContent(context),
if (AccountPref.of(widget.account)
.isEnableMemoryAlbumOr(true) &&
_smartCollections.isNotEmpty)
2022-11-12 10:55:33 +01:00
_buildSmartAlbumList(context),
BlocBuilder<ScanAccountDirBloc, ScanAccountDirBlocState>(
bloc: _bloc,
builder: (context, state) {
if (_isInitialSync(state)) {
return _InitialLoadingProgress(
progressBloc: _queryProgressBloc,
);
} else {
return buildItemStreamList(
maxCrossAxisExtent: _thumbSize.toDouble(),
onMaxExtentChanged: (value) {
setState(() {
_itemListMaxExtent = value;
});
},
isEnableVisibilityCallback: true,
);
}
2022-11-12 10:55:33 +01:00
},
),
SliverToBoxAdapter(
child: SizedBox(
height: _calcBottomAppBarExtent(context),
2022-04-09 12:45:17 +02:00
),
2022-11-12 10:55:33 +01:00
),
].whereNotNull().toList(),
),
2021-04-10 06:28:12 +02:00
),
2021-05-19 21:11:06 +02:00
),
2021-04-23 09:55:53 +02:00
),
2021-04-10 06:28:12 +02:00
),
2022-04-09 12:45:17 +02:00
Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
width: double.infinity,
height: _calcBottomAppBarExtent(context),
child: ClipRect(
child: BackdropFilter(
2022-11-19 04:32:38 +01:00
filter: Theme.of(context).appBarBlurFilter,
child: const ColoredBox(
color: Colors.transparent,
2022-04-09 12:45:17 +02:00
),
),
),
2021-05-19 21:11:06 +02:00
),
2022-04-09 12:45:17 +02:00
),
2021-05-19 21:11:06 +02:00
],
);
});
2021-04-10 06:28:12 +02:00
}
Widget _buildAppBar(BuildContext context) {
2021-04-23 09:55:53 +02:00
if (isSelectionMode) {
2021-04-10 06:28:12 +02:00
return _buildSelectionAppBar(context);
} else {
return _buildNormalAppBar(context);
}
}
Widget _buildSelectionAppBar(BuildContext conetxt) {
2021-07-25 16:27:19 +02:00
return SelectionAppBar(
count: selectedListItems.length,
onClosePressed: () {
setState(() {
clearSelectedItems();
});
},
actions: [
2021-10-09 10:42:18 +02:00
IconButton(
icon: const Icon(Icons.share),
tooltip: L10n.global().shareTooltip,
onPressed: () {
_onSelectionSharePressed(context);
},
),
2021-07-25 16:27:19 +02:00
IconButton(
icon: const Icon(Icons.add),
2023-04-28 19:50:54 +02:00
tooltip: L10n.global().addItemToCollectionTooltip,
2021-07-25 16:27:19 +02:00
onPressed: () {
2021-09-29 16:55:49 +02:00
_onSelectionAddToAlbumPressed(context);
2021-07-25 16:27:19 +02:00
},
),
2021-09-08 11:39:24 +02:00
PopupMenuButton<_SelectionMenuOption>(
2021-07-25 16:27:19 +02:00
tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
itemBuilder: (context) => [
2021-09-28 22:56:44 +02:00
PopupMenuItem(
value: _SelectionMenuOption.download,
child: Text(L10n.global().downloadTooltip),
),
2021-07-25 16:27:19 +02:00
PopupMenuItem(
2021-09-08 11:39:24 +02:00
value: _SelectionMenuOption.archive,
child: Text(L10n.global().archiveTooltip),
2021-07-25 16:27:19 +02:00
),
PopupMenuItem(
2021-09-08 11:39:24 +02:00
value: _SelectionMenuOption.delete,
child: Text(L10n.global().deleteTooltip),
2021-07-25 16:27:19 +02:00
),
],
onSelected: (option) {
2021-09-29 16:55:49 +02:00
_onSelectionMenuSelected(context, option);
2021-07-25 16:27:19 +02:00
},
),
],
2021-04-10 06:28:12 +02:00
);
}
Widget _buildNormalAppBar(BuildContext context) {
return BlocBuilder<ScanAccountDirBloc, ScanAccountDirBlocState>(
bloc: _bloc,
buildWhen: (previous, current) {
if (previous is ScanAccountDirBlocLoading &&
current is ScanAccountDirBlocLoading) {
// both loading, check if initial flag changed
return previous.isInitialLoad != current.isInitialLoad;
} else {
// check if any one is loading == state changed from/to loading
return previous is ScanAccountDirBlocLoading ||
current is ScanAccountDirBlocLoading;
}
},
builder: (context, state) {
return HomeSliverAppBar(
account: widget.account,
isShowProgressIcon: !_isInitialSync(state) &&
(state is ScanAccountDirBlocLoading ||
_buildItemQueue.isProcessing) &&
!_isRefreshIndicatorActive,
actions: [
ZoomMenuButton(
initialZoom: _thumbZoomLevel,
minZoom: -1,
maxZoom: 2,
onZoomChanged: (value) {
_setThumbZoomLevel(value.round());
Pref().setHomePhotosZoomLevel(_thumbZoomLevel);
},
),
],
);
2021-04-11 11:57:08 +02:00
},
2021-04-10 06:28:12 +02:00
);
}
2022-01-15 11:35:15 +01:00
Widget _buildSmartAlbumList(BuildContext context) {
return SliverToBoxAdapter(
child: _SmartCollectionList(
2022-12-17 04:55:54 +01:00
account: widget.account,
collections: _smartCollections,
2022-01-15 11:35:15 +01:00
),
);
}
2022-06-12 07:45:51 +02:00
Widget _buildScrollLabel(BuildContext context) {
2022-12-17 04:55:54 +01:00
final firstVisibleItem = _visibleItems
2022-06-12 07:45:51 +02:00
.sorted()
2022-12-17 04:55:54 +01:00
.firstWhereOrNull((e) => e.item is PhotoListFileItem);
final date =
firstVisibleItem?.item.as<PhotoListFileItem>()?.file.fdDateTime;
2022-06-12 07:45:51 +02:00
if (date != null) {
final text = DateFormat(DateFormat.YEAR_ABBR_MONTH,
Localizations.localeOf(context).languageCode)
.format(date.toLocal());
2022-12-17 04:55:54 +01:00
return _ScrollLabel(
child: Text(text),
2022-06-12 07:45:51 +02:00
);
} else {
return const SizedBox();
}
}
void _onStateChange(BuildContext context, ScanAccountDirBlocState state) {
if (state is ScanAccountDirBlocInit) {
2021-04-23 09:55:53 +02:00
itemStreamListItems = [];
} else if (state is ScanAccountDirBlocSuccess) {
_transformItems(state.files, isPostSuccess: true);
} else if (state is ScanAccountDirBlocLoading) {
2022-12-17 05:24:07 +01:00
if (state.files.length > ScanAccountDirBloc.scanMiniCount) {
_isScrollbarVisible = true;
}
_transformItems(state.files);
} else if (state is ScanAccountDirBlocFailure) {
_isScrollbarVisible = true;
_transformItems(state.files);
if (isPageVisible()) {
SnackBarManager().showSnackBar(SnackBar(
content: Text(exception_util.toUserString(state.exception)),
duration: k.snackBarDurationNormal,
));
}
} else if (state is ScanAccountDirBlocInconsistent) {
2021-04-11 11:52:39 +02:00
_reqQuery();
2021-04-10 06:28:12 +02:00
}
}
2021-09-29 16:55:49 +02:00
void _onRefreshSelected() {
_hasFiredMetadataTask.value = false;
_reqRefresh();
}
void _onSelectionMenuSelected(
BuildContext context, _SelectionMenuOption option) {
switch (option) {
case _SelectionMenuOption.archive:
_onSelectionArchivePressed(context);
break;
case _SelectionMenuOption.delete:
_onSelectionDeletePressed(context);
break;
case _SelectionMenuOption.download:
_onSelectionDownloadPressed();
break;
default:
_log.shout("[_onSelectionMenuSelected] Unknown option: $option");
break;
}
}
void _onSelectionSharePressed(BuildContext context) {
final c = KiwiContainer().resolve<DiContainer>();
final selected = selectedListItems
2022-06-06 19:37:46 +02:00
.whereType<PhotoListFileItem>()
.map((e) => e.file)
.toList();
2021-10-08 20:39:53 +02:00
ShareHandler(
c,
2021-10-08 20:39:53 +02:00
context: context,
clearSelection: () {
setState(() {
clearSelectedItems();
});
},
).shareFiles(widget.account, selected);
}
Future<void> _onSelectionAddToAlbumPressed(BuildContext context) {
return const AddSelectionToCollectionHandler()(
context: context,
selection: selectedListItems
2022-06-06 19:37:46 +02:00
.whereType<PhotoListFileItem>()
.map((e) => e.file)
.toList(),
clearSelection: () {
if (mounted) {
setState(() {
clearSelectedItems();
});
}
},
);
2021-04-10 06:28:12 +02:00
}
2021-09-29 16:55:49 +02:00
void _onSelectionDownloadPressed() {
final c = KiwiContainer().resolve<DiContainer>();
2021-09-28 22:56:44 +02:00
final selected = selectedListItems
2022-06-06 19:37:46 +02:00
.whereType<PhotoListFileItem>()
2021-09-28 22:56:44 +02:00
.map((e) => e.file)
.toList();
DownloadHandler(c).downloadFiles(widget.account, selected);
2021-09-28 22:56:44 +02:00
setState(() {
clearSelectedItems();
});
}
2021-09-29 16:55:49 +02:00
Future<void> _onSelectionArchivePressed(BuildContext context) async {
final c = KiwiContainer().resolve<DiContainer>();
2021-09-08 11:39:24 +02:00
final selectedFiles = selectedListItems
2022-06-06 19:37:46 +02:00
.whereType<PhotoListFileItem>()
2021-09-08 11:39:24 +02:00
.map((e) => e.file)
2021-04-10 06:28:12 +02:00
.toList();
2021-09-08 11:39:24 +02:00
setState(() {
clearSelectedItems();
});
await ArchiveSelectionHandler(c)(
2022-01-22 13:02:24 +01:00
account: widget.account,
selection: selectedFiles,
2022-01-22 13:02:24 +01:00
);
2021-04-10 06:28:12 +02:00
}
2021-09-29 16:55:49 +02:00
Future<void> _onSelectionDeletePressed(BuildContext context) async {
final c = KiwiContainer().resolve<DiContainer>();
2021-04-23 09:55:53 +02:00
final selectedFiles = selectedListItems
2022-06-06 19:37:46 +02:00
.whereType<PhotoListFileItem>()
2021-04-23 09:55:53 +02:00
.map((e) => e.file)
.toList();
2021-04-10 06:28:12 +02:00
setState(() {
2021-04-23 09:55:53 +02:00
clearSelectedItems();
2021-04-10 06:28:12 +02:00
});
await RemoveSelectionHandler(c)(
account: widget.account,
selection: selectedFiles,
isMoveToTrash: true,
);
2021-04-10 06:28:12 +02:00
}
void _onPrefUpdated(PrefUpdatedEvent ev) {
if (ev.key == PrefKey.enableExif) {
if (ev.value == true) {
_tryStartMetadataTask(ignoreFired: true);
} else {
_stopMetadataTask();
}
} else if (ev.key == PrefKey.isPhotosTabSortByName) {
if (_bloc.state is! ScanAccountDirBlocInit) {
_log.info("[_onPrefUpdated] Update view after changing sort option");
2022-06-06 19:37:46 +02:00
_transformItems(_bloc.state.files);
}
2022-09-05 06:49:26 +02:00
} else if (ev.key == PrefKey.memoriesRange) {
if (_bloc.state is! ScanAccountDirBlocInit) {
_log.info("[_onPrefUpdated] Update view after changing memories");
_transformItems(_bloc.state.files);
}
2021-09-12 19:30:09 +02:00
}
}
void _onImageProcessorUploadSuccessEvent(
ImageProcessorUploadSuccessEvent ev) {
_log.info(
"[_onImageProcessorUploadSuccessEvent] Scheduling metadata task after next refresh");
_hasFiredMetadataTask.value = false;
}
void _onBackToTop(HomePhotosBackToTopEvent ev) {
_scrollController.jumpTo(0);
}
Future<void> _tryStartMetadataTask({
bool ignoreFired = false,
}) async {
if (_bloc.state is ScanAccountDirBlocSuccess &&
2021-10-27 22:40:54 +02:00
Pref().isEnableExifOr() &&
(!_hasFiredMetadataTask.value || ignoreFired)) {
try {
final c = KiwiContainer().resolve<DiContainer>();
2022-10-18 18:12:30 +02:00
final missingMetadataCount = await c.sqliteDb.use((db) async {
return await db.countMissingMetadataByFileIds(
appAccount: widget.account,
fileIds: _backingFiles.map((e) => e.fdId).toList(),
);
});
_log.info(
"[_tryStartMetadataTask] Missing count: $missingMetadataCount");
if (missingMetadataCount > 0) {
if (_web != null) {
_web!.startMetadataTask(missingMetadataCount);
} else {
unawaited(service.startService());
}
}
_hasFiredMetadataTask.value = true;
} catch (e, stackTrace) {
_log.shout("[_tryStartMetadataTask] Failed starting metadata task", e,
stackTrace);
}
}
}
void _stopMetadataTask() {
if (_web == null) {
service.stopService();
}
}
2021-04-10 06:28:12 +02:00
/// Transform a File list to grid items
void _transformItems(
List<FileDescriptor> files, {
bool isSorted = false,
bool isPostSuccess = false,
}) {
2022-06-06 19:37:46 +02:00
_log.info("[_transformItems] Queue ${files.length} items");
2022-09-05 06:49:26 +02:00
final c = KiwiContainer().resolve<DiContainer>();
2022-06-06 19:37:46 +02:00
final PhotoListItemSorter? sorter;
final PhotoListItemGrouper? grouper;
if (Pref().isPhotosTabSortByNameOr()) {
sorter = isSorted ? null : photoListFilenameSorter;
grouper = null;
} else {
2022-06-06 19:37:46 +02:00
sorter = isSorted ? null : photoListFileDateTimeSorter;
grouper = PhotoListFileDateGrouper(isMonthOnly: _thumbZoomLevel < 0);
}
2022-06-06 19:37:46 +02:00
_buildItemQueue.addJob(
PhotoListItemBuilderArguments(
widget.account,
files,
sorter: sorter,
grouper: grouper,
2022-09-05 06:49:26 +02:00
smartAlbumConfig:
PhotoListItemSmartAlbumConfig(c.pref.getMemoriesRangeOr()),
2022-06-06 19:37:46 +02:00
shouldShowFavoriteBadge: true,
locale: language_util.getSelectedLocale() ??
PlatformDispatcher.instance.locale,
),
buildPhotoListItem,
(result) {
if (mounted) {
setState(() {
_backingFiles = result.backingFiles;
itemStreamListItems = result.listItems;
_smartCollections = result.smartCollections;
if (isPostSuccess) {
_isScrollbarVisible = true;
context.read<AccountController>().syncController.requestSync(
widget.account, _accountPrefController.personProvider.value);
_tryStartMetadataTask();
}
2022-06-06 19:37:46 +02:00
});
}
2022-06-06 19:37:46 +02:00
},
2021-10-04 15:53:03 +02:00
);
}
2021-04-11 11:52:39 +02:00
void _reqQuery() {
_bloc.add(ScanAccountDirBlocQuery(
progressBloc: _queryProgressBloc,
));
2021-04-10 06:28:12 +02:00
}
void _reqRefresh() {
_bloc.add(const ScanAccountDirBlocRefresh());
}
2021-11-18 14:04:42 +01:00
Future<void> _waitRefresh() async {
setState(() {
_isRefreshIndicatorActive = true;
});
try {
while (true) {
await Future.delayed(const Duration(seconds: 1));
if (_bloc.state is! ScanAccountDirBlocLoading) {
return;
}
2021-11-18 14:04:42 +01:00
}
} finally {
// To prevent the app bar icon appearing for a very short while
unawaited(Future.delayed(const Duration(seconds: 2)).then((_) {
if (mounted) {
setState(() {
_isRefreshIndicatorActive = false;
});
}
}));
2021-11-18 14:04:42 +01:00
}
}
2021-04-28 14:55:26 +02:00
void _setThumbZoomLevel(int level) {
final prevLevel = _thumbZoomLevel;
if ((prevLevel >= 0) != (level >= 0)) {
2022-06-06 19:37:46 +02:00
_thumbZoomLevel = level;
_transformItems(_backingFiles, isSorted: true);
} else {
setState(() {
_thumbZoomLevel = level;
});
2021-04-28 14:55:26 +02:00
}
}
2021-05-19 21:11:06 +02:00
/// Return the estimated scroll extent of the custom scroll view, or null
2022-04-09 12:43:50 +02:00
double? _getScrollViewExtent(
BuildContext context, BoxConstraints constraints) {
if (_itemListMaxExtent != null && constraints.hasBoundedHeight) {
final appBarExtent = _calcAppBarExtent(context);
2022-04-09 12:45:17 +02:00
final bottomAppBarExtent = _calcBottomAppBarExtent(context);
final metadataTaskHeaderExtent = _web?.getHeaderHeight() ?? 0;
2022-01-15 11:35:15 +01:00
final smartAlbumListHeight =
2022-01-15 15:21:22 +01:00
AccountPref.of(widget.account).isEnableMemoryAlbumOr(true) &&
_smartCollections.isNotEmpty
2022-01-15 15:21:22 +01:00
? _SmartAlbumItem.height
: 0;
2022-01-15 11:35:15 +01:00
// scroll extent = list height - widget viewport height
2022-04-09 12:45:17 +02:00
// + sliver app bar height + bottom app bar height
// + metadata task header height + smart album list height
final scrollExtent = _itemListMaxExtent! -
constraints.maxHeight +
2022-04-09 12:43:50 +02:00
appBarExtent +
2022-04-09 12:45:17 +02:00
bottomAppBarExtent +
metadataTaskHeaderExtent +
2022-01-15 16:36:27 +01:00
smartAlbumListHeight;
2021-05-19 21:11:06 +02:00
_log.info(
2022-04-09 12:45:17 +02:00
"[_getScrollViewExtent] $_itemListMaxExtent - ${constraints.maxHeight} + $appBarExtent + $bottomAppBarExtent + $metadataTaskHeaderExtent + $smartAlbumListHeight = $scrollExtent");
2021-05-19 21:11:06 +02:00
return scrollExtent;
} else {
return null;
}
}
bool _isInitialSync(ScanAccountDirBlocState state) =>
state is ScanAccountDirBlocLoading &&
state.files.isEmpty &&
state.isInitialLoad;
2022-04-09 12:43:50 +02:00
double _calcAppBarExtent(BuildContext context) =>
MediaQuery.of(context).padding.top + kToolbarHeight;
2022-11-12 10:55:33 +01:00
double _calcBottomAppBarExtent(BuildContext context) =>
NavigationBarTheme.of(context).height!;
2022-04-09 12:45:17 +02:00
Primitive<bool> get _hasFiredMetadataTask {
2022-01-27 11:02:45 +01:00
final name = bloc_util.getInstNameForRootAwareAccount(
"HomePhotosState.hasFiredMetadataTask", widget.account);
try {
2022-01-27 11:02:45 +01:00
_log.fine("[_hasFiredMetadataTask] Resolving for '$name'");
return KiwiContainer().resolve<Primitive<bool>>(name);
} catch (_) {
_log.info(
2022-01-27 11:02:45 +01:00
"[_hasFiredMetadataTask] New instance for account: ${widget.account}");
final obj = Primitive(false);
2022-01-31 05:24:48 +01:00
KiwiContainer().registerInstance<Primitive<bool>>(obj, name: name);
return obj;
}
}
late final _bloc = ScanAccountDirBloc.of(widget.account);
late final _queryProgressBloc = ProgressBloc();
2023-07-03 19:23:42 +02:00
late final _accountPrefController =
context.read<AccountController>().accountPrefController;
2021-04-10 06:28:12 +02:00
var _backingFiles = <FileDescriptor>[];
var _smartCollections = <Collection>[];
2021-04-10 06:28:12 +02:00
2022-06-06 19:37:46 +02:00
final _buildItemQueue =
ComputeQueue<PhotoListItemBuilderArguments, PhotoListItemBuilderResult>();
2021-04-10 06:28:12 +02:00
var _thumbZoomLevel = 0;
2021-12-19 12:44:41 +01:00
int get _thumbSize => photo_list_util.getThumbSize(_thumbZoomLevel);
2021-04-10 06:28:12 +02:00
2021-05-19 21:11:06 +02:00
final ScrollController _scrollController = ScrollController();
2021-07-23 22:05:57 +02:00
double? _itemListMaxExtent;
2021-05-19 21:11:06 +02:00
2022-06-12 07:45:51 +02:00
final _visibleItems = HashSet<_VisibleItem>();
late final _visibilityThrottler = Throttler(onTriggered: (_) {
// label text is always 1 frame behind, so we need to update the text for
// the last frame
if (mounted) {
_log.fine("[_visibilityThrottler] Update screen");
setState(() {});
}
});
late final _prefUpdatedListener =
AppEventListener<PrefUpdatedEvent>(_onPrefUpdated);
late final _imageProcessorUploadSuccessListener = platform_k.isWeb
? null
: NativeEventListener<ImageProcessorUploadSuccessEvent>(
_onImageProcessorUploadSuccessEvent);
late final _onBackToTopListener =
AppEventListener<HomePhotosBackToTopEvent>(_onBackToTop);
late final _Web? _web = platform_k.isWeb ? _Web(this) : null;
var _isScrollbarVisible = false;
var _isRefreshIndicatorActive = false;
}
class _Web {
_Web(this.state);
void onInitState() {
_metadataTaskStateChangedListener.begin();
_filePropertyUpdatedListener.begin();
}
void onDispose() {
_metadataTaskIconController.stop();
_metadataTaskStateChangedListener.end();
_filePropertyUpdatedListener.end();
}
Widget? buildContent(BuildContext context) {
if (_metadataTaskState != MetadataTaskState.idle) {
return _buildMetadataTaskHeader(context);
} else {
return null;
}
}
void startMetadataTask(int missingMetadataCount) {
final c = KiwiContainer().resolve<DiContainer>();
MetadataTaskManager().addTask(MetadataTask(
c, state.widget.account, AccountPref.of(state.widget.account)));
_metadataTaskProcessTotalCount = missingMetadataCount;
}
double getHeaderHeight() {
return _metadataTaskState == MetadataTaskState.idle
? 0
: _metadataTaskHeaderHeight;
}
Widget _buildMetadataTaskHeader(BuildContext context) {
return SliverPersistentHeader(
pinned: true,
floating: false,
delegate: _MetadataTaskHeaderDelegate(
extent: _metadataTaskHeaderHeight,
builder: (context) => Container(
height: double.infinity,
color: Theme.of(context).scaffoldBackgroundColor,
alignment: AlignmentDirectional.centerStart,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
children: [
if (_metadataTaskState == MetadataTaskState.prcoessing)
Row(
mainAxisSize: MainAxisSize.min,
children: [
_MetadataTaskLoadingIcon(
controller: _metadataTaskIconController,
),
const SizedBox(width: 4),
Text(
L10n.global().metadataTaskProcessingNotification +
_getMetadataTaskProgressString(),
style: const TextStyle(fontSize: 12),
),
],
)
else if (_metadataTaskState == MetadataTaskState.waitingForWifi)
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.sync_problem,
size: 16,
),
const SizedBox(width: 4),
Text(
L10n.global().metadataTaskPauseNoWiFiNotification,
style: const TextStyle(fontSize: 12),
),
],
2022-04-09 05:23:37 +02:00
)
else if (_metadataTaskState == MetadataTaskState.lowBattery)
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.sync_problem,
size: 16,
),
const SizedBox(width: 4),
Text(
L10n.global().metadataTaskPauseLowBatteryNotification,
style: const TextStyle(fontSize: 12),
),
],
),
Expanded(
child: Container(),
),
Material(
type: MaterialType.transparency,
child: InkWell(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
child: Text(
L10n.global().configButtonLabel,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontSize: 12,
),
),
),
onTap: () {
Navigator.of(context).pushNamed(Settings.routeName,
arguments: SettingsArguments(state.widget.account));
},
),
),
],
),
),
),
),
);
}
void _onMetadataTaskStateChanged(MetadataTaskStateChangedEvent ev) {
if (ev.state == MetadataTaskState.idle) {
_metadataTaskProcessCount = 0;
}
if (ev.state != _metadataTaskState) {
// ignore: invalid_use_of_protected_member
state.setState(() {
_metadataTaskState = ev.state;
});
}
}
void _onFilePropertyUpdated(FilePropertyUpdatedEvent ev) {
if (!ev.hasAnyProperties([
FilePropertyUpdatedEvent.propMetadata,
FilePropertyUpdatedEvent.propImageLocation,
])) {
return;
}
// ignore: invalid_use_of_protected_member
state.setState(() {
++_metadataTaskProcessCount;
});
}
String _getMetadataTaskProgressString() {
if (_metadataTaskProcessTotalCount == 0) {
return "";
}
final clippedCount =
math.min(_metadataTaskProcessCount, _metadataTaskProcessTotalCount - 1);
return " ($clippedCount/$_metadataTaskProcessTotalCount)";
}
final _HomePhotosState state;
late final _metadataTaskStateChangedListener =
AppEventListener<MetadataTaskStateChangedEvent>(
_onMetadataTaskStateChanged);
var _metadataTaskState = MetadataTaskManager().state;
2021-09-12 19:30:09 +02:00
late final _filePropertyUpdatedListener =
AppEventListener<FilePropertyUpdatedEvent>(_onFilePropertyUpdated);
var _metadataTaskProcessCount = 0;
var _metadataTaskProcessTotalCount = 0;
late final _metadataTaskIconController = AnimationController(
upperBound: 2 * math.pi,
duration: const Duration(seconds: 10),
vsync: state,
)..repeat();
static const _metadataTaskHeaderHeight = 32.0;
2021-04-10 06:28:12 +02:00
}
class _MetadataTaskHeaderDelegate extends SliverPersistentHeaderDelegate {
const _MetadataTaskHeaderDelegate({
required this.extent,
required this.builder,
});
@override
build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return builder(context);
}
@override
get maxExtent => extent;
@override
get minExtent => maxExtent;
@override
shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => true;
final double extent;
final Widget Function(BuildContext context) builder;
}
class _MetadataTaskLoadingIcon extends AnimatedWidget {
const _MetadataTaskLoadingIcon({
Key? key,
required AnimationController controller,
}) : super(key: key, listenable: controller);
@override
build(BuildContext context) {
return Transform.rotate(
angle: -_progress.value,
child: const Icon(
Icons.sync,
size: 16,
),
);
}
Animation<double> get _progress => listenable as Animation<double>;
}
class _SmartCollectionList extends StatelessWidget {
const _SmartCollectionList({
2022-12-17 04:55:54 +01:00
required this.account,
required this.collections,
2022-12-17 04:55:54 +01:00
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: _SmartAlbumItem.height,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8),
itemCount: collections.length,
2022-12-17 04:55:54 +01:00
itemBuilder: (context, index) {
final c = collections[index];
2022-12-17 04:55:54 +01:00
return _SmartAlbumItem(
account: account,
previewUrl: c.getCoverUrl(
k.photoThumbSize,
k.photoThumbSize,
isKeepAspectRatio: true,
),
label: c.name,
2022-12-17 04:55:54 +01:00
onTap: () {
Navigator.of(context).pushNamed(CollectionBrowser.routeName,
arguments: CollectionBrowserArguments(c));
2022-12-17 04:55:54 +01:00
},
);
},
separatorBuilder: (context, index) => const SizedBox(width: 8),
),
);
}
final Account account;
final List<Collection> collections;
2022-12-17 04:55:54 +01:00
}
2022-01-15 11:35:15 +01:00
class _SmartAlbumItem extends StatelessWidget {
static const width = 96.0;
static const height = width * 1.15;
const _SmartAlbumItem({
Key? key,
required this.account,
required this.previewUrl,
required this.label,
this.onTap,
}) : super(key: key);
@override
build(BuildContext context) {
return Align(
alignment: AlignmentDirectional.topStart,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: SizedBox(
width: width,
height: height,
child: Stack(
fit: StackFit.expand,
children: [
PhotoListImage(
account: account,
previewUrl: previewUrl,
padding: const EdgeInsets.all(0),
),
Positioned.fill(
child: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.center,
end: Alignment.bottomCenter,
colors: [Colors.transparent, Colors.black87],
),
),
),
),
Positioned.fill(
child: Align(
alignment: AlignmentDirectional.bottomStart,
child: Padding(
padding: const EdgeInsets.all(4),
child: Text(
label,
2022-11-12 10:55:33 +01:00
style: Theme.of(context).textTheme.labelLarge!.copyWith(
color: Theme.of(context).onDarkSurface,
),
2022-01-15 11:35:15 +01:00
),
),
),
),
if (onTap != null)
Positioned.fill(
child: Material(
type: MaterialType.transparency,
child: InkWell(
onTap: onTap,
),
),
),
],
),
),
),
);
}
final Account account;
final String? previewUrl;
final String label;
final VoidCallback? onTap;
}
2021-09-08 11:39:24 +02:00
enum _SelectionMenuOption {
2021-05-28 21:44:09 +02:00
archive,
delete,
2021-09-28 22:56:44 +02:00
download,
2021-05-28 21:44:09 +02:00
}
2022-06-12 07:45:51 +02:00
class _VisibleItem implements Comparable<_VisibleItem> {
const _VisibleItem(this.index, this.item);
@override
operator ==(Object other) => other is _VisibleItem && other.index == index;
@override
compareTo(_VisibleItem other) => index.compareTo(other.index);
@override
get hashCode => index.hashCode;
final int index;
final SelectableItem item;
}
class _InitialLoadingProgress extends StatelessWidget {
const _InitialLoadingProgress({
required this.progressBloc,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<ProgressBloc, ProgressBlocState>(
bloc: progressBloc,
buildWhen: (previous, current) => previous != current,
builder: (context, state) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 56, 16, 0),
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: Theme.of(context).widthLimitedContentMaxWidth,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
L10n.global().initialSyncMessage,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 8),
LinearProgressIndicator(
value: state.progress == 0 ? null : state.progress,
),
const SizedBox(height: 8),
Text(
state.text ?? "",
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
),
),
);
},
);
}
final ProgressBloc progressBloc;
}
2022-12-17 04:55:54 +01:00
class _ScrollLabel extends StatelessWidget {
const _ScrollLabel({
required this.child,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: DefaultTextStyle(
style: theme.textTheme.titleMedium!
.copyWith(color: theme.colorScheme.onInverseSurface),
child: child,
),
);
}
final Widget child;
}