Refactor: tidy up code

This commit is contained in:
Ming Ming 2023-12-28 01:08:13 +08:00
parent 67289f1ccd
commit 782f73448e
6 changed files with 560 additions and 541 deletions

View file

@ -72,8 +72,7 @@ part 'collection_browser/app_bar.dart';
part 'collection_browser/bloc.dart';
part 'collection_browser/state_event.dart';
part 'collection_browser/type.dart';
typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
part 'collection_browser/view.dart';
class CollectionBrowserArguments {
const CollectionBrowserArguments(this.collection);
@ -160,7 +159,7 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser>
child: Scaffold(
body: MultiBlocListener(
listeners: [
BlocListener<_Bloc, _State>(
_BlocListener(
listenWhen: (previous, current) =>
previous.items != current.items,
listener: (context, state) {
@ -169,7 +168,7 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser>
));
},
),
BlocListener<_Bloc, _State>(
_BlocListener(
listenWhen: (previous, current) =>
previous.editItems != current.editItems,
listener: (context, state) {
@ -180,7 +179,7 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser>
}
},
),
BlocListener<_Bloc, _State>(
_BlocListener(
listenWhen: (previous, current) =>
previous.importResult != current.importResult,
listener: (context, state) {
@ -192,7 +191,7 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser>
}
},
),
BlocListener<_Bloc, _State>(
_BlocListener(
listenWhen: (previous, current) =>
previous.isEditMode != current.isEditMode,
listener: (context, state) {
@ -212,7 +211,7 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser>
}
},
),
BlocListener<_Bloc, _State>(
_BlocListener(
listenWhen: (previous, current) =>
previous.error != current.error,
listener: (context, state) {
@ -225,7 +224,7 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser>
}
},
),
BlocListener<_Bloc, _State>(
_BlocListener(
listenWhen: (previous, current) =>
previous.message != current.message,
listener: (context, state) {
@ -300,10 +299,8 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser>
overlaySliver: const _ScalingList(),
);
} else {
if (context
.read<_Bloc>()
.isCollectionCapabilityPermitted(
CollectionCapability.manualSort)) {
if (context.bloc.isCollectionCapabilityPermitted(
CollectionCapability.manualSort)) {
return const _EditContentList();
} else {
return const _UnmodifiableEditContentList();
@ -338,8 +335,7 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser>
}
void _onPointerMove(BuildContext context, PointerMoveEvent event) {
final bloc = context.read<_Bloc>();
if (!bloc.state.isDragging) {
if (!context.state.isDragging) {
return;
}
if (event.position.dy >= MediaQuery.of(context).size.height - 100) {
@ -391,197 +387,18 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser>
}
}
late final _bloc = context.read<_Bloc>();
late final _bloc = context.bloc;
final _scrollController = ScrollController();
bool? _isDragScrollingDown;
int _finger = 0;
var _finger = 0;
}
class _ContentList extends StatelessWidget {
const _ContentList();
typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
typedef _BlocListener = BlocListener<_Bloc, _State>;
// typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
@override
Widget build(BuildContext context) {
return _BlocBuilder(
buildWhen: (previous, current) => previous.zoom != current.zoom,
builder: (context, state) => _ContentListBody(
maxCrossAxisExtent: photo_list_util.getThumbSize(state.zoom).toDouble(),
),
);
}
}
class _ScalingList extends StatelessWidget {
const _ScalingList();
@override
Widget build(BuildContext context) {
return _BlocBuilder(
buildWhen: (previous, current) => previous.scale != current.scale,
builder: (context, state) {
if (state.scale == null) {
return const SizedBox.shrink();
}
int nextZoom;
if (state.scale! > 1) {
nextZoom = state.zoom + 1;
} else {
nextZoom = state.zoom - 1;
}
nextZoom = nextZoom.clamp(-1, 2);
return _ContentListBody(
maxCrossAxisExtent: photo_list_util.getThumbSize(nextZoom).toDouble(),
);
},
);
}
}
class _ContentListBody extends StatelessWidget {
const _ContentListBody({
required this.maxCrossAxisExtent,
});
@override
Widget build(BuildContext context) {
final bloc = context.read<_Bloc>();
return _BlocBuilder(
buildWhen: (previous, current) =>
previous.collection != current.collection ||
previous.transformedItems != current.transformedItems ||
previous.selectedItems != current.selectedItems,
builder: (context, state) => SelectableItemList<_Item>(
maxCrossAxisExtent: maxCrossAxisExtent,
items: state.transformedItems,
itemBuilder: (context, _, item) => item.buildWidget(context),
staggeredTileBuilder: (_, item) => item.staggeredTile,
selectedItems: state.selectedItems,
onSelectionChange: (_, selected) {
bloc.add(_SetSelectedItems(items: selected.cast()));
},
onItemTap: (context, index, _) {
if (state.transformedItems[index] is! _FileItem) {
return;
}
final actualIndex = index -
state.transformedItems
.sublist(0, index)
.where((e) => e is! _FileItem)
.length;
Navigator.of(context).pushNamed(
Viewer.routeName,
arguments: ViewerArguments(
bloc.account,
state.transformedItems
.whereType<_FileItem>()
.map((e) => e.file)
.toList(),
actualIndex,
fromCollection: ViewerCollectionData(
state.collection,
state.transformedItems
.whereType<_ActualItem>()
.map((e) => e.original)
.toList(),
),
),
);
},
),
);
}
final double maxCrossAxisExtent;
}
class _EditContentList extends StatelessWidget {
const _EditContentList();
@override
Widget build(BuildContext context) {
return StreamBuilder<int>(
stream: context.read<PrefController>().albumBrowserZoomLevel,
initialData: context.read<PrefController>().albumBrowserZoomLevel.value,
builder: (_, zoomLevel) {
if (zoomLevel.hasError) {
context.read<_Bloc>().add(
_SetMessage(L10n.global().writePreferenceFailureNotification));
}
return _BlocBuilder(
buildWhen: (previous, current) =>
previous.editTransformedItems != current.editTransformedItems,
builder: (context, state) {
if (context.read<_Bloc>().isCollectionCapabilityPermitted(
CollectionCapability.manualSort)) {
return DraggableItemList<_Item>(
maxCrossAxisExtent: photo_list_util
.getThumbSize(zoomLevel.requireData)
.toDouble(),
items: state.editTransformedItems ?? state.transformedItems,
itemBuilder: (context, _, item) => item.buildWidget(context),
itemDragFeedbackBuilder: (context, _, item) =>
item.buildDragFeedbackWidget(context),
staggeredTileBuilder: (_, item) => item.staggeredTile,
onDragResult: (results) {
context.read<_Bloc>().add(_EditManualSort(results));
},
onDraggingChanged: (value) {
context.read<_Bloc>().add(_SetDragging(value));
},
);
} else {
return SelectableItemList<_Item>(
maxCrossAxisExtent: photo_list_util
.getThumbSize(zoomLevel.requireData)
.toDouble(),
items: state.editTransformedItems ?? state.transformedItems,
itemBuilder: (context, _, item) => item.buildWidget(context),
staggeredTileBuilder: (_, item) => item.staggeredTile,
);
}
},
);
},
);
}
}
/// Unmodifiable content list under edit mode
class _UnmodifiableEditContentList extends StatelessWidget {
const _UnmodifiableEditContentList();
@override
Widget build(BuildContext context) {
return SliverIgnorePointer(
ignoring: true,
sliver: SliverOpacity(
opacity: .25,
sliver: StreamBuilder<int>(
stream: context.read<PrefController>().albumBrowserZoomLevel,
initialData:
context.read<PrefController>().albumBrowserZoomLevel.value,
builder: (_, zoomLevel) {
if (zoomLevel.hasError) {
context.read<_Bloc>().add(_SetMessage(
L10n.global().writePreferenceFailureNotification));
}
return _BlocBuilder(
buildWhen: (previous, current) =>
previous.editTransformedItems != current.editTransformedItems,
builder: (context, state) {
return SelectableItemList<_Item>(
maxCrossAxisExtent: photo_list_util
.getThumbSize(zoomLevel.requireData)
.toDouble(),
items: state.editTransformedItems ?? state.transformedItems,
itemBuilder: (context, _, item) => item.buildWidget(context),
staggeredTileBuilder: (_, item) => item.staggeredTile,
);
},
);
},
),
),
);
}
extension on BuildContext {
_Bloc get bloc => read<_Bloc>();
_State get state => bloc.state;
void addEvent(_Event event) => bloc.add(event);
}

View file

@ -11,8 +11,8 @@ class _AppBar extends StatelessWidget {
previous.items != current.items ||
previous.collection != current.collection,
builder: (context, state) {
final bloc = context.read<_Bloc>();
final adapter = CollectionAdapter.of(c, bloc.account, state.collection);
final adapter =
CollectionAdapter.of(c, context.bloc.account, state.collection);
final canRename = adapter.isPermitted(CollectionCapability.rename);
final canManualCover =
adapter.isPermitted(CollectionCapability.manualCover);
@ -69,7 +69,8 @@ class _AppBar extends StatelessWidget {
],
if (state.collection.contentProvider
is CollectionAlbumProvider &&
CollectionAdapter.of(c, bloc.account, state.collection)
CollectionAdapter.of(
c, context.bloc.account, state.collection)
.isPermitted(CollectionCapability.share))
PopupMenuItem(
value: _MenuOption.albumFixShare,

View file

@ -0,0 +1,189 @@
part of '../collection_browser.dart';
class _ContentList extends StatelessWidget {
const _ContentList();
@override
Widget build(BuildContext context) {
return _BlocBuilder(
buildWhen: (previous, current) => previous.zoom != current.zoom,
builder: (context, state) => _ContentListBody(
maxCrossAxisExtent: photo_list_util.getThumbSize(state.zoom).toDouble(),
),
);
}
}
class _ScalingList extends StatelessWidget {
const _ScalingList();
@override
Widget build(BuildContext context) {
return _BlocBuilder(
buildWhen: (previous, current) => previous.scale != current.scale,
builder: (context, state) {
if (state.scale == null) {
return const SizedBox.shrink();
}
int nextZoom;
if (state.scale! > 1) {
nextZoom = state.zoom + 1;
} else {
nextZoom = state.zoom - 1;
}
nextZoom = nextZoom.clamp(-1, 2);
return _ContentListBody(
maxCrossAxisExtent: photo_list_util.getThumbSize(nextZoom).toDouble(),
);
},
);
}
}
class _ContentListBody extends StatelessWidget {
const _ContentListBody({
required this.maxCrossAxisExtent,
});
@override
Widget build(BuildContext context) {
return _BlocBuilder(
buildWhen: (previous, current) =>
previous.collection != current.collection ||
previous.transformedItems != current.transformedItems ||
previous.selectedItems != current.selectedItems,
builder: (context, state) => SelectableItemList<_Item>(
maxCrossAxisExtent: maxCrossAxisExtent,
items: state.transformedItems,
itemBuilder: (context, _, item) => item.buildWidget(context),
staggeredTileBuilder: (_, item) => item.staggeredTile,
selectedItems: state.selectedItems,
onSelectionChange: (_, selected) {
context.addEvent(_SetSelectedItems(items: selected.cast()));
},
onItemTap: (context, index, _) {
if (state.transformedItems[index] is! _FileItem) {
return;
}
final actualIndex = index -
state.transformedItems
.sublist(0, index)
.where((e) => e is! _FileItem)
.length;
Navigator.of(context).pushNamed(
Viewer.routeName,
arguments: ViewerArguments(
context.bloc.account,
state.transformedItems
.whereType<_FileItem>()
.map((e) => e.file)
.toList(),
actualIndex,
fromCollection: ViewerCollectionData(
state.collection,
state.transformedItems
.whereType<_ActualItem>()
.map((e) => e.original)
.toList(),
),
),
);
},
),
);
}
final double maxCrossAxisExtent;
}
class _EditContentList extends StatelessWidget {
const _EditContentList();
@override
Widget build(BuildContext context) {
return StreamBuilder<int>(
stream: context.read<PrefController>().albumBrowserZoomLevel,
initialData: context.read<PrefController>().albumBrowserZoomLevel.value,
builder: (_, zoomLevel) {
if (zoomLevel.hasError) {
context.addEvent(
_SetMessage(L10n.global().writePreferenceFailureNotification));
}
return _BlocBuilder(
buildWhen: (previous, current) =>
previous.editTransformedItems != current.editTransformedItems,
builder: (context, state) {
if (context.bloc.isCollectionCapabilityPermitted(
CollectionCapability.manualSort)) {
return DraggableItemList<_Item>(
maxCrossAxisExtent: photo_list_util
.getThumbSize(zoomLevel.requireData)
.toDouble(),
items: state.editTransformedItems ?? state.transformedItems,
itemBuilder: (context, _, item) => item.buildWidget(context),
itemDragFeedbackBuilder: (context, _, item) =>
item.buildDragFeedbackWidget(context),
staggeredTileBuilder: (_, item) => item.staggeredTile,
onDragResult: (results) {
context.addEvent(_EditManualSort(results));
},
onDraggingChanged: (value) {
context.addEvent(_SetDragging(value));
},
);
} else {
return SelectableItemList<_Item>(
maxCrossAxisExtent: photo_list_util
.getThumbSize(zoomLevel.requireData)
.toDouble(),
items: state.editTransformedItems ?? state.transformedItems,
itemBuilder: (context, _, item) => item.buildWidget(context),
staggeredTileBuilder: (_, item) => item.staggeredTile,
);
}
},
);
},
);
}
}
/// Unmodifiable content list under edit mode
class _UnmodifiableEditContentList extends StatelessWidget {
const _UnmodifiableEditContentList();
@override
Widget build(BuildContext context) {
return SliverIgnorePointer(
ignoring: true,
sliver: SliverOpacity(
opacity: .25,
sliver: StreamBuilder<int>(
stream: context.read<PrefController>().albumBrowserZoomLevel,
initialData:
context.read<PrefController>().albumBrowserZoomLevel.value,
builder: (_, zoomLevel) {
if (zoomLevel.hasError) {
context.addEvent(_SetMessage(
L10n.global().writePreferenceFailureNotification));
}
return _BlocBuilder(
buildWhen: (previous, current) =>
previous.editTransformedItems != current.editTransformedItems,
builder: (context, state) {
return SelectableItemList<_Item>(
maxCrossAxisExtent: photo_list_util
.getThumbSize(zoomLevel.requireData)
.toDouble(),
items: state.editTransformedItems ?? state.transformedItems,
itemBuilder: (context, _, item) => item.buildWidget(context),
staggeredTileBuilder: (_, item) => item.staggeredTile,
);
},
);
},
),
),
);
}
}

View file

@ -46,11 +46,11 @@ import 'package:np_codegen/np_codegen.dart';
import 'package:to_string/to_string.dart';
part 'home_collections.g.dart';
part 'home_collections/app_bar.dart';
part 'home_collections/bloc.dart';
part 'home_collections/state_event.dart';
part 'home_collections/type.dart';
typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
part 'home_collections/view.dart';
/// Show and manage a list of [Collection]s
class HomeCollections extends StatelessWidget {
@ -91,14 +91,14 @@ class _WrappedHomeCollectionsState extends State<_WrappedHomeCollections>
Widget build(BuildContext context) {
return MultiBlocListener(
listeners: [
BlocListener<_Bloc, _State>(
_BlocListener(
listenWhen: (previous, current) =>
previous.collections != current.collections,
listener: (context, state) {
_bloc.add(_TransformItems(state.collections));
},
),
BlocListener<_Bloc, _State>(
_BlocListener(
listenWhen: (previous, current) => previous.error != current.error,
listener: (context, state) {
if (state.error != null && isPageVisible()) {
@ -109,7 +109,7 @@ class _WrappedHomeCollectionsState extends State<_WrappedHomeCollections>
}
},
),
BlocListener<_Bloc, _State>(
_BlocListener(
listenWhen: (previous, current) =>
previous.removeError != current.removeError,
listener: (context, state) {
@ -266,336 +266,12 @@ class _WrappedHomeCollectionsState extends State<_WrappedHomeCollections>
late final _Bloc _bloc = context.read();
}
class _AppBar extends StatelessWidget {
const _AppBar();
typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
typedef _BlocListener = BlocListener<_Bloc, _State>;
// typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
@override
Widget build(BuildContext context) {
return _BlocBuilder(
buildWhen: (previous, current) => previous.isLoading != current.isLoading,
builder: (context, state) => HomeSliverAppBar(
account: context.read<_Bloc>().account,
isShowProgressIcon: state.isLoading,
menuActions: [
PopupMenuItem(
value: _menuValueSort,
child: Text(L10n.global().sortTooltip),
),
PopupMenuItem(
value: _menuValueImport,
child: Text(L10n.global().importFoldersTooltip),
),
],
onSelectedMenuActions: (option) {
switch (option) {
case _menuValueSort:
_onSortPressed(context);
break;
case _menuValueImport:
_onImportPressed(context);
break;
}
},
),
);
}
Future<void> _onSortPressed(BuildContext context) async {
final sort = context.read<_Bloc>().state.sort;
final result = await showDialog<collection_util.CollectionSort>(
context: context,
builder: (context) => FancyOptionPicker(
title: Text(L10n.global().sortOptionDialogTitle),
items: [
FancyOptionPickerItem(
label: L10n.global().sortOptionTimeDescendingLabel,
isSelected: sort == collection_util.CollectionSort.dateDescending,
onSelect: () {
Navigator.of(context)
.pop(collection_util.CollectionSort.dateDescending);
},
),
FancyOptionPickerItem(
label: L10n.global().sortOptionTimeAscendingLabel,
isSelected: sort == collection_util.CollectionSort.dateAscending,
onSelect: () {
Navigator.of(context)
.pop(collection_util.CollectionSort.dateAscending);
},
),
FancyOptionPickerItem(
label: L10n.global().sortOptionAlbumNameLabel,
isSelected: sort == collection_util.CollectionSort.nameAscending,
onSelect: () {
Navigator.of(context)
.pop(collection_util.CollectionSort.nameAscending);
},
),
FancyOptionPickerItem(
label: L10n.global().sortOptionAlbumNameDescendingLabel,
isSelected: sort == collection_util.CollectionSort.nameDescending,
onSelect: () {
Navigator.of(context)
.pop(collection_util.CollectionSort.nameDescending);
},
),
],
),
);
if (result == null) {
return;
}
context.read<_Bloc>().add(_SetCollectionSort(result));
}
void _onImportPressed(BuildContext context) {
Navigator.of(context).pushNamed(AlbumImporter.routeName,
arguments: AlbumImporterArguments(context.read<_Bloc>().account));
}
static const _menuValueImport = 0;
static const _menuValueSort = 1;
}
@npLog
class _SelectionAppBar extends StatelessWidget {
const _SelectionAppBar();
@override
Widget build(BuildContext context) {
return _BlocBuilder(
buildWhen: (previous, current) =>
previous.selectedItems != current.selectedItems,
builder: (context, state) => SelectionAppBar(
count: state.selectedItems.length,
onClosePressed: () {
context.read<_Bloc>().add(const _SetSelectedItems(items: {}));
},
actions: [
IconButton(
icon: const Icon(Icons.delete),
tooltip: L10n.global().deleteTooltip,
onPressed: () {
context.read<_Bloc>().add(const _RemoveSelectedItems());
},
),
],
),
);
}
}
class _ButtonGrid extends StatelessWidget {
const _ButtonGrid({
required this.account,
required this.isEnabled,
this.onSharingPressed,
this.onEnhancedPhotosPressed,
this.onArchivePressed,
this.onTrashbinPressed,
this.onNewCollectionPressed,
});
@override
Widget build(BuildContext context) {
// needed to workaround a scrolling bug when there are more than one
// SliverStaggeredGrids in a CustomScrollView
// see: https://github.com/letsar/flutter_staggered_grid_view/issues/98 and
// https://github.com/letsar/flutter_staggered_grid_view/issues/265
return SliverToBoxAdapter(
child: StaggeredGridView.extent(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.all(0),
maxCrossAxisExtent: 256,
staggeredTiles: List.filled(5, const StaggeredTile.fit(1)),
children: [
_ButtonGridItemView(
icon: Icons.share_outlined,
label: L10n.global().collectionSharingLabel,
isShowIndicator: AccountPref.of(account).hasNewSharedAlbumOr(),
isEnabled: isEnabled,
onTap: () {
onSharingPressed?.call();
},
),
if (features.isSupportEnhancement)
_ButtonGridItemView(
icon: Icons.auto_fix_high_outlined,
label: L10n.global().collectionEditedPhotosLabel,
isEnabled: isEnabled,
onTap: () {
onEnhancedPhotosPressed?.call();
},
),
_ButtonGridItemView(
icon: Icons.archive_outlined,
label: L10n.global().albumArchiveLabel,
isEnabled: isEnabled,
onTap: () {
onArchivePressed?.call();
},
),
_ButtonGridItemView(
icon: Icons.delete_outlined,
label: L10n.global().albumTrashLabel,
isEnabled: isEnabled,
onTap: () {
onTrashbinPressed?.call();
},
),
_ButtonGridItemView(
icon: Icons.add,
label: L10n.global().createCollectionTooltip,
isEnabled: isEnabled,
onTap: () {
onNewCollectionPressed?.call();
},
),
],
),
);
}
final Account account;
final bool isEnabled;
final VoidCallback? onSharingPressed;
final VoidCallback? onEnhancedPhotosPressed;
final VoidCallback? onArchivePressed;
final VoidCallback? onTrashbinPressed;
final VoidCallback? onNewCollectionPressed;
}
class _ButtonGridItemView extends StatelessWidget {
const _ButtonGridItemView({
required this.icon,
required this.label,
this.isShowIndicator = false,
required this.isEnabled,
this.onTap,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(4),
child: ActionChip(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
labelPadding: const EdgeInsetsDirectional.fromSTEB(8, 0, 0, 0),
// specify icon size explicitly to workaround size flickering during
// theme transition
avatar: Icon(icon, size: 18),
label: Row(
children: [
Expanded(
child: Text(label),
),
if (isShowIndicator)
Icon(
Icons.circle,
color: Theme.of(context).colorScheme.tertiary,
size: 8,
),
],
),
onPressed: isEnabled ? onTap : null,
),
);
}
final IconData icon;
final String label;
final bool isShowIndicator;
final bool isEnabled;
final VoidCallback? onTap;
}
class _ItemView extends StatelessWidget {
const _ItemView({
required this.account,
required this.item,
});
@override
Widget build(BuildContext context) {
Widget? icon;
switch (item.itemType) {
case _ItemType.ncAlbum:
icon = const Icon(Icons.cloud);
break;
case _ItemType.album:
icon = null;
break;
case _ItemType.tagAlbum:
icon = const Icon(Icons.local_offer);
break;
case _ItemType.dirAlbum:
icon = const Icon(Icons.folder);
break;
}
String subtitle = "";
if (item.isShared) {
subtitle = "${L10n.global().albumSharedLabel} | ";
}
subtitle += item.subtitle ?? "";
return CollectionGridItem(
cover: _CollectionCover(
account: account,
url: item.coverUrl,
),
title: item.name,
subtitle: subtitle,
icon: icon,
);
}
final Account account;
final _Item item;
}
class _CollectionCover extends StatelessWidget {
const _CollectionCover({
required this.account,
required this.url,
});
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Theme.of(context).listPlaceholderBackgroundColor,
constraints: const BoxConstraints.expand(),
child: url != null
? FittedBox(
clipBehavior: Clip.hardEdge,
fit: BoxFit.cover,
child: CachedNetworkImage(
cacheManager: CoverCacheManager.inst,
imageUrl: url!,
httpHeaders: {
"Authorization":
AuthUtil.fromAccount(account).toHeaderValue(),
},
fadeInDuration: const Duration(),
filterQuality: FilterQuality.high,
errorWidget: (context, url, error) {
// just leave it empty
return Container();
},
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
),
)
: Icon(
Icons.panorama,
color: Theme.of(context).listPlaceholderForegroundColor,
size: 88,
),
),
);
}
final Account account;
final String? url;
extension on BuildContext {
_Bloc get bloc => read<_Bloc>();
_State get state => bloc.state;
void addEvent(_Event event) => bloc.add(event);
}

View file

@ -0,0 +1,121 @@
part of '../home_collections.dart';
class _AppBar extends StatelessWidget {
const _AppBar();
@override
Widget build(BuildContext context) {
return _BlocBuilder(
buildWhen: (previous, current) => previous.isLoading != current.isLoading,
builder: (context, state) => HomeSliverAppBar(
account: context.bloc.account,
isShowProgressIcon: state.isLoading,
menuActions: [
PopupMenuItem(
value: _menuValueSort,
child: Text(L10n.global().sortTooltip),
),
PopupMenuItem(
value: _menuValueImport,
child: Text(L10n.global().importFoldersTooltip),
),
],
onSelectedMenuActions: (option) {
switch (option) {
case _menuValueSort:
_onSortPressed(context);
break;
case _menuValueImport:
_onImportPressed(context);
break;
}
},
),
);
}
Future<void> _onSortPressed(BuildContext context) async {
final sort = context.state.sort;
final result = await showDialog<collection_util.CollectionSort>(
context: context,
builder: (context) => FancyOptionPicker(
title: Text(L10n.global().sortOptionDialogTitle),
items: [
FancyOptionPickerItem(
label: L10n.global().sortOptionTimeDescendingLabel,
isSelected: sort == collection_util.CollectionSort.dateDescending,
onSelect: () {
Navigator.of(context)
.pop(collection_util.CollectionSort.dateDescending);
},
),
FancyOptionPickerItem(
label: L10n.global().sortOptionTimeAscendingLabel,
isSelected: sort == collection_util.CollectionSort.dateAscending,
onSelect: () {
Navigator.of(context)
.pop(collection_util.CollectionSort.dateAscending);
},
),
FancyOptionPickerItem(
label: L10n.global().sortOptionAlbumNameLabel,
isSelected: sort == collection_util.CollectionSort.nameAscending,
onSelect: () {
Navigator.of(context)
.pop(collection_util.CollectionSort.nameAscending);
},
),
FancyOptionPickerItem(
label: L10n.global().sortOptionAlbumNameDescendingLabel,
isSelected: sort == collection_util.CollectionSort.nameDescending,
onSelect: () {
Navigator.of(context)
.pop(collection_util.CollectionSort.nameDescending);
},
),
],
),
);
if (result == null) {
return;
}
context.addEvent(_SetCollectionSort(result));
}
void _onImportPressed(BuildContext context) {
Navigator.of(context).pushNamed(AlbumImporter.routeName,
arguments: AlbumImporterArguments(context.bloc.account));
}
static const _menuValueImport = 0;
static const _menuValueSort = 1;
}
@npLog
class _SelectionAppBar extends StatelessWidget {
const _SelectionAppBar();
@override
Widget build(BuildContext context) {
return _BlocBuilder(
buildWhen: (previous, current) =>
previous.selectedItems != current.selectedItems,
builder: (context, state) => SelectionAppBar(
count: state.selectedItems.length,
onClosePressed: () {
context.addEvent(const _SetSelectedItems(items: {}));
},
actions: [
IconButton(
icon: const Icon(Icons.delete),
tooltip: L10n.global().deleteTooltip,
onPressed: () {
context.addEvent(const _RemoveSelectedItems());
},
),
],
),
);
}
}

View file

@ -0,0 +1,215 @@
part of '../home_collections.dart';
class _ButtonGrid extends StatelessWidget {
const _ButtonGrid({
required this.account,
required this.isEnabled,
this.onSharingPressed,
this.onEnhancedPhotosPressed,
this.onArchivePressed,
this.onTrashbinPressed,
this.onNewCollectionPressed,
});
@override
Widget build(BuildContext context) {
// needed to workaround a scrolling bug when there are more than one
// SliverStaggeredGrids in a CustomScrollView
// see: https://github.com/letsar/flutter_staggered_grid_view/issues/98 and
// https://github.com/letsar/flutter_staggered_grid_view/issues/265
return SliverToBoxAdapter(
child: StaggeredGridView.extent(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.all(0),
maxCrossAxisExtent: 256,
staggeredTiles: List.filled(5, const StaggeredTile.fit(1)),
children: [
_ButtonGridItemView(
icon: Icons.share_outlined,
label: L10n.global().collectionSharingLabel,
isShowIndicator: AccountPref.of(account).hasNewSharedAlbumOr(),
isEnabled: isEnabled,
onTap: () {
onSharingPressed?.call();
},
),
if (features.isSupportEnhancement)
_ButtonGridItemView(
icon: Icons.auto_fix_high_outlined,
label: L10n.global().collectionEditedPhotosLabel,
isEnabled: isEnabled,
onTap: () {
onEnhancedPhotosPressed?.call();
},
),
_ButtonGridItemView(
icon: Icons.archive_outlined,
label: L10n.global().albumArchiveLabel,
isEnabled: isEnabled,
onTap: () {
onArchivePressed?.call();
},
),
_ButtonGridItemView(
icon: Icons.delete_outlined,
label: L10n.global().albumTrashLabel,
isEnabled: isEnabled,
onTap: () {
onTrashbinPressed?.call();
},
),
_ButtonGridItemView(
icon: Icons.add,
label: L10n.global().createCollectionTooltip,
isEnabled: isEnabled,
onTap: () {
onNewCollectionPressed?.call();
},
),
],
),
);
}
final Account account;
final bool isEnabled;
final VoidCallback? onSharingPressed;
final VoidCallback? onEnhancedPhotosPressed;
final VoidCallback? onArchivePressed;
final VoidCallback? onTrashbinPressed;
final VoidCallback? onNewCollectionPressed;
}
class _ButtonGridItemView extends StatelessWidget {
const _ButtonGridItemView({
required this.icon,
required this.label,
this.isShowIndicator = false,
required this.isEnabled,
this.onTap,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(4),
child: ActionChip(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
labelPadding: const EdgeInsetsDirectional.fromSTEB(8, 0, 0, 0),
// specify icon size explicitly to workaround size flickering during
// theme transition
avatar: Icon(icon, size: 18),
label: Row(
children: [
Expanded(
child: Text(label),
),
if (isShowIndicator)
Icon(
Icons.circle,
color: Theme.of(context).colorScheme.tertiary,
size: 8,
),
],
),
onPressed: isEnabled ? onTap : null,
),
);
}
final IconData icon;
final String label;
final bool isShowIndicator;
final bool isEnabled;
final VoidCallback? onTap;
}
class _ItemView extends StatelessWidget {
const _ItemView({
required this.account,
required this.item,
});
@override
Widget build(BuildContext context) {
Widget? icon;
switch (item.itemType) {
case _ItemType.ncAlbum:
icon = const Icon(Icons.cloud);
break;
case _ItemType.album:
icon = null;
break;
case _ItemType.tagAlbum:
icon = const Icon(Icons.local_offer);
break;
case _ItemType.dirAlbum:
icon = const Icon(Icons.folder);
break;
}
var subtitle = "";
if (item.isShared) {
subtitle = "${L10n.global().albumSharedLabel} | ";
}
subtitle += item.subtitle ?? "";
return CollectionGridItem(
cover: _CollectionCover(
account: account,
url: item.coverUrl,
),
title: item.name,
subtitle: subtitle,
icon: icon,
);
}
final Account account;
final _Item item;
}
class _CollectionCover extends StatelessWidget {
const _CollectionCover({
required this.account,
required this.url,
});
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Theme.of(context).listPlaceholderBackgroundColor,
constraints: const BoxConstraints.expand(),
child: url != null
? FittedBox(
clipBehavior: Clip.hardEdge,
fit: BoxFit.cover,
child: CachedNetworkImage(
cacheManager: CoverCacheManager.inst,
imageUrl: url!,
httpHeaders: {
"Authorization":
AuthUtil.fromAccount(account).toHeaderValue(),
},
fadeInDuration: const Duration(),
filterQuality: FilterQuality.high,
errorWidget: (context, url, error) {
// just leave it empty
return Container();
},
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
),
)
: Icon(
Icons.panorama,
color: Theme.of(context).listPlaceholderForegroundColor,
size: 88,
),
),
);
}
final Account account;
final String? url;
}