use get_url api by default, and fall back to old generation if get_url failed start to write a better cachemanager to implement in all systems write in more appropriate directories on windows and linux improve check for Connectivity by adding a fallback (needed for example on linux systems without NetworkManager) allow to dynamically change track quality without rebuilding the object
472 lines
13 KiB
Dart
472 lines
13 KiB
Dart
import 'dart:math';
|
|
|
|
import 'package:flutter/foundation.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;
|
|
|
|
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();
|
|
}
|
|
|
|
Future<void> updateAudioServiceQuality() async {
|
|
//Send wifi & mobile quality to audio service isolate
|
|
await audioHandler.customAction('updateQuality',
|
|
{'mobileQuality': mobileQuality, 'wifiQuality': wifiQuality});
|
|
}
|
|
|
|
// 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')
|
|
? 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);
|
|
|
|
late final Map<Themes, ThemeData> _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,
|
|
),
|
|
Themes.Dark: ThemeData(
|
|
textTheme: textTheme,
|
|
fontFamily: fontFamily,
|
|
brightness: Brightness.dark,
|
|
primaryColor: primaryColor,
|
|
colorScheme: ColorScheme.fromSeed(
|
|
seedColor: primaryColor,
|
|
brightness: Brightness.dark,
|
|
),
|
|
sliderTheme: _sliderTheme,
|
|
useMaterial3: true,
|
|
),
|
|
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,
|
|
),
|
|
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,
|
|
)
|
|
};
|
|
|
|
//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,
|
|
}
|