diff --git a/lib/collections/language_codes.dart b/lib/collections/language_codes.dart new file mode 100644 index 00000000..f533e14d --- /dev/null +++ b/lib/collections/language_codes.dart @@ -0,0 +1,262 @@ +class ISOLanguageName { + final String name; + final String nativeName; + + const ISOLanguageName({ + required this.name, + required this.nativeName, + }); +} + +abstract class LanguageLocals { + static final Map isoLangs = { + "ab": const ISOLanguageName(name: "Abkhaz", nativeName: "аҧсуа"), + "aa": const ISOLanguageName(name: "Afar", nativeName: "Afaraf"), + "af": const ISOLanguageName(name: "Afrikaans", nativeName: "Afrikaans"), + "ak": const ISOLanguageName(name: "Akan", nativeName: "Akan"), + "sq": const ISOLanguageName(name: "Albanian", nativeName: "Shqip"), + "am": const ISOLanguageName(name: "Amharic", nativeName: "አማርኛ"), + "ar": const ISOLanguageName(name: "Arabic", nativeName: "العربية"), + "an": const ISOLanguageName(name: "Aragonese", nativeName: "Aragonés"), + "hy": const ISOLanguageName(name: "Armenian", nativeName: "Հայերեն"), + "as": const ISOLanguageName(name: "Assamese", nativeName: "অসমীয়া"), + "av": const ISOLanguageName( + name: "Avaric", + nativeName: "авар мацӀ, магӀарул мацӀ", + ), + "ae": const ISOLanguageName(name: "Avestan", nativeName: "avesta"), + "ay": const ISOLanguageName(name: "Aymara", nativeName: "aymar aru"), + "az": const ISOLanguageName( + name: "Azerbaijani", + nativeName: "azərbaycan dili", + ), + "bm": const ISOLanguageName(name: "Bambara", nativeName: "bamanankan"), + "ba": const ISOLanguageName(name: "Bashkir", nativeName: "башҡорт теле"), + "eu": const ISOLanguageName(name: "Basque", nativeName: "euskara,"), + "be": const ISOLanguageName(name: "Belarusian", nativeName: "Беларуская"), + "bn": const ISOLanguageName(name: "Bengali", nativeName: "বাংলা"), + "bh": const ISOLanguageName(name: "Bihari", nativeName: "भोजपुरी"), + "bi": const ISOLanguageName(name: "Bislama", nativeName: "Bislama"), + "bs": const ISOLanguageName(name: "Bosnian", nativeName: "bosanski jezik"), + "br": const ISOLanguageName(name: "Breton", nativeName: "brezhoneg"), + "bg": + const ISOLanguageName(name: "Bulgarian", nativeName: "български език"), + "my": const ISOLanguageName(name: "Burmese", nativeName: "ဗမာစာ"), + "ca": + const ISOLanguageName(name: "Catalan; Valencian", nativeName: "Català"), + "ch": const ISOLanguageName(name: "Chamorro", nativeName: "Chamoru"), + "ce": const ISOLanguageName(name: "Chechen", nativeName: "нохчийн мотт"), + "ny": const ISOLanguageName(name: "Chichewa", nativeName: "chiCheŵa"), + "zh": const ISOLanguageName(name: "Chinese", nativeName: "汉语"), + "cv": const ISOLanguageName(name: "Chuvash", nativeName: "чӑваш чӗлхи"), + "kw": const ISOLanguageName(name: "Cornish", nativeName: "Kernewek"), + "co": const ISOLanguageName(name: "Corsican", nativeName: "lingua corsa"), + "cr": const ISOLanguageName(name: "Cree", nativeName: "ᓀᐦᐃᔭᐍᐏᐣ"), + "hr": const ISOLanguageName(name: "Croatian", nativeName: "hrvatski"), + "cs": const ISOLanguageName(name: "Czech", nativeName: "česky, čeština"), + "da": const ISOLanguageName(name: "Danish", nativeName: "dansk"), + "dv": const ISOLanguageName(name: "Maldivian;", nativeName: "ދިވެހި"), + "nl": const ISOLanguageName(name: "Dutch", nativeName: "Vlaams"), + "en": const ISOLanguageName(name: "English", nativeName: "English"), + "eo": const ISOLanguageName(name: "Esperanto", nativeName: "Esperanto"), + "et": const ISOLanguageName(name: "Estonian", nativeName: "eesti"), + "ee": const ISOLanguageName(name: "Ewe", nativeName: "Eʋegbe"), + "fo": const ISOLanguageName(name: "Faroese", nativeName: "føroyskt"), + "fj": const ISOLanguageName(name: "Fijian", nativeName: "vosa Vakaviti"), + "fi": const ISOLanguageName(name: "Finnish", nativeName: "suomi"), + "fr": const ISOLanguageName(name: "French", nativeName: "français"), + "ff": const ISOLanguageName( + name: "Fula; Fulah; Pulaar; Pular", + nativeName: "Fulfulde, Pulaar, Pular"), + "gl": const ISOLanguageName(name: "Galician", nativeName: "Galego"), + "ka": const ISOLanguageName(name: "Georgian", nativeName: "ქართული"), + "de": const ISOLanguageName(name: "German", nativeName: "Deutsch"), + "el": const ISOLanguageName(name: "Greek, Modern", nativeName: "Ελληνικά"), + "gn": const ISOLanguageName(name: "Guaraní", nativeName: "Avañeẽ"), + "gu": const ISOLanguageName(name: "Gujarati", nativeName: "ગુજરાતી"), + "ht": const ISOLanguageName( + name: "Haitian; Haitian Creole", nativeName: "Kreyòl ayisyen"), + "ha": const ISOLanguageName(name: "Hausa", nativeName: "Hausa, هَوُسَ"), + "he": const ISOLanguageName(name: "Hebrew (modern)", nativeName: "עברית"), + "hz": const ISOLanguageName(name: "Herero", nativeName: "Otjiherero"), + "hi": const ISOLanguageName(name: "Hindi", nativeName: "हिन्दी, हिंदी"), + "ho": const ISOLanguageName(name: "Hiri Motu", nativeName: "Hiri Motu"), + "hu": const ISOLanguageName(name: "Hungarian", nativeName: "Magyar"), + "ia": const ISOLanguageName(name: "Interlingua", nativeName: "Interlingua"), + "id": const ISOLanguageName( + name: "Indonesian", nativeName: "Bahasa Indonesia"), + "ie": const ISOLanguageName(name: "Interlingue", nativeName: "Occidental"), + "ga": const ISOLanguageName(name: "Irish", nativeName: "Gaeilge"), + "ig": const ISOLanguageName(name: "Igbo", nativeName: "Asụsụ Igbo"), + "ik": const ISOLanguageName( + name: "Inupiaq", nativeName: "Iñupiaq, Iñupiatun"), + "io": const ISOLanguageName(name: "Ido", nativeName: "Ido"), + "is": const ISOLanguageName(name: "Icelandic", nativeName: "Íslenska"), + "it": const ISOLanguageName(name: "Italian", nativeName: "Italiano"), + "iu": const ISOLanguageName(name: "Inuktitut", nativeName: "ᐃᓄᒃᑎᑐᑦ"), + "ja": + const ISOLanguageName(name: "Japanese", nativeName: "日本語 (にほんご/にっぽんご)"), + "jv": const ISOLanguageName(name: "Javanese", nativeName: "basa Jawa"), + "kl": const ISOLanguageName( + name: "Kalaallisut, Greenlandic", + nativeName: "kalaallisut, kalaallit oqaasii"), + "kn": const ISOLanguageName(name: "Kannada", nativeName: "ಕನ್ನಡ"), + "kr": const ISOLanguageName(name: "Kanuri", nativeName: "Kanuri"), + "ks": + const ISOLanguageName(name: "Kashmiri", nativeName: "कश्मीरी, كشميري‎"), + "kk": const ISOLanguageName(name: "Kazakh", nativeName: "Қазақ тілі"), + "km": const ISOLanguageName(name: "Khmer", nativeName: "ភាសាខ្មែរ"), + "ki": const ISOLanguageName(name: "Kikuyu, Gikuyu", nativeName: "Gĩkũyũ"), + "rw": + const ISOLanguageName(name: "Kinyarwanda", nativeName: "Ikinyarwanda"), + "ky": const ISOLanguageName( + name: "Kirghiz, Kyrgyz", nativeName: "кыргыз тили"), + "kv": const ISOLanguageName(name: "Komi", nativeName: "коми кыв"), + "kg": const ISOLanguageName(name: "Kongo", nativeName: "KiKongo"), + "ko": const ISOLanguageName( + name: "Korean", nativeName: "한국어 (韓國語), 조선말 (朝鮮語)"), + "ku": const ISOLanguageName(name: "Kurdish", nativeName: "Kurdî, كوردی‎"), + "kj": const ISOLanguageName( + name: "Kwanyama, Kuanyama", nativeName: "Kuanyama"), + "la": const ISOLanguageName( + name: "Latin", nativeName: "latine, lingua latina"), + "lb": const ISOLanguageName( + name: "Luxembourgish, Letzeburgesch", nativeName: "Lëtzebuergesch"), + "lg": const ISOLanguageName(name: "Luganda", nativeName: "Luganda"), + "li": const ISOLanguageName( + name: "Limburgish, Limburgan, Limburger", nativeName: "Limburgs"), + "ln": const ISOLanguageName(name: "Lingala", nativeName: "Lingála"), + "lo": const ISOLanguageName(name: "Lao", nativeName: "ພາສາລາວ"), + "lt": + const ISOLanguageName(name: "Lithuanian", nativeName: "lietuvių kalba"), + "lu": const ISOLanguageName(name: "Luba-Katanga", nativeName: ""), + "lv": const ISOLanguageName(name: "Latvian", nativeName: "latviešu valoda"), + "gv": const ISOLanguageName(name: "Manx", nativeName: "Gaelg, Gailck"), + "mk": const ISOLanguageName( + name: "Macedonian", nativeName: "македонски јазик"), + "mg": + const ISOLanguageName(name: "Malagasy", nativeName: "Malagasy fiteny"), + "ms": const ISOLanguageName( + name: "Malay", nativeName: "bahasa Melayu, بهاس ملايو‎"), + "ml": const ISOLanguageName(name: "Malayalam", nativeName: "മലയാളം"), + "mt": const ISOLanguageName(name: "Maltese", nativeName: "Malti"), + "mi": const ISOLanguageName(name: "Māori", nativeName: "te reo Māori"), + "mr": const ISOLanguageName(name: "Marathi (Marāṭhī)", nativeName: "मराठी"), + "mh": + const ISOLanguageName(name: "Marshallese", nativeName: "Kajin M̧ajeļ"), + "mn": const ISOLanguageName(name: "Mongolian", nativeName: "монгол"), + "na": const ISOLanguageName(name: "Nauru", nativeName: "Ekakairũ Naoero"), + "nv": const ISOLanguageName( + name: "Navajo, Navaho", nativeName: "Diné bizaad, Dinékʼehǰí"), + "nb": const ISOLanguageName( + name: "Norwegian Bokmål", nativeName: "Norsk bokmål"), + "nd": + const ISOLanguageName(name: "North Ndebele", nativeName: "isiNdebele"), + "ne": const ISOLanguageName(name: "Nepali", nativeName: "नेपाली"), + "ng": const ISOLanguageName(name: "Ndonga", nativeName: "Owambo"), + "nn": const ISOLanguageName( + name: "Norwegian Nynorsk", nativeName: "Norsk nynorsk"), + "no": const ISOLanguageName(name: "Norwegian", nativeName: "Norsk"), + "ii": const ISOLanguageName(name: "Nuosu", nativeName: "ꆈꌠ꒿ Nuosuhxop"), + "nr": + const ISOLanguageName(name: "South Ndebele", nativeName: "isiNdebele"), + "oc": const ISOLanguageName(name: "Occitan", nativeName: "Occitan"), + "oj": const ISOLanguageName(name: "Ojibwe, Ojibwa", nativeName: "ᐊᓂᔑᓈᐯᒧᐎᓐ"), + "cu": const ISOLanguageName( + name: "Old Church Slavonic", nativeName: "ѩзыкъ словѣньскъ"), + "om": const ISOLanguageName(name: "Oromo", nativeName: "Afaan Oromoo"), + "or": const ISOLanguageName(name: "Oriya", nativeName: "ଓଡ଼ିଆ"), + "os": const ISOLanguageName( + name: "Ossetian, Ossetic", nativeName: "ирон æвзаг"), + "pa": const ISOLanguageName( + name: "Panjabi, Punjabi", nativeName: "ਪੰਜਾਬੀ, پنجابی‎"), + "pi": const ISOLanguageName(name: "Pāli", nativeName: "पाऴि"), + "fa": const ISOLanguageName(name: "Persian", nativeName: "فارسی"), + "pl": const ISOLanguageName(name: "Polish", nativeName: "polski"), + "ps": const ISOLanguageName(name: "Pashto, Pushto", nativeName: "پښتو"), + "pt": const ISOLanguageName(name: "Portuguese", nativeName: "Português"), + "qu": + const ISOLanguageName(name: "Quechua", nativeName: "Runa Simi, Kichwa"), + "rm": const ISOLanguageName( + name: "Romansh", nativeName: "rumantsch grischun"), + "rn": const ISOLanguageName(name: "Kirundi", nativeName: "kiRundi"), + "ro": const ISOLanguageName( + name: "Romanian, Moldavian, Moldovan", nativeName: "română"), + "ru": const ISOLanguageName(name: "Russian", nativeName: "русский язык"), + "sa": const ISOLanguageName( + name: "Sanskrit (Saṁskṛta)", nativeName: "संस्कृतम्"), + "sc": const ISOLanguageName(name: "Sardinian", nativeName: "sardu"), + "sd": const ISOLanguageName( + name: "Sindhi", nativeName: "सिन्धी, سنڌي، سندھی‎"), + "se": const ISOLanguageName( + name: "Northern Sami", nativeName: "Davvisámegiella"), + "sm": const ISOLanguageName(name: "Samoan", nativeName: "gagana faa Samoa"), + "sg": const ISOLanguageName(name: "Sango", nativeName: "yângâ tî sängö"), + "sr": const ISOLanguageName(name: "Serbian", nativeName: "српски језик"), + "gd": const ISOLanguageName( + name: "Scottish Gaelic; Gaelic", nativeName: "Gàidhlig"), + "sn": const ISOLanguageName(name: "Shona", nativeName: "chiShona"), + "si": + const ISOLanguageName(name: "Sinhala, Sinhalese", nativeName: "සිංහල"), + "sk": const ISOLanguageName(name: "Slovak", nativeName: "slovenčina"), + "sl": const ISOLanguageName(name: "Slovene", nativeName: "slovenščina"), + "so": const ISOLanguageName( + name: "Somali", nativeName: "Soomaaliga, af Soomaali"), + "st": const ISOLanguageName(name: "Southern Sotho", nativeName: "Sesotho"), + "es": const ISOLanguageName( + name: "Spanish; Castilian", nativeName: "español, castellano"), + "su": const ISOLanguageName(name: "Sundanese", nativeName: "Basa Sunda"), + "sw": const ISOLanguageName(name: "Swahili", nativeName: "Kiswahili"), + "ss": const ISOLanguageName(name: "Swati", nativeName: "SiSwati"), + "sv": const ISOLanguageName(name: "Swedish", nativeName: "svenska"), + "ta": const ISOLanguageName(name: "Tamil", nativeName: "தமிழ்"), + "te": const ISOLanguageName(name: "Telugu", nativeName: "తెలుగు"), + "tg": const ISOLanguageName( + name: "Tajik", nativeName: "тоҷикӣ, toğikī, تاجیکی‎"), + "th": const ISOLanguageName(name: "Thai", nativeName: "ไทย"), + "ti": const ISOLanguageName(name: "Tigrinya", nativeName: "ትግርኛ"), + "bo": const ISOLanguageName( + name: "Tibetan Standard, Tibetan, Central", nativeName: "བོད་ཡིག"), + "tk": + const ISOLanguageName(name: "Turkmen", nativeName: "Türkmen, Түркмен"), + "tl": const ISOLanguageName( + name: "Tagalog", nativeName: "Wikang Tagalog, ᜏᜒᜃᜅ᜔ ᜆᜄᜎᜓᜄ᜔"), + "tn": const ISOLanguageName(name: "Tswana", nativeName: "Setswana"), + "to": const ISOLanguageName( + name: "Tonga (Tonga Islands)", nativeName: "faka Tonga"), + "tr": const ISOLanguageName(name: "Turkish", nativeName: "Türkçe"), + "ts": const ISOLanguageName(name: "Tsonga", nativeName: "Xitsonga"), + "tt": const ISOLanguageName( + name: "Tatar", nativeName: "татарча, tatarça, تاتارچا‎"), + "tw": const ISOLanguageName(name: "Twi", nativeName: "Twi"), + "ty": const ISOLanguageName(name: "Tahitian", nativeName: "Reo Tahiti"), + "ug": const ISOLanguageName( + name: "Uighur, Uyghur", nativeName: "Uyƣurqə, ئۇيغۇرچە‎"), + "uk": const ISOLanguageName(name: "Ukrainian", nativeName: "українська"), + "ur": const ISOLanguageName(name: "Urdu", nativeName: "اردو"), + "uz": const ISOLanguageName( + name: "Uzbek", nativeName: "zbek, Ўзбек, أۇزبېك‎"), + "ve": const ISOLanguageName(name: "Venda", nativeName: "Tshivenḓa"), + "vi": const ISOLanguageName(name: "Vietnamese", nativeName: "Tiếng Việt"), + "vo": const ISOLanguageName(name: "Volapük", nativeName: "Volapük"), + "wa": const ISOLanguageName(name: "Walloon", nativeName: "Walon"), + "cy": const ISOLanguageName(name: "Welsh", nativeName: "Cymraeg"), + "wo": const ISOLanguageName(name: "Wolof", nativeName: "Wollof"), + "fy": const ISOLanguageName(name: "Western Frisian", nativeName: "Frysk"), + "xh": const ISOLanguageName(name: "Xhosa", nativeName: "isiXhosa"), + "yi": const ISOLanguageName(name: "Yiddish", nativeName: "ייִדיש"), + "yo": const ISOLanguageName(name: "Yoruba", nativeName: "Yorùbá"), + "za": const ISOLanguageName( + name: "Zhuang, Chuang", + nativeName: "Saɯ cueŋƅ, Saw cuengh", + ) + }; + + static ISOLanguageName getDisplayLanguage(key) { + if (isoLangs.containsKey(key)) { + return isoLangs[key]!; + } else { + throw Exception("Language key incorrect"); + } + } +} diff --git a/lib/components/settings/section_card_with_heading.dart b/lib/components/settings/section_card_with_heading.dart new file mode 100644 index 00000000..87060579 --- /dev/null +++ b/lib/components/settings/section_card_with_heading.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +class SectionCardWithHeading extends StatelessWidget { + final String heading; + final List children; + const SectionCardWithHeading({ + super.key, + required this.heading, + required this.children, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + heading, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Card( + clipBehavior: Clip.antiAliasWithSaveLayer, + child: Column(mainAxisSize: MainAxisSize.min, children: children), + ), + ), + ], + ); + } +} diff --git a/lib/components/shared/adaptive/adaptive_select_tile.dart b/lib/components/shared/adaptive/adaptive_select_tile.dart new file mode 100644 index 00000000..0ee1d1fe --- /dev/null +++ b/lib/components/shared/adaptive/adaptive_select_tile.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:spotube/hooks/use_breakpoints.dart'; + +class AdaptiveSelectTile extends HookWidget { + final Widget title; + final Widget? subtitle; + final Widget? secondary; + final ListTileControlAffinity? controlAffinity; + final T value; + final ValueChanged? onChanged; + + final List> options; + + final Breakpoints breakAfterOr; + + const AdaptiveSelectTile({ + required this.title, + required this.value, + required this.onChanged, + required this.options, + this.controlAffinity = ListTileControlAffinity.trailing, + this.subtitle, + this.secondary, + this.breakAfterOr = Breakpoints.md, + super.key, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final breakpoint = useBreakpoints(); + final rawControl = DropdownButton( + items: options, + value: value, + onChanged: onChanged, + ); + final controlPlaceholder = useMemoized( + () => options + .firstWhere( + (element) => element.value == value, + orElse: () => DropdownMenuItem( + value: null, + child: Container(), + ), + ) + .child, + [value, options]); + + final control = breakpoint >= breakAfterOr + ? rawControl + : Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + border: Border.all( + color: theme.colorScheme.primary, + width: 2, + ), + borderRadius: BorderRadius.circular(10), + ), + child: DefaultTextStyle( + style: TextStyle( + color: theme.colorScheme.primary, + ), + child: controlPlaceholder, + ), + ); + + return ListTile( + title: title, + subtitle: subtitle, + leading: controlAffinity != ListTileControlAffinity.leading + ? secondary + : control, + trailing: controlAffinity == ListTileControlAffinity.leading + ? secondary + : control, + onTap: breakpoint >= breakAfterOr + ? null + : () { + showDialog( + context: context, + builder: (context) { + return SimpleDialog( + title: title, + children: [ + for (final option in options) + RadioListTile( + title: option.child, + value: option.value as T, + groupValue: value, + onChanged: (v) { + Navigator.pop(context); + onChanged?.call(v); + }, + ), + ], + ); + }, + ); + }, + ); + } +} diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 5dd91687..9a1c604d 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -6,13 +6,17 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/env.dart'; +import 'package:spotube/collections/language_codes.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/settings/color_scheme_picker_dialog.dart'; +import 'package:spotube/components/settings/section_card_with_heading.dart'; import 'package:spotube/components/shared/adaptive/adaptive_list_tile.dart'; +import 'package:spotube/components/shared/adaptive/adaptive_select_tile.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/collections/spotify_markets.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/hooks/use_breakpoints.dart'; import 'package:spotube/l10n/l10n.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/downloader_provider.dart'; @@ -61,398 +65,382 @@ class SettingsPage extends HookConsumerWidget { constraints: const BoxConstraints(maxWidth: 1366), child: ListView( children: [ - Text( - " ${context.l10n.account}", - style: theme.textTheme.headlineSmall - ?.copyWith(fontWeight: FontWeight.bold), - ), - if (auth == null) - AdaptiveListTile( - leading: Icon( - SpotubeIcons.login, - color: theme.colorScheme.primary, - ), - title: Align( - alignment: Alignment.centerLeft, - child: AutoSizeText( - context.l10n.login_with_spotify, - maxLines: 1, - style: TextStyle( + SectionCardWithHeading( + heading: context.l10n.account, + children: [ + if (auth == null) + AdaptiveListTile( + leading: Icon( + SpotubeIcons.login, color: theme.colorScheme.primary, ), - ), - ), - trailing: (context, update) => FilledButton( - onPressed: () { - GoRouter.of(context).push("/login"); - }, - style: ButtonStyle( - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(25.0), - ), - ), - ), - child: Text( - context.l10n.connect_with_spotify.toUpperCase(), - ), - ), - ) - else - Builder(builder: (context) { - return ListTile( - leading: const Icon(SpotubeIcons.logout), - title: SizedBox( - height: 50, - width: 180, - child: Align( + title: Align( alignment: Alignment.centerLeft, child: AutoSizeText( - context.l10n.logout_of_this_account, + context.l10n.login_with_spotify, maxLines: 1, + style: TextStyle( + color: theme.colorScheme.primary, + ), ), ), - ), - trailing: FilledButton( - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all(Colors.red), - foregroundColor: - MaterialStateProperty.all(Colors.white), + trailing: (context, update) => FilledButton( + onPressed: () { + GoRouter.of(context).push("/login"); + }, + style: ButtonStyle( + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(25.0), + ), + ), + ), + child: Text( + context.l10n.connect_with_spotify.toUpperCase(), + ), ), - onPressed: () async { - ref - .read( - AuthenticationNotifier.provider.notifier) - .logout(); - GoRouter.of(context).pop(); - }, - child: Text(context.l10n.logout), - ), - ); - }), - Text( - " ${context.l10n.language_region}", - style: theme.textTheme.headlineSmall - ?.copyWith(fontWeight: FontWeight.bold), + ) + else + Builder(builder: (context) { + return ListTile( + leading: const Icon(SpotubeIcons.logout), + title: SizedBox( + height: 50, + width: 180, + child: Align( + alignment: Alignment.centerLeft, + child: AutoSizeText( + context.l10n.logout_of_this_account, + maxLines: 1, + ), + ), + ), + trailing: FilledButton( + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(Colors.red), + foregroundColor: + MaterialStateProperty.all(Colors.white), + ), + onPressed: () async { + ref + .read(AuthenticationNotifier + .provider.notifier) + .logout(); + GoRouter.of(context).pop(); + }, + child: Text(context.l10n.logout), + ), + ); + }), + ], ), - ListTile( - leading: const Icon(SpotubeIcons.language), - title: Text(context.l10n.language), - trailing: DropdownButton( - value: preferences.locale, - items: [ - DropdownMenuItem( - value: const Locale("system", "system"), - child: Text(context.l10n.system_default), - ), - for (final locale in L10n.all) + SectionCardWithHeading( + heading: context.l10n.language_region, + children: [ + AdaptiveSelectTile( + value: preferences.locale, + onChanged: (locale) { + if (locale == null) return; + preferences.setLocale(locale); + }, + title: Text(context.l10n.language), + secondary: const Icon(SpotubeIcons.language), + options: [ DropdownMenuItem( - value: locale, - child: Text(locale.languageCode), + value: const Locale("system", "system"), + child: Text(context.l10n.system_default), ), - ], - onChanged: (value) { - if (value != null) { - preferences.setLocale(value); - } - }, - ), - ), - AdaptiveListTile( - leading: const Icon(SpotubeIcons.shoppingBag), - title: Text(context.l10n.market_place_region), - subtitle: Text(context.l10n.recommendation_country), - trailing: (context, update) => ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 350), - child: DropdownMenu( - initialSelection: preferences.recommendationMarket, - dropdownMenuEntries: spotifyMarkets + for (final locale in L10n.all) + DropdownMenuItem( + value: locale, + child: Builder(builder: (context) { + final isoCodeName = + LanguageLocals.getDisplayLanguage( + locale.languageCode, + ); + return Text( + "${isoCodeName.name} (${isoCodeName.nativeName})", + ); + }), + ), + ], + ), + AdaptiveSelectTile( + breakAfterOr: Breakpoints.lg, + secondary: const Icon(SpotubeIcons.shoppingBag), + title: Text(context.l10n.market_place_region), + subtitle: Text(context.l10n.recommendation_country), + value: preferences.recommendationMarket, + onChanged: (value) { + if (value == null) return; + preferences.setRecommendationMarket(value); + }, + options: spotifyMarkets .map( - (country) => DropdownMenuEntry( + (country) => DropdownMenuItem( value: country.first, - label: country.last, + child: Text(country.last), ), ) .toList(), - onSelected: (value) { - if (value == null) return; - preferences.setRecommendationMarket( - value as String, - ); - update?.call(() {}); + ), + ], + ), + SectionCardWithHeading( + heading: context.l10n.appearance, + children: [ + AdaptiveSelectTile( + secondary: const Icon(SpotubeIcons.dashboard), + title: Text(context.l10n.layout_mode), + subtitle: Text(context.l10n.override_layout_settings), + value: preferences.layoutMode, + onChanged: (value) { + if (value != null) { + preferences.setLayoutMode(value); + } }, - ), - ), - ), - Text( - " ${context.l10n.appearance}", - style: theme.textTheme.headlineSmall - ?.copyWith(fontWeight: FontWeight.bold), - ), - AdaptiveListTile( - leading: const Icon(SpotubeIcons.dashboard), - title: Text(context.l10n.layout_mode), - subtitle: Text(context.l10n.override_layout_settings), - trailing: (context, update) => DropdownButton( - items: [ - DropdownMenuItem( - value: LayoutMode.adaptive, - child: Text(context.l10n.adaptive), - ), - DropdownMenuItem( - value: LayoutMode.compact, - child: Text(context.l10n.compact), - ), - DropdownMenuItem( - value: LayoutMode.extended, - child: Text(context.l10n.extended), - ), - ], - value: preferences.layoutMode, - onChanged: (value) { - if (value != null) { - preferences.setLayoutMode(value); - update?.call(() {}); - } - }, - ), - ), - AdaptiveListTile( - leading: const Icon(SpotubeIcons.darkMode), - title: Text(context.l10n.theme), - trailing: (context, update) => DropdownButton( - value: preferences.themeMode, - items: [ - DropdownMenuItem( - value: ThemeMode.dark, - child: Text(context.l10n.dark), - ), - DropdownMenuItem( - value: ThemeMode.light, - child: Text(context.l10n.light), - ), - DropdownMenuItem( - value: ThemeMode.system, - child: Text(context.l10n.system), - ), - ], - onChanged: (value) { - if (value != null) { - preferences.setThemeMode(value); - update?.call(() {}); - } - }, - ), - ), - ListTile( - leading: const Icon(SpotubeIcons.palette), - title: Text(context.l10n.accent_color), - contentPadding: const EdgeInsets.symmetric( - horizontal: 15, - vertical: 5, - ), - trailing: ColorTile.compact( - color: preferences.accentColorScheme, - onPressed: pickColorScheme(), - isActive: true, - ), - onTap: pickColorScheme(), - ), - SwitchListTile( - secondary: const Icon(SpotubeIcons.colorSync), - title: Text(context.l10n.sync_album_color), - subtitle: Text(context.l10n.sync_album_color_description), - value: preferences.albumColorSync, - onChanged: preferences.setAlbumColorSync, - ), - Text( - " ${context.l10n.playback}", - style: theme.textTheme.headlineSmall - ?.copyWith(fontWeight: FontWeight.bold), - ), - AdaptiveListTile( - leading: const Icon(SpotubeIcons.audioQuality), - title: Text(context.l10n.audio_quality), - trailing: (context, update) => - DropdownButton( - value: preferences.audioQuality, - items: [ - DropdownMenuItem( - value: AudioQuality.high, - child: Text(context.l10n.high), - ), - DropdownMenuItem( - value: AudioQuality.low, - child: Text(context.l10n.low), - ), - ], - onChanged: (value) { - if (value != null) { - preferences.setAudioQuality(value); - update?.call(() {}); - } - }, - ), - ), - SwitchListTile( - secondary: const Icon(SpotubeIcons.download), - title: Text(context.l10n.pre_download_play), - subtitle: - Text(context.l10n.pre_download_play_description), - value: preferences.predownload, - onChanged: (state) { - preferences.setPredownload(state); - }, - ), - SwitchListTile( - secondary: const Icon(SpotubeIcons.fastForward), - title: Text(context.l10n.skip_non_music), - value: preferences.skipSponsorSegments, - onChanged: (state) { - preferences.setSkipSponsorSegments(state); - }, - ), - ListTile( - leading: const Icon(SpotubeIcons.playlistRemove), - title: Text(context.l10n.blacklist), - subtitle: Text(context.l10n.blacklist_description), - onTap: () { - GoRouter.of(context).push("/settings/blacklist"); - }, - trailing: const Icon(SpotubeIcons.angleRight), - ), - Text( - " ${context.l10n.downloads}", - style: theme.textTheme.headlineSmall - ?.copyWith(fontWeight: FontWeight.bold), - ), - Tooltip( - message: isDownloading - ? context.l10n.wait_for_download_to_finish - : "", - child: ListTile( - leading: const Icon(SpotubeIcons.download), - title: Text(context.l10n.download_location), - subtitle: Text(preferences.downloadLocation), - trailing: FilledButton( - onPressed: - isDownloading ? null : pickDownloadLocation, - child: const Icon(SpotubeIcons.folder), - ), - onTap: isDownloading ? null : pickDownloadLocation, - ), - ), - SwitchListTile( - secondary: const Icon(SpotubeIcons.lyrics), - title: Text(context.l10n.download_lyrics), - value: preferences.saveTrackLyrics, - onChanged: (state) { - preferences.setSaveTrackLyrics(state); - }, - ), - if (DesktopTools.platform.isDesktop) ...[ - Text( - " ${context.l10n.desktop}", - style: theme.textTheme.headlineSmall - ?.copyWith(fontWeight: FontWeight.bold), - ), - AdaptiveListTile( - leading: const Icon(SpotubeIcons.close), - title: Text(context.l10n.close_behavior), - trailing: (context, update) => - DropdownButton( - value: preferences.closeBehavior, - items: [ + options: [ DropdownMenuItem( - value: CloseBehavior.close, - child: Text(context.l10n.close), + value: LayoutMode.adaptive, + child: Text(context.l10n.adaptive), ), DropdownMenuItem( - value: CloseBehavior.minimizeToTray, - child: Text(context.l10n.minimize_to_tray), + value: LayoutMode.compact, + child: Text(context.l10n.compact), + ), + DropdownMenuItem( + value: LayoutMode.extended, + child: Text(context.l10n.extended), + ), + ], + ), + AdaptiveSelectTile( + secondary: const Icon(SpotubeIcons.darkMode), + title: Text(context.l10n.theme), + value: preferences.themeMode, + options: [ + DropdownMenuItem( + value: ThemeMode.dark, + child: Text(context.l10n.dark), + ), + DropdownMenuItem( + value: ThemeMode.light, + child: Text(context.l10n.light), + ), + DropdownMenuItem( + value: ThemeMode.system, + child: Text(context.l10n.system), ), ], onChanged: (value) { if (value != null) { - preferences.setCloseBehavior(value); - update?.call(() {}); + preferences.setThemeMode(value); } }, ), - ), - SwitchListTile( - secondary: const Icon(SpotubeIcons.tray), - title: Text(context.l10n.show_tray_icon), - value: preferences.showSystemTrayIcon, - onChanged: preferences.setShowSystemTrayIcon, - ), - ], - Text( - " ${context.l10n.about}", - style: theme.textTheme.headlineSmall - ?.copyWith(fontWeight: FontWeight.bold), + ListTile( + leading: const Icon(SpotubeIcons.palette), + title: Text(context.l10n.accent_color), + contentPadding: const EdgeInsets.symmetric( + horizontal: 15, + vertical: 5, + ), + trailing: ColorTile.compact( + color: preferences.accentColorScheme, + onPressed: pickColorScheme(), + isActive: true, + ), + onTap: pickColorScheme(), + ), + SwitchListTile( + secondary: const Icon(SpotubeIcons.colorSync), + title: Text(context.l10n.sync_album_color), + subtitle: + Text(context.l10n.sync_album_color_description), + value: preferences.albumColorSync, + onChanged: preferences.setAlbumColorSync, + ), + ], ), - AdaptiveListTile( - leading: const Icon( - SpotubeIcons.heart, - color: Colors.pink, + SectionCardWithHeading( + heading: context.l10n.playback, + children: [ + AdaptiveSelectTile( + secondary: const Icon(SpotubeIcons.audioQuality), + title: Text(context.l10n.audio_quality), + value: preferences.audioQuality, + options: [ + DropdownMenuItem( + value: AudioQuality.high, + child: Text(context.l10n.high), + ), + DropdownMenuItem( + value: AudioQuality.low, + child: Text(context.l10n.low), + ), + ], + onChanged: (value) { + if (value != null) { + preferences.setAudioQuality(value); + } + }, + ), + SwitchListTile( + secondary: const Icon(SpotubeIcons.download), + title: Text(context.l10n.pre_download_play), + subtitle: + Text(context.l10n.pre_download_play_description), + value: preferences.predownload, + onChanged: (state) { + preferences.setPredownload(state); + }, + ), + SwitchListTile( + secondary: const Icon(SpotubeIcons.fastForward), + title: Text(context.l10n.skip_non_music), + value: preferences.skipSponsorSegments, + onChanged: (state) { + preferences.setSkipSponsorSegments(state); + }, + ), + ListTile( + leading: const Icon(SpotubeIcons.playlistRemove), + title: Text(context.l10n.blacklist), + subtitle: Text(context.l10n.blacklist_description), + onTap: () { + GoRouter.of(context).push("/settings/blacklist"); + }, + trailing: const Icon(SpotubeIcons.angleRight), + ), + ], + ), + SectionCardWithHeading( + heading: context.l10n.downloads, + children: [ + Tooltip( + message: isDownloading + ? context.l10n.wait_for_download_to_finish + : "", + child: ListTile( + leading: const Icon(SpotubeIcons.download), + title: Text(context.l10n.download_location), + subtitle: Text(preferences.downloadLocation), + trailing: FilledButton( + onPressed: + isDownloading ? null : pickDownloadLocation, + child: const Icon(SpotubeIcons.folder), + ), + onTap: isDownloading ? null : pickDownloadLocation, + ), + ), + SwitchListTile( + secondary: const Icon(SpotubeIcons.lyrics), + title: Text(context.l10n.download_lyrics), + value: preferences.saveTrackLyrics, + onChanged: (state) { + preferences.setSaveTrackLyrics(state); + }, + ), + ], + ), + if (DesktopTools.platform.isDesktop) + SectionCardWithHeading( + heading: context.l10n.desktop, + children: [ + AdaptiveSelectTile( + secondary: const Icon(SpotubeIcons.close), + title: Text(context.l10n.close_behavior), + value: preferences.closeBehavior, + options: [ + DropdownMenuItem( + value: CloseBehavior.close, + child: Text(context.l10n.close), + ), + DropdownMenuItem( + value: CloseBehavior.minimizeToTray, + child: Text(context.l10n.minimize_to_tray), + ), + ], + onChanged: (value) { + if (value != null) { + preferences.setCloseBehavior(value); + } + }, + ), + SwitchListTile( + secondary: const Icon(SpotubeIcons.tray), + title: Text(context.l10n.show_tray_icon), + value: preferences.showSystemTrayIcon, + onChanged: preferences.setShowSystemTrayIcon, + ), + ], ), - title: SizedBox( - height: 50, - width: 200, - child: Align( - alignment: Alignment.centerLeft, - child: AutoSizeText( - context.l10n.u_love_spotube, - maxLines: 1, - style: const TextStyle( - color: Colors.pink, - fontWeight: FontWeight.bold, + SectionCardWithHeading( + heading: context.l10n.about, + children: [ + AdaptiveListTile( + leading: const Icon( + SpotubeIcons.heart, + color: Colors.pink, + ), + title: SizedBox( + height: 50, + width: 200, + child: Align( + alignment: Alignment.centerLeft, + child: AutoSizeText( + context.l10n.u_love_spotube, + maxLines: 1, + style: const TextStyle( + color: Colors.pink, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + trailing: (context, update) => FilledButton( + style: ButtonStyle( + backgroundColor: + MaterialStatePropertyAll(Colors.red[100]), + foregroundColor: const MaterialStatePropertyAll( + Colors.pinkAccent), + padding: const MaterialStatePropertyAll( + EdgeInsets.all(15)), + ), + onPressed: () { + launchUrlString( + "https://opencollective.com/spotube", + mode: LaunchMode.externalApplication, + ); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(SpotubeIcons.heart), + const SizedBox(width: 5), + Text(context.l10n.please_sponsor), + ], ), ), ), - ), - trailing: (context, update) => FilledButton( - style: ButtonStyle( - backgroundColor: - MaterialStatePropertyAll(Colors.red[100]), - foregroundColor: - const MaterialStatePropertyAll(Colors.pinkAccent), - padding: const MaterialStatePropertyAll( - EdgeInsets.all(15)), - ), - onPressed: () { - launchUrlString( - "https://opencollective.com/spotube", - mode: LaunchMode.externalApplication, - ); - }, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(SpotubeIcons.heart), - const SizedBox(width: 5), - Text(context.l10n.please_sponsor), - ], - ), - ), - ), - if (Env.enableUpdateChecker) - SwitchListTile( - secondary: const Icon(SpotubeIcons.update), - title: Text(context.l10n.check_for_updates), - value: preferences.checkUpdate, - onChanged: (checked) => - preferences.setCheckUpdate(checked), - ), - ListTile( - leading: const Icon(SpotubeIcons.info), - title: Text(context.l10n.about_spotube), - trailing: const Icon(SpotubeIcons.angleRight), - onTap: () { - GoRouter.of(context).push("/settings/about"); - }, + if (Env.enableUpdateChecker) + SwitchListTile( + secondary: const Icon(SpotubeIcons.update), + title: Text(context.l10n.check_for_updates), + value: preferences.checkUpdate, + onChanged: (checked) => + preferences.setCheckUpdate(checked), + ), + ListTile( + leading: const Icon(SpotubeIcons.info), + title: Text(context.l10n.about_spotube), + trailing: const Icon(SpotubeIcons.angleRight), + onTap: () { + GoRouter.of(context).push("/settings/about"); + }, + ) + ], ), ], ), diff --git a/lib/services/audio_player.dart b/lib/services/audio_player.dart index 46dc5741..960cb0e4 100644 --- a/lib/services/audio_player.dart +++ b/lib/services/audio_player.dart @@ -95,7 +95,7 @@ class SpotubeAudioPlayer { Stream get bufferingStream { if (apSupportedPlatform) { - return const Stream.empty(); + return Stream.value(false); } else { throw UnimplementedError(); } @@ -215,7 +215,6 @@ class SpotubeAudioPlayer { Future pause() async { await _audioPlayer?.pause(); - throw UnimplementedError(); } Future resume() async { diff --git a/lib/themes/theme.dart b/lib/themes/theme.dart index b513ca7e..4f04f612 100644 --- a/lib/themes/theme.dart +++ b/lib/themes/theme.dart @@ -13,6 +13,7 @@ ThemeData theme(Color seed, Brightness brightness) { horizontalTitleGap: 0, iconColor: scheme.onSurface, ), + appBarTheme: const AppBarTheme(surfaceTintColor: Colors.transparent), inputDecorationTheme: InputDecorationTheme( border: OutlineInputBorder( borderRadius: BorderRadius.circular(15),