import 'package:country_pickers/country.dart'; import 'package:country_pickers/country_picker_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:flutter_material_color_picker/flutter_material_color_picker.dart'; import 'package:fluttericon/font_awesome5_icons.dart'; import 'package:fluttericon/web_symbols_icons.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:freezer/api/definitions.dart'; import 'package:package_info/package_info.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:scrobblenaut/scrobblenaut.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:freezer/api/cache.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/download.dart'; import 'package:freezer/api/player.dart'; import 'package:freezer/ui/downloads_screen.dart'; import 'package:freezer/ui/elements.dart'; import 'package:freezer/ui/home_screen.dart'; import 'package:freezer/ui/updater.dart'; import 'package:freezer/translations.i18n.dart'; import 'package:freezer/settings.dart'; import 'package:freezer/main.dart'; class SettingsScreen extends StatefulWidget { @override _SettingsScreenState createState() => _SettingsScreenState(); } class _SettingsScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: FreezerAppBar('Settings'.i18n), body: ListView( children: [ ListTile( title: Text('General'.i18n), leading: LeadingIcon(Icons.settings, color: Color(0xffeca704)), onTap: () => Navigator.of(context) .pushRoute(builder: (context) => GeneralSettings()), ), ListTile( title: Text('Download Settings'.i18n), leading: LeadingIcon(Icons.cloud_download, color: Color(0xffbe3266)), onTap: () => Navigator.of(context) .pushRoute(builder: (context) => DownloadsSettings()), ), ListTile( title: Text('Appearance'.i18n), leading: LeadingIcon(Icons.color_lens, color: Color(0xff4b2e7e)), onTap: () => Navigator.of(context) .pushRoute(builder: (context) => AppearanceSettings()), ), ListTile( title: Text('Quality'.i18n), leading: LeadingIcon(Icons.high_quality, color: Color(0xff384697)), onTap: () => Navigator.of(context) .pushRoute(builder: (context) => QualitySettings()), ), ListTile( title: Text('Deezer'.i18n), leading: LeadingIcon(Icons.equalizer, color: Color(0xff0880b5)), onTap: () => Navigator.of(context) .pushRoute(builder: (context) => DeezerSettings()), ), //Language select ListTile( title: Text('Language'.i18n), leading: LeadingIcon(Icons.language, color: Color(0xff009a85)), onTap: () { showDialog( context: context, builder: (context) => SimpleDialog( title: Text('Select language'.i18n), children: List.generate(languages.length, (int i) { Language l = languages[i]; return ListTile( title: Text(l.name), subtitle: Text("${l.locale}-${l.country}"), onTap: () async { setState(() => settings.language = "${l.locale}_${l.country}"); await settings.save(); showDialog( context: context, builder: (context) { return AlertDialog( title: Text('Language'.i18n), content: Text( 'Language changed, please restart Freezer to apply!' .i18n), actions: [ TextButton( child: Text('OK'), onPressed: () { Navigator.of(context).pop(); Navigator.of(context).pop(); }, ) ], ); }); }); }))); }, ), ListTile( title: Text('Updates'.i18n), leading: LeadingIcon(Icons.update, color: Color(0xff2ba766)), onTap: () => Navigator.of(context) .pushRoute(builder: (context) => UpdaterScreen()), ), ListTile( title: Text('About'.i18n), leading: LeadingIcon(Icons.info, color: Colors.grey), onTap: () => Navigator.of(context) .pushRoute(builder: (context) => CreditsScreen()), ), ], ), ); } } class AppearanceSettings extends StatefulWidget { @override _AppearanceSettingsState createState() => _AppearanceSettingsState(); } class _AppearanceSettingsState extends State { ColorSwatch _swatch(int c) => ColorSwatch(c, {500: Color(c)}); @override Widget build(BuildContext context) { return Scaffold( appBar: FreezerAppBar('Appearance'.i18n), body: ListView( children: [ ListTile( title: Text('Theme'.i18n), subtitle: Text('Currently'.i18n + ': ${settings.theme.toString().split('.').lastItem}'), leading: Icon(Icons.color_lens), onTap: () { showDialog( context: context, builder: (context) { return SimpleDialog( title: Text('Select theme'.i18n), children: [ SimpleDialogOption( child: Text('Light'.i18n), onPressed: () { settings.theme = Themes.Light; settings.save(); updateTheme(); Navigator.of(context).pop(); }, ), SimpleDialogOption( child: Text('Dark'.i18n), onPressed: () { settings.theme = Themes.Dark; settings.save(); updateTheme(); Navigator.of(context).pop(); }, ), SimpleDialogOption( child: Text('Black (AMOLED)'.i18n), onPressed: () { settings.theme = Themes.Black; settings.save(); updateTheme(); Navigator.of(context).pop(); }, ), SimpleDialogOption( child: Text('Deezer (Dark)'.i18n), onPressed: () { settings.theme = Themes.Deezer; settings.save(); updateTheme(); Navigator.of(context).pop(); }, ), ], ); }); }, ), SwitchListTile( title: Text('Use system theme'.i18n), value: settings.useSystemTheme, onChanged: (bool v) async { settings.useSystemTheme = v; settings.save(); updateTheme(); }, secondary: Icon(Icons.android)), ListTile( title: Text('Font'.i18n), leading: Icon(Icons.font_download), subtitle: Text(settings.font), onTap: () { showDialog( context: context, builder: (context) => FontSelector(() => Navigator.of(context).pop())); }, ), SwitchListTile( title: Text('Player gradient background'.i18n), secondary: Icon(Icons.colorize), value: settings.colorGradientBackground, onChanged: (bool v) async { setState(() => settings.colorGradientBackground = v); await settings.save(); }, ), SwitchListTile( title: Text('Blur player background'.i18n), subtitle: Text('Might have impact on performance'.i18n), secondary: Icon(Icons.blur_on), value: settings.blurPlayerBackground, onChanged: (bool v) async { setState(() => settings.blurPlayerBackground = v); await settings.save(); }, ), SwitchListTile( title: Text('Use player background on lyrics page'), value: settings.playerBackgroundOnLyrics, secondary: Icon(Icons.wallpaper), onChanged: settings.blurPlayerBackground || settings.colorGradientBackground ? (bool v) { setState(() => settings.playerBackgroundOnLyrics = v); settings.save(); } : null), ListTile( title: Text('Screens style'), subtitle: Text('Style of the transition between screens within the app'), leading: Icon(Icons.auto_awesome_motion), onTap: () => showDialog( context: context, builder: (context) { return SimpleDialog( title: Text('Select screens style'), children: [ SimpleDialogOption( child: Text('Blur slide (might be laggy!)'), onPressed: () { settings.navigatorRouteType = NavigatorRouteType.blur_slide; settings.save(); Navigator.of(context).pop(); }, ), SimpleDialogOption( child: Text('Fade'), onPressed: () { settings.navigatorRouteType = NavigatorRouteType.fade; settings.save(); Navigator.of(context).pop(); }, ), SimpleDialogOption( child: Text('Fade with blur (might be laggy!)'), onPressed: () { settings.navigatorRouteType = NavigatorRouteType.fade_blur; settings.save(); Navigator.of(context).pop(); }, ), SimpleDialogOption( child: Text('Material (default)'), onPressed: () { settings.navigatorRouteType = NavigatorRouteType.material; settings.save(); Navigator.of(context).pop(); }, ), SimpleDialogOption( child: Text('Cupertino (iOS)'), onPressed: () { settings.navigatorRouteType = NavigatorRouteType.cupertino; settings.save(); Navigator.of(context).pop(); }, ), ], ); }), ), SwitchListTile( title: Text('Enable filled play button'), secondary: Icon(Icons.play_circle), value: settings.enableFilledPlayButton, onChanged: (bool v) { setState(() => settings.enableFilledPlayButton = v); settings.save(); }), SwitchListTile( title: Text('Visualizer'.i18n), subtitle: Text( 'Show visualizers on lyrics page. WARNING: Requires microphone permission!' .i18n), secondary: Icon(Icons.equalizer), value: settings.lyricsVisualizer, onChanged: null, // TODO: visualizer //(bool v) async { // if (await Permission.microphone.request().isGranted) { // setState(() => settings.lyricsVisualizer = v); // await settings.save(); // return; // } //}, ), ListTile( title: Text('Primary color'.i18n), leading: Icon(Icons.format_paint), trailing: Padding( padding: EdgeInsets.only(right: 8.0), child: CircleAvatar( backgroundColor: settings.primaryColor, )), onTap: () { showDialog( context: context, builder: (context) { return AlertDialog( title: Text('Primary color'.i18n), content: Container( height: 240, child: MaterialColorPicker( colors: [ ...Colors.primaries, //Logo colors _swatch(0xffeca704), _swatch(0xffbe3266), _swatch(0xff4b2e7e), _swatch(0xff384697), _swatch(0xff0880b5), _swatch(0xff009a85), _swatch(0xff2ba766) ], allowShades: false, selectedColor: settings.primaryColor, onMainColorChange: (ColorSwatch? color) { if (color == null) return; settings.primaryColor = color; settings.save(); updateTheme(); Navigator.of(context).pop(); }, ), ), ); }); }, ), SwitchListTile( title: Text('Use album art primary color'.i18n), subtitle: Text('Warning: might be buggy'.i18n), secondary: Icon(Icons.invert_colors), value: settings.useArtColor, onChanged: (v) => setState(() => settings.updateUseArtColor(v)), ), //Display mode ListTile( leading: Icon(Icons.screen_lock_portrait), title: Text('Change display mode'.i18n), subtitle: Text('Enable high refresh rates'.i18n), onTap: () async { List modes = await FlutterDisplayMode.supported; showDialog( context: context, builder: (context) { return SimpleDialog( title: Text('Display mode'.i18n), children: List.generate( modes.length, (i) => SimpleDialogOption( child: Text(modes[i].toString()), onPressed: () async { settings.displayMode = i; await settings.save(); await FlutterDisplayMode.setPreferredMode( modes[i]); Navigator.of(context).pop(); }, ))); }); }, ) ], ), ); } } class FontSelector extends StatefulWidget { final Function callback; FontSelector(this.callback, {Key? key}) : super(key: key); @override _FontSelectorState createState() => _FontSelectorState(); } class _FontSelectorState extends State { String query = ''; List get fonts { return settings.fonts .where((f) => f.toLowerCase().contains(query)) .toList(); } //Font selected void onTap(String font) { showDialog( context: context, builder: (context) => AlertDialog( title: Text('Warning'.i18n), content: Text( "This app isn't made for supporting many fonts, it can break layouts and overflow. Use at your own risk!" .i18n), actions: [ TextButton( onPressed: () async { setState(() => settings.font = font); await settings.save(); Navigator.of(context).pop(); widget.callback(); //Global setState updateTheme(); }, child: Text('Apply'.i18n), ), TextButton( onPressed: () { Navigator.of(context).pop(); widget.callback(); }, child: Text('Cancel'), ) ], )); } @override Widget build(BuildContext context) { return SimpleDialog( title: Text("Select font".i18n), children: [ Padding( padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0), child: TextField( decoration: InputDecoration(hintText: 'Search'.i18n), onChanged: (q) => setState(() => query = q), ), ), ...List.generate( fonts.length, (i) => SimpleDialogOption( child: Text(fonts[i]), onPressed: () => onTap(fonts[i]), )) ], ); } } class QualitySettings extends StatefulWidget { @override _QualitySettingsState createState() => _QualitySettingsState(); } class _QualitySettingsState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: FreezerAppBar('Quality'.i18n), body: ListView( children: [ ListTile( title: Text('Mobile streaming'.i18n), leading: LeadingIcon(Icons.network_cell, color: Color(0xff384697)), ), QualityPicker('mobile'), FreezerDivider(), ListTile( title: Text('Wifi streaming'.i18n), leading: LeadingIcon(Icons.network_wifi, color: Color(0xff0880b5)), ), QualityPicker('wifi'), FreezerDivider(), ListTile( title: Text('Offline'.i18n), leading: LeadingIcon(Icons.offline_pin, color: Color(0xff009a85)), ), QualityPicker('offline'), FreezerDivider(), ListTile( title: Text('External downloads'.i18n), leading: LeadingIcon(Icons.file_download, color: Color(0xff2ba766)), ), QualityPicker('download'), ], ), ); } } class QualityPicker extends StatefulWidget { final String field; QualityPicker(this.field, {Key? key}) : super(key: key); @override _QualityPickerState createState() => _QualityPickerState(); } class _QualityPickerState extends State { late AudioQuality _quality; @override void initState() { _getQuality(); super.initState(); } //Get current quality void _getQuality() { switch (widget.field) { case 'mobile': _quality = settings.mobileQuality; break; case 'wifi': _quality = settings.wifiQuality; break; case 'download': _quality = settings.downloadQuality; break; case 'offline': _quality = settings.offlineQuality; break; } } //Update quality in settings void _updateQuality(AudioQuality q) async { setState(() { _quality = q; }); switch (widget.field) { case 'mobile': settings.mobileQuality = _quality; settings.updateAudioServiceQuality(); break; case 'wifi': settings.wifiQuality = _quality; settings.updateAudioServiceQuality(); break; case 'download': settings.downloadQuality = _quality; break; case 'offline': settings.offlineQuality = _quality; break; } await settings.save(); await settings.updateAudioServiceQuality(); } @override Widget build(BuildContext context) { return Column( children: [ RadioListTile( title: Text('MP3 128kbps'), groupValue: _quality, value: AudioQuality.MP3_128, onChanged: (dynamic q) => _updateQuality(q), ), RadioListTile( title: Text('MP3 320kbps'), groupValue: _quality, value: AudioQuality.MP3_320, onChanged: (dynamic q) => _updateQuality(q), ), RadioListTile( title: Text('FLAC'), groupValue: _quality, value: AudioQuality.FLAC, onChanged: (dynamic q) => _updateQuality(q), ), if (widget.field == 'download') RadioListTile( title: Text('Ask before downloading'.i18n), groupValue: _quality, value: AudioQuality.ASK, onChanged: (dynamic q) => _updateQuality(q), ) ], ); } } class ContentLanguage { String code; String name; ContentLanguage(this.code, this.name); static List get all => [ ContentLanguage("cs", "Čeština"), ContentLanguage("da", "Dansk"), ContentLanguage("de", "Deutsch"), ContentLanguage("en", "English"), ContentLanguage("us", "English (us)"), ContentLanguage("es", "Español"), ContentLanguage("mx", "Español (latam)"), ContentLanguage("fr", "Français"), ContentLanguage("hr", "Hrvatski"), ContentLanguage("id", "Indonesia"), ContentLanguage("it", "Italiano"), ContentLanguage("hu", "Magyar"), ContentLanguage("ms", "Melayu"), ContentLanguage("nl", "Nederlands"), ContentLanguage("no", "Norsk"), ContentLanguage("pl", "Polski"), ContentLanguage("br", "Português (br)"), ContentLanguage("pt", "Português (pt)"), ContentLanguage("ro", "Română"), ContentLanguage("sk", "Slovenčina"), ContentLanguage("sl", "Slovenščina"), ContentLanguage("sq", "Shqip"), ContentLanguage("sr", "Srpski"), ContentLanguage("fi", "Suomi"), ContentLanguage("sv", "Svenska"), ContentLanguage("tr", "Türkçe"), ContentLanguage("bg", "Български"), ContentLanguage("ru", "Pусский"), ContentLanguage("uk", "Українська"), ContentLanguage("he", "עִברִית"), ContentLanguage("ar", "العربیة"), ContentLanguage("cn", "中文"), ContentLanguage("ja", "日本語"), ContentLanguage("ko", "한국어"), ContentLanguage("th", "ภาษาไทย"), ]; } class DeezerSettings extends StatefulWidget { @override _DeezerSettingsState createState() => _DeezerSettingsState(); } class _DeezerSettingsState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: FreezerAppBar('Deezer'.i18n), body: ListView( children: [ ListTile( title: Text('Content language'.i18n), subtitle: Text('Not app language, used in headers. Now'.i18n + ': ${settings.deezerLanguage}'), leading: Icon(Icons.language), onTap: () { showDialog( context: context, builder: (context) => SimpleDialog( title: Text('Select language'.i18n), children: List.generate( ContentLanguage.all.length, (i) => ListTile( title: Text(ContentLanguage.all[i].name), subtitle: Text(ContentLanguage.all[i].code), onTap: () async { setState(() => settings.deezerLanguage = ContentLanguage.all[i].code); await settings.save(); Navigator.of(context).pop(); }, )), )); }, ), ListTile( title: Text('Content country'.i18n), subtitle: Text('Country used in headers. Now'.i18n + ': ${settings.deezerCountry}'), leading: Icon(Icons.vpn_lock), onTap: () { showDialog( context: context, builder: (context) => CountryPickerDialog( titlePadding: EdgeInsets.all(8.0), isSearchable: true, onValuePicked: (Country country) { setState( () => settings.deezerCountry = country.isoCode); settings.save(); }, )); }, ), SwitchListTile( title: Text('Log tracks'.i18n), subtitle: Text( 'Send track listen logs to Deezer, enable it for features like Flow to work properly' .i18n), value: settings.logListen!, secondary: Icon(Icons.history_toggle_off), onChanged: (bool v) { setState(() => settings.logListen = v); settings.save(); }, ), //TODO: Reimplement proxy // ListTile( // title: Text('Proxy'.i18n), // leading: Icon(Icons.vpn_key), // subtitle: Text(settings.proxyAddress??'Not set'.i18n), // onTap: () { // String _new; // showDialog( // context: context, // builder: (BuildContext context) { // return AlertDialog( // title: Text('Proxy'.i18n), // content: TextField( // onChanged: (String v) => _new = v, // decoration: InputDecoration( // hintText: 'IP:PORT' // ), // ), // actions: [ // TextButton( // child: Text('Cancel'.i18n), // onPressed: () => Navigator.of(context).pop(), // ), // TextButton( // child: Text('Reset'.i18n), // onPressed: () async { // setState(() { // settings.proxyAddress = null; // }); // await settings.save(); // Navigator.of(context).pop(); // }, // ), // TextButton( // child: Text('Save'.i18n), // onPressed: () async { // setState(() { // settings.proxyAddress = _new; // }); // await settings.save(); // Navigator.of(context).pop(); // }, // ) // ], // ); // } // ); // }, // ) ], ), ); } } class FilenameTemplateDialog extends StatefulWidget { final String? initial; final Function onSave; FilenameTemplateDialog(this.initial, this.onSave, {Key? key}) : super(key: key); @override _FilenameTemplateDialogState createState() => _FilenameTemplateDialogState(); } class _FilenameTemplateDialogState extends State { TextEditingController? _controller; String? _new; @override void initState() { _controller = TextEditingController(text: widget.initial); _new = _controller!.value.text; super.initState(); } @override Widget build(BuildContext context) { //Dialog with filename format return AlertDialog( title: Text('Downloaded tracks filename'.i18n), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: _controller, onChanged: (String s) => _new = s, ), Container(height: 8.0), Text( 'Valid variables are'.i18n + ': %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%, %feats%, %playlistTrackNumber%, %0playlistTrackNumber%, %year%, %date%\n\n' + "If you want to use custom directory naming - use '/' as directory separator." .i18n, style: TextStyle( fontSize: 12.0, ), ) ], ), actions: [ TextButton( child: Text('Cancel'.i18n), onPressed: () => Navigator.of(context).pop(), ), TextButton( child: Text('Reset'.i18n), onPressed: () { _controller!.value = _controller!.value.copyWith(text: '%artist% - %title%'); _new = '%artist% - %title%'; }, ), TextButton( child: Text('Clear'.i18n), onPressed: () => _controller!.clear(), ), TextButton( child: Text('Save'.i18n), onPressed: () async { widget.onSave(_new); Navigator.of(context).pop(); }, ) ], ); } } class DownloadsSettings extends StatefulWidget { @override _DownloadsSettingsState createState() => _DownloadsSettingsState(); } class _DownloadsSettingsState extends State { double _downloadThreads = settings.downloadThreads.toDouble(); TextEditingController _artistSeparatorController = TextEditingController(text: settings.artistSeparator); @override Widget build(BuildContext context) { return Scaffold( appBar: FreezerAppBar('Download Settings'.i18n), body: ListView( children: [ ListTile( title: Text('Download path'.i18n), leading: Icon(Icons.folder), subtitle: Text(settings.downloadPath!), onTap: () async { //Check permissions if (!await Permission.storage.request().isGranted) return; DownloadManager.getDirectory('Pick-a-Path'.i18n).then((path) { if (path == null) return; // user canceled setState(() => settings.downloadPath = path); settings.save(); }); //Navigate // Navigator.of(context).pushRoute( // builder: (context) => DirectoryPicker( // settings.downloadPath, // onSelect: (String p) async { // setState(() => settings.downloadPath = p); // await settings.save(); // }, // )); }, ), ListTile( title: Text('Downloads naming'.i18n), subtitle: Text('Currently'.i18n + ': ${settings.downloadFilename}'), leading: Icon(Icons.text_format), onTap: () { showDialog( context: context, builder: (context) { return FilenameTemplateDialog(settings.downloadFilename, (f) async { setState(() => settings.downloadFilename = f); await settings.save(); }); }); }, ), ListTile( title: Text('Singleton naming'.i18n), subtitle: Text('Currently'.i18n + ': ${settings.singletonFilename}'), leading: Icon(Icons.text_format), onTap: () { showDialog( context: context, builder: (context) { return FilenameTemplateDialog(settings.singletonFilename, (f) async { setState(() => settings.singletonFilename = f); await settings.save(); }); }); }, ), Padding( padding: EdgeInsets.symmetric(horizontal: 16.0), child: Text( 'Download threads'.i18n + ': ${_downloadThreads.round().toString()}', style: TextStyle(fontSize: 16.0), ), ), Slider( min: 1, max: 16, divisions: 15, value: _downloadThreads, label: _downloadThreads.round().toString(), onChanged: (double v) => setState(() => _downloadThreads = v), onChangeEnd: (double val) async { _downloadThreads = val; setState(() { settings.downloadThreads = _downloadThreads.round(); _downloadThreads = settings.downloadThreads.toDouble(); }); await settings.save(); //Prevent null if (val > 8 && cache.threadsWarning != true) { showDialog( context: context, builder: (context) { return AlertDialog( title: Text('Warning'.i18n), content: Text( 'Using too many concurrent downloads on older/weaker devices might cause crashes!' .i18n), actions: [ TextButton( child: Text('Dismiss'.i18n), onPressed: () => Navigator.of(context).pop(), ) ], ); }); cache.threadsWarning = true; await cache.save(); } }), FreezerDivider(), ListTile( title: Text('Tags'.i18n), leading: Icon(Icons.label), onTap: () => Navigator.of(context) .pushRoute(builder: (context) => TagSelectionScreen()), ), SwitchListTile( title: Text('Create folders for artist'.i18n), value: settings.artistFolder, onChanged: (v) { setState(() => settings.artistFolder = v); settings.save(); }, secondary: Icon(Icons.folder), ), SwitchListTile( title: Text('Create folders for albums'.i18n), value: settings.albumFolder, onChanged: (v) { setState(() => settings.albumFolder = v); settings.save(); }, secondary: Icon(Icons.folder)), SwitchListTile( title: Text('Create folder for playlist'.i18n), value: settings.playlistFolder, onChanged: (v) { setState(() => settings.playlistFolder = v); settings.save(); }, secondary: Icon(Icons.folder)), FreezerDivider(), SwitchListTile( title: Text('Separate albums by discs'.i18n), value: settings.albumDiscFolder, onChanged: (v) { setState(() => settings.albumDiscFolder = v); settings.save(); }, secondary: Icon(Icons.album)), SwitchListTile( title: Text('Overwrite already downloaded files'.i18n), value: settings.overwriteDownload, onChanged: (v) { setState(() => settings.overwriteDownload = v); settings.save(); }, secondary: Icon(Icons.delete)), SwitchListTile( title: Text('Download .LRC lyrics'.i18n), value: settings.downloadLyrics, onChanged: (v) { setState(() => settings.downloadLyrics = v); settings.save(); }, secondary: Icon(Icons.subtitles)), FreezerDivider(), SwitchListTile( title: Text('Save cover file for every track'.i18n), value: settings.trackCover, onChanged: (v) { setState(() => settings.trackCover = v); settings.save(); }, secondary: Icon(Icons.image)), SwitchListTile( title: Text('Save album cover'.i18n), value: settings.albumCover, onChanged: (v) { setState(() => settings.albumCover = v); settings.save(); }, secondary: Icon(Icons.image)), ListTile( title: Text('Album cover resolution'.i18n), subtitle: Text( "WARNING: Resolutions above 1200 aren't officially supported" .i18n), leading: Icon(Icons.image), trailing: Container( width: 75.0, child: DropdownButton( value: settings.albumArtResolution, items: [400, 800, 1000, 1200, 1400, 1600, 1800] .map>( (int i) => DropdownMenuItem( value: i, child: Text(i.toString()), )) .toList(), onChanged: (int? n) async { if (n == null) return; setState(() { settings.albumArtResolution = n; }); await settings.save(); }, ))), SwitchListTile( title: Text('Create .nomedia files'.i18n), subtitle: Text('To prevent gallery being filled with album art'.i18n), value: settings.nomediaFiles, onChanged: (v) { setState(() => settings.nomediaFiles = v); settings.save(); }, secondary: Icon(Icons.insert_drive_file)), ListTile( title: Text('Artist separator'.i18n), leading: Icon(WebSymbols.tag), trailing: Container( width: 75.0, child: TextField( controller: _artistSeparatorController, onChanged: (s) async { settings.artistSeparator = s; await settings.save(); }, ), ), ), FreezerDivider(), ListTile( title: Text('Download Log'.i18n), leading: Icon(Icons.sticky_note_2), onTap: () => Navigator.of(context) .pushRoute(builder: (context) => DownloadLogViewer()), ) ], ), ); } } class TagOption { String title; String value; TagOption(this.title, this.value); } class TagSelectionScreen extends StatefulWidget { @override _TagSelectionScreenState createState() => _TagSelectionScreenState(); } class _TagSelectionScreenState extends State { List tags = [ TagOption("Title".i18n, 'title'), TagOption("Album".i18n, 'album'), TagOption('Artist'.i18n, 'artist'), TagOption('Track number'.i18n, 'track'), TagOption('Disc number'.i18n, 'disc'), TagOption('Album artist'.i18n, 'albumArtist'), TagOption('Date/Year'.i18n, 'date'), TagOption('Label'.i18n, 'label'), TagOption('ISRC'.i18n, 'isrc'), TagOption('UPC'.i18n, 'upc'), TagOption('Track total'.i18n, 'trackTotal'), TagOption('BPM'.i18n, 'bpm'), TagOption('Unsynchronized lyrics'.i18n, 'lyrics'), TagOption('Genre'.i18n, 'genre'), TagOption('Contributors'.i18n, 'contributors'), TagOption('Album art'.i18n, 'art') ]; @override Widget build(BuildContext context) { return Scaffold( appBar: FreezerAppBar('Tags'.i18n), body: ListView( children: List.generate( tags.length, (i) => ListTile( title: Text(tags[i].title), leading: Switch( value: settings.tags.contains(tags[i].value), onChanged: (v) async { //Update if (v) settings.tags.add(tags[i].value); else settings.tags.remove(tags[i].value); setState(() {}); await settings.save(); }, ), )), ), ); } } class GeneralSettings extends StatefulWidget { @override _GeneralSettingsState createState() => _GeneralSettingsState(); } class _GeneralSettingsState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: FreezerAppBar('General'.i18n), body: ListView( children: [ SwitchListTile( title: Text('Offline mode'.i18n), subtitle: Text('Will be overwritten on start.'.i18n), value: settings.offlineMode, secondary: Icon(Icons.lock), onChanged: (bool v) { if (v) { setState(() => settings.offlineMode = true); return; } showDialog( context: context, builder: (context) { deezerAPI.authorize().then((v) { if (v) { setState(() => settings.offlineMode = false); } else { Fluttertoast.showToast( msg: 'Error logging in, check your internet connections.' .i18n, gravity: ToastGravity.BOTTOM, toastLength: Toast.LENGTH_SHORT); } Navigator.of(context).pop(); }); return AlertDialog( title: Text('Logging in...'.i18n), content: const Center(child: CircularProgressIndicator())); }); }, ), SwitchListTile( title: Text('Enable equalizer'.i18n), subtitle: Text( 'Might enable some equalizer apps to work. Requires restart of Freezer' .i18n), secondary: Icon(Icons.equalizer), value: settings.enableEqualizer, onChanged: (v) async { setState(() => settings.enableEqualizer = v); settings.save(); }, ), SwitchListTile( title: Text('Ignore interruptions'.i18n), subtitle: Text('Requires app restart to apply!'.i18n), secondary: Icon(Icons.not_interested), value: settings.ignoreInterruptions, onChanged: (bool v) async { setState(() => settings.ignoreInterruptions = v); await settings.save(); }, ), ListTile( title: Text('LastFM'.i18n), subtitle: Text((settings.lastFMPassword != null && settings.lastFMUsername != null) ? 'Log out'.i18n : 'Login to enable scrobbling.'.i18n), leading: Icon(FontAwesome5.lastfm), onTap: () async { //Log out if (settings.lastFMPassword != null && settings.lastFMUsername != null) { settings.lastFMUsername = null; settings.lastFMPassword = null; await settings.save(); await audioHandler.customAction("disableLastFM", {}); setState(() {}); Fluttertoast.showToast(msg: 'Logged out!'.i18n); return; } await showDialog( context: context, builder: (context) => LastFMLogin()); setState(() {}); }, ), ListTile( title: Text( 'Log out'.i18n, style: TextStyle(color: Colors.red), ), leading: Icon(Icons.exit_to_app), onTap: () { showDialog( context: context, builder: (context) { return AlertDialog( title: Text('Log out'.i18n), // content: Text('Due to plugin incompatibility, login using browser is unavailable without restart.'.i18n), content: Text( 'Restart of app is required to properly log out!' .i18n), actions: [ TextButton( child: Text('Cancel'.i18n), onPressed: () => Navigator.of(context).pop(), ), // TextButton( // child: Text('(ARL ONLY) Continue'.i18n), // onPressed: () async { // await logOut(); // Navigator.of(context).pop(); // }, // ), TextButton( child: Text('Log out & Exit'.i18n), onPressed: () async { try { audioHandler.stop(); } catch (e) {} await logOut(); await DownloadManager.platform .invokeMethod("kill"); SystemNavigator.pop(); }, ) ], ); }); }), ListTile( title: Text('Copy ARL'.i18n), subtitle: Text('Copy userToken/ARL Cookie for use in other apps.'.i18n), leading: Icon(Icons.lock), onTap: () async { Clipboard.setData(ClipboardData(text: settings.arl)); await ScaffoldMessenger.of(context).snack('Copied'.i18n); }, ), ], ), ); } } class LastFMLogin extends StatefulWidget { @override _LastFMLoginState createState() => _LastFMLoginState(); } class _LastFMLoginState extends State { String _username = ''; String _password = ''; @override Widget build(BuildContext context) { return AlertDialog( title: Text('Login to LastFM'.i18n), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( decoration: InputDecoration(hintText: 'Username'.i18n), onChanged: (v) => _username = v, ), Container(height: 8.0), TextField( obscureText: true, decoration: InputDecoration(hintText: 'Password'.i18n), onChanged: (v) => _password = v, ) ], ), actions: [ TextButton( child: Text('Cancel'.i18n), onPressed: () => Navigator.of(context).pop(), ), TextButton( child: Text('Login'.i18n), onPressed: () async { LastFM last; try { last = await LastFM.authenticate( apiKey: 'b6ab5ae967bcd8b10b23f68f42493829', apiSecret: '861b0dff9a8a574bec747f9dab8b82bf', username: _username, password: _password); } catch (e) { Fluttertoast.showToast(msg: 'Authorization error!'.i18n); return; } //Save settings.lastFMUsername = last.username; settings.lastFMPassword = last.passwordHash; await settings.save(); await playerHelper.authorizeLastFM(); Navigator.of(context).pop(); }, ), ], ); } } // class DirectoryPicker extends StatefulWidget { // final String initialPath; // final Function onSelect; // DirectoryPicker(this.initialPath, {this.onSelect, Key key}) : super(key: key); // @override // _DirectoryPickerState createState() => _DirectoryPickerState(); // } // class _DirectoryPickerState extends State { // String _path; // String _previous; // String _root; // @override // void initState() { // _path = widget.initialPath; // super.initState(); // } // Future _resetPath() async { // StorageInfo si = (await PathProviderEx.getStorageInfo())[0]; // setState(() => _path = si.appFilesDir); // } // @override // Widget build(BuildContext context) { // return Scaffold( // appBar: FreezerAppBar( // 'Pick-a-Path'.i18n, // actions: [ // IconButton( // icon: Icon( // Icons.sd_card, // semanticLabel: 'Select storage'.i18n, // ), // onPressed: () { // String path = ''; // //Chose storage // showDialog( // context: context, // builder: (context) { // return AlertDialog( // title: Text('Select storage'.i18n), // content: FutureBuilder( // future: PathProviderEx.getStorageInfo(), // builder: (context, snapshot) { // if (snapshot.hasError) return ErrorScreen(); // if (!snapshot.hasData) // return Padding( // padding: EdgeInsets.symmetric(vertical: 8.0), // child: Row( // mainAxisAlignment: MainAxisAlignment.center, // children: [ // CircularProgressIndicator() // ], // ), // ); // return Column( // mainAxisSize: MainAxisSize.min, // children: List.generate( // snapshot.data.length, (int i) { // StorageInfo si = snapshot.data[i]; // return ListTile( // title: Text(si.rootDir), // leading: Icon(Icons.sd_card), // trailing: Text(filesize(si.availableBytes)), // onTap: () { // setState(() { // _path = si.appFilesDir; // //Android 5+ blocks sd card, so this prevents going outside // //app data dir, until permission request fix. // _root = si.rootDir; // if (i != 0) _root = si.appFilesDir; // }); // Navigator.of(context).pop(); // }, // ); // })); // }, // ), // ); // }); // }) // ], // ), // floatingActionButton: FloatingActionButton( // child: Icon(Icons.done), // onPressed: () { // //When folder confirmed // if (widget.onSelect != null) widget.onSelect(_path); // Navigator.of(context).pop(); // }, // ), // body: FutureBuilder( // future: Directory(_path).list().toList(), // builder: (BuildContext context, AsyncSnapshot snapshot) { // //On error go to last good path // if (snapshot.hasError) // Future.delayed(Duration(milliseconds: 50), () { // if (_previous == null) { // _resetPath(); // return; // } // setState(() => _path = _previous); // }); // if (!snapshot.hasData) // return Center( // child: CircularProgressIndicator(), // ); // List data = snapshot.data; // return ListView( // children: [ // ListTile( // title: Text(_path), // ), // ListTile( // title: Text('Go up'.i18n), // leading: Icon(Icons.arrow_upward), // onTap: () { // setState(() { // if (_root == _path) { // Fluttertoast.showToast( // msg: 'Permission denied'.i18n, // gravity: ToastGravity.BOTTOM); // return; // } // _previous = _path; // _path = Directory(_path).parent.path; // }); // }, // ), // ...List.generate(data.length, (i) { // FileSystemEntity f = data[i]; // if (f is Directory) { // return ListTile( // title: Text(f.path.split('/').last), // leading: Icon(Icons.folder), // onTap: () { // setState(() { // _previous = _path; // _path = f.path; // }); // }, // ); // } // return Container( // height: 0, // width: 0, // ); // }) // ], // ); // }, // ), // ); // } // } class CreditsScreen extends StatefulWidget { @override _CreditsScreenState createState() => _CreditsScreenState(); } class _CreditsScreenState extends State { String _version = ''; static final List> translators = [ ['Xandar Null', 'Arabic'], ['Markus', 'German'], ['Andrea', 'Italian'], ['Diego Hiro', 'Portuguese'], ['Orfej', 'Russian'], ['Chino Pacia', 'Filipino'], ['ArcherDelta & PetFix', 'Spanish'], ['Shazzaam', 'Croatian'], ['VIRGIN_KLM', 'Greek'], ['koreezzz', 'Korean'], ['Fwwwwwwwwwweze', 'French'], ['kobyrevah', 'Hebrew'], ['HoScHaKaL', 'Turkish'], ['MicroMihai', 'Romanian'], ['LenteraMalam', 'Indonesian'], ['RTWO2', 'Persian'] ]; @override void initState() { PackageInfo.fromPlatform().then((info) { setState(() { _version = 'v${info.version}'; }); }); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: FreezerAppBar('About'.i18n), body: ListView( children: [ FreezerTitle(), Text( _version, textAlign: TextAlign.center, style: TextStyle(fontStyle: FontStyle.italic), ), FreezerDivider(), ListTile( title: Text('Telegram Channel'.i18n), subtitle: Text('To get latest releases'.i18n), leading: Icon(FontAwesome5.telegram, color: Color(0xFF27A2DF), size: 36.0), onTap: () { launch('https://t.me/joinchat/Se4zLEBvjS1NCiY9'); }, ), ListTile( title: Text('Telegram Group'.i18n), subtitle: Text('Official chat'.i18n), leading: Icon(FontAwesome5.telegram, color: Colors.cyan, size: 36.0), onTap: () => launch('https://t.me/freezerandroid'), ), ListTile( title: Text('Discord'.i18n), subtitle: Text('Official Discord server'.i18n), leading: Icon(FontAwesome5.discord, color: Color(0xff7289da), size: 36.0), onTap: () => launch('https://discord.gg/qwJpa3r4dQ'), ), ListTile( title: Text('Repository'.i18n), subtitle: Text('Source code, report issues there.'.i18n), leading: Icon(Icons.code, color: Colors.green, size: 36.0), onTap: () { launch('https://git.freezer.life/exttex/freezer'); }, ), ListTile( title: Text('Donate'), subtitle: Text( 'You should rather support your favorite artists, instead of this app!'), leading: Icon(FontAwesome5.paypal, color: Colors.blue, size: 36.0), onTap: () => launch('https://paypal.me/exttex'), ), FreezerDivider(), ListTile( title: Text('exttex'), subtitle: Text('Developer'), ), ListTile( title: Text('Bas Curtiz'), subtitle: Text('Icon, logo, banner, design suggestions, tester'), ), ListTile( title: Text('Tobs'), subtitle: Text('Alpha testers'), ), ListTile( title: Text('Deemix'), subtitle: Text('Better app <3'), ), ListTile( title: Text('Xandar Null'), subtitle: Text('Tester, translations help'), ), ListTile( title: Text('Francesco'), subtitle: Text('Tester'), onTap: () { setState(() { settings.primaryColor = Color(0xff333333); }); updateTheme(); settings.save(); }, ), ListTile( title: Text('Annexhack'), subtitle: Text('Android Auto help'), ), FreezerDivider(), ...List.generate( translators.length, (i) => ListTile( title: Text(translators[i][0]), subtitle: Text(translators[i][1]), )), Padding( padding: EdgeInsets.fromLTRB(0, 4, 0, 8), child: Text( 'Huge thanks to all the contributors! <3'.i18n, textAlign: TextAlign.center, style: TextStyle(fontSize: 16.0), ), ) ], ), ); } }