From 09042541d7a7e6b9f42c751b229dd960cdfb2255 Mon Sep 17 00:00:00 2001 From: koukibadr Date: Sun, 23 Aug 2026 11:46:47 +0300 Subject: [PATCH] feat: setup dark theme support and update example --- example/lib/main.dart | 18 +++++++++++++--- example/lib/pages/home_page.dart | 33 +++++++++++++++++++++++++++- lib/flutter_skin.dart | 37 ++++++++++++++++++++++++++++++++ lib/models/skin_model.dart | 8 +++++++ test/mocks/skin_model_mocks.dart | 10 +++++++++ 5 files changed, 102 insertions(+), 4 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index b8d124c..38c9115 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -6,7 +6,7 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); await FlutterSkin.init( apiKey: - "fsk_dc0054468a27dde1671142669f2065f93870975bbb773f1af0ab5898797956db", + "fsk_b0ce429cfbded17bbca66eef6e68bd1f0b7fbf9d74be0c3a0ac8b2e0554b7919", ); runApp(const MyApp()); } @@ -19,6 +19,8 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { + ThemeMode _themeMode = ThemeMode.dark; + @override void initState() { super.initState(); @@ -31,8 +33,18 @@ class _MyAppState extends State { Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', - theme: FlutterSkin.toThemeData(), - home: const MyHomePage(title: 'Movie Browser'), + theme: FlutterSkin.toThemeData(fallbackTheme: ThemeData.light()), + darkTheme: FlutterSkin.darkTheme(fallbackTheme: ThemeData.dark()), + themeMode: _themeMode, + home: MyHomePage( + title: 'Movie Browser', + themeMode: _themeMode, + onThemeModeChanged: (themeMode) { + setState(() { + _themeMode = themeMode; + }); + }, + ), ); } } diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index b3f3e18..792dc2c 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -4,9 +4,16 @@ import '../widgets/movie_card.dart'; import '../widgets/movie_details_sidebar.dart'; class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); + const MyHomePage({ + super.key, + required this.title, + required this.themeMode, + required this.onThemeModeChanged, + }); final String title; + final ThemeMode themeMode; + final ValueChanged onThemeModeChanged; @override State createState() => _MyHomePageState(); @@ -104,6 +111,30 @@ class _MyHomePageState extends State { widget.title, style: TextStyle(color: theme.colorScheme.onPrimary), ), + actions: [ + PopupMenuButton( + tooltip: 'Theme settings', + icon: Icon( + widget.themeMode == ThemeMode.dark + ? Icons.dark_mode + : Icons.light_mode, + color: theme.colorScheme.onPrimary, + ), + onSelected: widget.onThemeModeChanged, + itemBuilder: (context) => [ + CheckedPopupMenuItem( + value: ThemeMode.light, + checked: widget.themeMode == ThemeMode.light, + child: const Text('Light mode'), + ), + CheckedPopupMenuItem( + value: ThemeMode.dark, + checked: widget.themeMode == ThemeMode.dark, + child: const Text('Dark mode'), + ), + ], + ), + ], ), body: Row( children: [ diff --git a/lib/flutter_skin.dart b/lib/flutter_skin.dart index a360089..84f316a 100644 --- a/lib/flutter_skin.dart +++ b/lib/flutter_skin.dart @@ -123,6 +123,43 @@ class FlutterSkin with WidgetsBindingObserver { return null; } + /// Query current active theme from remote config and return as ThemeData. + /// When there's no active theme, the result is null or fallbackTheme if provided. + static ThemeData? darkTheme({ + ThemeData? fallbackTheme, + String? fallbackFont, + }) { + ProjectConfig? config = remoteConfig.projectConfig; + ColorScheme? darkTheme = config?.skin?.darkTheme; + + String? fontFamily = (config?.skin?.fontFamily?.isEmpty == true) + ? null + : config?.skin?.fontFamily; + + String? googleFont = config?.skin?.googleFont?.isEmpty == true + ? null + : config?.skin?.googleFont; + + ThemeData remoteTheme = ThemeData( + colorScheme: darkTheme, + fontFamily: + fontFamily ?? + (googleFont != null + ? GoogleFonts.getFont(googleFont).fontFamily + : fallbackFont ?? 'Roboto'), + ); + if (darkTheme == null) { + if (fallbackTheme != null) { + _logger.logWarning('No active theme found. Returning fallback theme.'); + return fallbackTheme; + } + } else { + _logger.logMessage('Active theme found. Returning remote theme.'); + return remoteTheme; + } + return null; + } + /// Query current active theme from remote config and return as ThemeData. /// When there's no active theme, the result is null. static ThemeData? get theme { diff --git a/lib/models/skin_model.dart b/lib/models/skin_model.dart index 9f9f334..e777e69 100644 --- a/lib/models/skin_model.dart +++ b/lib/models/skin_model.dart @@ -10,6 +10,7 @@ class SkinModel { final DateTime? publishedAt; final DateTime? deletedAt; final ColorScheme? colors; + final ColorScheme? darkTheme; final String? fontFamily; final String? googleFont; @@ -20,6 +21,7 @@ class SkinModel { required this.version, required this.createdAt, required this.colors, + this.darkTheme, this.fontFamily, this.googleFont, this.publishedAt, @@ -29,6 +31,8 @@ class SkinModel { /// Factory constructor to create SkinModel from a map factory SkinModel.fromMap(Map map) { final tokens = map['tokens']; + final darkTheme = map['darkTheme']; + return SkinModel( id: map['id'] as String? ?? '', projectId: map['projectId'] as String? ?? '', @@ -46,6 +50,9 @@ class SkinModel { colors: tokens is Map ? fromSchemaString(tokens)?.colors : null, + darkTheme: darkTheme is Map + ? fromSchemaString(darkTheme)?.colors + : null, fontFamily: map['font'] as String?, googleFont: map['googleFont'] as String?, ); @@ -77,6 +84,7 @@ class SkinModel { 'publishedAt': publishedAt?.toIso8601String(), 'deletedAt': deletedAt?.toIso8601String(), 'colors': colors != null ? colorSchemeToJson(colors!) : null, + 'darkTheme': darkTheme != null ? colorSchemeToJson(darkTheme!) : null, 'font': fontFamily, 'googleFont': googleFont, }; diff --git a/test/mocks/skin_model_mocks.dart b/test/mocks/skin_model_mocks.dart index f1df147..235d7c1 100644 --- a/test/mocks/skin_model_mocks.dart +++ b/test/mocks/skin_model_mocks.dart @@ -14,6 +14,16 @@ var skinModelMock = SkinModel( onBackground: Color(0xFF000000), onSurface: Color(0xFF000000), ), + darkTheme: const ColorScheme.light( + primary: Color(0xFF6200EE), + secondary: Color(0xFF03DAC6), + background: Color(0xFFFFFFFF), + surface: Color(0xFFFFFFFF), + onPrimary: Color(0xFFFFFFFF), + onSecondary: Color(0xFF000000), + onBackground: Color(0xFF000000), + onSurface: Color(0xFF000000), + ), id: 'mock_id', projectId: 'mock_project_id', isActive: true,