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 : child: ConstrainedBox(
38 0 : constraints: BoxConstraints(minHeight: viewportConstraints.maxHeight, maxWidth: viewportConstraints.maxWidth),
39 0 : child: Container(
40 0 : color: settings.theme.backgroundPaneColor,
41 0 : child: Column(children: [
42 0 : ListTile(
43 0 : title: Text(AppLocalizations.of(context)!.settingLanguage),
44 0 : leading: Icon(CwtchIcons.change_language, color: settings.current().mainTextColor),
45 0 : trailing: Container(
46 0 : width: MediaQuery.of(context).size.width / 4,
47 0 : child: DropdownButton(
48 0 : key: Key("languagelist"),
49 : isExpanded: true,
50 0 : value: Provider.of<Settings>(context).locale.toString(),
51 0 : onChanged: (String? newValue) {
52 0 : setState(() {
53 0 : EnvironmentConfig.debugLog("setting language: $newValue");
54 0 : settings.switchLocaleByCode(newValue!);
55 0 : saveSettings(context);
56 : });
57 : },
58 0 : items: AppLocalizations.supportedLocales.map<DropdownMenuItem<String>>((Locale value) {
59 0 : return DropdownMenuItem<String>(
60 0 : value: value.toString(),
61 0 : child: Text(
62 0 : key: Key("dropdownLanguage" + value.languageCode),
63 0 : getLanguageFull(context, value.languageCode, value.countryCode),
64 0 : style: settings.scaleFonts(defaultDropDownMenuItemTextStyle),
65 : overflow: TextOverflow.ellipsis,
66 : ),
67 : );
68 0 : }).toList()))),
69 0 : SwitchListTile(
70 0 : title: Text(AppLocalizations.of(context)!.settingTheme),
71 0 : value: settings.current().mode == mode_light,
72 0 : onChanged: (bool value) {
73 : if (value) {
74 0 : settings.setTheme(settings.themeId ?? "cwtch", mode_light);
75 : } else {
76 0 : settings.setTheme(settings.themeId ?? "cwtch", mode_dark);
77 : }
78 :
79 : // Save Settings...
80 0 : saveSettings(context);
81 : },
82 0 : activeTrackColor: settings.theme.defaultButtonColor,
83 0 : inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
84 0 : secondary: Icon(CwtchIcons.change_theme, color: settings.current().mainTextColor),
85 : ),
86 0 : ListTile(
87 0 : title: Text(AppLocalizations.of(context)!.themeColorLabel),
88 0 : trailing: Container(
89 0 : width: MediaQuery.of(context).size.width / 4,
90 0 : child: DropdownButton<String>(
91 0 : key: Key("DropdownTheme"),
92 : isExpanded: true,
93 0 : value: Provider.of<Settings>(context).themeId,
94 0 : onChanged: (String? newValue) {
95 0 : setState(() {
96 0 : settings.setTheme(newValue!, settings.theme.mode);
97 0 : saveSettings(context);
98 : });
99 : },
100 0 : items: settings.themeloader.themes.keys.map<DropdownMenuItem<String>>((String themeId) {
101 0 : return DropdownMenuItem<String>(
102 : value: themeId,
103 : child:
104 0 : 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: OutlinedButton.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 : value: settings.fontScaling)),
213 0 : leading: Icon(Icons.format_size, color: settings.current().mainTextColor),
214 : ),
215 0 : SwitchListTile(
216 0 : title: Text(AppLocalizations.of(context)!.streamerModeLabel),
217 0 : subtitle: Text(AppLocalizations.of(context)!.descriptionStreamerMode),
218 0 : value: settings.streamerMode,
219 0 : onChanged: (bool value) {
220 0 : settings.setStreamerMode(value);
221 : // Save Settings...
222 0 : saveSettings(context);
223 : },
224 0 : activeTrackColor: settings.theme.defaultButtonColor,
225 0 : inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
226 0 : secondary: Icon(CwtchIcons.streamer_bunnymask, color: settings.current().mainTextColor),
227 : ),
228 0 : SwitchListTile(
229 0 : title: Text(AppLocalizations.of(context)!.formattingExperiment),
230 0 : subtitle: Text(AppLocalizations.of(context)!.messageFormattingDescription),
231 0 : value: settings.isExperimentEnabled(FormattingExperiment),
232 0 : onChanged: (bool value) {
233 : if (value) {
234 0 : settings.enableExperiment(FormattingExperiment);
235 : } else {
236 0 : settings.disableExperiment(FormattingExperiment);
237 : }
238 0 : saveSettings(context);
239 : },
240 0 : activeTrackColor: settings.theme.defaultButtonColor,
241 0 : inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
242 0 : secondary: Icon(Icons.text_fields, color: settings.current().mainTextColor),
243 : ),
244 : ])))));
245 : });
246 : });
247 : }
248 :
249 : /// A slightly verbose way to extract the full language name from
250 : /// an individual language code. There might be a more efficient way of doing this.
251 0 : String getLanguageFull(context, String languageCode, String? countryCode) {
252 0 : if (languageCode == "en") {
253 0 : return AppLocalizations.of(context)!.localeEn;
254 : }
255 0 : if (languageCode == "es") {
256 0 : return AppLocalizations.of(context)!.localeEs;
257 : }
258 0 : if (languageCode == "fr") {
259 0 : return AppLocalizations.of(context)!.localeFr;
260 : }
261 0 : if (languageCode == "pt" && countryCode == "BR") {
262 0 : return AppLocalizations.of(context)!.localePtBr;
263 : }
264 0 : if (languageCode == "pt") {
265 0 : return AppLocalizations.of(context)!.localePt;
266 : }
267 0 : if (languageCode == "de") {
268 0 : return AppLocalizations.of(context)!.localeDe;
269 : }
270 0 : if (languageCode == "el") {
271 0 : return AppLocalizations.of(context)!.localeEl;
272 : }
273 0 : if (languageCode == "it") {
274 0 : return AppLocalizations.of(context)!.localeIt;
275 : }
276 0 : if (languageCode == "no") {
277 0 : return AppLocalizations.of(context)!.localeNo;
278 : }
279 0 : if (languageCode == "pl") {
280 0 : return AppLocalizations.of(context)!.localePl;
281 : }
282 0 : if (languageCode == "lb") {
283 0 : return AppLocalizations.of(context)!.localeLb;
284 : }
285 0 : if (languageCode == "ru") {
286 0 : return AppLocalizations.of(context)!.localeRU;
287 : }
288 0 : if (languageCode == "ro") {
289 0 : return AppLocalizations.of(context)!.localeRo;
290 : }
291 0 : if (languageCode == "cy") {
292 0 : return AppLocalizations.of(context)!.localeCy;
293 : }
294 0 : if (languageCode == "da") {
295 0 : return AppLocalizations.of(context)!.localeDa;
296 : }
297 0 : if (languageCode == "tr") {
298 0 : return AppLocalizations.of(context)!.localeTr;
299 : }
300 0 : if (languageCode == "nl") {
301 0 : return AppLocalizations.of(context)!.localeNl;
302 : }
303 0 : if (languageCode == "sk") {
304 0 : return AppLocalizations.of(context)!.localeSk;
305 : }
306 0 : if (languageCode == "ko") {
307 0 : return AppLocalizations.of(context)!.localeKo;
308 : }
309 0 : if (languageCode == "ja") {
310 0 : return AppLocalizations.of(context)!.localeJa;
311 : }
312 0 : if (languageCode == "sv") {
313 0 : return AppLocalizations.of(context)!.localeSv;
314 : }
315 0 : if (languageCode == "sw") {
316 0 : return AppLocalizations.of(context)!.localeSw;
317 : }
318 0 : if (languageCode == "uk") {
319 0 : return AppLocalizations.of(context)!.localeUk;
320 : }
321 0 : if (languageCode == "uz") {
322 0 : return AppLocalizations.of(context)!.localeUzbek;
323 : }
324 : return languageCode;
325 : }
326 :
327 : /// Since we don't seem to able to dynamically pull translations, this function maps themes to their names
328 0 : String getThemeName(context, Settings settings, String theme) {
329 : switch (theme) {
330 0 : case cwtch_theme:
331 0 : return AppLocalizations.of(context)!.themeNameCwtch;
332 0 : case "ghost":
333 0 : return AppLocalizations.of(context)!.themeNameGhost;
334 0 : case "mermaid":
335 0 : return AppLocalizations.of(context)!.themeNameMermaid;
336 0 : case "midnight":
337 0 : return AppLocalizations.of(context)!.themeNameMidnight;
338 0 : case "neon1":
339 0 : return AppLocalizations.of(context)!.themeNameNeon1;
340 0 : case "neon2":
341 0 : return AppLocalizations.of(context)!.themeNameNeon2;
342 0 : case "pumpkin":
343 0 : return AppLocalizations.of(context)!.themeNamePumpkin;
344 0 : case "vampire":
345 0 : return AppLocalizations.of(context)!.themeNameVampire;
346 0 : case "witch":
347 0 : return AppLocalizations.of(context)!.themeNameWitch;
348 0 : case "juniper":
349 : return "Juniper"; // Juniper is a noun, and doesn't get subject to translation...
350 : }
351 0 : return settings.themeloader.themes[theme]?[mode_light]?.theme ?? settings.themeloader.themes[theme]?[mode_dark]?.theme ?? theme;
352 : }
353 :
354 0 : void importThemeCheck(BuildContext context, Settings settings, String themesDir, String newThemeDirectory) async {
355 : // check is theme
356 0 : final srcDir = Directory(newThemeDirectory);
357 0 : String themeName = path.basename(newThemeDirectory);
358 :
359 0 : File themeFile = File(path.join(newThemeDirectory, "theme.yml"));
360 :
361 0 : if (!themeFile.existsSync()) {
362 : // error, isnt valid theme, no .yml theme file found
363 0 : SnackBar err = SnackBar(content: Text(AppLocalizations.of(context)!.settingsThemeErrorInvalid.replaceAll("\$themeName", themeName)));
364 0 : ScaffoldMessenger.of(context).showSnackBar(err);
365 : return;
366 : }
367 :
368 0 : Directory targetDir = Directory(path.join(themesDir, themeName));
369 : // check if exists
370 0 : if (settings.themeloader.themes.containsKey(themeName) || targetDir.existsSync()) {
371 0 : _modalConfirmOverwriteCustomTheme(srcDir, targetDir, themesDir, themeName, settings);
372 : } else {
373 0 : importTheme(srcDir, targetDir, themesDir, themeName, settings);
374 : }
375 : }
376 :
377 0 : void importTheme(Directory srcDir, Directory targetDir, String themesDir, String themeName, Settings settings) async {
378 0 : if (!targetDir.existsSync()) {
379 0 : targetDir = await targetDir.create();
380 : }
381 :
382 : // importTheme(newVal)
383 0 : await for (var entity in srcDir.list(recursive: false)) {
384 0 : if (entity is File) {
385 0 : entity.copySync(path.join(targetDir.path, path.basename(entity.path)));
386 : }
387 : }
388 :
389 0 : var data = await loadFileYamlTheme(path.join(targetDir.path, "theme.yml"), targetDir.path);
390 : if (data != null) {
391 0 : settings.themeloader.themes[themeName] = data;
392 : }
393 :
394 0 : if (settings.current().theme == themeName) {
395 0 : settings.setTheme(themeName, settings.current().mode);
396 : }
397 : }
398 :
399 0 : void _modalConfirmOverwriteCustomTheme(Directory srcDir, Directory targetDir, String themesDir, String themeName, Settings settings) {
400 0 : showModalBottomSheet<void>(
401 0 : context: context,
402 : isScrollControlled: true,
403 0 : builder: (BuildContext context) {
404 0 : return Padding(
405 0 : padding: MediaQuery.of(context).viewInsets,
406 0 : child: RepaintBoundary(
407 0 : child: Container(
408 0 : height: Platform.isAndroid ? 250 : 200, // bespoke value courtesy of the [TextField] docs
409 0 : child: Center(
410 0 : child: Padding(
411 0 : padding: EdgeInsets.all(10.0),
412 0 : child: Column(mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: <Widget>[
413 0 : Text(AppLocalizations.of(context)!.settingThemeOverwriteQuestion.replaceAll("\$themeName", themeName)),
414 0 : SizedBox(
415 : height: 20,
416 : ),
417 0 : Row(
418 : mainAxisAlignment: MainAxisAlignment.spaceEvenly,
419 0 : children: [
420 0 : Spacer(),
421 0 : Expanded(
422 0 : child: ElevatedButton(
423 0 : child: Text(AppLocalizations.of(context)!.settingThemeOverwriteConfirm, semanticsLabel: AppLocalizations.of(context)!.settingThemeOverwriteConfirm),
424 0 : onPressed: () {
425 0 : importTheme(srcDir, targetDir, themesDir, themeName, settings);
426 :
427 0 : Navigator.pop(context);
428 : },
429 : )),
430 0 : SizedBox(
431 : width: 20,
432 : ),
433 0 : Expanded(
434 0 : child: ElevatedButton(
435 0 : child: Text(AppLocalizations.of(context)!.cancel, semanticsLabel: AppLocalizations.of(context)!.cancel),
436 0 : onPressed: () {
437 0 : Navigator.pop(context);
438 : },
439 : )),
440 0 : Spacer(),
441 : ],
442 : )
443 : ]))))));
444 : });
445 : }
446 : }
|