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