Merge branch 'new-collections-nav-bar'

This commit is contained in:
Ming Ming 2024-10-27 03:40:08 +08:00
commit cecd0691fe
30 changed files with 1557 additions and 214 deletions

View file

@ -12,9 +12,11 @@ import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/language_util.dart';
import 'package:nc_photos/protected_page_handler.dart';
import 'package:nc_photos/size.dart';
import 'package:nc_photos/widget/home_collections.dart';
import 'package:nc_photos/widget/viewer.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/object_util.dart';
import 'package:np_common/type.dart';
import 'package:np_gps_map/np_gps_map.dart';
import 'package:np_string/np_string.dart';
import 'package:rxdart/rxdart.dart';
@ -261,6 +263,17 @@ class PrefController {
defaultValue: _viewerBottomAppBarButtonsDefault,
);
Future<bool> setHomeCollectionsNavBarButtons(
List<PrefHomeCollectionsNavButton>? value) =>
_setOrRemove(
controller: _homeCollectionsNavBarButtonsController,
setter: (pref, value) => pref.setHomeCollectionsNavBarButtonsJson(
jsonEncode(value.map((e) => e.toJson()).toList())),
remover: (pref) => pref.setHomeCollectionsNavBarButtonsJson(null),
value: value,
defaultValue: _homeCollectionsNavBarButtonsDefault,
);
Future<bool> _set<T>({
required BehaviorSubject<T> controller,
required Future<bool> Function(Pref pref, T value) setter,
@ -406,6 +419,14 @@ class PrefController {
@npSubjectAccessor
late final _viewerBottomAppBarButtonsController = BehaviorSubject.seeded(
pref.getViewerBottomAppBarButtons() ?? _viewerBottomAppBarButtonsDefault);
@npSubjectAccessor
late final _homeCollectionsNavBarButtonsController = BehaviorSubject.seeded(
pref.getHomeCollectionsNavBarButtonsJson()?.let((s) =>
(jsonDecode(s) as List)
.cast<JsonObj>()
.map(PrefHomeCollectionsNavButton.fromJson)
.toList()) ??
_homeCollectionsNavBarButtonsDefault);
}
extension PrefControllerExtension on PrefController {
@ -561,6 +582,24 @@ const _viewerBottomAppBarButtonsDefault = [
ViewerAppBarButtonType.download,
ViewerAppBarButtonType.delete,
];
const _homeCollectionsNavBarButtonsDefault = [
PrefHomeCollectionsNavButton(
type: HomeCollectionsNavBarButtonType.sharing,
isMinimized: false,
),
PrefHomeCollectionsNavButton(
type: HomeCollectionsNavBarButtonType.edited,
isMinimized: false,
),
PrefHomeCollectionsNavButton(
type: HomeCollectionsNavBarButtonType.archive,
isMinimized: false,
),
PrefHomeCollectionsNavButton(
type: HomeCollectionsNavBarButtonType.trash,
isMinimized: false,
),
];
@npLog
// ignore: camel_case_types

View file

@ -252,6 +252,18 @@ extension $PrefControllerNpSubjectAccessor on PrefController {
viewerBottomAppBarButtons.distinct().skip(1);
List<ViewerAppBarButtonType> get viewerBottomAppBarButtonsValue =>
_viewerBottomAppBarButtonsController.value;
// _homeCollectionsNavBarButtonsController
ValueStream<List<PrefHomeCollectionsNavButton>>
get homeCollectionsNavBarButtons =>
_homeCollectionsNavBarButtonsController.stream;
Stream<List<PrefHomeCollectionsNavButton>>
get homeCollectionsNavBarButtonsNew =>
homeCollectionsNavBarButtons.skip(1);
Stream<List<PrefHomeCollectionsNavButton>>
get homeCollectionsNavBarButtonsChange =>
homeCollectionsNavBarButtons.distinct().skip(1);
List<PrefHomeCollectionsNavButton> get homeCollectionsNavBarButtonsValue =>
_homeCollectionsNavBarButtonsController.value;
}
extension $SecurePrefControllerNpSubjectAccessor on SecurePrefController {

View file

@ -14,3 +14,24 @@ enum PrefMapDefaultRangeType {
final int value;
}
class PrefHomeCollectionsNavButton {
const PrefHomeCollectionsNavButton({
required this.type,
required this.isMinimized,
});
static PrefHomeCollectionsNavButton fromJson(JsonObj json) =>
PrefHomeCollectionsNavButton(
type: HomeCollectionsNavBarButtonType.fromValue(json["type"]),
isMinimized: json["isMinimized"],
);
JsonObj toJson() => {
"type": type.index,
"isMinimized": isMinimized,
};
final HomeCollectionsNavBarButtonType type;
final bool isMinimized;
}

View file

@ -185,6 +185,16 @@ extension on Pref {
value.map((e) => e.index).toList());
}
}
String? getHomeCollectionsNavBarButtonsJson() =>
provider.getString(PrefKey.homeCollectionsNavBarButtons);
Future<bool> setHomeCollectionsNavBarButtonsJson(String? value) {
if (value == null) {
return provider.remove(PrefKey.homeCollectionsNavBarButtons);
} else {
return provider.setString(PrefKey.homeCollectionsNavBarButtons, value);
}
}
}
MapCoord? _tryMapCoordFromJson(dynamic json) {

View file

@ -117,6 +117,7 @@ enum PrefKey implements PrefKeyInterface {
mapDefaultCustomRange,
viewerAppBarButtons,
viewerBottomAppBarButtons,
homeCollectionsNavBarButtons,
;
@override
@ -215,6 +216,8 @@ enum PrefKey implements PrefKeyInterface {
return "viewerAppBarButtons";
case PrefKey.viewerBottomAppBarButtons:
return "viewerBottomAppBarButtons";
case PrefKey.homeCollectionsNavBarButtons:
return "homeCollectionsNavBarButtons";
}
}
}

View file

@ -8,6 +8,12 @@ import 'package:nc_photos/exception.dart';
import 'package:nc_photos/navigation_manager.dart';
import 'package:nc_photos/widget/trusted_cert_manager.dart';
class AppMessageException implements Exception {
const AppMessageException(this.message);
final String message;
}
/// Convert an exception to a user-facing string
///
/// Typically used with SnackBar to show a proper error message
@ -65,6 +71,8 @@ String toUserString(Object? exception) {
"Failed to update files: ${exception.files.map((f) => f.filename).join(", ")}",
null
);
} else if (exception is AppMessageException) {
return (exception.message, null);
}
return (exception?.toString() ?? "Unknown error", null);
}

View file

@ -360,6 +360,7 @@
"settingsViewerCustomizeBottomAppBarTitle": "Customize bottom app bar",
"settingsShowDateInAlbumTitle": "Group photos by date",
"settingsShowDateInAlbumDescription": "Apply only when the album is sorted by time",
"settingsCollectionsCustomizeNavigationBarTitle": "Customize navigation bar",
"settingsImageEditTitle": "Editor",
"@settingsImageEditTitle": {
"description": "Include settings for image enhancements and the image editor"
@ -1512,6 +1513,14 @@
"@dragAndDropRearrangeButtons": {
"description": "Instruction to customize buttons layout"
},
"customizeCollectionsNavBarDescription": "Drag and drop to rearrange buttons, tap the buttons above to minimize them",
"@customizeCollectionsNavBarDescription": {
"description": "Instruction to customize navigation bar buttons in the Collections page"
},
"customizeButtonsUnsupportedWarning": "This button cannot be customized",
"@customizeButtonsUnsupportedWarning": {
"description": "Some button can't be removed. This message will be shown instead when user try to do so"
},
"errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues",
"@errorUnauthenticated": {

View file

@ -18,6 +18,7 @@
"settingsViewerCustomizeBottomAppBarTitle",
"settingsShowDateInAlbumTitle",
"settingsShowDateInAlbumDescription",
"settingsCollectionsCustomizeNavigationBarTitle",
"settingsImageEditTitle",
"settingsImageEditDescription",
"settingsEnhanceMaxResolutionTitle2",
@ -265,6 +266,8 @@
"alternativeSignIn",
"livePhotoTooltip",
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning",
"errorUnauthenticated",
"errorDisconnected",
"errorLocked",
@ -278,17 +281,23 @@
"cs": [
"settingsViewerCustomizeAppBarTitle",
"settingsViewerCustomizeBottomAppBarTitle",
"settingsCollectionsCustomizeNavigationBarTitle",
"alternativeSignIn",
"livePhotoTooltip",
"dragAndDropRearrangeButtons"
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
],
"de": [
"settingsViewerCustomizeAppBarTitle",
"settingsViewerCustomizeBottomAppBarTitle",
"settingsCollectionsCustomizeNavigationBarTitle",
"alternativeSignIn",
"livePhotoTooltip",
"dragAndDropRearrangeButtons"
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
],
"el": [
@ -308,6 +317,7 @@
"settingsMemoriesRangeValueText",
"settingsViewerCustomizeAppBarTitle",
"settingsViewerCustomizeBottomAppBarTitle",
"settingsCollectionsCustomizeNavigationBarTitle",
"settingsImageEditTitle",
"settingsImageEditDescription",
"settingsEnhanceMaxResolutionTitle2",
@ -441,20 +451,26 @@
"todayText",
"alternativeSignIn",
"livePhotoTooltip",
"dragAndDropRearrangeButtons"
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
],
"es": [
"settingsViewerCustomizeAppBarTitle",
"settingsViewerCustomizeBottomAppBarTitle",
"settingsCollectionsCustomizeNavigationBarTitle",
"alternativeSignIn",
"livePhotoTooltip",
"dragAndDropRearrangeButtons"
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
],
"fi": [
"settingsViewerCustomizeAppBarTitle",
"settingsViewerCustomizeBottomAppBarTitle",
"settingsCollectionsCustomizeNavigationBarTitle",
"settingsThemePrimaryColor",
"settingsThemeSecondaryColor",
"settingsThemePresets",
@ -493,12 +509,15 @@
"todayText",
"alternativeSignIn",
"livePhotoTooltip",
"dragAndDropRearrangeButtons"
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
],
"fr": [
"settingsViewerCustomizeAppBarTitle",
"settingsViewerCustomizeBottomAppBarTitle",
"settingsCollectionsCustomizeNavigationBarTitle",
"settingsThemePrimaryColor",
"settingsThemeSecondaryColor",
"settingsThemePresets",
@ -537,13 +556,16 @@
"todayText",
"alternativeSignIn",
"livePhotoTooltip",
"dragAndDropRearrangeButtons"
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
],
"it": [
"settingsPersonProviderTitle",
"settingsViewerCustomizeAppBarTitle",
"settingsViewerCustomizeBottomAppBarTitle",
"settingsCollectionsCustomizeNavigationBarTitle",
"settingsImageEditTitle",
"settingsThemePrimaryColor",
"settingsThemeSecondaryColor",
@ -586,7 +608,9 @@
"todayText",
"alternativeSignIn",
"livePhotoTooltip",
"dragAndDropRearrangeButtons"
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
],
"nl": [
@ -647,6 +671,7 @@
"settingsViewerCustomizeBottomAppBarTitle",
"settingsShowDateInAlbumTitle",
"settingsShowDateInAlbumDescription",
"settingsCollectionsCustomizeNavigationBarTitle",
"settingsImageEditTitle",
"settingsImageEditDescription",
"settingsEnhanceMaxResolutionTitle2",
@ -972,6 +997,8 @@
"alternativeSignIn",
"livePhotoTooltip",
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning",
"errorUnauthenticated",
"errorDisconnected",
"errorLocked",
@ -986,6 +1013,7 @@
"settingsMemoriesRangeValueText",
"settingsViewerCustomizeAppBarTitle",
"settingsViewerCustomizeBottomAppBarTitle",
"settingsCollectionsCustomizeNavigationBarTitle",
"settingsThemePrimaryColor",
"settingsThemeSecondaryColor",
"settingsThemePresets",
@ -1027,7 +1055,9 @@
"todayText",
"alternativeSignIn",
"livePhotoTooltip",
"dragAndDropRearrangeButtons"
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
],
"pt": [
@ -1036,6 +1066,7 @@
"settingsPersonProviderTitle",
"settingsViewerCustomizeAppBarTitle",
"settingsViewerCustomizeBottomAppBarTitle",
"settingsCollectionsCustomizeNavigationBarTitle",
"settingsSeedColorSystemColorDescription",
"settingsThemePrimaryColor",
"settingsThemeSecondaryColor",
@ -1091,12 +1122,15 @@
"todayText",
"alternativeSignIn",
"livePhotoTooltip",
"dragAndDropRearrangeButtons"
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
],
"ru": [
"settingsViewerCustomizeAppBarTitle",
"settingsViewerCustomizeBottomAppBarTitle",
"settingsCollectionsCustomizeNavigationBarTitle",
"settingsThemePrimaryColor",
"settingsThemeSecondaryColor",
"settingsThemePresets",
@ -1135,15 +1169,20 @@
"todayText",
"alternativeSignIn",
"livePhotoTooltip",
"dragAndDropRearrangeButtons"
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
],
"tr": [
"settingsViewerCustomizeAppBarTitle",
"settingsViewerCustomizeBottomAppBarTitle",
"settingsCollectionsCustomizeNavigationBarTitle",
"alternativeSignIn",
"livePhotoTooltip",
"dragAndDropRearrangeButtons"
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
],
"zh": [
@ -1152,6 +1191,7 @@
"settingsMemoriesRangeValueText",
"settingsViewerCustomizeAppBarTitle",
"settingsViewerCustomizeBottomAppBarTitle",
"settingsCollectionsCustomizeNavigationBarTitle",
"settingsSeedColorDescription",
"settingsSeedColorSystemColorDescription",
"settingsThemePrimaryColor",
@ -1218,7 +1258,9 @@
"todayText",
"alternativeSignIn",
"livePhotoTooltip",
"dragAndDropRearrangeButtons"
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
],
"zh_Hant": [
@ -1238,6 +1280,7 @@
"settingsMemoriesRangeValueText",
"settingsViewerCustomizeAppBarTitle",
"settingsViewerCustomizeBottomAppBarTitle",
"settingsCollectionsCustomizeNavigationBarTitle",
"settingsImageEditTitle",
"settingsImageEditDescription",
"settingsEnhanceMaxResolutionTitle2",
@ -1387,6 +1430,8 @@
"todayText",
"alternativeSignIn",
"livePhotoTooltip",
"dragAndDropRearrangeButtons"
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
]
}

View file

@ -11,3 +11,34 @@ class ValueStreamBuilder<T> extends StreamBuilder<T> {
initialData: stream?.value,
);
}
class ValueStreamBuilderEx<T> extends StreamBuilder<T> {
ValueStreamBuilderEx({
super.key,
ValueStream<T>? stream,
required StreamWidgetBuilder builder,
}) : super(
stream: stream,
initialData: stream?.value,
builder: builder.snapshotBuilder ??
(context, snapshot) {
return builder.valueBuilder!(context, snapshot.requireData);
},
);
}
class StreamWidgetBuilder<T> {
const StreamWidgetBuilder._({
this.snapshotBuilder,
this.valueBuilder,
});
const StreamWidgetBuilder.snapshot(AsyncWidgetBuilder<T> builder)
: this._(snapshotBuilder: builder);
const StreamWidgetBuilder.value(
Widget Function(BuildContext context, T value) builder)
: this._(valueBuilder: builder);
final AsyncWidgetBuilder<T>? snapshotBuilder;
final Widget Function(BuildContext context, T value)? valueBuilder;
}

View file

@ -0,0 +1,118 @@
import 'dart:async';
import 'package:flutter/material.dart';
class FadeOutListContainer extends StatefulWidget {
const FadeOutListContainer({
super.key,
required this.scrollController,
required this.child,
});
@override
State<StatefulWidget> createState() => _FadeOutListContainerState();
final ScrollController scrollController;
final Widget child;
}
class _FadeOutListContainerState extends State<FadeOutListContainer> {
@override
void initState() {
super.initState();
widget.scrollController.addListener(_onScrollEvent);
_ensureUpdateButtonScroll();
}
@override
void dispose() {
widget.scrollController.removeListener(_onScrollEvent);
super.dispose();
}
@override
Widget build(BuildContext context) {
return ShaderMask(
shaderCallback: (rect) {
final colors = <Color>[];
final stops = <double>[];
if (_hasLeftContent) {
colors.addAll([Colors.white, Colors.transparent]);
stops.addAll([0, .1]);
} else {
colors.add(Colors.transparent);
stops.add(0);
}
if (_hasRightContent) {
colors.addAll([Colors.transparent, Colors.white]);
stops.addAll([.9, 1]);
} else {
colors.add(Colors.transparent);
stops.add(1);
}
return LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: colors,
stops: stops,
).createShader(rect);
},
blendMode: BlendMode.dstOut,
child: widget.child,
);
}
void _onScrollEvent() {
_updateButtonScroll(widget.scrollController.position);
}
bool _updateButtonScroll(ScrollPosition pos) {
if (!pos.hasContentDimensions || !pos.hasPixels) {
return false;
}
if (pos.pixels <= pos.minScrollExtent) {
if (_hasLeftContent) {
setState(() {
_hasLeftContent = false;
});
}
} else {
if (!_hasLeftContent) {
setState(() {
_hasLeftContent = true;
});
}
}
if (pos.pixels >= pos.maxScrollExtent) {
if (_hasRightContent) {
setState(() {
_hasRightContent = false;
});
}
} else {
if (!_hasRightContent) {
setState(() {
_hasRightContent = true;
});
}
}
_hasFirstScrollUpdate = true;
return true;
}
void _ensureUpdateButtonScroll() {
if (_hasFirstScrollUpdate || !mounted) {
return;
}
if (widget.scrollController.hasClients) {
if (_updateButtonScroll(widget.scrollController.position)) {
return;
}
}
Timer(const Duration(milliseconds: 100), _ensureUpdateButtonScroll);
}
var _hasFirstScrollUpdate = false;
var _hasLeftContent = false;
var _hasRightContent = false;
}

View file

@ -20,6 +20,7 @@ class HomeSliverAppBar extends StatelessWidget {
this.menuActions,
this.onSelectedMenuActions,
this.isShowProgressIcon = false,
this.bottom,
});
@override
@ -39,6 +40,7 @@ class HomeSliverAppBar extends StatelessWidget {
blurFilter: Theme.of(context).appBarBlurFilter,
floating: true,
automaticallyImplyLeading: false,
bottom: bottom,
actions: [
...actions ?? [],
if (menuActions?.isNotEmpty == true)
@ -70,6 +72,7 @@ class HomeSliverAppBar extends StatelessWidget {
/// Screen specific action buttons
final List<Widget>? actions;
final PreferredSizeWidget? bottom;
/// Screen specific actions under the overflow menu. The value of each item
/// much >= 0

View file

@ -21,10 +21,12 @@ import 'package:nc_photos/entity/collection/content_provider/album.dart';
import 'package:nc_photos/entity/collection/content_provider/nc_album.dart';
import 'package:nc_photos/entity/collection/util.dart' as collection_util;
import 'package:nc_photos/exception_event.dart';
import 'package:nc_photos/exception_util.dart';
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/np_api_util.dart';
import 'package:nc_photos/platform/features.dart' as features;
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/stream_util.dart';
import 'package:nc_photos/theme.dart';
import 'package:nc_photos/theme/dimension.dart';
import 'package:nc_photos/widget/album_importer.dart';
@ -32,6 +34,7 @@ import 'package:nc_photos/widget/archive_browser.dart';
import 'package:nc_photos/widget/collection_browser.dart';
import 'package:nc_photos/widget/collection_grid_item.dart';
import 'package:nc_photos/widget/enhanced_photo_browser.dart';
import 'package:nc_photos/widget/fade_out_list.dart';
import 'package:nc_photos/widget/handler/double_tap_exit_handler.dart';
import 'package:nc_photos/widget/home_app_bar.dart';
import 'package:nc_photos/widget/navigation_bar_blur_filter.dart';
@ -49,6 +52,8 @@ 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/nav_bar_buttons.dart';
part 'home_collections/navigation_bar.dart';
part 'home_collections/state_event.dart';
part 'home_collections/type.dart';
part 'home_collections/view.dart';
@ -138,41 +143,6 @@ class _WrappedHomeCollectionsState extends State<_WrappedHomeCollections>
? const _AppBar()
: const _SelectionAppBar(),
),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 8),
sliver: _BlocBuilder(
buildWhen: (previous, current) =>
previous.selectedItems.isEmpty !=
current.selectedItems.isEmpty,
builder: (context, state) => _ButtonGrid(
account: _bloc.account,
isEnabled: state.selectedItems.isEmpty,
onSharingPressed: () {
Navigator.of(context).pushNamed(
SharingBrowser.routeName,
arguments: SharingBrowserArguments(_bloc.account));
},
onEnhancedPhotosPressed: () {
Navigator.of(context).pushNamed(
EnhancedPhotoBrowser.routeName,
arguments:
const EnhancedPhotoBrowserArguments(null));
},
onArchivePressed: () {
Navigator.of(context)
.pushNamed(ArchiveBrowser.routeName);
},
onTrashbinPressed: () {
Navigator.of(context).pushNamed(
TrashbinBrowser.routeName,
arguments: TrashbinBrowserArguments(_bloc.account));
},
onNewCollectionPressed: () {
_onNewCollectionPressed(context);
},
),
),
),
const SliverToBoxAdapter(
child: SizedBox(height: 8),
),
@ -241,36 +211,6 @@ class _WrappedHomeCollectionsState extends State<_WrappedHomeCollections>
}
}
Future<void> _onNewCollectionPressed(BuildContext context) async {
try {
final collection = await showDialog<Collection>(
context: context,
builder: (_) => NewCollectionDialog(
account: _bloc.account,
),
);
if (collection == null) {
return;
}
// Right now we don't have a way to add photos inside the
// CollectionBrowser, eventually we should add that and remove this
// branching
if (collection.isDynamicCollection) {
// open the newly created collection
unawaited(Navigator.of(context).pushNamed(
CollectionBrowser.routeName,
arguments: CollectionBrowserArguments(collection),
));
}
} catch (e, stacktrace) {
_log.shout("[_onNewCollectionPressed] Failed", e, stacktrace);
SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global().createCollectionFailureNotification),
duration: k.snackBarDurationNormal,
));
}
}
Future<bool> _onBackButtonPressed(BuildContext context) async {
if (context.state.selectedItems.isEmpty) {
return DoubleTapExitHandler()();
@ -287,6 +227,7 @@ typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
typedef _BlocListener = BlocListener<_Bloc, _State>;
// typedef _BlocListenerT<T> = BlocListenerT<_Bloc, _State, T>;
typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
typedef _Emitter = Emitter<_State>;
extension on BuildContext {
_Bloc get bloc => read<_Bloc>();

View file

@ -20,6 +20,7 @@ abstract class $_StateCopyWithWorker {
List<_Item>? transformedItems,
Set<_Item>? selectedItems,
Map<String, int>? itemCounts,
List<PrefHomeCollectionsNavButton>? navBarButtons,
ExceptionEvent? error,
ExceptionEvent? removeError});
}
@ -35,6 +36,7 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
dynamic transformedItems,
dynamic selectedItems,
dynamic itemCounts,
dynamic navBarButtons,
dynamic error = copyWithNull,
dynamic removeError = copyWithNull}) {
return _State(
@ -45,6 +47,8 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
transformedItems as List<_Item>? ?? that.transformedItems,
selectedItems: selectedItems as Set<_Item>? ?? that.selectedItems,
itemCounts: itemCounts as Map<String, int>? ?? that.itemCounts,
navBarButtons: navBarButtons as List<PrefHomeCollectionsNavButton>? ??
that.navBarButtons,
error: error == copyWithNull ? that.error : error as ExceptionEvent?,
removeError: removeError == copyWithNull
? that.removeError
@ -85,6 +89,13 @@ extension _$_BlocNpLog on _Bloc {
static final log = Logger("widget.home_collections._Bloc");
}
extension _$_NavBarNewButtonNpLog on _NavBarNewButton {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("widget.home_collections._NavBarNewButton");
}
extension _$_ItemNpLog on _Item {
// ignore: unused_element
Logger get _log => log;
@ -99,7 +110,7 @@ extension _$_ItemNpLog on _Item {
extension _$_StateToString on _State {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_State {collections: [length: ${collections.length}], sort: ${sort.name}, isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, itemCounts: {length: ${itemCounts.length}}, error: $error, removeError: $removeError}";
return "_State {collections: [length: ${collections.length}], sort: ${sort.name}, isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, itemCounts: {length: ${itemCounts.length}}, navBarButtons: [length: ${navBarButtons.length}], error: $error, removeError: $removeError}";
}
}
@ -159,6 +170,13 @@ extension _$_SetItemCountToString on _SetItemCount {
}
}
extension _$_SetNavBarButtonsToString on _SetNavBarButtons {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetNavBarButtons {value: [length: ${value.length}]}";
}
}
extension _$_SetErrorToString on _SetError {
String _$toString() {
// ignore: unnecessary_string_interpolations

View file

@ -31,6 +31,10 @@ class _AppBar extends StatelessWidget {
break;
}
},
bottom: const PreferredSize(
preferredSize: Size.fromHeight(48),
child: _NavigationBar(),
),
),
);
}

View file

@ -9,6 +9,7 @@ class _Bloc extends Bloc<_Event, _State>
required this.prefController,
}) : super(_State.init(
sort: prefController.homeAlbumsSortValue,
navBarButtons: prefController.homeCollectionsNavBarButtonsValue,
)) {
on<_LoadCollections>(_onLoad);
on<_ReloadCollections>(_onReload);
@ -21,6 +22,8 @@ class _Bloc extends Bloc<_Event, _State>
on<_SetCollectionSort>(_onSetCollectionSort);
on<_SetItemCount>(_onSetItemCount);
on<_SetNavBarButtons>(_onSetNavBarButtons);
on<_SetError>(_onSetError);
_subscriptions.add(prefController.homeAlbumsSortChange.listen((event) {
@ -39,6 +42,10 @@ class _Bloc extends Bloc<_Event, _State>
}));
}
}));
_subscriptions
.add(prefController.homeCollectionsNavBarButtonsChange.listen((event) {
add(_SetNavBarButtons(event));
}));
}
@override
@ -134,6 +141,11 @@ class _Bloc extends Bloc<_Event, _State>
emit(state.copyWith(itemCounts: next));
}
void _onSetNavBarButtons(_SetNavBarButtons ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(navBarButtons: ev.value));
}
void _onSetError(_SetError ev, Emitter<_State> emit) {
_log.info(ev);
emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace)));

View file

@ -0,0 +1,244 @@
part of '../home_collections.dart';
class HomeCollectionsNavBarButton extends StatelessWidget {
const HomeCollectionsNavBarButton({
super.key,
required this.icon,
required this.label,
required this.isMinimized,
this.isShowIndicator = false,
this.isEnabled = true,
this.isUseTooltipWhenMinimized = true,
this.onPressed,
});
@override
Widget build(BuildContext context) {
if (isMinimized) {
return IconButtonTheme(
data: const IconButtonThemeData(
style: ButtonStyle(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
child: IconButton.outlined(
icon: Stack(
children: [
IconTheme(
data: IconThemeData(
size: 18,
color: Theme.of(context).colorScheme.secondary,
),
child: icon,
),
if (isShowIndicator)
const Positioned(
right: 2,
top: 2,
child: _NavBarButtonIndicator(),
),
],
),
tooltip: isUseTooltipWhenMinimized ? label : null,
onPressed: isEnabled ? onPressed : null,
),
);
} else {
return Theme(
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
child: ActionChip(
avatar: icon,
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(label),
if (isShowIndicator) ...const [
SizedBox(width: 4),
_NavBarButtonIndicator(),
],
],
),
onPressed: isEnabled ? onPressed : null,
),
);
}
}
final Widget icon;
final String label;
final bool isMinimized;
final bool isShowIndicator;
final bool isEnabled;
final bool isUseTooltipWhenMinimized;
final VoidCallback? onPressed;
}
class _NavBarButton extends StatelessWidget {
const _NavBarButton({
required this.icon,
required this.label,
required this.isMinimized,
this.isShowIndicator = false,
this.onPressed,
});
@override
Widget build(BuildContext context) {
return _BlocSelector(
selector: (state) => state.selectedItems.isEmpty,
builder: (context, isEnabled) => HomeCollectionsNavBarButton(
icon: icon,
label: label,
isMinimized: isMinimized,
isShowIndicator: isShowIndicator,
isEnabled: isEnabled,
onPressed: onPressed,
),
);
}
final Widget icon;
final String label;
final bool isMinimized;
final bool isShowIndicator;
final VoidCallback? onPressed;
}
@npLog
class _NavBarNewButton extends StatelessWidget {
const _NavBarNewButton();
@override
Widget build(BuildContext context) {
return _NavBarButton(
icon: const Icon(Icons.add_outlined),
label: L10n.global().createCollectionTooltip,
isMinimized: true,
onPressed: () async {
try {
final collection = await showDialog<Collection>(
context: context,
builder: (_) => NewCollectionDialog(
account: context.bloc.account,
),
);
if (collection == null) {
return;
}
// Right now we don't have a way to add photos inside the
// CollectionBrowser, eventually we should add that and remove this
// branching
if (collection.isDynamicCollection) {
// open the newly created collection
unawaited(Navigator.of(context).pushNamed(
CollectionBrowser.routeName,
arguments: CollectionBrowserArguments(collection),
));
}
} catch (e, stacktrace) {
_log.shout("[build] Uncaught exception", e, stacktrace);
context.addEvent(_SetError(AppMessageException(
L10n.global().createCollectionFailureNotification)));
}
},
);
}
}
class _NavBarSharingButton extends StatelessWidget {
const _NavBarSharingButton({
required this.isMinimized,
});
@override
Widget build(BuildContext context) {
return ValueStreamBuilderEx(
stream: context
.read<AccountController>()
.accountPrefController
.hasNewSharedAlbum,
builder: StreamWidgetBuilder.value(
(context, hasNewSharedAlbum) => _NavBarButton(
icon: const Icon(Icons.share_outlined),
label: L10n.global().collectionSharingLabel,
isMinimized: isMinimized,
isShowIndicator: hasNewSharedAlbum,
onPressed: () {
Navigator.of(context).pushNamed(
SharingBrowser.routeName,
arguments: SharingBrowserArguments(context.bloc.account),
);
},
),
),
);
}
final bool isMinimized;
}
class _NavBarEditedButton extends StatelessWidget {
const _NavBarEditedButton({
required this.isMinimized,
});
@override
Widget build(BuildContext context) {
return _NavBarButton(
icon: const Icon(Icons.auto_fix_high_outlined),
label: L10n.global().collectionEditedPhotosLabel,
isMinimized: isMinimized,
onPressed: () {
Navigator.of(context).pushNamed(
EnhancedPhotoBrowser.routeName,
arguments: const EnhancedPhotoBrowserArguments(null),
);
},
);
}
final bool isMinimized;
}
class _NavBarArchiveButton extends StatelessWidget {
const _NavBarArchiveButton({
required this.isMinimized,
});
@override
Widget build(BuildContext context) {
return _NavBarButton(
icon: const Icon(Icons.archive_outlined),
label: L10n.global().albumArchiveLabel,
isMinimized: isMinimized,
onPressed: () {
Navigator.of(context).pushNamed(ArchiveBrowser.routeName);
},
);
}
final bool isMinimized;
}
class _NavBarTrashButton extends StatelessWidget {
const _NavBarTrashButton({
required this.isMinimized,
});
@override
Widget build(BuildContext context) {
return _NavBarButton(
icon: const Icon(Icons.delete_outlined),
label: L10n.global().albumTrashLabel,
isMinimized: isMinimized,
onPressed: () {
Navigator.of(context).pushNamed(
TrashbinBrowser.routeName,
arguments: TrashbinBrowserArguments(context.bloc.account),
);
},
);
}
final bool isMinimized;
}

View file

@ -0,0 +1,98 @@
part of '../home_collections.dart';
enum HomeCollectionsNavBarButtonType {
// the order must not be changed
sharing,
edited,
archive,
trash,
;
static HomeCollectionsNavBarButtonType fromValue(int value) =>
HomeCollectionsNavBarButtonType.values[value];
}
class _NavigationBar extends StatefulWidget {
const _NavigationBar();
@override
State<StatefulWidget> createState() => _NavigationBarState();
}
class _NavigationBarState extends State<_NavigationBar> {
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: 48,
child: Row(
children: [
Expanded(
child: FadeOutListContainer(
scrollController: _scrollController,
child: _BlocSelector(
selector: (state) => state.navBarButtons,
builder: (context, navBarButtons) {
final buttons = navBarButtons
.map((e) => _buildButton(context, e))
.nonNulls
.toList();
return ListView.separated(
controller: _scrollController,
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.only(left: 16),
itemCount: buttons.length,
itemBuilder: (context, i) => Center(
child: buttons[i],
),
separatorBuilder: (context, _) => const SizedBox(width: 12),
);
},
),
),
),
const SizedBox(width: 8),
const _NavBarNewButton(),
const SizedBox(width: 16),
],
),
);
}
Widget? _buildButton(BuildContext context, PrefHomeCollectionsNavButton btn) {
switch (btn.type) {
case HomeCollectionsNavBarButtonType.sharing:
return _NavBarSharingButton(isMinimized: btn.isMinimized);
case HomeCollectionsNavBarButtonType.edited:
return features.isSupportEnhancement
? _NavBarEditedButton(isMinimized: btn.isMinimized)
: null;
case HomeCollectionsNavBarButtonType.archive:
return _NavBarArchiveButton(isMinimized: btn.isMinimized);
case HomeCollectionsNavBarButtonType.trash:
return _NavBarTrashButton(isMinimized: btn.isMinimized);
}
}
final _scrollController = ScrollController();
}
class _NavBarButtonIndicator extends StatelessWidget {
const _NavBarButtonIndicator();
@override
Widget build(BuildContext context) {
return ClipOval(
child: Container(
width: 4,
height: 4,
color: Theme.of(context).colorScheme.error,
),
);
}
}

View file

@ -10,12 +10,14 @@ class _State {
required this.transformedItems,
required this.selectedItems,
required this.itemCounts,
required this.navBarButtons,
this.error,
required this.removeError,
});
factory _State.init({
required collection_util.CollectionSort sort,
required List<PrefHomeCollectionsNavButton> navBarButtons,
}) {
return _State(
collections: [],
@ -24,6 +26,7 @@ class _State {
transformedItems: [],
selectedItems: {},
itemCounts: {},
navBarButtons: navBarButtons,
removeError: null,
);
}
@ -38,6 +41,8 @@ class _State {
final Set<_Item> selectedItems;
final Map<String, int> itemCounts;
final List<PrefHomeCollectionsNavButton> navBarButtons;
final ExceptionEvent? error;
final ExceptionEvent? removeError;
}
@ -128,6 +133,16 @@ class _SetItemCount implements _Event {
final int value;
}
@toString
class _SetNavBarButtons implements _Event {
const _SetNavBarButtons(this.value);
@override
String toString() => _$toString();
final List<PrefHomeCollectionsNavButton> value;
}
@toString
class _SetError implements _Event {
const _SetError(this.error, [this.stackTrace]);

View file

@ -1,133 +1,5 @@
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: context
.read<AccountController>()
.accountPrefController
.hasNewSharedAlbumValue,
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,

View file

@ -8,6 +8,7 @@ import 'package:nc_photos/controller/pref_controller.dart';
import 'package:nc_photos/exception_event.dart';
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/widget/page_visibility_mixin.dart';
import 'package:nc_photos/widget/settings/collections_nav_bar_settings.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:to_string/to_string.dart';
@ -85,6 +86,17 @@ class _WrappedAlbumSettingsState extends State<_WrappedAlbumSettings>
);
},
),
ListTile(
title: Text(L10n.global()
.settingsCollectionsCustomizeNavigationBarTitle),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const CollectionsNavBarSettings(),
),
);
},
),
],
),
),

View file

@ -0,0 +1,122 @@
part of '../collections_nav_bar_settings.dart';
@npLog
class _Bloc extends Bloc<_Event, _State>
with BlocLogger, BlocForEachMixin<_Event, _State> {
_Bloc({
required this.prefController,
required this.isBottom,
}) : super(_State.init(
buttons: prefController.homeCollectionsNavBarButtonsValue,
)) {
on<_MoveButton>(_onMoveButton);
on<_RemoveButton>(_onRemoveButton);
on<_ToggleMinimized>(_onToggleMinimized);
on<_RevertDefault>(_onRevertDefault);
on<_SetError>(_onSetError);
}
@override
Future<void> close() {
for (final s in _subscriptions) {
s.cancel();
}
return super.close();
}
@override
String get tag => _log.fullName;
@override
void onError(Object error, StackTrace stackTrace) {
// we need this to prevent onError being triggered recursively
if (!isClosed && !_isHandlingError) {
_isHandlingError = true;
try {
add(_SetError(error, stackTrace));
} catch (_) {}
_isHandlingError = false;
}
super.onError(error, stackTrace);
}
void _onMoveButton(_MoveButton ev, _Emitter emit) {
_log.info(ev);
final pos = state.buttons.indexWhere((e) => e.type == ev.which);
final found = pos >= 0 ? state.buttons[pos] : null;
final insert = found ??
PrefHomeCollectionsNavButton(type: ev.which, isMinimized: false);
var result =
pos >= 0 ? state.buttons.removedAt(pos) : List.of(state.buttons);
if (ev.before == null && ev.after == null) {
// add at the beginning
emit(state.copyWith(buttons: result..insert(0, insert)));
return;
}
final target = (ev.before ?? ev.after)!;
if (ev.which == target) {
// dropping on itself, do nothing
return;
}
final targetPos = result.indexWhere((e) => e.type == target);
if (targetPos == -1) {
_log.severe("[_onMoveButton] Target not found: $target");
return;
}
if (ev.before != null) {
// insert before
result.insert(targetPos, insert);
} else {
// insert after
result.insert(targetPos + 1, insert);
}
_log.fine(
"[_onMoveButton] From ${state.buttons.toReadableString()} -> ${result.toReadableString()}");
emit(state.copyWith(buttons: result));
}
void _onRemoveButton(_RemoveButton ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(
buttons: state.buttons.removedWhere((e) => e.type == ev.value),
));
}
void _onToggleMinimized(_ToggleMinimized ev, _Emitter emit) {
_log.info(ev);
final result = List.of(state.buttons);
final pos = result.indexWhere((e) => e.type == ev.value);
if (pos == -1) {
// button not enabled
_log.severe(
"[_onToggleMinimized] Type not found in buttons: ${ev.value}");
return;
}
result[pos] = PrefHomeCollectionsNavButton(
type: ev.value,
isMinimized: !result[pos].isMinimized,
);
emit(state.copyWith(buttons: result));
}
Future<void> _onRevertDefault(_RevertDefault ev, _Emitter emit) async {
_log.info(ev);
await prefController.setHomeCollectionsNavBarButtons(null);
emit(state.copyWith(
buttons: prefController.homeCollectionsNavBarButtonsValue,
));
}
void _onSetError(_SetError ev, _Emitter emit) {
_log.info(ev);
emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace)));
}
final PrefController prefController;
final bool isBottom;
final _subscriptions = <StreamSubscription>[];
var _isHandlingError = false;
}

View file

@ -0,0 +1,125 @@
part of '../collections_nav_bar_settings.dart';
class _NewButton extends StatelessWidget {
const _NewButton();
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global().customizeButtonsUnsupportedWarning),
duration: k.snackBarDurationNormal,
));
},
onLongPress: () {
SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global().customizeButtonsUnsupportedWarning),
duration: k.snackBarDurationNormal,
));
},
child: AbsorbPointer(
absorbing: true,
child: HomeCollectionsNavBarButton(
icon: const Icon(Icons.add_outlined),
label: L10n.global().createCollectionTooltip,
isMinimized: true,
isUseTooltipWhenMinimized: false,
onPressed: () {},
),
),
);
}
}
class _SharingButton extends StatelessWidget {
const _SharingButton({
required this.isMinimized,
this.onPressed,
});
@override
Widget build(BuildContext context) {
return HomeCollectionsNavBarButton(
icon: const Icon(Icons.share_outlined),
label: L10n.global().collectionSharingLabel,
isMinimized: isMinimized,
isUseTooltipWhenMinimized: false,
onPressed: () {
onPressed?.call();
},
);
}
final bool isMinimized;
final VoidCallback? onPressed;
}
class _EditedButton extends StatelessWidget {
const _EditedButton({
required this.isMinimized,
this.onPressed,
});
@override
Widget build(BuildContext context) {
return HomeCollectionsNavBarButton(
icon: const Icon(Icons.auto_fix_high_outlined),
label: L10n.global().collectionEditedPhotosLabel,
isMinimized: isMinimized,
isUseTooltipWhenMinimized: false,
onPressed: () {
onPressed?.call();
},
);
}
final bool isMinimized;
final VoidCallback? onPressed;
}
class _ArchiveButton extends StatelessWidget {
const _ArchiveButton({
required this.isMinimized,
this.onPressed,
});
@override
Widget build(BuildContext context) {
return HomeCollectionsNavBarButton(
icon: const Icon(Icons.archive_outlined),
label: L10n.global().albumArchiveLabel,
isMinimized: isMinimized,
isUseTooltipWhenMinimized: false,
onPressed: () {
onPressed?.call();
},
);
}
final bool isMinimized;
final VoidCallback? onPressed;
}
class _TrashButton extends StatelessWidget {
const _TrashButton({
required this.isMinimized,
this.onPressed,
});
@override
Widget build(BuildContext context) {
return HomeCollectionsNavBarButton(
icon: const Icon(Icons.delete_outlined),
label: L10n.global().albumTrashLabel,
isMinimized: isMinimized,
isUseTooltipWhenMinimized: false,
onPressed: () {
onPressed?.call();
},
);
}
final bool isMinimized;
final VoidCallback? onPressed;
}

View file

@ -0,0 +1,106 @@
part of '../collections_nav_bar_settings.dart';
@genCopyWith
@toString
class _State {
const _State({
required this.buttons,
this.error,
});
factory _State.init({
required List<PrefHomeCollectionsNavButton> buttons,
}) {
return _State(
buttons: buttons,
);
}
@override
String toString() => _$toString();
final List<PrefHomeCollectionsNavButton> buttons;
final ExceptionEvent? error;
}
abstract class _Event {
const _Event();
}
@toString
class _Init implements _Event {
const _Init();
@override
String toString() => _$toString();
}
@toString
class _MoveButton implements _Event {
const _MoveButton._({
required this.which,
this.before,
this.after,
});
const _MoveButton.first({
required HomeCollectionsNavBarButtonType which,
}) : this._(which: which);
const _MoveButton.before({
required HomeCollectionsNavBarButtonType which,
required HomeCollectionsNavBarButtonType target,
}) : this._(which: which, before: target);
const _MoveButton.after({
required HomeCollectionsNavBarButtonType which,
required HomeCollectionsNavBarButtonType target,
}) : this._(which: which, after: target);
@override
String toString() => _$toString();
final HomeCollectionsNavBarButtonType which;
final HomeCollectionsNavBarButtonType? before;
final HomeCollectionsNavBarButtonType? after;
}
@toString
class _RemoveButton implements _Event {
const _RemoveButton(this.value);
@override
String toString() => _$toString();
final HomeCollectionsNavBarButtonType value;
}
@toString
class _ToggleMinimized implements _Event {
const _ToggleMinimized(this.value);
@override
String toString() => _$toString();
final HomeCollectionsNavBarButtonType value;
}
@toString
class _RevertDefault implements _Event {
const _RevertDefault();
@override
String toString() => _$toString();
}
@toString
class _SetError implements _Event {
const _SetError(this.error, [this.stackTrace]);
@override
String toString() => _$toString();
final Object error;
final StackTrace? stackTrace;
}

View file

@ -0,0 +1,216 @@
part of '../collections_nav_bar_settings.dart';
class _DemoView extends StatefulWidget {
const _DemoView();
@override
State<StatefulWidget> createState() => _DemoViewState();
}
class _DemoViewState extends State<_DemoView> {
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return _BlocSelector(
selector: (state) => state.buttons,
builder: (context, buttons) {
final navBar = SizedBox(
height: 48,
child: Row(
children: [
Expanded(
child: FadeOutListContainer(
scrollController: _scrollController,
child: ListView.builder(
controller: _scrollController,
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.only(left: 16 - 6),
itemCount: buttons.length,
itemBuilder: (context, i) {
final btn = buttons[i];
return my.Draggable<HomeCollectionsNavBarButtonType>(
data: btn.type,
feedback: _CandidateButtonDelegate(btn.type),
onDropBefore: (data) {
context.addEvent(_MoveButton.before(
which: data,
target: btn.type,
));
},
onDropAfter: (data) {
context.addEvent(_MoveButton.after(
which: data,
target: btn.type,
));
},
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: _DemoButtonDelegate(
btn.type,
isMinimized: btn.isMinimized,
),
),
),
);
},
),
),
),
const SizedBox(width: 8),
const _NewButton(),
const SizedBox(width: 16),
],
),
);
if (buttons.isEmpty) {
return DragTarget<HomeCollectionsNavBarButtonType>(
builder: (context, candidateData, rejectedData) => SizedBox(
height: 48,
child: Stack(
children: [
navBar,
IgnorePointer(
child: Opacity(
opacity: candidateData.isNotEmpty ? .35 : 0,
child: Container(
color: Theme.of(context).colorScheme.onSurface,
),
),
),
],
),
),
onAcceptWithDetails: (details) {
context.addEvent(_MoveButton.first(which: details.data));
},
);
} else {
return navBar;
}
},
);
}
final _scrollController = ScrollController();
}
class _DemoButtonDelegate extends StatelessWidget {
const _DemoButtonDelegate(
this.type, {
required this.isMinimized,
});
@override
Widget build(BuildContext context) {
switch (type) {
case HomeCollectionsNavBarButtonType.sharing:
return _SharingButton(
isMinimized: isMinimized,
onPressed: () {
context.addEvent(_ToggleMinimized(type));
},
);
case HomeCollectionsNavBarButtonType.edited:
return _EditedButton(
isMinimized: isMinimized,
onPressed: () {
context.addEvent(_ToggleMinimized(type));
},
);
case HomeCollectionsNavBarButtonType.archive:
return _ArchiveButton(
isMinimized: isMinimized,
onPressed: () {
context.addEvent(_ToggleMinimized(type));
},
);
case HomeCollectionsNavBarButtonType.trash:
return _TrashButton(
isMinimized: isMinimized,
onPressed: () {
context.addEvent(_ToggleMinimized(type));
},
);
}
}
final HomeCollectionsNavBarButtonType type;
final bool isMinimized;
}
class _CandidateGrid extends StatelessWidget {
const _CandidateGrid();
@override
Widget build(BuildContext context) {
return DragTarget<HomeCollectionsNavBarButtonType>(
builder: (context, candidateData, rejectedData) => Stack(
children: [
_BlocSelector(
selector: (state) => state.buttons,
builder: (context, buttons) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Wrap(
direction: Axis.horizontal,
spacing: 16,
runSpacing: 8,
children: HomeCollectionsNavBarButtonType.values
.where((e) => !buttons.any((b) => b.type == e))
.map((e) => my.Draggable<HomeCollectionsNavBarButtonType>(
data: e,
feedback: _CandidateButtonDelegate(e),
child: _CandidateButtonDelegate(e),
))
.toList(),
),
),
),
IgnorePointer(
child: Opacity(
opacity: candidateData.isNotEmpty ? .1 : 0,
child: Container(
color: Theme.of(context).colorScheme.onSurface,
),
),
),
],
),
onAcceptWithDetails: (details) {
context.addEvent(_RemoveButton(details.data));
},
onWillAcceptWithDetails: (details) {
// moving down
return context.state.buttons.any((e) => e.type == details.data);
},
);
}
}
class _CandidateButtonDelegate extends StatelessWidget {
const _CandidateButtonDelegate(this.type);
@override
Widget build(BuildContext context) {
switch (type) {
case HomeCollectionsNavBarButtonType.sharing:
return const _SharingButton(isMinimized: false);
case HomeCollectionsNavBarButtonType.edited:
return const _EditedButton(isMinimized: false);
case HomeCollectionsNavBarButtonType.archive:
return const _ArchiveButton(isMinimized: false);
case HomeCollectionsNavBarButtonType.trash:
return const _TrashButton(isMinimized: false);
}
}
final HomeCollectionsNavBarButtonType type;
}

View file

@ -0,0 +1,122 @@
import 'dart:async';
import 'package:copy_with/copy_with.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc_util.dart';
import 'package:nc_photos/controller/pref_controller.dart';
import 'package:nc_photos/exception_event.dart';
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/widget/draggable.dart' as my;
import 'package:nc_photos/widget/fade_out_list.dart';
import 'package:nc_photos/widget/home_collections.dart';
import 'package:nc_photos/widget/page_visibility_mixin.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_collection/np_collection.dart';
import 'package:to_string/to_string.dart';
part 'collections_nav_bar/bloc.dart';
part 'collections_nav_bar/buttons.dart';
part 'collections_nav_bar/state_event.dart';
part 'collections_nav_bar/view.dart';
part 'collections_nav_bar_settings.g.dart';
class CollectionsNavBarSettings extends StatelessWidget {
const CollectionsNavBarSettings({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => _Bloc(
isBottom: true,
prefController: context.read(),
),
child: const _WrappedCollectionsNavBarSettings(),
);
}
}
class _WrappedCollectionsNavBarSettings extends StatefulWidget {
const _WrappedCollectionsNavBarSettings();
@override
State<StatefulWidget> createState() =>
_WrappedCollectionsNavBarSettingsState();
}
@npLog
class _WrappedCollectionsNavBarSettingsState
extends State<_WrappedCollectionsNavBarSettings>
with RouteAware, PageVisibilityMixin {
@override
Widget build(BuildContext context) {
return PopScope(
canPop: true,
onPopInvoked: (_) {
final prefController = context.bloc.prefController;
final from = prefController.homeCollectionsNavBarButtonsValue;
final to = context.state.buttons;
if (!listEquals(from, to)) {
_log.info("[build] Updated: ${to.toReadableString()}");
prefController.setHomeCollectionsNavBarButtons(to);
}
},
child: Scaffold(
appBar: AppBar(
title: Text(
L10n.global().settingsCollectionsCustomizeNavigationBarTitle),
actions: [
TextButton(
onPressed: () {
context.addEvent(const _RevertDefault());
},
child: Text(L10n.global().defaultButtonLabel),
),
],
),
body: MultiBlocListener(
listeners: [
_BlocListener(
listenWhen: (previous, current) =>
previous.error != current.error,
listener: (context, state) {
if (state.error != null && isPageVisible()) {
SnackBarManager()
.showSnackBarForException(state.error!.error);
}
},
),
],
child: Column(
children: [
const _DemoView(),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child:
Text(L10n.global().customizeCollectionsNavBarDescription),
),
const Expanded(child: _CandidateGrid()),
],
),
),
),
);
}
}
// typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
typedef _BlocListener = BlocListener<_Bloc, _State>;
// typedef _BlocListenerT<T> = BlocListenerT<_Bloc, _State, T>;
typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
typedef _Emitter = Emitter<_State>;
extension on BuildContext {
_Bloc get bloc => read<_Bloc>();
_State get state => bloc.state;
void addEvent(_Event event) => bloc.add(event);
}

View file

@ -0,0 +1,110 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'collections_nav_bar_settings.dart';
// **************************************************************************
// CopyWithLintRuleGenerator
// **************************************************************************
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
// **************************************************************************
// CopyWithGenerator
// **************************************************************************
abstract class $_StateCopyWithWorker {
_State call(
{List<PrefHomeCollectionsNavButton>? buttons, ExceptionEvent? error});
}
class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
_$_StateCopyWithWorkerImpl(this.that);
@override
_State call({dynamic buttons, dynamic error = copyWithNull}) {
return _State(
buttons: buttons as List<PrefHomeCollectionsNavButton>? ?? that.buttons,
error: error == copyWithNull ? that.error : error as ExceptionEvent?);
}
final _State that;
}
extension $_StateCopyWith on _State {
$_StateCopyWithWorker get copyWith => _$copyWith;
$_StateCopyWithWorker get _$copyWith => _$_StateCopyWithWorkerImpl(this);
}
// **************************************************************************
// NpLogGenerator
// **************************************************************************
extension _$_WrappedCollectionsNavBarSettingsStateNpLog
on _WrappedCollectionsNavBarSettingsState {
// ignore: unused_element
Logger get _log => log;
static final log = Logger(
"widget.settings.collections_nav_bar_settings._WrappedCollectionsNavBarSettingsState");
}
extension _$_BlocNpLog on _Bloc {
// ignore: unused_element
Logger get _log => log;
static final log =
Logger("widget.settings.collections_nav_bar_settings._Bloc");
}
// **************************************************************************
// ToStringGenerator
// **************************************************************************
extension _$_StateToString on _State {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_State {buttons: [length: ${buttons.length}], error: $error}";
}
}
extension _$_InitToString on _Init {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_Init {}";
}
}
extension _$_MoveButtonToString on _MoveButton {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_MoveButton {which: ${which.name}, before: ${before == null ? null : "${before!.name}"}, after: ${after == null ? null : "${after!.name}"}}";
}
}
extension _$_RemoveButtonToString on _RemoveButton {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_RemoveButton {value: ${value.name}}";
}
}
extension _$_ToggleMinimizedToString on _ToggleMinimized {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_ToggleMinimized {value: ${value.name}}";
}
}
extension _$_RevertDefaultToString on _RevertDefault {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_RevertDefault {}";
}
}
extension _$_SetErrorToString on _SetError {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetError {error: $error, stackTrace: $stackTrace}";
}
}

View file

@ -15,6 +15,35 @@ class _DemoButton extends StatelessWidget {
final Widget icon;
}
class _DemoMoreButton extends StatelessWidget {
const _DemoMoreButton();
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global().customizeButtonsUnsupportedWarning),
duration: k.snackBarDurationNormal,
));
},
onLongPress: () {
SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global().customizeButtonsUnsupportedWarning),
duration: k.snackBarDurationNormal,
));
},
child: AbsorbPointer(
absorbing: true,
child: IconButton(
onPressed: () {},
icon: Icon(Icons.adaptive.more),
),
),
);
}
}
class _DemoLivePhotoButton extends StatelessWidget {
const _DemoLivePhotoButton();

View file

@ -41,13 +41,7 @@ class _DemoView extends StatelessWidget {
child: _DemoButtonDelegate(e),
),
)),
IgnorePointer(
ignoring: true,
child: IconButton(
onPressed: () {},
icon: Icon(Icons.adaptive.more),
),
),
const _DemoMoreButton(),
],
);
if (buttons.isEmpty) {

View file

@ -10,6 +10,7 @@ import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc_util.dart';
import 'package:nc_photos/controller/pref_controller.dart';
import 'package:nc_photos/exception_event.dart';
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart';
import 'package:nc_photos/widget/draggable.dart' as my;

View file

@ -88,4 +88,7 @@ extension ListExtension<T> on List<T> {
List<T> removed(T value) => toList()..remove(value);
List<T> removedAt(int index) => toList()..removeAt(index);
List<T> removedWhere(bool Function(T element) test) =>
toList()..removeWhere(test);
}