freezer/lib/ui/settings_screen.dart
2021-02-09 21:14:14 +01:00

1630 lines
53 KiB
Dart

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_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:language_pickers/language_pickers.dart';
import 'package:language_pickers/languages.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> {
List<Map<String, String>> _languages() {
//Missing language
defaultLanguagesList.add({
'name': 'Filipino',
'isoCode': 'fil'
});
defaultLanguagesList.add({
'name': 'Furry',
'isoCode': 'uwu'
});
defaultLanguagesList.add({
'name': 'Asturian',
'isoCode': 'ast'
});
defaultLanguagesList.add({
'name': 'Chinese',
'isoCode': 'zh'
});
List<Map<String, String>> _l = supportedLocales.map<Map<String, String>>((l) {
Map _lang = defaultLanguagesList.firstWhere((lang) => lang['isoCode'] == l.languageCode);
return {
'name': _lang['name'],
'isoCode': _lang['isoCode'],
'locale': l.toString()
};
}).toList();
return _l;
}
@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) {
Map l = _languages()[i];
return ListTile(
title: Text(l['name']),
subtitle: Text(l['locale']),
onTap: () async {
setState(() => settings.language = l['locale']);
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: [
FlatButton(
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)),
),
)
],
),
);
}
}
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: [
FlatButton(
onPressed: () async {
setState(() => settings.font = font);
await settings.save();
Navigator.of(context).pop();
widget.callback();
//Global setState
updateTheme();
},
child: Text('Apply'.i18n),
),
FlatButton(
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 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) => LanguagePickerDialog(
titlePadding: EdgeInsets.all(8.0),
isSearchable: true,
title: Text('Select language'.i18n),
languagesList: defaultLanguagesList.map<Map<String, String>>((l) => {
'isoCode': l['isoCode'], 'name': l['name'] + ' (${l["isoCode"]})'
}).toList(),
onValuePicked: (Language language) {
setState(() => settings.deezerLanguage = language.isoCode);
settings.save();
},
)
);
},
),
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: [
// FlatButton(
// child: Text('Cancel'.i18n),
// onPressed: () => Navigator.of(context).pop(),
// ),
// FlatButton(
// child: Text('Reset'.i18n),
// onPressed: () async {
// setState(() {
// settings.proxyAddress = null;
// });
// await settings.save();
// Navigator.of(context).pop();
// },
// ),
// FlatButton(
// child: Text('Save'.i18n),
// onPressed: () async {
// setState(() {
// settings.proxyAddress = _new;
// });
// await settings.save();
// Navigator.of(context).pop();
// },
// )
// ],
// );
// }
// );
// },
// )
],
),
);
}
}
class FilenameTemplateDialog extends StatefulWidget {
String initial;
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: [
FlatButton(
child: Text('Cancel'.i18n),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text('Reset'.i18n),
onPressed: () {
_controller.value = _controller.value.copyWith(text: '%artist% - %title%');
_new = '%artist% - %title%';
},
),
FlatButton(
child: Text('Clear'.i18n),
onPressed: () => _controller.clear(),
),
FlatButton(
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: [
FlatButton(
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;
playerHelper.scrobblenaut = null;
await settings.save();
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>[
FlatButton(
child: Text('Cancel'.i18n),
onPressed: () => Navigator.of(context).pop(),
),
// FlatButton(
// child: Text('(ARL ONLY) Continue'.i18n),
// onPressed: () async {
// await logOut();
// Navigator.of(context).pop();
// },
// ),
FlatButton(
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: [
FlatButton(
child: Text('Cancel'.i18n),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
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();
playerHelper.scrobblenaut = Scrobblenaut(lastFM: last);
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),
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/freezereleases');
},
),
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.rip/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
),
),
)
],
),
);
}
}