mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 16:56:19 +01:00
Merge branch 'new-collections-nav-bar'
This commit is contained in:
commit
cecd0691fe
30 changed files with 1557 additions and 214 deletions
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
118
app/lib/widget/fade_out_list.dart
Normal file
118
app/lib/widget/fade_out_list.dart
Normal 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;
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -31,6 +31,10 @@ class _AppBar extends StatelessWidget {
|
|||
break;
|
||||
}
|
||||
},
|
||||
bottom: const PreferredSize(
|
||||
preferredSize: Size.fromHeight(48),
|
||||
child: _NavigationBar(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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)));
|
||||
|
|
244
app/lib/widget/home_collections/nav_bar_buttons.dart
Normal file
244
app/lib/widget/home_collections/nav_bar_buttons.dart
Normal 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;
|
||||
}
|
98
app/lib/widget/home_collections/navigation_bar.dart
Normal file
98
app/lib/widget/home_collections/navigation_bar.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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]);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
122
app/lib/widget/settings/collections_nav_bar/bloc.dart
Normal file
122
app/lib/widget/settings/collections_nav_bar/bloc.dart
Normal 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;
|
||||
}
|
125
app/lib/widget/settings/collections_nav_bar/buttons.dart
Normal file
125
app/lib/widget/settings/collections_nav_bar/buttons.dart
Normal 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;
|
||||
}
|
106
app/lib/widget/settings/collections_nav_bar/state_event.dart
Normal file
106
app/lib/widget/settings/collections_nav_bar/state_event.dart
Normal 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;
|
||||
}
|
216
app/lib/widget/settings/collections_nav_bar/view.dart
Normal file
216
app/lib/widget/settings/collections_nav_bar/view.dart
Normal 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;
|
||||
}
|
122
app/lib/widget/settings/collections_nav_bar_settings.dart
Normal file
122
app/lib/widget/settings/collections_nav_bar_settings.dart
Normal 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);
|
||||
}
|
110
app/lib/widget/settings/collections_nav_bar_settings.g.dart
Normal file
110
app/lib/widget/settings/collections_nav_bar_settings.g.dart
Normal 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}";
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue