diff --git a/android/app/build.gradle b/android/app/build.gradle index 24b775a..e1d566e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -51,7 +51,7 @@ android { applicationId "gg.norisk.noriskclient" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion 23 + minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/lib/config/Colors.dart b/lib/config/Colors.dart index b2b86e7..a592ecf 100644 --- a/lib/config/Colors.dart +++ b/lib/config/Colors.dart @@ -10,4 +10,4 @@ class NoRiskClientColors { // text static const Color text = Color.fromARGB(255, 255, 255, 255); static const Color textLight = Color.fromARGB(200, 130, 130, 130); -} \ No newline at end of file +} diff --git a/lib/config/ThemeProvider.dart b/lib/config/ThemeProvider.dart new file mode 100644 index 0000000..e20d492 --- /dev/null +++ b/lib/config/ThemeProvider.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class ThemeProvider extends ChangeNotifier { + Color _blue = const Color(0xFF0066CC); + Color _background = const Color(0xFF1A1A2E); + Color _darkerBackground = const Color(0xFF0F0F1E); + Color _text = Colors.grey; + + Color get blue => _blue; + Color get background => _background; + Color get darkerBackground => _darkerBackground; + Color get text => _text; + + ThemeProvider() { + _loadSavedColor(); + } + + Future _loadSavedColor() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + String? savedColorHex = prefs.getString('theme_color'); + + if (savedColorHex != null) { + _blue = _hexToColor(savedColorHex); + notifyListeners(); + } + } + + Future setBlue(Color color) async { + _blue = color; + + SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setString('theme_color', _colorToHex(color)); + + notifyListeners(); + } + + String _colorToHex(Color color) { + return '#${color.red.toRadixString(16).padLeft(2, '0')}' + '${color.green.toRadixString(16).padLeft(2, '0')}' + '${color.blue.toRadixString(16).padLeft(2, '0')}'; + } + + Color _hexToColor(String hex) { + hex = hex.replaceAll('#', ''); + return Color(int.parse('FF$hex', radix: 16)); + } +} diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 70f4c82..79924bb 100755 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -71,6 +71,7 @@ "settings_privacyPolicy": "Datenschutz", "settings_imprint": "Impressum", "settings_support": "Support", + "settings_theme": "Theme", "mcReal_profile_blockUserPopupTitle": "Bist du sicher?", "mcReal_profile_blockUserPopupContent": "Möchtest du diesen Spieler blockieren?", "mcReal_profile_unblockUserPopupTitle": "Bist du sicher?", @@ -84,5 +85,7 @@ "chat_message_deleted": "Diese Nachricht wurde gelöscht.", "chat_chat_empty": "Leerer Chat", "navbar_you": "Du", - "gamescom_no_infos": "Aktuell sind keine Infos verfügbar." + "gamescom_no_infos": "Aktuell sind keine Infos verfügbar.", + "theme_apply": "Anwenden", + "theme_color_picker_title": "Farb Auswahl" } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6f8f1d1..bbafd25 100755 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -71,6 +71,7 @@ "settings_privacyPolicy": "Privacy Policy", "settings_imprint": "Imprint", "settings_support": "Support", + "settings_theme": "Theme", "mcReal_profile_blockUserPopupTitle": "Are you sure?", "mcReal_profile_blockUserPopupContent": "Are you sure you want to block this player?", "mcReal_profile_unblockUserPopupTitle": "Are you sure?", @@ -84,5 +85,7 @@ "chat_message_deleted": "This message was deleted.", "chat_chat_empty": "Empty Chat", "navbar_you": "You", - "gamescom_no_infos": "There are no infos available at the moment." + "gamescom_no_infos": "There are no infos available at the moment.", + "theme_apply": "Apply", + "theme_color_picker_title": "Color Picker" } \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index f34d270..e34ece3 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -527,6 +527,12 @@ abstract class AppLocalizations { /// **'Support'** String get settings_support; + /// No description provided for @settings_theme. + /// + /// In en, this message translates to: + /// **'Theme'** + String get settings_theme; + /// No description provided for @mcReal_profile_blockUserPopupTitle. /// /// In en, this message translates to: @@ -610,6 +616,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'There are no infos available at the moment.'** String get gamescom_no_infos; + + /// No description provided for @theme_apply. + /// + /// In en, this message translates to: + /// **'Apply'** + String get theme_apply; + + /// No description provided for @theme_color_picker_title. + /// + /// In en, this message translates to: + /// **'Color Picker'** + String get theme_color_picker_title; } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 441f064..38617b0 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -224,6 +224,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_support => 'Support'; + @override + String get settings_theme => 'Theme'; + @override String get mcReal_profile_blockUserPopupTitle => 'Bist du sicher?'; @@ -265,4 +268,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get gamescom_no_infos => 'Aktuell sind keine Infos verfügbar.'; + + @override + String get theme_apply => 'Anwenden'; + + @override + String get theme_color_picker_title => 'Farb Auswahl'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index e7479b2..3d3a543 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -224,6 +224,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settings_support => 'Support'; + @override + String get settings_theme => 'Theme'; + @override String get mcReal_profile_blockUserPopupTitle => 'Are you sure?'; @@ -265,4 +268,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get gamescom_no_infos => 'There are no infos available at the moment.'; + + @override + String get theme_apply => 'Apply'; + + @override + String get theme_color_picker_title => 'Color Picker'; } diff --git a/lib/main.dart b/lib/main.dart index 2a3545c..f6bd1e1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,7 +5,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; -import 'package:noriskclient/config/Colors.dart'; +import 'package:noriskclient/config/Colors.dart'; +import 'package:noriskclient/config/ThemeProvider.dart'; import 'package:noriskclient/provider/localeProvider.dart'; import 'package:noriskclient/NoRiskClient.dart'; import 'package:noriskclient/screens/SignIn.dart'; @@ -17,7 +18,12 @@ import 'package:shared_preferences/shared_preferences.dart'; void main() { WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); - runApp(const App()); + runApp( + ChangeNotifierProvider( + create: (_) => ThemeProvider(), + child: const App(), + ), + ); } late bool isIOS; @@ -77,7 +83,7 @@ class AppState extends State { } super.initState(); - + loadUserData(); updateStream.stream.listen((List data) async { String event = data[0]; diff --git a/lib/screens/Settings.dart b/lib/screens/Settings.dart index 6844b57..f9b1d5a 100644 --- a/lib/screens/Settings.dart +++ b/lib/screens/Settings.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:noriskclient/config/ThemeProvider.dart'; import 'package:noriskclient/l10n/app_localizations.dart'; import 'package:noriskclient/config/Colors.dart'; import 'package:noriskclient/main.dart'; @@ -10,6 +11,7 @@ import 'package:noriskclient/screens/settings/Blocked.dart'; import 'package:noriskclient/widgets/NoRiskBackButton.dart'; import 'package:noriskclient/widgets/NoRiskContainer.dart'; import 'package:noriskclient/widgets/NoRiskText.dart'; +import 'package:noriskclient/widgets/SettingsColorPicker.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -34,423 +36,449 @@ class SettingsState extends State { @override Widget build(BuildContext context) { - return Scaffold( - resizeToAvoidBottomInset: true, - backgroundColor: NoRiskClientColors.background, - body: Padding( - padding: const EdgeInsets.all(15), - child: Column(children: [ - const SizedBox(height: 60), - Stack( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(top: 7.5), - child: NoRiskBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Consumer(builder: (context, theme, _) { + return Scaffold( + resizeToAvoidBottomInset: true, + backgroundColor: theme.background, + body: Padding( + padding: const EdgeInsets.all(15), + child: Column(children: [ + const SizedBox(height: 60), + Stack( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(top: 7.5), + child: NoRiskBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), ), - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - NoRiskText( - AppLocalizations.of(context)! - .settings_title - .toLowerCase(), - spaceTop: false, - spaceBottom: false, - style: const TextStyle( - color: NoRiskClientColors.text, - fontSize: 45, - fontWeight: FontWeight.bold)), - ], - ), - ], - ), - SizedBox( - width: double.infinity, - height: MediaQuery.of(context).size.height - 160, - child: ListView(children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const SizedBox(width: 5), - NoRiskText( - AppLocalizations.of(context)! - .settings_language - .toLowerCase(), - spaceTop: false, - spaceBottom: false, - style: const TextStyle( - color: NoRiskClientColors.text, - fontSize: 25, - fontWeight: FontWeight.bold)), - ], - ), - const SizedBox(height: 5), - GestureDetector( - onTap: () => setLanguage('de'), - child: NoRiskContainer( - width: double.infinity, - height: 50, - color: AppLocalizations.of(context)!.localeName == 'de' - ? NoRiskClientColors.blue - : NoRiskClientColors.text, - child: Center( - child: NoRiskText('Deutsch'.toLowerCase(), - style: TextStyle( - color: - AppLocalizations.of(context)!.localeName == - 'de' - ? NoRiskClientColors.blue - : NoRiskClientColors.text, - fontSize: 30, - fontWeight: FontWeight.bold)), - ), + ], ), - ), - const SizedBox(height: 5), - GestureDetector( - onTap: () => setLanguage('en'), - child: NoRiskContainer( - width: double.infinity, - height: 50, - color: AppLocalizations.of(context)!.localeName == 'en' - ? NoRiskClientColors.blue - : NoRiskClientColors.text, - child: Center( - child: NoRiskText('English'.toLowerCase(), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + NoRiskText( + AppLocalizations.of(context)! + .settings_title + .toLowerCase(), spaceTop: false, spaceBottom: false, - style: TextStyle( - color: - AppLocalizations.of(context)!.localeName == - 'en' - ? NoRiskClientColors.blue - : NoRiskClientColors.text, - fontSize: 30, + style: const TextStyle( + color: NoRiskClientColors.text, + fontSize: 45, fontWeight: FontWeight.bold)), - ), + ], ), - ), - const SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const SizedBox(width: 5), - NoRiskText( - AppLocalizations.of(context)! - .settings_blockedPlayers - .toLowerCase(), - spaceTop: false, - spaceBottom: false, - style: const TextStyle( - color: NoRiskClientColors.text, - fontSize: 25, - fontWeight: FontWeight.bold)), - ], - ), - const SizedBox(height: 5), - GestureDetector( - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => Blocked())), - child: NoRiskContainer( - width: double.infinity, - height: 50, - child: Center( - child: NoRiskText( + ], + ), + SizedBox( + width: double.infinity, + height: MediaQuery.of(context).size.height - 160, + child: ListView(children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox(width: 5), + NoRiskText( AppLocalizations.of(context)! - .settings_blockedPlayers + .settings_language .toLowerCase(), spaceTop: false, spaceBottom: false, style: const TextStyle( color: NoRiskClientColors.text, - fontSize: 30, + fontSize: 25, fontWeight: FontWeight.bold)), + ], + ), + const SizedBox(height: 5), + GestureDetector( + onTap: () => setLanguage('de'), + child: NoRiskContainer( + width: double.infinity, + height: 50, + color: AppLocalizations.of(context)!.localeName == 'de' + ? theme.blue + : theme.text, + child: Center( + child: NoRiskText('Deutsch'.toLowerCase(), + style: TextStyle( + color: AppLocalizations.of(context)! + .localeName == + 'de' + ? theme.blue + : theme.text, + fontSize: 30, + fontWeight: FontWeight.bold)), + ), ), ), - ), - const SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const SizedBox(width: 5), - NoRiskText( - AppLocalizations.of(context)! - .settings_legal - .toLowerCase(), - spaceTop: false, - spaceBottom: false, - style: const TextStyle( - color: NoRiskClientColors.text, - fontSize: 25, - fontWeight: FontWeight.bold)), - ], - ), - const SizedBox(height: 5), - GestureDetector( - onTap: () => launchUrl( - mode: LaunchMode.externalApplication, Config.termsUrl), - child: NoRiskContainer( - width: double.infinity, - height: 50, - child: Center( - child: NoRiskText( + const SizedBox(height: 5), + GestureDetector( + onTap: () => setLanguage('en'), + child: NoRiskContainer( + width: double.infinity, + height: 50, + color: AppLocalizations.of(context)!.localeName == 'en' + ? theme.blue + : theme.text, + child: Center( + child: NoRiskText('English'.toLowerCase(), + spaceTop: false, + spaceBottom: false, + style: TextStyle( + color: AppLocalizations.of(context)! + .localeName == + 'en' + ? theme.blue + : theme.text, + fontSize: 30, + fontWeight: FontWeight.bold)), + ), + ), + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox(width: 5), + NoRiskText( AppLocalizations.of(context)! - .settings_tos + .settings_blockedPlayers .toLowerCase(), spaceTop: false, spaceBottom: false, style: const TextStyle( color: NoRiskClientColors.text, - fontSize: 30, + fontSize: 25, fontWeight: FontWeight.bold)), + ], + ), + const SizedBox(height: 5), + GestureDetector( + onTap: () => Navigator.push(context, + MaterialPageRoute(builder: (context) => Blocked())), + child: NoRiskContainer( + width: double.infinity, + height: 50, + child: Center( + child: NoRiskText( + AppLocalizations.of(context)! + .settings_blockedPlayers + .toLowerCase(), + spaceTop: false, + spaceBottom: false, + style: const TextStyle( + color: NoRiskClientColors.text, + fontSize: 30, + fontWeight: FontWeight.bold)), + ), ), ), - ), - const SizedBox(height: 5), - GestureDetector( - onTap: () => launchUrl( - mode: LaunchMode.externalApplication, - Config.privacyUrl), - child: NoRiskContainer( - width: double.infinity, - height: 50, - child: Center( - child: NoRiskText( + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox(width: 5), + NoRiskText( AppLocalizations.of(context)! - .settings_privacyPolicy + .settings_theme .toLowerCase(), spaceTop: false, spaceBottom: false, style: const TextStyle( color: NoRiskClientColors.text, - fontSize: 30, + fontSize: 25, fontWeight: FontWeight.bold)), - ), + ], ), - ), - const SizedBox(height: 5), - GestureDetector( - onTap: () => launchUrl( - mode: LaunchMode.externalApplication, - Config.imprintUrl), - child: NoRiskContainer( - width: double.infinity, - height: 50, - decoration: BoxDecoration( - color: NoRiskClientColors.darkerBackground, - borderRadius: BorderRadius.circular(10)), - child: Center( - child: NoRiskText( + const SizedBox(height: 5), + SettingsColorPicker( + selectedColor: theme.blue, + onColorChange: (color) { + Provider.of(context, listen: false) + .setBlue(color); + }, + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox(width: 5), + NoRiskText( AppLocalizations.of(context)! - .settings_imprint + .settings_legal .toLowerCase(), spaceTop: false, spaceBottom: false, style: const TextStyle( color: NoRiskClientColors.text, - fontSize: 30, + fontSize: 25, fontWeight: FontWeight.bold)), + ], + ), + const SizedBox(height: 5), + GestureDetector( + onTap: () => launchUrl( + mode: LaunchMode.externalApplication, + Config.termsUrl), + child: NoRiskContainer( + width: double.infinity, + height: 50, + child: Center( + child: NoRiskText( + AppLocalizations.of(context)! + .settings_tos + .toLowerCase(), + spaceTop: false, + spaceBottom: false, + style: const TextStyle( + color: NoRiskClientColors.text, + fontSize: 30, + fontWeight: FontWeight.bold)), + ), ), ), - ), - const SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const SizedBox(width: 5), - NoRiskText( - AppLocalizations.of(context)! - .settings_support - .toLowerCase(), - spaceTop: false, - spaceBottom: false, - style: const TextStyle( - color: NoRiskClientColors.text, - fontSize: 25, - fontWeight: FontWeight.bold)), - ], - ), - const SizedBox(height: 5), - GestureDetector( - onTap: () => launchUrl(Config.supportUrl), - child: NoRiskContainer( - width: double.infinity, - height: 50, - color: Colors.green, - child: Center( - child: NoRiskText( + const SizedBox(height: 5), + GestureDetector( + onTap: () => launchUrl( + mode: LaunchMode.externalApplication, + Config.privacyUrl), + child: NoRiskContainer( + width: double.infinity, + height: 50, + child: Center( + child: NoRiskText( + AppLocalizations.of(context)! + .settings_privacyPolicy + .toLowerCase(), + spaceTop: false, + spaceBottom: false, + style: const TextStyle( + color: NoRiskClientColors.text, + fontSize: 30, + fontWeight: FontWeight.bold)), + ), + ), + ), + const SizedBox(height: 5), + GestureDetector( + onTap: () => launchUrl( + mode: LaunchMode.externalApplication, + Config.imprintUrl), + child: NoRiskContainer( + width: double.infinity, + height: 50, + decoration: BoxDecoration( + color: theme.darkerBackground, + borderRadius: BorderRadius.circular(10)), + child: Center( + child: NoRiskText( + AppLocalizations.of(context)! + .settings_imprint + .toLowerCase(), + spaceTop: false, + spaceBottom: false, + style: const TextStyle( + color: NoRiskClientColors.text, + fontSize: 30, + fontWeight: FontWeight.bold)), + ), + ), + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox(width: 5), + NoRiskText( AppLocalizations.of(context)! .settings_support .toLowerCase(), spaceTop: false, spaceBottom: false, style: const TextStyle( - color: Colors.green, - fontSize: 30, + color: NoRiskClientColors.text, + fontSize: 25, fontWeight: FontWeight.bold)), - ), + ], ), - ), - if (['DEVELOPER', 'ADMIN'].contains(cache['profiles'] - ?[getUserData['uuid']]?['nrcUser']?['rank'] - ?.toString() - .toUpperCase() ?? - 'DEFAULT')) - Column(children: [ - const SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const SizedBox(width: 5), - NoRiskText('Admin Options'.toLowerCase(), + const SizedBox(height: 5), + GestureDetector( + onTap: () => launchUrl(Config.supportUrl), + child: NoRiskContainer( + width: double.infinity, + height: 50, + color: Colors.green, + child: Center( + child: NoRiskText( + AppLocalizations.of(context)! + .settings_support + .toLowerCase(), spaceTop: false, spaceBottom: false, style: const TextStyle( - color: NoRiskClientColors.text, - fontSize: 25, + color: Colors.green, + fontSize: 30, fontWeight: FontWeight.bold)), - ], + ), ), - const SizedBox(height: 5), - GestureDetector( - onTap: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => - const ScanQRCode(isAdminScan: true))), - child: NoRiskContainer( - width: double.infinity, - height: 50, - child: Center( - child: NoRiskText('Get Giveaway Info'.toLowerCase(), + ), + if (['DEVELOPER', 'ADMIN'].contains(cache['profiles'] + ?[getUserData['uuid']]?['nrcUser']?['rank'] + ?.toString() + .toUpperCase() ?? + 'DEFAULT')) + Column(children: [ + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox(width: 5), + NoRiskText('Admin Options'.toLowerCase(), spaceTop: false, spaceBottom: false, style: const TextStyle( - color: Colors.white, - fontSize: 30, + color: NoRiskClientColors.text, + fontSize: 25, fontWeight: FontWeight.bold)), + ], + ), + const SizedBox(height: 5), + GestureDetector( + onTap: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + const ScanQRCode(isAdminScan: true))), + child: NoRiskContainer( + width: double.infinity, + height: 50, + child: Center( + child: NoRiskText( + 'Get Giveaway Info'.toLowerCase(), + spaceTop: false, + spaceBottom: false, + style: const TextStyle( + color: Colors.white, + fontSize: 30, + fontWeight: FontWeight.bold)), + ), ), ), - ), - ]), - const SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const SizedBox(width: 5), - NoRiskText('Updates'.toLowerCase(), - spaceTop: false, - spaceBottom: false, - style: const TextStyle( - color: NoRiskClientColors.text, - fontSize: 25, - fontWeight: FontWeight.bold)), - ], - ), - const SizedBox(height: 5), - GestureDetector( - onTap: () => launchUrl( - isAndroid ? Config.playStoreUrl : Config.appStoreUrl), - child: NoRiskContainer( - width: double.infinity, - height: 50, - child: Center( - child: NoRiskText( - (isAndroid ? 'PlayStore' : 'AppStore') - .toLowerCase(), + ]), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox(width: 5), + NoRiskText('Updates'.toLowerCase(), spaceTop: false, spaceBottom: false, style: const TextStyle( - fontSize: 30, - fontWeight: FontWeight.bold, - color: NoRiskClientColors.text)), + color: NoRiskClientColors.text, + fontSize: 25, + fontWeight: FontWeight.bold)), + ], + ), + const SizedBox(height: 5), + GestureDetector( + onTap: () => launchUrl( + isAndroid ? Config.playStoreUrl : Config.appStoreUrl), + child: NoRiskContainer( + width: double.infinity, + height: 50, + child: Center( + child: NoRiskText( + (isAndroid ? 'PlayStore' : 'AppStore') + .toLowerCase(), + spaceTop: false, + spaceBottom: false, + style: const TextStyle( + fontSize: 30, + fontWeight: FontWeight.bold, + color: NoRiskClientColors.text)), + ), ), ), - ), - const SizedBox(height: 50), - GestureDetector( - onTap: () { - getUpdateStream.sink.add(['signOut']); - Navigator.of(context).pop(); - }, - child: NoRiskContainer( - width: double.infinity, - height: 50, - color: Colors.red, - child: Center( + const SizedBox(height: 50), + GestureDetector( + onTap: () { + getUpdateStream.sink.add(['signOut']); + Navigator.of(context).pop(); + }, + child: NoRiskContainer( + width: double.infinity, + height: 50, + color: Colors.red, + child: Center( + child: NoRiskText( + AppLocalizations.of(context)! + .settings_signOut + .toLowerCase(), + spaceTop: false, + spaceBottom: false, + style: const TextStyle( + color: Colors.red, + fontSize: 30, + fontWeight: FontWeight.bold)), + ), + ), + ), + const SizedBox(height: 20), + if (packageInfo != null) + Center( child: NoRiskText( - AppLocalizations.of(context)! - .settings_signOut + "Version ${packageInfo!.version} - ${packageInfo!.buildNumber}" .toLowerCase(), spaceTop: false, spaceBottom: false, style: const TextStyle( - color: Colors.red, - fontSize: 30, - fontWeight: FontWeight.bold)), + color: NoRiskClientColors.textLight, + fontSize: 25)), ), - ), - ), - const SizedBox(height: 20), - if (packageInfo != null) + const SizedBox(height: 5), Center( - child: NoRiskText( - "Version ${packageInfo!.version} - ${packageInfo!.buildNumber}" - .toLowerCase(), - spaceTop: false, - spaceBottom: false, - style: const TextStyle( - color: NoRiskClientColors.textLight, - fontSize: 25)), - ), - const SizedBox(height: 5), - Center( - child: GestureDetector( - onTap: () => launchUrlString( - 'https://timlohrer.dev', - mode: LaunchMode.externalApplication), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - NoRiskText('Made with'.toLowerCase(), - spaceTop: false, - style: const TextStyle( - color: NoRiskClientColors.textLight, - fontSize: 25, - fontWeight: FontWeight.bold)), - Padding( - padding: const EdgeInsets.only(bottom: 2.5), - child: Text(' 🧡 '.toLowerCase(), + child: GestureDetector( + onTap: () => launchUrlString('https://timlohrer.dev', + mode: LaunchMode.externalApplication), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + NoRiskText('Made with'.toLowerCase(), + spaceTop: false, style: const TextStyle( color: NoRiskClientColors.textLight, - fontSize: 13.5, + fontSize: 25, fontWeight: FontWeight.bold)), - ), - NoRiskText('by Tim Lohrer'.toLowerCase(), - spaceTop: false, - style: const TextStyle( - color: NoRiskClientColors.textLight, - fontSize: 25, - fontWeight: FontWeight.bold)), - ], + Padding( + padding: const EdgeInsets.only(bottom: 2.5), + child: Text(' 🧡 '.toLowerCase(), + style: const TextStyle( + color: NoRiskClientColors.textLight, + fontSize: 13.5, + fontWeight: FontWeight.bold)), + ), + NoRiskText('by Tim Lohrer'.toLowerCase(), + spaceTop: false, + style: const TextStyle( + color: NoRiskClientColors.textLight, + fontSize: 25, + fontWeight: FontWeight.bold)), + ], + ), ), ), - ), - ]), - ) - ]))); + ]), + ) + ]))); + }); } Future setLanguage(String language) async { diff --git a/lib/widgets/NoRiskText.dart b/lib/widgets/NoRiskText.dart index cc81f22..76a6439 100644 --- a/lib/widgets/NoRiskText.dart +++ b/lib/widgets/NoRiskText.dart @@ -35,11 +35,13 @@ class NoRiskText extends Text { fontFamily: 'SmallCapsMC'), textAlign: textAlign, textDirection: textDirection, - textHeightBehavior: TextHeightBehavior(applyHeightToFirstAscent: spaceTop, applyHeightToLastDescent: spaceBottom), + textHeightBehavior: TextHeightBehavior( + applyHeightToFirstAscent: spaceTop, + applyHeightToLastDescent: spaceBottom), locale: locale, softWrap: softWrap, overflow: overflow, maxLines: maxLines, strutStyle: strutStyle, ); -} \ No newline at end of file +} diff --git a/lib/widgets/NoRiskTextField.dart b/lib/widgets/NoRiskTextField.dart index 615c92e..355a975 100644 --- a/lib/widgets/NoRiskTextField.dart +++ b/lib/widgets/NoRiskTextField.dart @@ -39,73 +39,78 @@ class NoRiskTextField extends StatelessWidget { return NoRiskContainer( padding: const EdgeInsets.only(left: 5, right: 5), constraints: BoxConstraints( - maxHeight: 60, - minHeight: 60, - maxWidth: width, - minWidth: width - ), + maxHeight: 60, minHeight: 60, maxWidth: width, minWidth: width), child: Row( children: [ TextField( - decoration: InputDecoration( - constraints: BoxConstraints( - minHeight: 70, - maxHeight: 70, - maxWidth: width - (hasSendButton ? 80 : 0), - minWidth: width - (hasSendButton ? 80 : 0), - ), - contentPadding: EdgeInsets.all(0), - disabledBorder: InputBorder.none, - enabledBorder: InputBorder.none, - border: InputBorder.none, - focusedBorder: InputBorder.none, - hint: NoRiskText( - hintText?.toLowerCase() ?? '', - spaceTop: false, - spaceBottom: false, - style: const TextStyle( - color: NoRiskClientColors.text, - fontSize: 20, - ), - ), - counter: SizedBox(width: 0, height: 0), - counterStyle: const TextStyle(fontFamily: 'SmallCapsMC', color: Colors.white, fontSize: 17.5), + decoration: InputDecoration( + constraints: BoxConstraints( + minHeight: 70, + maxHeight: 70, + maxWidth: width - (hasSendButton ? 80 : 0), + minWidth: width - (hasSendButton ? 80 : 0), + ), + contentPadding: EdgeInsets.all(0), + disabledBorder: InputBorder.none, + enabledBorder: InputBorder.none, + border: InputBorder.none, + focusedBorder: InputBorder.none, + hint: NoRiskText( + hintText?.toLowerCase() ?? '', + spaceTop: false, + spaceBottom: false, + style: const TextStyle( + color: NoRiskClientColors.text, + fontSize: 20, ), - minLines: 1, - enabled: true, - maxLines: 5, - controller: controller, - focusNode: focusNode, - keyboardType: TextInputType.text, - maxLength: Config.maxCommentContentLength, - cursorHeight: 10, - style: const TextStyle(fontFamily: 'SmallCapsMC', color: NoRiskClientColors.text, fontSize: 25, height: 0.5), - canRequestFocus: true, - onSubmitted: (value) => onSubmitted != null ? onSubmitted!(value, false) : null, - onEditingComplete: () => focusNode?.unfocus(), - onTapOutside: (e) => focusNode?.unfocus(), ), + counter: SizedBox(width: 0, height: 0), + counterStyle: const TextStyle( + fontFamily: 'SmallCapsMC', + color: Colors.white, + fontSize: 17.5), + ), + minLines: 1, + enabled: true, + maxLines: 5, + controller: controller, + focusNode: focusNode, + keyboardType: TextInputType.text, + maxLength: Config.maxCommentContentLength, + cursorHeight: 10, + style: const TextStyle( + fontFamily: 'SmallCapsMC', + color: NoRiskClientColors.text, + fontSize: 25, + height: 0.5), + canRequestFocus: true, + onSubmitted: (value) => + onSubmitted != null ? onSubmitted!(value, false) : null, + onEditingComplete: () => focusNode?.unfocus(), + onTapOutside: (e) => focusNode?.unfocus(), + ), + if (hasSendButton) const SizedBox(width: 5), if (hasSendButton) - const SizedBox(width: 5), - if (hasSendButton) - Padding( - padding: const EdgeInsets.only(top: 5, bottom: 5), - child: GestureDetector( - onTap: onSubmitted != null ? () => onSubmitted!(controller.text, true) : null, - child: NoRiskContainer( - width: 50, - color: NoRiskClientColors.blue, - padding: const EdgeInsets.only(bottom: 2.5), - child: Center( - child: NoRiskText('send', - spaceTop: false, - spaceBottom: false, - style: const TextStyle( - color: NoRiskClientColors.text, fontSize: 20)), - ), + Padding( + padding: const EdgeInsets.only(top: 5, bottom: 5), + child: GestureDetector( + onTap: onSubmitted != null + ? () => onSubmitted!(controller.text, true) + : null, + child: NoRiskContainer( + width: 50, + color: NoRiskClientColors.blue, + padding: const EdgeInsets.only(bottom: 2.5), + child: Center( + child: NoRiskText('send', + spaceTop: false, + spaceBottom: false, + style: const TextStyle( + color: NoRiskClientColors.text, fontSize: 20)), ), ), ), + ), ], ), ); diff --git a/lib/widgets/SettingsColorInfo.dart b/lib/widgets/SettingsColorInfo.dart new file mode 100644 index 0000000..bc3c3cc --- /dev/null +++ b/lib/widgets/SettingsColorInfo.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:noriskclient/config/Colors.dart'; +import 'package:noriskclient/widgets/NoRiskContainer.dart'; +import 'package:noriskclient/widgets/NoRiskText.dart'; + +class SettingsColorInfo extends StatelessWidget { + final Color color; + + const SettingsColorInfo({ + super.key, + required this.color, + }); + + String colorToHex(Color color) => + '#${color.red.toRadixString(16).padLeft(2, '0').toUpperCase()}' + '${color.green.toRadixString(16).padLeft(2, '0').toUpperCase()}' + '${color.blue.toRadixString(16).padLeft(2, '0').toUpperCase()}'; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildColorValueColumn("HEX", colorToHex(color), 100), + _buildColorValueColumn("R", color.red.toString(), 70), + _buildColorValueColumn("G", color.green.toString(), 70), + _buildColorValueColumn("B", color.blue.toString(), 70), + ], + ); + } + + Widget _buildColorValueColumn(String label, String value, double width) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NoRiskText( + label, + spaceTop: false, + spaceBottom: false, + style: const TextStyle( + color: Colors.white, + fontSize: 25, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 5), + NoRiskContainer( + width: width, + height: 50, + color: Colors.grey, + child: Center( + child: NoRiskText( + value, + spaceTop: false, + spaceBottom: false, + style: const TextStyle( + color: NoRiskClientColors.text, + fontSize: 20, + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/SettingsColorPicker.dart b/lib/widgets/SettingsColorPicker.dart new file mode 100644 index 0000000..6d0b80f --- /dev/null +++ b/lib/widgets/SettingsColorPicker.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:noriskclient/widgets/NoRiskContainer.dart'; +import 'package:noriskclient/widgets/SettingsColorSelector.dart'; + +class SettingsColorPicker extends StatefulWidget { + final Color selectedColor; + final ValueChanged onColorChange; + + const SettingsColorPicker({ + super.key, + required this.selectedColor, + required this.onColorChange, + }); + + @override + State createState() => _SettingsColorPicker(); +} + +class _SettingsColorPicker extends State { + final List _colors = const [ + Color(0xFF0066CC), + Color(0xFF00A3FF), + Color(0xFF1E88E5), + Color(0xFF4CAF50), + Color(0xFFFFC107), + Color(0xFFD32F2F), + ]; + + late Color _selectedColor; + + @override + void initState() { + super.initState(); + _selectedColor = widget.selectedColor; + } + + @override + void didUpdateWidget(SettingsColorPicker oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.selectedColor != widget.selectedColor) { + setState(() { + _selectedColor = widget.selectedColor; + }); + } + } + + bool _isPresetColor() { + return _colors.any((color) => _colorsEqual(color, _selectedColor)); + } + + bool _colorsEqual(Color a, Color b) { + return a.toARGB32() == b.toARGB32(); + } + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 12, + runSpacing: 12, + children: [ + ..._colors.map((color) { + final bool isSelected = _colorsEqual(_selectedColor, color); + return GestureDetector( + onTap: () { + setState(() => _selectedColor = color); + widget.onColorChange(color); + }, + child: NoRiskContainer( + height: 44, + width: 44, + color: color, + child: isSelected + ? const Icon(Icons.check, color: Colors.white, size: 22) + : null, + )); + }), + GestureDetector( + onTap: () async { + await _openColorWheel(); + }, + child: NoRiskContainer( + width: 44, + height: 44, + color: _selectedColor, + child: Icon( + _isPresetColor() ? Icons.edit : Icons.check, + color: Colors.white, + size: 22, + ), + )), + ], + ); + } + + Future _openColorWheel() async { + await showGeneralDialog( + context: context, + barrierLabel: "ColorPicker", + barrierDismissible: true, + barrierColor: Colors.black.withValues(alpha: 0.6), + pageBuilder: (_, __, ___) { + return Center( + child: Material( + type: MaterialType.transparency, + child: SettingsColorSelector( + initialColor: _selectedColor, + onColorSelected: (color) { + setState(() { + _selectedColor = color; + }); + widget.onColorChange(color); + Navigator.of(context).pop(); + }, + ), + ), + ); + }, + transitionBuilder: (_, animation, __, child) { + return Transform.scale( + scale: Curves.easeOutBack.transform(animation.value), + child: Opacity( + opacity: animation.value, + child: child, + ), + ); + }, + transitionDuration: const Duration(milliseconds: 200), + ); + } +} diff --git a/lib/widgets/SettingsColorSelector.dart b/lib/widgets/SettingsColorSelector.dart new file mode 100644 index 0000000..8d3e577 --- /dev/null +++ b/lib/widgets/SettingsColorSelector.dart @@ -0,0 +1,160 @@ +import 'package:flutter/material.dart'; +import 'package:noriskclient/config/Colors.dart'; +import 'package:noriskclient/config/ThemeProvider.dart'; +import 'package:noriskclient/l10n/app_localizations.dart'; +import 'package:noriskclient/widgets/NoRiskContainer.dart'; +import 'package:noriskclient/widgets/NoRiskText.dart'; +import 'package:noriskclient/widgets/NoRiskBackButton.dart'; +import 'package:noriskclient/widgets/NoRiskButton.dart'; +import 'package:noriskclient/widgets/SettingsColorInfo.dart'; +import 'package:noriskclient/widgets/SettingsHueSlider.dart'; +import 'package:noriskclient/widgets/SettingsSaturationValuePicker.dart'; +import 'package:provider/provider.dart'; + +class SettingsColorSelector extends StatefulWidget { + final Color initialColor; + final ValueChanged onColorSelected; + + const SettingsColorSelector({ + super.key, + required this.initialColor, + required this.onColorSelected, + }); + + @override + State createState() => _SettingsColorSelectorState(); +} + +class _SettingsColorSelectorState extends State { + double hue = 0; + double saturation = 1; + double value = 1; + + @override + void initState() { + super.initState(); + final hsv = HSVColor.fromColor(widget.initialColor); + hue = hsv.hue; + saturation = hsv.saturation; + value = hsv.value; + } + + Color get currentColor => + HSVColor.fromAHSV(1, hue, saturation, value).toColor(); + + void _updateSaturationValue(double newSaturation, double newValue) { + setState(() { + saturation = newSaturation; + value = newValue; + }); + } + + void _updateHue(double newHue) { + setState(() { + hue = newHue; + }); + } + + @override + Widget build(BuildContext context) { + final selectedColor = currentColor; + final theme = Provider.of(context); + + return Center( + child: NoRiskContainer( + width: 400, + height: 505, + backgroundOpacity: 255, + borderOpacity: 140, + color: theme.darkerBackground, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF1A1A2E), + borderRadius: BorderRadius.circular(8), + ), + child: Stack( + children: [ + Column( + children: [ + Stack( + children: [ + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only(top: 7.5), + child: NoRiskBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ), + Center( + child: NoRiskText( + AppLocalizations.of(context)! + .theme_color_picker_title + .toLowerCase(), + spaceTop: false, + spaceBottom: false, + style: const TextStyle( + color: Colors.white, + fontSize: 45, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + SettingsSaturationValuePicker( + hue: hue, + saturation: saturation, + value: value, + onChanged: _updateSaturationValue, + ), + const SizedBox(height: 16), + SettingsHueSlider( + hue: hue, + onChanged: _updateHue, + ), + const SizedBox(height: 16), + SettingsColorInfo(color: selectedColor), + const SizedBox(height: 30), + Row( + children: [ + NoRiskContainer( + width: 50, + height: 50, + color: selectedColor, + ), + const SizedBox(width: 10), + Expanded( + child: NoRiskButton( + onTap: () { + widget.onColorSelected(selectedColor); + }, + height: 50, + child: NoRiskText( + AppLocalizations.of(context)! + .theme_apply + .toLowerCase(), + spaceTop: false, + spaceBottom: false, + style: const TextStyle( + color: NoRiskClientColors.text, + fontSize: 25, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/SettingsHueSlider.dart b/lib/widgets/SettingsHueSlider.dart new file mode 100644 index 0000000..7ec83b0 --- /dev/null +++ b/lib/widgets/SettingsHueSlider.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:noriskclient/widgets/SettingsHueSliderPainter.dart'; + +class SettingsHueSlider extends StatelessWidget { + final double hue; + final ValueChanged onChanged; + + const SettingsHueSlider({ + super.key, + required this.hue, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final GlobalKey hueSliderKey = GlobalKey(); + + void updateHue(Offset globalPosition) { + final box = hueSliderKey.currentContext!.findRenderObject() as RenderBox; + final localPos = box.globalToLocal(globalPosition); + final width = box.size.width; + + final newHue = (localPos.dx.clamp(0.0, width) / width) * 360; + onChanged(newHue); + } + + return GestureDetector( + onPanUpdate: (details) => updateHue(details.globalPosition), + onPanDown: (details) => updateHue(details.globalPosition), + child: Container( + key: hueSliderKey, + width: double.infinity, + height: 20, + decoration: BoxDecoration( + border: Border.all(color: Colors.white, width: 2), + borderRadius: BorderRadius.circular(0), + ), + child: CustomPaint( + painter: SettingsHueSliderPainter(hue), + ), + ), + ); + } +} diff --git a/lib/widgets/SettingsHueSliderPainter.dart b/lib/widgets/SettingsHueSliderPainter.dart new file mode 100644 index 0000000..60c7900 --- /dev/null +++ b/lib/widgets/SettingsHueSliderPainter.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'dart:ui' as ui; + +class SettingsHueSliderPainter extends CustomPainter { + final double hue; + + SettingsHueSliderPainter(this.hue); + + @override + void paint(Canvas canvas, Size size) { + final rect = Offset.zero & size; + + final hueGradient = ui.Gradient.linear( + rect.topLeft, + rect.topRight, + [ + HSVColor.fromAHSV(1, 0, 1, 1).toColor(), + HSVColor.fromAHSV(1, 60, 1, 1).toColor(), + HSVColor.fromAHSV(1, 120, 1, 1).toColor(), + HSVColor.fromAHSV(1, 180, 1, 1).toColor(), + HSVColor.fromAHSV(1, 240, 1, 1).toColor(), + HSVColor.fromAHSV(1, 300, 1, 1).toColor(), + HSVColor.fromAHSV(1, 360, 1, 1).toColor(), + ], + [0.0, 0.17, 0.33, 0.5, 0.67, 0.83, 1.0], + ); + + final paint = Paint()..shader = hueGradient; + canvas.drawRect(rect, paint); + + final markerX = (hue / 360) * size.width; + const markerWidth = 8.0; + + final markerRect = Rect.fromLTWH( + markerX - markerWidth / 2, + 0, + markerWidth, + size.height, + ); + + final markerPaint = Paint() + ..color = Colors.white + ..style = PaintingStyle.fill; + + canvas.drawRect(markerRect, markerPaint); + } + + @override + bool shouldRepaint(covariant SettingsHueSliderPainter oldDelegate) => + oldDelegate.hue != hue; +} diff --git a/lib/widgets/SettingsSaturationValuePainter.dart b/lib/widgets/SettingsSaturationValuePainter.dart new file mode 100644 index 0000000..b433fb1 --- /dev/null +++ b/lib/widgets/SettingsSaturationValuePainter.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'dart:ui' as ui; + +class SettingsSaturationValuePainter extends CustomPainter { + final double hue; + final double saturation; + final double value; + + SettingsSaturationValuePainter(this.hue, this.saturation, this.value); + + @override + void paint(Canvas canvas, Size size) { + final rect = Offset.zero & size; + + final saturationGradient = ui.Gradient.linear( + rect.topLeft, + rect.topRight, + [Colors.white, HSVColor.fromAHSV(1, hue, 1, 1).toColor()], + ); + + final valueGradient = ui.Gradient.linear( + rect.topLeft, + rect.bottomLeft, + [Colors.transparent, Colors.black], + ); + + final paint = Paint()..shader = saturationGradient; + canvas.drawRect(rect, paint); + + final valuePaint = Paint()..shader = valueGradient; + canvas.drawRect(rect, valuePaint); + + final markerPaint = Paint() + ..color = Colors.white + ..style = PaintingStyle.stroke + ..strokeWidth = 2; + + final markerX = saturation * size.width; + final markerY = (1 - value) * size.height; + + const markerSize = 12.0; + final markerRect = Rect.fromCenter( + center: Offset(markerX, markerY), + width: markerSize, + height: markerSize, + ); + + canvas.drawRect(markerRect, markerPaint); + } + + @override + bool shouldRepaint(covariant SettingsSaturationValuePainter oldDelegate) => + oldDelegate.hue != hue || + oldDelegate.saturation != saturation || + oldDelegate.value != value; +} diff --git a/lib/widgets/SettingsSaturationValuePicker.dart b/lib/widgets/SettingsSaturationValuePicker.dart new file mode 100644 index 0000000..d665cfa --- /dev/null +++ b/lib/widgets/SettingsSaturationValuePicker.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:noriskclient/widgets/NoRiskContainer.dart'; +import 'package:noriskclient/widgets/SettingsSaturationValuePainter.dart'; + +class SettingsSaturationValuePicker extends StatelessWidget { + final double hue; + final double saturation; + final double value; + final Function(double saturation, double value) onChanged; + + const SettingsSaturationValuePicker({ + super.key, + required this.hue, + required this.saturation, + required this.value, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final GlobalKey colorBoxKey = GlobalKey(); + + return GestureDetector( + onPanUpdate: (details) { + final box = colorBoxKey.currentContext!.findRenderObject() as RenderBox; + final localPos = box.globalToLocal(details.globalPosition); + final width = box.size.width; + final height = box.size.height; + + final newSaturation = localPos.dx.clamp(0.0, width) / width; + final newValue = 1 - (localPos.dy.clamp(0.0, height) / height); + + onChanged(newSaturation, newValue); + }, + child: NoRiskContainer( + key: colorBoxKey, + width: double.infinity, + height: 200, + child: CustomPaint( + painter: SettingsSaturationValuePainter(hue, saturation, value), + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index cb1330e..f529d7b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -225,26 +225,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -273,10 +273,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mobile_scanner: dependency: "direct main" description: @@ -478,10 +478,10 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.7" typed_data: dependency: transitive description: @@ -566,10 +566,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vibration: dependency: "direct main" description: @@ -643,5 +643,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.8.0-0 <4.0.0" flutter: ">=3.24.0"