freezer/lib/settings.dart

505 lines
15 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/download.dart';
import 'package:freezer/api/player/audio_handler.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter/material.dart';
import 'dart:convert';
import 'dart:async';
import 'dart:io' show Platform;
part 'settings.g.dart';
late Settings settings;
@HiveType(typeId: 24)
@JsonSerializable()
class Settings {
//Language
@HiveField(0)
@JsonKey(defaultValue: null)
String? language;
//Main
@HiveField(1)
bool ignoreInterruptions = false;
@HiveField(2)
bool enableEqualizer = false;
//Account
@HiveField(3)
String? arl;
@JsonKey(includeToJson: false)
bool offlineMode = false;
//Quality
@HiveField(4)
AudioQuality wifiQuality = AudioQuality.MP3_320;
@HiveField(5)
AudioQuality mobileQuality = AudioQuality.MP3_128;
@HiveField(6)
AudioQuality offlineQuality = AudioQuality.FLAC;
@HiveField(7)
AudioQuality downloadQuality = AudioQuality.FLAC;
//Download options
@HiveField(8)
String? downloadPath;
@HiveField(9)
String downloadFilename = "%artist% - %title%";
@HiveField(10)
bool albumFolder = true;
@HiveField(11)
bool artistFolder = true;
@HiveField(12)
bool albumDiscFolder = true;
@HiveField(13)
bool overwriteDownload = false;
@HiveField(14)
int downloadThreads = 2;
@HiveField(15)
bool playlistFolder = false;
@HiveField(16)
bool downloadLyrics = true;
@HiveField(17)
bool trackCover = false;
@HiveField(18)
bool albumCover = true;
@HiveField(19)
bool nomediaFiles = false;
@HiveField(20)
String artistSeparator = ', ';
@HiveField(21)
String singletonFilename = "%artist% - %title%";
@HiveField(22)
int albumArtResolution = 1400;
@HiveField(23)
List<String> tags = [
"title",
"album",
"artist",
"track",
"disc",
"albumArtist",
"date",
"label",
"isrc",
"upc",
"trackTotal",
"bpm",
"lyrics",
"genre",
"contributors",
"art"
];
//Appearance
@HiveField(24)
Themes theme = Themes.Dark;
@HiveField(25)
bool useSystemTheme = false;
@HiveField(26)
bool colorGradientBackground = false;
@HiveField(27)
bool blurPlayerBackground = false;
@HiveField(28)
String font = 'Deezer';
@HiveField(29)
bool lyricsVisualizer = false;
@HiveField(30)
int? displayMode;
@HiveField(31, defaultValue: true)
bool enableFilledPlayButton = true;
@HiveField(32, defaultValue: false)
bool playerBackgroundOnLyrics = false;
@HiveField(33, defaultValue: NavigatorRouteType.material)
NavigatorRouteType navigatorRouteType = NavigatorRouteType.material;
//Colors
@HiveField(34, defaultValue: Colors.blue)
@JsonKey(toJson: _colorToJson, fromJson: _colorFromJson)
Color primaryColor = Colors.blue;
static _colorToJson(Color c) => c.value;
static _colorFromJson(int? v) => Color(v ?? Colors.blue.value);
@HiveField(35)
bool useArtColor = false;
//Deezer
@HiveField(36)
// TODO: maybe convert to [Locale]?
String deezerLanguage = 'en';
@HiveField(37)
String deezerCountry = 'US';
@HiveField(38)
bool logListen = false;
@HiveField(39)
String? proxyAddress;
//LastFM
@HiveField(40)
String? lastFMUsername;
@HiveField(41)
String? lastFMPassword;
//Spotify
@HiveField(42)
String? spotifyClientId;
@HiveField(43)
String? spotifyClientSecret;
@HiveField(44)
SpotifyCredentialsSave? spotifyCredentials;
@HiveField(45, defaultValue: false)
bool materialYouAccent = false;
@HiveField(46, defaultValue: true)
bool playerAlbumArtDropShadow = true;
@HiveField(47, defaultValue: false)
bool seekAsSkip = false;
@HiveField(48, defaultValue: NavigationRailAppearance.expand_on_hover)
NavigationRailAppearance navigationRailAppearance =
NavigationRailAppearance.expand_on_hover;
@HiveField(49, defaultValue: true)
bool enableMaterial3PlayButton = true;
static LazyBox<Settings>? __box;
static Future<LazyBox<Settings>> get _box async =>
__box ??= await Hive.openLazyBox<Settings>('settings');
Settings();
void checkQuality(bool canStreamHQ, bool canStreamLossless) {
if (canStreamLossless) return;
final maxQuality =
canStreamHQ ? AudioQuality.MP3_320 : AudioQuality.MP3_128;
wifiQuality = _minQuality(wifiQuality, maxQuality);
mobileQuality = _minQuality(mobileQuality, maxQuality);
offlineQuality = _minQuality(offlineQuality, maxQuality);
downloadQuality = _minQuality(downloadQuality, maxQuality);
}
AudioQuality _minQuality(AudioQuality a, AudioQuality b) => a < b ? a : b;
ThemeData? get themeData {
//System theme
if (useSystemTheme) {
if (PlatformDispatcher.instance.platformBrightness == Brightness.light) {
return _themeData[Themes.Light];
}
if (theme == Themes.Light) return _themeData[Themes.Dark];
return _themeData[theme];
}
//Theme
return _themeData[theme] ?? ThemeData();
}
//Get all available fonts
List<String> get fonts {
return ['System', 'Deezer', ...GoogleFonts.asMap().keys];
}
//JSON to forward into download service
Map getServiceSettings() {
return {"json": jsonEncode(this)};
}
void updateUseArtColor(bool v) {
useArtColor = v;
//TODO: let's reimplement this somewhere better
//if (v) {
// //On media item change set color
// _useArtColorSub =
// AudioService.currentMediaItemStream.listen((event) async {
// if (event == null || event.artUri == null) return;
// this.primaryColor =
// await imagesDatabase.getPrimaryColor(event.artUri.toString());
// updateTheme();
// });
//} else {
// //Cancel stream subscription
// if (_useArtColorSub != null) {
// _useArtColorSub!.cancel();
// _useArtColorSub = null;
// }
//}
}
SliderThemeData get _sliderTheme => SliderThemeData(
thumbColor: primaryColor,
activeTrackColor: primaryColor,
inactiveTrackColor: primaryColor.withOpacity(0.2));
//Load settings/init
static Future<Settings> load() async {
final box = await _box;
if (box.containsKey(0)) {
return (await box.get(0))!;
}
final settings = Settings();
if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) {
settings.downloadPath =
await getDownloadsDirectory().then((path) => path!.path);
} else {
settings.downloadPath =
await getExternalStorageDirectories(type: StorageDirectory.music)
.then((paths) => paths![0].path);
}
return settings;
}
Future<void> save() async {
final box = await _box;
await box.clear();
await box.put(0, this);
downloadManager.updateServiceSettings();
}
// MaterialColor get _primarySwatch =>
// MaterialColor(primaryColor.value, <int, Color>{
// 50: primaryColor.withOpacity(.1),
// 100: primaryColor.withOpacity(.2),
// 200: primaryColor.withOpacity(.3),
// 300: primaryColor.withOpacity(.4),
// 400: primaryColor.withOpacity(.5),
// 500: primaryColor.withOpacity(.6),
// 600: primaryColor.withOpacity(.7),
// 700: primaryColor.withOpacity(.8),
// 800: primaryColor.withOpacity(.9),
// 900: primaryColor.withOpacity(1),
// });
//Check if is dark, can't use theme directly, because of system themes, and Theme.of(context).brightness broke
bool get isDark {
if (useSystemTheme) {
if (PlatformDispatcher.instance.platformBrightness == Brightness.light) {
return false;
}
return true;
}
if (theme == Themes.Light) return false;
return true;
}
static const deezerBg = Color(0xFF1F1A16);
static const deezerBottom = Color(0xFF1b1714);
TextTheme? get textTheme => (font == 'Deezer' || font == 'System')
? null
: GoogleFonts.getTextTheme(font,
isDark ? ThemeData.dark().textTheme : ThemeData.light().textTheme);
String? get fontFamily => (font == 'Deezer') ? 'MabryPro' : null;
final _elevation1Black = Color.alphaBlend(Colors.white12, Colors.black);
Map<Themes, ThemeData> get _themeData => {
Themes.Light: ThemeData(
textTheme: textTheme,
fontFamily: fontFamily,
brightness: Brightness.light,
primaryColor: primaryColor,
colorScheme: ColorScheme.fromSeed(seedColor: primaryColor),
sliderTheme: _sliderTheme,
bottomAppBarTheme: const BottomAppBarTheme(color: Color(0xfff5f5f5)),
useMaterial3: true,
appBarTheme: const AppBarTheme(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.dark,
systemNavigationBarIconBrightness: Brightness.dark,
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
)),
),
Themes.Dark: ThemeData(
textTheme: textTheme,
fontFamily: fontFamily,
brightness: Brightness.dark,
primaryColor: primaryColor,
colorScheme: ColorScheme.fromSeed(
seedColor: primaryColor,
brightness: Brightness.dark,
),
sliderTheme: _sliderTheme,
useMaterial3: true,
appBarTheme: const AppBarTheme(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarBrightness: Brightness.light,
statusBarIconBrightness: Brightness.light,
systemNavigationBarIconBrightness: Brightness.light,
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
)),
),
Themes.Deezer: ThemeData(
textTheme: textTheme,
fontFamily: fontFamily,
brightness: Brightness.dark,
primaryColor: primaryColor,
colorScheme: ColorScheme.fromSeed(
primary: primaryColor,
seedColor: deezerBg,
brightness: Brightness.dark),
sliderTheme: _sliderTheme,
scaffoldBackgroundColor: deezerBg,
bottomAppBarTheme: const BottomAppBarTheme(color: deezerBottom),
dialogBackgroundColor: deezerBottom,
bottomSheetTheme:
const BottomSheetThemeData(backgroundColor: deezerBottom),
cardColor: deezerBg,
useMaterial3: true,
appBarTheme: const AppBarTheme(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarBrightness: Brightness.light,
statusBarIconBrightness: Brightness.light,
systemNavigationBarIconBrightness: Brightness.light,
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
)),
),
Themes.Black: ThemeData(
textTheme: textTheme,
fontFamily: fontFamily,
brightness: Brightness.dark,
primaryColor: primaryColor,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.black,
primary: primaryColor,
background: Colors.black,
brightness: Brightness.dark),
scaffoldBackgroundColor: Colors.black,
navigationBarTheme:
const NavigationBarThemeData(backgroundColor: Colors.black),
bottomAppBarTheme: const BottomAppBarTheme(color: Colors.black),
dialogBackgroundColor: _elevation1Black,
sliderTheme: _sliderTheme,
bottomSheetTheme:
BottomSheetThemeData(backgroundColor: _elevation1Black),
cardColor: _elevation1Black,
useMaterial3: true,
appBarTheme: const AppBarTheme(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarBrightness: Brightness.light,
statusBarIconBrightness: Brightness.light,
systemNavigationBarIconBrightness: Brightness.light,
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
)),
)
};
//JSON
factory Settings.fromJson(Map<String, dynamic> json) =>
_$SettingsFromJson(json);
Map<String, dynamic> toJson() => _$SettingsToJson(this);
}
@HiveType(typeId: 29)
enum AudioQuality {
@HiveField(0)
MP3_128,
@HiveField(1)
MP3_320,
@HiveField(2)
FLAC,
@HiveField(3)
ASK
}
extension Deezer on AudioQuality {
static AudioQuality fromDeezerQualityInt(int quality) {
return const {
1: AudioQuality.MP3_128,
3: AudioQuality.MP3_320,
9: AudioQuality.FLAC,
}[quality]!;
}
bool operator <(AudioQuality other) =>
toDeezerQualityInt() < other.toDeezerQualityInt();
bool operator >(AudioQuality other) =>
toDeezerQualityInt() > other.toDeezerQualityInt();
bool operator <=(AudioQuality other) =>
toDeezerQualityInt() <= other.toDeezerQualityInt();
bool operator >=(AudioQuality other) =>
toDeezerQualityInt() >= other.toDeezerQualityInt();
int toDeezerQualityInt() {
return const {
AudioQuality.MP3_128: 1,
AudioQuality.MP3_320: 3,
AudioQuality.FLAC: 9,
}[this]!;
}
String toDeezerQualityString() {
return const {
AudioQuality.MP3_128: 'MP3_128',
AudioQuality.MP3_320: 'MP3_320',
AudioQuality.FLAC: 'FLAC',
}[this]!;
}
}
@HiveType(typeId: 28)
enum Themes {
@HiveField(0)
Light,
@HiveField(1)
Dark,
@HiveField(2)
Deezer,
@HiveField(3)
Black
}
@HiveType(typeId: 25)
@JsonSerializable()
class SpotifyCredentialsSave {
@HiveField(0)
String? accessToken;
@HiveField(1)
String? refreshToken;
@HiveField(2)
List<String>? scopes;
@HiveField(3)
DateTime? expiration;
SpotifyCredentialsSave(
{this.accessToken, this.refreshToken, this.scopes, this.expiration});
//JSON
factory SpotifyCredentialsSave.fromJson(Map<String, dynamic> json) =>
_$SpotifyCredentialsSaveFromJson(json);
Map<String, dynamic> toJson() => _$SpotifyCredentialsSaveToJson(this);
}
@HiveType(typeId: 34)
enum NavigationRailAppearance {
@HiveField(0)
expand_on_hover,
@HiveField(1)
always_expanded,
@HiveField(2)
icons_only,
}