From 267ecb74993a8f330ef4d247aa856e9204d1b597 Mon Sep 17 00:00:00 2001 From: Vishesh Handa Date: Wed, 10 Jun 2020 00:47:54 +0200 Subject: [PATCH] Implement a very basic file system view This will show all the files and not just the "notes". This way the user can easily figure out why a file isn't included in the list of notes. It's currently disabled by default as it clearly needs a lot more work. --- assets/langs/en.yaml | 11 +++ lib/analytics.dart | 4 + lib/app.dart | 5 +- lib/core/git_repo.dart | 15 ++++ lib/core/notes_folder_fs.dart | 37 +++++++- lib/features.dart | 1 + lib/screens/filesystem_screen.dart | 137 +++++++++++++++++++++++++++++ lib/state_container.dart | 18 ++++ lib/widgets/app_drawer.dart | 10 +++ 9 files changed, 233 insertions(+), 5 deletions(-) create mode 100644 lib/screens/filesystem_screen.dart diff --git a/assets/langs/en.yaml b/assets/langs/en.yaml index df9820f3..5895856b 100644 --- a/assets/langs/en.yaml +++ b/assets/langs/en.yaml @@ -84,6 +84,14 @@ screens: empty: Please enter a name tags: title: Tags + filesystem: + ignoredFile: + title: File Ignored + ok: Ok + rename: Rename + rename: + decoration: File Name + title: Rename widgets: rename: yes: Rename @@ -102,3 +110,6 @@ widgets: other: "{} Notes link to this Note" rootFolder: Root Folder +ignoredFiles: + dot: Starts with a . + ext: Doesn't end with .md or .txt diff --git a/lib/analytics.dart b/lib/analytics.dart index a9a14a37..c85b5949 100644 --- a/lib/analytics.dart +++ b/lib/analytics.dart @@ -17,6 +17,7 @@ enum Event { NoteUndoDeleted, NoteRenamed, NoteMoved, + FileRenamed, FolderAdded, FolderDeleted, FolderRenamed, @@ -39,6 +40,9 @@ String _eventToString(Event e) { case Event.NoteMoved: return "note_moved"; + case Event.FileRenamed: + return "file_renamed"; + case Event.FolderAdded: return "folder_added"; case Event.FolderDeleted: diff --git a/lib/app.dart b/lib/app.dart index 673c8eab..397686e0 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -5,6 +5,7 @@ import 'package:device_info/device_info.dart'; import 'package:firebase_analytics/observer.dart'; import 'package:flutter/material.dart'; import 'package:gitjournal/analytics.dart'; +import 'package:gitjournal/screens/filesystem_screen.dart'; import 'package:gitjournal/screens/folder_listing.dart'; import 'package:gitjournal/screens/tag_listing.dart'; import 'package:gitjournal/screens/note_editor.dart'; @@ -306,7 +307,7 @@ class _JournalAppState extends State { //debugShowMaterialGrid: true, onGenerateRoute: (settings) { var route = settings.name; - if (route == '/folders' || route == '/tags') { + if (route == '/folders' || route == '/tags' || route == '/filesystem') { return PageRouteBuilder( settings: settings, pageBuilder: (_, __, ___) => _screenForRoute(route, stateContainer), @@ -333,6 +334,8 @@ class _JournalAppState extends State { return HomeScreen(); case '/folders': return FolderListingScreen(); + case '/filesystem': + return FileSystemScreen(); case '/tags': return TagListingScreen(); case '/settings': diff --git a/lib/core/git_repo.dart b/lib/core/git_repo.dart index ae2779c7..c45f7012 100644 --- a/lib/core/git_repo.dart +++ b/lib/core/git_repo.dart @@ -102,6 +102,21 @@ class GitNoteRepository { return NoteRepoResult(noteFilePath: newFullPath, error: false); } + Future renameFile( + String oldFullPath, + String newFullPath, + ) async { + // FIXME: This is a hacky way of adding the changes, ideally we should be calling rm + add or something + await _gitRepo.add("."); + await _gitRepo.commit( + message: "Renamed File", + authorEmail: Settings.instance.gitAuthorEmail, + authorName: Settings.instance.gitAuthor, + ); + + return NoteRepoResult(noteFilePath: newFullPath, error: false); + } + Future moveNote( String oldFullPath, String newFullPath, diff --git a/lib/core/notes_folder_fs.dart b/lib/core/notes_folder_fs.dart index 6dc6ab5a..17063432 100644 --- a/lib/core/notes_folder_fs.dart +++ b/lib/core/notes_folder_fs.dart @@ -7,11 +7,23 @@ import 'package:gitjournal/utils/logger.dart'; import 'package:path/path.dart' as p; import 'package:path/path.dart'; import 'package:synchronized/synchronized.dart'; +import 'package:meta/meta.dart'; import 'note.dart'; import 'notes_folder.dart'; import 'notes_folder_notifier.dart'; +class IgnoredFile { + String filePath; + String reason; + + IgnoredFile({@required this.filePath, @required this.reason}); + + String get fileName { + return p.basename(filePath); + } +} + class NotesFolderFS with NotesFolderNotifier implements NotesFolder { final NotesFolderFS _parent; String _folderPath; @@ -19,6 +31,7 @@ class NotesFolderFS with NotesFolderNotifier implements NotesFolder { List _notes = []; List _folders = []; + List _ignoredFiles = []; Map _entityMap = {}; NotesFolderConfig _config; @@ -119,6 +132,8 @@ class NotesFolderFS with NotesFolderNotifier implements NotesFolder { @override List get subFolders => subFoldersFS; + List get ignoredFiles => _ignoredFiles; + List get subFoldersFS { // FIXME: This is really not ideal _folders.sort((NotesFolderFS a, NotesFolderFS b) => @@ -172,6 +187,8 @@ class NotesFolderFS with NotesFolderNotifier implements NotesFolder { _config = await NotesFolderConfig.fromFS(this); } + _ignoredFiles = []; + final dir = Directory(folderPath); var lister = dir.list(recursive: false, followLinks: false); await for (var fsEntity in lister) { @@ -208,9 +225,15 @@ class NotesFolderFS with NotesFolderNotifier implements NotesFolder { var note = Note(this, fsEntity.path); if (note.fileName.startsWith('.')) { + var ignoredFile = IgnoredFile( + filePath: fsEntity.path, + reason: tr("ignoredFiles.dot"), + ); + _ignoredFiles.add(ignoredFile); + Log.v("Ignoring file", props: { - "path": fsEntity.path, - "reason": "Starts with a .", + "path": ignoredFile.filePath, + "reason": ignoredFile.reason, }); continue; } @@ -218,9 +241,15 @@ class NotesFolderFS with NotesFolderNotifier implements NotesFolder { var isMarkdownFile = noteFilePath.endsWith('.md'); var isTxtFile = noteFilePath.endsWith('.txt'); if (!isMarkdownFile && !isTxtFile) { + var ignoredFile = IgnoredFile( + filePath: fsEntity.path, + reason: tr("ignoredFiles.ext"), + ); + _ignoredFiles.add(ignoredFile); + Log.v("Ignoring file", props: { - "path": fsEntity.path, - "reason": "Doesn't end with .md or .txt", + "path": ignoredFile.filePath, + "reason": ignoredFile.reason, }); continue; } diff --git a/lib/features.dart b/lib/features.dart index 51d98688..ac717bab 100644 --- a/lib/features.dart +++ b/lib/features.dart @@ -1,3 +1,4 @@ class Features { static bool perFolderConfig = false; + static bool showFileSystem = false; } diff --git a/lib/screens/filesystem_screen.dart b/lib/screens/filesystem_screen.dart new file mode 100644 index 00000000..2c642dda --- /dev/null +++ b/lib/screens/filesystem_screen.dart @@ -0,0 +1,137 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:function_types/function_types.dart'; +import 'package:gitjournal/state_container.dart'; +import 'package:gitjournal/widgets/rename_dialog.dart'; +import 'package:provider/provider.dart'; + +import 'package:gitjournal/core/note.dart'; +import 'package:gitjournal/core/notes_folder_fs.dart'; +import 'package:gitjournal/widgets/app_drawer.dart'; + +class FileSystemScreen extends StatefulWidget { + @override + _FileSystemScreenState createState() => _FileSystemScreenState(); +} + +class _FileSystemScreenState extends State { + @override + Widget build(BuildContext context) { + final rootFolder = Provider.of(context); + + return Scaffold( + appBar: AppBar( + title: Text(rootFolder.publicName), + ), + body: Scrollbar( + child: FileSystemView( + rootFolder, + onFolderSelected: _onFolderSelected, + onNoteSelected: _onNoteSelected, + onIgnoredFileSelected: _onIgnoredFileSelected, + ), + ), + drawer: AppDrawer(), + ); + } + + void _onFolderSelected(NotesFolderFS folder) {} + void _onNoteSelected(Note note) {} + + void _onIgnoredFileSelected(IgnoredFile ignoredFile) async { + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text(tr("screens.filesystem.ignoredFile.title")), + content: Text(ignoredFile.reason), + actions: [ + FlatButton( + onPressed: () async { + Navigator.of(context).pop(false); + _renameFile(ignoredFile.filePath); + }, + child: Text(tr('screens.filesystem.ignoredFile.rename')), + ), + FlatButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(tr('screens.filesystem.ignoredFile.ok')), + ), + ], + ); + }, + ); + } + + void _renameFile(String oldPath) async { + var newFileName = await showDialog( + context: context, + builder: (_) => RenameDialog( + oldPath: oldPath, + inputDecoration: tr("screens.filesystem.rename.decoration"), + dialogTitle: tr("screens.filesystem.rename.title"), + ), + ); + if (newFileName is String) { + var container = Provider.of(context, listen: false); + container.renameFile(oldPath, newFileName); + } + } +} + +class FileSystemView extends StatelessWidget { + final NotesFolderFS folder; + + final Func1 onFolderSelected; + final Func1 onNoteSelected; + final Func1 onIgnoredFileSelected; + + FileSystemView( + this.folder, { + @required this.onFolderSelected, + @required this.onNoteSelected, + @required this.onIgnoredFileSelected, + }); + + @override + Widget build(BuildContext context) { + return ListView( + children: [ + for (var folder in folder.subFolders) + _buildFolderTile(folder.fsFolder as NotesFolderFS), + for (var note in folder.notes) _buildNoteTile(note), + for (var ignoredFile in folder.ignoredFiles) + _buildIgnoredFileTile(ignoredFile), + ], + ); + } + + Widget _buildFolderTile(NotesFolderFS folder) { + return ListTile( + leading: Icon(Icons.folder), + title: Text(folder.name), + dense: true, + onTap: () => onFolderSelected(folder), + ); + } + + Widget _buildNoteTile(Note note) { + return ListTile( + leading: Icon(Icons.note), + title: Text(note.fileName), + dense: true, + onTap: () => onNoteSelected(note), + ); + } + + Widget _buildIgnoredFileTile(IgnoredFile ignoredFile) { + // FIXME: Paint with Ignored colours + return ListTile( + leading: Icon(Icons.broken_image), + title: Text(ignoredFile.fileName), + dense: true, + enabled: true, + onTap: () => onIgnoredFileSelected(ignoredFile), + ); + } +} diff --git a/lib/state_container.dart b/lib/state_container.dart index 0d0f758d..7954414b 100644 --- a/lib/state_container.dart +++ b/lib/state_container.dart @@ -218,6 +218,24 @@ class StateContainer with ChangeNotifier { }); } + void renameFile(String oldPath, String newFileName) async { + logEvent(Event.NoteRenamed); + + return _opLock.synchronized(() async { + Log.d("Got renameNote lock"); + + var newPath = p.join(p.dirname(oldPath), newFileName); + await File(oldPath).rename(newPath); + notifyListeners(); + + _gitRepo.renameFile(oldPath, newPath).then((NoteRepoResult _) { + _syncNotes(); + appState.numChanges += 1; + notifyListeners(); + }); + }); + } + void moveNote(Note note, NotesFolderFS destFolder) async { if (destFolder.folderPath == note.parent.folderPath) { return; diff --git a/lib/widgets/app_drawer.dart b/lib/widgets/app_drawer.dart index fa6501b5..274fa3f8 100644 --- a/lib/widgets/app_drawer.dart +++ b/lib/widgets/app_drawer.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_email_sender/flutter_email_sender.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:gitjournal/features.dart'; import 'package:gitjournal/settings.dart'; import 'package:gitjournal/utils/logger.dart'; import 'package:launch_review/launch_review.dart'; @@ -93,6 +94,15 @@ class AppDrawer extends StatelessWidget { onTap: () => _navTopLevel(context, '/folders'), selected: currentRoute == "/folders", ), + if (Features.showFileSystem) + _buildDrawerTile( + context, + icon: FontAwesomeIcons.solidFolderOpen, + isFontAwesome: true, + title: "File System", + onTap: () => _navTopLevel(context, '/filesystem'), + selected: currentRoute == "/filesystem", + ), _buildDrawerTile( context, icon: FontAwesomeIcons.tag,