diff --git a/lib/pages/settings_views/global_settings_view/appearance_settings/manage_themes.dart b/lib/pages/settings_views/global_settings_view/appearance_settings/manage_themes.dart index 1d0ae9e0f..aa386583c 100644 --- a/lib/pages/settings_views/global_settings_view/appearance_settings/manage_themes.dart +++ b/lib/pages/settings_views/global_settings_view/appearance_settings/manage_themes.dart @@ -1,7 +1,11 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/isar/stack_theme.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/install_theme_from_file_dialog.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/stack_theme_card.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/themes/theme_service.dart'; @@ -14,6 +18,7 @@ import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:tuple/tuple.dart'; class ManageThemesView extends ConsumerStatefulWidget { const ManageThemesView({Key? key}) : super(key: key); @@ -85,39 +90,51 @@ class _ManageThemesViewState extends ConsumerState { ), ], ) - : Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - children: [ - RoundedWhiteContainer( - child: Text( - "You are using Incognito Mode. Please press the" - " button below to load available themes from our server" - " or upload a theme file manually from your device.", - style: STextStyles.smallMed12(context), - ), + : SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: IntrinsicHeight( + child: Column( + children: [ + RoundedWhiteContainer( + child: Text( + "You are using Incognito Mode. Please press the" + " button below to load available themes from our server" + " or upload a theme file manually from your device.", + style: STextStyles.smallMed12(context), + ), + ), + const SizedBox( + height: 12, + ), + PrimaryButton( + label: "Load themes", + onPressed: () { + setState(() { + _showThemes = true; + future = ref.watch(pThemeService).fetchThemes(); + }); + }, + ), + const SizedBox( + height: 12, + ), + SecondaryButton( + label: "Install theme file", + onPressed: _onInstallPressed, + ), + const SizedBox( + height: 16, + ), + const Expanded( + child: _IncognitoInstalledThemes(), + ), + const SizedBox( + height: 16, + ), + ], ), - const SizedBox( - height: 12, - ), - PrimaryButton( - label: "Load themes", - onPressed: () { - setState(() { - _showThemes = true; - future = ref.watch(pThemeService).fetchThemes(); - }); - }, - ), - const SizedBox( - height: 12, - ), - SecondaryButton( - label: "Install theme file", - onPressed: _onInstallPressed, - ), - const Spacer(), - ], + ), ), ), ), @@ -161,3 +178,75 @@ class _ManageThemesViewState extends ConsumerState { ); } } + +class _IncognitoInstalledThemes extends ConsumerStatefulWidget { + const _IncognitoInstalledThemes({Key? key}) : super(key: key); + + @override + ConsumerState<_IncognitoInstalledThemes> createState() => + _IncognitoInstalledThemesState(); +} + +class _IncognitoInstalledThemesState + extends ConsumerState<_IncognitoInstalledThemes> { + late final StreamSubscription _subscription; + + List> installedThemeIdNames = []; + + void _updateInstalledList() { + installedThemeIdNames = ref + .read(pThemeService) + .installedThemes + .where((e) => e.themeId != "light" && e.themeId != "dark") + .map((e) => Tuple2(e.themeId, e.name)) + .toList(); + } + + @override + void initState() { + _updateInstalledList(); + + _subscription = + ref.read(mainDBProvider).isar.stackThemes.watchLazy().listen((_) { + if (mounted) { + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _updateInstalledList(); + }); + }); + } + }); + + super.initState(); + } + + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 16, + runSpacing: 16, + children: installedThemeIdNames + .map( + (e) => SizedBox( + width: (MediaQuery.of(context).size.width - 48) / 2, + child: StackThemeCard( + data: StackThemeMetaData( + name: e.item2, + id: e.item1, + sha256: "", + size: "", + previewImageUrl: "", + ), + ), + ), + ) + .toList(), + ); + } +} diff --git a/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/install_theme_from_file_dialog.dart b/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/install_theme_from_file_dialog.dart index 563561c30..e702cc9eb 100644 --- a/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/install_theme_from_file_dialog.dart +++ b/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/install_theme_from_file_dialog.dart @@ -31,12 +31,20 @@ class _InstallThemeFromFileDialogState Future _install() async { try { - final fileBytes = await File(controller.text).readAsBytes(); - await ref.read(pThemeService).install( - themeArchive: ByteData.view( - fileBytes.buffer, - ), + final timedFuture = Future.delayed(const Duration(seconds: 2)); + final installFuture = File(controller.text).readAsBytes().then( + (fileBytes) => ref.read(pThemeService).install( + themeArchive: ByteData.view( + fileBytes.buffer, + ), + ), ); + + // wait for at least 2 seconds to prevent annoying screen flashing + await Future.wait([ + installFuture, + timedFuture, + ]); return true; } catch (e, s) { Logging.instance.log( diff --git a/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/stack_theme_card.dart b/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/stack_theme_card.dart index 43f0d3edc..f8d62192b 100644 --- a/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/stack_theme_card.dart +++ b/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/stack_theme_card.dart @@ -1,14 +1,20 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/models/isar/stack_theme.dart'; import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/themes/theme_providers.dart'; import 'package:stackwallet/themes/theme_service.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/show_loading.dart'; +import 'package:stackwallet/utilities/stack_file_system.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/animated_text.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -30,6 +36,7 @@ class _StackThemeCardState extends ConsumerState { late final StreamSubscription _subscription; late bool _hasTheme; + String? _cachedSize; Future _downloadAndInstall() async { final service = ref.read(pThemeService); @@ -88,6 +95,34 @@ class _StackThemeCardState extends ConsumerState { prefs.systemBrightnessLightThemeId == themeId; } + Future getThemeDirectorySize() async { + final themesDir = await StackFileSystem.applicationThemesDirectory(); + final themeDir = Directory("${themesDir.path}/${widget.data.id}"); + int bytes = 0; + if (await themeDir.exists()) { + await for (FileSystemEntity entity in themeDir.list(recursive: true)) { + if (entity is File) { + bytes += await entity.length(); + } + } + } else if (widget.data.size.isNotEmpty) { + return widget.data.size; + } + + if (bytes < 1024) { + return '$bytes B'; + } else if (bytes < 1048576) { + double kbSize = bytes / 1024; + return '${kbSize.toStringAsFixed(2)} KB'; + } else if (bytes < 1073741824) { + double mbSize = bytes / 1048576; + return '${mbSize.toStringAsFixed(2)} MB'; + } else { + double gbSize = bytes / 1073741824; + return '${gbSize.toStringAsFixed(2)} GB'; + } + } + @override void initState() { _hasTheme = ref @@ -142,27 +177,77 @@ class _StackThemeCardState extends ConsumerState { padding: const EdgeInsets.symmetric( horizontal: 18, ), - child: AspectRatio( - aspectRatio: 1, - child: ClipRRect( - borderRadius: BorderRadius.circular(100), - child: Image.network( - widget.data.previewImageUrl, - ), - ), - ), + child: widget.data.previewImageUrl.isNotEmpty + ? AspectRatio( + aspectRatio: 1, + child: ClipRRect( + borderRadius: BorderRadius.circular(100), + child: Image.network( + widget.data.previewImageUrl, + ), + ), + ) + : Builder( + builder: (context) { + final incognitoFile = ref.watch( + themeProvider.select( + (value) => value.assets.personaIncognito, + ), + ); + + return (incognitoFile.endsWith(".png")) + ? Image.file( + File( + incognitoFile, + ), + height: 100, + ) + : SvgPicture.file( + File( + incognitoFile, + ), + height: 100, + ); + }, + ), ), const SizedBox( height: 12, ), Text( widget.data.name, + style: STextStyles.itemSubtitle12(context), ), const SizedBox( height: 6, ), - Text( - widget.data.size, + FutureBuilder( + future: getThemeDirectorySize(), + builder: ( + context, + AsyncSnapshot snapshot, + ) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + _cachedSize = snapshot.data; + } + if (_cachedSize == null) { + return AnimatedText( + stringsToLoopThrough: const [ + "Calculating size ", + "Calculating size. ", + "Calculating size.. ", + "Calculating size...", + ], + style: STextStyles.label(context), + ); + } else { + return Text( + _cachedSize!, + style: STextStyles.label(context), + ); + } + }, ), const SizedBox( height: 12,