mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-03-25 00:14:42 +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/language_util.dart';
|
||||||
import 'package:nc_photos/protected_page_handler.dart';
|
import 'package:nc_photos/protected_page_handler.dart';
|
||||||
import 'package:nc_photos/size.dart';
|
import 'package:nc_photos/size.dart';
|
||||||
|
import 'package:nc_photos/widget/home_collections.dart';
|
||||||
import 'package:nc_photos/widget/viewer.dart';
|
import 'package:nc_photos/widget/viewer.dart';
|
||||||
import 'package:np_codegen/np_codegen.dart';
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
import 'package:np_common/object_util.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_gps_map/np_gps_map.dart';
|
||||||
import 'package:np_string/np_string.dart';
|
import 'package:np_string/np_string.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
@ -261,6 +263,17 @@ class PrefController {
|
||||||
defaultValue: _viewerBottomAppBarButtonsDefault,
|
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>({
|
Future<bool> _set<T>({
|
||||||
required BehaviorSubject<T> controller,
|
required BehaviorSubject<T> controller,
|
||||||
required Future<bool> Function(Pref pref, T value) setter,
|
required Future<bool> Function(Pref pref, T value) setter,
|
||||||
|
@ -406,6 +419,14 @@ class PrefController {
|
||||||
@npSubjectAccessor
|
@npSubjectAccessor
|
||||||
late final _viewerBottomAppBarButtonsController = BehaviorSubject.seeded(
|
late final _viewerBottomAppBarButtonsController = BehaviorSubject.seeded(
|
||||||
pref.getViewerBottomAppBarButtons() ?? _viewerBottomAppBarButtonsDefault);
|
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 {
|
extension PrefControllerExtension on PrefController {
|
||||||
|
@ -561,6 +582,24 @@ const _viewerBottomAppBarButtonsDefault = [
|
||||||
ViewerAppBarButtonType.download,
|
ViewerAppBarButtonType.download,
|
||||||
ViewerAppBarButtonType.delete,
|
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
|
@npLog
|
||||||
// ignore: camel_case_types
|
// ignore: camel_case_types
|
||||||
|
|
|
@ -252,6 +252,18 @@ extension $PrefControllerNpSubjectAccessor on PrefController {
|
||||||
viewerBottomAppBarButtons.distinct().skip(1);
|
viewerBottomAppBarButtons.distinct().skip(1);
|
||||||
List<ViewerAppBarButtonType> get viewerBottomAppBarButtonsValue =>
|
List<ViewerAppBarButtonType> get viewerBottomAppBarButtonsValue =>
|
||||||
_viewerBottomAppBarButtonsController.value;
|
_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 {
|
extension $SecurePrefControllerNpSubjectAccessor on SecurePrefController {
|
||||||
|
|
|
@ -14,3 +14,24 @@ enum PrefMapDefaultRangeType {
|
||||||
|
|
||||||
final int value;
|
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());
|
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) {
|
MapCoord? _tryMapCoordFromJson(dynamic json) {
|
||||||
|
|
|
@ -117,6 +117,7 @@ enum PrefKey implements PrefKeyInterface {
|
||||||
mapDefaultCustomRange,
|
mapDefaultCustomRange,
|
||||||
viewerAppBarButtons,
|
viewerAppBarButtons,
|
||||||
viewerBottomAppBarButtons,
|
viewerBottomAppBarButtons,
|
||||||
|
homeCollectionsNavBarButtons,
|
||||||
;
|
;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -215,6 +216,8 @@ enum PrefKey implements PrefKeyInterface {
|
||||||
return "viewerAppBarButtons";
|
return "viewerAppBarButtons";
|
||||||
case PrefKey.viewerBottomAppBarButtons:
|
case PrefKey.viewerBottomAppBarButtons:
|
||||||
return "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/navigation_manager.dart';
|
||||||
import 'package:nc_photos/widget/trusted_cert_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
|
/// Convert an exception to a user-facing string
|
||||||
///
|
///
|
||||||
/// Typically used with SnackBar to show a proper error message
|
/// 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(", ")}",
|
"Failed to update files: ${exception.files.map((f) => f.filename).join(", ")}",
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
} else if (exception is AppMessageException) {
|
||||||
|
return (exception.message, null);
|
||||||
}
|
}
|
||||||
return (exception?.toString() ?? "Unknown error", null);
|
return (exception?.toString() ?? "Unknown error", null);
|
||||||
}
|
}
|
||||||
|
|
|
@ -360,6 +360,7 @@
|
||||||
"settingsViewerCustomizeBottomAppBarTitle": "Customize bottom app bar",
|
"settingsViewerCustomizeBottomAppBarTitle": "Customize bottom app bar",
|
||||||
"settingsShowDateInAlbumTitle": "Group photos by date",
|
"settingsShowDateInAlbumTitle": "Group photos by date",
|
||||||
"settingsShowDateInAlbumDescription": "Apply only when the album is sorted by time",
|
"settingsShowDateInAlbumDescription": "Apply only when the album is sorted by time",
|
||||||
|
"settingsCollectionsCustomizeNavigationBarTitle": "Customize navigation bar",
|
||||||
"settingsImageEditTitle": "Editor",
|
"settingsImageEditTitle": "Editor",
|
||||||
"@settingsImageEditTitle": {
|
"@settingsImageEditTitle": {
|
||||||
"description": "Include settings for image enhancements and the image editor"
|
"description": "Include settings for image enhancements and the image editor"
|
||||||
|
@ -1512,6 +1513,14 @@
|
||||||
"@dragAndDropRearrangeButtons": {
|
"@dragAndDropRearrangeButtons": {
|
||||||
"description": "Instruction to customize buttons layout"
|
"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": "Unauthenticated access. Please sign-in again if the problem continues",
|
||||||
"@errorUnauthenticated": {
|
"@errorUnauthenticated": {
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
"settingsViewerCustomizeBottomAppBarTitle",
|
"settingsViewerCustomizeBottomAppBarTitle",
|
||||||
"settingsShowDateInAlbumTitle",
|
"settingsShowDateInAlbumTitle",
|
||||||
"settingsShowDateInAlbumDescription",
|
"settingsShowDateInAlbumDescription",
|
||||||
|
"settingsCollectionsCustomizeNavigationBarTitle",
|
||||||
"settingsImageEditTitle",
|
"settingsImageEditTitle",
|
||||||
"settingsImageEditDescription",
|
"settingsImageEditDescription",
|
||||||
"settingsEnhanceMaxResolutionTitle2",
|
"settingsEnhanceMaxResolutionTitle2",
|
||||||
|
@ -265,6 +266,8 @@
|
||||||
"alternativeSignIn",
|
"alternativeSignIn",
|
||||||
"livePhotoTooltip",
|
"livePhotoTooltip",
|
||||||
"dragAndDropRearrangeButtons",
|
"dragAndDropRearrangeButtons",
|
||||||
|
"customizeCollectionsNavBarDescription",
|
||||||
|
"customizeButtonsUnsupportedWarning",
|
||||||
"errorUnauthenticated",
|
"errorUnauthenticated",
|
||||||
"errorDisconnected",
|
"errorDisconnected",
|
||||||
"errorLocked",
|
"errorLocked",
|
||||||
|
@ -278,17 +281,23 @@
|
||||||
"cs": [
|
"cs": [
|
||||||
"settingsViewerCustomizeAppBarTitle",
|
"settingsViewerCustomizeAppBarTitle",
|
||||||
"settingsViewerCustomizeBottomAppBarTitle",
|
"settingsViewerCustomizeBottomAppBarTitle",
|
||||||
|
"settingsCollectionsCustomizeNavigationBarTitle",
|
||||||
"alternativeSignIn",
|
"alternativeSignIn",
|
||||||
"livePhotoTooltip",
|
"livePhotoTooltip",
|
||||||
"dragAndDropRearrangeButtons"
|
"dragAndDropRearrangeButtons",
|
||||||
|
"customizeCollectionsNavBarDescription",
|
||||||
|
"customizeButtonsUnsupportedWarning"
|
||||||
],
|
],
|
||||||
|
|
||||||
"de": [
|
"de": [
|
||||||
"settingsViewerCustomizeAppBarTitle",
|
"settingsViewerCustomizeAppBarTitle",
|
||||||
"settingsViewerCustomizeBottomAppBarTitle",
|
"settingsViewerCustomizeBottomAppBarTitle",
|
||||||
|
"settingsCollectionsCustomizeNavigationBarTitle",
|
||||||
"alternativeSignIn",
|
"alternativeSignIn",
|
||||||
"livePhotoTooltip",
|
"livePhotoTooltip",
|
||||||
"dragAndDropRearrangeButtons"
|
"dragAndDropRearrangeButtons",
|
||||||
|
"customizeCollectionsNavBarDescription",
|
||||||
|
"customizeButtonsUnsupportedWarning"
|
||||||
],
|
],
|
||||||
|
|
||||||
"el": [
|
"el": [
|
||||||
|
@ -308,6 +317,7 @@
|
||||||
"settingsMemoriesRangeValueText",
|
"settingsMemoriesRangeValueText",
|
||||||
"settingsViewerCustomizeAppBarTitle",
|
"settingsViewerCustomizeAppBarTitle",
|
||||||
"settingsViewerCustomizeBottomAppBarTitle",
|
"settingsViewerCustomizeBottomAppBarTitle",
|
||||||
|
"settingsCollectionsCustomizeNavigationBarTitle",
|
||||||
"settingsImageEditTitle",
|
"settingsImageEditTitle",
|
||||||
"settingsImageEditDescription",
|
"settingsImageEditDescription",
|
||||||
"settingsEnhanceMaxResolutionTitle2",
|
"settingsEnhanceMaxResolutionTitle2",
|
||||||
|
@ -441,20 +451,26 @@
|
||||||
"todayText",
|
"todayText",
|
||||||
"alternativeSignIn",
|
"alternativeSignIn",
|
||||||
"livePhotoTooltip",
|
"livePhotoTooltip",
|
||||||
"dragAndDropRearrangeButtons"
|
"dragAndDropRearrangeButtons",
|
||||||
|
"customizeCollectionsNavBarDescription",
|
||||||
|
"customizeButtonsUnsupportedWarning"
|
||||||
],
|
],
|
||||||
|
|
||||||
"es": [
|
"es": [
|
||||||
"settingsViewerCustomizeAppBarTitle",
|
"settingsViewerCustomizeAppBarTitle",
|
||||||
"settingsViewerCustomizeBottomAppBarTitle",
|
"settingsViewerCustomizeBottomAppBarTitle",
|
||||||
|
"settingsCollectionsCustomizeNavigationBarTitle",
|
||||||
"alternativeSignIn",
|
"alternativeSignIn",
|
||||||
"livePhotoTooltip",
|
"livePhotoTooltip",
|
||||||
"dragAndDropRearrangeButtons"
|
"dragAndDropRearrangeButtons",
|
||||||
|
"customizeCollectionsNavBarDescription",
|
||||||
|
"customizeButtonsUnsupportedWarning"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fi": [
|
"fi": [
|
||||||
"settingsViewerCustomizeAppBarTitle",
|
"settingsViewerCustomizeAppBarTitle",
|
||||||
"settingsViewerCustomizeBottomAppBarTitle",
|
"settingsViewerCustomizeBottomAppBarTitle",
|
||||||
|
"settingsCollectionsCustomizeNavigationBarTitle",
|
||||||
"settingsThemePrimaryColor",
|
"settingsThemePrimaryColor",
|
||||||
"settingsThemeSecondaryColor",
|
"settingsThemeSecondaryColor",
|
||||||
"settingsThemePresets",
|
"settingsThemePresets",
|
||||||
|
@ -493,12 +509,15 @@
|
||||||
"todayText",
|
"todayText",
|
||||||
"alternativeSignIn",
|
"alternativeSignIn",
|
||||||
"livePhotoTooltip",
|
"livePhotoTooltip",
|
||||||
"dragAndDropRearrangeButtons"
|
"dragAndDropRearrangeButtons",
|
||||||
|
"customizeCollectionsNavBarDescription",
|
||||||
|
"customizeButtonsUnsupportedWarning"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fr": [
|
"fr": [
|
||||||
"settingsViewerCustomizeAppBarTitle",
|
"settingsViewerCustomizeAppBarTitle",
|
||||||
"settingsViewerCustomizeBottomAppBarTitle",
|
"settingsViewerCustomizeBottomAppBarTitle",
|
||||||
|
"settingsCollectionsCustomizeNavigationBarTitle",
|
||||||
"settingsThemePrimaryColor",
|
"settingsThemePrimaryColor",
|
||||||
"settingsThemeSecondaryColor",
|
"settingsThemeSecondaryColor",
|
||||||
"settingsThemePresets",
|
"settingsThemePresets",
|
||||||
|
@ -537,13 +556,16 @@
|
||||||
"todayText",
|
"todayText",
|
||||||
"alternativeSignIn",
|
"alternativeSignIn",
|
||||||
"livePhotoTooltip",
|
"livePhotoTooltip",
|
||||||
"dragAndDropRearrangeButtons"
|
"dragAndDropRearrangeButtons",
|
||||||
|
"customizeCollectionsNavBarDescription",
|
||||||
|
"customizeButtonsUnsupportedWarning"
|
||||||
],
|
],
|
||||||
|
|
||||||
"it": [
|
"it": [
|
||||||
"settingsPersonProviderTitle",
|
"settingsPersonProviderTitle",
|
||||||
"settingsViewerCustomizeAppBarTitle",
|
"settingsViewerCustomizeAppBarTitle",
|
||||||
"settingsViewerCustomizeBottomAppBarTitle",
|
"settingsViewerCustomizeBottomAppBarTitle",
|
||||||
|
"settingsCollectionsCustomizeNavigationBarTitle",
|
||||||
"settingsImageEditTitle",
|
"settingsImageEditTitle",
|
||||||
"settingsThemePrimaryColor",
|
"settingsThemePrimaryColor",
|
||||||
"settingsThemeSecondaryColor",
|
"settingsThemeSecondaryColor",
|
||||||
|
@ -586,7 +608,9 @@
|
||||||
"todayText",
|
"todayText",
|
||||||
"alternativeSignIn",
|
"alternativeSignIn",
|
||||||
"livePhotoTooltip",
|
"livePhotoTooltip",
|
||||||
"dragAndDropRearrangeButtons"
|
"dragAndDropRearrangeButtons",
|
||||||
|
"customizeCollectionsNavBarDescription",
|
||||||
|
"customizeButtonsUnsupportedWarning"
|
||||||
],
|
],
|
||||||
|
|
||||||
"nl": [
|
"nl": [
|
||||||
|
@ -647,6 +671,7 @@
|
||||||
"settingsViewerCustomizeBottomAppBarTitle",
|
"settingsViewerCustomizeBottomAppBarTitle",
|
||||||
"settingsShowDateInAlbumTitle",
|
"settingsShowDateInAlbumTitle",
|
||||||
"settingsShowDateInAlbumDescription",
|
"settingsShowDateInAlbumDescription",
|
||||||
|
"settingsCollectionsCustomizeNavigationBarTitle",
|
||||||
"settingsImageEditTitle",
|
"settingsImageEditTitle",
|
||||||
"settingsImageEditDescription",
|
"settingsImageEditDescription",
|
||||||
"settingsEnhanceMaxResolutionTitle2",
|
"settingsEnhanceMaxResolutionTitle2",
|
||||||
|
@ -972,6 +997,8 @@
|
||||||
"alternativeSignIn",
|
"alternativeSignIn",
|
||||||
"livePhotoTooltip",
|
"livePhotoTooltip",
|
||||||
"dragAndDropRearrangeButtons",
|
"dragAndDropRearrangeButtons",
|
||||||
|
"customizeCollectionsNavBarDescription",
|
||||||
|
"customizeButtonsUnsupportedWarning",
|
||||||
"errorUnauthenticated",
|
"errorUnauthenticated",
|
||||||
"errorDisconnected",
|
"errorDisconnected",
|
||||||
"errorLocked",
|
"errorLocked",
|
||||||
|
@ -986,6 +1013,7 @@
|
||||||
"settingsMemoriesRangeValueText",
|
"settingsMemoriesRangeValueText",
|
||||||
"settingsViewerCustomizeAppBarTitle",
|
"settingsViewerCustomizeAppBarTitle",
|
||||||
"settingsViewerCustomizeBottomAppBarTitle",
|
"settingsViewerCustomizeBottomAppBarTitle",
|
||||||
|
"settingsCollectionsCustomizeNavigationBarTitle",
|
||||||
"settingsThemePrimaryColor",
|
"settingsThemePrimaryColor",
|
||||||
"settingsThemeSecondaryColor",
|
"settingsThemeSecondaryColor",
|
||||||
"settingsThemePresets",
|
"settingsThemePresets",
|
||||||
|
@ -1027,7 +1055,9 @@
|
||||||
"todayText",
|
"todayText",
|
||||||
"alternativeSignIn",
|
"alternativeSignIn",
|
||||||
"livePhotoTooltip",
|
"livePhotoTooltip",
|
||||||
"dragAndDropRearrangeButtons"
|
"dragAndDropRearrangeButtons",
|
||||||
|
"customizeCollectionsNavBarDescription",
|
||||||
|
"customizeButtonsUnsupportedWarning"
|
||||||
],
|
],
|
||||||
|
|
||||||
"pt": [
|
"pt": [
|
||||||
|
@ -1036,6 +1066,7 @@
|
||||||
"settingsPersonProviderTitle",
|
"settingsPersonProviderTitle",
|
||||||
"settingsViewerCustomizeAppBarTitle",
|
"settingsViewerCustomizeAppBarTitle",
|
||||||
"settingsViewerCustomizeBottomAppBarTitle",
|
"settingsViewerCustomizeBottomAppBarTitle",
|
||||||
|
"settingsCollectionsCustomizeNavigationBarTitle",
|
||||||
"settingsSeedColorSystemColorDescription",
|
"settingsSeedColorSystemColorDescription",
|
||||||
"settingsThemePrimaryColor",
|
"settingsThemePrimaryColor",
|
||||||
"settingsThemeSecondaryColor",
|
"settingsThemeSecondaryColor",
|
||||||
|
@ -1091,12 +1122,15 @@
|
||||||
"todayText",
|
"todayText",
|
||||||
"alternativeSignIn",
|
"alternativeSignIn",
|
||||||
"livePhotoTooltip",
|
"livePhotoTooltip",
|
||||||
"dragAndDropRearrangeButtons"
|
"dragAndDropRearrangeButtons",
|
||||||
|
"customizeCollectionsNavBarDescription",
|
||||||
|
"customizeButtonsUnsupportedWarning"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ru": [
|
"ru": [
|
||||||
"settingsViewerCustomizeAppBarTitle",
|
"settingsViewerCustomizeAppBarTitle",
|
||||||
"settingsViewerCustomizeBottomAppBarTitle",
|
"settingsViewerCustomizeBottomAppBarTitle",
|
||||||
|
"settingsCollectionsCustomizeNavigationBarTitle",
|
||||||
"settingsThemePrimaryColor",
|
"settingsThemePrimaryColor",
|
||||||
"settingsThemeSecondaryColor",
|
"settingsThemeSecondaryColor",
|
||||||
"settingsThemePresets",
|
"settingsThemePresets",
|
||||||
|
@ -1135,15 +1169,20 @@
|
||||||
"todayText",
|
"todayText",
|
||||||
"alternativeSignIn",
|
"alternativeSignIn",
|
||||||
"livePhotoTooltip",
|
"livePhotoTooltip",
|
||||||
"dragAndDropRearrangeButtons"
|
"dragAndDropRearrangeButtons",
|
||||||
|
"customizeCollectionsNavBarDescription",
|
||||||
|
"customizeButtonsUnsupportedWarning"
|
||||||
],
|
],
|
||||||
|
|
||||||
"tr": [
|
"tr": [
|
||||||
"settingsViewerCustomizeAppBarTitle",
|
"settingsViewerCustomizeAppBarTitle",
|
||||||
"settingsViewerCustomizeBottomAppBarTitle",
|
"settingsViewerCustomizeBottomAppBarTitle",
|
||||||
|
"settingsCollectionsCustomizeNavigationBarTitle",
|
||||||
"alternativeSignIn",
|
"alternativeSignIn",
|
||||||
"livePhotoTooltip",
|
"livePhotoTooltip",
|
||||||
"dragAndDropRearrangeButtons"
|
"dragAndDropRearrangeButtons",
|
||||||
|
"customizeCollectionsNavBarDescription",
|
||||||
|
"customizeButtonsUnsupportedWarning"
|
||||||
],
|
],
|
||||||
|
|
||||||
"zh": [
|
"zh": [
|
||||||
|
@ -1152,6 +1191,7 @@
|
||||||
"settingsMemoriesRangeValueText",
|
"settingsMemoriesRangeValueText",
|
||||||
"settingsViewerCustomizeAppBarTitle",
|
"settingsViewerCustomizeAppBarTitle",
|
||||||
"settingsViewerCustomizeBottomAppBarTitle",
|
"settingsViewerCustomizeBottomAppBarTitle",
|
||||||
|
"settingsCollectionsCustomizeNavigationBarTitle",
|
||||||
"settingsSeedColorDescription",
|
"settingsSeedColorDescription",
|
||||||
"settingsSeedColorSystemColorDescription",
|
"settingsSeedColorSystemColorDescription",
|
||||||
"settingsThemePrimaryColor",
|
"settingsThemePrimaryColor",
|
||||||
|
@ -1218,7 +1258,9 @@
|
||||||
"todayText",
|
"todayText",
|
||||||
"alternativeSignIn",
|
"alternativeSignIn",
|
||||||
"livePhotoTooltip",
|
"livePhotoTooltip",
|
||||||
"dragAndDropRearrangeButtons"
|
"dragAndDropRearrangeButtons",
|
||||||
|
"customizeCollectionsNavBarDescription",
|
||||||
|
"customizeButtonsUnsupportedWarning"
|
||||||
],
|
],
|
||||||
|
|
||||||
"zh_Hant": [
|
"zh_Hant": [
|
||||||
|
@ -1238,6 +1280,7 @@
|
||||||
"settingsMemoriesRangeValueText",
|
"settingsMemoriesRangeValueText",
|
||||||
"settingsViewerCustomizeAppBarTitle",
|
"settingsViewerCustomizeAppBarTitle",
|
||||||
"settingsViewerCustomizeBottomAppBarTitle",
|
"settingsViewerCustomizeBottomAppBarTitle",
|
||||||
|
"settingsCollectionsCustomizeNavigationBarTitle",
|
||||||
"settingsImageEditTitle",
|
"settingsImageEditTitle",
|
||||||
"settingsImageEditDescription",
|
"settingsImageEditDescription",
|
||||||
"settingsEnhanceMaxResolutionTitle2",
|
"settingsEnhanceMaxResolutionTitle2",
|
||||||
|
@ -1387,6 +1430,8 @@
|
||||||
"todayText",
|
"todayText",
|
||||||
"alternativeSignIn",
|
"alternativeSignIn",
|
||||||
"livePhotoTooltip",
|
"livePhotoTooltip",
|
||||||
"dragAndDropRearrangeButtons"
|
"dragAndDropRearrangeButtons",
|
||||||
|
"customizeCollectionsNavBarDescription",
|
||||||
|
"customizeButtonsUnsupportedWarning"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,3 +11,34 @@ class ValueStreamBuilder<T> extends StreamBuilder<T> {
|
||||||
initialData: stream?.value,
|
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.menuActions,
|
||||||
this.onSelectedMenuActions,
|
this.onSelectedMenuActions,
|
||||||
this.isShowProgressIcon = false,
|
this.isShowProgressIcon = false,
|
||||||
|
this.bottom,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -39,6 +40,7 @@ class HomeSliverAppBar extends StatelessWidget {
|
||||||
blurFilter: Theme.of(context).appBarBlurFilter,
|
blurFilter: Theme.of(context).appBarBlurFilter,
|
||||||
floating: true,
|
floating: true,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
|
bottom: bottom,
|
||||||
actions: [
|
actions: [
|
||||||
...actions ?? [],
|
...actions ?? [],
|
||||||
if (menuActions?.isNotEmpty == true)
|
if (menuActions?.isNotEmpty == true)
|
||||||
|
@ -70,6 +72,7 @@ class HomeSliverAppBar extends StatelessWidget {
|
||||||
|
|
||||||
/// Screen specific action buttons
|
/// Screen specific action buttons
|
||||||
final List<Widget>? actions;
|
final List<Widget>? actions;
|
||||||
|
final PreferredSizeWidget? bottom;
|
||||||
|
|
||||||
/// Screen specific actions under the overflow menu. The value of each item
|
/// Screen specific actions under the overflow menu. The value of each item
|
||||||
/// much >= 0
|
/// 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/content_provider/nc_album.dart';
|
||||||
import 'package:nc_photos/entity/collection/util.dart' as collection_util;
|
import 'package:nc_photos/entity/collection/util.dart' as collection_util;
|
||||||
import 'package:nc_photos/exception_event.dart';
|
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/k.dart' as k;
|
||||||
import 'package:nc_photos/np_api_util.dart';
|
import 'package:nc_photos/np_api_util.dart';
|
||||||
import 'package:nc_photos/platform/features.dart' as features;
|
import 'package:nc_photos/platform/features.dart' as features;
|
||||||
import 'package:nc_photos/snack_bar_manager.dart';
|
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.dart';
|
||||||
import 'package:nc_photos/theme/dimension.dart';
|
import 'package:nc_photos/theme/dimension.dart';
|
||||||
import 'package:nc_photos/widget/album_importer.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_browser.dart';
|
||||||
import 'package:nc_photos/widget/collection_grid_item.dart';
|
import 'package:nc_photos/widget/collection_grid_item.dart';
|
||||||
import 'package:nc_photos/widget/enhanced_photo_browser.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/handler/double_tap_exit_handler.dart';
|
||||||
import 'package:nc_photos/widget/home_app_bar.dart';
|
import 'package:nc_photos/widget/home_app_bar.dart';
|
||||||
import 'package:nc_photos/widget/navigation_bar_blur_filter.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.g.dart';
|
||||||
part 'home_collections/app_bar.dart';
|
part 'home_collections/app_bar.dart';
|
||||||
part 'home_collections/bloc.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/state_event.dart';
|
||||||
part 'home_collections/type.dart';
|
part 'home_collections/type.dart';
|
||||||
part 'home_collections/view.dart';
|
part 'home_collections/view.dart';
|
||||||
|
@ -138,41 +143,6 @@ class _WrappedHomeCollectionsState extends State<_WrappedHomeCollections>
|
||||||
? const _AppBar()
|
? const _AppBar()
|
||||||
: const _SelectionAppBar(),
|
: 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(
|
const SliverToBoxAdapter(
|
||||||
child: SizedBox(height: 8),
|
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 {
|
Future<bool> _onBackButtonPressed(BuildContext context) async {
|
||||||
if (context.state.selectedItems.isEmpty) {
|
if (context.state.selectedItems.isEmpty) {
|
||||||
return DoubleTapExitHandler()();
|
return DoubleTapExitHandler()();
|
||||||
|
@ -287,6 +227,7 @@ typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
|
||||||
typedef _BlocListener = BlocListener<_Bloc, _State>;
|
typedef _BlocListener = BlocListener<_Bloc, _State>;
|
||||||
// typedef _BlocListenerT<T> = BlocListenerT<_Bloc, _State, T>;
|
// typedef _BlocListenerT<T> = BlocListenerT<_Bloc, _State, T>;
|
||||||
typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
|
typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
|
||||||
|
typedef _Emitter = Emitter<_State>;
|
||||||
|
|
||||||
extension on BuildContext {
|
extension on BuildContext {
|
||||||
_Bloc get bloc => read<_Bloc>();
|
_Bloc get bloc => read<_Bloc>();
|
||||||
|
|
|
@ -20,6 +20,7 @@ abstract class $_StateCopyWithWorker {
|
||||||
List<_Item>? transformedItems,
|
List<_Item>? transformedItems,
|
||||||
Set<_Item>? selectedItems,
|
Set<_Item>? selectedItems,
|
||||||
Map<String, int>? itemCounts,
|
Map<String, int>? itemCounts,
|
||||||
|
List<PrefHomeCollectionsNavButton>? navBarButtons,
|
||||||
ExceptionEvent? error,
|
ExceptionEvent? error,
|
||||||
ExceptionEvent? removeError});
|
ExceptionEvent? removeError});
|
||||||
}
|
}
|
||||||
|
@ -35,6 +36,7 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
|
||||||
dynamic transformedItems,
|
dynamic transformedItems,
|
||||||
dynamic selectedItems,
|
dynamic selectedItems,
|
||||||
dynamic itemCounts,
|
dynamic itemCounts,
|
||||||
|
dynamic navBarButtons,
|
||||||
dynamic error = copyWithNull,
|
dynamic error = copyWithNull,
|
||||||
dynamic removeError = copyWithNull}) {
|
dynamic removeError = copyWithNull}) {
|
||||||
return _State(
|
return _State(
|
||||||
|
@ -45,6 +47,8 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
|
||||||
transformedItems as List<_Item>? ?? that.transformedItems,
|
transformedItems as List<_Item>? ?? that.transformedItems,
|
||||||
selectedItems: selectedItems as Set<_Item>? ?? that.selectedItems,
|
selectedItems: selectedItems as Set<_Item>? ?? that.selectedItems,
|
||||||
itemCounts: itemCounts as Map<String, int>? ?? that.itemCounts,
|
itemCounts: itemCounts as Map<String, int>? ?? that.itemCounts,
|
||||||
|
navBarButtons: navBarButtons as List<PrefHomeCollectionsNavButton>? ??
|
||||||
|
that.navBarButtons,
|
||||||
error: error == copyWithNull ? that.error : error as ExceptionEvent?,
|
error: error == copyWithNull ? that.error : error as ExceptionEvent?,
|
||||||
removeError: removeError == copyWithNull
|
removeError: removeError == copyWithNull
|
||||||
? that.removeError
|
? that.removeError
|
||||||
|
@ -85,6 +89,13 @@ extension _$_BlocNpLog on _Bloc {
|
||||||
static final log = Logger("widget.home_collections._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 {
|
extension _$_ItemNpLog on _Item {
|
||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
Logger get _log => log;
|
Logger get _log => log;
|
||||||
|
@ -99,7 +110,7 @@ extension _$_ItemNpLog on _Item {
|
||||||
extension _$_StateToString on _State {
|
extension _$_StateToString on _State {
|
||||||
String _$toString() {
|
String _$toString() {
|
||||||
// ignore: unnecessary_string_interpolations
|
// 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 {
|
extension _$_SetErrorToString on _SetError {
|
||||||
String _$toString() {
|
String _$toString() {
|
||||||
// ignore: unnecessary_string_interpolations
|
// ignore: unnecessary_string_interpolations
|
||||||
|
|
|
@ -31,6 +31,10 @@ class _AppBar extends StatelessWidget {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
bottom: const PreferredSize(
|
||||||
|
preferredSize: Size.fromHeight(48),
|
||||||
|
child: _NavigationBar(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ class _Bloc extends Bloc<_Event, _State>
|
||||||
required this.prefController,
|
required this.prefController,
|
||||||
}) : super(_State.init(
|
}) : super(_State.init(
|
||||||
sort: prefController.homeAlbumsSortValue,
|
sort: prefController.homeAlbumsSortValue,
|
||||||
|
navBarButtons: prefController.homeCollectionsNavBarButtonsValue,
|
||||||
)) {
|
)) {
|
||||||
on<_LoadCollections>(_onLoad);
|
on<_LoadCollections>(_onLoad);
|
||||||
on<_ReloadCollections>(_onReload);
|
on<_ReloadCollections>(_onReload);
|
||||||
|
@ -21,6 +22,8 @@ class _Bloc extends Bloc<_Event, _State>
|
||||||
on<_SetCollectionSort>(_onSetCollectionSort);
|
on<_SetCollectionSort>(_onSetCollectionSort);
|
||||||
on<_SetItemCount>(_onSetItemCount);
|
on<_SetItemCount>(_onSetItemCount);
|
||||||
|
|
||||||
|
on<_SetNavBarButtons>(_onSetNavBarButtons);
|
||||||
|
|
||||||
on<_SetError>(_onSetError);
|
on<_SetError>(_onSetError);
|
||||||
|
|
||||||
_subscriptions.add(prefController.homeAlbumsSortChange.listen((event) {
|
_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
|
@override
|
||||||
|
@ -134,6 +141,11 @@ class _Bloc extends Bloc<_Event, _State>
|
||||||
emit(state.copyWith(itemCounts: next));
|
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) {
|
void _onSetError(_SetError ev, Emitter<_State> emit) {
|
||||||
_log.info(ev);
|
_log.info(ev);
|
||||||
emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace)));
|
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.transformedItems,
|
||||||
required this.selectedItems,
|
required this.selectedItems,
|
||||||
required this.itemCounts,
|
required this.itemCounts,
|
||||||
|
required this.navBarButtons,
|
||||||
this.error,
|
this.error,
|
||||||
required this.removeError,
|
required this.removeError,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory _State.init({
|
factory _State.init({
|
||||||
required collection_util.CollectionSort sort,
|
required collection_util.CollectionSort sort,
|
||||||
|
required List<PrefHomeCollectionsNavButton> navBarButtons,
|
||||||
}) {
|
}) {
|
||||||
return _State(
|
return _State(
|
||||||
collections: [],
|
collections: [],
|
||||||
|
@ -24,6 +26,7 @@ class _State {
|
||||||
transformedItems: [],
|
transformedItems: [],
|
||||||
selectedItems: {},
|
selectedItems: {},
|
||||||
itemCounts: {},
|
itemCounts: {},
|
||||||
|
navBarButtons: navBarButtons,
|
||||||
removeError: null,
|
removeError: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -38,6 +41,8 @@ class _State {
|
||||||
final Set<_Item> selectedItems;
|
final Set<_Item> selectedItems;
|
||||||
final Map<String, int> itemCounts;
|
final Map<String, int> itemCounts;
|
||||||
|
|
||||||
|
final List<PrefHomeCollectionsNavButton> navBarButtons;
|
||||||
|
|
||||||
final ExceptionEvent? error;
|
final ExceptionEvent? error;
|
||||||
final ExceptionEvent? removeError;
|
final ExceptionEvent? removeError;
|
||||||
}
|
}
|
||||||
|
@ -128,6 +133,16 @@ class _SetItemCount implements _Event {
|
||||||
final int value;
|
final int value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@toString
|
||||||
|
class _SetNavBarButtons implements _Event {
|
||||||
|
const _SetNavBarButtons(this.value);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
|
final List<PrefHomeCollectionsNavButton> value;
|
||||||
|
}
|
||||||
|
|
||||||
@toString
|
@toString
|
||||||
class _SetError implements _Event {
|
class _SetError implements _Event {
|
||||||
const _SetError(this.error, [this.stackTrace]);
|
const _SetError(this.error, [this.stackTrace]);
|
||||||
|
|
|
@ -1,133 +1,5 @@
|
||||||
part of '../home_collections.dart';
|
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 {
|
class _ItemView extends StatelessWidget {
|
||||||
const _ItemView({
|
const _ItemView({
|
||||||
required this.account,
|
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/exception_event.dart';
|
||||||
import 'package:nc_photos/snack_bar_manager.dart';
|
import 'package:nc_photos/snack_bar_manager.dart';
|
||||||
import 'package:nc_photos/widget/page_visibility_mixin.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:np_codegen/np_codegen.dart';
|
||||||
import 'package:to_string/to_string.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;
|
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 {
|
class _DemoLivePhotoButton extends StatelessWidget {
|
||||||
const _DemoLivePhotoButton();
|
const _DemoLivePhotoButton();
|
||||||
|
|
||||||
|
|
|
@ -41,13 +41,7 @@ class _DemoView extends StatelessWidget {
|
||||||
child: _DemoButtonDelegate(e),
|
child: _DemoButtonDelegate(e),
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
IgnorePointer(
|
const _DemoMoreButton(),
|
||||||
ignoring: true,
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: () {},
|
|
||||||
icon: Icon(Icons.adaptive.more),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
if (buttons.isEmpty) {
|
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/bloc_util.dart';
|
||||||
import 'package:nc_photos/controller/pref_controller.dart';
|
import 'package:nc_photos/controller/pref_controller.dart';
|
||||||
import 'package:nc_photos/exception_event.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/snack_bar_manager.dart';
|
||||||
import 'package:nc_photos/theme.dart';
|
import 'package:nc_photos/theme.dart';
|
||||||
import 'package:nc_photos/widget/draggable.dart' as my;
|
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> removed(T value) => toList()..remove(value);
|
||||||
|
|
||||||
List<T> removedAt(int index) => toList()..removeAt(index);
|
List<T> removedAt(int index) => toList()..removeAt(index);
|
||||||
|
|
||||||
|
List<T> removedWhere(bool Function(T element) test) =>
|
||||||
|
toList()..removeWhere(test);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue