nc-photos/lib/widget/home_photos.dart

884 lines
26 KiB
Dart
Raw Normal View History

import 'dart:math' as math;
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/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:intl/intl.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';
2021-04-10 06:28:12 +02:00
import 'package:nc_photos/bloc/scan_dir.dart';
2021-09-04 14:35:04 +02:00
import 'package:nc_photos/debug_util.dart';
2021-09-28 22:56:44 +02:00
import 'package:nc_photos/download_handler.dart';
2021-04-10 06:28:12 +02:00
import 'package:nc_photos/entity/album.dart';
2021-07-05 09:54:01 +02:00
import 'package:nc_photos/entity/album/item.dart';
2021-06-24 18:26:56 +02:00
import 'package:nc_photos/entity/album/provider.dart';
2021-04-10 06:28:12 +02:00
import 'package:nc_photos/entity/file.dart';
2021-05-24 09:09:25 +02:00
import 'package:nc_photos/entity/file/data_source.dart';
2021-04-10 06:28:12 +02:00
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/event/event.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';
2021-09-08 11:39:24 +02:00
import 'package:nc_photos/notified_action.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/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';
import 'package:nc_photos/use_case/add_to_album.dart';
2021-04-10 06:28:12 +02:00
import 'package:nc_photos/use_case/remove.dart';
2021-05-28 21:44:09 +02:00
import 'package:nc_photos/use_case/update_property.dart';
2021-04-10 06:28:12 +02:00
import 'package:nc_photos/widget/album_picker_dialog.dart';
import 'package:nc_photos/widget/home_app_bar.dart';
2021-05-19 21:11:06 +02:00
import 'package:nc_photos/widget/measure.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-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-07-23 22:05:57 +02:00
_thumbZoomLevel = Pref.inst().getHomePhotosZoomLevelOr(0);
_initBloc();
_metadataTaskStateChangedListener.begin();
_prefUpdatedListener.begin();
2021-09-12 19:30:09 +02:00
_filePropertyUpdatedListener.begin();
}
@override
dispose() {
_metadataTaskIconController.stop();
_metadataTaskStateChangedListener.end();
_prefUpdatedListener.end();
2021-09-12 19:30:09 +02:00
_filePropertyUpdatedListener.end();
super.dispose();
2021-04-10 06:28:12 +02:00
}
@override
build(BuildContext context) {
return BlocListener<ScanDirBloc, ScanDirBlocState>(
bloc: _bloc,
listener: (context, state) => _onStateChange(context, state),
child: BlocBuilder<ScanDirBloc, ScanDirBlocState>(
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() {
2021-04-11 17:38:01 +02:00
_bloc = ScanDirBloc.of(widget.account);
2021-04-10 06:28:12 +02:00
if (_bloc.state is ScanDirBlocInit) {
_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, ScanDirBlocState state) {
2021-05-19 21:11:06 +02:00
return LayoutBuilder(builder: (context, constraints) {
final scrollExtent = _getScrollViewExtent(constraints);
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,
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context)
.copyWith(scrollbars: false),
child: CustomScrollView(
controller: _scrollController,
slivers: [
_buildAppBar(context),
if (_metadataTaskState != MetadataTaskState.idle)
_buildMetadataTaskHeader(context),
SliverPadding(
2021-07-05 07:23:01 +02:00
padding: const EdgeInsets.symmetric(vertical: 8),
2021-07-06 07:10:36 +02:00
sliver: buildItemStreamList(
maxCrossAxisExtent: _thumbSize.toDouble(),
onMaxExtentChanged: (value) {
setState(() {
_itemListMaxExtent = value;
});
},
),
),
],
),
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
),
2021-05-19 21:11:06 +02:00
if (state is ScanDirBlocLoading)
2021-09-15 08:58:06 +02:00
const Align(
2021-05-19 21:11:06 +02:00
alignment: Alignment.bottomCenter,
2021-09-15 08:58:06 +02:00
child: LinearProgressIndicator(),
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: [
if (platform_k.isAndroid)
2021-04-10 06:28:12 +02:00
IconButton(
2021-07-25 16:27:19 +02:00
icon: const Icon(Icons.share),
tooltip: L10n.global().shareTooltip,
2021-04-10 06:28:12 +02:00
onPressed: () {
2021-07-25 16:27:19 +02:00
_onSelectionAppBarSharePressed(context);
2021-05-28 21:44:09 +02:00
},
),
2021-07-25 16:27:19 +02:00
IconButton(
icon: const Icon(Icons.playlist_add),
tooltip: L10n.global().addToAlbumTooltip,
2021-07-25 16:27:19 +02:00
onPressed: () {
2021-09-08 11:39:24 +02:00
_onAddToAlbumPressed(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) {
_onSelectionAppBarMenuSelected(context, option);
},
),
],
2021-04-10 06:28:12 +02:00
);
}
Widget _buildNormalAppBar(BuildContext context) {
2021-05-19 21:11:06 +02:00
return SliverMeasureExtent(
onChange: (extent) {
_appBarExtent = extent;
2021-04-11 11:57:08 +02:00
},
2021-05-19 21:11:06 +02:00
child: HomeSliverAppBar(
account: widget.account,
actions: [
2021-07-25 10:52:24 +02:00
ZoomMenuButton(
initialZoom: _thumbZoomLevel,
minZoom: -1,
maxZoom: 2,
onZoomChanged: (value) {
setState(() {
_setThumbZoomLevel(value.round());
});
Pref.inst().setHomePhotosZoomLevel(_thumbZoomLevel);
},
2021-05-19 21:11:06 +02:00
),
],
menuActions: [
PopupMenuItem(
value: _menuValueRefresh,
child: Text(L10n.global().refreshMenuLabel),
2021-05-19 21:11:06 +02:00
),
],
onSelectedMenuActions: (option) {
switch (option) {
case _menuValueRefresh:
_onRefreshSelected();
break;
}
},
),
2021-04-10 06:28:12 +02:00
);
}
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(
2021-09-12 19:30:09 +02:00
L10n.global().metadataTaskProcessingNotification +
_getMetadataTaskProgressString(),
2021-09-15 08:58:06 +02:00
style: const TextStyle(fontSize: 12),
),
],
)
else if (_metadataTaskState == MetadataTaskState.waitingForWifi)
Row(
mainAxisSize: MainAxisSize.min,
children: [
2021-09-15 08:58:06 +02:00
const Icon(
Icons.sync_problem,
size: 16,
),
const SizedBox(width: 4),
Text(
L10n.global().metadataTaskPauseNoWiFiNotification,
2021-09-15 08:58:06 +02:00
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(widget.account));
},
),
),
],
),
),
),
),
);
}
2021-04-10 06:28:12 +02:00
void _onStateChange(BuildContext context, ScanDirBlocState state) {
if (state is ScanDirBlocInit) {
2021-04-23 09:55:53 +02:00
itemStreamListItems = [];
2021-04-10 06:28:12 +02:00
} else if (state is ScanDirBlocSuccess || state is ScanDirBlocLoading) {
_transformItems(state.files);
if (state is ScanDirBlocSuccess) {
_tryStartMetadataTask();
}
2021-04-10 06:28:12 +02:00
} else if (state is ScanDirBlocFailure) {
_transformItems(state.files);
if (isPageVisible()) {
SnackBarManager().showSnackBar(SnackBar(
content: Text(exception_util.toUserString(state.exception)),
duration: k.snackBarDurationNormal,
));
}
2021-04-10 06:28:12 +02:00
} else if (state is ScanDirBlocInconsistent) {
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
}
void _onSelectionAppBarSharePressed(BuildContext context) {
assert(platform_k.isAndroid);
final selected = selectedListItems
.whereType<_FileListItem>()
.map((e) => e.file)
.toList();
ShareHandler().shareFiles(context, widget.account, selected).then((_) {
setState(() {
clearSelectedItems();
});
});
}
2021-09-08 11:39:24 +02:00
Future<void> _onAddToAlbumPressed(BuildContext context) async {
try {
final value = await showDialog<Album>(
context: context,
builder: (_) => AlbumPickerDialog(
account: widget.account,
),
);
2021-04-10 06:28:12 +02:00
if (value == null) {
// user cancelled the dialog
2021-09-08 11:39:24 +02:00
return;
}
_log.info("[_onAddToAlbumPressed] Album picked: ${value.name}");
await NotifiedAction(
() async {
assert(value.provider is AlbumStaticProvider);
final selected = selectedListItems
.whereType<_FileListItem>()
.map((e) => AlbumFileItem(file: e.file))
.toList();
final albumRepo = AlbumRepo(AlbumCachedDataSource());
await AddToAlbum(albumRepo)(widget.account, value, selected);
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
});
2021-09-08 11:39:24 +02:00
},
null,
L10n.global().addSelectedToAlbumSuccessNotification(value.name),
failureText: L10n.global().addSelectedToAlbumFailureNotification,
)();
} catch (e, stackTrace) {
_log.shout("[_onAddToAlbumPressed] Exception", e, stackTrace);
}
2021-04-10 06:28:12 +02:00
}
2021-09-28 22:56:44 +02:00
void _onDownloadPressed() {
final selected = selectedListItems
.whereType<_FileListItem>()
.map((e) => e.file)
.toList();
DownloadHandler().downloadFiles(widget.account, selected);
setState(() {
clearSelectedItems();
});
}
2021-09-08 11:39:24 +02:00
Future<void> _onArchivePressed(BuildContext context) async {
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();
});
final fileRepo = FileRepo(FileCachedDataSource());
await NotifiedListAction<File>(
list: selectedFiles,
action: (file) async {
await UpdateProperty(fileRepo)
.updateIsArchived(widget.account, file, true);
},
processingText: L10n.global()
.archiveSelectedProcessingNotification(selectedFiles.length),
successText: L10n.global().archiveSelectedSuccessNotification,
getFailureText: (failures) =>
L10n.global().archiveSelectedFailureNotification(failures.length),
onActionError: (file, e, stackTrace) {
_log.shout(
"[_onArchivePressed] Failed while archiving file" +
(shouldLogFileName ? ": ${file.path}" : ""),
e,
stackTrace);
},
)();
2021-04-10 06:28:12 +02:00
}
2021-09-08 11:39:24 +02:00
Future<void> _onDeletePressed(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
});
final fileRepo = FileRepo(FileCachedDataSource());
final albumRepo = AlbumRepo(AlbumCachedDataSource());
2021-09-08 11:39:24 +02:00
await NotifiedListAction<File>(
list: selectedFiles,
action: (file) async {
await Remove(fileRepo, albumRepo)(widget.account, file);
},
processingText: L10n.global()
.deleteSelectedProcessingNotification(selectedFiles.length),
successText: L10n.global().deleteSelectedSuccessNotification,
getFailureText: (failures) =>
L10n.global().deleteSelectedFailureNotification(failures.length),
onActionError: (file, e, stackTrace) {
2021-04-27 22:06:16 +02:00
_log.shout(
2021-09-08 11:39:24 +02:00
"[_onDeletePressed] Failed while removing file" +
(shouldLogFileName ? ": ${file.path}" : ""),
2021-04-10 06:28:12 +02:00
e,
2021-09-08 11:39:24 +02:00
stackTrace);
},
)();
2021-04-10 06:28:12 +02:00
}
2021-05-28 21:44:09 +02:00
void _onSelectionAppBarMenuSelected(
2021-09-08 11:39:24 +02:00
BuildContext context, _SelectionMenuOption option) {
2021-05-28 21:44:09 +02:00
switch (option) {
2021-09-08 11:39:24 +02:00
case _SelectionMenuOption.archive:
_onArchivePressed(context);
2021-05-28 21:44:09 +02:00
break;
2021-09-08 11:39:24 +02:00
case _SelectionMenuOption.delete:
_onDeletePressed(context);
2021-07-10 16:31:57 +02:00
break;
2021-09-28 22:56:44 +02:00
case _SelectionMenuOption.download:
_onDownloadPressed();
break;
2021-05-28 21:44:09 +02:00
default:
_log.shout("[_onSelectionAppBarMenuSelected] Unknown option: $option");
break;
}
}
2021-04-11 11:57:08 +02:00
void _onRefreshSelected() {
_hasFiredMetadataTask.value = false;
_reqRefresh();
2021-04-11 11:57:08 +02:00
}
void _onMetadataTaskStateChanged(MetadataTaskStateChangedEvent ev) {
2021-09-12 19:30:09 +02:00
if (ev.state == MetadataTaskState.idle) {
_metadataTaskProcessCount = 0;
}
if (ev.state != _metadataTaskState) {
setState(() {
_metadataTaskState = ev.state;
});
}
}
void _onPrefUpdated(PrefUpdatedEvent ev) {
if (ev.key == PrefKey.enableExif && ev.value == true) {
_tryStartMetadataTask(ignoreFired: true);
}
}
2021-09-12 19:30:09 +02:00
void _onFilePropertyUpdated(FilePropertyUpdatedEvent ev) {
if (!ev.hasAnyProperties([FilePropertyUpdatedEvent.propMetadata])) {
return;
}
setState(() {
++_metadataTaskProcessCount;
});
}
void _tryStartMetadataTask({
bool ignoreFired = false,
}) {
if (_bloc.state is ScanDirBlocSuccess &&
Pref.inst().isEnableExifOr(false) &&
(!_hasFiredMetadataTask.value || ignoreFired)) {
MetadataTaskManager().addTask(MetadataTask(widget.account));
2021-09-12 19:30:09 +02:00
_metadataTaskProcessTotalCount = _backingFiles
.where((f) => file_util.isSupportedFormat(f) && f.metadata == null)
.length;
_hasFiredMetadataTask.value = true;
}
}
2021-04-10 06:28:12 +02:00
/// Transform a File list to grid items
void _transformItems(List<File> files) {
_backingFiles = files
2021-05-28 20:45:00 +02:00
.where((element) =>
file_util.isSupportedFormat(element) && element.isArchived != true)
2021-04-10 06:28:12 +02:00
.sorted(compareFileDateTimeDescending);
2021-07-23 22:05:57 +02:00
DateTime? currentDate;
final isMonthOnly = _thumbZoomLevel < 0;
2021-04-23 09:55:53 +02:00
itemStreamListItems = () sync* {
for (int i = 0; i < _backingFiles.length; ++i) {
final f = _backingFiles[i];
2021-04-28 14:55:26 +02:00
2021-07-23 22:05:57 +02:00
final newDate = f.bestDateTime.toLocal();
if (newDate.year != currentDate?.year ||
newDate.month != currentDate?.month ||
(!isMonthOnly && newDate.day != currentDate?.day)) {
yield _DateListItem(date: newDate, isMonthOnly: isMonthOnly);
currentDate = newDate;
2021-04-23 09:55:53 +02:00
}
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();
2021-04-10 06:28:12 +02:00
}
2021-04-11 11:52:39 +02:00
void _reqQuery() {
2021-04-10 06:28:12 +02:00
_bloc.add(ScanDirBlocQuery(
widget.account,
2021-04-11 11:52:39 +02:00
widget.account.roots
2021-04-10 06:28:12 +02:00
.map((e) => File(
path:
"${api_util.getWebdavRootUrlRelative(widget.account)}/$e"))
.toList()));
}
void _reqRefresh() {
_bloc.add(ScanDirBlocRefresh(
widget.account,
widget.account.roots
.map((e) => File(
path:
"${api_util.getWebdavRootUrlRelative(widget.account)}/$e"))
.toList()));
}
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
2021-07-23 22:05:57 +02:00
double? _getScrollViewExtent(BoxConstraints constraints) {
2021-07-06 07:10:36 +02:00
if (_itemListMaxExtent != null &&
2021-05-19 21:11:06 +02:00
constraints.hasBoundedHeight &&
_appBarExtent != null) {
final metadataTaskHeaderExtent =
_metadataTaskState == MetadataTaskState.idle
? 0
: _metadataTaskHeaderHeight;
// scroll extent = list height - widget viewport height + sliver app bar height + metadata task header height + list padding
final scrollExtent = _itemListMaxExtent! -
constraints.maxHeight +
_appBarExtent! +
metadataTaskHeaderExtent +
16;
2021-05-19 21:11:06 +02:00
_log.info(
"[_getScrollViewExtent] $_itemListMaxExtent - ${constraints.maxHeight} + $_appBarExtent + $metadataTaskHeaderExtent + 16 = $scrollExtent");
2021-05-19 21:11:06 +02:00
return scrollExtent;
} else {
return null;
}
}
2021-09-12 19:30:09 +02:00
String _getMetadataTaskProgressString() {
if (_metadataTaskProcessTotalCount == 0) {
return "";
}
final clippedCount =
math.min(_metadataTaskProcessCount, _metadataTaskProcessTotalCount - 1);
return " ($clippedCount/$_metadataTaskProcessTotalCount)";
}
2021-04-10 06:28:12 +02:00
int get _thumbSize {
switch (_thumbZoomLevel) {
2021-04-28 14:55:26 +02:00
case -1:
return 96;
2021-04-10 06:28:12 +02:00
case 1:
return 176;
case 2:
return 256;
case 0:
default:
return 112;
}
}
Primitive<bool> get _hasFiredMetadataTask {
final blocId =
"${widget.account.scheme}://${widget.account.username}@${widget.account.address}";
try {
_log.fine("[_hasFiredMetadataTask] Resolving bloc for '$blocId'");
return KiwiContainer().resolve<Primitive<bool>>(
"HomePhotosState.hasFiredMetadataTask($blocId)");
} catch (_) {
_log.info(
"[_hasFiredMetadataTask] New bloc instance for account: ${widget.account}");
final obj = Primitive(false);
KiwiContainer().registerInstance<Primitive<bool>>(obj,
name: "HomePhotosState.hasFiredMetadataTask($blocId)");
return obj;
}
}
2021-07-23 22:05:57 +02:00
late ScanDirBloc _bloc;
2021-04-10 06:28:12 +02:00
var _backingFiles = <File>[];
var _thumbZoomLevel = 0;
2021-05-19 21:11:06 +02:00
final ScrollController _scrollController = ScrollController();
2021-07-23 22:05:57 +02:00
double? _appBarExtent;
double? _itemListMaxExtent;
2021-05-19 21:11:06 +02:00
late final _metadataTaskStateChangedListener =
AppEventListener<MetadataTaskStateChangedEvent>(
_onMetadataTaskStateChanged);
var _metadataTaskState = MetadataTaskManager().state;
late final _prefUpdatedListener =
AppEventListener<PrefUpdatedEvent>(_onPrefUpdated);
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: this,
)..repeat();
2021-04-10 06:28:12 +02:00
static final _log = Logger("widget.home_photos._HomePhotosState");
2021-04-11 11:57:08 +02:00
static const _menuValueRefresh = 0;
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) {
String subtitle = "";
if (date != null) {
final pattern =
isMonthOnly ? DateFormat.YEAR_MONTH : DateFormat.YEAR_MONTH_DAY;
subtitle =
DateFormat(pattern, Localizations.localeOf(context).languageCode)
2021-07-23 22:05:57 +02:00
.format(date!.toLocal());
}
2021-04-23 09:55:53 +02:00
return Align(
alignment: AlignmentDirectional.centerStart,
2021-07-05 07:23:01 +02:00
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
subtitle,
2021-07-23 22:05:57 +02:00
style: Theme.of(context).textTheme.caption!.copyWith(
2021-07-05 07:23:01 +02:00
color: AppTheme.getPrimaryTextColor(context),
fontWeight: FontWeight.bold,
),
),
2021-04-23 09:55:53 +02:00
),
);
}
2021-04-10 06:28:12 +02:00
2021-07-23 22:05:57 +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",
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,
);
}
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>;
}
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
}