Pato05
87c9733f51
fix audio service stop on android getTrack backend improvements get new track token when expired move shuffle button into LibraryPlaylists as FAB move favoriteButton next to track title move lyrics button on top of album art search: fix chips, and remove checkbox when selected
1882 lines
69 KiB
Dart
1882 lines
69 KiB
Dart
import 'dart:async';
|
||
import 'dart:io';
|
||
import 'dart:math';
|
||
|
||
import 'package:country_pickers/country.dart';
|
||
import 'package:country_pickers/country_picker_dialog.dart';
|
||
import 'package:flex_color_picker/flex_color_picker.dart';
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||
import 'package:fluttertoast/fluttertoast.dart';
|
||
import 'package:freezer/api/definitions.dart';
|
||
import 'package:freezer/api/player/systray.dart';
|
||
import 'package:freezer/icons.dart';
|
||
import 'package:freezer/ui/login_on_other_device.dart';
|
||
import 'package:freezer/ui/login_screen.dart';
|
||
import 'package:package_info_plus/package_info_plus.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/audio_handler.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 {
|
||
const SettingsScreen({super.key});
|
||
|
||
@override
|
||
State<SettingsScreen> createState() => _SettingsScreenState();
|
||
}
|
||
|
||
class _SettingsScreenState extends State<SettingsScreen> {
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(title: Text('Settings'.i18n)),
|
||
body: ListView(
|
||
children: <Widget>[
|
||
ListTile(
|
||
title: Text('General'.i18n),
|
||
leading:
|
||
const LeadingIcon(Icons.settings, color: Color(0xffeca704)),
|
||
onTap: () => Navigator.of(context)
|
||
.pushRoute(builder: (context) => const GeneralSettings()),
|
||
),
|
||
ListTile(
|
||
title: Text('Download Settings'.i18n),
|
||
leading: const LeadingIcon(Icons.cloud_download,
|
||
color: Color(0xffbe3266)),
|
||
onTap: () => Navigator.of(context)
|
||
.pushRoute(builder: (context) => const DownloadsSettings()),
|
||
),
|
||
ListTile(
|
||
title: Text('Appearance'.i18n),
|
||
leading:
|
||
const LeadingIcon(Icons.color_lens, color: Color(0xff4b2e7e)),
|
||
onTap: () => Navigator.of(context)
|
||
.pushRoute(builder: (context) => const AppearanceSettings()),
|
||
),
|
||
ListTile(
|
||
title: Text('Quality'.i18n),
|
||
leading:
|
||
const LeadingIcon(Icons.high_quality, color: Color(0xff384697)),
|
||
onTap: () => Navigator.of(context)
|
||
.pushRoute(builder: (context) => const QualitySettings()),
|
||
),
|
||
ListTile(
|
||
title: Text('Deezer'.i18n),
|
||
leading:
|
||
const LeadingIcon(Icons.equalizer, color: Color(0xff0880b5)),
|
||
onTap: () => Navigator.of(context)
|
||
.pushRoute(builder: (context) => const DeezerSettings()),
|
||
),
|
||
//Language select
|
||
ListTile(
|
||
title: Text('Language'.i18n),
|
||
leading:
|
||
const 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) {
|
||
final Language l = languages[i];
|
||
return ListTile(
|
||
title: Text(l.name),
|
||
subtitle: Text("${l.locale}-${l.country}"),
|
||
onTap: () async {
|
||
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: const Text('OK'),
|
||
onPressed: () {
|
||
Navigator.pop(context);
|
||
Navigator.pop(context);
|
||
},
|
||
)
|
||
],
|
||
);
|
||
});
|
||
});
|
||
})));
|
||
},
|
||
),
|
||
ListTile(
|
||
title: Text('Updates'.i18n),
|
||
leading: const LeadingIcon(Icons.update, color: Color(0xff2ba766)),
|
||
onTap: () => Navigator.of(context)
|
||
.pushRoute(builder: (context) => const UpdaterScreen()),
|
||
),
|
||
ListTile(
|
||
title: Text('About'.i18n),
|
||
leading: const LeadingIcon(Icons.info, color: Colors.grey),
|
||
onTap: () => Navigator.of(context)
|
||
.pushRoute(builder: (context) => const CreditsScreen()),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class AppearanceSettings extends StatefulWidget {
|
||
const AppearanceSettings({super.key});
|
||
|
||
@override
|
||
State<AppearanceSettings> createState() => _AppearanceSettingsState();
|
||
}
|
||
|
||
class _AppearanceSettingsState extends State<AppearanceSettings> {
|
||
ColorSwatch<dynamic> _swatch(int c) => ColorSwatch(c, {500: Color(c)});
|
||
|
||
String _navigationRailAppearanceToString(
|
||
NavigationRailAppearance navigationRailAppearance) {
|
||
switch (navigationRailAppearance) {
|
||
case NavigationRailAppearance.always_expanded:
|
||
return 'Always expanded'.i18n;
|
||
case NavigationRailAppearance.expand_on_hover:
|
||
return 'Expand on hover'.i18n;
|
||
case NavigationRailAppearance.icons_only:
|
||
return 'Icons only'.i18n;
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(title: Text('Appearance'.i18n)),
|
||
body: ListView(
|
||
children: <Widget>[
|
||
ListTile(
|
||
title: Text('Theme'.i18n),
|
||
subtitle: Text(
|
||
'${'Currently'.i18n}: ${settings.theme.toString().split('.').lastItem}'),
|
||
leading: const Icon(Icons.color_lens),
|
||
enabled: !settings.materialYouAccent,
|
||
onTap: () {
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) {
|
||
return SimpleDialog(
|
||
title: Text('Select theme'.i18n),
|
||
children: <Widget>[
|
||
SimpleDialogOption(
|
||
child: Text('Light'.i18n),
|
||
onPressed: () {
|
||
settings.theme = Themes.Light;
|
||
settings.save();
|
||
updateTheme(context);
|
||
Navigator.of(context).pop();
|
||
},
|
||
),
|
||
SimpleDialogOption(
|
||
child: Text('Dark'.i18n),
|
||
onPressed: () {
|
||
settings.theme = Themes.Dark;
|
||
settings.save();
|
||
updateTheme(context);
|
||
Navigator.of(context).pop();
|
||
},
|
||
),
|
||
SimpleDialogOption(
|
||
child: Text('Black (AMOLED)'.i18n),
|
||
onPressed: () {
|
||
settings.theme = Themes.Black;
|
||
settings.save();
|
||
updateTheme(context);
|
||
Navigator.of(context).pop();
|
||
},
|
||
),
|
||
SimpleDialogOption(
|
||
child: Text('Deezer (Dark)'.i18n),
|
||
onPressed: () {
|
||
settings.theme = Themes.Deezer;
|
||
settings.save();
|
||
updateTheme(context);
|
||
Navigator.of(context).pop();
|
||
},
|
||
),
|
||
],
|
||
);
|
||
});
|
||
},
|
||
),
|
||
SwitchListTile(
|
||
title: Text('Use system theme'.i18n),
|
||
value: settings.useSystemTheme,
|
||
onChanged: settings.materialYouAccent
|
||
? null
|
||
: (bool v) async {
|
||
settings.useSystemTheme = v;
|
||
|
||
settings.save();
|
||
updateTheme(context);
|
||
},
|
||
secondary: const Icon(Icons.android)),
|
||
SwitchListTile(
|
||
value: settings.materialYouAccent,
|
||
title: Text('Use Material You accent'.i18n),
|
||
onChanged: (bool v) {
|
||
settings.materialYouAccent = v;
|
||
|
||
settings.save();
|
||
updateTheme(context);
|
||
}),
|
||
ListTile(
|
||
title: Text('Font'.i18n),
|
||
leading: const 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: const Icon(Icons.colorize),
|
||
value: settings.colorGradientBackground,
|
||
onChanged: (bool v) async {
|
||
setState(() => settings.colorGradientBackground = v);
|
||
await settings.save();
|
||
},
|
||
),
|
||
SwitchListTile(
|
||
title: Text('Player album art drop shadow'.i18n),
|
||
secondary: const Icon(Icons.opacity),
|
||
value: settings.playerAlbumArtDropShadow,
|
||
onChanged: (bool v) async {
|
||
setState(() => settings.playerAlbumArtDropShadow = v);
|
||
await settings.save();
|
||
},
|
||
),
|
||
SwitchListTile(
|
||
title: Text('Blur player background'.i18n),
|
||
subtitle: Text('Might have impact on performance'.i18n),
|
||
secondary: const Icon(Icons.blur_on),
|
||
value: settings.blurPlayerBackground,
|
||
onChanged: (bool v) async {
|
||
setState(() => settings.blurPlayerBackground = v);
|
||
await settings.save();
|
||
},
|
||
),
|
||
SwitchListTile(
|
||
title: const Text('Use player background on lyrics page'),
|
||
value: settings.playerBackgroundOnLyrics,
|
||
secondary: const Icon(Icons.wallpaper),
|
||
onChanged: settings.blurPlayerBackground ||
|
||
settings.colorGradientBackground
|
||
? (bool v) {
|
||
setState(() => settings.playerBackgroundOnLyrics = v);
|
||
settings.save();
|
||
}
|
||
: null),
|
||
ListTile(
|
||
title: const Text('Screens style'),
|
||
subtitle: const Text(
|
||
'Style of the transition between screens within the app'),
|
||
leading: const Icon(Icons.auto_awesome_motion),
|
||
onTap: () => showDialog(
|
||
context: context,
|
||
builder: (context) {
|
||
return SimpleDialog(
|
||
title: const Text('Select screens style'),
|
||
children: <Widget>[
|
||
SimpleDialogOption(
|
||
child: const Text('Blur slide (might be laggy!)'),
|
||
onPressed: () {
|
||
settings.navigatorRouteType =
|
||
NavigatorRouteType.blur_slide;
|
||
settings.save();
|
||
Navigator.of(context).pop();
|
||
},
|
||
),
|
||
SimpleDialogOption(
|
||
child: const Text('Fade'),
|
||
onPressed: () {
|
||
settings.navigatorRouteType = NavigatorRouteType.fade;
|
||
settings.save();
|
||
Navigator.of(context).pop();
|
||
},
|
||
),
|
||
SimpleDialogOption(
|
||
child: const Text('Fade with blur (might be laggy!)'),
|
||
onPressed: () {
|
||
settings.navigatorRouteType =
|
||
NavigatorRouteType.fade_blur;
|
||
settings.save();
|
||
Navigator.of(context).pop();
|
||
},
|
||
),
|
||
SimpleDialogOption(
|
||
child: const Text('Material (default)'),
|
||
onPressed: () {
|
||
settings.navigatorRouteType =
|
||
NavigatorRouteType.material;
|
||
settings.save();
|
||
Navigator.of(context).pop();
|
||
},
|
||
),
|
||
SimpleDialogOption(
|
||
child: const Text('Cupertino (iOS)'),
|
||
onPressed: () {
|
||
settings.navigatorRouteType =
|
||
NavigatorRouteType.cupertino;
|
||
settings.save();
|
||
Navigator.of(context).pop();
|
||
},
|
||
),
|
||
],
|
||
);
|
||
}),
|
||
),
|
||
SwitchListTile(
|
||
title: Text('Enable filled play button'.i18n),
|
||
secondary: const Icon(Icons.play_circle),
|
||
value: settings.enableFilledPlayButton,
|
||
onChanged: (bool v) {
|
||
setState(() => settings.enableFilledPlayButton = v);
|
||
settings.save();
|
||
}),
|
||
SwitchListTile(
|
||
title: Text('Material 3 play button'.i18n),
|
||
secondary: const Icon(Icons.play_circle_outline),
|
||
value: settings.enableMaterial3PlayButton,
|
||
onChanged: (bool v) {
|
||
setState(() => settings.enableMaterial3PlayButton = v);
|
||
settings.save();
|
||
}),
|
||
SwitchListTile(
|
||
title: Text('Visualizer'.i18n),
|
||
subtitle: Text(
|
||
'Show visualizers on lyrics page. WARNING: Requires microphone permission!'
|
||
.i18n),
|
||
secondary: const 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: const Icon(Icons.format_paint),
|
||
trailing: Padding(
|
||
padding: const EdgeInsets.only(right: 8.0),
|
||
child: CircleAvatar(
|
||
backgroundColor: settings.primaryColor,
|
||
)),
|
||
enabled: !settings.materialYouAccent,
|
||
onTap: () async {
|
||
final color = await showDialog<Color>(
|
||
context: context, builder: (context) => const _ColorPicker());
|
||
if (color == null) return;
|
||
settings.primaryColor = color;
|
||
settings.save();
|
||
updateTheme(context);
|
||
|
||
//showDialog(
|
||
// context: context,
|
||
// builder: (context) {
|
||
// return AlertDialog(
|
||
// title: Text('Primary color'.i18n),
|
||
// content: SizedBox(
|
||
// 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(context);
|
||
// Navigator.of(context).pop();
|
||
// },
|
||
// ),
|
||
// ),
|
||
// );
|
||
// });
|
||
},
|
||
),
|
||
SwitchListTile(
|
||
title: Text('Use album art primary color'.i18n),
|
||
subtitle: Text('Warning: might be buggy'.i18n),
|
||
secondary: const Icon(Icons.invert_colors),
|
||
value: settings.useArtColor,
|
||
onChanged: (v) => setState(() => settings.updateUseArtColor(v)),
|
||
),
|
||
if (MainScreen.of(context).isDesktop)
|
||
ListTile(
|
||
leading: const Icon(Icons.view_sidebar),
|
||
title: Text('Navigation rail appearance'.i18n),
|
||
subtitle: Text(
|
||
'${'Currently'.i18n}: ${_navigationRailAppearanceToString(settings.navigationRailAppearance)}'),
|
||
onTap: () => showDialog(
|
||
context: context,
|
||
builder: (context) => SimpleDialog(
|
||
title: Text('Navigation rail appearance'.i18n),
|
||
children: NavigationRailAppearance.values
|
||
.map((value) => SimpleDialogOption(
|
||
child: Text(
|
||
_navigationRailAppearanceToString(value)),
|
||
onPressed: () {
|
||
settings.navigationRailAppearance = value;
|
||
Navigator.pop(context);
|
||
settings
|
||
.save()
|
||
.then((_) => updateTheme(context));
|
||
}))
|
||
.toList(growable: false),
|
||
)),
|
||
),
|
||
if (Platform.isLinux || Platform.isWindows || Platform.isMacOS)
|
||
SwitchListTile(
|
||
title: Text('Use colorful tray icon'.i18n),
|
||
secondary:
|
||
Image.asset(SysTray.getIcon(forcePng: true), height: 24.0),
|
||
value: settings.useColorTrayIcon,
|
||
onChanged: (value) {
|
||
setState(() {
|
||
settings.useColorTrayIcon = value;
|
||
unawaited(settings.save());
|
||
sysTray.updateIcon();
|
||
});
|
||
},
|
||
),
|
||
|
||
//Display mode (Android only!)
|
||
if (defaultTargetPlatform == TargetPlatform.android)
|
||
ListTile(
|
||
leading: const Icon(Icons.screen_lock_portrait),
|
||
title: Text('Change display mode'.i18n),
|
||
subtitle: Text('Enable high refresh rates'.i18n),
|
||
onTap: () async {
|
||
final modes = await FlutterDisplayMode.supported;
|
||
// ignore: use_build_context_synchronously
|
||
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 {
|
||
final navigator = Navigator.of(context);
|
||
settings.displayMode = i;
|
||
await settings.save();
|
||
await FlutterDisplayMode.setPreferredMode(
|
||
modes[i]);
|
||
navigator.pop();
|
||
},
|
||
)));
|
||
});
|
||
},
|
||
)
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _ColorPicker extends StatefulWidget {
|
||
const _ColorPicker({super.key});
|
||
|
||
@override
|
||
State<_ColorPicker> createState() => _ColorPickerState();
|
||
}
|
||
|
||
class _ColorPickerState extends State<_ColorPicker> {
|
||
Color color = settings.primaryColor;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return AlertDialog(
|
||
title: Text('Primary color'.i18n),
|
||
content: SizedBox(
|
||
height: 600.0,
|
||
width: min(MediaQuery.of(context).size.width * 0.9, 600.0),
|
||
child: SingleChildScrollView(
|
||
child: ColorPicker(
|
||
width: 56.0,
|
||
height: 56.0,
|
||
borderRadius: 50.0,
|
||
onColorChanged: (color) => setState(() => this.color = color),
|
||
color: color,
|
||
showColorCode: true,
|
||
pickersEnabled: const {
|
||
ColorPickerType.both: false,
|
||
ColorPickerType.primary: true,
|
||
ColorPickerType.accent: false,
|
||
ColorPickerType.bw: false,
|
||
ColorPickerType.custom: true,
|
||
ColorPickerType.wheel: true,
|
||
},
|
||
),
|
||
),
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.pop(context, color),
|
||
child: Text('OK'.i18n)),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class FontSelector extends StatefulWidget {
|
||
final Function callback;
|
||
|
||
const FontSelector(this.callback, {Key? key}) : super(key: key);
|
||
|
||
@override
|
||
State<FontSelector> 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(context);
|
||
},
|
||
child: Text('Apply'.i18n),
|
||
),
|
||
TextButton(
|
||
onPressed: () {
|
||
Navigator.of(context).pop();
|
||
widget.callback();
|
||
},
|
||
child: const Text('Cancel'),
|
||
)
|
||
],
|
||
));
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return AlertDialog(
|
||
scrollable: false,
|
||
title: Text("Select font".i18n),
|
||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
|
||
child: TextField(
|
||
decoration: InputDecoration(
|
||
hintText: 'Search'.i18n,
|
||
prefixIcon: const Icon(Icons.search),
|
||
border: const OutlineInputBorder()),
|
||
onChanged: (q) => setState(() => query = q),
|
||
),
|
||
),
|
||
SizedBox(
|
||
height: MediaQuery.of(context).size.height - 300.0,
|
||
width: 400.0,
|
||
child: Material(
|
||
type: MaterialType.transparency,
|
||
child: ListView.builder(
|
||
shrinkWrap: true,
|
||
itemExtent: 56.0,
|
||
itemCount: fonts.length,
|
||
itemBuilder: (context, index) => ListTile(
|
||
title: Text(fonts[index]),
|
||
onTap: () => onTap(fonts[index]))),
|
||
),
|
||
),
|
||
]),
|
||
);
|
||
}
|
||
}
|
||
|
||
class QualitySettings extends StatefulWidget {
|
||
const QualitySettings({super.key});
|
||
|
||
@override
|
||
State<QualitySettings> createState() => _QualitySettingsState();
|
||
}
|
||
|
||
class _QualitySettingsState extends State<QualitySettings> {
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(title: Text('Quality'.i18n)),
|
||
body: ListView(
|
||
children: <Widget>[
|
||
...(playerHelper.isConnectivityPluginAvailable
|
||
? [
|
||
ListTile(
|
||
title: Text('Mobile streaming'.i18n),
|
||
leading: const LeadingIcon(Icons.network_cell,
|
||
color: Color(0xff384697)),
|
||
),
|
||
const QualityPicker('mobile'),
|
||
const FreezerDivider(),
|
||
ListTile(
|
||
title: Text('Wifi streaming'.i18n),
|
||
leading: const LeadingIcon(Icons.network_wifi,
|
||
color: Color(0xff0880b5)),
|
||
),
|
||
const QualityPicker('wifi'),
|
||
]
|
||
: [
|
||
ListTile(
|
||
title: Text('Streaming'.i18n),
|
||
leading: const LeadingIcon(Icons.cloud,
|
||
color: Color(0xff384697))),
|
||
const QualityPicker('mobile_wifi'),
|
||
]),
|
||
const FreezerDivider(),
|
||
ListTile(
|
||
title: Text('Offline'.i18n),
|
||
leading:
|
||
const LeadingIcon(Icons.offline_pin, color: Color(0xff009a85)),
|
||
),
|
||
const QualityPicker('offline'),
|
||
const FreezerDivider(),
|
||
ListTile(
|
||
title: Text('External downloads'.i18n),
|
||
leading: const LeadingIcon(Icons.file_download,
|
||
color: Color(0xff2ba766)),
|
||
),
|
||
const QualityPicker('download'),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class QualityPicker extends StatefulWidget {
|
||
final String field;
|
||
const QualityPicker(this.field, {Key? key}) : super(key: key);
|
||
|
||
@override
|
||
State<QualityPicker> createState() => _QualityPickerState();
|
||
}
|
||
|
||
class _QualityPickerState extends State<QualityPicker> {
|
||
late AudioQuality _quality;
|
||
bool flacDisabled = !cache.canStreamLossless;
|
||
bool hqDisabled = !cache.canStreamHQ;
|
||
|
||
@override
|
||
void initState() {
|
||
_getQuality();
|
||
super.initState();
|
||
}
|
||
|
||
//Get current quality
|
||
void _getQuality() {
|
||
switch (widget.field) {
|
||
case 'mobile_wifi':
|
||
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 {
|
||
if (q == null) return;
|
||
|
||
setState(() {
|
||
_quality = q;
|
||
});
|
||
switch (widget.field) {
|
||
case 'mobile_wifi':
|
||
settings.mobileQuality = settings.wifiQuality = _quality;
|
||
break;
|
||
case 'mobile':
|
||
settings.mobileQuality = _quality;
|
||
break;
|
||
case 'wifi':
|
||
settings.wifiQuality = _quality;
|
||
break;
|
||
case 'download':
|
||
settings.downloadQuality = _quality;
|
||
break;
|
||
case 'offline':
|
||
settings.offlineQuality = _quality;
|
||
break;
|
||
}
|
||
await settings.save();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Column(
|
||
children: <Widget>[
|
||
RadioListTile(
|
||
title: const Text('MP3 128kbps'),
|
||
groupValue: _quality,
|
||
value: AudioQuality.MP3_128,
|
||
onChanged: (AudioQuality? q) => _updateQuality(q),
|
||
),
|
||
RadioListTile(
|
||
title: const Text('MP3 320kbps'),
|
||
groupValue: _quality,
|
||
value: AudioQuality.MP3_320,
|
||
onChanged: hqDisabled ? null : (AudioQuality? q) => _updateQuality(q),
|
||
),
|
||
RadioListTile(
|
||
title: const Text('FLAC'),
|
||
groupValue: _quality,
|
||
value: AudioQuality.FLAC,
|
||
onChanged:
|
||
flacDisabled ? null : (AudioQuality? q) => _updateQuality(q),
|
||
),
|
||
if (widget.field == 'download')
|
||
RadioListTile(
|
||
title: Text('Ask before downloading'.i18n),
|
||
groupValue: _quality,
|
||
value: AudioQuality.ASK,
|
||
onChanged: (AudioQuality? 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 {
|
||
const DeezerSettings({super.key});
|
||
|
||
@override
|
||
State<DeezerSettings> createState() => _DeezerSettingsState();
|
||
}
|
||
|
||
class _DeezerSettingsState extends State<DeezerSettings> {
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(title: Text('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: const 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();
|
||
deezerAPI.updateHeaders();
|
||
Navigator.of(context).pop();
|
||
},
|
||
)),
|
||
));
|
||
},
|
||
),
|
||
ListTile(
|
||
title: Text('Content country'.i18n),
|
||
subtitle: Text(
|
||
'${'Country used in headers. Now'.i18n}: ${settings.deezerCountry}'),
|
||
leading: const Icon(Icons.vpn_lock),
|
||
onTap: () {
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => CountryPickerDialog(
|
||
titlePadding: const EdgeInsets.all(8.0),
|
||
isSearchable: true,
|
||
onValuePicked: (Country country) {
|
||
setState(
|
||
() => settings.deezerCountry = country.isoCode);
|
||
deezerAPI.updateHeaders();
|
||
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: const 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;
|
||
const FilenameTemplateDialog(this.initial, this.onSave, {Key? key})
|
||
: super(key: key);
|
||
|
||
@override
|
||
State<FilenameTemplateDialog> 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: const 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 {
|
||
const DownloadsSettings({super.key});
|
||
|
||
@override
|
||
State<DownloadsSettings> createState() => _DownloadsSettingsState();
|
||
}
|
||
|
||
class _DownloadsSettingsState extends State<DownloadsSettings> {
|
||
double _downloadThreads = settings.downloadThreads.toDouble();
|
||
final TextEditingController _artistSeparatorController =
|
||
TextEditingController(text: settings.artistSeparator);
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(title: Text('Download Settings'.i18n)),
|
||
body: ListView(
|
||
children: [
|
||
ListTile(
|
||
title: Text('Download path'.i18n),
|
||
leading: const Icon(Icons.folder),
|
||
subtitle: Text(
|
||
settings.downloadPath ?? 'Not set, click here to set!'.i18n),
|
||
onTap: () async {
|
||
//Check permissions
|
||
if (!await DownloadManager.checkPermission()) {
|
||
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: const 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: const 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: const EdgeInsets.symmetric(horizontal: 16.0),
|
||
child: Text(
|
||
'${'Download threads'.i18n}: ${_downloadThreads.round().toString()}',
|
||
style: const 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();
|
||
}
|
||
}),
|
||
const FreezerDivider(),
|
||
ListTile(
|
||
title: Text('Tags'.i18n),
|
||
leading: const Icon(Icons.label),
|
||
onTap: () => Navigator.of(context)
|
||
.pushRoute(builder: (context) => const TagSelectionScreen()),
|
||
),
|
||
SwitchListTile(
|
||
title: Text('Create folders for artist'.i18n),
|
||
value: settings.artistFolder,
|
||
onChanged: (v) {
|
||
setState(() => settings.artistFolder = v);
|
||
settings.save();
|
||
},
|
||
secondary: const Icon(Icons.folder),
|
||
),
|
||
SwitchListTile(
|
||
title: Text('Create folders for albums'.i18n),
|
||
value: settings.albumFolder,
|
||
onChanged: (v) {
|
||
setState(() => settings.albumFolder = v);
|
||
settings.save();
|
||
},
|
||
secondary: const Icon(Icons.folder)),
|
||
SwitchListTile(
|
||
title: Text('Create folder for playlist'.i18n),
|
||
value: settings.playlistFolder,
|
||
onChanged: (v) {
|
||
setState(() => settings.playlistFolder = v);
|
||
settings.save();
|
||
},
|
||
secondary: const Icon(Icons.folder)),
|
||
const FreezerDivider(),
|
||
SwitchListTile(
|
||
title: Text('Separate albums by discs'.i18n),
|
||
value: settings.albumDiscFolder,
|
||
onChanged: (v) {
|
||
setState(() => settings.albumDiscFolder = v);
|
||
settings.save();
|
||
},
|
||
secondary: const Icon(Icons.album)),
|
||
SwitchListTile(
|
||
title: Text('Overwrite already downloaded files'.i18n),
|
||
value: settings.overwriteDownload,
|
||
onChanged: (v) {
|
||
setState(() => settings.overwriteDownload = v);
|
||
settings.save();
|
||
},
|
||
secondary: const Icon(Icons.delete)),
|
||
SwitchListTile(
|
||
title: Text('Download .LRC lyrics'.i18n),
|
||
value: settings.downloadLyrics,
|
||
onChanged: (v) {
|
||
setState(() => settings.downloadLyrics = v);
|
||
settings.save();
|
||
},
|
||
secondary: const Icon(Icons.subtitles)),
|
||
const FreezerDivider(),
|
||
SwitchListTile(
|
||
title: Text('Save cover file for every track'.i18n),
|
||
value: settings.trackCover,
|
||
onChanged: (v) {
|
||
setState(() => settings.trackCover = v);
|
||
settings.save();
|
||
},
|
||
secondary: const Icon(Icons.image)),
|
||
SwitchListTile(
|
||
title: Text('Save album cover'.i18n),
|
||
value: settings.albumCover,
|
||
onChanged: (v) {
|
||
setState(() => settings.albumCover = v);
|
||
settings.save();
|
||
},
|
||
secondary: const Icon(Icons.image)),
|
||
ListTile(
|
||
title: Text('Album cover resolution'.i18n),
|
||
subtitle: Text(
|
||
"WARNING: Resolutions above 1200 aren't officially supported"
|
||
.i18n),
|
||
leading: const Icon(Icons.image),
|
||
trailing: SizedBox(
|
||
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 {
|
||
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: const Icon(Icons.insert_drive_file)),
|
||
ListTile(
|
||
title: Text('Artist separator'.i18n),
|
||
leading: const Icon(Icons.safety_divider),
|
||
trailing: SizedBox(
|
||
width: 75.0,
|
||
child: TextField(
|
||
controller: _artistSeparatorController,
|
||
onChanged: (s) async {
|
||
settings.artistSeparator = s;
|
||
await settings.save();
|
||
},
|
||
),
|
||
),
|
||
),
|
||
const FreezerDivider(),
|
||
ListTile(
|
||
title: Text('Download Log'.i18n),
|
||
leading: const Icon(Icons.sticky_note_2),
|
||
onTap: () => Navigator.of(context)
|
||
.pushRoute(builder: (context) => const DownloadLogViewer()),
|
||
)
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class TagOption {
|
||
String title;
|
||
String value;
|
||
TagOption(this.title, this.value);
|
||
}
|
||
|
||
class TagSelectionScreen extends StatefulWidget {
|
||
const TagSelectionScreen({super.key});
|
||
|
||
@override
|
||
State<TagSelectionScreen> 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: AppBar(title: Text('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 {
|
||
const GeneralSettings({super.key});
|
||
|
||
@override
|
||
State<GeneralSettings> createState() => _GeneralSettingsState();
|
||
}
|
||
|
||
class _GeneralSettingsState extends State<GeneralSettings> {
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(title: Text('General'.i18n)),
|
||
body: ListView(
|
||
children: <Widget>[
|
||
SwitchListTile(
|
||
title: Text('Offline mode'.i18n),
|
||
subtitle: Text('Will be overridden on start.'.i18n),
|
||
value: settings.offlineMode,
|
||
secondary: const 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 CircularProgressIndicator());
|
||
});
|
||
},
|
||
),
|
||
// SwitchListTile(
|
||
// title: Text('Enable equalizer'.i18n),
|
||
// subtitle: Text(
|
||
// 'Might enable some equalizer apps to work. Requires restart of Freezer'
|
||
// .i18n),
|
||
// secondary: const 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: const Icon(Icons.not_interested),
|
||
value: settings.ignoreInterruptions,
|
||
onChanged: (bool v) async {
|
||
setState(() => settings.ignoreInterruptions = v);
|
||
await settings.save();
|
||
},
|
||
),
|
||
SwitchListTile(
|
||
title: Text('Use seek buttons as skip'.i18n),
|
||
subtitle: Text('May be useful for Android TV. '.i18n +
|
||
'Requires app restart to apply!'.i18n),
|
||
secondary: const Icon(Icons.fast_forward),
|
||
value: settings.seekAsSkip,
|
||
onChanged: (bool v) async {
|
||
setState(() => settings.seekAsSkip = 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: const Icon(FreezerIcons.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) => const LastFMLogin());
|
||
setState(() {});
|
||
},
|
||
),
|
||
ListTile(
|
||
title: Text('Login on other device'.i18n),
|
||
leading: const Icon(Icons.send_to_mobile),
|
||
onTap: () => showDialog(
|
||
context: context,
|
||
builder: (context) => const LoginOnOtherDevice()),
|
||
),
|
||
ListTile(
|
||
title: Text(
|
||
'Log out'.i18n,
|
||
style: const TextStyle(color: Colors.red),
|
||
),
|
||
leading: const 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 {
|
||
await audioHandler.stop();
|
||
await DownloadManager.platform
|
||
.invokeMethod("kill");
|
||
} catch (e) {}
|
||
await logOut();
|
||
|
||
SystemNavigator.pop();
|
||
},
|
||
)
|
||
],
|
||
);
|
||
});
|
||
}),
|
||
ListTile(
|
||
title: Text('Copy ARL'.i18n),
|
||
subtitle:
|
||
Text('Copy userToken/ARL Cookie for use in other apps.'.i18n),
|
||
leading: const Icon(Icons.lock),
|
||
onTap: () async {
|
||
if (settings.arl == null) return;
|
||
Clipboard.setData(ClipboardData(text: settings.arl!));
|
||
ScaffoldMessenger.of(context).snack('Copied'.i18n);
|
||
},
|
||
),
|
||
// ListTile(
|
||
// title: const Text('DEBUG: stop audioHandler'),
|
||
// onTap: () => audioHandler.stop()),
|
||
// ListTile(
|
||
// title: const Text('DEBUG: show login screen'),
|
||
// onTap: () => Navigator.of(context, rootNavigator: true)
|
||
// .pushRoute(builder: (ctx) => LoginWidget())),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class LastFMLogin extends StatefulWidget {
|
||
const LastFMLogin({super.key});
|
||
|
||
@override
|
||
State<LastFMLogin> 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(labelText: 'Username'.i18n),
|
||
onChanged: (v) => _username = v,
|
||
),
|
||
Container(height: 8.0),
|
||
TextField(
|
||
obscureText: true,
|
||
decoration: InputDecoration(labelText: '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
|
||
// State<DirectoryPicker> 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: AppBar(
|
||
// '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: List<Widget>.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<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 {
|
||
const CreditsScreen({super.key});
|
||
|
||
@override
|
||
State<CreditsScreen> 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: AppBar(title: Text('About'.i18n)),
|
||
body: ListView(
|
||
children: [
|
||
const FreezerTitle(),
|
||
Text(
|
||
_version,
|
||
textAlign: TextAlign.center,
|
||
style: const TextStyle(fontStyle: FontStyle.italic),
|
||
),
|
||
const FreezerDivider(),
|
||
const ListTile(
|
||
title: Text('Pato05'),
|
||
subtitle: Text('Current Developer - best of all'),
|
||
),
|
||
const ListTile(
|
||
title: Text('iDrinkCoffee'),
|
||
subtitle: Text('idk, he\'s romanian'),
|
||
),
|
||
const ListTile(
|
||
title: Text('exttex'),
|
||
subtitle: Text('Ex-Developer'),
|
||
),
|
||
const ListTile(
|
||
title: Text('Bas Curtiz'),
|
||
subtitle: Text('Icon, logo, banner, design suggestions, tester'),
|
||
),
|
||
const ListTile(
|
||
title: Text('Tobs'),
|
||
subtitle: Text('Alpha testers'),
|
||
),
|
||
const ListTile(
|
||
title: Text('Deemix'),
|
||
subtitle: Text('Better app <3'),
|
||
),
|
||
const ListTile(
|
||
title: Text('Xandar Null'),
|
||
subtitle: Text('Tester, translations help'),
|
||
),
|
||
ListTile(
|
||
title: const Text('Francesco'),
|
||
subtitle: const Text('Tester'),
|
||
onTap: () {
|
||
setState(() {
|
||
settings.primaryColor = const Color(0xff333333);
|
||
});
|
||
updateTheme(context);
|
||
settings.save();
|
||
},
|
||
),
|
||
const ListTile(
|
||
title: Text('Annexhack'),
|
||
subtitle: Text('Android Auto help'),
|
||
),
|
||
const FreezerDivider(),
|
||
...List.generate(
|
||
translators.length,
|
||
(i) => ListTile(
|
||
title: Text(translators[i][0]),
|
||
subtitle: Text(translators[i][1]),
|
||
)),
|
||
Padding(
|
||
padding: const EdgeInsets.fromLTRB(0, 4, 0, 8),
|
||
child: Text(
|
||
'Huge thanks to all the contributors! <3'.i18n,
|
||
textAlign: TextAlign.center,
|
||
style: const TextStyle(fontSize: 16.0),
|
||
),
|
||
)
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|