freezer/lib/ui/settings_screen.dart

1670 lines
55 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:audio_service/audio_service.dart';
import 'package:country_pickers/country.dart';
import 'package:country_pickers/country_picker_dialog.dart';
import 'package:filesize/filesize.dart';
import 'package:flutter/cupertino.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/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/error.dart';
import 'package:freezer/ui/home_screen.dart';
import 'package:freezer/ui/updater.dart';
import 'package:package_info/package_info.dart';
import 'package:path_provider_ex/path_provider_ex.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:freezer/translations.i18n.dart';
import 'package:clipboard/clipboard.dart';
import 'package:scrobblenaut/scrobblenaut.dart';
import 'package:url_launcher/url_launcher.dart';
import '../settings.dart';
import '../main.dart';
import 'dart:io';
class SettingsScreen extends StatefulWidget {
@override
_SettingsScreenState createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: FreezerAppBar('Settings'.i18n),
body: ListView(
children: <Widget>[
ListTile(
title: Text('General'.i18n),
leading: LeadingIcon(Icons.settings, color: Color(0xffeca704)),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => GeneralSettings()
)),
),
ListTile(
title: Text('Download Settings'.i18n),
leading: LeadingIcon(Icons.cloud_download, color: Color(0xffbe3266)),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => DownloadsSettings()
)),
),
ListTile(
title: Text('Appearance'.i18n),
leading: LeadingIcon(Icons.color_lens, color: Color(0xff4b2e7e)),
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => AppearanceSettings())
),
),
ListTile(
title: Text('Quality'.i18n),
leading: LeadingIcon(Icons.high_quality, color: Color(0xff384697)),
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => QualitySettings())
),
),
ListTile(
title: Text('Deezer'.i18n),
leading: LeadingIcon(Icons.equalizer, color: Color(0xff0880b5)),
onTap: () => Navigator.push(context, MaterialPageRoute(
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.push(context, MaterialPageRoute(
builder: (context) => UpdaterScreen()
)),
),
ListTile(
title: Text('About'.i18n),
leading: LeadingIcon(Icons.info, color: Colors.grey),
onTap: () => Navigator.push(context, MaterialPageRoute(
builder: (context) => CreditsScreen()
)),
),
],
),
);
}
}
class AppearanceSettings extends StatefulWidget {
@override
_AppearanceSettingsState createState() => _AppearanceSettingsState();
}
class _AppearanceSettingsState extends State<AppearanceSettings> {
ColorSwatch<dynamic> _swatch(int c) => ColorSwatch(c, {500: Color(c)});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: FreezerAppBar('Appearance'.i18n),
body: ListView(
children: <Widget>[
ListTile(
title: Text('Theme'.i18n),
subtitle: Text('Currently'.i18n + ': ${settings.theme.toString().split('.').last}'),
leading: Icon(Icons.color_lens),
onTap: () {
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
title: Text('Select theme'.i18n),
children: <Widget>[
SimpleDialogOption(
child: Text('Light'.i18n),
onPressed: () {
setState(() => settings.theme = Themes.Light);
settings.save();
updateTheme();
Navigator.of(context).pop();
},
),
SimpleDialogOption(
child: Text('Dark'.i18n),
onPressed: () {
setState(() => settings.theme = Themes.Dark);
settings.save();
updateTheme();
Navigator.of(context).pop();
},
),
SimpleDialogOption(
child: Text('Black (AMOLED)'.i18n),
onPressed: () {
setState(() => settings.theme = Themes.Black);
settings.save();
updateTheme();
Navigator.of(context).pop();
},
),
SimpleDialogOption(
child: Text('Deezer (Dark)'.i18n),
onPressed: () {
setState(() => settings.theme = Themes.Deezer);
settings.save();
updateTheme();
Navigator.of(context).pop();
},
),
],
);
}
);
},
),
ListTile(
title: Text('Use system theme'.i18n),
trailing: Switch(
value: settings.useSystemTheme,
onChanged: (bool v) async {
setState(() {
settings.useSystemTheme = v;
});
updateTheme();
await settings.save();
},
),
leading: 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())
);
},
),
ListTile(
title: Text('Player gradient background'.i18n),
leading: Icon(Icons.colorize),
trailing: Switch(
value: settings.colorGradientBackground,
onChanged: (bool v) async {
setState(() => settings.colorGradientBackground = v);
await settings.save();
},
),
),
ListTile(
title: Text('Blur player background'.i18n),
subtitle: Text('Might have impact on performance'.i18n),
leading: Icon(Icons.blur_on),
trailing: Switch(
value: settings.blurPlayerBackground,
onChanged: (bool v) async {
setState(() => settings.blurPlayerBackground = v);
await settings.save();
},
),
),
ListTile(
title: Text('Visualizer'.i18n),
subtitle: Text('Show visualizers on lyrics page. WARNING: Requires microphone permission!'.i18n),
leading: Icon(Icons.equalizer),
trailing: Switch(
value: settings.lyricsVisualizer,
onChanged: (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),
subtitle: Text(
'Selected color'.i18n,
style: TextStyle(
color: 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) {
setState(() {
settings.primaryColor = color;
});
settings.save();
updateTheme();
Navigator.of(context).pop();
},
),
),
);
}
);
},
),
ListTile(
title: Text('Use album art primary color'.i18n),
subtitle: Text('Warning: might be buggy'.i18n),
leading: Icon(Icons.invert_colors),
trailing: Switch(
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.setMode(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<FontSelector> {
String query = '';
List<String> 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<QualitySettings> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: FreezerAppBar('Quality'.i18n),
body: ListView(
children: <Widget>[
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<QualityPicker> {
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: <Widget>[
ListTile(
title: Text('MP3 128kbps'),
leading: Radio(
groupValue: _quality,
value: AudioQuality.MP3_128,
onChanged: (q) => _updateQuality(q),
),
),
ListTile(
title: Text('MP3 320kbps'),
leading: Radio(
groupValue: _quality,
value: AudioQuality.MP3_320,
onChanged: (q) => _updateQuality(q),
),
),
ListTile(
title: Text('FLAC'),
leading: Radio(
groupValue: _quality,
value: AudioQuality.FLAC,
onChanged: (q) => _updateQuality(q),
),
),
if (widget.field == 'download')
ListTile(
title: Text('Ask before downloading'.i18n),
leading: Radio(
groupValue: _quality,
value: AudioQuality.ASK,
onChanged: (q) => _updateQuality(q),
)
)
],
);
}
}
class ContentLanguage {
String code;
String name;
ContentLanguage(this.code, this.name);
static List<ContentLanguage> 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<DeezerSettings> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: FreezerAppBar('Deezer'.i18n),
body: ListView(
children: <Widget>[
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();
},
)
);
},
),
ListTile(
title: Text('Log tracks'.i18n),
subtitle: Text('Send track listen logs to Deezer, enable it for features like Flow to work properly'.i18n),
trailing: Switch(
value: settings.logListen,
onChanged: (bool v) {
setState(() => settings.logListen = v);
settings.save();
},
),
leading: Icon(Icons.history_toggle_off),
),
//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<FilenameTemplateDialog> {
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<DownloadsSettings> {
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;
//Navigate
Navigator.of(context).push(MaterialPageRoute(
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).push(MaterialPageRoute(
builder: (context) => TagSelectionScreen()
)),
),
ListTile(
title: Text('Create folders for artist'.i18n),
trailing: Switch(
value: settings.artistFolder,
onChanged: (v) {
setState(() => settings.artistFolder = v);
settings.save();
},
),
leading: Icon(Icons.folder),
),
ListTile(
title: Text('Create folders for albums'.i18n),
trailing: Switch(
value: settings.albumFolder,
onChanged: (v) {
setState(() => settings.albumFolder = v);
settings.save();
},
),
leading: Icon(Icons.folder)
),
ListTile(
title: Text('Create folder for playlist'.i18n),
trailing: Switch(
value: settings.playlistFolder,
onChanged: (v) {
setState(() => settings.playlistFolder = v);
settings.save();
},
),
leading: Icon(Icons.folder)
),
FreezerDivider(),
ListTile(
title: Text('Separate albums by discs'.i18n),
trailing: Switch(
value: settings.albumDiscFolder,
onChanged: (v) {
setState(() => settings.albumDiscFolder = v);
settings.save();
},
),
leading: Icon(Icons.album)
),
ListTile(
title: Text('Overwrite already downloaded files'.i18n),
trailing: Switch(
value: settings.overwriteDownload,
onChanged: (v) {
setState(() => settings.overwriteDownload = v);
settings.save();
},
),
leading: Icon(Icons.delete)
),
ListTile(
title: Text('Download .LRC lyrics'.i18n),
trailing: Switch(
value: settings.downloadLyrics,
onChanged: (v) {
setState(() => settings.downloadLyrics = v);
settings.save();
},
),
leading: Icon(Icons.subtitles)
),
FreezerDivider(),
ListTile(
title: Text('Save cover file for every track'.i18n),
trailing: Switch(
value: settings.trackCover,
onChanged: (v) {
setState(() => settings.trackCover = v);
settings.save();
},
),
leading: Icon(Icons.image)
),
ListTile(
title: Text('Save album cover'.i18n),
trailing: Switch(
value: settings.albumCover,
onChanged: (v) {
setState(() => settings.albumCover = v);
settings.save();
},
),
leading: 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<int>(
value: settings.albumArtResolution,
items: [400, 800, 1000, 1200, 1400, 1600, 1800].map<DropdownMenuItem<int>>((int i) => DropdownMenuItem<int>(
value: i,
child: Text(i.toString()),
)).toList(),
onChanged: (int n) async {
setState(() {
settings.albumArtResolution = n;
});
await settings.save();
},
)
)
),
ListTile(
title: Text('Create .nomedia files'.i18n),
subtitle: Text('To prevent gallery being filled with album art'.i18n),
trailing: Switch(
value: settings.nomediaFiles,
onChanged: (v) {
setState(() => settings.nomediaFiles = v);
settings.save();
},
),
leading: 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).push(
MaterialPageRoute(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<TagSelectionScreen> {
List<TagOption> 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<GeneralSettings> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: FreezerAppBar('General'.i18n),
body: ListView(
children: <Widget>[
ListTile(
title: Text('Offline mode'.i18n),
subtitle: Text('Will be overwritten on start.'.i18n),
trailing: Switch(
value: settings.offlineMode,
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: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircularProgressIndicator()
],
)
);
}
);
},
),
leading: Icon(Icons.lock),
),
ListTile(
title: Text('Copy ARL'.i18n),
subtitle: Text('Copy userToken/ARL Cookie for use in other apps.'.i18n),
leading: Icon(Icons.lock),
onTap: () async {
await FlutterClipboard.copy(settings.arl);
await Fluttertoast.showToast(
msg: 'Copied'.i18n,
);
},
),
ListTile(
title: Text('Enable equalizer'.i18n),
subtitle: Text('Might enable some equalizer apps to work. Requires restart of Freezer'.i18n),
leading: Icon(Icons.equalizer),
trailing: Switch(
value: settings.enableEqualizer,
onChanged: (v) async {
setState(() => settings.enableEqualizer = v);
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 AudioService.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: <Widget>[
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 {AudioService.stop();} catch (e) {}
await logOut();
await DownloadManager.platform.invokeMethod("kill");
SystemNavigator.pop();
},
)
],
);
}
);
}
),
ListTile(
title: Text('Ignore interruptions'.i18n),
subtitle: Text('Requires app restart to apply!'.i18n),
leading: Icon(Icons.not_interested),
trailing: Switch(
value: settings.ignoreInterruptions,
onChanged: (bool v) async {
setState(() => settings.ignoreInterruptions = v);
await settings.save();
},
),
)
],
),
);
}
}
class LastFMLogin extends StatefulWidget {
@override
_LastFMLoginState createState() => _LastFMLoginState();
}
class _LastFMLoginState extends State<LastFMLogin> {
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<DirectoryPicker> {
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: <Widget>[
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: <Widget>[
CircularProgressIndicator()
],
),
);
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
...List.generate(snapshot.data.length, (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<FileSystemEntity> data = snapshot.data;
return ListView(
children: <Widget>[
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<CreditsScreen> {
String _version = '';
static final List<List<String>> 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
),
),
)
],
),
);
}
}