diff --git a/system_theme/CHANGELOG.md b/system_theme/CHANGELOG.md index a8db54c..c76d572 100644 --- a/system_theme/CHANGELOG.md +++ b/system_theme/CHANGELOG.md @@ -1,6 +1,9 @@ -## [3.2.0] - [30/12/2025] +## [3.2.0] - [31/12/2025] * feat: Reactive theming for macOS ([#45](https://github.com/bdlukaa/system_theme/pull/45)) +* chore: Migrate iOS and macOS to Swift Package Manager. ([#46](https://github.com/bdlukaa/system_theme/pull/46), [#45](https://github.com/bdlukaa/system_theme/pull/45)) +* feat: Automatically adjust lightness if the platform doesn't support it natively. ([#46](https://github.com/bdlukaa/system_theme/pull/46)) + This is enabled by default. You can disable it by setting `SystemTheme.autoAdjustLightness` to `false`. ## [3.1.2] - [04/10/2024] diff --git a/system_theme/README.md b/system_theme/README.md index 56bbf40..dde6b3c 100644 --- a/system_theme/README.md +++ b/system_theme/README.md @@ -1,5 +1,5 @@
-

system_theme

+

system_theme

@@ -7,32 +7,22 @@ - - - -

-

- - -

- A flutter plugin to get the current system theme information + A flutter plugin to retrieve the current system theme information

-- [Supported platforms](#supported-platforms) -- [Usage](#usage) - - [Get system accent color](#get-system-accent-color) -- [Contribution](#contribution) - - [Acknowlegments](#acknowlegments) - ### Supported platforms -| Feature | Android 10+ | iOS | Web | MacOs 10.14+ | Windows 10+ and XBox | Linux GTK 3+ | -| ----------------- | :---------: | :-: | :-: | :---------: | :------------------: | :----------: | -| Get accent color | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | -| Listen to changes | | | | ✔️ | ✔️ | | +| Platform | Accent Color | Listen to Changes | Minimum Version | +| :--- | :---: | :---: | :--- | +| **Android** | ✔️ | | Android 10+ | +| **iOS** | ✔️ | | iOS 14+ | +| **Windows** | ✔️ | ✔️ | Windows 10+ | +| **macOS** | ✔️ | ✔️ | Mojave 10.14+ | +| **Linux** | ✔️ | | GTK 3+ | +| **Web** | ✔️ | | All modern browsers | ## Usage @@ -94,25 +84,16 @@ SystemTheme.onChange.listen((event) { Alteratively, you can the `SystemThemeBuilder` widget to listen to changes on the system accent color: ```dart -SystemThemeBuilder(builder: (context, accent) { - return ColoredBox(color: accent.accentColor); -}); -``` - -### Checking if accent color is supported - -The `flutter/foundation` package provides a `defaultTargetPlatform` getter, which can be used to check what platform the current app is running on. - -You can check if the current platform supports accent colors using this extension method: - -```dart -import 'package:flutter/foundation.dart' show defaultTargetPlatform; - -void main() { - final supported = defaultTargetPlatform.supportsAccentColor; - - print('Accent color is: ${supported ? 'supported' : 'not supported'} on the current platform'); -} +SystemThemeBuilder( + builder: (context, color) { + return ColoredBox( + color: color.accent, // Automatically updates when system theme changes + child: const Center( + child: Text('System Accent Color'), + ), + ); + }, +); ``` ## Contribution diff --git a/system_theme/android/src/main/kotlin/com/bruno/system_theme/SystemThemePlugin.kt b/system_theme/android/src/main/kotlin/com/bruno/system_theme/SystemThemePlugin.kt index 0b8f58d..98dd79a 100644 --- a/system_theme/android/src/main/kotlin/com/bruno/system_theme/SystemThemePlugin.kt +++ b/system_theme/android/src/main/kotlin/com/bruno/system_theme/SystemThemePlugin.kt @@ -26,16 +26,19 @@ class SystemThemePlugin: FlutterPlugin, ActivityAware, MethodCallHandler { override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) { when (call.method) { "SystemTheme.accentColor" -> { - val accentColor = getDeviceAccentColor(activity) - val hexColor = java.lang.String.format("#%06X", 0xFFFFFF and accentColor) - val rgb = getRGB(hexColor) + val color = getDeviceAccentColor(activity) + val r = (color shr 16) and 0xFF + val g = (color shr 8) and 0xFF + val b = color and 0xFF + // val a = (color shr 24) and 0xFF + result.success(hashMapOf( - "accent" to hashMapOf( - "R" to rgb[0], - "G" to rgb[1], - "B" to rgb[2], - "A" to 1 - ) + "accent" to hashMapOf( + "R" to r, + "G" to g, + "B" to b, + "A" to 255 + ) )) } else -> { @@ -51,15 +54,6 @@ class SystemThemePlugin: FlutterPlugin, ActivityAware, MethodCallHandler { return value.data } - private fun getRGB(rgb: String): IntArray { - var color = rgb; - if (rgb.startsWith("#")) color = rgb.replace("#", ""); - val r = color.substring(0, 2).toInt(16) // 16 for hex - val g = color.substring(2, 4).toInt(16) // 16 for hex - val b = color.substring(4, 6).toInt(16) // 16 for hex - return intArrayOf(r, g, b) - } - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { channel.setMethodCallHandler(null) } diff --git a/system_theme/example/.gitignore b/system_theme/example/.gitignore index 0fa6b67..a1345d0 100644 --- a/system_theme/example/.gitignore +++ b/system_theme/example/.gitignore @@ -32,7 +32,6 @@ /build/ # Web related -lib/generated_plugin_registrant.dart # Symbolication related app.*.symbols diff --git a/system_theme/example/.metadata b/system_theme/example/.metadata index 784ce12..7ad3ada 100644 --- a/system_theme/example/.metadata +++ b/system_theme/example/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "a14f74ff3a1cbd521163c5f03d68113d50af93d3" + revision: "f6ff1529fd6d8af5f706051d9251ac9231c83407" channel: "stable" project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 - base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 - - platform: web - create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 - base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + - platform: android + create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 # User provided section diff --git a/system_theme/example/android/.gitignore b/system_theme/example/android/.gitignore index 6f56801..be3943c 100644 --- a/system_theme/example/android/.gitignore +++ b/system_theme/example/android/.gitignore @@ -5,9 +5,10 @@ gradle-wrapper.jar /gradlew.bat /local.properties GeneratedPluginRegistrant.java +.cxx/ # Remember to never publicly share your keystore. -# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +# See https://flutter.dev/to/reference-keystore key.properties **/*.keystore **/*.jks diff --git a/system_theme/example/android/app/build.gradle b/system_theme/example/android/app/build.gradle deleted file mode 100644 index f4f24d0..0000000 --- a/system_theme/example/android/app/build.gradle +++ /dev/null @@ -1,71 +0,0 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -android { - compileSdkVersion flutter.compileSdkVersion - ndkVersion flutter.ndkVersion - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = '1.8' - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.bruno.system_theme_example" - // You can update the following values to match your application needs. - // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. - minSdkVersion flutter.minSdkVersion - targetSdkVersion flutter.targetSdkVersion - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} diff --git a/system_theme/example/android/app/build.gradle.kts b/system_theme/example/android/app/build.gradle.kts new file mode 100644 index 0000000..564b35d --- /dev/null +++ b/system_theme/example/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.bruno.system_theme_example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.bruno.system_theme_example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/system_theme/example/android/app/src/debug/AndroidManifest.xml b/system_theme/example/android/app/src/debug/AndroidManifest.xml index f3e987b..399f698 100644 --- a/system_theme/example/android/app/src/debug/AndroidManifest.xml +++ b/system_theme/example/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,4 @@ - + + + + + + + diff --git a/system_theme/example/android/app/src/main/kotlin/com/bruno/system_theme_example/MainActivity.kt b/system_theme/example/android/app/src/main/kotlin/com/bruno/system_theme_example/MainActivity.kt index 7b943a7..168209f 100644 --- a/system_theme/example/android/app/src/main/kotlin/com/bruno/system_theme_example/MainActivity.kt +++ b/system_theme/example/android/app/src/main/kotlin/com/bruno/system_theme_example/MainActivity.kt @@ -2,5 +2,4 @@ package com.bruno.system_theme_example import io.flutter.embedding.android.FlutterActivity -class MainActivity: FlutterActivity() { -} +class MainActivity : FlutterActivity() diff --git a/system_theme/example/android/app/src/profile/AndroidManifest.xml b/system_theme/example/android/app/src/profile/AndroidManifest.xml index f3e987b..399f698 100644 --- a/system_theme/example/android/app/src/profile/AndroidManifest.xml +++ b/system_theme/example/android/app/src/profile/AndroidManifest.xml @@ -1,5 +1,4 @@ - + - + - + system_theme_example + - + + \ No newline at end of file diff --git a/system_theme/example/web/manifest.json b/system_theme/example/web/manifest.json index 096edf8..144aaca 100644 --- a/system_theme/example/web/manifest.json +++ b/system_theme/example/web/manifest.json @@ -1,6 +1,6 @@ { - "name": "example", - "short_name": "example", + "name": "system_theme_example", + "short_name": "system_theme_example", "start_url": ".", "display": "standalone", "background_color": "#0175C2", diff --git a/system_theme/ios/system_theme.podspec b/system_theme/ios/system_theme.podspec index 3322ae4..03dfe61 100644 --- a/system_theme/ios/system_theme.podspec +++ b/system_theme/ios/system_theme.podspec @@ -4,16 +4,17 @@ # Pod::Spec.new do |s| s.name = 'system_theme' - s.version = '0.0.1' - s.summary = 'A new Flutter plugin project.' + s.version = '3.2.0' + s.summary = 'A Flutter Plugin to retrieve the system theme.' s.description = <<-DESC -A new Flutter plugin project. +A plugin to get the current system theme info. Supports Android, Web, Windows, Linux and macOS DESC - s.homepage = 'http://example.com' + s.homepage = 'https://github.com/bdlukaa/system_theme' s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } + s.author = { 'Bruno D Luka' => 'email@example.com' } + s.source = { :path => '.' } - s.source_files = 'Classes/**/*' + s.source_files = 'system_theme/Sources/system_theme/**/*.swift' s.dependency 'Flutter' s.platform = :ios, '11.0' diff --git a/system_theme/ios/system_theme/Package.swift b/system_theme/ios/system_theme/Package.swift new file mode 100644 index 0000000..b3797d2 --- /dev/null +++ b/system_theme/ios/system_theme/Package.swift @@ -0,0 +1,23 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "system_theme", + platforms: [ + .iOS("13") + ], + products: [ + .library(name: "system-theme", targets: ["system_theme"]) + ], + dependencies: [], + targets: [ + .target( + name: "system_theme", + dependencies: [], + resources: [ + ] + ) + ] +) diff --git a/system_theme/ios/Classes/SystemThemePlugin.swift b/system_theme/ios/system_theme/Sources/system_theme/SystemThemePlugin.swift similarity index 100% rename from system_theme/ios/Classes/SystemThemePlugin.swift rename to system_theme/ios/system_theme/Sources/system_theme/SystemThemePlugin.swift diff --git a/system_theme/lib/system_theme.dart b/system_theme/lib/system_theme.dart index ab7e505..43ab5e5 100644 --- a/system_theme/lib/system_theme.dart +++ b/system_theme/lib/system_theme.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart' show TargetPlatform, debugPrint, defaultTargetPlatform, kIsWeb; +import 'package:flutter/painting.dart'; import 'package:flutter/services.dart' show Color, EventChannel, MethodChannel, MissingPluginException; import 'package:flutter/widgets.dart' show WidgetsFlutterBinding; @@ -52,6 +53,12 @@ class SystemTheme { /// Returns [kDefaultFallbackColor] if not set static Color fallbackColor = kDefaultFallbackColor; + /// Whether to automatically adjust lightness if the color if the platform + /// doesn't support it natively. + /// + /// Enabled by default. + static bool autoAdjustLightness = true; + /// Get the system accent color. /// /// This is available for the following platforms: @@ -60,6 +67,7 @@ class SystemTheme { /// - Android /// - iOS /// - Mac + /// - Linux /// /// It returns [kDefaultFallbackColor] for unsupported platforms static final SystemAccentColor accentColor = SystemAccentColor(fallbackColor) @@ -133,13 +141,7 @@ class SystemAccentColor { SystemAccentColor._fromMap(dynamic colors) : defaultAccentColor = SystemTheme.fallbackColor { - accent = _retrieve(colors['accent']) ?? defaultAccentColor; - light = _retrieve(colors['light']) ?? accent; - lighter = _retrieve(colors['lighter']) ?? accent; - lightest = _retrieve(colors['lightest']) ?? accent; - dark = _retrieve(colors['dark']) ?? accent; - darker = _retrieve(colors['darker']) ?? accent; - darkest = _retrieve(colors['darkest']) ?? accent; + _retrieveFromColors(colors); } /// Updates the fetched accent colors on Windows. @@ -149,14 +151,7 @@ class SystemAccentColor { try { final colors = await _channel.invokeMethod(kGetSystemAccentColorMethod); if (colors == null) return; - - accent = _retrieve(colors['accent'])!; - light = _retrieve(colors['light']) ?? accent; - lighter = _retrieve(colors['lighter']) ?? accent; - lightest = _retrieve(colors['lightest']) ?? accent; - dark = _retrieve(colors['dark']) ?? accent; - darker = _retrieve(colors['darker']) ?? accent; - darkest = _retrieve(colors['darkest']) ?? accent; + _retrieveFromColors(colors); } on MissingPluginException { debugPrint('system_theme does not implement the current platform'); return; @@ -175,6 +170,25 @@ class SystemAccentColor { }); } + void _retrieveFromColors(dynamic colors) { + accent = _retrieve(colors['accent']) ?? defaultAccentColor; + + light = _retrieve(colors['light']) ?? _adjustLightness(accent, 0.1); + lighter = _retrieve(colors['lighter']) ?? _adjustLightness(accent, 0.2); + lightest = _retrieve(colors['lightest']) ?? _adjustLightness(accent, 0.3); + + dark = _retrieve(colors['dark']) ?? _adjustLightness(accent, -0.1); + darker = _retrieve(colors['darker']) ?? _adjustLightness(accent, -0.2); + darkest = _retrieve(colors['darkest']) ?? _adjustLightness(accent, -0.3); + } + + Color _adjustLightness(Color color, double amount) { + if (!SystemTheme.autoAdjustLightness) return color; + final hsl = HSLColor.fromColor(color); + final newLightness = (hsl.lightness + amount).clamp(0.0, 1.0); + return hsl.withLightness(newLightness).toColor(); + } + Color? _retrieve(dynamic map) { assert(map == null || map is Map); if (map == null) return null; diff --git a/system_theme/lib/system_theme_builder.dart b/system_theme/lib/system_theme_builder.dart index 1a4260d..e2e48be 100644 --- a/system_theme/lib/system_theme_builder.dart +++ b/system_theme/lib/system_theme_builder.dart @@ -9,8 +9,8 @@ typedef ThemeWidgetBuilder = Widget Function( /// A widget that rebuilds when the system theme changes. /// /// ```dart -/// SystemThemeBuilder(builder: (context, accent) { -/// return ColoredBox(color: accent.accent); +/// SystemThemeBuilder(builder: (context, color) { +/// return ColoredBox(color: color.accent); /// }); /// ``` /// @@ -27,6 +27,7 @@ class SystemThemeBuilder extends StatelessWidget { @override Widget build(BuildContext context) { return StreamBuilder( + initialData: SystemTheme.accentColor, stream: SystemTheme.onChange, builder: (context, snapshot) { return builder(context, snapshot.data ?? SystemTheme.accentColor); diff --git a/system_theme/macos/system_theme/Package.swift b/system_theme/macos/system_theme/Package.swift index a4b5906..29c7bc9 100644 --- a/system_theme/macos/system_theme/Package.swift +++ b/system_theme/macos/system_theme/Package.swift @@ -17,16 +17,6 @@ let package = Package( name: "system_theme", dependencies: [], resources: [ - // TODO: If your plugin requires a privacy manifest - // (e.g. if it uses any required reason APIs), update the PrivacyInfo.xcprivacy file - // to describe your plugin's privacy impact, and then uncomment this line. - // For more information, see: - // https://developer.apple.com/documentation/bundleresources/privacy_manifest_files - // .process("PrivacyInfo.xcprivacy"), - - // TODO: If you have other resources that need to be bundled with your plugin, refer to - // the following instructions to add them: - // https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package ] ) ] diff --git a/system_theme/pubspec.lock b/system_theme/pubspec.lock index 7d8361d..b6a1789 100644 --- a/system_theme/pubspec.lock +++ b/system_theme/pubspec.lock @@ -177,10 +177,10 @@ packages: dependency: "direct main" description: name: system_theme_web - sha256: "900c92c5c050ce58048f241ef9a17e5cd8629808325a05b473dc62a6e99bae77" + sha256: a354b25ff0788ed802b48b632187d344a841b2034f16e4f6cdaa295e4a41a8aa url: "https://pub.dev" source: hosted - version: "0.0.3" + version: "0.0.4" term_glyph: dependency: transitive description: diff --git a/system_theme/pubspec.yaml b/system_theme/pubspec.yaml index 7438cb6..544a9be 100644 --- a/system_theme/pubspec.yaml +++ b/system_theme/pubspec.yaml @@ -1,15 +1,15 @@ name: system_theme -description: A plugin to get the current system theme info. Supports Android, Web, Windows, Linux and macOS +description: A Flutter Plugin to retrieve and listen to the system's accent color. Supports Android, iOS, Web, Windows, macOS, and Linux. version: 3.2.0 repository: https://github.com/bdlukaa/system_theme homepage: https://github.com/bdlukaa/system_theme/tree/master/system_theme environment: - sdk: '>=2.12.0 <4.0.0' + sdk: '>=3.0.0 <4.0.0' flutter: ">=1.20.0" dependencies: - system_theme_web: ^0.0.3 + system_theme_web: ^0.0.4 # path: ../system_theme_web/ flutter: sdk: flutter diff --git a/system_theme/test/system_theme_test.dart b/system_theme/test/system_theme_test.dart index b37ef59..f508c97 100644 --- a/system_theme/test/system_theme_test.dart +++ b/system_theme/test/system_theme_test.dart @@ -1,31 +1,179 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:system_theme/system_theme.dart'; void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + const MethodChannel channel = MethodChannel('system_theme'); - TestWidgetsFlutterBinding.ensureInitialized(); + final List log = []; + + // Helper to create a color map in the format the plugin expects + Map createColorMap( + {int r = 0, int g = 0, int b = 0, int a = 255}) { + return {'R': r, 'G': g, 'B': b, 'A': a}; + } + + void resetSingleton() { + final color = SystemTheme.accentColor; + color.accent = kDefaultFallbackColor; + color.light = kDefaultFallbackColor; + color.lighter = kDefaultFallbackColor; + color.lightest = kDefaultFallbackColor; + color.dark = kDefaultFallbackColor; + color.darker = kDefaultFallbackColor; + color.darkest = kDefaultFallbackColor; + } setUp(() { + log.clear(); + resetSingleton(); + SystemTheme.fallbackColor = kDefaultFallbackColor; + SystemTheme.autoAdjustLightness = true; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, (methodCall) async { - switch (methodCall.method) { - case kGetSystemAccentColorMethod: - return kDefaultFallbackColor.toString(); - default: - return null; + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + log.add(methodCall); + if (methodCall.method == 'SystemTheme.accentColor') { + return { + 'accent': createColorMap(r: 0, g: 0, b: 255), + }; } + return null; }); }); - test('Get accent color', () async { - final color = await channel.invokeMethod(kGetSystemAccentColorMethod); - expect(kDefaultFallbackColor.toString(), color); - }); - tearDown(() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, null); + debugDefaultTargetPlatformOverride = null; + }); + + group('SystemTheme', () { + test('Check platform support for accent color', () { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + expect(defaultTargetPlatform.supportsAccentColor, isTrue); + + debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia; + expect(defaultTargetPlatform.supportsAccentColor, isFalse); + }); + + test('Check platform support for listening to changes', () { + debugDefaultTargetPlatformOverride = TargetPlatform.windows; + expect( + defaultTargetPlatform.supportsListeningToAccentColorChanges, isTrue); + + debugDefaultTargetPlatformOverride = TargetPlatform.android; + expect( + defaultTargetPlatform.supportsListeningToAccentColorChanges, isFalse); + }); + + test('Loads accent color correctly (Singleton)', () async { + await SystemTheme.accentColor.load(); + + expect(log, isNotEmpty); + expect(log.last.method, 'SystemTheme.accentColor'); + + expect(SystemTheme.accentColor.accent, const Color(0xFF0000FF)); + }); + + test('Handles MissingPluginException gracefully', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + throw MissingPluginException(); + }); + + final testTheme = SystemAccentColor(kDefaultFallbackColor); + + await testTheme.load(); + + expect(testTheme.accent, kDefaultFallbackColor); + }); + + test('Respects custom fallback color', () async { + const customFallback = Color(0xFF123456); + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + return null; + }); + + final testTheme = SystemAccentColor(customFallback); + + await testTheme.load(); + + expect(testTheme.accent, customFallback); + }); + }); + + group('SystemAccentColor Variant Logic', () { + test('Auto-generates variants when platform only returns accent', () async { + SystemTheme.autoAdjustLightness = true; + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + return { + 'accent': createColorMap(r: 0, g: 0, b: 255), + }; + }); + + final testTheme = SystemAccentColor(kDefaultFallbackColor); + await testTheme.load(); + + expect(testTheme.accent, const Color(0xFF0000FF)); + expect(testTheme.light, isNot(testTheme.accent)); + expect(testTheme.dark, isNot(testTheme.accent)); + }); + + test('Uses platform variants when provided', () async { + SystemTheme.autoAdjustLightness = true; + + const platformLightColor = Color(0xFF00FF00); + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + return { + 'accent': createColorMap(r: 0, g: 0, b: 255), + 'light': createColorMap(r: 0, g: 255, b: 0), + }; + }); + + final testTheme = SystemAccentColor(kDefaultFallbackColor); + await testTheme.load(); + + expect(testTheme.light, platformLightColor); + }); + + test('Disables auto-adjustment when flag is false', () async { + SystemTheme.autoAdjustLightness = false; + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + return { + 'accent': createColorMap(r: 0, g: 0, b: 255), + }; + }); + + final testTheme = SystemAccentColor(kDefaultFallbackColor); + await testTheme.load(); + + expect(testTheme.light, testTheme.accent); + }); + }); + + group('Object contracts', () { + test('Equality and Hashcode', () async { + final color1 = SystemTheme.accentColor; + final color2 = SystemTheme.accentColor; + + expect(color1, equals(color2)); + expect(color1.hashCode, equals(color2.hashCode)); + }); + + test('toString contains class name', () { + expect(SystemTheme.accentColor.toString(), contains('SystemAccentColor')); + }); }); } diff --git a/system_theme_web/CHANGELOG.md b/system_theme_web/CHANGELOG.md index a95f033..0beed4f 100644 --- a/system_theme_web/CHANGELOG.md +++ b/system_theme_web/CHANGELOG.md @@ -1,10 +1,14 @@ +## 0.0.4 + +- Correctly handle colors without alpha channel. + ## 0.0.3 -- Support RGBA colors +- Support RGBA colors. ## 0.0.2 -- Support for getting accent color +- Support for getting accent color. ## 0.0.1 diff --git a/system_theme_web/lib/system_theme_web.dart b/system_theme_web/lib/system_theme_web.dart index 68f2b53..351b7fc 100644 --- a/system_theme_web/lib/system_theme_web.dart +++ b/system_theme_web/lib/system_theme_web.dart @@ -17,9 +17,6 @@ class SystemThemeWeb { channel.setMethodCallHandler(pluginInstance.handleMethodCall); } - /// Handles method calls over the MethodChannel of this plugin. - /// Note: Check the "federated" architecture for a new way of doing this: - /// https://flutter.dev/go/federated-plugins Future handleMethodCall(MethodCall call) async { switch (call.method) { case 'SystemTheme.accentColor': @@ -34,27 +31,7 @@ class SystemThemeWeb { } if (backgroundColor != null) { - backgroundColor = backgroundColor - // most browsers return rgb, but some may return rgba. - .replaceAll('rgba', 'rgb') - .replaceAll('rgb(', '') - .replaceAll(')', '') - .replaceAll(' ', ''); - final rgb = backgroundColor.split(','); - - final r = int.tryParse(rgb[0]) ?? 255; - final g = int.tryParse(rgb[1]) ?? 255; - final b = int.tryParse(rgb[2]) ?? 255; - final a = int.tryParse(rgb[3]) ?? 255; - - return { - 'accent': { - 'R': r, - 'G': g, - 'B': b, - 'A': a, - } - }; + return extractColorFromCss(backgroundColor); } return null; default: @@ -64,4 +41,84 @@ class SystemThemeWeb { ); } } + + Map extractColorFromCss(String colorString) { + int r = 255; + int g = 255; + int b = 255; + int a = 255; + + // 1. Handle HEX input (e.g. #FFF, #000000, #000000FF) + if (colorString.startsWith('#')) { + var hex = colorString.replaceAll('#', ''); + + // Handle 3-digit hex (e.g. #FFF -> #FFFFFF) + if (hex.length == 3) { + hex = hex.split('').map((c) => '$c$c').join(); + } + + // Handle 6-digit (RRGGBB) or 8-digit (RRGGBBAA) + if (hex.length == 6 || hex.length == 8) { + r = int.parse(hex.substring(0, 2), radix: 16); + g = int.parse(hex.substring(2, 4), radix: 16); + b = int.parse(hex.substring(4, 6), radix: 16); + + if (hex.length == 8) { + a = int.parse(hex.substring(6, 8), radix: 16); + } + } else { + throw PlatformException( + code: 'Unsupported', + details: 'Invalid Hex color format: $colorString', + ); + } + } + // 2. Handle RGB / RGBA input + else if (colorString.startsWith('rgb')) { + final cleanString = colorString + .replaceAll('rgba', '') + .replaceAll('rgb', '') + .replaceAll('(', '') + .replaceAll(')', ''); + + final values = cleanString.split(',').map((s) => s.trim()).toList(); + + if (values.length < 3) { + throw PlatformException( + code: 'Unsupported', + details: 'The accent color is not available in this browser.', + ); + } + + r = int.tryParse(values[0]) ?? 255; + g = int.tryParse(values[1]) ?? 255; + b = int.tryParse(values[2]) ?? 255; + + if (values.length > 3) { + final alphaRaw = values[3]; + // Browser might return alpha as "0.5" (float) or "128" (int) + // If it contains a dot, treat as float 0.0-1.0 and convert to 0-255 + if (alphaRaw.contains('.')) { + final alphaFloat = double.tryParse(alphaRaw) ?? 1.0; + a = (alphaFloat * 255).round(); + } else { + a = int.tryParse(alphaRaw) ?? 255; + } + } + } else { + throw PlatformException( + code: 'Unsupported', + details: 'Unknown color format: $colorString', + ); + } + + return { + 'accent': { + 'R': r, + 'G': g, + 'B': b, + 'A': a, + } + }; + } } diff --git a/system_theme_web/pubspec.yaml b/system_theme_web/pubspec.yaml index a6dc1bc..6efbdab 100644 --- a/system_theme_web/pubspec.yaml +++ b/system_theme_web/pubspec.yaml @@ -1,10 +1,10 @@ name: system_theme_web description: system_theme implementation for the Web -version: 0.0.3 +version: 0.0.4 homepage: https://github.com/bdlukaa/system_theme/tree/master/system_theme_web environment: - sdk: '>=2.12.0 <4.0.0' + sdk: '>=3.0.0 <4.0.0' flutter: ">=1.20.0" dependencies: