diff --git a/assets/svg/file-upload.svg b/assets/svg/file-upload.svg new file mode 100644 index 000000000..0e3576cbe --- /dev/null +++ b/assets/svg/file-upload.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/svg/file.svg b/assets/svg/file.svg new file mode 100644 index 000000000..2c4f26fa8 --- /dev/null +++ b/assets/svg/file.svg @@ -0,0 +1,3 @@ + + + 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 84722a626..e25368ece 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 @@ -34,7 +34,7 @@ class ManageThemesView extends ConsumerStatefulWidget { class _ManageThemesViewState extends ConsumerState { late bool _showThemes; - Future> future = Future(() => []); + Future> Function() future = () async => []; void _onInstallPressed() { showDialog( @@ -46,6 +46,9 @@ class _ManageThemesViewState extends ConsumerState { @override void initState() { _showThemes = ref.read(prefsChangeNotifierProvider).externalCalls; + if (_showThemes) { + future = ref.read(pThemeService).fetchThemes; + } super.initState(); } @@ -121,7 +124,7 @@ class _ManageThemesViewState extends ConsumerState { 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.", + " or install a theme file manually from your device.", style: STextStyles.smallMed12(context), ), ), @@ -133,7 +136,7 @@ class _ManageThemesViewState extends ConsumerState { onPressed: () { setState(() { _showThemes = true; - future = ref.watch(pThemeService).fetchThemes(); + future = ref.watch(pThemeService).fetchThemes; }); }, ), @@ -147,8 +150,11 @@ class _ManageThemesViewState extends ConsumerState { const SizedBox( height: 16, ), - const Expanded( - child: _IncognitoInstalledThemes(), + Expanded( + child: IncognitoInstalledThemes( + cardWidth: + (MediaQuery.of(context).size.width - 48) / 2, + ), ), const SizedBox( height: 16, @@ -164,7 +170,7 @@ class _ManageThemesViewState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.center, children: [ FutureBuilder( - future: future, + future: future(), builder: ( context, AsyncSnapshot> snapshot, @@ -177,6 +183,7 @@ class _ManageThemesViewState extends ConsumerState { children: snapshot.data! .map( (e) => SizedBox( + key: Key("ManageThemesView_card_${e.id}_key"), width: (MediaQuery.of(context).size.width - 48) / 2, child: StackThemeCard( data: e, @@ -200,16 +207,21 @@ class _ManageThemesViewState extends ConsumerState { } } -class _IncognitoInstalledThemes extends ConsumerStatefulWidget { - const _IncognitoInstalledThemes({Key? key}) : super(key: key); +class IncognitoInstalledThemes extends ConsumerStatefulWidget { + const IncognitoInstalledThemes({ + Key? key, + required this.cardWidth, + }) : super(key: key); + + final double cardWidth; @override - ConsumerState<_IncognitoInstalledThemes> createState() => + ConsumerState createState() => _IncognitoInstalledThemesState(); } class _IncognitoInstalledThemesState - extends ConsumerState<_IncognitoInstalledThemes> { + extends ConsumerState { late final StreamSubscription _subscription; List> installedThemeIdNames = []; @@ -255,7 +267,8 @@ class _IncognitoInstalledThemesState children: installedThemeIdNames .map( (e) => SizedBox( - width: (MediaQuery.of(context).size.width - 48) / 2, + key: Key("IncognitoInstalledThemes_card_${e.item1}_key"), + width: widget.cardWidth, child: StackThemeCard( data: StackThemeMetaData( name: e.item2, 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 f8d62192b..b3ecd0b85 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 @@ -8,12 +8,14 @@ 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/stack_colors.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/utilities/util.dart'; import 'package:stackwallet/widgets/animated_text.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; @@ -33,6 +35,7 @@ class StackThemeCard extends ConsumerStatefulWidget { } class _StackThemeCardState extends ConsumerState { + final isDesktop = Util.isDesktop; late final StreamSubscription _subscription; late bool _hasTheme; @@ -170,6 +173,10 @@ class _StackThemeCardState extends ConsumerState { @override Widget build(BuildContext context) { return RoundedWhiteContainer( + radiusMultiplier: isDesktop ? 2.5 : 1, + borderColor: isDesktop + ? Theme.of(context).extension()!.textSubtitle6 + : null, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -259,13 +266,13 @@ class _StackThemeCardState extends ConsumerState { : CrossFadeState.showFirst, firstChild: PrimaryButton( label: "Download", - buttonHeight: ButtonHeight.l, + buttonHeight: isDesktop ? ButtonHeight.s : ButtonHeight.l, onPressed: _downloadPressed, ), secondChild: SecondaryButton( - label: themeIsInUse ? "Theme is active" : "Remove from device", + label: themeIsInUse ? "Theme is active" : "Remove", enabled: !themeIsInUse, - buttonHeight: ButtonHeight.l, + buttonHeight: isDesktop ? ButtonHeight.s : ButtonHeight.l, onPressed: _uninstallThemePressed, ), ), diff --git a/lib/pages_desktop_specific/settings/desktop_settings_view.dart b/lib/pages_desktop_specific/settings/desktop_settings_view.dart index 704647f0d..b30018370 100644 --- a/lib/pages_desktop_specific/settings/desktop_settings_view.dart +++ b/lib/pages_desktop_specific/settings/desktop_settings_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages_desktop_specific/settings/settings_menu.dart'; import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/advanced_settings/advanced_settings.dart'; -import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/appearance_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/appearance_settings/appearance_settings.dart'; import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/backup_and_restore/backup_and_restore_settings.dart'; import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/currency_settings/currency_settings.dart'; import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/language_settings/language_settings.dart'; diff --git a/lib/pages_desktop_specific/settings/settings_menu/appearance_settings.dart b/lib/pages_desktop_specific/settings/settings_menu/appearance_settings/appearance_settings.dart similarity index 84% rename from lib/pages_desktop_specific/settings/settings_menu/appearance_settings.dart rename to lib/pages_desktop_specific/settings/settings_menu/appearance_settings/appearance_settings.dart index 213b2ac0c..7b165395a 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/appearance_settings.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/appearance_settings/appearance_settings.dart @@ -1,8 +1,12 @@ +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:stackwallet/models/isar/stack_theme.dart'; +import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/appearance_settings/sub_widgets/desktop_manage_themes.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_providers.dart'; @@ -200,6 +204,7 @@ class ThemeToggle extends ConsumerStatefulWidget { } class _ThemeToggle extends ConsumerState { + late final StreamSubscription _subscription; late int _current; List> installedThemeIdNames = []; @@ -261,13 +266,24 @@ class _ThemeToggle extends ConsumerState { } } - @override - void initState() { + void _updateInstalledList() { installedThemeIdNames = ref .read(pThemeService) .installedThemes .map((e) => Tuple3(e.themeId, e.name, e.assets.themePreview)) .toList(); + } + + void _manageThemesPressed() { + showDialog( + context: context, + builder: (_) => const DesktopManageThemesDialog(), + ); + } + + @override + void initState() { + _updateInstalledList(); if (ref.read(prefsChangeNotifierProvider).enableSystemBrightness) { _current = installedThemeIdNames.length; @@ -282,9 +298,26 @@ class _ThemeToggle extends ConsumerState { } } + _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( @@ -372,7 +405,56 @@ class _ThemeToggle extends ConsumerState { ), ), ), - ) + ), + Container( + decoration: BoxDecoration( + border: Border.all( + width: 2.5, + color: Theme.of(context).extension()!.popupBG, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Semantics( + label: "Manage themes", + button: true, + excludeSemantics: true, + child: RawMaterialButton( + onPressed: _manageThemesPressed, + elevation: 0, + focusElevation: 0, + hoverElevation: 0, + highlightElevation: 0, + fillColor: Theme.of(context) + .extension()! + .textFieldActiveBG, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Container( + color: Colors.transparent, + height: 160, + width: 200, + child: Center( + child: SvgPicture.asset( + Assets.svg.circlePlusFilled, + color: Theme.of(context) + .extension()! + .textSubtitle2, + width: 20, + height: 20, + ), + ), + ), + ), + ), + ), + ), ], ); } diff --git a/lib/pages_desktop_specific/settings/settings_menu/appearance_settings/sub_widgets/desktop_install_theme.dart b/lib/pages_desktop_specific/settings/settings_menu/appearance_settings/sub_widgets/desktop_install_theme.dart new file mode 100644 index 000000000..4c94f532d --- /dev/null +++ b/lib/pages_desktop_specific/settings/settings_menu/appearance_settings/sub_widgets/desktop_install_theme.dart @@ -0,0 +1,331 @@ +import 'dart:typed_data'; + +import 'package:desktop_drop/desktop_drop.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/themes/theme_service.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/show_loading.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/desktop/outline_blue_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; + +class DesktopInstallTheme extends ConsumerStatefulWidget { + const DesktopInstallTheme({Key? key}) : super(key: key); + + @override + ConsumerState createState() => + _DesktopInstallThemeState(); +} + +class _DesktopInstallThemeState extends ConsumerState { + final _boxKey = GlobalKey(debugLabel: "selectThemeFileBoxKey"); + + XFile? _selectedFile; + bool? _installedState; + Size? _size; + bool _dragging = false; + + Future _install() async { + try { + final timedFuture = Future.delayed(const Duration(seconds: 2)); + final installFuture = _selectedFile!.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( + "Failed to install theme: $e\n$s", + level: LogLevel.Warning, + ); + return false; + } + } + + Future _chooseFile() async { + try { + final result = await FilePicker.platform.pickFiles( + dialogTitle: "Choose theme file", + type: FileType.custom, + allowedExtensions: ["zip"], + lockParentWindow: true, // windows only + ); + + if (result != null && mounted) { + if (result.paths.isNotEmpty && result.paths.first != null) { + setState(() { + _selectedFile = XFile(result.paths.first!); + }); + } + } + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Error); + } + } + + void setBoxSize() { + if (_size == null) { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + setState(() { + _size = _boxKey.currentContext?.size; + }); + }); + } + } + + @override + void initState() { + setBoxSize(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Text( + "Install theme file", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ], + ), + const SizedBox( + height: 12, + ), + DropTarget( + onDragDone: (detail) { + setState(() { + if (detail.files.isNotEmpty) { + _selectedFile = detail.files.first; + } + }); + }, + onDragEntered: (detail) { + setState(() { + _dragging = true; + }); + }, + onDragExited: (detail) { + setState(() { + _dragging = false; + }); + }, + child: RoundedContainer( + key: _boxKey, + height: _size?.height, + color: _dragging + ? Theme.of(context).extension()!.textSubtitle6 + : Theme.of(context).extension()!.popupBG, + borderColor: + Theme.of(context).extension()!.textSubtitle6, + child: _selectedFile == null + ? Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 12, + ), + SvgPicture.asset( + Assets.svg.fileUpload, + color: Theme.of(context) + .extension()! + .textSubtitle2, + width: 24, + height: 24, + ), + const SizedBox( + height: 12, + ), + Text( + "Drag and drop your file here", + style: STextStyles.fieldLabel(context), + ), + const SizedBox( + height: 12, + ), + OutlineBlueButton( + label: "Browse", + buttonHeight: ButtonHeight.s, + width: 140, + onPressed: _chooseFile, + ), + const SizedBox( + height: 12, + ), + ], + ) + : Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + RoundedContainer( + padding: EdgeInsets.zero, + color: Theme.of(context) + .extension()! + .textFieldActiveBG, + width: 300, + child: Row( + children: [ + const SizedBox( + width: 10, + ), + SvgPicture.asset( + Assets.svg.file, + color: Theme.of(context) + .extension()! + .textDark, + width: 16, + height: 16, + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + _selectedFile!.name, + style: STextStyles.w500_14(context), + ), + ), + IconButton( + onPressed: () { + setState(() { + _selectedFile = null; + }); + }, + icon: SvgPicture.asset( + Assets.svg.circleX, + color: Theme.of(context) + .extension()! + .textSubtitle2, + width: 16, + height: 16, + ), + ), + ], + ), + ) + ], + ), + ), + ), + if (_selectedFile != null) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(top: 16, bottom: 20), + child: PrimaryButton( + label: "Install", + buttonHeight: ButtonHeight.s, + width: 140, + enabled: _installedState == null, + onPressed: () async { + final result = await showLoading( + whileFuture: _install(), + context: context, + message: "Installing ${_selectedFile!.name}...", + ); + if (mounted) { + setState(() { + _installedState = result; + }); + + await Future.delayed( + const Duration(milliseconds: 2000), + ).then((_) { + if (mounted) { + setState(() { + _selectedFile = null; + _installedState = null; + }); + } + }); + } + }, + ), + ), + ], + ), + if (_installedState == true) + RoundedContainer( + color: + Theme.of(context).extension()!.snackBarBackSuccess, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.circleX, + color: Theme.of(context) + .extension()! + .snackBarTextSuccess, + width: 16, + height: 16, + ), + const SizedBox( + width: 10, + ), + Text( + "${_selectedFile?.name} theme installed", + style: + STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .snackBarTextSuccess, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + if (_installedState == false) + RoundedContainer( + color: + Theme.of(context).extension()!.snackBarBackError, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.circleX, + color: Theme.of(context) + .extension()! + .snackBarTextError, + width: 16, + height: 16, + ), + const SizedBox( + width: 10, + ), + Text( + "Failed to install ${_selectedFile?.name} theme", + style: + STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .snackBarTextError, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/settings/settings_menu/appearance_settings/sub_widgets/desktop_manage_themes.dart b/lib/pages_desktop_specific/settings/settings_menu/appearance_settings/sub_widgets/desktop_manage_themes.dart new file mode 100644 index 000000000..277bc0c9e --- /dev/null +++ b/lib/pages_desktop_specific/settings/settings_menu/appearance_settings/sub_widgets/desktop_manage_themes.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/appearance_settings/sub_widgets/desktop_install_theme.dart'; +import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/appearance_settings/sub_widgets/desktop_themes_gallery.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/toggle.dart'; + +class DesktopManageThemesDialog extends ConsumerStatefulWidget { + const DesktopManageThemesDialog({Key? key}) : super(key: key); + + @override + ConsumerState createState() => + _DesktopManageThemesDialogState(); +} + +class _DesktopManageThemesDialogState + extends ConsumerState { + static const width = 580.0; + bool _isInstallFromFile = false; + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: width, + maxHeight: 708, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Add more themes", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: SizedBox( + height: 56, + child: Toggle( + isOn: _isInstallFromFile, + onValueChanged: (value) { + if (value != _isInstallFromFile) { + setState(() { + _isInstallFromFile = value; + }); + } + }, + onColor: Theme.of(context) + .extension()! + .rateTypeToggleDesktopColorOn, + offColor: Theme.of(context) + .extension()! + .rateTypeToggleDesktopColorOff, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onText: "Theme gallery", + offText: "Install file", + ), + ), + ), + const SizedBox( + height: 16, + ), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + return AnimatedCrossFade( + crossFadeState: _isInstallFromFile + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + duration: const Duration( + milliseconds: 300, + ), + firstChild: SizedBox( + height: constraints.maxHeight, + child: const DesktopThemeGallery( + dialogWidth: width, + ), + ), + secondChild: SizedBox( + height: constraints.maxHeight, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 32), + child: DesktopInstallTheme(), + ), + ), + ); + }, + ), + ), + const SizedBox( + height: 32, + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/settings/settings_menu/appearance_settings/sub_widgets/desktop_themes_gallery.dart b/lib/pages_desktop_specific/settings/settings_menu/appearance_settings/sub_widgets/desktop_themes_gallery.dart new file mode 100644 index 000000000..3e0bf3ec3 --- /dev/null +++ b/lib/pages_desktop_specific/settings/settings_menu/appearance_settings/sub_widgets/desktop_themes_gallery.dart @@ -0,0 +1,153 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/appearance_settings/manage_themes.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/stack_theme_card.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/themes/theme_service.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/loading_indicator.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class DesktopThemeGallery extends ConsumerStatefulWidget { + const DesktopThemeGallery({ + Key? key, + required this.dialogWidth, + }) : super(key: key); + + final double dialogWidth; + + @override + ConsumerState createState() => + _DesktopThemeGalleryState(); +} + +class _DesktopThemeGalleryState extends ConsumerState { + late bool _showThemes; + Future> Function() future = () async => []; + + @override + void initState() { + _showThemes = ref.read(prefsChangeNotifierProvider).externalCalls; + if (_showThemes) { + future = ref.read(pThemeService).fetchThemes; + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Theme Gallery", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + ], + ), + const SizedBox( + height: 12, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: SingleChildScrollView( + child: _showThemes + ? Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FutureBuilder( + future: future(), + builder: ( + context, + AsyncSnapshot> snapshot, + ) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + return Wrap( + spacing: 16, + runSpacing: 16, + children: snapshot.data! + .map( + (e) => SizedBox( + key: Key( + "_DesktopThemeGalleryState_card_${e.id}_key"), + width: + (widget.dialogWidth - 64 - 32) / 3, + child: StackThemeCard( + data: e, + ), + ), + ) + .toList(), + ); + } else { + return const Center( + child: LoadingIndicator( + width: 200, + ), + ); + } + }, + ), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RoundedWhiteContainer( + borderColor: Theme.of(context) + .extension()! + .textSubtitle6, + child: Text( + "You are using Incognito Mode." + " Please press the button below to load " + "available themes from our server or install a " + "theme file manually from your computer.", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + const SizedBox( + height: 16, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + PrimaryButton( + label: "Load themes", + width: 140, + buttonHeight: ButtonHeight.s, + onPressed: () { + setState(() { + _showThemes = true; + future = ref.read(pThemeService).fetchThemes; + }); + }, + ), + ], + ), + const SizedBox( + height: 20, + ), + IncognitoInstalledThemes( + cardWidth: (widget.dialogWidth - 64 - 32) / 3, + ), + ], + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 2c079377b..3ff594299 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -101,7 +101,6 @@ import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_set import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart'; import 'package:stackwallet/pages/stack_privacy_calls.dart'; -import 'package:stackwallet/widgets/choose_coin_view.dart'; import 'package:stackwallet/pages/token_view/my_tokens_view.dart'; import 'package:stackwallet/pages/token_view/token_contract_details_view.dart'; import 'package:stackwallet/pages/token_view/token_view.dart'; @@ -136,7 +135,7 @@ import 'package:stackwallet/pages_desktop_specific/password/forgot_password_desk import 'package:stackwallet/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart'; import 'package:stackwallet/pages_desktop_specific/settings/desktop_settings_view.dart'; import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/advanced_settings/advanced_settings.dart'; -import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/appearance_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/appearance_settings/appearance_settings.dart'; import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/backup_and_restore/backup_and_restore_settings.dart'; import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/currency_settings/currency_settings.dart'; import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/desktop_about_view.dart'; @@ -151,6 +150,7 @@ import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_ import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/add_wallet_type_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/widgets/choose_coin_view.dart'; import 'package:tuple/tuple.dart'; class RouteGenerator { diff --git a/lib/themes/stack_colors.dart b/lib/themes/stack_colors.dart index 667355848..2aeade5b9 100644 --- a/lib/themes/stack_colors.dart +++ b/lib/themes/stack_colors.dart @@ -1749,6 +1749,30 @@ class StackColors extends ThemeExtension { ), ); + ButtonStyle? getOutlineBlueButtonStyle(BuildContext context) => + Theme.of(context).textButtonTheme.style?.copyWith( + backgroundColor: MaterialStateProperty.all( + Colors.transparent, + ), + side: MaterialStateProperty.all( + BorderSide( + color: customTextButtonEnabledText, + ), + ), + ); + + ButtonStyle? getOutlineBlueButtonDisabledStyle(BuildContext context) => + Theme.of(context).textButtonTheme.style?.copyWith( + backgroundColor: MaterialStateProperty.all( + Colors.transparent, + ), + side: MaterialStateProperty.all( + BorderSide( + color: customTextButtonDisabledText, + ), + ), + ); + ButtonStyle? getSecondaryEnabledButtonStyle(BuildContext context) => Theme.of(context).textButtonTheme.style?.copyWith( backgroundColor: MaterialStateProperty.all( diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index d473b3ef8..2a0909966 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -182,6 +182,8 @@ class _SVG { String get tokens => "assets/svg/tokens.svg"; String get circlePlusDark => "assets/svg/circle-plus.svg"; String get creditCard => "assets/svg/cc.svg"; + String get file => "assets/svg/file.svg"; + String get fileUpload => "assets/svg/file-upload.svg"; String get ellipse1 => "assets/svg/Ellipse-43.svg"; String get ellipse2 => "assets/svg/Ellipse-42.svg"; diff --git a/lib/widgets/desktop/outline_blue_button.dart b/lib/widgets/desktop/outline_blue_button.dart new file mode 100644 index 000000000..406337f84 --- /dev/null +++ b/lib/widgets/desktop/outline_blue_button.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/custom_text_button.dart'; + +export 'package:stackwallet/widgets/desktop/custom_text_button.dart'; + +class OutlineBlueButton extends StatelessWidget { + const OutlineBlueButton({ + Key? key, + this.width, + this.height, + this.label, + this.icon, + this.onPressed, + this.enabled = true, + this.buttonHeight, + this.iconSpacing = 10, + }) : super(key: key); + + final double? width; + final double? height; + final String? label; + final VoidCallback? onPressed; + final bool enabled; + final Widget? icon; + final ButtonHeight? buttonHeight; + final double? iconSpacing; + + TextStyle getStyle(bool isDesktop, BuildContext context) { + if (isDesktop) { + if (buttonHeight == null) { + return enabled + ? STextStyles.desktopButtonEnabled(context).copyWith( + color: Theme.of(context) + .extension()! + .customTextButtonEnabledText, + ) + : STextStyles.desktopButtonDisabled(context).copyWith( + color: Theme.of(context) + .extension()! + .customTextButtonDisabledText, + ); + } + + switch (buttonHeight!) { + case ButtonHeight.xxs: + case ButtonHeight.xs: + case ButtonHeight.s: + return STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: enabled + ? Theme.of(context) + .extension()! + .customTextButtonEnabledText + : Theme.of(context) + .extension()! + .customTextButtonDisabledText, + ); + + case ButtonHeight.m: + case ButtonHeight.l: + return STextStyles.desktopTextExtraSmall(context).copyWith( + color: enabled + ? Theme.of(context) + .extension()! + .customTextButtonEnabledText + : Theme.of(context) + .extension()! + .customTextButtonDisabledText, + ); + + case ButtonHeight.xl: + case ButtonHeight.xxl: + return enabled + ? STextStyles.desktopButtonEnabled(context) + : STextStyles.desktopButtonDisabled(context); + } + } else { + if (buttonHeight == ButtonHeight.l) { + return STextStyles.button(context).copyWith( + fontSize: 10, + color: enabled + ? Theme.of(context) + .extension()! + .customTextButtonEnabledText + : Theme.of(context) + .extension()! + .customTextButtonDisabledText, + ); + } + return STextStyles.button(context).copyWith( + color: enabled + ? Theme.of(context) + .extension()! + .customTextButtonEnabledText + : Theme.of(context) + .extension()! + .customTextButtonDisabledText, + ); + } + } + + double? _getHeight() { + if (buttonHeight == null) { + return height; + } + + if (Util.isDesktop) { + switch (buttonHeight!) { + case ButtonHeight.xxs: + return 32; + case ButtonHeight.xs: + return 37; + case ButtonHeight.s: + return 40; + case ButtonHeight.m: + return 48; + case ButtonHeight.l: + return 56; + case ButtonHeight.xl: + return 70; + case ButtonHeight.xxl: + return 96; + } + } else { + switch (buttonHeight!) { + case ButtonHeight.xxs: + case ButtonHeight.xs: + case ButtonHeight.s: + case ButtonHeight.m: + return 28; + case ButtonHeight.l: + return 30; + case ButtonHeight.xl: + return 46; + case ButtonHeight.xxl: + return 56; + } + } + } + + @override + Widget build(BuildContext context) { + final isDesktop = Util.isDesktop; + + return CustomTextButtonBase( + height: _getHeight(), + width: width, + textButton: TextButton( + onPressed: enabled ? onPressed : null, + style: enabled + ? Theme.of(context) + .extension()! + .getOutlineBlueButtonStyle(context) + : Theme.of(context) + .extension()! + .getOutlineBlueButtonDisabledStyle(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (icon != null) icon!, + if (icon != null && label != null) + SizedBox( + width: iconSpacing, + ), + if (label != null) + Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + label!, + style: getStyle(isDesktop, context), + ), + if (buttonHeight != null && buttonHeight == ButtonHeight.s) + const SizedBox( + height: 2, + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index a4836c367..cb1ecd755 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -329,6 +329,8 @@ flutter: - assets/svg/framed-gear.svg - assets/svg/list-ul.svg - assets/svg/cc.svg + - assets/svg/file.svg + - assets/svg/file-upload.svg - assets/svg/trocador_rating_a.svg - assets/svg/trocador_rating_b.svg - assets/svg/trocador_rating_c.svg