diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 70e6715..5f44016 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2031,6 +2031,11 @@ "type": "String", "placeholders": {} }, + "pureBlackToggle": "Pure Black", + "@pureBlackToggle": { + "type": "String", + "placeholders": {} + }, "singlesignon": "Single Sign on", "@singlesignon": { "type": "String", diff --git a/lib/config/setting_keys.dart b/lib/config/setting_keys.dart index 14d2594..2cc0ccf 100644 --- a/lib/config/setting_keys.dart +++ b/lib/config/setting_keys.dart @@ -1,6 +1,7 @@ import 'package:shared_preferences/shared_preferences.dart'; abstract class SettingKeys { + static const String pureBlack = 'xyz.extera.next.pureBlack'; static const String renderHtml = 'chat.fluffy.renderHtml'; static const String hideRedactedEvents = 'chat.fluffy.hideRedactedEvents'; static const String hideUnknownEvents = 'chat.fluffy.hideUnknownEvents'; diff --git a/lib/config/themes.dart b/lib/config/themes.dart index 826f644..87b383b 100644 --- a/lib/config/themes.dart +++ b/lib/config/themes.dart @@ -40,10 +40,38 @@ abstract class FluffyThemes { BuildContext context, Brightness brightness, [ Color? seed, + bool? pureBlack, ]) { + final extraDarkColors = (brightness == Brightness.dark && pureBlack == true) + ? { + 'surface': const Color.fromARGB(255, 0, 0, 0), + 'surfaceBright': const Color.fromARGB(255, 0, 0, 0), + 'surfaceContainer': const Color.fromARGB(255, 22, 22, 22), + 'surfaceContainerHigh': const Color.fromARGB(255, 33, 33, 33), + 'surfaceContainerHighest': const Color.fromARGB(255, 33, 33, 33), + 'surfaceContainerLow': const Color.fromARGB(255, 22, 22, 22), + 'surfaceContainerLowest': const Color.fromARGB(255, 22, 22, 22), + 'surfaceDim': const Color.fromARGB(255, 0, 0, 0), + 'surfaceTint': const Color.fromARGB(255, 11, 11, 11), + 'surfaceVariant': const Color.fromARGB(255, 0, 0, 0), + 'background': const Color.fromARGB(255, 0, 0, 0), + } + : {}; + final colorScheme = ColorScheme.fromSeed( brightness: brightness, seedColor: seed ?? AppConfig.colorSchemeSeed ?? AppConfig.primaryColor, + surface: extraDarkColors['surface'], + surfaceBright: extraDarkColors['surfaceBright'], + surfaceContainer: extraDarkColors['surfaceContainer'], + surfaceContainerHigh: extraDarkColors['surfaceContainerHigh'], + surfaceContainerHighest: extraDarkColors['surfaceContainerHighest'], + surfaceContainerLow: extraDarkColors['surfaceContainerLow'], + surfaceContainerLowest: extraDarkColors['surfaceContainerLowest'], + surfaceDim: extraDarkColors['surfaceDim'], + surfaceTint: extraDarkColors['surfaceTint'], + surfaceVariant: extraDarkColors['surfaceVariant'], + background: extraDarkColors['background'], ); final isColumnMode = FluffyThemes.isColumnMode(context); return ThemeData( diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index e305c93..1013d7d 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -167,7 +167,7 @@ class MessageContent extends StatelessWidget { linkColor: linkColor, ); case MessageTypes.Video: - return EventVideoPlayer(event, textColor: textColor); + return EventVideoPlayer(event, textColor, linkColor, timeline: timeline,); case MessageTypes.File: return MessageDownloadContent( event, diff --git a/lib/pages/chat/events/video_player.dart b/lib/pages/chat/events/video_player.dart index e361809..323dd02 100644 --- a/lib/pages/chat/events/video_player.dart +++ b/lib/pages/chat/events/video_player.dart @@ -1,6 +1,8 @@ import 'dart:io'; import 'package:fluffychat/pages/chat/events/html_message.dart'; +import 'package:fluffychat/pages/image_viewer/image_viewer.dart'; +import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -22,116 +24,40 @@ import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/widgets/blur_hash.dart'; import '../../../utils/error_reporter.dart'; -class EventVideoPlayer extends StatefulWidget { +class EventVideoPlayer extends StatelessWidget { final Event event; - final Color? textColor; - final Color? linkColor; + final Timeline? timeline; + final Color textColor; + final Color linkColor; const EventVideoPlayer( - this.event, { + this.event, this.textColor, - this.linkColor, + this.linkColor, { + this.timeline, super.key, }); - @override - EventVideoPlayerState createState() => EventVideoPlayerState(); -} - -class EventVideoPlayerState extends State { - ChewieController? _chewieController; - VideoPlayerController? _videoPlayerController; - bool _isDownloading = false; - - // The video_player package only doesn't support Windows and Linux. - final _supportsVideoPlayer = - !PlatformInfos.isWindows && !PlatformInfos.isLinux; - - void _downloadAction() async { - if (!_supportsVideoPlayer) { - widget.event.saveFile(context); - return; - } - - setState(() => _isDownloading = true); - - try { - final videoFile = await widget.event.downloadAndDecryptAttachment(); - - // Dispose the controllers if we already have them. - _disposeControllers(); - late VideoPlayerController videoPlayerController; - - // Create the VideoPlayerController from the contents of videoFile. - if (kIsWeb) { - final blob = html.Blob([videoFile.bytes]); - final networkUri = Uri.parse(html.Url.createObjectUrlFromBlob(blob)); - videoPlayerController = VideoPlayerController.networkUrl(networkUri); - } else { - final tempDir = await getTemporaryDirectory(); - final fileName = Uri.encodeComponent( - widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last, - ); - final file = File('${tempDir.path}/${fileName}_${videoFile.name}'); - if (await file.exists() == false) { - await file.writeAsBytes(videoFile.bytes); - } - videoPlayerController = VideoPlayerController.file(file); - } - _videoPlayerController = videoPlayerController; - - await videoPlayerController.initialize(); - - // Create a ChewieController on top. - _chewieController = ChewieController( - videoPlayerController: videoPlayerController, - useRootNavigator: !kIsWeb, - autoPlay: true, - autoInitialize: true, - ); - } on IOException catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(e.toLocalizedString(context)), - ), - ); - } catch (e, s) { - ErrorReporter(context, 'Unable to play video').onErrorCallback(e, s); - } finally { - setState(() => _isDownloading = false); - } - } - - void _disposeControllers() { - _chewieController?.dispose(); - _videoPlayerController?.dispose(); - _chewieController = null; - _videoPlayerController = null; - } - - @override - void dispose() { - _disposeControllers(); - super.dispose(); - } - static const String fallbackBlurHash = 'L5H2EC=PM+yV0g-mq.wG9c010J}I'; @override Widget build(BuildContext context) { - final theme = Theme.of(context); + final supportsVideoPlayer = PlatformInfos.supportsVideoPlayer; - final hasThumbnail = widget.event.hasThumbnail; - final blurHash = (widget.event.infoMap as Map) + final blurHash = (event.infoMap as Map) .tryGet('xyz.amorgan.blurhash') ?? fallbackBlurHash; - final fileDescription = widget.event.fileDescription; - final textColor = widget.textColor; - final linkColor = widget.linkColor; + final fileDescription = event.fileDescription; + final infoMap = event.content.tryGetMap('info'); + final videoWidth = infoMap?.tryGet('w') ?? 400; + final videoHeight = infoMap?.tryGet('h') ?? 300; + const height = 300.0; + final width = videoWidth * (height / videoHeight); - const width = 300.0; + final durationInt = infoMap?.tryGet('duration'); + final duration = + durationInt == null ? null : Duration(milliseconds: durationInt); - final chewieController = _chewieController; return Column( mainAxisSize: MainAxisSize.min, spacing: 8, @@ -139,55 +65,72 @@ class EventVideoPlayerState extends State { Material( color: Colors.black, borderRadius: BorderRadius.circular(AppConfig.borderRadius), - child: SizedBox( - height: width, - child: chewieController != null - ? Center(child: Chewie(controller: chewieController)) - : Stack( - children: [ - if (hasThumbnail) - Center( - child: ImageBubble( - widget.event, - tapToView: false, - textColor: widget.textColor, - ), - ) - else - BlurHash( - blurhash: blurHash, - width: width, - height: width, - ), - Center( - child: IconButton( - style: IconButton.styleFrom( - backgroundColor: theme.colorScheme.surface, - ), - icon: _isDownloading - ? const SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator.adaptive( - strokeWidth: 2, - ), - ) - : _supportsVideoPlayer - ? const Icon(Icons.play_circle_outlined) - : const Icon(Icons.file_download_outlined), - tooltip: _isDownloading - ? L10n.of(context).loadingPleaseWait - : L10n.of(context).videoWithSize( - widget.event.sizeString ?? '?MB', - ), - onPressed: _isDownloading ? null : _downloadAction, + child: InkWell( + onTap: () => supportsVideoPlayer + ? showDialog( + context: context, + builder: (_) => ImageViewer( + event, + timeline: timeline, + outerContext: context, + ), + ) + : event.saveFile(context), + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + child: SizedBox( + width: width, + height: height, + child: Stack( + children: [ + if (event.hasThumbnail) + MxcImage( + event: event, + isThumbnail: true, + width: width, + height: height, + fit: BoxFit.cover, + placeholder: (context) => BlurHash( + blurhash: blurHash, + width: width, + height: height, + fit: BoxFit.cover, + ), + ) + else + BlurHash( + blurhash: blurHash, + width: width, + height: height, + fit: BoxFit.cover, + ), + Center( + child: CircleAvatar( + child: supportsVideoPlayer + ? const Icon(Icons.play_arrow_outlined) + : const Icon(Icons.file_download_outlined), + ), + ), + if (duration != null) + Positioned( + bottom: 8, + left: 16, + child: Text( + '${duration.inMinutes.toString().padLeft(2, '0')}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}', + style: TextStyle( + color: Colors.white, + backgroundColor: Colors.black.withAlpha(32), ), ), - ], - ), + ), + ], + ), + ), ), ), - if (fileDescription != null && textColor != null && linkColor != null && !widget.event.isRichFileDescription) + if (fileDescription != null && + textColor != null && + linkColor != null && + !event.isRichFileDescription) SizedBox( width: width, child: Padding( @@ -215,7 +158,10 @@ class EventVideoPlayerState extends State { ), ), ), - if (fileDescription != null && textColor != null && linkColor != null && widget.event.isRichFileDescription) + if (fileDescription != null && + textColor != null && + linkColor != null && + event.isRichFileDescription) SizedBox( width: width, child: Padding( @@ -224,20 +170,19 @@ class EventVideoPlayerState extends State { vertical: 8, ), child: HtmlMessage( - html: fileDescription, - textColor: textColor, - room: widget.event.room, + html: fileDescription, + textColor: textColor, + room: event.room, + fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize, + linkStyle: TextStyle( + color: linkColor, fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize, - linkStyle: TextStyle( - color: linkColor, - fontSize: - AppConfig.fontSizeFactor * AppConfig.messageFontSize, - decoration: TextDecoration.underline, - decorationColor: linkColor, - ), - onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), + decoration: TextDecoration.underline, + decorationColor: linkColor, ), + onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), + ), ), ), ], diff --git a/lib/pages/chat_search/chat_search_images_tab.dart b/lib/pages/chat_search/chat_search_images_tab.dart index e2465a6..b52f983 100644 --- a/lib/pages/chat_search/chat_search_images_tab.dart +++ b/lib/pages/chat_search/chat_search_images_tab.dart @@ -27,6 +27,7 @@ class ChatSearchImagesTab extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); final borderRadius = BorderRadius.circular(AppConfig.borderRadius / 2); return StreamBuilder( stream: searchStream, @@ -156,7 +157,7 @@ class ChatSearchImagesTab extends StatelessWidget { return Material( clipBehavior: Clip.hardEdge, borderRadius: borderRadius, - child: EventVideoPlayer(event), + child: EventVideoPlayer(event, theme.colorScheme.onSurface, theme.colorScheme.primary), ); } return InkWell( diff --git a/lib/pages/image_viewer/image_viewer.dart b/lib/pages/image_viewer/image_viewer.dart index 80f9b37..d42a845 100644 --- a/lib/pages/image_viewer/image_viewer.dart +++ b/lib/pages/image_viewer/image_viewer.dart @@ -33,7 +33,13 @@ class ImageViewerController extends State { void initState() { super.initState(); allEvents = widget.timeline?.events - .where((event) => event.messageType == MessageTypes.Image) + .where( + (event) => { + MessageTypes.Image, + MessageTypes.Sticker, + if (PlatformInfos.supportsVideoPlayer) MessageTypes.Video, + }.contains(event.messageType), + ) .toList() .reversed .toList() ?? diff --git a/lib/pages/image_viewer/image_viewer_view.dart b/lib/pages/image_viewer/image_viewer_view.dart index 997a9e6..e4f6e88 100644 --- a/lib/pages/image_viewer/image_viewer_view.dart +++ b/lib/pages/image_viewer/image_viewer_view.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; +import 'package:fluffychat/pages/image_viewer/video_player.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/hover_builder.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; @@ -75,27 +77,46 @@ class ImageViewerView extends StatelessWidget { child: PageView.builder( controller: controller.pageController, itemCount: controller.allEvents.length, - itemBuilder: (context, i) => InteractiveViewer( - minScale: 1.0, - maxScale: 10.0, - onInteractionEnd: controller.onInteractionEnds, - child: Center( - child: Hero( - tag: controller.allEvents[i].eventId, - child: GestureDetector( - // Ignore taps to not go back here: - onTap: () {}, - child: MxcImage( - key: ValueKey(controller.allEvents[i].eventId), - event: controller.allEvents[i], - fit: BoxFit.contain, - isThumbnail: false, - animated: true, + itemBuilder: (context, i) { + final event = controller.allEvents[i]; + switch (event.messageType) { + case MessageTypes.Video: + return Padding( + padding: const EdgeInsets.only(top: 52.0), + child: Center( + child: GestureDetector( + // Ignore taps to not go back here: + onTap: () {}, + child: EventVideoPlayer(event), + ), ), - ), - ), - ), - ), + ); + case MessageTypes.Image: + case MessageTypes.Sticker: + default: + return InteractiveViewer( + minScale: 1.0, + maxScale: 10.0, + onInteractionEnd: controller.onInteractionEnds, + child: Center( + child: Hero( + tag: event.eventId, + child: GestureDetector( + // Ignore taps to not go back here: + onTap: () {}, + child: MxcImage( + key: ValueKey(event.eventId), + event: event, + fit: BoxFit.contain, + isThumbnail: false, + animated: true, + ), + ), + ), + ), + ); + } + }, ), ), if (hovered && controller.canGoBack) @@ -130,4 +151,4 @@ class ImageViewerView extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/lib/pages/image_viewer/video_player.dart b/lib/pages/image_viewer/video_player.dart new file mode 100644 index 0000000..a43f245 --- /dev/null +++ b/lib/pages/image_viewer/video_player.dart @@ -0,0 +1,152 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:chewie/chewie.dart'; +import 'package:matrix/matrix.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:universal_html/html.dart' as html; +import 'package:video_player/video_player.dart'; + +import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/blur_hash.dart'; +import '../../../utils/error_reporter.dart'; +import '../../widgets/mxc_image.dart'; + +class EventVideoPlayer extends StatefulWidget { + final Event event; + + const EventVideoPlayer( + this.event, { + super.key, + }); + + @override + EventVideoPlayerState createState() => EventVideoPlayerState(); +} + +class EventVideoPlayerState extends State { + ChewieController? _chewieController; + VideoPlayerController? _videoPlayerController; + + // The video_player package only doesn't support Windows and Linux. + final _supportsVideoPlayer = + !PlatformInfos.isWindows && !PlatformInfos.isLinux; + + void _downloadAction() async { + if (!_supportsVideoPlayer) { + widget.event.saveFile(context); + return; + } + + try { + final videoFile = await widget.event.downloadAndDecryptAttachment(); + + // Dispose the controllers if we already have them. + _disposeControllers(); + late VideoPlayerController videoPlayerController; + + // Create the VideoPlayerController from the contents of videoFile. + if (kIsWeb) { + final blob = html.Blob([videoFile.bytes]); + final networkUri = Uri.parse(html.Url.createObjectUrlFromBlob(blob)); + videoPlayerController = VideoPlayerController.networkUrl(networkUri); + } else { + final tempDir = await getTemporaryDirectory(); + final fileName = Uri.encodeComponent( + widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last, + ); + final file = File('${tempDir.path}/${fileName}_${videoFile.name}'); + if (await file.exists() == false) { + await file.writeAsBytes(videoFile.bytes); + } + videoPlayerController = VideoPlayerController.file(file); + } + _videoPlayerController = videoPlayerController; + + await videoPlayerController.initialize(); + + // Create a ChewieController on top. + _chewieController = ChewieController( + videoPlayerController: videoPlayerController, + useRootNavigator: !kIsWeb, + autoPlay: true, + autoInitialize: true, + looping: true, + ); + } on IOException catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(e.toLocalizedString(context)), + ), + ); + } catch (e, s) { + ErrorReporter(context, 'Unable to play video').onErrorCallback(e, s); + } + } + + void _disposeControllers() { + _chewieController?.dispose(); + _videoPlayerController?.dispose(); + _chewieController = null; + _videoPlayerController = null; + } + + @override + void dispose() { + _disposeControllers(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _downloadAction(); + }); + } + + static const String fallbackBlurHash = 'L5H2EC=PM+yV0g-mq.wG9c010J}I'; + + @override + Widget build(BuildContext context) { + final hasThumbnail = widget.event.hasThumbnail; + final blurHash = (widget.event.infoMap as Map) + .tryGet('xyz.amorgan.blurhash') ?? + fallbackBlurHash; + + const width = 300.0; + + final chewieController = _chewieController; + return chewieController != null + ? Center(child: Chewie(controller: chewieController)) + : Stack( + children: [ + Center( + child: hasThumbnail + ? MxcImage( + event: widget.event, + isThumbnail: true, + width: width, + fit: BoxFit.cover, + placeholder: (context) => BlurHash( + blurhash: blurHash, + width: width, + height: width, + fit: BoxFit.cover, + ), + ) + : BlurHash( + blurhash: blurHash, + width: width, + height: width, + ), + ), + const Center(child: CircularProgressIndicator.adaptive()), + ], + ); + } +} \ No newline at end of file diff --git a/lib/pages/settings_style/settings_style_view.dart b/lib/pages/settings_style/settings_style_view.dart index fc97935..6b6e7e2 100644 --- a/lib/pages/settings_style/settings_style_view.dart +++ b/lib/pages/settings_style/settings_style_view.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:fluffychat/widgets/theme_builder.dart'; import 'package:flutter/material.dart'; import 'package:dynamic_color/dynamic_color.dart'; @@ -66,6 +67,12 @@ class SettingsStyleView extends StatelessWidget { ], ), ), + SettingsSwitchListTile.adaptive( + title: L10n.of(context).pureBlackToggle, + onChanged: (b) => ThemeController.of(context).setPureBlack(b), + storeKey: SettingKeys.pureBlack, + defaultValue: ThemeController.of(context).pureBlack, + ), Divider( color: theme.dividerColor, ), @@ -226,7 +233,7 @@ class SettingsStyleView extends StatelessWidget { vertical: 8, ), child: Text( - 'погнали в роблокс?', + 'Рассказать шутку?', style: TextStyle( color: theme.onBubbleColor, fontSize: AppConfig.messageFontSize * @@ -259,7 +266,7 @@ class SettingsStyleView extends StatelessWidget { vertical: 8, ), child: Text( - 'Го, в toilet tower defense', + 'Давай', style: TextStyle( color: theme.colorScheme.onSurface, fontSize: AppConfig.messageFontSize * @@ -270,6 +277,38 @@ class SettingsStyleView extends StatelessWidget { ), ), ), + Padding( + padding: EdgeInsets.only( + left: 12 + 12 + Avatar.defaultSize, + right: 12, + top: accountConfig.wallpaperUrl == null + ? 0 + : 12, + bottom: 12, + ), + child: DecoratedBox( + decoration: BoxDecoration( + color: theme.bubbleColor, + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Text( + 'Видит заяц как лиса кушает помидор. Он спрашивает: "лиса, а что это ты кушаешь?", она отвечает - помидор.\nЗаяц просит: "А можно мне тоже?", на что лиса отвечает "Нет, но в той стороне медведь раздаёт помидоры.".\nЗаяц поскакал куда лиса указала, а чтобы не забыть, всю дорогу говорил "помидор". Вдруг, он упал и забыл это слово.\n"Как там было..." - подумал заяц, и вспомнил: "А, точно, поморда!"\nЗаяц допрыгивает до того места, и спрашивает медведя: "Медведь, а можно мне поморда?".\nНу и медведь шарахнул битой ему по физиономии.', + style: TextStyle( + color: theme.onBubbleColor, + fontSize: AppConfig.messageFontSize * + AppConfig.fontSizeFactor, + ), + ), + ), + ), + ), ], ), ], diff --git a/lib/utils/platform_infos.dart b/lib/utils/platform_infos.dart index c235483..86de2e6 100644 --- a/lib/utils/platform_infos.dart +++ b/lib/utils/platform_infos.dart @@ -29,6 +29,8 @@ abstract class PlatformInfos { static bool get usesTouchscreen => !isMobile; + static bool get supportsVideoPlayer => !isWindows && !isLinux; + /// Web could also record in theory but currently only wav which is too large static bool get platformCanRecord => (isMobile || isMacOS); diff --git a/lib/widgets/fluffy_chat_app.dart b/lib/widgets/fluffy_chat_app.dart index 661fb33..1b7a325 100644 --- a/lib/widgets/fluffy_chat_app.dart +++ b/lib/widgets/fluffy_chat_app.dart @@ -42,12 +42,12 @@ class FluffyChatApp extends StatelessWidget { @override Widget build(BuildContext context) { return ThemeBuilder( - builder: (context, themeMode, primaryColor) => MaterialApp.router( + builder: (context, themeMode, primaryColor, pureBlack) => MaterialApp.router( title: AppConfig.applicationName, themeMode: themeMode, - theme: FluffyThemes.buildTheme(context, Brightness.light, primaryColor), + theme: FluffyThemes.buildTheme(context, Brightness.light, primaryColor, pureBlack), darkTheme: - FluffyThemes.buildTheme(context, Brightness.dark, primaryColor), + FluffyThemes.buildTheme(context, Brightness.dark, primaryColor, pureBlack), scrollBehavior: CustomScrollBehavior(), localizationsDelegates: L10n.localizationsDelegates, supportedLocales: L10n.supportedLocales, diff --git a/lib/widgets/theme_builder.dart b/lib/widgets/theme_builder.dart index 1ce0a6f..c329cdc 100644 --- a/lib/widgets/theme_builder.dart +++ b/lib/widgets/theme_builder.dart @@ -12,15 +12,18 @@ class ThemeBuilder extends StatefulWidget { BuildContext context, ThemeMode themeMode, Color? primaryColor, + bool pureBlack, ) builder; final String themeModeSettingsKey; final String primaryColorSettingsKey; + final String pureBlackSettingsKey; const ThemeBuilder({ required this.builder, this.themeModeSettingsKey = 'theme_mode', this.primaryColorSettingsKey = 'primary_color', + this.pureBlackSettingsKey = 'pure_black', super.key, }); @@ -32,11 +35,14 @@ class ThemeController extends State { SharedPreferences? _sharedPreferences; ThemeMode? _themeMode; Color? _primaryColor; + bool? _pureBlack; ThemeMode get themeMode => _themeMode ?? ThemeMode.system; Color? get primaryColor => _primaryColor; + bool get pureBlack => _pureBlack ?? false; + static ThemeController of(BuildContext context) => Provider.of( context, @@ -49,11 +55,13 @@ class ThemeController extends State { final rawThemeMode = preferences.getString(widget.themeModeSettingsKey); final rawColor = preferences.getInt(widget.primaryColorSettingsKey); + final rawPureBlack = preferences.getBool(widget.pureBlackSettingsKey); setState(() { _themeMode = ThemeMode.values .singleWhereOrNull((value) => value.name == rawThemeMode); _primaryColor = rawColor == null ? null : Color(rawColor); + _pureBlack = rawPureBlack; }); } @@ -82,6 +90,15 @@ class ThemeController extends State { }); } + Future setPureBlack(bool newPureBlack) async { + final preferences = + _sharedPreferences ??= await SharedPreferences.getInstance(); + await preferences.setBool(widget.pureBlackSettingsKey, newPureBlack); + setState(() { + _pureBlack = newPureBlack; + }); + } + @override void initState() { WidgetsBinding.instance.addPostFrameCallback(_loadData); @@ -97,6 +114,7 @@ class ThemeController extends State { context, themeMode, primaryColor ?? light?.primary, + pureBlack, ), ), ); diff --git a/test/utils/test_client.dart.txt b/test/utils/test_client.dart.txt index 7cbe72f..ec3c0b0 100644 --- a/test/utils/test_client.dart.txt +++ b/test/utils/test_client.dart.txt @@ -3,8 +3,6 @@ import 'package:matrix/encryption/utils/key_verification.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart'; - Future prepareTestClient({ bool loggedIn = false, Uri? homeserver,