From 91afa4431f64ffb27325168cf9369eb54f13da7b Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sun, 9 Jun 2024 22:50:44 +0200 Subject: [PATCH] Integrate lw_file_system in packs --- app/lib/api/storage.dart | 15 +++ app/lib/l10n/app_en.arb | 4 +- app/lib/main.dart | 10 +- app/lib/models/definitions/meta.dart | 16 +-- app/lib/models/definitions/pack.dart | 2 + app/lib/pages/home/packs.dart | 175 +++++++++++++++++++++++++-- app/lib/pages/home/page.dart | 9 +- app/lib/services/packs.dart | 34 +++++- app/lib/widgets/loading.dart | 3 +- app/pack/pack.json | 3 +- app/pubspec.lock | 4 +- app/pubspec.yaml | 3 +- 12 files changed, 244 insertions(+), 34 deletions(-) create mode 100644 app/lib/api/storage.dart diff --git a/app/lib/api/storage.dart b/app/lib/api/storage.dart new file mode 100644 index 0000000..758ed46 --- /dev/null +++ b/app/lib/api/storage.dart @@ -0,0 +1,15 @@ +import 'dart:io'; + +import 'package:path_provider/path_provider.dart'; + +const quokkaSubDirectory = '/Linwood/Quokka'; + +Future getQuokkaDirectory() async { + String? path; + if (Platform.isAndroid) { + path ??= (await getExternalStorageDirectory())?.path; + } + path ??= (await getApplicationDocumentsDirectory()).path; + path += quokkaSubDirectory; + return path; +} diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index dce8041..8c8f8e3 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -89,5 +89,7 @@ "configuration": "Configuration", "background": "Background", "search": "Search", - "onlyFavorites": "Only favorites" + "onlyFavorites": "Only favorites", + "loading": "Loading", + "install": "Install" } diff --git a/app/lib/main.dart b/app/lib/main.dart index f1a011b..fdaa9f0 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -13,6 +13,7 @@ import 'package:quokka/pages/home/page.dart'; import 'package:quokka/pages/settings/general.dart'; import 'package:quokka/pages/settings/personalization.dart'; import 'package:quokka/services/network.dart'; +import 'package:quokka/services/packs.dart'; import 'package:quokka/theme.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_localized_locales/flutter_localized_locales.dart'; @@ -47,8 +48,13 @@ Future main(List args) async { BlocProvider.value( value: WindowCubit(fullScreen: await isFullScreen())), ], - child: RepositoryProvider( - create: (context) => NetworkingService(settingsCubit), + child: MultiRepositoryProvider( + providers: [ + RepositoryProvider( + create: (context) => NetworkingService(settingsCubit), + ), + RepositoryProvider(create: (context) => PacksService()), + ], child: QuokkaApp(), ), ), diff --git a/app/lib/models/definitions/meta.dart b/app/lib/models/definitions/meta.dart index 5e99c51..a9894e0 100644 --- a/app/lib/models/definitions/meta.dart +++ b/app/lib/models/definitions/meta.dart @@ -4,15 +4,15 @@ part 'meta.mapper.dart'; @MappableClass() class PackMetadata with PackMetadataMappable { - final String? name; - final String? description; - final String? author; - final String? version; + final String name; + final String description; + final String author; + final String version; PackMetadata({ - this.name, - this.description, - this.author, - this.version, + this.name = '', + this.description = '', + this.author = '', + this.version = '', }); } diff --git a/app/lib/models/definitions/pack.dart b/app/lib/models/definitions/pack.dart index b9a868c..153cd0c 100644 --- a/app/lib/models/definitions/pack.dart +++ b/app/lib/models/definitions/pack.dart @@ -95,4 +95,6 @@ class PackData { String getTranslationOrKey(String path, String key) => getTranslation(path, key) ?? key; + + Uint8List export() => Uint8List.fromList(ZipEncoder().encode(archive) ?? []); } diff --git a/app/lib/pages/home/packs.dart b/app/lib/pages/home/packs.dart index 012f4d5..55e1048 100644 --- a/app/lib/pages/home/packs.dart +++ b/app/lib/pages/home/packs.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:material_leap/material_leap.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; +import 'package:quokka/models/definitions/pack.dart'; +import 'package:quokka/services/packs.dart'; import 'package:quokka/widgets/search.dart'; class PacksDialog extends StatefulWidget { @@ -14,12 +17,26 @@ class PacksDialog extends StatefulWidget { class _PacksDialogState extends State with TickerProviderStateMixin { bool _gridView = false; + late final AnimationController _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 100), + ); late final TabController _tabController; + Future>? _packsFuture; + (PackData, String, bool)? _selectedPack; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); + _controller.addStatusListener((status) { + if (status == AnimationStatus.dismissed) { + setState(() { + _selectedPack = null; + }); + } + }); + _packsFuture = context.read().getPacks(); } @override @@ -28,8 +45,19 @@ class _PacksDialogState extends State super.dispose(); } + void _selectPack(PackData pack, String id, bool installed) { + _controller.forward(); + setState(() { + _selectedPack = (pack, id, installed); + }); + } + + void _deselectPack() => _controller.reverse(); + @override Widget build(BuildContext context) { + final currentSize = MediaQuery.sizeOf(context).width; + final isMobile = currentSize < LeapBreakpoints.medium; return ResponsiveAlertDialog( title: Text(AppLocalizations.of(context).packs), constraints: const BoxConstraints( @@ -52,17 +80,69 @@ class _PacksDialogState extends State ), ], ), + const SizedBox(height: 8), Expanded( - child: TabBarView( - controller: _tabController, - children: [ - Center( - child: Text(AppLocalizations.of(context).comingSoon), - ), - Center( - child: Text(AppLocalizations.of(context).comingSoon), - ), - ], + child: FutureBuilder>( + future: _packsFuture, + builder: (context, snapshot) { + final packs = snapshot.data?.entries.toList() ?? []; + final view = TabBarView( + controller: _tabController, + children: [ + ListView.builder( + itemCount: packs.length, + itemBuilder: (context, index) { + final key = packs[index].key; + final pack = packs[index].value; + final metadata = pack.getMetadata(); + return ListTile( + title: Text(metadata?.name ?? + AppLocalizations.of(context).unnamed), + selected: _selectedPack?.$1 == pack, + onTap: () => _selectPack(pack, key, true), + ); + }, + ), + Center( + child: Text(AppLocalizations.of(context).comingSoon), + ), + ], + ); + if (isMobile) { + return view; + } + return Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: view, + ), + SizeTransition( + sizeFactor: CurvedAnimation( + parent: _controller, + curve: Curves.fastOutSlowIn, + ), + axis: Axis.horizontal, + child: SizedBox( + width: 300, + child: Card( + child: _PacksDetailsView( + pack: _selectedPack?.$1, + onClose: _deselectPack, + onInstall: _selectedPack?.$3 ?? false + ? null + : _deselectPack, + onRemove: (_selectedPack?.$3 ?? false) && + (_selectedPack?.$2.isNotEmpty ?? true) + ? _deselectPack + : null, + ), + ), + ), + ), + ], + ); + }, ), ), ], @@ -87,3 +167,78 @@ class _PacksDialogState extends State ); } } + +class _PacksDetailsView extends StatelessWidget { + final PackData? pack; + final VoidCallback? onClose, onInstall, onRemove; + + const _PacksDetailsView({ + this.pack, + this.onClose, + this.onInstall, + this.onRemove, + }); + + @override + Widget build(BuildContext context) { + final metadata = pack?.getMetadata(); + if (metadata == null) { + return const SizedBox(); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Header( + title: Text(metadata.name), + actions: [ + if (onClose != null) ...[ + IconButton.outlined( + icon: const Icon(PhosphorIconsLight.x), + onPressed: onClose, + ), + ], + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: ListView( + shrinkWrap: true, + children: [ + Text(metadata.description), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (onInstall != null) ...[ + SizedBox( + height: 42, + child: FilledButton.tonalIcon( + onPressed: onInstall, + label: Text(AppLocalizations.of(context).install), + icon: const Icon(PhosphorIconsLight.download), + ), + ), + ], + if (onRemove != null) ...[ + SizedBox( + height: 42, + child: FilledButton.tonalIcon( + onPressed: onRemove, + label: Text(AppLocalizations.of(context).remove), + icon: const Icon(PhosphorIconsLight.trash), + ), + ), + ], + ], + ), + ), + ], + ); + } +} diff --git a/app/lib/pages/home/page.dart b/app/lib/pages/home/page.dart index a161bda..3a56321 100644 --- a/app/lib/pages/home/page.dart +++ b/app/lib/pages/home/page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:material_leap/material_leap.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; import 'package:quokka/cubits/settings.dart'; @@ -50,14 +51,14 @@ class HomePage extends StatelessWidget { overflowSpacing: 8, children: [ ( - 'Play', + AppLocalizations.of(context).play, PhosphorIconsLight.play, () => showDialog( context: context, builder: (context) => const PlayDialog()), ), ( - 'Connect', + AppLocalizations.of(context).connect, PhosphorIconsLight.plugsConnected, () => showDialog( context: context, @@ -65,7 +66,7 @@ class HomePage extends StatelessWidget { const ConnectDialog()), ), ( - 'Packs', + AppLocalizations.of(context).packs, PhosphorIconsLight.package, () => showDialog( context: context, @@ -73,7 +74,7 @@ class HomePage extends StatelessWidget { ), ), ( - 'Options', + AppLocalizations.of(context).settings, PhosphorIconsLight.gear, () => openSettings(context), ), diff --git a/app/lib/services/packs.dart b/app/lib/services/packs.dart index 2ad8a2f..29fb476 100644 --- a/app/lib/services/packs.dart +++ b/app/lib/services/packs.dart @@ -1,17 +1,43 @@ +import 'dart:io'; + import 'package:collection/collection.dart'; +import 'package:lw_file_system/lw_file_system.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:quokka/api/storage.dart'; import 'package:quokka/models/definitions/pack.dart'; class PacksService { final PackData? _corePack; + final TypedKeyFileSystem _fileSystem; PacksService({ PackData? corePack, - }) : _corePack = corePack; + }) : _corePack = corePack, + _fileSystem = TypedKeyFileSystem.build( + FileSystemConfig( + passwordStorage: SecureStoragePasswordStorage(), + storeName: 'packs', + getDirectory: (storage) async => + '${await getQuokkaDirectory()}/Packs', + database: 'quokka.db', + ), + onDecode: (data) => PackData.fromData(data), + onEncode: (data) => data.export(), + ); Future fetchCorePack() async => _corePack ?? await PackData.getCorePack(); - Future> getPacks() async => [ - await fetchCorePack(), - ].whereNotNull().toList(); + Future> getPacks() async { + final corePack = await fetchCorePack(); + await _fileSystem.initialize(); + return { + ...Map.fromEntries((await _fileSystem.getFiles()) + .map((file) => file.data == null + ? null + : MapEntry(file.fileNameWithoutExtension, file.data!)) + .whereNotNull()), + if (corePack != null) '': corePack, + }; + } } diff --git a/app/lib/widgets/loading.dart b/app/lib/widgets/loading.dart index 71d7203..41ef7cd 100644 --- a/app/lib/widgets/loading.dart +++ b/app/lib/widgets/loading.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; LoadingController showLoadingOverlay(BuildContext context) { final GlobalKey<_PlayOverlayState> key = GlobalKey(); @@ -65,7 +66,7 @@ class _PlayOverlayState extends State const CircularProgressIndicator(), const SizedBox(height: 16), Text( - _label ?? 'Playing...', + _label ?? AppLocalizations.of(context).loading, textAlign: TextAlign.center, ), ], diff --git a/app/pack/pack.json b/app/pack/pack.json index e65eba7..3aea865 100644 --- a/app/pack/pack.json +++ b/app/pack/pack.json @@ -1,5 +1,6 @@ { "name": "Core pack", "description": "Official general pack", - "author": "Linwood" + "author": "Linwood", + "version": "1.0.0" } \ No newline at end of file diff --git a/app/pubspec.lock b/app/pubspec.lock index f624ec3..4995db7 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -641,8 +641,8 @@ packages: dependency: "direct main" description: path: "packages/lw_file_system" - ref: "09b05acf67be0640baa5f6a347c7ad9ed6a3831c" - resolved-ref: "09b05acf67be0640baa5f6a347c7ad9ed6a3831c" + ref: "1dc208fb1d91a41b09697411834b147d59db3c29" + resolved-ref: "1dc208fb1d91a41b09697411834b147d59db3c29" url: "https://github.com/LinwoodDev/dart_pkgs.git" source: git version: "1.0.0" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 327f4d4..33d5bd2 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -34,7 +34,7 @@ dependencies: lw_file_system: git: url: https://github.com/LinwoodDev/dart_pkgs.git - ref: 09b05acf67be0640baa5f6a347c7ad9ed6a3831c + ref: 1dc208fb1d91a41b09697411834b147d59db3c29 path: packages/lw_file_system material_leap: git: @@ -106,6 +106,7 @@ flutter: - assets/tiles/ - assets/fonts/Comfortaa-LICENSE.txt - assets/fonts/Roboto-LICENSE.txt + - assets/pack.qka fonts: - family: Comfortaa fonts: