From 595a09500421a2593767d7b468516d8e4e5578bb Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sat, 28 Oct 2023 13:54:50 +0200 Subject: [PATCH] Readd recent documents, closes #512 --- api/lib/src/models/asset.dart | 4 +- app/lib/api/file_system/file_system.dart | 52 +++++- app/lib/api/file_system/file_system_dav.dart | 2 +- app/lib/api/file_system/file_system_html.dart | 8 +- .../file_system/file_system_html_stub.dart | 2 +- app/lib/api/file_system/file_system_io.dart | 27 +-- app/lib/api/open.dart | 26 +++ app/lib/cubits/settings.dart | 3 + app/lib/services/sync.dart | 3 +- app/lib/views/app_bar.dart | 13 +- app/lib/views/files.dart | 159 +++++++++++++++--- app/lib/views/home.dart | 62 ------- .../metadata/android/en-US/changelogs/77.txt | 1 + 13 files changed, 252 insertions(+), 110 deletions(-) diff --git a/api/lib/src/models/asset.dart b/api/lib/src/models/asset.dart index 5689d92f8060..be39380138cc 100644 --- a/api/lib/src/models/asset.dart +++ b/api/lib/src/models/asset.dart @@ -27,8 +27,10 @@ sealed class AssetLocation with _$AssetLocation { const AssetLocation._(); + bool get isRemote => remote.isNotEmpty; + String get identifier => - remote == '' ? pathWithLeadingSlash : '$pathWithLeadingSlash@$remote'; + isRemote ? '$pathWithLeadingSlash@$remote' : pathWithLeadingSlash; String get pathWithLeadingSlash => path.startsWith('/') ? path : '/$path'; diff --git a/app/lib/api/file_system/file_system.dart b/app/lib/api/file_system/file_system.dart index 457e12bfcb41..0e24a13f863d 100644 --- a/app/lib/api/file_system/file_system.dart +++ b/app/lib/api/file_system/file_system.dart @@ -5,6 +5,7 @@ import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import 'package:rxdart/rxdart.dart'; import 'file_system_dav.dart'; import 'file_system_io.dart'; @@ -103,7 +104,56 @@ abstract class DocumentFileSystem extends GeneralFileSystem { @override FutureOr getDirectory(); - Stream fetchAsset(String path); + Stream fetchAsset(String path, [bool listFiles = false]); + + Stream> fetchAssets(Stream paths, + [bool listFiles = false]) { + final files = []; + final streams = paths.asyncExpand((e) async* { + int? index; + await for (final file in fetchAsset(e, false).whereNotNull()) { + if (index == null) { + index = files.length; + files.add(file); + } else { + files[index] = file; + } + yield null; + } + }); + return streams.map((event) => files); + } + + Stream> fetchAssetsSync(Iterable paths, + [bool listFiles = false]) => + fetchAssets(Stream.fromIterable(paths), listFiles); + + static Stream> fetchAssetsGlobal( + Stream locations, ButterflySettings settings, + [bool listFiles = true]) { + final files = []; + final streams = locations.asyncExpand((e) async* { + final fileSystem = + DocumentFileSystem.fromPlatform(remote: settings.getRemote(e.remote)); + int? index; + await for (final file + in fileSystem.fetchAsset(e.path, listFiles).whereNotNull()) { + if (index == null) { + index = files.length; + files.add(file); + } else { + files[index] = file; + } + yield null; + } + }); + return streams.map((event) => files); + } + + static Stream> fetchAssetsGlobalSync( + Iterable locations, ButterflySettings settings, + [bool listFiles = true]) => + fetchAssetsGlobal(Stream.fromIterable(locations), settings, listFiles); Future getAsset(String path) => fetchAsset(path).last; diff --git a/app/lib/api/file_system/file_system_dav.dart b/app/lib/api/file_system/file_system_dav.dart index f126ea831928..df1a30d3a311 100644 --- a/app/lib/api/file_system/file_system_dav.dart +++ b/app/lib/api/file_system/file_system_dav.dart @@ -70,7 +70,7 @@ class DavRemoteDocumentFileSystem extends DocumentRemoteSystem { @override Stream fetchAsset(String path, - {bool forceRemote = false}) async* { + [bool listFiles = true, bool forceRemote = false]) async* { if (path.endsWith('/')) { path = path.substring(0, path.length - 1); } diff --git a/app/lib/api/file_system/file_system_html.dart b/app/lib/api/file_system/file_system_html.dart index 2a47bec49021..ce401148d88e 100644 --- a/app/lib/api/file_system/file_system_html.dart +++ b/app/lib/api/file_system/file_system_html.dart @@ -124,7 +124,8 @@ class WebDocumentFileSystem extends DocumentFileSystem { } @override - Stream fetchAsset(String path) async* { + Stream fetchAsset(String path, + [bool listFiles = true]) async* { // Add leading slash if (!path.startsWith('/')) { path = '/$path'; @@ -163,6 +164,11 @@ class WebDocumentFileSystem extends DocumentFileSystem { yield await file; return; } else if (map['type'] == 'directory') { + if (!listFiles) { + await txn.completed; + yield AppDocumentDirectory(AssetLocation.local(path), const []); + return; + } var cursor = store.openCursor(autoAdvance: true); var assets = await Future.wait( await cursor.map>((cursor) async { diff --git a/app/lib/api/file_system/file_system_html_stub.dart b/app/lib/api/file_system/file_system_html_stub.dart index 13705a2fb028..1117c0477a6a 100644 --- a/app/lib/api/file_system/file_system_html_stub.dart +++ b/app/lib/api/file_system/file_system_html_stub.dart @@ -15,7 +15,7 @@ class WebDocumentFileSystem extends DocumentFileSystem { } @override - Stream fetchAsset(String path) { + Stream fetchAsset(String path, [bool listFiles = true]) { throw UnimplementedError(); } diff --git a/app/lib/api/file_system/file_system_io.dart b/app/lib/api/file_system/file_system_io.dart index 356169c4ea30..4d5c5c3d4023 100644 --- a/app/lib/api/file_system/file_system_io.dart +++ b/app/lib/api/file_system/file_system_io.dart @@ -6,7 +6,6 @@ import 'package:butterfly_api/butterfly_api.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:rxdart/rxdart.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'file_system.dart'; @@ -54,7 +53,7 @@ class IODocumentFileSystem extends DocumentFileSystem { @override Stream fetchAsset(String path, - [bool goFurther = true]) async* { + [bool listFiles = true]) async* { // Add leading slash if (!path.startsWith('/')) { path = '/$path'; @@ -77,24 +76,12 @@ class IODocumentFileSystem extends DocumentFileSystem { } } else if (await directory.exists()) { yield AppDocumentDirectory(AssetLocation.local(path), []); - if (goFurther) { - final files = []; - final streams = directory.list().asyncExpand((e) async* { - final currentPath = - '$path/${e.path.replaceAll('\\', '/').split('/').last}'; - int? index; - await for (final file - in fetchAsset(currentPath, false).whereNotNull()) { - if (index == null) { - index = files.length; - files.add(file); - } else { - files[index] = file; - } - yield null; - } - }); - await for (final _ in streams) { + if (listFiles) { + final streams = fetchAssets( + directory.list().map( + (e) => '$path/${e.path.replaceAll('\\', '/').split('/').last}'), + false); + await for (final files in streams) { yield AppDocumentDirectory(AssetLocation.local(path), files); } } diff --git a/app/lib/api/open.dart b/app/lib/api/open.dart index 6c7746ab7d63..90d9d2255376 100644 --- a/app/lib/api/open.dart +++ b/app/lib/api/open.dart @@ -1,8 +1,11 @@ import 'dart:async'; import 'dart:io'; +import 'package:butterfly_api/butterfly_api.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher.dart'; Future openHelp(List pageLocation, [String? fragment]) { @@ -27,3 +30,26 @@ Future openBfly() async { var e = files!.files.first; return e.bytes; } + +void openFile(BuildContext context, AssetLocation location, [Object? data]) { + if (location.isRemote) { + GoRouter.of(context).goNamed('remote', + pathParameters: { + 'remote': location.remote, + 'path': location.pathWithoutLeadingSlash, + }, + queryParameters: { + 'type': location.fileType?.name, + }, + extra: data); + return; + } + GoRouter.of(context).goNamed('local', + pathParameters: { + 'path': location.pathWithoutLeadingSlash, + }, + queryParameters: { + 'type': location.fileType?.name, + }, + extra: data); +} diff --git a/app/lib/cubits/settings.dart b/app/lib/cubits/settings.dart index 2ea7bea61006..ab6f8d7eab09 100644 --- a/app/lib/cubits/settings.dart +++ b/app/lib/cubits/settings.dart @@ -476,6 +476,9 @@ class ButterflySettings with _$ButterflySettings { } ExternalStorage? getRemote(String? identifier) { + if (identifier?.isEmpty ?? true) { + return getDefaultRemote(); + } return connections .firstWhereOrNull((e) => e.identifier == (identifier ?? defaultRemote)); } diff --git a/app/lib/services/sync.dart b/app/lib/services/sync.dart index 25328a9eb389..d0608385da1a 100644 --- a/app/lib/services/sync.dart +++ b/app/lib/services/sync.dart @@ -232,8 +232,7 @@ class RemoteSync { return fileSystem.deleteCachedContent(path); case FileSyncStatus.conflict: await fileSystem.cache(path); - final remoteAsset = - await fileSystem.fetchAsset(path, forceRemote: true).last; + final remoteAsset = await fileSystem.fetchAsset(path, true, true).last; await remoteAsset?.maybeMap( file: (file) async { if (remoteAsset is! AppDocumentFile) return; diff --git a/app/lib/views/app_bar.dart b/app/lib/views/app_bar.dart index 2c3eb8402bad..69f4af610aeb 100644 --- a/app/lib/views/app_bar.dart +++ b/app/lib/views/app_bar.dart @@ -4,6 +4,7 @@ import 'package:butterfly/actions/change_path.dart'; import 'package:butterfly/actions/settings.dart'; import 'package:butterfly/actions/svg_export.dart'; import 'package:butterfly/api/file_system/file_system.dart'; +import 'package:butterfly/api/open.dart'; import 'package:butterfly/cubits/current_index.dart'; import 'package:butterfly/services/import.dart'; import 'package:butterfly/views/edit.dart'; @@ -423,7 +424,7 @@ class _MainPopupMenu extends StatelessWidget { }, child: Text(AppLocalizations.of(context).packs), ), - const PopupMenuDivider(), + const Divider(), MenuItemButton( leadingIcon: const PhosphorIcon(PhosphorIconsLight.filePlus), shortcut: @@ -443,6 +444,16 @@ class _MainPopupMenu extends StatelessWidget { }, child: Text(AppLocalizations.of(context).templates), ), + SubmenuButton( + menuChildren: settingsCubit.state.history + .map((e) => MenuItemButton( + child: Text(e.identifier), + onPressed: () => openFile(context, e), + )) + .toList(), + leadingIcon: const PhosphorIcon(PhosphorIconsLight.clock), + child: Text(AppLocalizations.of(context).recentFiles), + ), ], if (state.embedding == null) ...[ MenuItemButton( diff --git a/app/lib/views/files.dart b/app/lib/views/files.dart index fd401010b8a1..5f23798964d8 100644 --- a/app/lib/views/files.dart +++ b/app/lib/views/files.dart @@ -241,6 +241,8 @@ class _FilesViewState extends State { ], ); }), + const SizedBox(height: 8), + const _RecentFilesView(), const SizedBox(height: 16), LayoutBuilder(builder: (context, constraints) { final searchBar = SearchBar( @@ -471,26 +473,7 @@ class _FilesViewState extends State { } final location = entity.location; final data = entity.data; - if (location.remote != '') { - GoRouter.of(context).pushReplacementNamed('remote', - pathParameters: { - 'remote': location.remote, - 'path': location.pathWithoutLeadingSlash, - }, - queryParameters: { - 'type': location.fileType?.name, - }, - extra: data); - return; - } - GoRouter.of(context).pushReplacementNamed('local', - pathParameters: { - 'path': location.pathWithoutLeadingSlash, - }, - queryParameters: { - 'type': location.fileType?.name, - }, - extra: data); + openFile(context, location, data); } int _sortAssets(AppDocumentEntity a, AppDocumentEntity b) { @@ -561,3 +544,139 @@ class _FilesViewState extends State { } } } + +class _RecentFilesView extends StatefulWidget { + const _RecentFilesView(); + + @override + State<_RecentFilesView> createState() => _RecentFilesViewState(); +} + +class _RecentFilesViewState extends State<_RecentFilesView> { + late Stream> _stream; + + @override + void initState() { + super.initState(); + _setStream(context.read().state); + } + + void _setStream(ButterflySettings settings) => _stream = + DocumentFileSystem.fetchAssetsGlobalSync(settings.history, settings); + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (_, state) => setState(() { + _setStream(state); + }), + child: StreamBuilder>( + stream: _stream, + builder: (context, snapshot) { + final files = snapshot.data ?? []; + if (files.isEmpty) { + return Container(); + } + return SizedBox( + height: 150, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: files.length, + itemBuilder: (context, index) { + final entity = files[index]; + FileMetadata? metadata; + Uint8List? thumbnail; + List? data; + if (entity is AppDocumentFile) { + metadata = entity.metadata; + thumbnail = entity.thumbnail; + data = entity.data; + } + return AssetCard( + metadata: metadata, + thumbnail: thumbnail, + name: entity.fileName, + onTap: () => openFile(context, entity.location, data), + ); + }, + ), + ); + }), + ); + } +} + +class AssetCard extends StatelessWidget { + const AssetCard({ + super.key, + required this.metadata, + required this.thumbnail, + required this.onTap, + this.name, + }); + final String? name; + final FileMetadata? metadata; + final Uint8List? thumbnail; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textStyle = Theme.of(context).textTheme.bodyLarge?.copyWith( + color: colorScheme.onSurface, + ); + return ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 200), + child: AspectRatio( + aspectRatio: 16 / 9, + child: Card( + elevation: 5, + clipBehavior: Clip.hardEdge, + child: InkWell( + onTap: onTap, + child: Stack( + children: [ + if (thumbnail?.isNotEmpty ?? false) + Align( + child: Image.memory( + thumbnail!, + fit: BoxFit.cover, + width: 640, + alignment: Alignment.center, + ), + ), + if ((metadata?.name.isNotEmpty ?? false) || + (name?.isNotEmpty ?? false)) + Align( + alignment: Alignment.bottomLeft, + child: Container( + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: colorScheme.primaryContainer.withAlpha(200), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (metadata?.name.isNotEmpty ?? false) + Text( + metadata!.name, + style: textStyle, + ), + if (name?.isNotEmpty ?? false) + Text( + name!, + style: textStyle, + ), + ], + ), + ), + ), + ], + ), + ), + ), + )); + } +} diff --git a/app/lib/views/home.dart b/app/lib/views/home.dart index 736ad7e3eecc..4c68ce8391eb 100644 --- a/app/lib/views/home.dart +++ b/app/lib/views/home.dart @@ -1,5 +1,3 @@ -import 'dart:typed_data'; - import 'package:butterfly/actions/settings.dart'; import 'package:butterfly/api/file_system/file_system.dart'; import 'package:butterfly/api/open.dart'; @@ -526,63 +524,3 @@ class _QuickstartHomeViewState extends State<_QuickstartHomeView> { ); } } - -class AssetCard extends StatelessWidget { - const AssetCard({ - super.key, - required this.metadata, - required this.thumbnail, - required this.onTap, - }); - - final FileMetadata metadata; - final Uint8List? thumbnail; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - return ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 200), - child: AspectRatio( - aspectRatio: 16 / 9, - child: Card( - elevation: 5, - clipBehavior: Clip.hardEdge, - child: InkWell( - onTap: onTap, - child: Stack( - children: [ - if (thumbnail?.isNotEmpty ?? false) - Align( - child: Image.memory( - thumbnail!, - fit: BoxFit.cover, - width: 640, - alignment: Alignment.center, - ), - ), - Align( - alignment: Alignment.bottomLeft, - child: Container( - padding: const EdgeInsets.all(8), - margin: const EdgeInsets.all(8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: colorScheme.primaryContainer.withAlpha(200), - ), - child: Text( - metadata.name, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: colorScheme.onSurface, - ), - ), - ), - ), - ], - ), - ), - ), - )); - } -} diff --git a/fastlane/metadata/android/en-US/changelogs/77.txt b/fastlane/metadata/android/en-US/changelogs/77.txt index d1a03b0696b6..ffffa2027beb 100644 --- a/fastlane/metadata/android/en-US/changelogs/77.txt +++ b/fastlane/metadata/android/en-US/changelogs/77.txt @@ -1,4 +1,5 @@ * Allow moving tools when selected +* Readd recent files ([#512](https://github.com/LinwoodDev/Butterfly/issues/512)) * Fix tool indicator alignment if toolbar is in column mode * Fix moving issues when painting * Fix painting issues when using gestures