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/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:flutter/material.dart'; import 'dart:convert'; import 'dart:async'; import 'dart:io' show Directory, 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 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; // DESKTOP ONLY -- TRAY ICON @HiveField(50, defaultValue: false) bool useColorTrayIcon = false; static LazyBox? __box; static Future> get _box async => __box ??= await Hive.openLazyBox('settings'); Settings(); AudioQuality maxQualityFor( AudioQuality quality, bool canStreamHQ, bool canStreamLossless) { if (canStreamLossless) return quality; final maxQuality = canStreamHQ ? AudioQuality.MP3_320 : AudioQuality.MP3_128; return _minQuality(quality, maxQuality); } 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(); } final customFonts = ['System', 'YouTube Sans', 'Deezer']; //Get all available fonts List get fonts { return [...customFonts, ...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 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) async => (await Directory(join(path!.path, 'Freezer')).create(recursive: true)) .path); } else { // settings.downloadPath = // await getExternalStorageDirectories(type: StorageDirectory.music) // .then((paths) => paths![0].path); } return settings; } Future save() async { final box = await _box; await box.clear(); await box.put(0, this); downloadManager.updateServiceSettings(); } // MaterialColor get _primarySwatch => // MaterialColor(primaryColor.value, { // 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 => customFonts.contains(font) ? null : GoogleFonts.getTextTheme(font, isDark ? ThemeData.dark().textTheme : ThemeData.light().textTheme); String? get fontFamily => (font == 'Deezer') ? 'Mabry Pro' : null; final _elevation1Black = Color.alphaBlend(Colors.white12, Colors.black); Map 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 json) => _$SettingsFromJson(json); Map 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? scopes; @HiveField(3) DateTime? expiration; SpotifyCredentialsSave( {this.accessToken, this.refreshToken, this.scopes, this.expiration}); //JSON factory SpotifyCredentialsSave.fromJson(Map json) => _$SpotifyCredentialsSaveFromJson(json); Map toJson() => _$SpotifyCredentialsSaveToJson(this); } @HiveType(typeId: 34) enum NavigationRailAppearance { @HiveField(0) expand_on_hover, @HiveField(1) always_expanded, @HiveField(2) icons_only, }