make folder parent assignable
This commit is contained in:
@@ -62,8 +62,15 @@ class BookmarksRepository extends _$BookmarksRepository {
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
Future<void> editFolder({required String guid, required String title}) async {
|
||||
await _service.updateNode(guid, BookmarkInfo(title: title));
|
||||
Future<void> editFolder({
|
||||
required String guid,
|
||||
String? title,
|
||||
String? parentGuid,
|
||||
}) async {
|
||||
await _service.updateNode(
|
||||
guid,
|
||||
BookmarkInfo(title: title, parentGuid: parentGuid),
|
||||
);
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ final class BookmarksRepositoryProvider
|
||||
}
|
||||
|
||||
String _$bookmarksRepositoryHash() =>
|
||||
r'932441e926afcb1d60dc34b8a8756b7ad8fe0a27';
|
||||
r'c53414612bf1d1da824e1150ca4eca7bb7c3bec7';
|
||||
|
||||
abstract class _$BookmarksRepository extends $AsyncNotifier<BookmarkItem?> {
|
||||
FutureOr<BookmarkItem?> build();
|
||||
|
||||
+4
-105
@@ -17,19 +17,16 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:animated_tree_view/animated_tree_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/core/routing/routes.dart';
|
||||
import 'package:weblibre/features/geckoview/features/bookmarks/domain/entities/bookmark_item.dart';
|
||||
import 'package:weblibre/features/geckoview/features/bookmarks/domain/providers/bookmarks.dart';
|
||||
import 'package:weblibre/features/geckoview/features/bookmarks/domain/repositories/bookmarks.dart';
|
||||
import 'package:weblibre/features/geckoview/features/bookmarks/presentation/dialogs/delete_bookmark_dialog.dart';
|
||||
import 'package:weblibre/presentation/widgets/failure_widget.dart';
|
||||
import 'package:weblibre/features/geckoview/features/bookmarks/presentation/widgets/folder_tree_picker.dart';
|
||||
import 'package:weblibre/utils/form_validators.dart';
|
||||
import 'package:weblibre/utils/uri_parser.dart' as uri_parser;
|
||||
|
||||
@@ -45,11 +42,6 @@ class BookmarkEntryEditScreen extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||
final treeKey = useMemoized(() => GlobalKey<TreeViewState>());
|
||||
|
||||
final folderList = ref.watch(
|
||||
bookmarksProvider<BookmarkFolder>(BookmarkRoot.root.id),
|
||||
);
|
||||
|
||||
final nameTextController = useTextEditingController(
|
||||
text: initialInfo?.title ?? exisitingEntry?.title,
|
||||
@@ -153,102 +145,9 @@ class BookmarkEntryEditScreen extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('Folder', style: Theme.of(context).textTheme.labelMedium),
|
||||
folderList.when(
|
||||
skipLoadingOnReload: true,
|
||||
data: (list) {
|
||||
TreeNode<BookmarkFolder> addChildren(
|
||||
TreeNode<BookmarkFolder>? parent,
|
||||
BookmarkFolder item,
|
||||
) {
|
||||
final node = TreeNode(
|
||||
key: item.guid,
|
||||
data: item,
|
||||
parent: parent,
|
||||
);
|
||||
final targetNode = (parent?..add(node)) ?? node;
|
||||
|
||||
if (item.children != null) {
|
||||
for (final child in item.children!) {
|
||||
addChildren(node, child as BookmarkFolder);
|
||||
}
|
||||
}
|
||||
|
||||
return targetNode;
|
||||
}
|
||||
|
||||
final root = (list != null)
|
||||
? addChildren(null, list)
|
||||
: TreeNode<BookmarkFolder>.root();
|
||||
|
||||
return TreeView.simple(
|
||||
key: treeKey,
|
||||
tree: root,
|
||||
shrinkWrap: true,
|
||||
onTreeReady: (controller) {
|
||||
controller.expandAllChildren(root, recursive: true);
|
||||
},
|
||||
expansionIndicatorBuilder: (context, tree) =>
|
||||
ChevronIndicator.upDown(
|
||||
tree: tree,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16.0,
|
||||
horizontal: 12.0,
|
||||
),
|
||||
),
|
||||
builder: (context, item) {
|
||||
final isSelected = item.data?.guid == parentGuid.value;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 42.0),
|
||||
child: switch (item.data) {
|
||||
final BookmarkFolder folder => ListTile(
|
||||
key: ValueKey(folder.guid),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
selected: isSelected,
|
||||
leading: (item.isExpanded)
|
||||
? const Icon(MdiIcons.folderOpen)
|
||||
: const Icon(MdiIcons.folder),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isSelected) const Icon(Icons.check),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await BookmarkFolderAddRoute(
|
||||
parentGuid: folder.guid,
|
||||
).push(context);
|
||||
},
|
||||
icon: const Icon(MdiIcons.folderPlus),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: Text(folder.title),
|
||||
onTap: () {
|
||||
parentGuid.value = folder.guid;
|
||||
},
|
||||
),
|
||||
null => const SizedBox.shrink(),
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
error: (error, stackTrace) => Center(
|
||||
child: FailureWidget(
|
||||
title: 'Failed to load Bookmark Folders',
|
||||
exception: error,
|
||||
onRetry: () {
|
||||
// ignore: unused_result
|
||||
ref.refresh(
|
||||
bookmarksProvider<BookmarkFolder>(
|
||||
BookmarkRoot.root.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
loading: () => const SizedBox.shrink(),
|
||||
FolderTreePicker(
|
||||
selectedFolderGuid: parentGuid,
|
||||
entryGuid: BookmarkRoot.root.id,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (exisitingEntry != null)
|
||||
|
||||
+26
-2
@@ -25,6 +25,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/features/geckoview/features/bookmarks/domain/entities/bookmark_item.dart';
|
||||
import 'package:weblibre/features/geckoview/features/bookmarks/domain/repositories/bookmarks.dart';
|
||||
import 'package:weblibre/features/geckoview/features/bookmarks/presentation/dialogs/delete_folder_dialog.dart';
|
||||
import 'package:weblibre/features/geckoview/features/bookmarks/presentation/widgets/folder_tree_picker.dart';
|
||||
import 'package:weblibre/utils/form_validators.dart';
|
||||
|
||||
class BookmarkFolderEditScreen extends HookConsumerWidget {
|
||||
@@ -38,6 +39,14 @@ class BookmarkFolderEditScreen extends HookConsumerWidget {
|
||||
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||
final nameTextController = useTextEditingController(text: folder?.title);
|
||||
|
||||
final currentParentGuid = useState(
|
||||
parentGuid ?? folder?.parentGuid ?? BookmarkRoot.mobile.id,
|
||||
);
|
||||
|
||||
// Check if this is a bookmark root folder (these cannot be moved)
|
||||
final isBookmarkRoot =
|
||||
folder != null && bookmarkRootIds.contains(folder!.guid);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: (folder != null)
|
||||
@@ -52,7 +61,14 @@ class BookmarkFolderEditScreen extends HookConsumerWidget {
|
||||
.read(bookmarksRepositoryProvider.notifier)
|
||||
.editFolder(
|
||||
guid: folder!.guid,
|
||||
title: nameTextController.text,
|
||||
title: (nameTextController.text != folder!.title)
|
||||
? nameTextController.text
|
||||
: null,
|
||||
parentGuid:
|
||||
(!isBookmarkRoot &&
|
||||
currentParentGuid.value != folder!.parentGuid)
|
||||
? currentParentGuid.value
|
||||
: null,
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
@@ -62,7 +78,7 @@ class BookmarkFolderEditScreen extends HookConsumerWidget {
|
||||
await ref
|
||||
.read(bookmarksRepositoryProvider.notifier)
|
||||
.addFolder(
|
||||
parentGuid: parentGuid ?? BookmarkRoot.mobile.id,
|
||||
parentGuid: currentParentGuid.value,
|
||||
title: nameTextController.text,
|
||||
);
|
||||
|
||||
@@ -91,6 +107,14 @@ class BookmarkFolderEditScreen extends HookConsumerWidget {
|
||||
validator: validateRequired,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (!isBookmarkRoot) ...[
|
||||
FolderTreePicker(
|
||||
selectedFolderGuid: currentParentGuid,
|
||||
excludeFolderGuid: folder?.guid,
|
||||
entryGuid: BookmarkRoot.root.id,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
if (folder != null)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:animated_tree_view/animated_tree_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/core/routing/routes.dart';
|
||||
import 'package:weblibre/features/geckoview/features/bookmarks/domain/entities/bookmark_item.dart';
|
||||
import 'package:weblibre/features/geckoview/features/bookmarks/domain/providers/bookmarks.dart';
|
||||
import 'package:weblibre/presentation/widgets/failure_widget.dart';
|
||||
|
||||
/// A widget that displays a tree view of bookmark folders and allows the user
|
||||
/// to select a parent folder.
|
||||
///
|
||||
/// When editing a folder, pass [excludeFolderGuid] to prevent selecting the
|
||||
/// folder itself or its descendants as the parent (which would create a circular reference).
|
||||
class FolderTreePicker extends HookConsumerWidget {
|
||||
/// The currently selected folder GUID
|
||||
final ValueNotifier<String> selectedFolderGuid;
|
||||
|
||||
/// Optional folder GUID to exclude from the tree (along with its descendants).
|
||||
/// Used when editing a folder to prevent circular parent relationships.
|
||||
final String? excludeFolderGuid;
|
||||
|
||||
final String entryGuid;
|
||||
|
||||
const FolderTreePicker({
|
||||
required this.selectedFolderGuid,
|
||||
required this.entryGuid,
|
||||
this.excludeFolderGuid,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final treeKey = useMemoized(() => GlobalKey<TreeViewState>());
|
||||
|
||||
final folderList = ref.watch(bookmarksProvider<BookmarkFolder>(entryGuid));
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Folder', style: Theme.of(context).textTheme.labelMedium),
|
||||
folderList.when(
|
||||
skipLoadingOnReload: true,
|
||||
data: (list) {
|
||||
TreeNode<BookmarkFolder> addChildren(
|
||||
TreeNode<BookmarkFolder>? parent,
|
||||
BookmarkFolder item,
|
||||
) {
|
||||
final node = TreeNode(key: item.guid, data: item, parent: parent);
|
||||
final targetNode = (parent?..add(node)) ?? node;
|
||||
|
||||
if (item.children != null) {
|
||||
for (final child in item.children!) {
|
||||
// Skip the excluded folder and its descendants
|
||||
if (child is BookmarkFolder &&
|
||||
child.guid != excludeFolderGuid) {
|
||||
addChildren(node, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return targetNode;
|
||||
}
|
||||
|
||||
final root = (list != null)
|
||||
? addChildren(null, list)
|
||||
: TreeNode<BookmarkFolder>.root();
|
||||
|
||||
return TreeView.simple(
|
||||
key: treeKey,
|
||||
tree: root,
|
||||
shrinkWrap: true,
|
||||
showRootNode: entryGuid != BookmarkRoot.root.id,
|
||||
onTreeReady: (controller) {
|
||||
controller.expandAllChildren(root, recursive: true);
|
||||
},
|
||||
expansionIndicatorBuilder: (context, tree) =>
|
||||
ChevronIndicator.upDown(
|
||||
tree: tree,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16.0,
|
||||
horizontal: 12.0,
|
||||
),
|
||||
),
|
||||
builder: (context, item) {
|
||||
final isSelected = item.data?.guid == selectedFolderGuid.value;
|
||||
|
||||
// BookmarkRoot.root cannot be selected as a parent
|
||||
final isRootFolder = item.data?.guid == BookmarkRoot.root.id;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 42.0),
|
||||
child: switch (item.data) {
|
||||
final BookmarkFolder folder => ListTile(
|
||||
key: ValueKey(folder.guid),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
selected: isSelected,
|
||||
enabled: !isRootFolder,
|
||||
leading: (item.isExpanded)
|
||||
? const Icon(MdiIcons.folderOpen)
|
||||
: const Icon(MdiIcons.folder),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isSelected) const Icon(Icons.check),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await BookmarkFolderAddRoute(
|
||||
parentGuid: folder.guid,
|
||||
).push(context);
|
||||
},
|
||||
icon: const Icon(MdiIcons.folderPlus),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: Text(folder.title),
|
||||
onTap: !isRootFolder
|
||||
? () {
|
||||
selectedFolderGuid.value = folder.guid;
|
||||
}
|
||||
: null,
|
||||
),
|
||||
null => const SizedBox.shrink(),
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
error: (error, stackTrace) => Center(
|
||||
child: FailureWidget(
|
||||
title: 'Failed to load Bookmark Folders',
|
||||
exception: error,
|
||||
onRetry: () {
|
||||
ref.invalidate(bookmarksProvider<BookmarkFolder>(entryGuid));
|
||||
},
|
||||
),
|
||||
),
|
||||
loading: () => const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user