diff --git a/android/app/build.gradle b/android/app/build.gradle index 3b3fd01e2..d435a1f66 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -53,7 +53,7 @@ android { applicationId "com.adilhanney.saber" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. - minSdkVersion 23 + minSdkVersion 24 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/lib/components/home/sort_button.dart b/lib/components/home/sort_button.dart new file mode 100644 index 000000000..9e5a5d857 --- /dev/null +++ b/lib/components/home/sort_button.dart @@ -0,0 +1,188 @@ +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'; + +class SortNotes { + SortNotes._(); + + static final List, bool)> _sortFunctions = [ + _sortNotesAlpha, + _sortNotesLastModified, + _sortNotesSize, + ]; + static final PlainPref _sortFunctionIdx = Prefs.sortFunctionIdx; + static final PlainPref _isIncreasingOrder = Prefs.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 badab6d76..5d364c99e 100644 --- a/lib/data/prefs.dart +++ b/lib/data/prefs.dart @@ -100,6 +100,9 @@ abstract class Prefs { static late final PlainPref hideHomeBackgrounds; static late final PlainPref printPageIndicators; + static late final PlainPref sortFunctionIdx; + static late final PlainPref isSortIncreasing; + static late final PlainPref maxImageSize; static late final PlainPref autoClearWhiteboardOnExit; @@ -212,6 +215,9 @@ abstract class Prefs { hideHomeBackgrounds = PlainPref('hideHomeBackgrounds', false); printPageIndicators = PlainPref('printPageIndicators', false); + sortFunctionIdx = PlainPref('sortFunctionIdx', 0); + isSortIncreasing = PlainPref('isSortIncreasing', true); + maxImageSize = PlainPref('maxImageSize', 1000); autoClearWhiteboardOnExit = PlainPref('autoClearWhiteboardOnExit', false); diff --git a/lib/i18n/en.i18n.yaml b/lib/i18n/en.i18n.yaml index fc65a75ae..d5c01d36a 100644 --- a/lib/i18n/en.i18n.yaml +++ b/lib/i18n/en.i18n.yaml @@ -48,6 +48,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 cad47248e..3b44e875e 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 61659f1e0..5d474ac6b 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -75,6 +75,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); } @@ -309,6 +310,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 129d200da..09cb2db4a 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -72,6 +72,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); } @@ -306,6 +307,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 f4dd9069f..830a72692 100644 --- a/lib/pages/home/browse.dart +++ b/lib/pages/home/browse.dart @@ -10,6 +10,7 @@ import 'package:saber/components/home/move_note_button.dart'; import 'package:saber/components/home/new_note_button.dart'; import 'package:saber/components/home/no_files.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'; @@ -30,6 +31,8 @@ class BrowsePage extends StatefulWidget { class _BrowsePageState extends State { DirectoryChildren? children; + final List files = []; + final List folders = []; final List pathHistory = []; String? path; @@ -41,6 +44,7 @@ class _BrowsePageState extends State { path = widget.initialPath; findChildrenOfPath(); + fileWriteSubscription = FileManager.fileWriteStream.stream.listen(fileWriteListener); selectedFiles.addListener(_setState); @@ -73,6 +77,16 @@ class _BrowsePageState extends State { } children = 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(() {}); } @@ -135,8 +149,18 @@ class _BrowsePageState extends State { titlePadding: EdgeInsetsDirectional.only( start: cupertino ? 0 : 16, bottom: 16), ), - actions: const [ - SyncingButton(), + actions: [ + const SyncingButton(), + SortButton( + callback: () => { + if (SortNotes.isNeeded) + { + SortNotes.sortNotes(files, forced: true), + SortNotes.sortNotes(folders, forced: true), + setState(() {}), + } + }, + ), ], ), ), @@ -164,10 +188,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 @@ -185,10 +206,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 a369e4cb6..5ac1e70f5 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([]); @@ -59,7 +61,8 @@ class _RecentPageState extends State { @override void initState() { - findRecentlyAccessedNotes(); + findRecentlyAccessedNotes() + .then((_) => SortNotes.sortNotes(filePaths, forced: true)); fileWriteSubscription = FileManager.fileWriteStream.stream.listen(fileWriteListener); selectedFiles.addListener(_setState); @@ -136,8 +139,14 @@ class _RecentPageState extends State { titlePadding: EdgeInsetsDirectional.only( start: cupertino ? 0 : 16, bottom: 16), ), - actions: const [ - SyncingButton(), + actions: [ + const SyncingButton(), + SortButton( + callback: () => { + SortNotes.sortNotes(filePaths), + setState(() {}), + }, + ), ], ), ), @@ -155,9 +164,7 @@ class _RecentPageState extends State { ), sliver: MasonryFiles( crossAxisCount: crossAxisCount, - files: [ - for (String filePath in filePaths) filePath, - ], + files: filePaths, selectedFiles: selectedFiles, ), ),