first commit
This commit is contained in:
@@ -0,0 +1,618 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_downloader/flutter_downloader.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:path/path.dart' as path_helper;
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../services/finamp_settings_helper.dart';
|
||||
import '../services/get_internal_song_dir.dart';
|
||||
import 'jellyfin_models.dart';
|
||||
|
||||
part 'finamp_models.g.dart';
|
||||
|
||||
@HiveType(typeId: 8)
|
||||
class FinampUser {
|
||||
FinampUser({
|
||||
required this.id,
|
||||
required this.baseUrl,
|
||||
required this.accessToken,
|
||||
required this.serverId,
|
||||
this.currentViewId,
|
||||
required this.views,
|
||||
});
|
||||
|
||||
@HiveField(0)
|
||||
String id;
|
||||
@HiveField(1)
|
||||
String baseUrl;
|
||||
@HiveField(2)
|
||||
String accessToken;
|
||||
@HiveField(3)
|
||||
String serverId;
|
||||
@HiveField(4)
|
||||
String? currentViewId;
|
||||
@HiveField(5)
|
||||
Map<String, BaseItemDto> views;
|
||||
|
||||
BaseItemDto? get currentView => views[currentViewId];
|
||||
}
|
||||
|
||||
// These consts are so that we can easily keep the same default for
|
||||
// FinampSettings's constructor and Hive's defaultValue.
|
||||
const _songShuffleItemCountDefault = 250;
|
||||
const _contentViewType = ContentViewType.list;
|
||||
const _contentGridViewCrossAxisCountPortrait = 2;
|
||||
const _contentGridViewCrossAxisCountLandscape = 3;
|
||||
const _showTextOnGridView = true;
|
||||
const _sleepTimerSeconds = 1800; // 30 Minutes
|
||||
const _showCoverAsPlayerBackground = true;
|
||||
const _hideSongArtistsIfSameAsAlbumArtists = true;
|
||||
const _disableGesture = false;
|
||||
const _showFastScroller = true;
|
||||
const _bufferDurationSeconds = 50;
|
||||
const _tabOrder = TabContentType.values;
|
||||
const _swipeInsertQueueNext = false;
|
||||
|
||||
@HiveType(typeId: 28)
|
||||
class FinampSettings {
|
||||
FinampSettings({
|
||||
this.isOffline = false,
|
||||
this.shouldTranscode = false,
|
||||
this.transcodeBitrate = 320000,
|
||||
// downloadLocations is required since the other values can be created with
|
||||
// default values. create() is used to return a FinampSettings with
|
||||
// downloadLocations.
|
||||
required this.downloadLocations,
|
||||
this.androidStopForegroundOnPause = true,
|
||||
required this.showTabs,
|
||||
this.isFavourite = false,
|
||||
this.sortBy = SortBy.sortName,
|
||||
this.sortOrder = SortOrder.ascending,
|
||||
this.songShuffleItemCount = _songShuffleItemCountDefault,
|
||||
this.contentViewType = _contentViewType,
|
||||
this.contentGridViewCrossAxisCountPortrait =
|
||||
_contentGridViewCrossAxisCountPortrait,
|
||||
this.contentGridViewCrossAxisCountLandscape =
|
||||
_contentGridViewCrossAxisCountLandscape,
|
||||
this.showTextOnGridView = _showTextOnGridView,
|
||||
this.sleepTimerSeconds = _sleepTimerSeconds,
|
||||
required this.downloadLocationsMap,
|
||||
this.showCoverAsPlayerBackground = _showCoverAsPlayerBackground,
|
||||
this.hideSongArtistsIfSameAsAlbumArtists =
|
||||
_hideSongArtistsIfSameAsAlbumArtists,
|
||||
this.bufferDurationSeconds = _bufferDurationSeconds,
|
||||
required this.tabSortBy,
|
||||
required this.tabSortOrder,
|
||||
this.tabOrder = _tabOrder,
|
||||
this.hasCompletedBlurhashImageMigration = true,
|
||||
this.hasCompletedBlurhashImageMigrationIdFix = true,
|
||||
this.swipeInsertQueueNext = _swipeInsertQueueNext,
|
||||
});
|
||||
|
||||
@HiveField(0)
|
||||
bool isOffline;
|
||||
@HiveField(1)
|
||||
bool shouldTranscode;
|
||||
@HiveField(2)
|
||||
int transcodeBitrate;
|
||||
|
||||
@Deprecated("Use downloadedLocationsMap instead")
|
||||
@HiveField(3)
|
||||
List<DownloadLocation> downloadLocations;
|
||||
|
||||
@HiveField(4)
|
||||
bool androidStopForegroundOnPause;
|
||||
@HiveField(5)
|
||||
Map<TabContentType, bool> showTabs;
|
||||
|
||||
/// Used to remember if the user has set their music screen to favourites
|
||||
/// mode.
|
||||
@HiveField(6)
|
||||
bool isFavourite;
|
||||
|
||||
/// Current sort by setting.
|
||||
@Deprecated("Use per-tab sort by instead")
|
||||
@HiveField(7)
|
||||
SortBy sortBy;
|
||||
|
||||
/// Current sort order setting.
|
||||
@Deprecated("Use per-tab sort order instead")
|
||||
@HiveField(8)
|
||||
SortOrder sortOrder;
|
||||
|
||||
/// Amount of songs to get when shuffling songs.
|
||||
@HiveField(9, defaultValue: _songShuffleItemCountDefault)
|
||||
int songShuffleItemCount;
|
||||
|
||||
/// The content view type used by the music screen.
|
||||
@HiveField(10, defaultValue: _contentViewType)
|
||||
ContentViewType contentViewType;
|
||||
|
||||
/// Amount of grid tiles to use per-row when portrait.
|
||||
@HiveField(11, defaultValue: _contentGridViewCrossAxisCountPortrait)
|
||||
int contentGridViewCrossAxisCountPortrait;
|
||||
|
||||
/// Amount of grid tiles to use per-row when landscape.
|
||||
@HiveField(12, defaultValue: _contentGridViewCrossAxisCountLandscape)
|
||||
int contentGridViewCrossAxisCountLandscape;
|
||||
|
||||
/// Whether or not to show the text (title, artist etc) on the grid music
|
||||
/// screen.
|
||||
@HiveField(13, defaultValue: _showTextOnGridView)
|
||||
bool showTextOnGridView = _showTextOnGridView;
|
||||
|
||||
/// The number of seconds to wait in a sleep timer. This is so that the app
|
||||
/// can remember the last duration. I'd use a Duration type here but Hive
|
||||
/// doesn't come with an adapter for it by default.
|
||||
@HiveField(14, defaultValue: _sleepTimerSeconds)
|
||||
int sleepTimerSeconds;
|
||||
|
||||
@HiveField(15, defaultValue: {})
|
||||
Map<String, DownloadLocation> downloadLocationsMap;
|
||||
|
||||
/// Whether or not to use blurred cover art as background on player screen.
|
||||
@HiveField(16, defaultValue: _showCoverAsPlayerBackground)
|
||||
bool showCoverAsPlayerBackground = _showCoverAsPlayerBackground;
|
||||
|
||||
@HiveField(17, defaultValue: _hideSongArtistsIfSameAsAlbumArtists)
|
||||
bool hideSongArtistsIfSameAsAlbumArtists =
|
||||
_hideSongArtistsIfSameAsAlbumArtists;
|
||||
|
||||
@HiveField(18, defaultValue: _bufferDurationSeconds)
|
||||
int bufferDurationSeconds;
|
||||
|
||||
@HiveField(19, defaultValue: _disableGesture)
|
||||
bool disableGesture = _disableGesture;
|
||||
|
||||
@HiveField(20, defaultValue: {})
|
||||
Map<TabContentType, SortBy> tabSortBy;
|
||||
|
||||
@HiveField(21, defaultValue: {})
|
||||
Map<TabContentType, SortOrder> tabSortOrder;
|
||||
|
||||
@HiveField(22, defaultValue: _tabOrder)
|
||||
List<TabContentType> tabOrder;
|
||||
|
||||
@HiveField(23, defaultValue: false)
|
||||
bool hasCompletedBlurhashImageMigration;
|
||||
|
||||
@HiveField(24, defaultValue: false)
|
||||
bool hasCompletedBlurhashImageMigrationIdFix;
|
||||
|
||||
@HiveField(25, defaultValue: _showFastScroller)
|
||||
bool showFastScroller = _showFastScroller;
|
||||
|
||||
@HiveField(26, defaultValue: _swipeInsertQueueNext)
|
||||
bool swipeInsertQueueNext;
|
||||
|
||||
static Future<FinampSettings> create() async {
|
||||
final internalSongDir = await getInternalSongDir();
|
||||
final downloadLocation = DownloadLocation.create(
|
||||
name: "Internal Storage",
|
||||
path: internalSongDir.path,
|
||||
useHumanReadableNames: false,
|
||||
deletable: false,
|
||||
);
|
||||
return FinampSettings(
|
||||
downloadLocations: [],
|
||||
// Create a map of TabContentType from TabContentType's values.
|
||||
showTabs: Map.fromEntries(
|
||||
TabContentType.values.map(
|
||||
(e) => MapEntry(e, true),
|
||||
),
|
||||
),
|
||||
downloadLocationsMap: {downloadLocation.id: downloadLocation},
|
||||
tabSortBy: {},
|
||||
tabSortOrder: {},
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the DownloadLocation that is the internal song dir. See the
|
||||
/// description of the "deletable" property to see how this works. This can
|
||||
/// technically throw a StateError, but that should never happen™.
|
||||
DownloadLocation get internalSongDir =>
|
||||
downloadLocationsMap.values.firstWhere((element) => !element.deletable);
|
||||
|
||||
Duration get bufferDuration => Duration(seconds: bufferDurationSeconds);
|
||||
|
||||
set bufferDuration(Duration duration) =>
|
||||
bufferDurationSeconds = duration.inSeconds;
|
||||
|
||||
SortBy getTabSortBy(TabContentType tabType) {
|
||||
return tabSortBy[tabType] ?? SortBy.sortName;
|
||||
}
|
||||
|
||||
SortOrder getSortOrder(TabContentType tabType) {
|
||||
return tabSortOrder[tabType] ?? SortOrder.ascending;
|
||||
}
|
||||
|
||||
bool get shouldRunBlurhashImageMigrationIdFix =>
|
||||
hasCompletedBlurhashImageMigration &&
|
||||
!hasCompletedBlurhashImageMigrationIdFix;
|
||||
}
|
||||
|
||||
/// Custom storage locations for storing music.
|
||||
@HiveType(typeId: 31)
|
||||
class DownloadLocation {
|
||||
DownloadLocation(
|
||||
{required this.name,
|
||||
required this.path,
|
||||
required this.useHumanReadableNames,
|
||||
required this.deletable,
|
||||
required this.id});
|
||||
|
||||
/// Human-readable name for the path (shown in settings)
|
||||
@HiveField(0)
|
||||
String name;
|
||||
|
||||
/// The path. We store this as a string since it's easier to put into Hive.
|
||||
@HiveField(1)
|
||||
String path;
|
||||
|
||||
/// If true, store songs using their actual names instead of Jellyfin item IDs.
|
||||
@HiveField(2)
|
||||
bool useHumanReadableNames;
|
||||
|
||||
/// If true, the user can delete this storage location. It's a bit of a hack,
|
||||
/// but the only undeletable location is the internal storage dir, so we can
|
||||
/// use this value to get the internal song dir.
|
||||
@HiveField(3)
|
||||
bool deletable;
|
||||
|
||||
/// Unique ID for the DownloadLocation. If this DownloadLocation was created
|
||||
/// before 0.6, it will be "0", very temporarily until it is changed on
|
||||
/// startup.
|
||||
@HiveField(4, defaultValue: "0")
|
||||
String id;
|
||||
|
||||
/// Initialises a new DownloadLocation. id will be a UUID.
|
||||
static DownloadLocation create({
|
||||
required String name,
|
||||
required String path,
|
||||
required bool useHumanReadableNames,
|
||||
required bool deletable,
|
||||
}) {
|
||||
return DownloadLocation(
|
||||
name: name,
|
||||
path: path,
|
||||
useHumanReadableNames: useHumanReadableNames,
|
||||
deletable: deletable,
|
||||
id: const Uuid().v4(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Class used in AddDownloadLocationScreen. Basically just a DownloadLocation
|
||||
/// with nullable values. Shouldn't be used for actually storing download
|
||||
/// locations.
|
||||
class NewDownloadLocation {
|
||||
NewDownloadLocation({
|
||||
this.name,
|
||||
this.path,
|
||||
this.useHumanReadableNames,
|
||||
required this.deletable,
|
||||
});
|
||||
|
||||
String? name;
|
||||
String? path;
|
||||
bool? useHumanReadableNames;
|
||||
bool deletable;
|
||||
}
|
||||
|
||||
/// Supported tab types in MusicScreenTabView.
|
||||
@HiveType(typeId: 36)
|
||||
enum TabContentType {
|
||||
@HiveField(0)
|
||||
albums,
|
||||
@HiveField(1)
|
||||
artists,
|
||||
@HiveField(2)
|
||||
playlists,
|
||||
@HiveField(3)
|
||||
genres,
|
||||
@HiveField(4)
|
||||
songs;
|
||||
|
||||
/// Human-readable version of the [TabContentType]. For example, toString() on
|
||||
/// [TabContentType.songs], toString() would return "TabContentType.songs".
|
||||
/// With this function, the same input would return "Songs".
|
||||
@override
|
||||
@Deprecated("Use toLocalisedString when possible")
|
||||
String toString() => _humanReadableName(this);
|
||||
|
||||
String toLocalisedString(BuildContext context) =>
|
||||
_humanReadableLocalisedName(this, context);
|
||||
|
||||
String _humanReadableName(TabContentType tabContentType) {
|
||||
switch (tabContentType) {
|
||||
case TabContentType.songs:
|
||||
return "Songs";
|
||||
case TabContentType.albums:
|
||||
return "Albums";
|
||||
case TabContentType.artists:
|
||||
return "Artists";
|
||||
case TabContentType.genres:
|
||||
return "Genres";
|
||||
case TabContentType.playlists:
|
||||
return "Playlists";
|
||||
}
|
||||
}
|
||||
|
||||
String _humanReadableLocalisedName(
|
||||
TabContentType tabContentType, BuildContext context) {
|
||||
switch (tabContentType) {
|
||||
case TabContentType.songs:
|
||||
return AppLocalizations.of(context)!.songs;
|
||||
case TabContentType.albums:
|
||||
return AppLocalizations.of(context)!.albums;
|
||||
case TabContentType.artists:
|
||||
return AppLocalizations.of(context)!.artists;
|
||||
case TabContentType.genres:
|
||||
return AppLocalizations.of(context)!.genres;
|
||||
case TabContentType.playlists:
|
||||
return AppLocalizations.of(context)!.playlists;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@HiveType(typeId: 39)
|
||||
enum ContentViewType {
|
||||
@HiveField(0)
|
||||
list,
|
||||
@HiveField(1)
|
||||
grid;
|
||||
|
||||
/// Human-readable version of this enum. I've written longer descriptions on
|
||||
/// enums like [TabContentType], and I can't be bothered to copy and paste it
|
||||
/// again.
|
||||
@override
|
||||
@Deprecated("Use toLocalisedString when possible")
|
||||
String toString() => _humanReadableName(this);
|
||||
|
||||
String toLocalisedString(BuildContext context) =>
|
||||
_humanReadableLocalisedName(this, context);
|
||||
|
||||
String _humanReadableName(ContentViewType contentViewType) {
|
||||
switch (contentViewType) {
|
||||
case ContentViewType.list:
|
||||
return "List";
|
||||
case ContentViewType.grid:
|
||||
return "Grid";
|
||||
}
|
||||
}
|
||||
|
||||
String _humanReadableLocalisedName(
|
||||
ContentViewType contentViewType, BuildContext context) {
|
||||
switch (contentViewType) {
|
||||
case ContentViewType.list:
|
||||
return AppLocalizations.of(context)!.list;
|
||||
case ContentViewType.grid:
|
||||
return AppLocalizations.of(context)!.grid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@HiveType(typeId: 3)
|
||||
@JsonSerializable(
|
||||
explicitToJson: true,
|
||||
anyMap: true,
|
||||
)
|
||||
class DownloadedSong {
|
||||
DownloadedSong({
|
||||
required this.song,
|
||||
required this.mediaSourceInfo,
|
||||
required this.downloadId,
|
||||
required this.requiredBy,
|
||||
required this.path,
|
||||
required this.useHumanReadableNames,
|
||||
required this.viewId,
|
||||
this.isPathRelative = true,
|
||||
required this.downloadLocationId,
|
||||
});
|
||||
|
||||
/// The Jellyfin item for the song
|
||||
@HiveField(0)
|
||||
BaseItemDto song;
|
||||
|
||||
/// The media source info for the song (used to get file format)
|
||||
@HiveField(1)
|
||||
MediaSourceInfo mediaSourceInfo;
|
||||
|
||||
/// The download ID of the song (for FlutterDownloader)
|
||||
@HiveField(2)
|
||||
String downloadId;
|
||||
|
||||
/// The list of parent item IDs the item is downloaded for. If this is 0, the
|
||||
/// song should be deleted.
|
||||
@HiveField(3)
|
||||
List<String> requiredBy;
|
||||
|
||||
/// The path of the song file. if [isPathRelative] is true, this will be a
|
||||
/// relative path from the song's DownloadLocation.
|
||||
@HiveField(4)
|
||||
String path;
|
||||
|
||||
/// Whether or not the file is stored with a human readable name. We need this
|
||||
/// when deleting downloads, as we need to check for empty folders when
|
||||
/// deleting files with human readable names.
|
||||
@HiveField(5)
|
||||
bool useHumanReadableNames;
|
||||
|
||||
/// The view that this download is in. Used for sorting in offline mode.
|
||||
@HiveField(6)
|
||||
String viewId;
|
||||
|
||||
/// Whether or not [path] is relative.
|
||||
@HiveField(7, defaultValue: false)
|
||||
bool isPathRelative;
|
||||
|
||||
/// The ID of the DownloadLocation that holds this file. Will be null if made
|
||||
/// before 0.6.
|
||||
@HiveField(8)
|
||||
String? downloadLocationId;
|
||||
|
||||
File get file {
|
||||
if (isPathRelative) {
|
||||
final downloadLocation = FinampSettingsHelper
|
||||
.finampSettings.downloadLocationsMap[downloadLocationId];
|
||||
|
||||
if (downloadLocation == null) {
|
||||
throw "DownloadLocation was null in file getter for DownloadsSong!";
|
||||
}
|
||||
|
||||
return File(path_helper.join(downloadLocation.path, path));
|
||||
}
|
||||
|
||||
return File(path);
|
||||
}
|
||||
|
||||
DownloadLocation? get downloadLocation => FinampSettingsHelper
|
||||
.finampSettings.downloadLocationsMap[downloadLocationId];
|
||||
|
||||
Future<DownloadTask?> get downloadTask async {
|
||||
final tasks = await FlutterDownloader.loadTasksWithRawQuery(
|
||||
query: "SELECT * FROM task WHERE task_id = '$downloadId'");
|
||||
|
||||
if (tasks?.isEmpty == false) {
|
||||
return tasks!.first;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
factory DownloadedSong.fromJson(Map<String, dynamic> json) =>
|
||||
_$DownloadedSongFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$DownloadedSongToJson(this);
|
||||
}
|
||||
|
||||
@HiveType(typeId: 4)
|
||||
class DownloadedParent {
|
||||
DownloadedParent({
|
||||
required this.item,
|
||||
required this.downloadedChildren,
|
||||
required this.viewId,
|
||||
});
|
||||
|
||||
@HiveField(0)
|
||||
BaseItemDto item;
|
||||
@HiveField(1)
|
||||
Map<String, BaseItemDto> downloadedChildren;
|
||||
|
||||
/// The view that this download is in. Used for sorting in offline mode.
|
||||
@HiveField(2)
|
||||
String viewId;
|
||||
}
|
||||
|
||||
@HiveType(typeId: 40)
|
||||
class DownloadedImage {
|
||||
DownloadedImage({
|
||||
required this.id,
|
||||
required this.downloadId,
|
||||
required this.path,
|
||||
required this.requiredBy,
|
||||
required this.downloadLocationId,
|
||||
});
|
||||
|
||||
/// The image ID
|
||||
@HiveField(0)
|
||||
String id;
|
||||
|
||||
/// The download ID of the song (for FlutterDownloader)
|
||||
@HiveField(1)
|
||||
String downloadId;
|
||||
|
||||
/// The relative path to the image file. To get the absolute path, use the
|
||||
/// file getter.
|
||||
@HiveField(2)
|
||||
String path;
|
||||
|
||||
/// The list of item IDs that use this image. If this is empty, the image
|
||||
/// should be deleted.
|
||||
/// TODO: Investigate adding set support to Hive
|
||||
@HiveField(3)
|
||||
List<String> requiredBy;
|
||||
|
||||
/// The ID of the DownloadLocation that holds this file.
|
||||
@HiveField(4)
|
||||
String downloadLocationId;
|
||||
|
||||
DownloadLocation? get downloadLocation => FinampSettingsHelper
|
||||
.finampSettings.downloadLocationsMap[downloadLocationId];
|
||||
|
||||
File get file {
|
||||
if (downloadLocation == null) {
|
||||
throw "Download location is null for image $id, this shouldn't happen...";
|
||||
}
|
||||
|
||||
return File(path_helper.join(downloadLocation!.path, path));
|
||||
}
|
||||
|
||||
Future<DownloadTask?> get downloadTask async {
|
||||
final tasks = await FlutterDownloader.loadTasksWithRawQuery(
|
||||
query: "SELECT * FROM task WHERE task_id = '$downloadId'");
|
||||
|
||||
if (tasks?.isEmpty == false) {
|
||||
return tasks!.first;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Creates a new DownloadedImage. Does not actually handle downloading or
|
||||
/// anything. This is only really a thing since having to manually specify
|
||||
/// empty lists is a bit jank.
|
||||
static DownloadedImage create({
|
||||
required String id,
|
||||
required String downloadId,
|
||||
required String path,
|
||||
List<String>? requiredBy,
|
||||
required String downloadLocationId,
|
||||
}) =>
|
||||
DownloadedImage(
|
||||
id: id,
|
||||
downloadId: downloadId,
|
||||
path: path,
|
||||
requiredBy: requiredBy ?? [],
|
||||
downloadLocationId: downloadLocationId,
|
||||
);
|
||||
}
|
||||
|
||||
@HiveType(typeId: 43)
|
||||
class OfflineListen {
|
||||
OfflineListen({
|
||||
required this.timestamp,
|
||||
required this.userId,
|
||||
required this.itemId,
|
||||
required this.name,
|
||||
this.artist,
|
||||
this.album,
|
||||
this.trackMbid,
|
||||
});
|
||||
|
||||
/// The stop timestamp of the listen, measured in seconds since the epoch.
|
||||
@HiveField(0)
|
||||
int timestamp;
|
||||
|
||||
@HiveField(1)
|
||||
String userId;
|
||||
|
||||
@HiveField(2)
|
||||
String itemId;
|
||||
|
||||
@HiveField(3)
|
||||
String name;
|
||||
|
||||
@HiveField(4)
|
||||
String? artist;
|
||||
|
||||
@HiveField(5)
|
||||
String? album;
|
||||
|
||||
// The MusicBrainz ID of the track, if available.
|
||||
@HiveField(6)
|
||||
String? trackMbid;
|
||||
}
|
||||
Reference in New Issue
Block a user