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

1041 lines
31 KiB
Dart
Raw Normal View History

import 'dart:math' as math;
2022-04-09 12:45:17 +02:00
import 'dart:ui';
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';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api_util.dart' as api_util;
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/scan_account_dir.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';
2022-01-15 11:35:15 +01:00
import 'package:nc_photos/entity/album.dart';
2021-04-10 06:28:12 +02:00
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/event/event.dart';
2022-01-31 05:24:48 +01:00
import 'package:nc_photos/exception.dart';
2021-04-10 06:28:12 +02:00
import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/k.dart' as k;
2021-04-11 11:57:08 +02:00
import 'package:nc_photos/metadata_task_manager.dart';
import 'package:nc_photos/platform/k.dart' as platform_k;
2021-04-10 06:28:12 +02:00
import 'package:nc_photos/pref.dart';
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-01-31 05:24:48 +01:00
import 'package:nc_photos/use_case/sync_favorite.dart';
2022-01-15 11:35:15 +01:00
import 'package:nc_photos/widget/album_browser_util.dart' as album_browser_util;
import 'package:nc_photos/widget/handler/add_selection_to_album_handler.dart';
2022-01-22 13:02:24 +01:00
import 'package:nc_photos/widget/handler/archive_selection_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';
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;
}
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();
}
@override
dispose() {
_prefUpdatedListener.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: BlocBuilder<ScanAccountDirBloc, ScanAccountDirBlocState>(
2021-04-10 06:28:12 +02:00
bloc: _bloc,
2021-04-23 09:55:53 +02:00
builder: (context, state) => _buildContent(context, state),
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
2021-07-23 22:05:57 +02:00
WidgetsBinding.instance!.addPostFrameCallback((_) {
2021-07-03 13:32:23 +02:00
setState(() {
_onStateChange(context, _bloc.state);
});
});
2021-04-10 06:28:12 +02:00
}
}
Widget _buildContent(BuildContext context, ScanAccountDirBlocState state) {
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,
child: Theme(
data: Theme.of(context).copyWith(
colorScheme: Theme.of(context).colorScheme.copyWith(
secondary: AppTheme.getOverscrollIndicatorColor(context),
),
2021-05-19 21:11:06 +02:00
),
child: DraggableScrollbar.semicircle(
controller: _scrollController,
overrideMaxScrollExtent: scrollExtent,
// status bar + app bar
2022-04-09 12:43:50 +02:00
topOffset: _calcAppBarExtent(context),
2022-04-09 12:45:17 +02:00
bottomOffset: _calcBottomAppBarExtent(context),
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context)
.copyWith(scrollbars: false),
2021-10-04 20:41:30 +02:00
child: RefreshIndicator(
backgroundColor: Colors.grey[100],
onRefresh: () async {
_onRefreshSelected();
2021-11-18 14:04:42 +01:00
await _waitRefresh();
2021-10-04 20:41:30 +02:00
},
child: CustomScrollView(
controller: _scrollController,
slivers: [
_buildAppBar(context),
_web?.buildContent(context),
2022-01-15 15:21:22 +01:00
if (AccountPref.of(widget.account)
.isEnableMemoryAlbumOr(true) &&
_smartAlbums.isNotEmpty)
2022-01-15 11:35:15 +01:00
_buildSmartAlbumList(context),
2022-01-15 16:36:27 +01:00
buildItemStreamList(
maxCrossAxisExtent: _thumbSize.toDouble(),
onMaxExtentChanged: (value) {
setState(() {
_itemListMaxExtent = value;
});
},
2021-07-06 07:10:36 +02:00
),
2022-04-09 12:45:17 +02:00
SliverToBoxAdapter(
child: SizedBox(
height: _calcBottomAppBarExtent(context),
),
),
].whereType<Widget>().toList(),
2021-10-04 20:41:30 +02:00
),
),
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: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (state is ScanAccountDirBlocLoading)
const LinearProgressIndicator(),
SizedBox(
width: double.infinity,
height: _calcBottomAppBarExtent(context),
child: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4),
child: const ColoredBox(
color: Colors.transparent,
),
),
),
),
],
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),
tooltip: L10n.global().addToAlbumTooltip,
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) {
2022-04-09 12:43:50 +02:00
return HomeSliverAppBar(
account: widget.account,
actions: [
ZoomMenuButton(
initialZoom: _thumbZoomLevel,
minZoom: -1,
maxZoom: 2,
onZoomChanged: (value) {
setState(() {
_setThumbZoomLevel(value.round());
});
Pref().setHomePhotosZoomLevel(_thumbZoomLevel);
},
),
],
menuActions: [
PopupMenuItem(
value: _menuValueRefresh,
child: Text(L10n.global().refreshMenuLabel),
),
],
onSelectedMenuActions: (option) {
switch (option) {
case _menuValueRefresh:
_onRefreshSelected();
break;
}
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: SizedBox(
height: _SmartAlbumItem.height,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8),
itemCount: _smartAlbums.length,
itemBuilder: (context, index) {
final a = _smartAlbums[index];
final coverFile = a.coverProvider.getCover(a);
return _SmartAlbumItem(
account: widget.account,
previewUrl: coverFile == null
? null
: api_util.getFilePreviewUrl(widget.account, coverFile,
width: k.photoThumbSize, height: k.photoThumbSize),
label: a.name,
onTap: () {
album_browser_util.push(context, widget.account, a);
},
);
},
separatorBuilder: (context, index) => const SizedBox(width: 8),
),
),
);
}
void _onStateChange(BuildContext context, ScanAccountDirBlocState state) {
if (state is ScanAccountDirBlocInit) {
2021-04-23 09:55:53 +02:00
itemStreamListItems = [];
} else if (state is ScanAccountDirBlocSuccess ||
state is ScanAccountDirBlocLoading) {
2021-04-10 06:28:12 +02:00
_transformItems(state.files);
if (state is ScanAccountDirBlocSuccess) {
2022-01-31 05:24:48 +01:00
_syncFavorite();
_tryStartMetadataTask();
}
} else if (state is ScanAccountDirBlocFailure) {
_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-04-23 09:55:53 +02:00
void _onItemTap(int index) {
Navigator.pushNamed(context, Viewer.routeName,
arguments: ViewerArguments(widget.account, _backingFiles, index));
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 selected = selectedListItems
.whereType<_FileListItem>()
.map((e) => e.file)
.toList();
2021-10-08 20:39:53 +02:00
ShareHandler(
context: context,
clearSelection: () {
setState(() {
clearSelectedItems();
});
},
).shareFiles(widget.account, selected);
}
Future<void> _onSelectionAddToAlbumPressed(BuildContext context) {
return AddSelectionToAlbumHandler()(
context: context,
account: widget.account,
selectedFiles: selectedListItems
.whereType<_FileListItem>()
.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() {
2021-09-28 22:56:44 +02:00
final selected = selectedListItems
.whereType<_FileListItem>()
.map((e) => e.file)
.toList();
DownloadHandler().downloadFiles(widget.account, selected);
setState(() {
clearSelectedItems();
});
}
2021-09-29 16:55:49 +02:00
Future<void> _onSelectionArchivePressed(BuildContext context) async {
2021-09-08 11:39:24 +02:00
final selectedFiles = selectedListItems
2021-04-23 09:55:53 +02:00
.whereType<_FileListItem>()
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();
});
2022-01-22 13:02:24 +01:00
await ArchiveSelectionHandler(KiwiContainer().resolve<DiContainer>())(
account: widget.account,
selectedFiles: selectedFiles,
);
2021-04-10 06:28:12 +02:00
}
2021-09-29 16:55:49 +02:00
Future<void> _onSelectionDeletePressed(BuildContext context) async {
2021-04-23 09:55:53 +02:00
final selectedFiles = selectedListItems
.whereType<_FileListItem>()
.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()(
account: widget.account,
selectedFiles: 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();
}
2021-09-12 19:30:09 +02:00
}
}
void _tryStartMetadataTask({
bool ignoreFired = false,
}) {
if (_bloc.state is ScanAccountDirBlocSuccess &&
2021-10-27 22:40:54 +02:00
Pref().isEnableExifOr() &&
(!_hasFiredMetadataTask.value || ignoreFired)) {
final missingMetadataCount = _backingFiles
2021-10-27 19:23:03 +02:00
.where(
(f) => file_util.isSupportedImageFormat(f) && f.metadata == null)
2021-09-12 19:30:09 +02:00
.length;
if (missingMetadataCount > 0) {
if (_web != null) {
_web!.startMetadataTask(missingMetadataCount);
} else {
service.startService();
}
}
_hasFiredMetadataTask.value = true;
}
}
void _stopMetadataTask() {
if (_web == null) {
service.stopService();
}
}
2022-01-31 05:24:48 +01:00
Future<void> _syncFavorite() async {
if (!_hasResyncedFavorites.value) {
final c = KiwiContainer().resolve<DiContainer>();
try {
await SyncFavorite(c)(widget.account);
} catch (e, stackTrace) {
if (e is! ApiException) {
_log.shout(
"[_syncFavorite] Failed while SyncFavorite", e, stackTrace);
}
}
_hasResyncedFavorites.value = true;
}
}
2021-04-10 06:28:12 +02:00
/// Transform a File list to grid items
void _transformItems(List<File> files) {
_backingFiles = files
.where((f) => f.isArchived != true)
2021-04-10 06:28:12 +02:00
.sorted(compareFileDateTimeDescending);
final isMonthOnly = _thumbZoomLevel < 0;
2021-12-19 12:30:56 +01:00
final dateHelper = photo_list_util.DateGroupHelper(
2021-10-04 15:53:03 +02:00
isMonthOnly: isMonthOnly,
);
2022-01-15 11:35:15 +01:00
final today = DateTime.now();
final memoryAlbumHelper = photo_list_util.MemoryAlbumHelper(today);
2021-04-23 09:55:53 +02:00
itemStreamListItems = () sync* {
for (int i = 0; i < _backingFiles.length; ++i) {
final f = _backingFiles[i];
2021-10-04 15:53:03 +02:00
final date = dateHelper.onFile(f);
if (date != null) {
yield _DateListItem(date: date, isMonthOnly: isMonthOnly);
2021-04-23 09:55:53 +02:00
}
2022-01-15 11:35:15 +01:00
memoryAlbumHelper.addFile(f);
2021-04-28 14:55:26 +02:00
2021-05-05 22:03:39 +02:00
final previewUrl = api_util.getFilePreviewUrl(widget.account, f,
2021-09-16 12:25:08 +02:00
width: k.photoThumbSize, height: k.photoThumbSize);
2021-05-06 13:36:20 +02:00
if (file_util.isSupportedImageFormat(f)) {
yield _ImageListItem(
file: f,
account: widget.account,
previewUrl: previewUrl,
onTap: () => _onItemTap(i),
);
} else if (file_util.isSupportedVideoFormat(f)) {
yield _VideoListItem(
file: f,
account: widget.account,
previewUrl: previewUrl,
onTap: () => _onItemTap(i),
);
} else {
_log.shout(
"[_transformItems] Unsupported file format: ${f.contentType}");
}
2021-04-10 06:28:12 +02:00
}
2021-07-07 20:51:34 +02:00
}()
.toList();
2022-01-15 11:35:15 +01:00
_smartAlbums = memoryAlbumHelper
.build((year) => L10n.global().memoryAlbumName(today.year - year));
2021-04-10 06:28:12 +02:00
}
2021-04-11 11:52:39 +02:00
void _reqQuery() {
_bloc.add(const ScanAccountDirBlocQuery());
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 {
while (true) {
await Future.delayed(const Duration(seconds: 1));
if (_bloc.state is! ScanAccountDirBlocLoading) {
return;
}
}
}
2021-04-28 14:55:26 +02:00
void _setThumbZoomLevel(int level) {
final prevLevel = _thumbZoomLevel;
_thumbZoomLevel = level;
if ((prevLevel >= 0) != (level >= 0)) {
_transformItems(_backingFiles);
}
}
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) &&
_smartAlbums.isNotEmpty
? _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;
}
}
2022-04-09 12:43:50 +02:00
double _calcAppBarExtent(BuildContext context) =>
MediaQuery.of(context).padding.top + kToolbarHeight;
2022-04-09 12:45:17 +02:00
double _calcBottomAppBarExtent(BuildContext context) => kToolbarHeight;
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-27 11:02:45 +01:00
KiwiContainer().registerInstance<Primitive<bool>>(obj, name: name);
return obj;
}
}
2022-01-31 05:24:48 +01:00
Primitive<bool> get _hasResyncedFavorites {
final name = bloc_util.getInstNameForRootAwareAccount(
"HomePhotosState._hasResyncedFavorites", widget.account);
try {
_log.fine("[_hasResyncedFavorites] Resolving for '$name'");
return KiwiContainer().resolve<Primitive<bool>>(name);
} catch (_) {
_log.info(
"[_hasResyncedFavorites] New instance for account: ${widget.account}");
final obj = Primitive(false);
KiwiContainer().registerInstance<Primitive<bool>>(obj, name: name);
return obj;
}
}
late final _bloc = ScanAccountDirBloc.of(widget.account);
2021-04-10 06:28:12 +02:00
var _backingFiles = <File>[];
2022-01-15 11:35:15 +01:00
var _smartAlbums = <Album>[];
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
late final _prefUpdatedListener =
AppEventListener<PrefUpdatedEvent>(_onPrefUpdated);
late final _Web? _web = platform_k.isWeb ? _Web(this) : null;
static final _log = Logger("widget.home_photos._HomePhotosState");
static const _menuValueRefresh = 0;
}
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) {
MetadataTaskManager().addTask(MetadataTask(
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])) {
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
}
2021-07-06 15:03:29 +02:00
abstract class _ListItem implements SelectableItem {
_ListItem({
2021-07-23 22:05:57 +02:00
VoidCallback? onTap,
2021-07-06 15:03:29 +02:00
}) : _onTap = onTap;
@override
get onTap => _onTap;
@override
get isSelectable => true;
@override
get staggeredTile => const StaggeredTile.count(1, 1);
2021-07-23 22:05:57 +02:00
final VoidCallback? _onTap;
2021-07-06 15:03:29 +02:00
}
class _DateListItem extends _ListItem {
_DateListItem({
2021-07-23 22:05:57 +02:00
required this.date,
this.isMonthOnly = false,
2021-07-06 15:03:29 +02:00
});
@override
get isSelectable => false;
@override
get staggeredTile => const StaggeredTile.extent(99, 32);
2021-04-10 06:28:12 +02:00
2021-04-23 09:55:53 +02:00
@override
buildWidget(BuildContext context) {
2021-10-04 15:53:03 +02:00
return PhotoListDate(
date: date,
isMonthOnly: isMonthOnly,
2021-04-23 09:55:53 +02:00
);
}
2021-04-10 06:28:12 +02:00
2021-10-04 15:53:03 +02:00
final DateTime date;
final bool isMonthOnly;
2021-04-10 06:28:12 +02:00
}
2021-07-06 15:03:29 +02:00
abstract class _FileListItem extends _ListItem {
2021-04-23 09:55:53 +02:00
_FileListItem({
2021-07-23 22:05:57 +02:00
required this.file,
VoidCallback? onTap,
2021-07-06 15:03:29 +02:00
}) : super(onTap: onTap);
2021-04-10 06:28:12 +02:00
2021-04-23 09:55:53 +02:00
@override
operator ==(Object other) {
return other is _FileListItem && file.path == other.file.path;
}
2021-04-10 06:28:12 +02:00
2021-04-23 09:55:53 +02:00
@override
get hashCode => file.path.hashCode;
2021-04-10 06:28:12 +02:00
final File file;
}
2021-04-23 09:55:53 +02:00
class _ImageListItem extends _FileListItem {
_ImageListItem({
2021-07-23 22:05:57 +02:00
required File file,
required this.account,
required this.previewUrl,
VoidCallback? onTap,
2021-04-23 09:55:53 +02:00
}) : super(file: file, onTap: onTap);
@override
buildWidget(BuildContext context) {
2021-05-06 13:29:20 +02:00
return PhotoListImage(
account: account,
previewUrl: previewUrl,
2021-06-22 07:24:37 +02:00
isGif: file.contentType == "image/gif",
2022-01-25 11:15:17 +01:00
isFavorite: file.isFavorite == true,
2021-04-23 09:55:53 +02:00
);
}
2021-04-10 06:28:12 +02:00
2021-04-23 09:55:53 +02:00
final Account account;
2021-04-10 06:28:12 +02:00
final String previewUrl;
}
2021-05-06 13:36:20 +02:00
class _VideoListItem extends _FileListItem {
_VideoListItem({
2021-07-23 22:05:57 +02:00
required File file,
required this.account,
required this.previewUrl,
VoidCallback? onTap,
2021-05-06 13:36:20 +02:00
}) : super(file: file, onTap: onTap);
@override
buildWidget(BuildContext context) {
return PhotoListVideo(
account: account,
previewUrl: previewUrl,
2022-01-25 11:15:17 +01:00
isFavorite: file.isFavorite == true,
2021-05-06 13:36:20 +02:00
);
}
final Account account;
final String previewUrl;
}
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>;
}
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,
style: TextStyle(color: AppTheme.primaryTextColorDark),
),
),
),
),
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
}