2023-10-16 22:22:50 +00:00
|
|
|
|
import 'dart:io';
|
|
|
|
|
|
2023-07-29 02:17:26 +00:00
|
|
|
|
import 'package:country_pickers/country.dart';
|
|
|
|
|
import 'package:country_pickers/country_picker_dialog.dart';
|
2023-10-16 22:22:50 +00:00
|
|
|
|
import 'package:flutter/foundation.dart';
|
2023-07-29 02:17:26 +00:00
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:flutter/services.dart';
|
|
|
|
|
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
|
|
|
|
import 'package:flutter_material_color_picker/flutter_material_color_picker.dart';
|
|
|
|
|
import 'package:fluttericon/font_awesome5_icons.dart';
|
|
|
|
|
import 'package:fluttericon/web_symbols_icons.dart';
|
|
|
|
|
import 'package:fluttertoast/fluttertoast.dart';
|
|
|
|
|
import 'package:freezer/api/definitions.dart';
|
|
|
|
|
import 'package:package_info_plus/package_info_plus.dart';
|
|
|
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
|
|
|
import 'package:scrobblenaut/scrobblenaut.dart';
|
|
|
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
|
|
|
|
|
|
import 'package:freezer/api/cache.dart';
|
|
|
|
|
import 'package:freezer/api/deezer.dart';
|
|
|
|
|
import 'package:freezer/api/download.dart';
|
|
|
|
|
import 'package:freezer/api/player.dart';
|
|
|
|
|
import 'package:freezer/ui/downloads_screen.dart';
|
|
|
|
|
import 'package:freezer/ui/elements.dart';
|
|
|
|
|
import 'package:freezer/ui/home_screen.dart';
|
|
|
|
|
import 'package:freezer/ui/updater.dart';
|
|
|
|
|
import 'package:freezer/translations.i18n.dart';
|
|
|
|
|
import 'package:freezer/settings.dart';
|
|
|
|
|
import 'package:freezer/main.dart';
|
|
|
|
|
|
|
|
|
|
class SettingsScreen extends StatefulWidget {
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const SettingsScreen({super.key});
|
2023-10-08 10:53:22 +00:00
|
|
|
|
|
2023-07-29 02:17:26 +00:00
|
|
|
|
@override
|
2023-10-12 22:09:37 +00:00
|
|
|
|
State<SettingsScreen> createState() => _SettingsScreenState();
|
2023-07-29 02:17:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading:
|
|
|
|
|
const LeadingIcon(Icons.settings, color: Color(0xffeca704)),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onTap: () => Navigator.of(context)
|
2023-10-12 22:09:37 +00:00
|
|
|
|
.pushRoute(builder: (context) => const GeneralSettings()),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
),
|
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('Download Settings'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading: const LeadingIcon(Icons.cloud_download,
|
|
|
|
|
color: Color(0xffbe3266)),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onTap: () => Navigator.of(context)
|
2023-10-12 22:09:37 +00:00
|
|
|
|
.pushRoute(builder: (context) => const DownloadsSettings()),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
),
|
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('Appearance'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading:
|
|
|
|
|
const LeadingIcon(Icons.color_lens, color: Color(0xff4b2e7e)),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onTap: () => Navigator.of(context)
|
2023-10-12 22:09:37 +00:00
|
|
|
|
.pushRoute(builder: (context) => const AppearanceSettings()),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
),
|
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('Quality'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading:
|
|
|
|
|
const LeadingIcon(Icons.high_quality, color: Color(0xff384697)),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onTap: () => Navigator.of(context)
|
2023-10-12 22:09:37 +00:00
|
|
|
|
.pushRoute(builder: (context) => const QualitySettings()),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
),
|
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('Deezer'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading:
|
|
|
|
|
const LeadingIcon(Icons.equalizer, color: Color(0xff0880b5)),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onTap: () => Navigator.of(context)
|
2023-10-12 22:09:37 +00:00
|
|
|
|
.pushRoute(builder: (context) => const DeezerSettings()),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
),
|
|
|
|
|
//Language select
|
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('Language'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading:
|
|
|
|
|
const LeadingIcon(Icons.language, color: Color(0xff009a85)),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onTap: () {
|
|
|
|
|
showDialog(
|
|
|
|
|
context: context,
|
|
|
|
|
builder: (context) => SimpleDialog(
|
|
|
|
|
title: Text('Select language'.i18n),
|
|
|
|
|
children: List.generate(languages.length, (int i) {
|
2023-10-12 22:09:37 +00:00
|
|
|
|
final Language l = languages[i];
|
2023-07-29 02:17:26 +00:00
|
|
|
|
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(
|
2023-10-12 22:09:37 +00:00
|
|
|
|
child: const Text('OK'),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onPressed: () {
|
2023-10-12 22:09:37 +00:00
|
|
|
|
Navigator.pop(context);
|
|
|
|
|
Navigator.pop(context);
|
2023-07-29 02:17:26 +00:00
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
})));
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('Updates'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading: const LeadingIcon(Icons.update, color: Color(0xff2ba766)),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onTap: () => Navigator.of(context)
|
2023-10-12 22:09:37 +00:00
|
|
|
|
.pushRoute(builder: (context) => const UpdaterScreen()),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
),
|
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('About'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading: const LeadingIcon(Icons.info, color: Colors.grey),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onTap: () => Navigator.of(context)
|
2023-10-12 22:09:37 +00:00
|
|
|
|
.pushRoute(builder: (context) => const CreditsScreen()),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class AppearanceSettings extends StatefulWidget {
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const AppearanceSettings({super.key});
|
|
|
|
|
|
2023-07-29 02:17:26 +00:00
|
|
|
|
@override
|
2023-10-12 22:09:37 +00:00
|
|
|
|
State<AppearanceSettings> createState() => _AppearanceSettingsState();
|
2023-07-29 02:17:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|
|
|
|
ColorSwatch<dynamic> _swatch(int c) => ColorSwatch(c, {500: Color(c)});
|
|
|
|
|
|
2023-10-16 22:22:50 +00:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-29 02:17:26 +00:00
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Scaffold(
|
|
|
|
|
appBar: AppBar(title: Text('Appearance'.i18n)),
|
|
|
|
|
body: ListView(
|
|
|
|
|
children: <Widget>[
|
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('Theme'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
subtitle: Text(
|
|
|
|
|
'${'Currently'.i18n}: ${settings.theme.toString().split('.').lastItem}'),
|
|
|
|
|
leading: const Icon(Icons.color_lens),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onTap: settings.materialYouAccent
|
|
|
|
|
? null
|
|
|
|
|
: () {
|
|
|
|
|
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();
|
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
SimpleDialogOption(
|
|
|
|
|
child: Text('Dark'.i18n),
|
|
|
|
|
onPressed: () {
|
|
|
|
|
settings.theme = Themes.Dark;
|
|
|
|
|
settings.save();
|
|
|
|
|
updateTheme();
|
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
SimpleDialogOption(
|
|
|
|
|
child: Text('Black (AMOLED)'.i18n),
|
|
|
|
|
onPressed: () {
|
|
|
|
|
settings.theme = Themes.Black;
|
|
|
|
|
settings.save();
|
|
|
|
|
updateTheme();
|
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
SimpleDialogOption(
|
|
|
|
|
child: Text('Deezer (Dark)'.i18n),
|
|
|
|
|
onPressed: () {
|
|
|
|
|
settings.theme = Themes.Deezer;
|
|
|
|
|
settings.save();
|
|
|
|
|
updateTheme();
|
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
SwitchListTile(
|
|
|
|
|
title: Text('Use system theme'.i18n),
|
|
|
|
|
value: settings.useSystemTheme,
|
|
|
|
|
onChanged: settings.materialYouAccent
|
|
|
|
|
? null
|
|
|
|
|
: (bool v) async {
|
|
|
|
|
settings.useSystemTheme = v;
|
|
|
|
|
|
|
|
|
|
settings.save();
|
|
|
|
|
updateTheme();
|
|
|
|
|
},
|
2023-10-12 22:09:37 +00:00
|
|
|
|
secondary: const Icon(Icons.android)),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
SwitchListTile(
|
|
|
|
|
value: settings.materialYouAccent,
|
|
|
|
|
title: Text('Use Material You accent'.i18n),
|
|
|
|
|
onChanged: (bool v) {
|
|
|
|
|
settings.materialYouAccent = v;
|
|
|
|
|
|
|
|
|
|
settings.save();
|
|
|
|
|
updateTheme();
|
|
|
|
|
}),
|
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('Font'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading: const Icon(Icons.font_download),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
subtitle: Text(settings.font),
|
|
|
|
|
onTap: () {
|
|
|
|
|
showDialog(
|
|
|
|
|
context: context,
|
|
|
|
|
builder: (context) =>
|
|
|
|
|
FontSelector(() => Navigator.of(context).pop()));
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
SwitchListTile(
|
|
|
|
|
title: Text('Player gradient background'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
secondary: const Icon(Icons.colorize),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
value: settings.colorGradientBackground,
|
|
|
|
|
onChanged: (bool v) async {
|
|
|
|
|
setState(() => settings.colorGradientBackground = v);
|
|
|
|
|
await settings.save();
|
|
|
|
|
},
|
|
|
|
|
),
|
2023-09-26 00:06:59 +00:00
|
|
|
|
SwitchListTile(
|
|
|
|
|
title: Text('Player album art drop shadow'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
secondary: const Icon(Icons.opacity),
|
2023-09-26 00:06:59 +00:00
|
|
|
|
value: settings.playerAlbumArtDropShadow,
|
|
|
|
|
onChanged: (bool v) async {
|
|
|
|
|
setState(() => settings.playerAlbumArtDropShadow = v);
|
|
|
|
|
await settings.save();
|
|
|
|
|
},
|
|
|
|
|
),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
SwitchListTile(
|
|
|
|
|
title: Text('Blur player background'.i18n),
|
|
|
|
|
subtitle: Text('Might have impact on performance'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
secondary: const Icon(Icons.blur_on),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
value: settings.blurPlayerBackground,
|
|
|
|
|
onChanged: (bool v) async {
|
|
|
|
|
setState(() => settings.blurPlayerBackground = v);
|
|
|
|
|
await settings.save();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
SwitchListTile(
|
2023-10-12 22:09:37 +00:00
|
|
|
|
title: const Text('Use player background on lyrics page'),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
value: settings.playerBackgroundOnLyrics,
|
2023-10-12 22:09:37 +00:00
|
|
|
|
secondary: const Icon(Icons.wallpaper),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onChanged: settings.blurPlayerBackground ||
|
|
|
|
|
settings.colorGradientBackground
|
|
|
|
|
? (bool v) {
|
|
|
|
|
setState(() => settings.playerBackgroundOnLyrics = v);
|
|
|
|
|
settings.save();
|
|
|
|
|
}
|
|
|
|
|
: null),
|
|
|
|
|
ListTile(
|
2023-10-12 22:09:37 +00:00
|
|
|
|
title: const Text('Screens style'),
|
|
|
|
|
subtitle: const Text(
|
|
|
|
|
'Style of the transition between screens within the app'),
|
|
|
|
|
leading: const Icon(Icons.auto_awesome_motion),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onTap: () => showDialog(
|
|
|
|
|
context: context,
|
|
|
|
|
builder: (context) {
|
|
|
|
|
return SimpleDialog(
|
2023-10-12 22:09:37 +00:00
|
|
|
|
title: const Text('Select screens style'),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
children: <Widget>[
|
|
|
|
|
SimpleDialogOption(
|
2023-10-12 22:09:37 +00:00
|
|
|
|
child: const Text('Blur slide (might be laggy!)'),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onPressed: () {
|
|
|
|
|
settings.navigatorRouteType =
|
|
|
|
|
NavigatorRouteType.blur_slide;
|
|
|
|
|
settings.save();
|
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
SimpleDialogOption(
|
2023-10-12 22:09:37 +00:00
|
|
|
|
child: const Text('Fade'),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onPressed: () {
|
|
|
|
|
settings.navigatorRouteType = NavigatorRouteType.fade;
|
|
|
|
|
settings.save();
|
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
SimpleDialogOption(
|
2023-10-12 22:09:37 +00:00
|
|
|
|
child: const Text('Fade with blur (might be laggy!)'),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onPressed: () {
|
|
|
|
|
settings.navigatorRouteType =
|
|
|
|
|
NavigatorRouteType.fade_blur;
|
|
|
|
|
settings.save();
|
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
SimpleDialogOption(
|
2023-10-12 22:09:37 +00:00
|
|
|
|
child: const Text('Material (default)'),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onPressed: () {
|
|
|
|
|
settings.navigatorRouteType =
|
|
|
|
|
NavigatorRouteType.material;
|
|
|
|
|
settings.save();
|
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
SimpleDialogOption(
|
2023-10-12 22:09:37 +00:00
|
|
|
|
child: const Text('Cupertino (iOS)'),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onPressed: () {
|
|
|
|
|
settings.navigatorRouteType =
|
|
|
|
|
NavigatorRouteType.cupertino;
|
|
|
|
|
settings.save();
|
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
SwitchListTile(
|
2023-10-12 22:09:37 +00:00
|
|
|
|
title: const Text('Enable filled play button'),
|
|
|
|
|
secondary: const Icon(Icons.play_circle),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
value: settings.enableFilledPlayButton,
|
|
|
|
|
onChanged: (bool v) {
|
|
|
|
|
setState(() => settings.enableFilledPlayButton = v);
|
|
|
|
|
settings.save();
|
|
|
|
|
}),
|
|
|
|
|
SwitchListTile(
|
|
|
|
|
title: Text('Visualizer'.i18n),
|
|
|
|
|
subtitle: Text(
|
|
|
|
|
'Show visualizers on lyrics page. WARNING: Requires microphone permission!'
|
|
|
|
|
.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
secondary: const Icon(Icons.equalizer),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
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),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading: const Icon(Icons.format_paint),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
trailing: Padding(
|
2023-10-12 22:09:37 +00:00
|
|
|
|
padding: const EdgeInsets.only(right: 8.0),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
child: CircleAvatar(
|
|
|
|
|
backgroundColor: settings.primaryColor,
|
|
|
|
|
)),
|
|
|
|
|
onTap: settings.materialYouAccent
|
|
|
|
|
? null
|
|
|
|
|
: () {
|
|
|
|
|
showDialog(
|
|
|
|
|
context: context,
|
|
|
|
|
builder: (context) {
|
|
|
|
|
return AlertDialog(
|
|
|
|
|
title: Text('Primary color'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
content: SizedBox(
|
2023-07-29 02:17:26 +00:00
|
|
|
|
height: 240,
|
|
|
|
|
child: MaterialColorPicker(
|
|
|
|
|
colors: [
|
|
|
|
|
...Colors.primaries,
|
|
|
|
|
//Logo colors
|
|
|
|
|
_swatch(0xffeca704),
|
|
|
|
|
_swatch(0xffbe3266),
|
|
|
|
|
_swatch(0xff4b2e7e),
|
|
|
|
|
_swatch(0xff384697),
|
|
|
|
|
_swatch(0xff0880b5),
|
|
|
|
|
_swatch(0xff009a85),
|
|
|
|
|
_swatch(0xff2ba766)
|
|
|
|
|
],
|
|
|
|
|
allowShades: false,
|
|
|
|
|
selectedColor: settings.primaryColor,
|
|
|
|
|
onMainColorChange: (ColorSwatch? color) {
|
|
|
|
|
if (color == null) return;
|
|
|
|
|
settings.primaryColor = color;
|
|
|
|
|
settings.save();
|
|
|
|
|
updateTheme();
|
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
SwitchListTile(
|
|
|
|
|
title: Text('Use album art primary color'.i18n),
|
|
|
|
|
subtitle: Text('Warning: might be buggy'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
secondary: const Icon(Icons.invert_colors),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
value: settings.useArtColor,
|
|
|
|
|
onChanged: (v) => setState(() => settings.updateUseArtColor(v)),
|
|
|
|
|
),
|
2023-10-16 22:22:50 +00:00
|
|
|
|
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(
|
2023-07-29 02:17:26 +00:00
|
|
|
|
context: context,
|
2023-10-16 22:22:50 +00:00
|
|
|
|
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());
|
|
|
|
|
}))
|
|
|
|
|
.toList(growable: false),
|
|
|
|
|
)),
|
|
|
|
|
),
|
|
|
|
|
//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();
|
|
|
|
|
},
|
|
|
|
|
)));
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
)
|
2023-07-29 02:17:26 +00:00
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class FontSelector extends StatefulWidget {
|
|
|
|
|
final Function callback;
|
|
|
|
|
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const FontSelector(this.callback, {Key? key}) : super(key: key);
|
2023-07-29 02:17:26 +00:00
|
|
|
|
|
|
|
|
|
@override
|
2023-10-12 22:09:37 +00:00
|
|
|
|
State<FontSelector> createState() => _FontSelectorState();
|
2023-07-29 02:17:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
},
|
2023-10-12 22:09:37 +00:00
|
|
|
|
child: const Text('Cancel'),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2023-09-26 00:06:59 +00:00
|
|
|
|
return AlertDialog(
|
|
|
|
|
scrollable: false,
|
2023-07-29 02:17:26 +00:00
|
|
|
|
title: Text("Select font".i18n),
|
2023-09-26 00:06:59 +00:00
|
|
|
|
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
2023-07-29 02:17:26 +00:00
|
|
|
|
Padding(
|
2023-10-12 22:09:37 +00:00
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
child: TextField(
|
2023-09-26 00:06:59 +00:00
|
|
|
|
decoration: InputDecoration(
|
|
|
|
|
hintText: 'Search'.i18n,
|
|
|
|
|
prefixIcon: const Icon(Icons.search),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
border: const OutlineInputBorder()),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onChanged: (q) => setState(() => query = q),
|
|
|
|
|
),
|
|
|
|
|
),
|
2023-09-26 00:06:59 +00:00
|
|
|
|
SingleChildScrollView(
|
|
|
|
|
child: SizedBox(
|
|
|
|
|
child: ListView.builder(
|
|
|
|
|
shrinkWrap: true,
|
|
|
|
|
itemExtent: 56.0,
|
|
|
|
|
itemCount: fonts.length,
|
|
|
|
|
itemBuilder: (context, index) => ListTile(
|
|
|
|
|
title: Text(fonts[index]),
|
|
|
|
|
onTap: () => onTap(fonts[index]))),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
]),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class QualitySettings extends StatefulWidget {
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const QualitySettings({super.key});
|
|
|
|
|
|
2023-07-29 02:17:26 +00:00
|
|
|
|
@override
|
2023-10-12 22:09:37 +00:00
|
|
|
|
State<QualitySettings> createState() => _QualitySettingsState();
|
2023-07-29 02:17:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _QualitySettingsState extends State<QualitySettings> {
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Scaffold(
|
|
|
|
|
appBar: AppBar(title: Text('Quality'.i18n)),
|
|
|
|
|
body: ListView(
|
|
|
|
|
children: <Widget>[
|
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('Mobile streaming'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading:
|
|
|
|
|
const LeadingIcon(Icons.network_cell, color: Color(0xff384697)),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const QualityPicker('mobile'),
|
|
|
|
|
const FreezerDivider(),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('Wifi streaming'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading:
|
|
|
|
|
const LeadingIcon(Icons.network_wifi, color: Color(0xff0880b5)),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const QualityPicker('wifi'),
|
|
|
|
|
const FreezerDivider(),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('Offline'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading:
|
|
|
|
|
const LeadingIcon(Icons.offline_pin, color: Color(0xff009a85)),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const QualityPicker('offline'),
|
|
|
|
|
const FreezerDivider(),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('External downloads'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading: const LeadingIcon(Icons.file_download,
|
|
|
|
|
color: Color(0xff2ba766)),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const QualityPicker('download'),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class QualityPicker extends StatefulWidget {
|
|
|
|
|
final String field;
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const QualityPicker(this.field, {Key? key}) : super(key: key);
|
2023-07-29 02:17:26 +00:00
|
|
|
|
|
|
|
|
|
@override
|
2023-10-12 22:09:37 +00:00
|
|
|
|
State<QualityPicker> createState() => _QualityPickerState();
|
2023-07-29 02:17:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _QualityPickerState extends State<QualityPicker> {
|
|
|
|
|
late AudioQuality _quality;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
_getQuality();
|
|
|
|
|
super.initState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//Get current quality
|
|
|
|
|
void _getQuality() {
|
|
|
|
|
switch (widget.field) {
|
|
|
|
|
case 'mobile':
|
|
|
|
|
_quality = settings.mobileQuality;
|
|
|
|
|
break;
|
|
|
|
|
case 'wifi':
|
|
|
|
|
_quality = settings.wifiQuality;
|
|
|
|
|
break;
|
|
|
|
|
case 'download':
|
|
|
|
|
_quality = settings.downloadQuality;
|
|
|
|
|
break;
|
|
|
|
|
case 'offline':
|
|
|
|
|
_quality = settings.offlineQuality;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//Update quality in settings
|
2023-10-08 10:53:22 +00:00
|
|
|
|
void _updateQuality(AudioQuality? q) async {
|
|
|
|
|
if (q == null) return;
|
|
|
|
|
|
2023-07-29 02:17:26 +00:00
|
|
|
|
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>[
|
|
|
|
|
RadioListTile(
|
2023-10-12 22:09:37 +00:00
|
|
|
|
title: const Text('MP3 128kbps'),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
groupValue: _quality,
|
|
|
|
|
value: AudioQuality.MP3_128,
|
2023-10-08 10:53:22 +00:00
|
|
|
|
onChanged: (q) => _updateQuality(q),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
),
|
|
|
|
|
RadioListTile(
|
2023-10-12 22:09:37 +00:00
|
|
|
|
title: const Text('MP3 320kbps'),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
groupValue: _quality,
|
|
|
|
|
value: AudioQuality.MP3_320,
|
2023-10-08 10:53:22 +00:00
|
|
|
|
onChanged: (q) => _updateQuality(q),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
),
|
|
|
|
|
RadioListTile(
|
2023-10-12 22:09:37 +00:00
|
|
|
|
title: const Text('FLAC'),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
groupValue: _quality,
|
|
|
|
|
value: AudioQuality.FLAC,
|
2023-10-08 10:53:22 +00:00
|
|
|
|
onChanged: (q) => _updateQuality(q),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
),
|
|
|
|
|
if (widget.field == 'download')
|
|
|
|
|
RadioListTile(
|
|
|
|
|
title: Text('Ask before downloading'.i18n),
|
|
|
|
|
groupValue: _quality,
|
|
|
|
|
value: AudioQuality.ASK,
|
2023-10-08 10:53:22 +00:00
|
|
|
|
onChanged: (q) => _updateQuality(q),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const DeezerSettings({super.key});
|
|
|
|
|
|
2023-07-29 02:17:26 +00:00
|
|
|
|
@override
|
2023-10-12 22:09:37 +00:00
|
|
|
|
State<DeezerSettings> createState() => _DeezerSettingsState();
|
2023-07-29 02:17:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
subtitle: Text(
|
|
|
|
|
'${'Not app language, used in headers. Now'.i18n}: ${settings.deezerLanguage}'),
|
|
|
|
|
leading: const Icon(Icons.language),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
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),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
subtitle: Text(
|
|
|
|
|
'${'Country used in headers. Now'.i18n}: ${settings.deezerCountry}'),
|
|
|
|
|
leading: const Icon(Icons.vpn_lock),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onTap: () {
|
|
|
|
|
showDialog(
|
|
|
|
|
context: context,
|
|
|
|
|
builder: (context) => CountryPickerDialog(
|
2023-10-12 22:09:37 +00:00
|
|
|
|
titlePadding: const EdgeInsets.all(8.0),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
isSearchable: true,
|
|
|
|
|
onValuePicked: (Country country) {
|
|
|
|
|
setState(
|
|
|
|
|
() => settings.deezerCountry = country.isoCode);
|
|
|
|
|
settings.save();
|
|
|
|
|
},
|
|
|
|
|
));
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
SwitchListTile(
|
|
|
|
|
title: Text('Log tracks'.i18n),
|
|
|
|
|
subtitle: Text(
|
|
|
|
|
'Send track listen logs to Deezer, enable it for features like Flow to work properly'
|
|
|
|
|
.i18n),
|
|
|
|
|
value: settings.logListen,
|
2023-10-12 22:09:37 +00:00
|
|
|
|
secondary: const Icon(Icons.history_toggle_off),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
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;
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const FilenameTemplateDialog(this.initial, this.onSave, {Key? key})
|
2023-07-29 02:17:26 +00:00
|
|
|
|
: super(key: key);
|
|
|
|
|
|
|
|
|
|
@override
|
2023-10-12 22:09:37 +00:00
|
|
|
|
State<FilenameTemplateDialog> createState() => _FilenameTemplateDialogState();
|
2023-07-29 02:17:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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(
|
2023-10-12 22:09:37 +00:00
|
|
|
|
'${'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(
|
2023-07-29 02:17:26 +00:00
|
|
|
|
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 {
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const DownloadsSettings({super.key});
|
|
|
|
|
|
2023-07-29 02:17:26 +00:00
|
|
|
|
@override
|
2023-10-12 22:09:37 +00:00
|
|
|
|
State<DownloadsSettings> createState() => _DownloadsSettingsState();
|
2023-07-29 02:17:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|
|
|
|
double _downloadThreads = settings.downloadThreads.toDouble();
|
2023-10-12 22:09:37 +00:00
|
|
|
|
final TextEditingController _artistSeparatorController =
|
2023-07-29 02:17:26 +00:00
|
|
|
|
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),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading: const Icon(Icons.folder),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
subtitle: Text(settings.downloadPath!),
|
|
|
|
|
onTap: () async {
|
|
|
|
|
//Check permissions
|
|
|
|
|
if (!await Permission.storage.request().isGranted) return;
|
|
|
|
|
DownloadManager.getDirectory('Pick-a-Path'.i18n).then((path) {
|
|
|
|
|
if (path == null) return; // user canceled
|
|
|
|
|
setState(() => settings.downloadPath = path);
|
|
|
|
|
settings.save();
|
|
|
|
|
});
|
|
|
|
|
//Navigate
|
|
|
|
|
// Navigator.of(context).pushRoute(
|
|
|
|
|
// builder: (context) => DirectoryPicker(
|
|
|
|
|
// settings.downloadPath,
|
|
|
|
|
// onSelect: (String p) async {
|
|
|
|
|
// setState(() => settings.downloadPath = p);
|
|
|
|
|
// await settings.save();
|
|
|
|
|
// },
|
|
|
|
|
// ));
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('Downloads naming'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
subtitle: Text('${'Currently'.i18n}: ${settings.downloadFilename}'),
|
|
|
|
|
leading: const Icon(Icons.text_format),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
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:
|
2023-10-12 22:09:37 +00:00
|
|
|
|
Text('${'Currently'.i18n}: ${settings.singletonFilename}'),
|
|
|
|
|
leading: const Icon(Icons.text_format),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onTap: () {
|
|
|
|
|
showDialog(
|
|
|
|
|
context: context,
|
|
|
|
|
builder: (context) {
|
|
|
|
|
return FilenameTemplateDialog(settings.singletonFilename,
|
|
|
|
|
(f) async {
|
|
|
|
|
setState(() => settings.singletonFilename = f);
|
|
|
|
|
await settings.save();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
Padding(
|
2023-10-12 22:09:37 +00:00
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
child: Text(
|
2023-10-12 22:09:37 +00:00
|
|
|
|
'${'Download threads'.i18n}: ${_downloadThreads.round().toString()}',
|
|
|
|
|
style: const TextStyle(fontSize: 16.0),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const FreezerDivider(),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('Tags'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading: const Icon(Icons.label),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onTap: () => Navigator.of(context)
|
2023-10-12 22:09:37 +00:00
|
|
|
|
.pushRoute(builder: (context) => const TagSelectionScreen()),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
),
|
|
|
|
|
SwitchListTile(
|
|
|
|
|
title: Text('Create folders for artist'.i18n),
|
|
|
|
|
value: settings.artistFolder,
|
|
|
|
|
onChanged: (v) {
|
|
|
|
|
setState(() => settings.artistFolder = v);
|
|
|
|
|
settings.save();
|
|
|
|
|
},
|
2023-10-12 22:09:37 +00:00
|
|
|
|
secondary: const Icon(Icons.folder),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
),
|
|
|
|
|
SwitchListTile(
|
|
|
|
|
title: Text('Create folders for albums'.i18n),
|
|
|
|
|
value: settings.albumFolder,
|
|
|
|
|
onChanged: (v) {
|
|
|
|
|
setState(() => settings.albumFolder = v);
|
|
|
|
|
settings.save();
|
|
|
|
|
},
|
2023-10-12 22:09:37 +00:00
|
|
|
|
secondary: const Icon(Icons.folder)),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
SwitchListTile(
|
|
|
|
|
title: Text('Create folder for playlist'.i18n),
|
|
|
|
|
value: settings.playlistFolder,
|
|
|
|
|
onChanged: (v) {
|
|
|
|
|
setState(() => settings.playlistFolder = v);
|
|
|
|
|
settings.save();
|
|
|
|
|
},
|
2023-10-12 22:09:37 +00:00
|
|
|
|
secondary: const Icon(Icons.folder)),
|
|
|
|
|
const FreezerDivider(),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
SwitchListTile(
|
|
|
|
|
title: Text('Separate albums by discs'.i18n),
|
|
|
|
|
value: settings.albumDiscFolder,
|
|
|
|
|
onChanged: (v) {
|
|
|
|
|
setState(() => settings.albumDiscFolder = v);
|
|
|
|
|
settings.save();
|
|
|
|
|
},
|
2023-10-12 22:09:37 +00:00
|
|
|
|
secondary: const Icon(Icons.album)),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
SwitchListTile(
|
|
|
|
|
title: Text('Overwrite already downloaded files'.i18n),
|
|
|
|
|
value: settings.overwriteDownload,
|
|
|
|
|
onChanged: (v) {
|
|
|
|
|
setState(() => settings.overwriteDownload = v);
|
|
|
|
|
settings.save();
|
|
|
|
|
},
|
2023-10-12 22:09:37 +00:00
|
|
|
|
secondary: const Icon(Icons.delete)),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
SwitchListTile(
|
|
|
|
|
title: Text('Download .LRC lyrics'.i18n),
|
|
|
|
|
value: settings.downloadLyrics,
|
|
|
|
|
onChanged: (v) {
|
|
|
|
|
setState(() => settings.downloadLyrics = v);
|
|
|
|
|
settings.save();
|
|
|
|
|
},
|
2023-10-12 22:09:37 +00:00
|
|
|
|
secondary: const Icon(Icons.subtitles)),
|
|
|
|
|
const FreezerDivider(),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
SwitchListTile(
|
|
|
|
|
title: Text('Save cover file for every track'.i18n),
|
|
|
|
|
value: settings.trackCover,
|
|
|
|
|
onChanged: (v) {
|
|
|
|
|
setState(() => settings.trackCover = v);
|
|
|
|
|
settings.save();
|
|
|
|
|
},
|
2023-10-12 22:09:37 +00:00
|
|
|
|
secondary: const Icon(Icons.image)),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
SwitchListTile(
|
|
|
|
|
title: Text('Save album cover'.i18n),
|
|
|
|
|
value: settings.albumCover,
|
|
|
|
|
onChanged: (v) {
|
|
|
|
|
setState(() => settings.albumCover = v);
|
|
|
|
|
settings.save();
|
|
|
|
|
},
|
2023-10-12 22:09:37 +00:00
|
|
|
|
secondary: const Icon(Icons.image)),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('Album cover resolution'.i18n),
|
|
|
|
|
subtitle: Text(
|
|
|
|
|
"WARNING: Resolutions above 1200 aren't officially supported"
|
|
|
|
|
.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading: const Icon(Icons.image),
|
|
|
|
|
trailing: SizedBox(
|
2023-07-29 02:17:26 +00:00
|
|
|
|
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();
|
|
|
|
|
},
|
2023-10-12 22:09:37 +00:00
|
|
|
|
secondary: const Icon(Icons.insert_drive_file)),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('Artist separator'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading: const Icon(WebSymbols.tag),
|
|
|
|
|
trailing: SizedBox(
|
2023-07-29 02:17:26 +00:00
|
|
|
|
width: 75.0,
|
|
|
|
|
child: TextField(
|
|
|
|
|
controller: _artistSeparatorController,
|
|
|
|
|
onChanged: (s) async {
|
|
|
|
|
settings.artistSeparator = s;
|
|
|
|
|
await settings.save();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const FreezerDivider(),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('Download Log'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading: const Icon(Icons.sticky_note_2),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onTap: () => Navigator.of(context)
|
2023-10-12 22:09:37 +00:00
|
|
|
|
.pushRoute(builder: (context) => const DownloadLogViewer()),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class TagOption {
|
|
|
|
|
String title;
|
|
|
|
|
String value;
|
|
|
|
|
TagOption(this.title, this.value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class TagSelectionScreen extends StatefulWidget {
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const TagSelectionScreen({super.key});
|
|
|
|
|
|
2023-07-29 02:17:26 +00:00
|
|
|
|
@override
|
2023-10-12 22:09:37 +00:00
|
|
|
|
State<TagSelectionScreen> createState() => _TagSelectionScreenState();
|
2023-07-29 02:17:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2023-10-12 22:09:37 +00:00
|
|
|
|
if (v) {
|
2023-07-29 02:17:26 +00:00
|
|
|
|
settings.tags.add(tags[i].value);
|
2023-10-12 22:09:37 +00:00
|
|
|
|
} else {
|
2023-07-29 02:17:26 +00:00
|
|
|
|
settings.tags.remove(tags[i].value);
|
2023-10-12 22:09:37 +00:00
|
|
|
|
}
|
2023-07-29 02:17:26 +00:00
|
|
|
|
setState(() {});
|
|
|
|
|
await settings.save();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
)),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class GeneralSettings extends StatefulWidget {
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const GeneralSettings({super.key});
|
|
|
|
|
|
2023-07-29 02:17:26 +00:00
|
|
|
|
@override
|
2023-10-12 22:09:37 +00:00
|
|
|
|
State<GeneralSettings> createState() => _GeneralSettingsState();
|
2023-07-29 02:17:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
subtitle: Text('Will be overridden on start.'.i18n),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
value: settings.offlineMode,
|
2023-10-12 22:09:37 +00:00
|
|
|
|
secondary: const Icon(Icons.lock),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onChanged: (bool v) {
|
|
|
|
|
if (v) {
|
|
|
|
|
setState(() => settings.offlineMode = true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
showDialog(
|
|
|
|
|
context: context,
|
|
|
|
|
builder: (context) {
|
|
|
|
|
deezerAPI.authorize().then((v) {
|
|
|
|
|
if (v) {
|
|
|
|
|
setState(() => settings.offlineMode = false);
|
|
|
|
|
} else {
|
|
|
|
|
Fluttertoast.showToast(
|
|
|
|
|
msg:
|
|
|
|
|
'Error logging in, check your internet connections.'
|
|
|
|
|
.i18n,
|
|
|
|
|
gravity: ToastGravity.BOTTOM,
|
|
|
|
|
toastLength: Toast.LENGTH_SHORT);
|
|
|
|
|
}
|
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
|
});
|
|
|
|
|
return AlertDialog(
|
|
|
|
|
title: Text('Logging in...'.i18n),
|
|
|
|
|
content:
|
|
|
|
|
const Center(child: CircularProgressIndicator()));
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
SwitchListTile(
|
|
|
|
|
title: Text('Enable equalizer'.i18n),
|
|
|
|
|
subtitle: Text(
|
|
|
|
|
'Might enable some equalizer apps to work. Requires restart of Freezer'
|
|
|
|
|
.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
secondary: const Icon(Icons.equalizer),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
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),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
secondary: const Icon(Icons.not_interested),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
value: settings.ignoreInterruptions,
|
|
|
|
|
onChanged: (bool v) async {
|
|
|
|
|
setState(() => settings.ignoreInterruptions = v);
|
|
|
|
|
await settings.save();
|
|
|
|
|
},
|
|
|
|
|
),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
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();
|
|
|
|
|
},
|
|
|
|
|
),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('LastFM'.i18n),
|
|
|
|
|
subtitle: Text((settings.lastFMPassword != null &&
|
|
|
|
|
settings.lastFMUsername != null)
|
|
|
|
|
? 'Log out'.i18n
|
|
|
|
|
: 'Login to enable scrobbling.'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading: const Icon(FontAwesome5.lastfm),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
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(
|
2023-10-12 22:09:37 +00:00
|
|
|
|
context: context, builder: (context) => const LastFMLogin());
|
2023-07-29 02:17:26 +00:00
|
|
|
|
setState(() {});
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
ListTile(
|
|
|
|
|
title: Text(
|
|
|
|
|
'Log out'.i18n,
|
2023-10-12 22:09:37 +00:00
|
|
|
|
style: const TextStyle(color: Colors.red),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading: const Icon(Icons.exit_to_app),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
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 {
|
|
|
|
|
audioHandler.stop();
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
await logOut();
|
|
|
|
|
await DownloadManager.platform
|
|
|
|
|
.invokeMethod("kill");
|
|
|
|
|
SystemNavigator.pop();
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}),
|
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('Copy ARL'.i18n),
|
|
|
|
|
subtitle:
|
|
|
|
|
Text('Copy userToken/ARL Cookie for use in other apps.'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading: const Icon(Icons.lock),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onTap: () async {
|
|
|
|
|
if (settings.arl == null) return;
|
|
|
|
|
Clipboard.setData(ClipboardData(text: settings.arl!));
|
|
|
|
|
ScaffoldMessenger.of(context).snack('Copied'.i18n);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class LastFMLogin extends StatefulWidget {
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const LastFMLogin({super.key});
|
|
|
|
|
|
2023-07-29 02:17:26 +00:00
|
|
|
|
@override
|
2023-10-12 22:09:37 +00:00
|
|
|
|
State<LastFMLogin> createState() => _LastFMLoginState();
|
2023-07-29 02:17:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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(
|
2023-09-26 00:06:59 +00:00
|
|
|
|
decoration: InputDecoration(labelText: 'Username'.i18n),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onChanged: (v) => _username = v,
|
|
|
|
|
),
|
|
|
|
|
Container(height: 8.0),
|
|
|
|
|
TextField(
|
|
|
|
|
obscureText: true,
|
2023-09-26 00:06:59 +00:00
|
|
|
|
decoration: InputDecoration(labelText: 'Password'.i18n),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
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
|
2023-10-12 22:09:37 +00:00
|
|
|
|
// State<DirectoryPicker> createState() => _DirectoryPickerState();
|
2023-07-29 02:17:26 +00:00
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// 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 {
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const CreditsScreen({super.key});
|
|
|
|
|
|
2023-07-29 02:17:26 +00:00
|
|
|
|
@override
|
2023-10-12 22:09:37 +00:00
|
|
|
|
State<CreditsScreen> createState() => _CreditsScreenState();
|
2023-07-29 02:17:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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: [
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const FreezerTitle(),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
Text(
|
|
|
|
|
_version,
|
|
|
|
|
textAlign: TextAlign.center,
|
2023-10-12 22:09:37 +00:00
|
|
|
|
style: const TextStyle(fontStyle: FontStyle.italic),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const FreezerDivider(),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('Telegram Channel'.i18n),
|
|
|
|
|
subtitle: Text('To get latest releases'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading: const Icon(FontAwesome5.telegram,
|
2023-07-29 02:17:26 +00:00
|
|
|
|
color: Color(0xFF27A2DF), size: 36.0),
|
|
|
|
|
onTap: () {
|
|
|
|
|
launchUrl(Uri.parse('https://t.me/joinchat/Se4zLEBvjS1NCiY9'));
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('Telegram Group'.i18n),
|
|
|
|
|
subtitle: Text('Official chat'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading: const Icon(FontAwesome5.telegram,
|
|
|
|
|
color: Colors.cyan, size: 36.0),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onTap: () => launchUrl(Uri.parse('https://t.me/freezerandroid')),
|
|
|
|
|
),
|
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('Discord'.i18n),
|
|
|
|
|
subtitle: Text('Official Discord server'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading: const Icon(FontAwesome5.discord,
|
2023-07-29 02:17:26 +00:00
|
|
|
|
color: Color(0xff7289da), size: 36.0),
|
|
|
|
|
onTap: () => launchUrl(Uri.parse('https://discord.gg/qwJpa3r4dQ')),
|
|
|
|
|
),
|
|
|
|
|
ListTile(
|
|
|
|
|
title: Text('Repository'.i18n),
|
|
|
|
|
subtitle: Text('Source code, report issues there.'.i18n),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading: const Icon(Icons.code, color: Colors.green, size: 36.0),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onTap: () {
|
|
|
|
|
launchUrl(Uri.parse('https://git.freezer.life/exttex/freezer'));
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
ListTile(
|
2023-10-12 22:09:37 +00:00
|
|
|
|
title: const Text('Donate'),
|
|
|
|
|
subtitle: const Text(
|
2023-07-29 02:17:26 +00:00
|
|
|
|
'You should rather support your favorite artists, instead of this app!'),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
leading:
|
|
|
|
|
const Icon(FontAwesome5.paypal, color: Colors.blue, size: 36.0),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onTap: () => launchUrl(Uri.parse('https://paypal.me/exttex')),
|
|
|
|
|
),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const FreezerDivider(),
|
|
|
|
|
const ListTile(
|
2023-07-29 02:17:26 +00:00
|
|
|
|
title: Text('exttex'),
|
|
|
|
|
subtitle: Text('Developer'),
|
|
|
|
|
),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const ListTile(
|
2023-07-29 02:17:26 +00:00
|
|
|
|
title: Text('Bas Curtiz'),
|
|
|
|
|
subtitle: Text('Icon, logo, banner, design suggestions, tester'),
|
|
|
|
|
),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const ListTile(
|
2023-07-29 02:17:26 +00:00
|
|
|
|
title: Text('Tobs'),
|
|
|
|
|
subtitle: Text('Alpha testers'),
|
|
|
|
|
),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const ListTile(
|
2023-07-29 02:17:26 +00:00
|
|
|
|
title: Text('Deemix'),
|
|
|
|
|
subtitle: Text('Better app <3'),
|
|
|
|
|
),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const ListTile(
|
2023-07-29 02:17:26 +00:00
|
|
|
|
title: Text('Xandar Null'),
|
|
|
|
|
subtitle: Text('Tester, translations help'),
|
|
|
|
|
),
|
|
|
|
|
ListTile(
|
2023-10-12 22:09:37 +00:00
|
|
|
|
title: const Text('Francesco'),
|
|
|
|
|
subtitle: const Text('Tester'),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
onTap: () {
|
|
|
|
|
setState(() {
|
2023-10-12 22:09:37 +00:00
|
|
|
|
settings.primaryColor = const Color(0xff333333);
|
2023-07-29 02:17:26 +00:00
|
|
|
|
});
|
|
|
|
|
updateTheme();
|
|
|
|
|
settings.save();
|
|
|
|
|
},
|
|
|
|
|
),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const ListTile(
|
2023-07-29 02:17:26 +00:00
|
|
|
|
title: Text('Annexhack'),
|
|
|
|
|
subtitle: Text('Android Auto help'),
|
|
|
|
|
),
|
2023-10-12 22:09:37 +00:00
|
|
|
|
const FreezerDivider(),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
...List.generate(
|
|
|
|
|
translators.length,
|
|
|
|
|
(i) => ListTile(
|
|
|
|
|
title: Text(translators[i][0]),
|
|
|
|
|
subtitle: Text(translators[i][1]),
|
|
|
|
|
)),
|
|
|
|
|
Padding(
|
2023-10-12 22:09:37 +00:00
|
|
|
|
padding: const EdgeInsets.fromLTRB(0, 4, 0, 8),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
child: Text(
|
|
|
|
|
'Huge thanks to all the contributors! <3'.i18n,
|
|
|
|
|
textAlign: TextAlign.center,
|
2023-10-12 22:09:37 +00:00
|
|
|
|
style: const TextStyle(fontSize: 16.0),
|
2023-07-29 02:17:26 +00:00
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|