diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index cac5777c5..941ca7b2e 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2790,5 +2790,8 @@ "oneOfYourDevicesIsNotVerified": "One of your devices is not verified", "noticeChatBackupDeviceVerification": "Note: When you connect all your devices to the chat backup, they are automatically verified.", "continueText": "Continue", - "welcomeText": "Hey Hey 👋 This is FluffyChat. You can sign in to any homeserver, which is compatible with https://matrix.org. And then chat with anyone. It's a huge decentralized messaging network!" + "welcomeText": "Hey Hey 👋 This is FluffyChat. You can sign in to any homeserver, which is compatible with https://matrix.org. And then chat with anyone. It's a huge decentralized messaging network!", + "blur": "Blur:", + "opacity": "Opacity:", + "setWallpaper": "Set wallpaper" } diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index beabe749a..0f7c45570 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -1,3 +1,5 @@ +import 'dart:ui' as ui; + import 'package:flutter/material.dart'; import 'package:badges/badges.dart'; @@ -262,14 +264,21 @@ class ChatView extends StatelessWidget { children: [ if (accountConfig.wallpaperUrl != null) Opacity( - opacity: accountConfig.wallpaperOpacity ?? 1, - child: MxcImage( - uri: accountConfig.wallpaperUrl, - fit: BoxFit.cover, - isThumbnail: true, - width: FluffyThemes.columnWidth * 4, - height: FluffyThemes.columnWidth * 4, - placeholder: (_) => Container(), + opacity: accountConfig.wallpaperOpacity ?? 0.5, + child: ImageFiltered( + imageFilter: ui.ImageFilter.blur( + sigmaX: accountConfig.wallpaperBlur ?? 0.0, + sigmaY: accountConfig.wallpaperBlur ?? 0.0, + ), + child: MxcImage( + cacheKey: accountConfig.wallpaperUrl.toString(), + uri: accountConfig.wallpaperUrl, + fit: BoxFit.cover, + height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.width, + isThumbnail: false, + placeholder: (_) => Container(), + ), ), ), SafeArea( diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 60c1950f0..f72aa9b2c 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -449,12 +449,6 @@ class Message extends StatelessWidget { fontWeight: FontWeight.bold, fontSize: 12 * AppConfig.fontSizeFactor, color: theme.colorScheme.secondary, - shadows: [ - Shadow( - color: theme.colorScheme.surface, - blurRadius: 3, - ), - ], ), ), ), @@ -495,12 +489,6 @@ class Message extends StatelessWidget { fontWeight: FontWeight.bold, fontSize: 12 * AppConfig.fontSizeFactor, color: theme.colorScheme.secondary, - shadows: [ - Shadow( - color: theme.colorScheme.surface, - blurRadius: 3, - ), - ], ), ), ), diff --git a/lib/pages/chat/events/state_message.dart b/lib/pages/chat/events/state_message.dart index ab135a5c1..5fe56d1e7 100644 --- a/lib/pages/chat/events/state_message.dart +++ b/lib/pages/chat/events/state_message.dart @@ -12,8 +12,6 @@ class StateMessage extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Center( @@ -27,12 +25,6 @@ class StateMessage extends StatelessWidget { style: TextStyle( fontSize: 12 * AppConfig.fontSizeFactor, decoration: event.redacted ? TextDecoration.lineThrough : null, - shadows: [ - Shadow( - color: theme.colorScheme.surface, - blurRadius: 3, - ), - ], ), ), ), diff --git a/lib/pages/settings_style/settings_style.dart b/lib/pages/settings_style/settings_style.dart index 121a3504c..49b0b003c 100644 --- a/lib/pages/settings_style/settings_style.dart +++ b/lib/pages/settings_style/settings_style.dart @@ -45,14 +45,59 @@ class SettingsStyleController extends State { ); } - void setChatWallpaperOpacity(double opacity) { + double get wallpaperOpacity => + _wallpaperOpacity ?? + Matrix.of(context).client.applicationAccountConfig.wallpaperOpacity ?? + 0.5; + + double? _wallpaperOpacity; + + void saveWallpaperOpacity(double opacity) async { final client = Matrix.of(context).client; - showFutureLoadingDialog( + final result = await showFutureLoadingDialog( context: context, future: () => client.updateApplicationAccountConfig( ApplicationAccountConfig(wallpaperOpacity: opacity), ), ); + if (result.isValue) return; + + setState(() { + _wallpaperOpacity = client.applicationAccountConfig.wallpaperOpacity; + }); + } + + void updateWallpaperOpacity(double opacity) { + setState(() { + _wallpaperOpacity = opacity; + }); + } + + double get wallpaperBlur => + _wallpaperBlur ?? + Matrix.of(context).client.applicationAccountConfig.wallpaperBlur ?? + 0.5; + double? _wallpaperBlur; + + void saveWallpaperBlur(double blur) async { + final client = Matrix.of(context).client; + final result = await showFutureLoadingDialog( + context: context, + future: () => client.updateApplicationAccountConfig( + ApplicationAccountConfig(wallpaperBlur: blur), + ), + ); + if (result.isValue) return; + + setState(() { + _wallpaperBlur = client.applicationAccountConfig.wallpaperBlur; + }); + } + + void updateWallpaperBlur(double blur) { + setState(() { + _wallpaperBlur = blur; + }); } void deleteChatWallpaper() => showFutureLoadingDialog( @@ -60,7 +105,7 @@ class SettingsStyleController extends State { future: () => Matrix.of(context).client.setApplicationAccountConfig( const ApplicationAccountConfig( wallpaperUrl: null, - wallpaperOpacity: null, + wallpaperBlur: null, ), ), ); @@ -72,10 +117,26 @@ class SettingsStyleController extends State { null, AppConfig.chatColor, Colors.indigo, + Colors.blue, + Colors.blueAccent, + Colors.teal, + Colors.tealAccent, Colors.green, + Colors.greenAccent, + Colors.yellow, + Colors.yellowAccent, Colors.orange, + Colors.orangeAccent, + Colors.red, + Colors.redAccent, Colors.pink, + Colors.pinkAccent, + Colors.purple, + Colors.purpleAccent, Colors.blueGrey, + Colors.grey, + Colors.white, + Colors.black, ]; void switchTheme(ThemeMode? newTheme) { diff --git a/lib/pages/settings_style/settings_style_view.dart b/lib/pages/settings_style/settings_style_view.dart index d40315697..78ea12b4d 100644 --- a/lib/pages/settings_style/settings_style_view.dart +++ b/lib/pages/settings_style/settings_style_view.dart @@ -1,9 +1,14 @@ +import 'dart:ui'; + import 'package:flutter/material.dart'; +import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pages/chat/events/state_message.dart'; import 'package:fluffychat/utils/account_config.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; @@ -32,7 +37,36 @@ class SettingsStyleView extends StatelessWidget { backgroundColor: theme.colorScheme.surface, body: MaxWidthBody( child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + Padding( + padding: const EdgeInsets.all(12.0), + child: SegmentedButton( + selected: {controller.currentTheme}, + onSelectionChanged: (selected) => + controller.switchTheme(selected.single), + segments: [ + ButtonSegment( + value: ThemeMode.light, + label: Text(L10n.of(context).lightTheme), + icon: const Icon(Icons.light_mode_outlined), + ), + ButtonSegment( + value: ThemeMode.dark, + label: Text(L10n.of(context).darkTheme), + icon: const Icon(Icons.dark_mode_outlined), + ), + ButtonSegment( + value: ThemeMode.system, + label: Text(L10n.of(context).systemTheme), + icon: const Icon(Icons.auto_mode_outlined), + ), + ], + ), + ), + Divider( + color: theme.dividerColor, + ), ListTile( title: Text( L10n.of(context).setColorTheme, @@ -42,152 +76,61 @@ class SettingsStyleView extends StatelessWidget { ), ), ), - SizedBox( - height: colorPickerSize + 24, - child: ListView( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - children: SettingsStyleController.customColors - .map( - (color) => Padding( - padding: const EdgeInsets.all(12.0), + DynamicColorBuilder( + builder: (light, dark) { + final systemColor = + Theme.of(context).brightness == Brightness.light + ? light?.primary + : dark?.primary; + final colors = + List.from(SettingsStyleController.customColors); + if (systemColor == null) { + colors.remove(null); + } + return GridView.builder( + shrinkWrap: true, + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 64, + ), + itemCount: colors.length, + itemBuilder: (context, i) { + final color = colors[i]; + return Padding( + padding: const EdgeInsets.all(12.0), + child: Tooltip( + message: color == null + ? L10n.of(context).systemTheme + : '#${color.value.toRadixString(16).toUpperCase()}', child: InkWell( borderRadius: BorderRadius.circular(colorPickerSize), onTap: () => controller.setChatColor(color), - child: color == null - ? Material( - elevation: 6, - shadowColor: AppConfig.colorSchemeSeed, - borderRadius: - BorderRadius.circular(colorPickerSize), - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - colorPickerSize, - ), - gradient: FluffyThemes.backgroundGradient( - context, - 255, + child: Material( + color: color ?? systemColor, + elevation: 6, + borderRadius: + BorderRadius.circular(colorPickerSize), + child: SizedBox( + width: colorPickerSize, + height: colorPickerSize, + child: controller.currentColor == color + ? Center( + child: Icon( + Icons.check, + size: 16, + color: Theme.of(context) + .colorScheme + .onPrimary, ), - ), - child: SizedBox( - height: colorPickerSize, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - ), - child: Center( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (controller.currentColor == - null) - Padding( - padding: - const EdgeInsets.only( - right: 8.0, - ), - child: Icon( - Icons.check, - size: 16, - color: theme - .colorScheme.onSurface, - ), - ), - Text( - L10n.of(context).systemTheme, - textAlign: TextAlign.center, - style: TextStyle( - color: theme - .colorScheme.onSurface, - ), - ), - ], - ), - ), - ), - ), - ), - ) - : Material( - color: color, - elevation: 6, - borderRadius: - BorderRadius.circular(colorPickerSize), - child: SizedBox( - width: colorPickerSize, - height: colorPickerSize, - child: controller.currentColor == color - ? const Center( - child: Icon( - Icons.check, - size: 16, - color: Colors.white, - ), - ) - : null, - ), - ), + ) + : null, + ), + ), ), ), - ) - .toList(), - ), - ), - const SizedBox(height: 8), - Divider( - color: theme.dividerColor, - ), - ListTile( - title: Text( - L10n.of(context).setTheme, - style: TextStyle( - color: theme.colorScheme.secondary, - fontWeight: FontWeight.bold, - ), - ), - ), - RadioListTile( - groupValue: controller.currentTheme, - value: ThemeMode.system, - title: Text(L10n.of(context).systemTheme), - onChanged: controller.switchTheme, - ), - RadioListTile( - groupValue: controller.currentTheme, - value: ThemeMode.light, - title: Text(L10n.of(context).lightTheme), - onChanged: controller.switchTheme, - ), - RadioListTile( - groupValue: controller.currentTheme, - value: ThemeMode.dark, - title: Text(L10n.of(context).darkTheme), - onChanged: controller.switchTheme, - ), - Divider( - color: theme.dividerColor, - ), - ListTile( - title: Text( - L10n.of(context).overview, - style: TextStyle( - color: theme.colorScheme.secondary, - fontWeight: FontWeight.bold, - ), - ), - ), - SettingsSwitchListTile.adaptive( - title: L10n.of(context).presencesToggle, - onChanged: (b) => AppConfig.showPresences = b, - storeKey: SettingKeys.showPresences, - defaultValue: AppConfig.showPresences, - ), - SettingsSwitchListTile.adaptive( - title: L10n.of(context).separateChatTypes, - onChanged: (b) => AppConfig.separateChatTypes = b, - storeKey: SettingKeys.separateChatTypes, - defaultValue: AppConfig.separateChatTypes, + ); + }, + ); + }, ), Divider( color: theme.dividerColor, @@ -213,8 +156,6 @@ class SettingsStyleView extends StatelessWidget { ), builder: (context, snapshot) { final accountConfig = client.applicationAccountConfig; - final wallpaperOpacity = accountConfig.wallpaperOpacity ?? 1; - final wallpaperOpacityIsDefault = wallpaperOpacity == 1; return Column( mainAxisSize: MainAxisSize.min, @@ -222,56 +163,119 @@ class SettingsStyleView extends StatelessWidget { AnimatedContainer( duration: FluffyThemes.animationDuration, curve: FluffyThemes.animationCurve, - alignment: Alignment.centerLeft, decoration: const BoxDecoration(), clipBehavior: Clip.hardEdge, child: Stack( + alignment: Alignment.center, children: [ if (accountConfig.wallpaperUrl != null) Opacity( - opacity: wallpaperOpacity, - child: MxcImage( - uri: accountConfig.wallpaperUrl, - fit: BoxFit.cover, - isThumbnail: true, - width: FluffyThemes.columnWidth * 2, - height: 156, + opacity: controller.wallpaperOpacity, + child: ImageFiltered( + imageFilter: ImageFilter.blur( + sigmaX: controller.wallpaperBlur, + sigmaY: controller.wallpaperBlur, + ), + child: MxcImage( + key: ValueKey(accountConfig.wallpaperUrl), + uri: accountConfig.wallpaperUrl, + fit: BoxFit.cover, + isThumbnail: true, + width: FluffyThemes.columnWidth * 2, + height: 212, + ), ), ), - Padding( - padding: EdgeInsets.only( - left: 12 + 12 + Avatar.defaultSize, - right: 12, - top: accountConfig.wallpaperUrl == null ? 0 : 12, - bottom: 12, - ), - child: Material( - color: theme.colorScheme.primary, - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, + Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 16), + StateMessage( + Event( + eventId: 'style_dummy', + room: + Room(id: '!style_dummy', client: client), + content: {'membership': 'join'}, + type: EventTypes.RoomMember, + senderId: client.userID!, + originServerTs: DateTime.now(), + stateKey: client.userID!, + ), ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, + Padding( + padding: EdgeInsets.only( + left: 12 + 12 + Avatar.defaultSize, + right: 12, + top: accountConfig.wallpaperUrl == null + ? 0 + : 12, + bottom: 12, ), - child: Text( - 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor', - style: TextStyle( - color: theme.colorScheme.onPrimary, - fontSize: AppConfig.messageFontSize * - AppConfig.fontSizeFactor, + child: Material( + color: theme.colorScheme.primary, + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Text( + 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor', + style: TextStyle( + color: theme.colorScheme.onPrimary, + fontSize: AppConfig.messageFontSize * + AppConfig.fontSizeFactor, + ), + ), ), ), ), - ), + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: EdgeInsets.only( + right: 12, + left: 12, + top: accountConfig.wallpaperUrl == null + ? 0 + : 12, + bottom: 12, + ), + child: Material( + color: theme + .colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Text( + 'Lorem ipsum dolor sit amet', + style: TextStyle( + color: theme.colorScheme.onSurface, + fontSize: AppConfig.messageFontSize * + AppConfig.fontSizeFactor, + ), + ), + ), + ), + ), + ), + ], ), ], ), ), ListTile( - title: Text(L10n.of(context).wallpaper), - leading: const Icon(Icons.photo_outlined), + title: OutlinedButton( + onPressed: controller.setWallpaper, + child: Text(L10n.of(context).setWallpaper), + ), trailing: accountConfig.wallpaperUrl == null ? null : IconButton( @@ -279,23 +283,29 @@ class SettingsStyleView extends StatelessWidget { color: theme.colorScheme.error, onPressed: controller.deleteChatWallpaper, ), - onTap: controller.setWallpaper, - ), - AnimatedSize( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - child: accountConfig.wallpaperUrl != null - ? SwitchListTile.adaptive( - title: Text(L10n.of(context).transparent), - secondary: const Icon(Icons.blur_linear_outlined), - value: !wallpaperOpacityIsDefault, - onChanged: (_) => - controller.setChatWallpaperOpacity( - wallpaperOpacityIsDefault ? 0.4 : 1, - ), - ) - : null, ), + if (accountConfig.wallpaperUrl != null) ...[ + ListTile(title: Text(L10n.of(context).opacity)), + Slider.adaptive( + min: 0.1, + max: 1.0, + divisions: 9, + semanticFormatterCallback: (d) => d.toString(), + value: controller.wallpaperOpacity, + onChanged: controller.updateWallpaperOpacity, + onChangeEnd: controller.saveWallpaperOpacity, + ), + ListTile(title: Text(L10n.of(context).blur)), + Slider.adaptive( + min: 0.0, + max: 10.0, + divisions: 10, + semanticFormatterCallback: (d) => d.toString(), + value: controller.wallpaperBlur, + onChanged: controller.updateWallpaperBlur, + onChangeEnd: controller.saveWallpaperBlur, + ), + ], ], ); }, @@ -312,6 +322,30 @@ class SettingsStyleView extends StatelessWidget { semanticFormatterCallback: (d) => d.toString(), onChanged: controller.changeFontSizeFactor, ), + Divider( + color: theme.dividerColor, + ), + ListTile( + title: Text( + L10n.of(context).overview, + style: TextStyle( + color: theme.colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + ), + SettingsSwitchListTile.adaptive( + title: L10n.of(context).presencesToggle, + onChanged: (b) => AppConfig.showPresences = b, + storeKey: SettingKeys.showPresences, + defaultValue: AppConfig.showPresences, + ), + SettingsSwitchListTile.adaptive( + title: L10n.of(context).separateChatTypes, + onChanged: (b) => AppConfig.separateChatTypes = b, + storeKey: SettingKeys.separateChatTypes, + defaultValue: AppConfig.separateChatTypes, + ), ], ), ), diff --git a/lib/utils/account_config.dart b/lib/utils/account_config.dart index ee80e246e..a5e2dfd00 100644 --- a/lib/utils/account_config.dart +++ b/lib/utils/account_config.dart @@ -29,6 +29,7 @@ extension ApplicationAccountConfigExtension on Client { wallpaperUrl: config.wallpaperUrl ?? currentConfig.wallpaperUrl, wallpaperOpacity: config.wallpaperOpacity ?? currentConfig.wallpaperOpacity, + wallpaperBlur: config.wallpaperBlur ?? currentConfig.wallpaperBlur, ).toJson(), ); } @@ -37,10 +38,12 @@ extension ApplicationAccountConfigExtension on Client { class ApplicationAccountConfig { final Uri? wallpaperUrl; final double? wallpaperOpacity; + final double? wallpaperBlur; const ApplicationAccountConfig({ this.wallpaperUrl, this.wallpaperOpacity, + this.wallpaperBlur, }); static double _sanitizedOpacity(double? opacity) { @@ -56,10 +59,12 @@ class ApplicationAccountConfig { : null, wallpaperOpacity: _sanitizedOpacity(json.tryGet('wallpaper_opacity')), + wallpaperBlur: json.tryGet('wallpaper_blur'), ); Map toJson() => { 'wallpaper_url': wallpaperUrl?.toString(), 'wallpaper_opacity': wallpaperOpacity, + 'wallpaper_blur': wallpaperBlur, }; }