import 'dart:io'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:path/path.dart' as path; import '../config.dart'; import '../controllers/filesharing.dart'; import '../cwtch_icons_icons.dart'; import '../main.dart'; import '../models/appstate.dart'; import '../settings.dart'; import '../themes/cwtch.dart'; import '../themes/opaque.dart'; import '../themes/yamltheme.dart'; import 'globalsettingsview.dart'; class GlobalSettingsAppearanceView extends StatefulWidget { @override _GlobalSettingsAppearanceViewState createState() => _GlobalSettingsAppearanceViewState(); } class _GlobalSettingsAppearanceViewState extends State { ScrollController settingsListScrollController = ScrollController(); Widget build(BuildContext context) { return Consumer(builder: (ccontext, settings, child) { return LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) { return Scrollbar( key: Key("AppearanceSettingsView"), trackVisibility: true, controller: settingsListScrollController, child: SingleChildScrollView( clipBehavior: Clip.antiAlias, controller: settingsListScrollController, child: ConstrainedBox( constraints: BoxConstraints(minHeight: viewportConstraints.maxHeight, maxWidth: viewportConstraints.maxWidth), child: Container( color: settings.theme.backgroundPaneColor, child: Column(children: [ ListTile( title: Text(AppLocalizations.of(context)!.settingLanguage), leading: Icon(CwtchIcons.change_language, color: settings.current().mainTextColor), trailing: Container( width: MediaQuery.of(context).size.width / 4, child: DropdownButton( key: Key("languagelist"), isExpanded: true, value: Provider.of(context).locale.toString(), onChanged: (String? newValue) { setState(() { EnvironmentConfig.debugLog("setting language: $newValue"); settings.switchLocaleByCode(newValue!); saveSettings(context); }); }, items: AppLocalizations.supportedLocales.map>((Locale value) { return DropdownMenuItem( value: value.toString(), child: Text( key: Key("dropdownLanguage" + value.languageCode), getLanguageFull(context, value.languageCode, value.countryCode), style: settings.scaleFonts(defaultDropDownMenuItemTextStyle), overflow: TextOverflow.ellipsis, ), ); }).toList()))), SwitchListTile( title: Text(AppLocalizations.of(context)!.settingTheme), value: settings.current().mode == mode_light, onChanged: (bool value) { if (value) { settings.setTheme(settings.theme.theme, mode_light); } else { settings.setTheme(settings.theme.theme, mode_dark); } // Save Settings... saveSettings(context); }, activeTrackColor: settings.theme.defaultButtonColor, inactiveTrackColor: settings.theme.defaultButtonDisabledColor, secondary: Icon(CwtchIcons.change_theme, color: settings.current().mainTextColor), ), ListTile( title: Text(AppLocalizations.of(context)!.themeColorLabel), trailing: Container( width: MediaQuery.of(context).size.width / 4, child: DropdownButton( key: Key("DropdownTheme"), isExpanded: true, value: Provider.of(context).themeId, onChanged: (String? newValue) { setState(() { settings.setTheme(newValue!, settings.theme.mode); saveSettings(context); }); }, items: settings.themeloader.themes.keys.map>((String themeId) { return DropdownMenuItem( value: themeId, child: Text(getThemeName(context, settings, themeId), style: settings.scaleFonts(defaultDropDownMenuItemTextStyle)), //"ddi_$themeId", key: Key("ddi_$themeId")), ); }).toList())), leading: Icon(Icons.palette, color: settings.current().mainTextColor), ), Visibility( // TODO: Android support needs gomobile support for reading / writing themes, and ideally importing from a .zip or .tar.gz visible: !Platform.isAndroid, child: ListTile( leading: Icon(Icons.palette, color: Provider.of(context).theme.messageFromMeTextColor), title: Text(AppLocalizations.of(context)!.settingsImportThemeTitle), subtitle: Text(AppLocalizations.of(context)!.settingsImportThemeDescription), //AppLocalizations.of( //context)! //.fileSharingSettingsDownloadFolderDescription, trailing: Container( width: MediaQuery.of(context).size.width / 4, child: ElevatedButton.icon( label: Text(AppLocalizations.of(context)!.settingsImportThemeButton), onPressed: Provider.of(context).disableFilePicker ? null : () async { if (Platform.isAndroid) { return; } var selectedDirectory = await showSelectDirectoryPicker(context); if (selectedDirectory != null) { selectedDirectory += path.separator; final customThemeDir = path.join(await Provider.of(context, listen: false).cwtch.getCwtchDir(), custom_themes_subdir); importThemeCheck(context, settings, customThemeDir, selectedDirectory); } else { // User canceled the picker } }, //onChanged: widget.onSave, icon: Icon(Icons.folder), //tooltip: widget.tooltip, )))), SwitchListTile( title: Text(AppLocalizations.of(context)!.settingsThemeImages), subtitle: Text(AppLocalizations.of(context)!.settingsThemeImagesDescription), value: settings.themeImages, onChanged: (bool value) { settings.themeImages = value; // Save Settings... saveSettings(context); }, activeTrackColor: settings.theme.defaultButtonColor, inactiveTrackColor: settings.theme.defaultButtonDisabledColor, secondary: Icon(Icons.image, color: settings.current().mainTextColor), ), ListTile( title: Text(AppLocalizations.of(context)!.settingUIColumnPortrait), leading: Icon(Icons.table_chart, color: settings.current().mainTextColor), trailing: Container( width: MediaQuery.of(context).size.width / 4, child: DropdownButton( isExpanded: true, value: settings.uiColumnModePortrait.toString(), onChanged: (String? newValue) { settings.uiColumnModePortrait = Settings.uiColumnModeFromString(newValue!); saveSettings(context); }, items: Settings.uiColumnModeOptions(false).map>((DualpaneMode value) { return DropdownMenuItem( value: value.toString(), child: Text(Settings.uiColumnModeToString(value, context), style: settings.scaleFonts(defaultDropDownMenuItemTextStyle)), ); }).toList()))), ListTile( title: Text( AppLocalizations.of(context)!.settingUIColumnLandscape, textWidthBasis: TextWidthBasis.longestLine, softWrap: true, ), leading: Icon(Icons.stay_primary_landscape, color: settings.current().mainTextColor), trailing: Container( width: MediaQuery.of(context).size.width / 4, child: Container( width: MediaQuery.of(context).size.width / 4, child: DropdownButton( isExpanded: true, value: settings.uiColumnModeLandscape.toString(), onChanged: (String? newValue) { settings.uiColumnModeLandscape = Settings.uiColumnModeFromString(newValue!); saveSettings(context); }, items: Settings.uiColumnModeOptions(true).map>((DualpaneMode value) { return DropdownMenuItem( value: value.toString(), child: Text(Settings.uiColumnModeToString(value, context), overflow: TextOverflow.ellipsis, style: settings.scaleFonts(defaultDropDownMenuItemTextStyle)), ); }).toList())))), ListTile( title: Text(AppLocalizations.of(context)!.defaultScalingText), subtitle: Text(AppLocalizations.of(context)!.fontScalingDescription), trailing: Container( width: MediaQuery.of(context).size.width / 4, child: Slider( onChanged: (double value) { settings.fontScaling = value; // Save Settings... saveSettings(context); EnvironmentConfig.debugLog("Font Scaling: $value"); }, min: 0.5, divisions: 12, max: 2.0, label: '${settings.fontScaling * 100}%', activeColor: settings.current().defaultButtonColor, thumbColor: settings.current().mainTextColor, overlayColor: MaterialStateProperty.all(settings.current().mainTextColor), inactiveColor: settings.theme.defaultButtonDisabledColor, value: settings.fontScaling)), leading: Icon(Icons.format_size, color: settings.current().mainTextColor), ), SwitchListTile( title: Text(AppLocalizations.of(context)!.streamerModeLabel), subtitle: Text(AppLocalizations.of(context)!.descriptionStreamerMode), value: settings.streamerMode, onChanged: (bool value) { settings.setStreamerMode(value); // Save Settings... saveSettings(context); }, activeTrackColor: settings.theme.defaultButtonColor, inactiveTrackColor: settings.theme.defaultButtonDisabledColor, secondary: Icon(CwtchIcons.streamer_bunnymask, color: settings.current().mainTextColor), ), SwitchListTile( title: Text(AppLocalizations.of(context)!.formattingExperiment), subtitle: Text(AppLocalizations.of(context)!.messageFormattingDescription), value: settings.isExperimentEnabled(FormattingExperiment), onChanged: (bool value) { if (value) { settings.enableExperiment(FormattingExperiment); } else { settings.disableExperiment(FormattingExperiment); } saveSettings(context); }, activeTrackColor: settings.theme.defaultButtonColor, inactiveTrackColor: settings.theme.defaultButtonDisabledColor, secondary: Icon(Icons.text_fields, color: settings.current().mainTextColor), ), ]))))); }); }); } /// A slightly verbose way to extract the full language name from /// an individual language code. There might be a more efficient way of doing this. String getLanguageFull(context, String languageCode, String? countryCode) { if (languageCode == "en") { return AppLocalizations.of(context)!.localeEn; } if (languageCode == "es") { return AppLocalizations.of(context)!.localeEs; } if (languageCode == "fr") { return AppLocalizations.of(context)!.localeFr; } if (languageCode == "pt" && countryCode == "BR") { return AppLocalizations.of(context)!.localePtBr; } if (languageCode == "pt") { return AppLocalizations.of(context)!.localePt; } if (languageCode == "de") { return AppLocalizations.of(context)!.localeDe; } if (languageCode == "el") { return AppLocalizations.of(context)!.localeEl; } if (languageCode == "it") { return AppLocalizations.of(context)!.localeIt; } if (languageCode == "no") { return AppLocalizations.of(context)!.localeNo; } if (languageCode == "pl") { return AppLocalizations.of(context)!.localePl; } if (languageCode == "lb") { return AppLocalizations.of(context)!.localeLb; } if (languageCode == "ru") { return AppLocalizations.of(context)!.localeRU; } if (languageCode == "ro") { return AppLocalizations.of(context)!.localeRo; } if (languageCode == "cy") { return AppLocalizations.of(context)!.localeCy; } if (languageCode == "da") { return AppLocalizations.of(context)!.localeDa; } if (languageCode == "tr") { return AppLocalizations.of(context)!.localeTr; } if (languageCode == "nl") { return AppLocalizations.of(context)!.localeNl; } if (languageCode == "sk") { return AppLocalizations.of(context)!.localeSk; } if (languageCode == "ko") { return AppLocalizations.of(context)!.localeKo; } if (languageCode == "ja") { return AppLocalizations.of(context)!.localeJa; } if (languageCode == "sv") { return AppLocalizations.of(context)!.localeSv; } if (languageCode == "sw") { return AppLocalizations.of(context)!.localeSw; } if (languageCode == "uk") { return AppLocalizations.of(context)!.localeUk; } if (languageCode == "uz") { return AppLocalizations.of(context)!.localeUzbek; } return languageCode; } /// Since we don't seem to able to dynamically pull translations, this function maps themes to their names String getThemeName(context, Settings settings, String theme) { switch (theme) { case cwtch_theme: return AppLocalizations.of(context)!.themeNameCwtch; case "ghost": return AppLocalizations.of(context)!.themeNameGhost; case "mermaid": return AppLocalizations.of(context)!.themeNameMermaid; case "midnight": return AppLocalizations.of(context)!.themeNameMidnight; case "neon1": return AppLocalizations.of(context)!.themeNameNeon1; case "neon2": return AppLocalizations.of(context)!.themeNameNeon2; case "pumpkin": return AppLocalizations.of(context)!.themeNamePumpkin; case "vampire": return AppLocalizations.of(context)!.themeNameVampire; case "witch": return AppLocalizations.of(context)!.themeNameWitch; case "juniper": return "Juniper"; // Juniper is a noun, and doesn't get subject to translation... } return settings.themeloader.themes[theme]?[mode_light]?.theme ?? settings.themeloader.themes[theme]?[mode_dark]?.theme ?? theme; } void importThemeCheck(BuildContext context, Settings settings, String themesDir, String newThemeDirectory) async { // check is theme final srcDir = Directory(newThemeDirectory); String themeName = path.basename(newThemeDirectory); File themeFile = File(path.join(newThemeDirectory, "theme.yml")); if (!themeFile.existsSync()) { // error, isnt valid theme, no .yml theme file found SnackBar err = SnackBar(content: Text(AppLocalizations.of(context)!.settingsThemeErrorInvalid.replaceAll("\$themeName", themeName))); ScaffoldMessenger.of(context).showSnackBar(err); return; } Directory targetDir = Directory(path.join(themesDir, themeName)); // check if exists if (settings.themeloader.themes.containsKey(themeName) || targetDir.existsSync()) { _modalConfirmOverwriteCustomTheme(srcDir, targetDir, themesDir, themeName, settings); } else { importTheme(srcDir, targetDir, themesDir, themeName, settings); } } void importTheme(Directory srcDir, Directory targetDir, String themesDir, String themeName, Settings settings) async { if (!targetDir.existsSync()) { targetDir = await targetDir.create(); } // importTheme(newVal) await for (var entity in srcDir.list(recursive: false)) { if (entity is File) { entity.copySync(path.join(targetDir.path, path.basename(entity.path))); } } var data = await loadFileYamlTheme(path.join(targetDir.path, "theme.yml"), targetDir.path); if (data != null) { settings.themeloader.themes[themeName] = data; } if (settings.current().theme == themeName) { settings.setTheme(themeName, settings.current().mode); } } void _modalConfirmOverwriteCustomTheme(Directory srcDir, Directory targetDir, String themesDir, String themeName, Settings settings) { showModalBottomSheet( context: context, isScrollControlled: true, builder: (BuildContext context) { return Padding( padding: MediaQuery.of(context).viewInsets, child: RepaintBoundary( child: Container( height: Platform.isAndroid ? 250 : 200, // bespoke value courtesy of the [TextField] docs child: Center( child: Padding( padding: EdgeInsets.all(10.0), child: Column(mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Text(AppLocalizations.of(context)!.settingThemeOverwriteQuestion.replaceAll("\$themeName", themeName)), SizedBox( height: 20, ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Spacer(), Expanded( child: ElevatedButton( child: Text(AppLocalizations.of(context)!.settingThemeOverwriteConfirm, semanticsLabel: AppLocalizations.of(context)!.settingThemeOverwriteConfirm), onPressed: () { importTheme(srcDir, targetDir, themesDir, themeName, settings); Navigator.pop(context); }, )), SizedBox( width: 20, ), Expanded( child: ElevatedButton( child: Text(AppLocalizations.of(context)!.cancel, semanticsLabel: AppLocalizations.of(context)!.cancel), onPressed: () { Navigator.pop(context); }, )), Spacer(), ], ) ])))))); }); } }