diff --git a/lib/components/home/sort_button.dart b/lib/components/home/sort_button.dart new file mode 100644 index 0000000000..e24cc03d41 --- /dev/null +++ b/lib/components/home/sort_button.dart @@ -0,0 +1,189 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:saber/data/file_manager/file_manager.dart'; +import 'package:saber/data/prefs.dart'; +import 'package:saber/i18n/strings.g.dart'; +import 'package:saber/pages/editor/editor.dart'; +import 'package:stow_plain/stow_plain.dart'; + +class SortNotes { + SortNotes._(); + + static final List, bool)> _sortFunctions = [ + _sortNotesAlpha, + _sortNotesLastModified, + _sortNotesSize, + ]; + static final PlainStow _sortFunctionIdx = stows.sortFunctionIdx; + static final PlainStow _isIncreasingOrder = stows.isSortIncreasing; + + static bool _isNeeded = true; + static bool get isNeeded => _isNeeded; + + static int get sortFunctionIdx => _sortFunctionIdx.value; + static set sortFunctionIdx(int value) { + _sortFunctionIdx.value = value; + _isNeeded = true; + } + + static bool get isIncreasingOrder => _isIncreasingOrder.value; + static set isIncreasingOrder(bool value) { + _isIncreasingOrder.value = value; + _isNeeded = true; + } + + static void _reverse(List list) { + final n = list.length; + for (int i = 0; i < n / 2; i++) { + final tmp = list[i]; + list[i] = list[n - i - 1]; + list[n - i - 1] = tmp; + } + } + + static void sortNotes(List filePaths, {bool forced = false}) { + if (_isNeeded || forced) { + _sortFunctions[sortFunctionIdx].call(filePaths, isIncreasingOrder); + _isNeeded = false; + } + } + + static void _sortNotesAlpha(List filePaths, bool isIncreasing) { + filePaths.sort((a, b) => a.split('/').last.compareTo(b.split('/').last)); + if (!isIncreasing) _reverse(filePaths); + } + + static DateTime _getDirLastModified(Directory dir) { + assert(dir.existsSync()); + DateTime out = dir.statSync().modified; + for (FileSystemEntity entity + in dir.listSync(recursive: true, followLinks: false)) { + if (entity is File && entity.absolute.path.endsWith(Editor.extension)) { + final DateTime curFileModified = entity.lastModifiedSync(); + if (curFileModified.isAfter(out)) out = curFileModified; + } + } + return out; + } + + static void _sortNotesLastModified( + List filePaths, bool isIncreasing) { + filePaths.sort((a, b) { + final Directory firstDir = Directory(FileManager.documentsDirectory + a); + final Directory secondDir = Directory(FileManager.documentsDirectory + b); + final DateTime firstTime = firstDir.existsSync() + ? _getDirLastModified(firstDir) + : FileManager.lastModified(a + Editor.extension); + final DateTime secondTime = secondDir.existsSync() + ? _getDirLastModified(secondDir) + : FileManager.lastModified(b + Editor.extension); + return firstTime.compareTo(secondTime); + }); + if (!isIncreasing) _reverse(filePaths); + } + + static int _getDirSize(Directory dir) { + assert(dir.existsSync()); + int out = 0; + for (FileSystemEntity entity + in dir.listSync(recursive: true, followLinks: false)) { + if (entity is File && entity.absolute.path.endsWith(Editor.extension)) { + final int curFileSize = entity.lengthSync(); + out += curFileSize; + } + } + return out; + } + + static void _sortNotesSize(List filePaths, bool isIncreasing) { + filePaths.sort((a, b) { + final Directory firstDir = Directory(FileManager.documentsDirectory + a); + final Directory secondDir = Directory(FileManager.documentsDirectory + b); + final int firstSize = firstDir.existsSync() + ? _getDirSize(firstDir) + : FileManager.getFile('$a${Editor.extension}').statSync().size; + final int secondSize = secondDir.existsSync() + ? _getDirSize(secondDir) + : FileManager.getFile('$b${Editor.extension}').statSync().size; + return firstSize.compareTo(secondSize); + }); + if (!isIncreasing) _reverse(filePaths); + } +} + +class SortButton extends StatelessWidget { + const SortButton({ + super.key, + required this.callback, + }); + + final void Function() callback; + + @override + Widget build(BuildContext context) { + return IconButton( + icon: Icon(Icons.sort), + onPressed: () async { + showDialog( + context: context, + builder: (BuildContext context) { + return _SortButtonDialog(); + }, + ).then((_) => callback()); + }, + ); + } +} + +class _SortButtonDialog extends StatefulWidget { + @override + State<_SortButtonDialog> createState() => _SortButtonDialogState(); +} + +class _SortButtonDialogState extends State<_SortButtonDialog> { + @override + Widget build(BuildContext context) { + // Needs to match the order of _sortFunctions + final List sortNames = [ + t.home.sortNames.alphabetical, + t.home.sortNames.lastModified, + t.home.sortNames.sizeOnDisk, + ]; + + return Align( + alignment: Alignment.topRight, + child: Container( + width: 220, + decoration: BoxDecoration(borderRadius: BorderRadius.circular(5)), + clipBehavior: Clip.antiAlias, + child: Material( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (int idx = 0; idx < sortNames.length; idx++) + RadioListTile( + title: Text(sortNames[idx]), + onChanged: (int? newValue) => { + SortNotes.sortFunctionIdx = newValue!, + setState(() {}), + // Navigator.pop(context), + }, + groupValue: SortNotes.sortFunctionIdx, + value: idx, + ), + CheckboxListTile( + controlAffinity: ListTileControlAffinity.leading, + title: Text(t.home.sortNames.increasing), + value: SortNotes.isIncreasingOrder, + onChanged: (bool? v) => { + SortNotes.isIncreasingOrder = v!, + setState(() {}), + }), + ], + ), + ), + ), + ); + } +} diff --git a/lib/data/prefs.dart b/lib/data/prefs.dart index 25d988875d..a205a8b364 100644 --- a/lib/data/prefs.dart +++ b/lib/data/prefs.dart @@ -122,6 +122,11 @@ class Stows { final printPageIndicators = PlainStow('printPageIndicators', false, volatile: !_isOnMainIsolate); + final sortFunctionIdx = + PlainStow('sortFunctionIdx', 0, volatile: !_isOnMainIsolate); + final isSortIncreasing = + PlainStow('isSortIncreasing', true, volatile: !_isOnMainIsolate); + final maxImageSize = PlainStow('maxImageSize', 1000, volatile: !_isOnMainIsolate); diff --git a/lib/i18n/en.i18n.yaml b/lib/i18n/en.i18n.yaml index 2b64a24d27..3dc01a8c71 100644 --- a/lib/i18n/en.i18n.yaml +++ b/lib/i18n/en.i18n.yaml @@ -49,6 +49,11 @@ home: multipleRenamedTo: "The following notes will be renamed:" numberRenamedTo: $n notes will be renamed to avoid conflicts deleteNote: Delete note + sortNames: + alphabetical: Alphabetical + lastModified: Last Modified + sizeOnDisk: Size + increasing: Increasing renameFolder: renameFolder: Rename folder folderName: Folder name diff --git a/lib/i18n/it.i18n.yaml b/lib/i18n/it.i18n.yaml index c1527e4216..854adcc069 100644 --- a/lib/i18n/it.i18n.yaml +++ b/lib/i18n/it.i18n.yaml @@ -48,6 +48,11 @@ home: multipleRenamedTo: "Le note seguenti verranno rinominate:" numberRenamedTo: $n le note verranno rinominate per evitare conflitti deleteNote: Elimina nota + sortNames: + alphabetical: Alfabetico + lastModified: Ultima Modifica + sizeOnDisk: Dimensioni + increasing: Crescente renameFolder: renameFolder: Rinomina cartella folderName: Nome cartella diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index fae5ccef55..205b8339dc 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -78,6 +78,7 @@ class TranslationsHomeEn { late final TranslationsHomeRenameNoteEn renameNote = TranslationsHomeRenameNoteEn.internal(_root); late final TranslationsHomeMoveNoteEn moveNote = TranslationsHomeMoveNoteEn.internal(_root); String get deleteNote => 'Delete note'; + late final TranslationsHomeSortNamesEn sortNames = TranslationsHomeSortNamesEn.internal(_root); late final TranslationsHomeRenameFolderEn renameFolder = TranslationsHomeRenameFolderEn.internal(_root); late final TranslationsHomeDeleteFolderEn deleteFolder = TranslationsHomeDeleteFolderEn.internal(_root); } @@ -314,6 +315,19 @@ class TranslationsHomeMoveNoteEn { String numberRenamedTo({required Object n}) => '${n} notes will be renamed to avoid conflicts'; } +// Path: home.sortNames +class TranslationsHomeSortNamesEn { + TranslationsHomeSortNamesEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get alphabetical => 'Alphabetical'; + String get lastModified => 'Last Modified'; + String get sizeOnDisk => 'Size'; + String get increasing => 'Increasing'; +} + // Path: home.renameFolder class TranslationsHomeRenameFolderEn { TranslationsHomeRenameFolderEn.internal(this._root); diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index 34371896bd..a347d34f32 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -75,6 +75,7 @@ class _TranslationsHomeIt extends TranslationsHomeEn { @override late final _TranslationsHomeRenameNoteIt renameNote = _TranslationsHomeRenameNoteIt._(_root); @override late final _TranslationsHomeMoveNoteIt moveNote = _TranslationsHomeMoveNoteIt._(_root); @override String get deleteNote => 'Elimina nota'; + @override late final _TranslationsHomeSortNamesIt sortNames = _TranslationsHomeSortNamesIt._(_root); @override late final _TranslationsHomeRenameFolderIt renameFolder = _TranslationsHomeRenameFolderIt._(_root); @override late final _TranslationsHomeDeleteFolderIt deleteFolder = _TranslationsHomeDeleteFolderIt._(_root); @override String get noPreviewAvailable => 'Nessuna anteprima disponibile'; @@ -312,6 +313,19 @@ class _TranslationsHomeMoveNoteIt extends TranslationsHomeMoveNoteEn { @override String numberRenamedTo({required Object n}) => '${n} le note verranno rinominate per evitare conflitti'; } +// Path: home.sortNames +class _TranslationsHomeSortNamesIt extends TranslationsHomeSortNamesEn { + _TranslationsHomeSortNamesIt._(TranslationsIt root) : this._root = root, super.internal(root); + + final TranslationsIt _root; // ignore: unused_field + + // Translations + @override String get alphabetical => 'Alfabetico'; + @override String get lastModified => 'Ultima Modifica'; + @override String get sizeOnDisk => 'Dimensioni'; + @override String get increasing => 'Crescente'; +} + // Path: home.renameFolder class _TranslationsHomeRenameFolderIt extends TranslationsHomeRenameFolderEn { _TranslationsHomeRenameFolderIt._(TranslationsIt root) : this._root = root, super.internal(root); diff --git a/lib/pages/home/browse.dart b/lib/pages/home/browse.dart index 7891776e32..ac6c6182d4 100644 --- a/lib/pages/home/browse.dart +++ b/lib/pages/home/browse.dart @@ -11,6 +11,7 @@ import 'package:saber/components/home/new_note_button.dart'; import 'package:saber/components/home/no_files.dart'; import 'package:saber/components/home/path_components.dart'; import 'package:saber/components/home/rename_note_button.dart'; +import 'package:saber/components/home/sort_button.dart'; import 'package:saber/components/home/syncing_button.dart'; import 'package:saber/data/file_manager/file_manager.dart'; import 'package:saber/data/routes.dart'; @@ -33,6 +34,8 @@ class BrowsePage extends StatefulWidget { class _BrowsePageState extends State { DirectoryChildren? children; + final List files = []; + final List folders = []; final List pathHistory = []; String? path; @@ -44,6 +47,7 @@ class _BrowsePageState extends State { path = widget.initialPath; findChildrenOfPath(); + fileWriteSubscription = FileManager.fileWriteStream.stream.listen(fileWriteListener); selectedFiles.addListener(_setState); @@ -77,6 +81,16 @@ class _BrowsePageState extends State { children = widget.overrideChildren ?? await FileManager.getChildrenOfDirectory(path ?? '/'); + files.clear(); + for (String filePath in children?.files ?? const []) { + files.add("${path ?? ""}/$filePath"); + } + folders.clear(); + for (String directoryPath in children?.directories ?? const []) { + folders.add("${path ?? ""}/$directoryPath"); + } + SortNotes.sortNotes(files, forced: true); + SortNotes.sortNotes(folders, forced: true); if (mounted) setState(() {}); } @@ -142,8 +156,18 @@ class _BrowsePageState extends State { titlePadding: EdgeInsetsDirectional.only( start: cupertino ? 0 : 16, bottom: 8), ), - actions: const [ - SyncingButton(), + actions: [ + const SyncingButton(), + SortButton( + callback: () => { + if (SortNotes.isNeeded) + { + SortNotes.sortNotes(files, forced: true), + SortNotes.sortNotes(folders, forced: true), + setState(() {}), + } + }, + ), ], ), SliverToBoxAdapter( @@ -177,10 +201,7 @@ class _BrowsePageState extends State { await FileManager.deleteDirectory(folderPath); findChildrenOfPath(); }, - folders: [ - for (String directoryPath in children?.directories ?? const []) - directoryPath, - ], + folders: folders.map((e) => e.split('/').last).toList(), ), if (children == null) ...[ // loading @@ -200,10 +221,7 @@ class _BrowsePageState extends State { ), sliver: MasonryFiles( crossAxisCount: crossAxisCount, - files: [ - for (String filePath in children?.files ?? const []) - "${path ?? ""}/$filePath", - ], + files: files, selectedFiles: selectedFiles, ), ), diff --git a/lib/pages/home/recent_notes.dart b/lib/pages/home/recent_notes.dart index fe8e26b589..bec55dbbbc 100644 --- a/lib/pages/home/recent_notes.dart +++ b/lib/pages/home/recent_notes.dart @@ -9,6 +9,7 @@ import 'package:saber/components/home/masonry_files.dart'; import 'package:saber/components/home/move_note_button.dart'; import 'package:saber/components/home/new_note_button.dart'; import 'package:saber/components/home/rename_note_button.dart'; +import 'package:saber/components/home/sort_button.dart'; import 'package:saber/components/home/syncing_button.dart'; import 'package:saber/components/home/welcome.dart'; import 'package:saber/data/file_manager/file_manager.dart'; @@ -26,6 +27,7 @@ class RecentPage extends StatefulWidget { class _RecentPageState extends State { final List filePaths = []; + bool failed = false; final ValueNotifier> selectedFiles = ValueNotifier([]); @@ -136,8 +138,8 @@ class _RecentPageState extends State { titlePadding: EdgeInsetsDirectional.only( start: cupertino ? 0 : 16, bottom: 16), ), - actions: const [ - SyncingButton(), + actions: [ + const SyncingButton(), ], ), ), @@ -155,9 +157,7 @@ class _RecentPageState extends State { ), sliver: MasonryFiles( crossAxisCount: crossAxisCount, - files: [ - for (String filePath in filePaths) filePath, - ], + files: filePaths, selectedFiles: selectedFiles, ), ),