Line data Source code
1 : import 'dart:io';
2 :
3 : import 'package:flutter/material.dart';
4 : import 'package:provider/provider.dart';
5 : import 'package:flutter_gen/gen_l10n/app_localizations.dart';
6 : import 'package:path/path.dart' as path;
7 :
8 : import '../config.dart';
9 : import '../controllers/filesharing.dart';
10 : import '../cwtch_icons_icons.dart';
11 : import '../main.dart';
12 : import '../models/appstate.dart';
13 : import '../settings.dart';
14 : import '../themes/cwtch.dart';
15 : import '../themes/opaque.dart';
16 : import '../themes/yamltheme.dart';
17 : import 'globalsettingsview.dart';
18 :
19 : class GlobalSettingsAppearanceView extends StatefulWidget {
20 0 : @override
21 0 : _GlobalSettingsAppearanceViewState createState() => _GlobalSettingsAppearanceViewState();
22 : }
23 :
24 : class _GlobalSettingsAppearanceViewState extends State<GlobalSettingsAppearanceView> {
25 : ScrollController settingsListScrollController = ScrollController();
26 :
27 0 : Widget build(BuildContext context) {
28 0 : return Consumer<Settings>(builder: (ccontext, settings, child) {
29 0 : return LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) {
30 0 : return Scrollbar(
31 0 : key: Key("AppearanceSettingsView"),
32 : trackVisibility: true,
33 0 : controller: settingsListScrollController,
34 0 : child: SingleChildScrollView(
35 : clipBehavior: Clip.antiAlias,
36 0 : controller: settingsListScrollController,
37 0 : padding: EdgeInsets.symmetric(vertical: 0, horizontal: 20),
38 0 : child: ConstrainedBox(
39 0 : constraints: BoxConstraints(
40 0 : minHeight: viewportConstraints.maxHeight,
41 : ),
42 0 : child: Column(children: [
43 0 : ListTile(
44 0 : title: Text(AppLocalizations.of(context)!.settingLanguage),
45 0 : leading: Icon(CwtchIcons.change_language, color: settings.current().mainTextColor),
46 0 : trailing: Container(
47 0 : width: MediaQuery.of(context).size.width / 4,
48 0 : child: DropdownButton(
49 0 : key: Key("languagelist"),
50 : isExpanded: true,
51 0 : value: Provider.of<Settings>(context).locale.toString(),
52 0 : onChanged: (String? newValue) {
53 0 : setState(() {
54 0 : EnvironmentConfig.debugLog("setting language: $newValue");
55 0 : settings.switchLocaleByCode(newValue!);
56 0 : saveSettings(context);
57 : });
58 : },
59 0 : items: AppLocalizations.supportedLocales.map<DropdownMenuItem<String>>((Locale value) {
60 0 : return DropdownMenuItem<String>(
61 0 : value: value.toString(),
62 0 : child: Text(
63 0 : key: Key("dropdownLanguage" + value.languageCode),
64 0 : getLanguageFull(context, value.languageCode, value.countryCode),
65 0 : style: settings.scaleFonts(defaultDropDownMenuItemTextStyle),
66 : overflow: TextOverflow.ellipsis,
67 : ),
68 : );
69 0 : }).toList()))),
70 0 : SwitchListTile(
71 0 : title: Text(AppLocalizations.of(context)!.settingTheme),
72 0 : value: settings.current().mode == mode_light,
73 0 : onChanged: (bool value) {
74 : if (value) {
75 0 : settings.setTheme(settings.theme.theme, mode_light);
76 : } else {
77 0 : settings.setTheme(settings.theme.theme, mode_dark);
78 : }
79 :
80 : // Save Settings...
81 0 : saveSettings(context);
82 : },
83 0 : activeTrackColor: settings.theme.defaultButtonColor,
84 0 : inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
85 0 : secondary: Icon(CwtchIcons.change_theme, color: settings.current().mainTextColor),
86 : ),
87 0 : ListTile(
88 0 : title: Text(AppLocalizations.of(context)!.themeColorLabel),
89 0 : trailing: Container(
90 0 : width: MediaQuery.of(context).size.width / 4,
91 0 : child: DropdownButton<String>(
92 0 : key: Key("DropdownTheme"),
93 : isExpanded: true,
94 0 : value: Provider.of<Settings>(context).themeId,
95 0 : onChanged: (String? newValue) {
96 0 : setState(() {
97 0 : settings.setTheme(newValue!, settings.theme.mode);
98 0 : saveSettings(context);
99 : });
100 : },
101 0 : items: settings.themeloader.themes.keys.map<DropdownMenuItem<String>>((String themeId) {
102 0 : return DropdownMenuItem<String>(
103 : value: themeId,
104 0 : child: Text(getThemeName(context, settings, themeId), style: settings.scaleFonts(defaultDropDownMenuItemTextStyle)), //"ddi_$themeId", key: Key("ddi_$themeId")),
105 : );
106 0 : }).toList())),
107 0 : leading: Icon(Icons.palette, color: settings.current().mainTextColor),
108 : ),
109 0 : Visibility(
110 : // TODO: Android support needs gomobile support for reading / writing themes, and ideally importing from a .zip or .tar.gz
111 0 : visible: !Platform.isAndroid,
112 0 : child: ListTile(
113 0 : leading: Icon(Icons.palette, color: Provider.of<Settings>(context).theme.messageFromMeTextColor),
114 0 : title: Text(AppLocalizations.of(context)!.settingsImportThemeTitle),
115 0 : subtitle: Text(AppLocalizations.of(context)!.settingsImportThemeDescription),
116 : //AppLocalizations.of(
117 : //context)!
118 : //.fileSharingSettingsDownloadFolderDescription,
119 0 : trailing: Container(
120 0 : width: MediaQuery.of(context).size.width / 4,
121 0 : child: ElevatedButton.icon(
122 0 : label: Text(AppLocalizations.of(context)!.settingsImportThemeButton),
123 0 : onPressed: Provider.of<AppState>(context).disableFilePicker
124 : ? null
125 0 : : () async {
126 0 : if (Platform.isAndroid) {
127 : return;
128 : }
129 0 : var selectedDirectory = await showSelectDirectoryPicker(context);
130 : if (selectedDirectory != null) {
131 0 : selectedDirectory += path.separator;
132 0 : final customThemeDir = path.join(await Provider.of<FlwtchState>(context, listen: false).cwtch.getCwtchDir(), custom_themes_subdir);
133 0 : importThemeCheck(context, settings, customThemeDir, selectedDirectory);
134 : } else {
135 : // User canceled the picker
136 : }
137 : },
138 : //onChanged: widget.onSave,
139 0 : icon: Icon(Icons.folder),
140 : //tooltip: widget.tooltip,
141 : )))),
142 0 : SwitchListTile(
143 0 : title: Text(AppLocalizations.of(context)!.settingsThemeImages),
144 0 : subtitle: Text(AppLocalizations.of(context)!.settingsThemeImagesDescription),
145 0 : value: settings.themeImages,
146 0 : onChanged: (bool value) {
147 0 : settings.themeImages = value; // Save Settings...
148 0 : saveSettings(context);
149 : },
150 0 : activeTrackColor: settings.theme.defaultButtonColor,
151 0 : inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
152 0 : secondary: Icon(Icons.image, color: settings.current().mainTextColor),
153 : ),
154 0 : ListTile(
155 0 : title: Text(AppLocalizations.of(context)!.settingUIColumnPortrait),
156 0 : leading: Icon(Icons.table_chart, color: settings.current().mainTextColor),
157 0 : trailing: Container(
158 0 : width: MediaQuery.of(context).size.width / 4,
159 0 : child: DropdownButton(
160 : isExpanded: true,
161 0 : value: settings.uiColumnModePortrait.toString(),
162 0 : onChanged: (String? newValue) {
163 0 : settings.uiColumnModePortrait = Settings.uiColumnModeFromString(newValue!);
164 0 : saveSettings(context);
165 : },
166 0 : items: Settings.uiColumnModeOptions(false).map<DropdownMenuItem<String>>((DualpaneMode value) {
167 0 : return DropdownMenuItem<String>(
168 0 : value: value.toString(),
169 0 : child: Text(Settings.uiColumnModeToString(value, context), style: settings.scaleFonts(defaultDropDownMenuItemTextStyle)),
170 : );
171 0 : }).toList()))),
172 0 : ListTile(
173 0 : title: Text(
174 0 : AppLocalizations.of(context)!.settingUIColumnLandscape,
175 : textWidthBasis: TextWidthBasis.longestLine,
176 : softWrap: true,
177 : ),
178 0 : leading: Icon(Icons.stay_primary_landscape, color: settings.current().mainTextColor),
179 0 : trailing: Container(
180 0 : width: MediaQuery.of(context).size.width / 4,
181 0 : child: Container(
182 0 : width: MediaQuery.of(context).size.width / 4,
183 0 : child: DropdownButton(
184 : isExpanded: true,
185 0 : value: settings.uiColumnModeLandscape.toString(),
186 0 : onChanged: (String? newValue) {
187 0 : settings.uiColumnModeLandscape = Settings.uiColumnModeFromString(newValue!);
188 0 : saveSettings(context);
189 : },
190 0 : items: Settings.uiColumnModeOptions(true).map<DropdownMenuItem<String>>((DualpaneMode value) {
191 0 : return DropdownMenuItem<String>(
192 0 : value: value.toString(),
193 0 : child: Text(Settings.uiColumnModeToString(value, context), overflow: TextOverflow.ellipsis, style: settings.scaleFonts(defaultDropDownMenuItemTextStyle)),
194 : );
195 0 : }).toList())))),
196 0 : ListTile(
197 0 : title: Text(AppLocalizations.of(context)!.defaultScalingText),
198 0 : subtitle: Text(AppLocalizations.of(context)!.fontScalingDescription),
199 0 : trailing: Container(
200 0 : width: MediaQuery.of(context).size.width / 4,
201 0 : child: Slider(
202 0 : onChanged: (double value) {
203 0 : settings.fontScaling = value;
204 : // Save Settings...
205 0 : saveSettings(context);
206 0 : EnvironmentConfig.debugLog("Font Scaling: $value");
207 : },
208 : min: 0.5,
209 : divisions: 12,
210 : max: 2.0,
211 0 : label: '${settings.fontScaling * 100}%',
212 0 : activeColor: settings.current().defaultButtonColor,
213 0 : thumbColor: settings.current().mainTextColor,
214 0 : overlayColor: MaterialStateProperty.all(settings.current().mainTextColor),
215 0 : inactiveColor: settings.theme.defaultButtonDisabledColor,
216 0 : value: settings.fontScaling)),
217 0 : leading: Icon(Icons.format_size, color: settings.current().mainTextColor),
218 : ),
219 0 : SwitchListTile(
220 0 : title: Text(AppLocalizations.of(context)!.streamerModeLabel),
221 0 : subtitle: Text(AppLocalizations.of(context)!.descriptionStreamerMode),
222 0 : value: settings.streamerMode,
223 0 : onChanged: (bool value) {
224 0 : settings.setStreamerMode(value);
225 : // Save Settings...
226 0 : saveSettings(context);
227 : },
228 0 : activeTrackColor: settings.theme.defaultButtonColor,
229 0 : inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
230 0 : secondary: Icon(CwtchIcons.streamer_bunnymask, color: settings.current().mainTextColor),
231 : ),
232 0 : SwitchListTile(
233 0 : title: Text(AppLocalizations.of(context)!.formattingExperiment),
234 0 : subtitle: Text(AppLocalizations.of(context)!.messageFormattingDescription),
235 0 : value: settings.isExperimentEnabled(FormattingExperiment),
236 0 : onChanged: (bool value) {
237 : if (value) {
238 0 : settings.enableExperiment(FormattingExperiment);
239 : } else {
240 0 : settings.disableExperiment(FormattingExperiment);
241 : }
242 0 : saveSettings(context);
243 : },
244 0 : activeTrackColor: settings.theme.defaultButtonActiveColor,
245 0 : inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
246 0 : secondary: Icon(Icons.text_fields, color: settings.current().mainTextColor),
247 : ),
248 : ]))));
249 : });
250 : });
251 : }
252 :
253 : /// A slightly verbose way to extract the full language name from
254 : /// an individual language code. There might be a more efficient way of doing this.
255 0 : String getLanguageFull(context, String languageCode, String? countryCode) {
256 0 : if (languageCode == "en") {
257 0 : return AppLocalizations.of(context)!.localeEn;
258 : }
259 0 : if (languageCode == "es") {
260 0 : return AppLocalizations.of(context)!.localeEs;
261 : }
262 0 : if (languageCode == "fr") {
263 0 : return AppLocalizations.of(context)!.localeFr;
264 : }
265 0 : if (languageCode == "pt" && countryCode == "BR") {
266 0 : return AppLocalizations.of(context)!.localePtBr;
267 : }
268 0 : if (languageCode == "pt") {
269 0 : return AppLocalizations.of(context)!.localePt;
270 : }
271 0 : if (languageCode == "de") {
272 0 : return AppLocalizations.of(context)!.localeDe;
273 : }
274 0 : if (languageCode == "el") {
275 0 : return AppLocalizations.of(context)!.localeEl;
276 : }
277 0 : if (languageCode == "it") {
278 0 : return AppLocalizations.of(context)!.localeIt;
279 : }
280 0 : if (languageCode == "no") {
281 0 : return AppLocalizations.of(context)!.localeNo;
282 : }
283 0 : if (languageCode == "pl") {
284 0 : return AppLocalizations.of(context)!.localePl;
285 : }
286 0 : if (languageCode == "lb") {
287 0 : return AppLocalizations.of(context)!.localeLb;
288 : }
289 0 : if (languageCode == "ru") {
290 0 : return AppLocalizations.of(context)!.localeRU;
291 : }
292 0 : if (languageCode == "ro") {
293 0 : return AppLocalizations.of(context)!.localeRo;
294 : }
295 0 : if (languageCode == "cy") {
296 0 : return AppLocalizations.of(context)!.localeCy;
297 : }
298 0 : if (languageCode == "da") {
299 0 : return AppLocalizations.of(context)!.localeDa;
300 : }
301 0 : if (languageCode == "tr") {
302 0 : return AppLocalizations.of(context)!.localeTr;
303 : }
304 0 : if (languageCode == "nl") {
305 0 : return AppLocalizations.of(context)!.localeNl;
306 : }
307 0 : if (languageCode == "sk") {
308 0 : return AppLocalizations.of(context)!.localeSk;
309 : }
310 0 : if (languageCode == "ko") {
311 0 : return AppLocalizations.of(context)!.localeKo;
312 : }
313 0 : if (languageCode == "ja") {
314 0 : return AppLocalizations.of(context)!.localeJa;
315 : }
316 0 : if (languageCode == "sv") {
317 0 : return AppLocalizations.of(context)!.localeSv;
318 : }
319 0 : if (languageCode == "sw") {
320 0 : return AppLocalizations.of(context)!.localeSw;
321 : }
322 0 : if (languageCode == "uk") {
323 0 : return AppLocalizations.of(context)!.localeUk;
324 : }
325 0 : if (languageCode == "uz") {
326 0 : return AppLocalizations.of(context)!.localeUzbek;
327 : }
328 : return languageCode;
329 : }
330 :
331 : /// Since we don't seem to able to dynamically pull translations, this function maps themes to their names
332 0 : String getThemeName(context, Settings settings, String theme) {
333 : switch (theme) {
334 0 : case cwtch_theme:
335 0 : return AppLocalizations.of(context)!.themeNameCwtch;
336 0 : case "ghost":
337 0 : return AppLocalizations.of(context)!.themeNameGhost;
338 0 : case "mermaid":
339 0 : return AppLocalizations.of(context)!.themeNameMermaid;
340 0 : case "midnight":
341 0 : return AppLocalizations.of(context)!.themeNameMidnight;
342 0 : case "neon1":
343 0 : return AppLocalizations.of(context)!.themeNameNeon1;
344 0 : case "neon2":
345 0 : return AppLocalizations.of(context)!.themeNameNeon2;
346 0 : case "pumpkin":
347 0 : return AppLocalizations.of(context)!.themeNamePumpkin;
348 0 : case "vampire":
349 0 : return AppLocalizations.of(context)!.themeNameVampire;
350 0 : case "witch":
351 0 : return AppLocalizations.of(context)!.themeNameWitch;
352 0 : case "juniper":
353 : return "Juniper"; // Juniper is a noun, and doesn't get subject to translation...
354 : }
355 0 : return settings.themeloader.themes[theme]?[mode_light]?.theme ?? settings.themeloader.themes[theme]?[mode_dark]?.theme ?? theme;
356 : }
357 :
358 0 : void importThemeCheck(BuildContext context, Settings settings, String themesDir, String newThemeDirectory) async {
359 : // check is theme
360 0 : final srcDir = Directory(newThemeDirectory);
361 0 : String themeName = path.basename(newThemeDirectory);
362 :
363 0 : File themeFile = File(path.join(newThemeDirectory, "theme.yml"));
364 :
365 0 : if (!themeFile.existsSync()) {
366 : // error, isnt valid theme, no .yml theme file found
367 0 : SnackBar err = SnackBar(content: Text(AppLocalizations.of(context)!.settingsThemeErrorInvalid.replaceAll("\$themeName", themeName)));
368 0 : ScaffoldMessenger.of(context).showSnackBar(err);
369 : return;
370 : }
371 :
372 0 : Directory targetDir = Directory(path.join(themesDir, themeName));
373 : // check if exists
374 0 : if (settings.themeloader.themes.containsKey(themeName) || targetDir.existsSync()) {
375 0 : _modalConfirmOverwriteCustomTheme(srcDir, targetDir, themesDir, themeName, settings);
376 : } else {
377 0 : importTheme(srcDir, targetDir, themesDir, themeName, settings);
378 : }
379 : }
380 :
381 0 : void importTheme(Directory srcDir, Directory targetDir, String themesDir, String themeName, Settings settings) async {
382 0 : if (!targetDir.existsSync()) {
383 0 : targetDir = await targetDir.create();
384 : }
385 :
386 : // importTheme(newVal)
387 0 : await for (var entity in srcDir.list(recursive: false)) {
388 0 : if (entity is File) {
389 0 : entity.copySync(path.join(targetDir.path, path.basename(entity.path)));
390 : }
391 : }
392 :
393 0 : var data = await loadFileYamlTheme(path.join(targetDir.path, "theme.yml"), targetDir.path);
394 : if (data != null) {
395 0 : settings.themeloader.themes[themeName] = data;
396 : }
397 :
398 0 : if (settings.current().theme == themeName) {
399 0 : settings.setTheme(themeName, settings.current().mode);
400 : }
401 : }
402 :
403 0 : void _modalConfirmOverwriteCustomTheme(Directory srcDir, Directory targetDir, String themesDir, String themeName, Settings settings) {
404 0 : showModalBottomSheet<void>(
405 0 : context: context,
406 : isScrollControlled: true,
407 0 : builder: (BuildContext context) {
408 0 : return Padding(
409 0 : padding: MediaQuery.of(context).viewInsets,
410 0 : child: RepaintBoundary(
411 0 : child: Container(
412 0 : height: Platform.isAndroid ? 250 : 200, // bespoke value courtesy of the [TextField] docs
413 0 : child: Center(
414 0 : child: Padding(
415 0 : padding: EdgeInsets.all(10.0),
416 0 : child: Column(mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: <Widget>[
417 0 : Text(AppLocalizations.of(context)!.settingThemeOverwriteQuestion.replaceAll("\$themeName", themeName)),
418 0 : SizedBox(
419 : height: 20,
420 : ),
421 0 : Row(
422 : mainAxisAlignment: MainAxisAlignment.spaceEvenly,
423 0 : children: [
424 0 : Spacer(),
425 0 : Expanded(
426 0 : child: ElevatedButton(
427 0 : child: Text(AppLocalizations.of(context)!.settingThemeOverwriteConfirm, semanticsLabel: AppLocalizations.of(context)!.settingThemeOverwriteConfirm),
428 0 : onPressed: () {
429 0 : importTheme(srcDir, targetDir, themesDir, themeName, settings);
430 :
431 0 : Navigator.pop(context);
432 : },
433 : )),
434 0 : SizedBox(
435 : width: 20,
436 : ),
437 0 : Expanded(
438 0 : child: ElevatedButton(
439 0 : child: Text(AppLocalizations.of(context)!.cancel, semanticsLabel: AppLocalizations.of(context)!.cancel),
440 0 : onPressed: () {
441 0 : Navigator.pop(context);
442 : },
443 : )),
444 0 : Spacer(),
445 : ],
446 : )
447 : ]))))));
448 : });
449 : }
450 : }
|