/* * SPDX-FileCopyrightText: 2019-2021 Vishesh Handa * * SPDX-License-Identifier: AGPL-3.0-or-later */ import 'package:flutter/material.dart'; import 'package:gitjournal/analytics/analytics.dart'; import 'package:gitjournal/app_router.dart'; import 'package:gitjournal/core/folder/filtered_notes_folder.dart'; import 'package:gitjournal/core/folder/notes_folder.dart'; import 'package:gitjournal/core/folder/notes_folder_fs.dart'; import 'package:gitjournal/core/folder/sorted_notes_folder.dart'; import 'package:gitjournal/core/folder/sorting_mode.dart'; import 'package:gitjournal/core/markdown/md_yaml_doc_codec.dart'; import 'package:gitjournal/core/note.dart'; import 'package:gitjournal/editors/common_types.dart'; import 'package:gitjournal/editors/note_editor.dart'; import 'package:gitjournal/folder_views/common.dart'; import 'package:gitjournal/folder_views/folder_view_configuration_dialog.dart'; import 'package:gitjournal/folder_views/folder_view_selection_dialog.dart'; import 'package:gitjournal/folder_views/standard_view.dart'; import 'package:gitjournal/l10n.dart'; import 'package:gitjournal/repository.dart'; import 'package:gitjournal/settings/settings.dart'; import 'package:gitjournal/utils/utils.dart'; import 'package:gitjournal/widgets/app_bar_menu_button.dart'; import 'package:gitjournal/widgets/app_drawer.dart'; import 'package:gitjournal/widgets/folder_selection_dialog.dart'; import 'package:gitjournal/widgets/new_note_nav_bar.dart'; import 'package:gitjournal/widgets/note_delete_dialog.dart'; import 'package:gitjournal/widgets/note_search_delegate.dart'; import 'package:gitjournal/widgets/sorting_mode_selection_dialog.dart'; import 'package:gitjournal/widgets/sync_button.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; enum DropDownChoices { SortingOptions, ViewOptions, } enum NoteSelectedExtraActions { MoveToFolder, } class FolderView extends StatefulWidget { final NotesFolder notesFolder; final Map newNoteExtraProps; const FolderView({ required this.notesFolder, this.newNoteExtraProps = const {}, }); @override _FolderViewState createState() => _FolderViewState(); } class _FolderViewState extends State { SortedNotesFolder? _sortedNotesFolder; SortedNotesFolder? _pinnedNotesFolder; FolderViewType _viewType = FolderViewType.Standard; var _headerType = StandardViewHeader.TitleGenerated; bool _showSummary = true; var _selectedNotes = []; bool get inSelectionMode => _selectedNotes.isNotEmpty; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) => _init()); } Future _init() async { _viewType = widget.notesFolder.config.defaultView.toFolderViewType(); _showSummary = widget.notesFolder.config.showNoteSummary; _headerType = widget.notesFolder.config.viewHeader; var otherNotesFolder = SortedNotesFolder( folder: await FilteredNotesFolder.load( widget.notesFolder, title: context.loc.widgetsFolderViewPinned, filter: (Note note) async => !note.pinned, ), sortingMode: widget.notesFolder.config.sortingMode, ); var pinnedFolder = SortedNotesFolder( folder: await FilteredNotesFolder.load( widget.notesFolder, title: context.loc.widgetsFolderViewPinned, filter: (Note note) async => note.pinned, ), sortingMode: widget.notesFolder.config.sortingMode, ); setState(() { _sortedNotesFolder = otherNotesFolder; _pinnedNotesFolder = pinnedFolder; }); } @override void dispose() { _sortedNotesFolder?.dispose(); _pinnedNotesFolder?.dispose(); super.dispose(); } @override void didUpdateWidget(FolderView oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.notesFolder != widget.notesFolder) { _init(); } } Widget _buildBody(BuildContext context) { if (_sortedNotesFolder == null) { return Container(); } var title = widget.notesFolder.publicName(context); if (inSelectionMode) { title = NumberFormat.compact().format(_selectedNotes.length); } var folderView = buildFolderView( viewType: _viewType, folder: _sortedNotesFolder!, emptyText: context.loc.screensFolderViewEmpty, header: _headerType, showSummary: _showSummary, noteTapped: _noteTapped, noteLongPressed: _noteLongPress, isNoteSelected: (n) => _selectedNotes.contains(n), ); Widget pinnedFolderView = const SizedBox(); if (_pinnedNotesFolder != null) { pinnedFolderView = buildFolderView( viewType: _viewType, folder: _pinnedNotesFolder!, emptyText: null, header: _headerType, showSummary: _showSummary, noteTapped: _noteTapped, noteLongPressed: _noteLongPress, isNoteSelected: (n) => _selectedNotes.contains(n), ); } var settings = context.watch(); final showButtomMenuBar = settings.bottomMenuBar; // So the FAB doesn't hide parts of the last entry if (!showButtomMenuBar) { folderView = SliverPadding( sliver: folderView, padding: const EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 48.0), ); } var backButton = IconButton( icon: const Icon(Icons.arrow_back), onPressed: _resetSelection, ); var havePinnedNotes = _pinnedNotesFolder != null ? !_pinnedNotesFolder!.isEmpty : false; return NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ SliverAppBar( title: Text(title), leading: inSelectionMode ? backButton : GJAppBarMenuButton(), actions: inSelectionMode ? _buildInSelectionNoteActions() : _buildNoteActions(), forceElevated: true, ), ]; }, floatHeaderSlivers: true, // Stupid scrollbar has a top padding otherwise // - from : https://stackoverflow.com/questions/64404873/remove-the-top-padding-from-scrollbar-when-wrapping-listview body: MediaQuery.removePadding( context: context, removeTop: true, child: Scrollbar( child: Builder(builder: (context) { var view = CustomScrollView(slivers: [ if (havePinnedNotes) _SliverHeader(text: context.loc.widgetsFolderViewPinned), if (havePinnedNotes) pinnedFolderView, if (havePinnedNotes) _SliverHeader(text: context.loc.widgetsFolderViewOthers), folderView, ]); if (settings.remoteSyncFrequency == RemoteSyncFrequency.Manual) { return view; } return RefreshIndicator( onRefresh: () => syncRepo(context), child: view, ); }), ), ), ); } void _noteLongPress(Note note) { var i = _selectedNotes.indexOf(note); if (i != -1) { setState(() { _selectedNotes.removeAt(i); }); } else { setState(() { _selectedNotes.add(note); }); } } void _noteTapped(Note note) { if (!inSelectionMode) { openNoteEditor(context, note, widget.notesFolder); return; } var i = _selectedNotes.indexOf(note); if (i != -1) { setState(() { _selectedNotes.removeAt(i); }); } else { setState(() { _selectedNotes.add(note); }); } } @override Widget build(BuildContext context) { var createButton = FloatingActionButton( key: const ValueKey("FAB"), onPressed: () => _newPost(widget.notesFolder.config.defaultEditor.toEditorType()), child: const Icon(Icons.add), ); var settings = context.watch(); final showButtomMenuBar = settings.bottomMenuBar; return Scaffold( body: Builder(builder: _buildBody), extendBody: true, drawer: AppDrawer(), floatingActionButton: createButton, floatingActionButtonLocation: showButtomMenuBar ? FloatingActionButtonLocation.endDocked : null, bottomNavigationBar: showButtomMenuBar ? NewNoteNavBar(onPressed: _newPost) : null, ); } Future _newPost(EditorType editorType) async { var settings = context.read(); var rootFolder = context.read(); var folder = widget.notesFolder; var fsFolder = folder.fsFolder as NotesFolderFS; var isVirtualFolder = folder.name != folder.fsFolder!.name; if (isVirtualFolder) { fsFolder = getFolderForEditor(settings, rootFolder, editorType); } if (editorType == EditorType.Journal) { if (settings.journalEditordefaultNewNoteFolderSpec.isNotEmpty) { var spec = settings.journalEditordefaultNewNoteFolderSpec; fsFolder = rootFolder.getFolderWithSpec(spec) ?? rootFolder; if (!isVirtualFolder) { showSnackbar( context, context.loc.settingsEditorsJournalDefaultFolderSelect(spec), ); } } if (settings.journalEditorSingleNote) { var note = await getTodayJournalEntry(fsFolder.rootFolder); if (note != null) { return openNoteEditor( context, note, fsFolder, editMode: true, ); } } } var routeType = SettingsEditorType.fromEditorType(editorType).toInternalString(); var extraProps = Map.from(widget.newNoteExtraProps); if (settings.customMetaData.isNotEmpty) { var map = MarkdownYAMLCodec.parseYamlText(settings.customMetaData); map.forEach((key, val) { extraProps[key] = val; }); } var route = newNoteRoute( NoteEditor.newNote( fsFolder, widget.notesFolder, editorType, newNoteExtraProps: extraProps, existingText: "", existingImages: const [], ), AppRoute.NewNotePrefix + routeType, ); await Navigator.push(context, route); ScaffoldMessenger.of(context).removeCurrentSnackBar(); } Future _sortButtonPressed() async { if (_sortedNotesFolder == null) { return; } var newSortingMode = await showDialog( context: context, builder: (BuildContext context) => SortingModeSelectionDialog(_sortedNotesFolder!.sortingMode), ); if (newSortingMode != null) { var folderConfig = _sortedNotesFolder!.config; folderConfig.sortingField = newSortingMode.field; folderConfig.sortingOrder = newSortingMode.order; folderConfig.save(); setState(() { _sortedNotesFolder!.changeSortingMode(newSortingMode); }); } } Future _configureViewButtonPressed() async { await showDialog( context: context, builder: _viewDialog, ); setState(() {}); } Widget _viewDialog(BuildContext context) { void headerTypeChanged(StandardViewHeader? newHeader) { if (newHeader == null) { return; } setState(() { _headerType = newHeader; }); var folderConfig = _sortedNotesFolder!.config; folderConfig.viewHeader = _headerType; folderConfig.save(); } void summaryChanged(bool newVal) { setState(() { _showSummary = newVal; }); var folderConfig = _sortedNotesFolder!.config; folderConfig.showNoteSummary = newVal; folderConfig.save(); } return FolderViewConfigurationDialog( headerType: _headerType, showSummary: _showSummary, onHeaderTypeChanged: headerTypeChanged, onShowSummaryChanged: summaryChanged, ); } Future _folderViewChooserSelected() async { var newViewType = await showDialog( context: context, builder: (BuildContext context) { return FolderViewSelectionDialog( viewType: _viewType, onViewChange: (vt) => Navigator.of(context).pop(vt), ); }, ); if (newViewType != null) { setState(() { _viewType = newViewType; }); var folderConfig = widget.notesFolder.config; folderConfig.defaultView = SettingsFolderViewType.fromFolderViewType(newViewType); folderConfig.save(); } } List _buildNoteActions() { final repo = context.watch(); var extraActions = PopupMenuButton( key: const ValueKey("PopupMenu"), onSelected: (DropDownChoices choice) { switch (choice) { case DropDownChoices.SortingOptions: _sortButtonPressed(); break; case DropDownChoices.ViewOptions: _configureViewButtonPressed(); break; } }, itemBuilder: (BuildContext context) => >[ PopupMenuItem( key: const ValueKey("SortingOptions"), value: DropDownChoices.SortingOptions, child: Text(context.loc.widgetsFolderViewSortingOptions), ), if (_viewType == FolderViewType.Standard) PopupMenuItem( key: const ValueKey("ViewOptions"), value: DropDownChoices.ViewOptions, child: Text(context.loc.widgetsFolderViewViewOptions), ), ], ); return [ IconButton( icon: const Icon(Icons.library_books), onPressed: _folderViewChooserSelected, key: const ValueKey("FolderViewSelector"), ), if (repo.remoteGitRepoConfigured) SyncButton(), IconButton( icon: const Icon(Icons.search), onPressed: () { logEvent(Event.SearchButtonPressed); showSearch( context: context, delegate: NoteSearchDelegate( _sortedNotesFolder!.notes, _viewType, ), ); }, ), extraActions, ]; } List _buildInSelectionNoteActions() { var extraActions = PopupMenuButton( key: const ValueKey("PopupMenu"), onSelected: (NoteSelectedExtraActions choice) { switch (choice) { case NoteSelectedExtraActions.MoveToFolder: _moveSelectedNotesToFolder(); break; } }, itemBuilder: (BuildContext context) => >[ PopupMenuItem( value: NoteSelectedExtraActions.MoveToFolder, child: Text(context.loc.widgetsFolderViewActionsMoveToFolder), ), ], ); return [ if (_selectedNotes.length == 1) IconButton( icon: const Icon(Icons.share), onPressed: () async { await shareNote(_selectedNotes.first); _resetSelection(); }, ), IconButton( icon: const Icon(Icons.delete), onPressed: _deleteSelectedNotes, ), extraActions, ]; } Future _deleteSelectedNotes() async { var settings = context.read(); var shouldDelete = true; if (settings.confirmDelete) { shouldDelete = (await showDialog( context: context, builder: (context) => NoteDeleteDialog(num: _selectedNotes.length), )) == true; } if (shouldDelete == true) { var repo = context.read(); repo.removeNotes(_selectedNotes); } _resetSelection(); } Future _moveSelectedNotesToFolder() async { var destFolder = await showDialog( context: context, builder: (context) => FolderSelectionDialog(), ); if (destFolder != null) { try { var repo = context.read(); await repo.moveNotes(_selectedNotes, destFolder); } catch (ex) { showErrorSnackbar(context, ex); } } _resetSelection(); } void _resetSelection() { setState(() { _selectedNotes = []; }); } } class _SliverHeader extends StatelessWidget { final String text; const _SliverHeader({required this.text}); @override Widget build(BuildContext context) { var textTheme = Theme.of(context).textTheme; return SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 8.0), child: Text(text, style: textTheme.titleSmall), ), ); } } Future syncRepo(BuildContext context) async { try { var container = context.read(); await container.syncNotes(); } catch (e) { showErrorSnackbar(context, e); } }