redesign app phase 3
This commit is contained in:
@@ -29,19 +29,30 @@ if (keystorePropertiesFile.exists()) {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 34
|
||||
namespace "com.unicornsonlsd.finamp"
|
||||
compileSdkVersion 36
|
||||
ndkVersion "28.2.13676358"
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable 'InvalidPackage'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.unicornsonlsd.finamp"
|
||||
minSdkVersion 21
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
targetSdkVersion 34
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
|
||||
@@ -14,8 +14,8 @@ subprojects {
|
||||
if (project.plugins.hasPlugin("com.android.application") ||
|
||||
project.plugins.hasPlugin("com.android.library")) {
|
||||
project.android {
|
||||
compileSdkVersion 34
|
||||
buildToolsVersion "34.0.0"
|
||||
compileSdkVersion 36
|
||||
buildToolsVersion "36.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
org.gradle.jvmargs=-Xmx4096M -XX:MaxMetaspaceSize=1024M
|
||||
android.enableR8=true
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||
android.builtInKotlin=false
|
||||
# This newDsl flag was added automatically by Flutter migrator
|
||||
android.newDsl=false
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
|
||||
|
||||
@@ -19,8 +19,8 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "7.4.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.9.10" apply false
|
||||
id "com.android.application" version "8.11.1" apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.0.20" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
||||
+21
-21
@@ -46,39 +46,39 @@ const lightColorScheme = ColorScheme(
|
||||
const darkColorScheme = ColorScheme(
|
||||
brightness: Brightness.dark,
|
||||
// Primary
|
||||
primary: jellyfinBlueColor,
|
||||
onPrimary: Color(0xFF001E2C),
|
||||
primaryContainer: Color(0xFF004C68),
|
||||
onPrimaryContainer: Color(0xFFC3E7FF),
|
||||
primary: Color(0xFF25D4FF),
|
||||
onPrimary: Color(0xFF02111F),
|
||||
primaryContainer: Color(0xFF0B4268),
|
||||
onPrimaryContainer: Color(0xFFD6F3FF),
|
||||
// Secondary
|
||||
secondary: Color(0xFF60B4DD),
|
||||
onSecondary: Color(0xFF112732),
|
||||
secondaryContainer: Color(0xFF206B8C),
|
||||
onSecondaryContainer: Color(0xFFCEEEFF),
|
||||
secondary: Color(0xFF7E8CFF),
|
||||
onSecondary: Color(0xFF080B2E),
|
||||
secondaryContainer: Color(0xFF262B72),
|
||||
onSecondaryContainer: Color(0xFFE0E3FF),
|
||||
// Tertiary
|
||||
tertiary: Color(0xFFC979E2),
|
||||
onTertiary: Color(0xFF3D0050),
|
||||
tertiaryContainer: Color(0xFF762A90),
|
||||
onTertiaryContainer: Color(0xFFFAD7FF),
|
||||
tertiary: Color(0xFFFF5FC8),
|
||||
onTertiary: Color(0xFF370027),
|
||||
tertiaryContainer: Color(0xFF6B0B55),
|
||||
onTertiaryContainer: Color(0xFFFFD7F0),
|
||||
// Error
|
||||
error: Color(0xFFFFB4AB),
|
||||
errorContainer: Color(0xFF93000A),
|
||||
onError: Color(0xFF690005),
|
||||
onErrorContainer: Color(0xFFFFDAD6),
|
||||
// Background & Surface
|
||||
background: Color(0xFF101315),
|
||||
onBackground: Color(0xFFE1E2E5),
|
||||
surface: Color(0xFF101315),
|
||||
onSurface: Color(0xFFE1E2E5),
|
||||
surfaceVariant: Color(0xFF333A3E),
|
||||
onSurfaceVariant: Color(0xFFC0C7CD),
|
||||
background: Color(0xFF030916),
|
||||
onBackground: Color(0xFFE8F2FF),
|
||||
surface: Color(0xFF061426),
|
||||
onSurface: Color(0xFFE8F2FF),
|
||||
surfaceVariant: Color(0xFF10233D),
|
||||
onSurfaceVariant: Color(0xFFA8B8D2),
|
||||
// Other colors
|
||||
outline: Color(0xFF80878C),
|
||||
outline: Color(0xFF61708D),
|
||||
onInverseSurface: Color(0xFF191C1E),
|
||||
inverseSurface: Color(0xFFE1E2E5),
|
||||
inversePrimary: Color(0xFF00668A),
|
||||
shadow: Color(0xFF000000),
|
||||
surfaceTint: Color(0xFF7BD0FF),
|
||||
outlineVariant: Color(0xFF41484D),
|
||||
surfaceTint: Color(0xFF25D4FF),
|
||||
outlineVariant: Color(0xFF233753),
|
||||
scrim: Color(0xFF000000),
|
||||
);
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter_downloader/flutter_downloader.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
import '../../models/finamp_models.dart';
|
||||
import '../../services/download_support.dart';
|
||||
import '../error_snackbar.dart';
|
||||
import '../album_image.dart';
|
||||
|
||||
@@ -25,6 +26,10 @@ class _CurrentDownloadsListState extends State<CurrentDownloadsList> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
if (!isDownloaderSupported) {
|
||||
return;
|
||||
}
|
||||
|
||||
IsolateNameServer.registerPortWithName(
|
||||
_port.sendPort, 'downloader_send_port');
|
||||
_port.listen((dynamic data) {
|
||||
@@ -36,7 +41,9 @@ class _CurrentDownloadsListState extends State<CurrentDownloadsList> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (isDownloaderSupported) {
|
||||
IsolateNameServer.removePortNameMapping('downloader_send_port');
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:finamp/l10n/app_localizations.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
import '../../services/download_update_stream.dart';
|
||||
import '../../services/download_support.dart';
|
||||
import '../error_snackbar.dart';
|
||||
|
||||
const double downloadsOverviewCardLoadingHeight = 120;
|
||||
@@ -39,7 +40,9 @@ class _DownloadsOverviewState extends State<DownloadsOverview> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_downloadsOverviewFuture = FlutterDownloader.loadTasks();
|
||||
_downloadsOverviewFuture = isDownloaderSupported
|
||||
? FlutterDownloader.loadTasks()
|
||||
: Future.value([]);
|
||||
|
||||
// Like in DownloadedIndicator, we use our own listener instead of a
|
||||
// StreamBuilder to ensure that we capture all events.
|
||||
|
||||
@@ -25,14 +25,34 @@ class AlbumItemCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
// In AlbumItem, the OpenContainer handles padding.
|
||||
margin: EdgeInsets.zero,
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.shadow.withOpacity(0.32),
|
||||
blurRadius: 22,
|
||||
offset: const Offset(0, 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: AlbumImage.borderRadius,
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
child: Stack(
|
||||
children: [
|
||||
AlbumImage(item: item),
|
||||
Positioned.fill(child: AlbumImage(item: item)),
|
||||
Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.10),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
),
|
||||
),
|
||||
),
|
||||
addSettingsListener
|
||||
? // We need this ValueListenableBuilder to react to changes to
|
||||
// showTextOnGridView. When shown in a MusicScreen, this widget
|
||||
@@ -88,19 +108,21 @@ class _AlbumItemCardText extends StatelessWidget {
|
||||
return Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
// We fade from half transparent black to transparent so that text is visible on bright images
|
||||
Colors.black.withOpacity(0.5),
|
||||
Colors.black.withOpacity(0.86),
|
||||
Colors.black.withOpacity(0.24),
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: const [0.0, 0.58, 1.0],
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
padding: const EdgeInsets.fromLTRB(14, 46, 14, 14),
|
||||
child: Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Column(
|
||||
@@ -110,19 +132,22 @@ class _AlbumItemCardText extends StatelessWidget {
|
||||
Text(
|
||||
item.name ?? "Unknown Name",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 3,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge!
|
||||
.copyWith(color: Colors.white),
|
||||
maxLines: 2,
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1.05,
|
||||
),
|
||||
),
|
||||
if (subtitle != null)
|
||||
Text(
|
||||
subtitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(color: Colors.white.withOpacity(0.7)),
|
||||
.copyWith(color: Colors.white.withOpacity(0.72)),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
@@ -25,24 +25,61 @@ class AlbumItemListTile extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final jellyfinApiHelper = GetIt.instance<JellyfinApiHelper>();
|
||||
final subtitle = generateSubtitle(item, parentType, context);
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return ListTile(
|
||||
// This widget is used on the add to playlist screen, so we allow a custom
|
||||
// onTap to be passed as an argument.
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
||||
child: Material(
|
||||
color: colorScheme.surfaceVariant.withOpacity(0.42),
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
border: Border.all(
|
||||
color: colorScheme.outlineVariant.withOpacity(0.24),
|
||||
),
|
||||
),
|
||||
child: ListTile(
|
||||
// This widget is used on the add to playlist screen, so we allow a
|
||||
// custom onTap to be passed as an argument.
|
||||
onTap: onTap,
|
||||
leading: AlbumImage(item: item),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
leading: SizedBox.square(
|
||||
dimension: 62,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: AlbumImage(item: item),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
item.name ?? AppLocalizations.of(context)!.unknownName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
subtitle: subtitle == null ? null : Text(
|
||||
),
|
||||
subtitle: subtitle == null
|
||||
? null
|
||||
: Text(
|
||||
subtitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
trailing: jellyfinApiHelper.selectedMixAlbumIds.contains(item.id)
|
||||
? const Icon(Icons.explore)
|
||||
: null,
|
||||
? Icon(Icons.explore_rounded, color: colorScheme.primary)
|
||||
: Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,13 @@ import 'package:flutter/material.dart';
|
||||
import 'package:finamp/l10n/app_localizations.dart';
|
||||
|
||||
/// Snackbar with error icon for displaying errors
|
||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> errorSnackbar(
|
||||
dynamic error, BuildContext context) {
|
||||
return ScaffoldMessenger.of(context).showSnackBar(
|
||||
void errorSnackbar(dynamic error, BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.anErrorHasOccured),
|
||||
action: SnackBarAction(
|
||||
@@ -18,7 +22,8 @@ ScaffoldFeatureController<SnackBar, SnackBarClosedReason> errorSnackbar(
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(MaterialLocalizations.of(context).closeButtonLabel),
|
||||
child:
|
||||
Text(MaterialLocalizations.of(context).closeButtonLabel),
|
||||
)
|
||||
],
|
||||
),
|
||||
@@ -26,6 +31,7 @@ ScaffoldFeatureController<SnackBar, SnackBarClosedReason> errorSnackbar(
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
String _errorText(dynamic error, BuildContext context) {
|
||||
|
||||
+15
-11
@@ -9,7 +9,6 @@ import 'package:finamp/screens/interaction_settings_screen.dart';
|
||||
import 'package:finamp/services/finamp_settings_helper.dart';
|
||||
import 'package:finamp/services/finamp_user_helper.dart';
|
||||
import 'package:finamp/services/offline_listen_helper.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_downloader/flutter_downloader.dart';
|
||||
@@ -46,6 +45,7 @@ import 'screens/user_selector.dart';
|
||||
import 'screens/view_selector.dart';
|
||||
import 'services/audio_service_helper.dart';
|
||||
import 'services/download_update_stream.dart';
|
||||
import 'services/download_support.dart';
|
||||
import 'services/downloads_helper.dart';
|
||||
import 'services/jellyfin_api_helper.dart';
|
||||
import 'services/locale_helper.dart';
|
||||
@@ -123,7 +123,7 @@ Future<void> _setupDownloader() async {
|
||||
GetIt.instance.registerSingleton(DownloadUpdateStream());
|
||||
GetIt.instance<DownloadUpdateStream>().setupSendPort();
|
||||
|
||||
if (!_isDownloaderSupported) {
|
||||
if (!isDownloaderSupported) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -137,15 +137,6 @@ Future<void> _setupDownloader() async {
|
||||
FlutterDownloader.registerCallback(_DummyCallback.callback);
|
||||
}
|
||||
|
||||
bool get _isDownloaderSupported {
|
||||
if (kIsWeb) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return defaultTargetPlatform == TargetPlatform.android ||
|
||||
defaultTargetPlatform == TargetPlatform.iOS;
|
||||
}
|
||||
|
||||
Future<void> setupHive() async {
|
||||
await Hive.initFlutter();
|
||||
Hive.registerAdapter(BaseItemDtoAdapter());
|
||||
@@ -369,6 +360,19 @@ class Finamp extends StatelessWidget {
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
colorScheme: darkColorScheme,
|
||||
scaffoldBackgroundColor: darkColorScheme.background,
|
||||
cardTheme: CardThemeData(
|
||||
color: darkColorScheme.surfaceVariant,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
),
|
||||
),
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
),
|
||||
),
|
||||
themeMode: box.get("ThemeMode"),
|
||||
localizationsDelegates: const [
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:path/path.dart' as path_helper;
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../services/finamp_settings_helper.dart';
|
||||
import '../services/download_support.dart';
|
||||
import '../services/get_internal_song_dir.dart';
|
||||
import 'jellyfin_models.dart';
|
||||
|
||||
@@ -474,6 +475,10 @@ class DownloadedSong {
|
||||
.finampSettings.downloadLocationsMap[downloadLocationId];
|
||||
|
||||
Future<DownloadTask?> get downloadTask async {
|
||||
if (!isDownloaderSupported) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final tasks = await FlutterDownloader.loadTasksWithRawQuery(
|
||||
query: "SELECT * FROM task WHERE task_id = '$downloadId'");
|
||||
|
||||
@@ -553,6 +558,10 @@ class DownloadedImage {
|
||||
}
|
||||
|
||||
Future<DownloadTask?> get downloadTask async {
|
||||
if (!isDownloaderSupported) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final tasks = await FlutterDownloader.loadTasksWithRawQuery(
|
||||
query: "SELECT * FROM task WHERE task_id = '$downloadId'");
|
||||
|
||||
|
||||
+140
-79
@@ -265,22 +265,10 @@ class _MusicScreenState extends State<MusicScreen>
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: getFloatingActionButton(),
|
||||
),
|
||||
body: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.primaryContainer
|
||||
.withOpacity(0.45),
|
||||
Theme.of(context).colorScheme.surface,
|
||||
Theme.of(context).colorScheme.surface,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
body: Stack(
|
||||
children: [
|
||||
const _MusicBackdrop(),
|
||||
SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -310,24 +298,29 @@ class _MusicScreenState extends State<MusicScreen>
|
||||
tabs: tabs.toList(),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.fromLTRB(12, 8, 12, 0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(28),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 14, 14, 0),
|
||||
child: Material(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surface
|
||||
.withOpacity(0.74),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(34),
|
||||
),
|
||||
side: BorderSide(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outlineVariant
|
||||
.withOpacity(0.42),
|
||||
),
|
||||
),
|
||||
elevation: 16,
|
||||
shadowColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.shadow
|
||||
.withOpacity(0.08),
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, -6),
|
||||
),
|
||||
],
|
||||
),
|
||||
.withOpacity(0.24),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
@@ -335,7 +328,8 @@ class _MusicScreenState extends State<MusicScreen>
|
||||
.map((tabType) => MusicScreenTabView(
|
||||
tabContentType: tabType,
|
||||
searchTerm: searchQuery,
|
||||
isFavourite: finampSettings.isFavourite,
|
||||
isFavourite:
|
||||
finampSettings.isFavourite,
|
||||
sortBy: finampSettings
|
||||
.getTabSortBy(tabType),
|
||||
sortOrder: finampSettings
|
||||
@@ -347,9 +341,11 @@ class _MusicScreenState extends State<MusicScreen>
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -360,6 +356,78 @@ class _MusicScreenState extends State<MusicScreen>
|
||||
}
|
||||
}
|
||||
|
||||
class _MusicBackdrop extends StatelessWidget {
|
||||
const _MusicBackdrop();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
const Color(0xFF081D38),
|
||||
colorScheme.background,
|
||||
const Color(0xFF020611),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: -110,
|
||||
right: -80,
|
||||
child: _GlowOrb(
|
||||
color: colorScheme.primary.withOpacity(0.28),
|
||||
size: 280,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 90,
|
||||
left: -120,
|
||||
child: _GlowOrb(
|
||||
color: colorScheme.tertiary.withOpacity(0.20),
|
||||
size: 260,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: -140,
|
||||
right: -130,
|
||||
child: _GlowOrb(
|
||||
color: colorScheme.secondary.withOpacity(0.16),
|
||||
size: 320,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GlowOrb extends StatelessWidget {
|
||||
const _GlowOrb({required this.color, required this.size});
|
||||
|
||||
final Color color;
|
||||
final double size;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [color, Colors.transparent],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MusicHeroHeader extends StatelessWidget {
|
||||
const _MusicHeroHeader({
|
||||
required this.title,
|
||||
@@ -381,50 +449,40 @@ class _MusicHeroHeader extends StatelessWidget {
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 86, 20, 12),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
colorScheme.primary.withOpacity(0.95),
|
||||
colorScheme.tertiary.withOpacity(0.78),
|
||||
],
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.primary.withOpacity(0.28),
|
||||
blurRadius: 34,
|
||||
offset: const Offset(0, 18),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
padding: const EdgeInsets.fromLTRB(24, 88, 24, 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isOffline ? 'Offline library' : 'Your Jellyfin music',
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
color: colorScheme.onPrimary.withOpacity(0.76),
|
||||
letterSpacing: 0.6,
|
||||
isOffline ? 'OFFLINE MODE' : 'FINAMP MUSIC',
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: colorScheme.primary,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 2.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.headlineMedium?.copyWith(
|
||||
color: colorScheme.onPrimary,
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1.04,
|
||||
style: theme.textTheme.displaySmall?.copyWith(
|
||||
color: colorScheme.onBackground,
|
||||
fontWeight: FontWeight.w900,
|
||||
height: 0.98,
|
||||
letterSpacing: -1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Browse your Jellyfin collection with artwork-first shelves.',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 22),
|
||||
Row(
|
||||
children: [
|
||||
FilledButton.icon(
|
||||
@@ -432,23 +490,24 @@ class _MusicHeroHeader extends StatelessWidget {
|
||||
icon: const Icon(Icons.shuffle_rounded),
|
||||
label: const Text('Shuffle'),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: colorScheme.onPrimary,
|
||||
foregroundColor: colorScheme.primary,
|
||||
backgroundColor: colorScheme.primary,
|
||||
foregroundColor: colorScheme.onPrimary,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 18,
|
||||
vertical: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_HeroChip(
|
||||
icon: isFavourite
|
||||
? Icons.favorite_rounded
|
||||
: Icons.library_music_rounded,
|
||||
icon:
|
||||
isFavourite ? Icons.favorite_rounded : Icons.album_rounded,
|
||||
label: isFavourite ? 'Favourites' : activeTab,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -466,19 +525,19 @@ class _HeroChip extends StatelessWidget {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.onPrimary.withOpacity(0.14),
|
||||
color: colorScheme.surfaceVariant.withOpacity(0.58),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
border: Border.all(color: colorScheme.onPrimary.withOpacity(0.16)),
|
||||
border: Border.all(color: colorScheme.outlineVariant.withOpacity(0.55)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, color: colorScheme.onPrimary, size: 18),
|
||||
Icon(icon, color: colorScheme.secondary, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onPrimary,
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
@@ -502,10 +561,10 @@ class _ModernMusicTabBar extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface.withOpacity(0.7),
|
||||
color: colorScheme.surfaceVariant.withOpacity(0.50),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
border:
|
||||
Border.all(color: colorScheme.outlineVariant.withOpacity(0.4)),
|
||||
Border.all(color: colorScheme.outlineVariant.withOpacity(0.55)),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: controller,
|
||||
@@ -518,7 +577,9 @@ class _ModernMusicTabBar extends StatelessWidget {
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
indicator: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
color: colorScheme.primary,
|
||||
gradient: LinearGradient(
|
||||
colors: [colorScheme.primary, colorScheme.secondary],
|
||||
),
|
||||
),
|
||||
labelColor: colorScheme.onPrimary,
|
||||
unselectedLabelColor: colorScheme.onSurfaceVariant,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
@@ -68,6 +69,7 @@ class PlayerScreen extends StatelessWidget {
|
||||
children: [
|
||||
if (FinampSettingsHelper.finampSettings.showCoverAsPlayerBackground)
|
||||
const _BlurredPlayerScreenBackground(),
|
||||
const _PlayerAmbientVisualizer(),
|
||||
SafeArea(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
@@ -113,6 +115,158 @@ class PlayerScreen extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _PlayerAmbientVisualizer extends StatefulWidget {
|
||||
const _PlayerAmbientVisualizer({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_PlayerAmbientVisualizer> createState() =>
|
||||
_PlayerAmbientVisualizerState();
|
||||
}
|
||||
|
||||
class _PlayerAmbientVisualizerState extends State<_PlayerAmbientVisualizer>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
StreamSubscription<PlaybackState>? _subscription;
|
||||
bool _isPlaying = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1800),
|
||||
);
|
||||
|
||||
final audioHandler = GetIt.instance<MusicPlayerBackgroundTask>();
|
||||
_subscription = audioHandler.playbackState.listen((playbackState) {
|
||||
if (_isPlaying == playbackState.playing) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isPlaying = playbackState.playing;
|
||||
});
|
||||
|
||||
if (_isPlaying) {
|
||||
_controller.repeat();
|
||||
} else {
|
||||
_controller.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription?.cancel();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return IgnorePointer(
|
||||
child: AnimatedOpacity(
|
||||
opacity: _isPlaying ? 1 : 0.18,
|
||||
duration: const Duration(milliseconds: 450),
|
||||
child: RepaintBoundary(
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, _) {
|
||||
return CustomPaint(
|
||||
painter: _AmbientVisualizerPainter(
|
||||
progress: _controller.value,
|
||||
primary: colorScheme.primary,
|
||||
secondary: colorScheme.secondary,
|
||||
tertiary: colorScheme.tertiary,
|
||||
),
|
||||
size: Size.infinite,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AmbientVisualizerPainter extends CustomPainter {
|
||||
const _AmbientVisualizerPainter({
|
||||
required this.progress,
|
||||
required this.primary,
|
||||
required this.secondary,
|
||||
required this.tertiary,
|
||||
});
|
||||
|
||||
final double progress;
|
||||
final Color primary;
|
||||
final Color secondary;
|
||||
final Color tertiary;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = Offset(size.width * 0.5, size.height * 0.42);
|
||||
final pulse = 0.5 + sin(progress * pi * 2) * 0.5;
|
||||
|
||||
_paintOrb(
|
||||
canvas,
|
||||
center.translate(size.width * -0.22, size.height * -0.08),
|
||||
size.shortestSide * (0.34 + pulse * 0.05),
|
||||
primary.withOpacity(0.16),
|
||||
);
|
||||
_paintOrb(
|
||||
canvas,
|
||||
center.translate(size.width * 0.23, size.height * 0.02),
|
||||
size.shortestSide * (0.30 + (1 - pulse) * 0.07),
|
||||
tertiary.withOpacity(0.14),
|
||||
);
|
||||
|
||||
final barPaint = Paint()..style = PaintingStyle.fill;
|
||||
final barCount = max(16, (size.width / 28).floor());
|
||||
final maxBarHeight = size.height * 0.14;
|
||||
final baseline = size.height * 0.86;
|
||||
final totalWidth = min(size.width * 0.72, 620.0);
|
||||
final gap = totalWidth / barCount;
|
||||
final startX = (size.width - totalWidth) / 2;
|
||||
|
||||
for (var i = 0; i < barCount; i++) {
|
||||
final wave = sin(progress * pi * 2 + i * 0.62);
|
||||
final alternateWave = sin(progress * pi * 4 + i * 0.31);
|
||||
final normalized = (wave + alternateWave * 0.35 + 1.35) / 2.7;
|
||||
final barHeight = 12 + normalized * maxBarHeight;
|
||||
final x = startX + i * gap + gap * 0.22;
|
||||
final width = gap * 0.48;
|
||||
final rect = RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(x, baseline - barHeight, width, barHeight),
|
||||
Radius.circular(width),
|
||||
);
|
||||
|
||||
barPaint.color = Color.lerp(primary, secondary, i / barCount)!
|
||||
.withOpacity(0.16 + normalized * 0.26);
|
||||
canvas.drawRRect(rect, barPaint);
|
||||
}
|
||||
}
|
||||
|
||||
void _paintOrb(Canvas canvas, Offset center, double radius, Color color) {
|
||||
final paint = Paint()
|
||||
..shader = RadialGradient(
|
||||
colors: [color, Colors.transparent],
|
||||
).createShader(Rect.fromCircle(center: center, radius: radius));
|
||||
|
||||
canvas.drawCircle(center, radius, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _AmbientVisualizerPainter oldDelegate) {
|
||||
return progress != oldDelegate.progress ||
|
||||
primary != oldDelegate.primary ||
|
||||
secondary != oldDelegate.secondary ||
|
||||
tertiary != oldDelegate.tertiary;
|
||||
}
|
||||
}
|
||||
|
||||
class _PlayerArtworkSurface extends StatelessWidget {
|
||||
const _PlayerArtworkSurface({Key? key}) : super(key: key);
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
bool get isDownloaderSupported {
|
||||
if (kIsWeb) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return defaultTargetPlatform == TargetPlatform.android ||
|
||||
defaultTargetPlatform == TargetPlatform.iOS;
|
||||
}
|
||||
@@ -31,7 +31,7 @@ class DownloadUpdateStream {
|
||||
_port.sendPort, 'downloader_send_port');
|
||||
_port.listen((dynamic data) {
|
||||
String id = data[0];
|
||||
DownloadTaskStatus status = DownloadTaskStatus(data[1]);
|
||||
DownloadTaskStatus status = DownloadTaskStatus.fromInt(data[1]);
|
||||
int progress = data[2];
|
||||
|
||||
add(DownloadUpdate(
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:path/path.dart' as path_helper;
|
||||
|
||||
import 'finamp_settings_helper.dart';
|
||||
import 'finamp_user_helper.dart';
|
||||
import 'download_support.dart';
|
||||
import 'jellyfin_api.dart';
|
||||
import 'jellyfin_api_helper.dart';
|
||||
import 'get_internal_song_dir.dart';
|
||||
@@ -61,6 +62,10 @@ class DownloadsHelper {
|
||||
/// The view that this download is in. Used for sorting in offline mode.
|
||||
required String viewId,
|
||||
}) async {
|
||||
if (!isDownloaderSupported) {
|
||||
return Future.error("Downloads are only supported on Android and iOS");
|
||||
}
|
||||
|
||||
// Check if we have external storage permission. It's a bit of a hack, but
|
||||
// we only do this if downloadLocation.deletable is true because if it's
|
||||
// true, we're downloading to a user location. You wouldn't want the app
|
||||
@@ -228,6 +233,10 @@ class DownloadsHelper {
|
||||
/// Gets the download status for the given item ids (Jellyfin item id, not flutter_downloader task id).
|
||||
Future<List<DownloadTask>?> getDownloadStatus(List<String> itemIds) async {
|
||||
try {
|
||||
if (!isDownloaderSupported) {
|
||||
return [];
|
||||
}
|
||||
|
||||
List<String> downloadIds = [];
|
||||
|
||||
for (final itemId in itemIds) {
|
||||
@@ -248,7 +257,8 @@ class DownloadsHelper {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removeChildFromParent({required String parentId, required List<String> childIds}) async {
|
||||
Future<void> removeChildFromParent(
|
||||
{required String parentId, required List<String> childIds}) async {
|
||||
var album = _downloadedParentsBox.get(parentId);
|
||||
for (String childId in childIds) {
|
||||
album?.downloadedChildren.removeWhere((key, value) => key == childId);
|
||||
@@ -266,17 +276,18 @@ class DownloadsHelper {
|
||||
_downloadsLogger.info(
|
||||
"Could not find $jellyfinItemId in downloadedItemsBox, assuming already deleted");
|
||||
} else {
|
||||
DownloadedImage? downloadedImage = getDownloadedImage(downloadedSong.song);
|
||||
DownloadedImage? downloadedImage =
|
||||
getDownloadedImage(downloadedSong.song);
|
||||
downloadedImage?.requiredBy.remove(jellyfinItemId);
|
||||
if (downloadedSong.requiredBy.isEmpty) {
|
||||
_downloadsLogger.info(
|
||||
"Item $jellyfinItemId has no dependencies, deleting files");
|
||||
_downloadsLogger
|
||||
.info("Item $jellyfinItemId has no dependencies, deleting files");
|
||||
|
||||
_downloadsLogger.info(
|
||||
"Deleting ${downloadedSong.downloadId} from flutter_downloader");
|
||||
deleteDownloadFutures.add(FlutterDownloader.remove(
|
||||
deleteDownloadFutures.add(_removeDownload(
|
||||
taskId: downloadedSong.downloadId,
|
||||
shouldDeleteContent: true,
|
||||
file: downloadedSong.file,
|
||||
));
|
||||
|
||||
await _downloadedItemsBox.delete(jellyfinItemId);
|
||||
@@ -308,7 +319,8 @@ class DownloadsHelper {
|
||||
|
||||
/// This function will delete all given jellyfinItemIds regardless if they are still used
|
||||
/// by other albums/playlists
|
||||
Future<void> deleteDownloadChildren({required List<String> jellyfinItemIds, String? deletedFor}) async {
|
||||
Future<void> deleteDownloadChildren(
|
||||
{required List<String> jellyfinItemIds, String? deletedFor}) async {
|
||||
final List<Future> deleteDownloadFutures = [];
|
||||
final Map<String, Directory> directoriesToCheck = {};
|
||||
|
||||
@@ -324,8 +336,8 @@ class DownloadsHelper {
|
||||
getDownloadedImage(downloadedSong.song);
|
||||
|
||||
if (deletedFor != null) {
|
||||
_downloadsLogger
|
||||
.info("Removing $deletedFor dependency from $jellyfinItemId, current dependencies ${downloadedSong.requiredBy}");
|
||||
_downloadsLogger.info(
|
||||
"Removing $deletedFor dependency from $jellyfinItemId, current dependencies ${downloadedSong.requiredBy}");
|
||||
downloadedSong.requiredBy.removeWhere((item) => item == deletedFor);
|
||||
}
|
||||
|
||||
@@ -337,9 +349,9 @@ class DownloadsHelper {
|
||||
|
||||
_downloadsLogger.info(
|
||||
"Deleting ${downloadedSong.downloadId} from flutter_downloader");
|
||||
deleteDownloadFutures.add(FlutterDownloader.remove(
|
||||
deleteDownloadFutures.add(_removeDownload(
|
||||
taskId: downloadedSong.downloadId,
|
||||
shouldDeleteContent: true,
|
||||
file: downloadedSong.file,
|
||||
));
|
||||
|
||||
await _downloadedItemsBox.delete(jellyfinItemId);
|
||||
@@ -417,7 +429,8 @@ class DownloadsHelper {
|
||||
String? deletedFor,
|
||||
}) async {
|
||||
try {
|
||||
await deleteDownloadChildren(jellyfinItemIds: jellyfinItemIds, deletedFor: deletedFor);
|
||||
await deleteDownloadChildren(
|
||||
jellyfinItemIds: jellyfinItemIds, deletedFor: deletedFor);
|
||||
if (deletedFor != null) {
|
||||
await deleteDownloadParent(deletedFor: deletedFor);
|
||||
}
|
||||
@@ -445,10 +458,25 @@ class DownloadsHelper {
|
||||
_downloadedImagesBox.delete(downloadedImage.id);
|
||||
_downloadedImageIdsBox.delete(downloadedImage.downloadId);
|
||||
|
||||
await FlutterDownloader.remove(
|
||||
await _removeDownload(
|
||||
taskId: downloadedImage.downloadId,
|
||||
file: downloadedImage.file,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _removeDownload(
|
||||
{required String taskId, required File file}) async {
|
||||
if (isDownloaderSupported) {
|
||||
await FlutterDownloader.remove(
|
||||
taskId: taskId,
|
||||
shouldDeleteContent: true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the total file size of the given directory.
|
||||
@@ -480,6 +508,10 @@ class DownloadsHelper {
|
||||
|
||||
Future<List<DownloadTask>?> getIncompleteDownloads() async {
|
||||
try {
|
||||
if (!isDownloaderSupported) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return await FlutterDownloader.loadTasksWithRawQuery(
|
||||
query: "SELECT * FROM task WHERE status <> 3");
|
||||
} catch (e) {
|
||||
@@ -491,9 +523,13 @@ class DownloadsHelper {
|
||||
Future<List<DownloadTask>?> getDownloadsWithStatus(
|
||||
DownloadTaskStatus downloadTaskStatus) async {
|
||||
try {
|
||||
if (!isDownloaderSupported) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return await FlutterDownloader.loadTasksWithRawQuery(
|
||||
query:
|
||||
"SELECT * FROM task WHERE status = ${downloadTaskStatus.value}");
|
||||
"SELECT * FROM task WHERE status = ${downloadTaskStatus.index}");
|
||||
} catch (e) {
|
||||
_downloadsLogger.severe(e);
|
||||
return Future.error(e);
|
||||
@@ -851,6 +887,10 @@ class DownloadsHelper {
|
||||
/// creating new ones with the same settings. Returns number of songs
|
||||
/// redownloaded
|
||||
Future<int> redownloadFailed() async {
|
||||
if (!isDownloaderSupported) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
final failedDownloadTasks =
|
||||
await getDownloadsWithStatus(DownloadTaskStatus.failed);
|
||||
|
||||
@@ -883,8 +923,8 @@ class DownloadsHelper {
|
||||
for (String parent in parents) {
|
||||
// We don't specify deletedFor here because it could cause the parent
|
||||
// to get deleted
|
||||
deleteFutures
|
||||
.add(deleteParentAndChildDownloads(jellyfinItemIds: [downloadedSong.song.id]));
|
||||
deleteFutures.add(deleteParentAndChildDownloads(
|
||||
jellyfinItemIds: [downloadedSong.song.id]));
|
||||
|
||||
if (parentItems[downloadedSong.song.id] == null) {
|
||||
parentItems[downloadedSong.song.id] = [];
|
||||
@@ -1055,7 +1095,6 @@ class DownloadsHelper {
|
||||
{List<String>? keys}) =>
|
||||
_downloadedItemsBox.listenable(keys: keys);
|
||||
|
||||
|
||||
/// Converts a dart list to a string with the correct SQL syntax
|
||||
String _dartListToSqlList(List dartList) {
|
||||
try {
|
||||
@@ -1106,6 +1145,10 @@ class DownloadsHelper {
|
||||
}) async {
|
||||
assert(item.blurHash != null);
|
||||
|
||||
if (!isDownloaderSupported) {
|
||||
return Future.error("Downloads are only supported on Android and iOS");
|
||||
}
|
||||
|
||||
if (_downloadedImagesBox.containsKey(item.blurHash)) return;
|
||||
|
||||
final imageUrl = _jellyfinApiData.getImageUrl(
|
||||
|
||||
+5
-6
@@ -369,12 +369,11 @@ packages:
|
||||
flutter_downloader:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: db66f36ec48dc50117e510e2f37995cc17b44534
|
||||
resolved-ref: db66f36ec48dc50117e510e2f37995cc17b44534
|
||||
url: "https://github.com/jmshrv/flutter_downloader.git"
|
||||
source: git
|
||||
version: "1.10.4"
|
||||
name: flutter_downloader
|
||||
sha256: "93a9ddbd561f8a3f5483b4189453fba145a0a1014a88143c96a966296b78a118"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.0"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
||||
+1
-4
@@ -34,10 +34,7 @@ dependencies:
|
||||
audio_session: ^0.1.21
|
||||
rxdart: ^0.28.0
|
||||
simple_gesture_detector: ^0.2.0
|
||||
flutter_downloader:
|
||||
git:
|
||||
url: https://github.com/jmshrv/flutter_downloader.git
|
||||
ref: "db66f36ec48dc50117e510e2f37995cc17b44534"
|
||||
flutter_downloader: ^1.12.0
|
||||
path_provider: ^2.0.14
|
||||
hive: ^2.2.3
|
||||
hive_flutter: ^1.1.0
|
||||
|
||||
Reference in New Issue
Block a user