diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 9a6c1e70..1aa08927 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -27,7 +27,7 @@ android { minSdk = 29 targetSdk = 34 versionCode = 1 - versionName = "0.2.0-beta.0" + versionName = "0.2.0-beta.2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -118,8 +118,6 @@ dependencies { implementation("androidx.navigation:navigation-compose:$nav_version") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") - implementation("io.coil-kt.coil3:coil-compose:3.0.0-rc02") - implementation("io.coil-kt.coil3:coil-network-okhttp:3.0.0-rc02") implementation("androidx.media3:media3-exoplayer:$media3_version") implementation("androidx.media3:media3-exoplayer-dash:$media3_version") implementation("androidx.media3:media3-session:$media3_version") diff --git a/android/app/src/main/java/com/kutedev/easemusicplayer/components/EaseImage.kt b/android/app/src/main/java/com/kutedev/easemusicplayer/components/EaseImage.kt new file mode 100644 index 00000000..6a98f884 --- /dev/null +++ b/android/app/src/main/java/com/kutedev/easemusicplayer/components/EaseImage.kt @@ -0,0 +1,38 @@ +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.layout.ContentScale +import com.kutedev.easemusicplayer.core.UIBridgeController +import uniffi.ease_client_shared.DataSourceKey + +@Composable +fun EaseImage( + modifier: Modifier = Modifier, + dataSourceKey: DataSourceKey, + contentScale: ContentScale +) { + val bridge = UIBridgeController.current + var bitmap by remember { mutableStateOf(null) } + + LaunchedEffect(dataSourceKey) { + val data = bridge.bitmapDataSources.load(dataSourceKey) + bitmap = data + } + + if (bitmap == null) { + return + } + + Image( + modifier = modifier, + bitmap = bitmap!!, + contentDescription = null, + contentScale = contentScale, + ) +} \ No newline at end of file diff --git a/android/app/src/main/java/com/kutedev/easemusicplayer/components/ImportCover.kt b/android/app/src/main/java/com/kutedev/easemusicplayer/components/ImportCover.kt index ac2514c7..bb297c64 100644 --- a/android/app/src/main/java/com/kutedev/easemusicplayer/components/ImportCover.kt +++ b/android/app/src/main/java/com/kutedev/easemusicplayer/components/ImportCover.kt @@ -1,5 +1,6 @@ package com.kutedev.easemusicplayer.components +import EaseImage import android.provider.CalendarContract.Colors import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -29,16 +30,16 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import coil3.compose.AsyncImage import com.kutedev.easemusicplayer.R +import uniffi.ease_client_shared.DataSourceKey @Composable fun ImportCover( - url: String, + dataSourceKey: DataSourceKey?, onAdd: () -> Unit, onRemove: () -> Unit, ) { - if (url.isNotEmpty()) { + if (dataSourceKey != null) { Box( modifier = Modifier .size(90.dp) @@ -50,9 +51,8 @@ fun ImportCover( .width(80.dp) .height(80.dp) ) { - AsyncImage( - model = url, - contentDescription = null, + EaseImage( + dataSourceKey = dataSourceKey, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.FillWidth ) @@ -105,26 +105,4 @@ fun ImportCover( } } } -} - -@Preview -@Composable -private fun ImportCoverPreview() { - var url by remember { mutableStateOf("") } - - Box( - modifier = Modifier - .offset(20.dp, 20.dp) - .size(300.dp), - ) { - ImportCover( - url = url, - onAdd = { - url = "https://upload.wikimedia.org/wikipedia/commons/b/b6/WikiWiki.jpg" - }, - onRemove = { - url = "" - } - ) - } } \ No newline at end of file diff --git a/android/app/src/main/java/com/kutedev/easemusicplayer/components/MusicCover.kt b/android/app/src/main/java/com/kutedev/easemusicplayer/components/MusicCover.kt index 4fafe272..ee777d5e 100644 --- a/android/app/src/main/java/com/kutedev/easemusicplayer/components/MusicCover.kt +++ b/android/app/src/main/java/com/kutedev/easemusicplayer/components/MusicCover.kt @@ -1,5 +1,6 @@ package com.kutedev.easemusicplayer.components +import EaseImage import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -9,29 +10,27 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.Dp -import coil3.compose.AsyncImage import com.kutedev.easemusicplayer.R +import uniffi.ease_client_shared.DataSourceKey @Composable fun MusicCover( modifier: Modifier, - coverUrl: String, + coverDataSourceKey: DataSourceKey? ) { Box( modifier = modifier, ) { - if (coverUrl.isEmpty()) { + if (coverDataSourceKey == null) { Image( modifier = Modifier.fillMaxSize(), painter = painterResource(id = R.drawable.cover_default_image), // Replace with actual image resource contentDescription = null, ) } else { - AsyncImage( + EaseImage( modifier = Modifier.background(MaterialTheme.colorScheme.onSurfaceVariant).fillMaxSize(), - model = coverUrl, - contentDescription = null, + dataSourceKey = coverDataSourceKey, contentScale = ContentScale.FillWidth, ) } diff --git a/android/app/src/main/java/com/kutedev/easemusicplayer/core/MusicPlayer.kt b/android/app/src/main/java/com/kutedev/easemusicplayer/core/MusicPlayer.kt index 4428bca0..ad0ef9e2 100644 --- a/android/app/src/main/java/com/kutedev/easemusicplayer/core/MusicPlayer.kt +++ b/android/app/src/main/java/com/kutedev/easemusicplayer/core/MusicPlayer.kt @@ -177,8 +177,8 @@ private class EaseMusicPlayerDelegate : IPlayerDelegateForeign { override fun setMusicUrl(item: MusicToPlay) { val player = _internal ?: return - val coverURI = if (item.coverUrl.isNotEmpty()) { - Uri.parse(item.coverUrl) + val coverURI = if (item.hasCover) { + null } else { Uri.parse(DEFAULT_COVER_BASE64) } diff --git a/android/app/src/main/java/com/kutedev/easemusicplayer/core/UIBridge.kt b/android/app/src/main/java/com/kutedev/easemusicplayer/core/UIBridge.kt index 9456a2b4..a492b006 100644 --- a/android/app/src/main/java/com/kutedev/easemusicplayer/core/UIBridge.kt +++ b/android/app/src/main/java/com/kutedev/easemusicplayer/core/UIBridge.kt @@ -8,11 +8,16 @@ import android.content.ComponentName import android.content.pm.PackageManager import android.os.Build import androidx.activity.result.ActivityResultLauncher +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap import androidx.media3.session.MediaController import androidx.media3.session.SessionToken import androidx.navigation.NavHostController import com.google.common.util.concurrent.MoreExecutors import com.kutedev.easemusicplayer.utils.nextTickOnMain +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import uniffi.ease_client_android.IPermissionServiceForeign import uniffi.ease_client_android.IRouterServiceForeign import uniffi.ease_client_android.IToastServiceForeign @@ -40,8 +45,10 @@ import uniffi.ease_client_android.IAsyncAdapterForeign import uniffi.ease_client_android.apiBuildClient import uniffi.ease_client_android.apiDestroyClient import uniffi.ease_client_android.apiEmitViewAction +import uniffi.ease_client_android.apiLoadAsset import uniffi.ease_client_android.apiStartClient import uniffi.ease_client_shared.ArgInitializeApp +import uniffi.ease_client_shared.DataSourceKey interface IOnNotifyView { @@ -126,7 +133,45 @@ private class PermissionService : IPermissionServiceForeign { this.requestPermissionLauncher?.launch(READ_EXTERNAL_STORAGE) } } +} + +class BitmapDataSources { + class K(key: DataSourceKey) { + private val _key = key; + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is K) return false + + if (this._key is DataSourceKey.Music && other._key is DataSourceKey.Music) { + return this._key.id == other._key.id + } + if (this._key is DataSourceKey.Cover && other._key is DataSourceKey.Cover) { + return this._key.id == other._key.id + } + if (this._key is DataSourceKey.AnyEntry && other._key is DataSourceKey.AnyEntry) { + return this._key.entry.storageId == other._key.entry.storageId && this._key.entry.path == other._key.entry.path; + } + return false; + } + } + private val _map = HashMap() + + suspend fun load(key: DataSourceKey): ImageBitmap? { + val cached = this._map[K(key)] + if (cached != null) { + return null + } + + val data = apiLoadAsset(key) + val decoded = data?.let { + val bitmap = android.graphics.BitmapFactory.decodeByteArray(it, 0, it.size) + bitmap?.asImageBitmap() + } + this._map[K(key)] = decoded + return decoded + } } class UIBridge { @@ -141,6 +186,7 @@ class UIBridge { private val _permissionService = PermissionService() private var _playerController: MediaController? = null private var _executingAction = false + val bitmapDataSources = BitmapDataSources() fun onBackendConnected() { apiStartClient(this._handle) diff --git a/android/app/src/main/java/com/kutedev/easemusicplayer/viewmodels/viewmodels.kt b/android/app/src/main/java/com/kutedev/easemusicplayer/viewmodels/viewmodels.kt index b3d441dc..0e21628d 100644 --- a/android/app/src/main/java/com/kutedev/easemusicplayer/viewmodels/viewmodels.kt +++ b/android/app/src/main/java/com/kutedev/easemusicplayer/viewmodels/viewmodels.kt @@ -40,7 +40,7 @@ val DefaultCurrentPlaylistState = items = emptyList(), title = "", duration = "", - coverUrl = "" + cover = null, ) val DefaultCurrentMusicState = VCurrentMusicState( @@ -53,9 +53,9 @@ val DefaultCurrentMusicState = canChangePosition = false, canPlayNext = false, canPlayPrevious = false, - previousCover = "", - nextCover = "", - cover = "", + previousCover = null, + nextCover = null, + cover = null, playMode = PlayMode.SINGLE, playing = false, lyricIndex = 0, @@ -102,14 +102,14 @@ val DefaultCreatePlaylistState = VCreatePlaylistState( mode = CreatePlaylistMode.FULL, name = "", - picture = "", + picture = null, musicCount = 0u, recommendPlaylistNames = emptyList(), fullImported = false, modalOpen = false, canSubmit = false ) -val DefaultEditPlaylistState = VEditPlaylistState(name = "", picture = "", modalOpen = false) +val DefaultEditPlaylistState = VEditPlaylistState(name = "", picture = null, modalOpen = false) val DefaultCurrentMusicLyricState = VCurrentMusicLyricState(lyricLines = listOf(), loadState = LyricLoadState.LOADING) diff --git a/android/app/src/main/java/com/kutedev/easemusicplayer/widgets/musics/MiniPlayer.kt b/android/app/src/main/java/com/kutedev/easemusicplayer/widgets/musics/MiniPlayer.kt index fdc32137..35839278 100644 --- a/android/app/src/main/java/com/kutedev/easemusicplayer/widgets/musics/MiniPlayer.kt +++ b/android/app/src/main/java/com/kutedev/easemusicplayer/widgets/musics/MiniPlayer.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -37,12 +36,13 @@ import com.kutedev.easemusicplayer.core.UIBridgeController import com.kutedev.easemusicplayer.viewmodels.EaseViewModel import uniffi.ease_client.MainBodyWidget import uniffi.ease_client.MusicControlWidget +import uniffi.ease_client_shared.DataSourceKey @Composable private fun MiniPlayerCore( isPlaying: Boolean, title: String, - coverUrl: String, + cover: DataSourceKey?, currentDurationMS: ULong, totalDuration: String, totalDurationMS: ULong, @@ -62,7 +62,7 @@ private fun MiniPlayerCore( modifier = Modifier .clip(RoundedCornerShape(10.dp)) .size(60.dp), - coverUrl = coverUrl, + coverDataSourceKey = cover, ) Box(modifier = Modifier.width(16.dp)) Column( @@ -152,7 +152,7 @@ fun MiniPlayer( MiniPlayerCore( isPlaying = state.playing, title = state.title, - coverUrl = state.cover, + cover = state.cover, currentDurationMS = state.currentDurationMs, totalDuration = state.totalDuration, totalDurationMS = state.totalDurationMs, @@ -171,7 +171,7 @@ private fun MiniPlayerPreview() { MiniPlayerCore( isPlaying = true, title = "Very very very very very long music title", - coverUrl = "", + cover = null, currentDurationMS = 10uL, totalDuration = "00:06", totalDurationMS = 60uL, diff --git a/android/app/src/main/java/com/kutedev/easemusicplayer/widgets/musics/PlayerPage.kt b/android/app/src/main/java/com/kutedev/easemusicplayer/widgets/musics/PlayerPage.kt index 59392fa2..64faea7e 100644 --- a/android/app/src/main/java/com/kutedev/easemusicplayer/widgets/musics/PlayerPage.kt +++ b/android/app/src/main/java/com/kutedev/easemusicplayer/widgets/musics/PlayerPage.kt @@ -71,9 +71,9 @@ import uniffi.ease_client.MusicControlAction import uniffi.ease_client.MusicControlWidget import uniffi.ease_client.MusicDetailWidget import uniffi.ease_client.MusicLyricWidget -import uniffi.ease_client.TimeToPauseWidget import uniffi.ease_client.VLyricLine import uniffi.ease_client.ViewAction +import uniffi.ease_client_shared.DataSourceKey import uniffi.ease_client_shared.LyricLoadState import uniffi.ease_client_shared.PlayMode import kotlin.math.absoluteValue @@ -254,7 +254,7 @@ private fun MusicSlider( @Composable -private fun CoverImage(url: String) { +private fun CoverImage(dataSourceKey: DataSourceKey?) { Box( contentAlignment = Alignment.Center, modifier = Modifier @@ -270,7 +270,7 @@ private fun CoverImage(url: String) { ) .clip(RoundedCornerShape(20.dp)) .size(300.dp), - coverUrl = url, + coverDataSourceKey = dataSourceKey, ) } } @@ -383,9 +383,9 @@ private fun MusicLyric( private fun MusicPlayerBody( onPrev: () -> Unit, onNext: () -> Unit, - cover: String, - prevCover: String, - nextCover: String, + cover: DataSourceKey?, + prevCover: DataSourceKey?, + nextCover: DataSourceKey?, canPrev: Boolean, canNext: Boolean, lyricIndex: Int, @@ -483,7 +483,7 @@ private fun MusicPlayerBody( .offset(x = -widgetWidthDp + deltaDp), contentAlignment = Alignment.Center, ) { - CoverImage(url = prevCover) + CoverImage(dataSourceKey = prevCover) } } if (canNext) { @@ -492,7 +492,7 @@ private fun MusicPlayerBody( .offset(x = widgetWidthDp + deltaDp), contentAlignment = Alignment.Center, ) { - CoverImage(url = nextCover) + CoverImage(dataSourceKey = nextCover) } } } @@ -502,7 +502,7 @@ private fun MusicPlayerBody( contentAlignment = Alignment.Center, ) { if (!showLyric) { - CoverImage(url = cover) + CoverImage(dataSourceKey = cover) } else { MusicLyric( lyricIndex = lyricIndex, @@ -761,9 +761,9 @@ private fun MusicPlayerBodyPreview() { }, onNext = { }, - cover = "", - prevCover = "", - nextCover = "", + cover = null, + prevCover = null, + nextCover = null, canPrev = canPrev, canNext = canNext, lyricIndex = 0, diff --git a/android/app/src/main/java/com/kutedev/easemusicplayer/widgets/playlists/PlaylistDialog.kt b/android/app/src/main/java/com/kutedev/easemusicplayer/widgets/playlists/PlaylistDialog.kt index 3411c3cb..5185bcb6 100644 --- a/android/app/src/main/java/com/kutedev/easemusicplayer/widgets/playlists/PlaylistDialog.kt +++ b/android/app/src/main/java/com/kutedev/easemusicplayer/widgets/playlists/PlaylistDialog.kt @@ -169,7 +169,7 @@ private fun FullImportBlock( text = stringResource(R.string.playlists_dialog_cover), ) ImportCover( - url = state.picture, + dataSourceKey = state.picture, onAdd = { bridge.dispatchClick(PlaylistCreateWidget.Cover); }, @@ -317,7 +317,7 @@ fun EditPlaylistsDialog( text = stringResource(R.string.playlists_dialog_cover), ) ImportCover( - url = state.picture, + dataSourceKey = state.picture, onAdd = { bridge.dispatchClick(PlaylistEditWidget.Cover); }, diff --git a/android/app/src/main/java/com/kutedev/easemusicplayer/widgets/playlists/PlaylistPage.kt b/android/app/src/main/java/com/kutedev/easemusicplayer/widgets/playlists/PlaylistPage.kt index e4b60de0..fd99c59b 100644 --- a/android/app/src/main/java/com/kutedev/easemusicplayer/widgets/playlists/PlaylistPage.kt +++ b/android/app/src/main/java/com/kutedev/easemusicplayer/widgets/playlists/PlaylistPage.kt @@ -1,5 +1,6 @@ package com.kutedev.easemusicplayer.widgets.playlists +import EaseImage import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.DecayAnimationSpec import androidx.compose.animation.core.tween @@ -55,7 +56,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import coil3.compose.AsyncImage import com.kutedev.easemusicplayer.R import com.kutedev.easemusicplayer.components.ConfirmDialog import com.kutedev.easemusicplayer.components.CustomAnchoredDraggableState @@ -75,6 +75,7 @@ import uniffi.ease_client.PlaylistDetailWidget import uniffi.ease_client.RoutesKey import uniffi.ease_client.VCurrentMusicState import uniffi.ease_client.VPlaylistMusicItem +import uniffi.ease_client_shared.DataSourceKey import uniffi.ease_client_shared.MusicId import kotlin.math.roundToInt @@ -104,7 +105,7 @@ private fun RemovePlaylistDialog( @Composable private fun PlaylistHeader( - coverUrl: String, + cover: DataSourceKey?, title: String, duration: String, items: List, @@ -123,7 +124,7 @@ private fun PlaylistHeader( .height(157.dp) .fillMaxWidth() ) { - if (coverUrl.isEmpty()) { + if (cover == null) { Image( modifier = Modifier .fillMaxSize(), @@ -135,11 +136,10 @@ private fun PlaylistHeader( Box( modifier = Modifier.fillMaxSize() ) { - AsyncImage( + EaseImage( modifier = Modifier .fillMaxSize(), - model = coverUrl, - contentDescription = null, + dataSourceKey = cover, contentScale = ContentScale.FillWidth ) Box( @@ -451,7 +451,7 @@ fun PlaylistPage( ) { Column { PlaylistHeader( - coverUrl = state.coverUrl, + cover = state.cover, title = state.title, duration = state.duration, items = items, diff --git a/android/app/src/main/java/com/kutedev/easemusicplayer/widgets/playlists/PlaylistsPage.kt b/android/app/src/main/java/com/kutedev/easemusicplayer/widgets/playlists/PlaylistsPage.kt index 0072a983..ab6ee628 100644 --- a/android/app/src/main/java/com/kutedev/easemusicplayer/widgets/playlists/PlaylistsPage.kt +++ b/android/app/src/main/java/com/kutedev/easemusicplayer/widgets/playlists/PlaylistsPage.kt @@ -1,5 +1,6 @@ package com.kutedev.easemusicplayer.widgets.playlists +import EaseImage import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -34,7 +35,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow -import coil3.compose.AsyncImage import com.kutedev.easemusicplayer.components.EaseIconButton import com.kutedev.easemusicplayer.components.EaseIconButtonSize import com.kutedev.easemusicplayer.components.EaseIconButtonType @@ -127,7 +127,7 @@ private fun PlaylistItem(playlist: VPlaylistAbstractItem) { Box( modifier = Modifier.clip(RoundedCornerShape(20.dp)).background(MaterialTheme.colorScheme.onSurfaceVariant).size(136.dp) ) { - if (playlist.coverUrl.isEmpty()) { + if (playlist.cover == null) { Image( modifier = Modifier.fillMaxSize(), painter = painterResource(id = R.drawable.cover_default_image), @@ -135,10 +135,9 @@ private fun PlaylistItem(playlist: VPlaylistAbstractItem) { contentScale = ContentScale.FillWidth ) } else { - AsyncImage( + EaseImage( modifier = Modifier.fillMaxSize(), - model = playlist.coverUrl, - contentDescription = null, + dataSourceKey = playlist.cover!!, contentScale = ContentScale.FillWidth ) } diff --git a/rust-libs/ease-client-android/src/api.rs b/rust-libs/ease-client-android/src/api.rs index 8ea489c5..e674e14a 100644 --- a/rust-libs/ease-client-android/src/api.rs +++ b/rust-libs/ease-client-android/src/api.rs @@ -8,7 +8,7 @@ use ease_client::{build_client, Action, ViewAction}; use ease_client_backend::Backend; use ease_client_shared::backends::{ app::ArgInitializeApp, encode_message_payload, generated::Code, player::PlayerDelegateEvent, - MessagePayload, + storage::DataSourceKey, MessagePayload, }; use misty_vm::{AppPods, AsyncRuntime, IAsyncRuntimeAdapter, LocalBoxFuture}; use once_cell::sync::Lazy; @@ -190,6 +190,29 @@ pub fn api_backend_play_previous() { }); } +#[uniffi::export] +pub fn api_flush_backend_spawned_local() { + let _guard = RT.enter(); + if let Some(backend) = BACKEND.try_backend() { + backend.flush_spawned_locals(); + } +} + +#[uniffi::export] +pub async fn api_load_asset(key: DataSourceKey) -> Option> { + RT.spawn(async move { + if let Some(backend) = BACKEND.try_backend() { + let file = backend.load_asset(key).await; + if let Ok(Some(file)) = file { + return file.bytes().await.ok().map(|v| v.to_vec()); + } + } + None + }) + .await + .unwrap() +} + #[uniffi::export] pub fn api_build_client( permission: Arc, @@ -243,11 +266,3 @@ pub fn api_flush_spawned_locals(handle: u64) { client.flush_spawned(); } } - -#[uniffi::export] -pub fn api_flush_backend_spawned_local() { - let _guard = RT.enter(); - if let Some(backend) = BACKEND.try_backend() { - backend.flush_spawned_locals(); - } -} diff --git a/rust-libs/ease-client-backend/src/lib.rs b/rust-libs/ease-client-backend/src/lib.rs index 90f7e3cf..e2d843f2 100644 --- a/rust-libs/ease-client-backend/src/lib.rs +++ b/rust-libs/ease-client-backend/src/lib.rs @@ -15,7 +15,7 @@ use ease_client_shared::backends::{ app::ArgInitializeApp, connector::IConnectorNotifier, message::MessagePayload, storage::DataSourceKey, }; -use ease_remote_storage::StreamFile; +pub use ease_remote_storage::StreamFile; use error::BResult; use misty_async::{AsyncRuntime, IOnAsyncRuntime}; pub use services::player::{IPlayerDelegate, MusicToPlay}; @@ -74,7 +74,6 @@ impl Backend { } pub async fn load_asset(&self, key: DataSourceKey) -> BResult> { - let cx = self.cx.clone(); load_asset(&self.cx, key).await } diff --git a/rust-libs/ease-client-backend/src/services/music/mod.rs b/rust-libs/ease-client-backend/src/services/music/mod.rs index 01236102..f088f3ad 100644 --- a/rust-libs/ease-client-backend/src/services/music/mod.rs +++ b/rust-libs/ease-client-backend/src/services/music/mod.rs @@ -4,7 +4,7 @@ use ease_client_shared::backends::{ connector::ConnectorAction, music::{LyricLoadState, Music, MusicAbstract, MusicId, MusicLyric, MusicMeta}, music_duration::MusicDuration, - storage::StorageEntryLoc, + storage::{DataSourceKey, StorageEntryLoc}, }; use crate::{ @@ -91,19 +91,19 @@ pub(crate) fn build_music_meta(model: MusicModel) -> MusicMeta { } pub(crate) fn build_music_abstract(cx: &Arc, model: MusicModel) -> MusicAbstract { - let cover_url = if model + let cover = if model .cover .as_ref() .map(|v| !v.is_empty()) .unwrap_or_default() { - get_serve_cover_url_from_music_id(&cx, model.id) + Some(DataSourceKey::Cover { id: model.id }) } else { Default::default() }; MusicAbstract { - cover_url, + cover, meta: build_music_meta(model), } } @@ -200,17 +200,17 @@ pub(crate) async fn get_music(cx: &Arc, id: MusicId) -> BResult< } let lyric: Option = load_lyric(&cx, lyric_loc, using_fallback).await; - let cover_url = if model.cover.unwrap_or_default().is_empty() { + let cover = if model.cover.unwrap_or_default().is_empty() { Default::default() } else { - get_serve_cover_url_from_music_id(&cx, model.id) + Some(DataSourceKey::Cover { id: model.id }) }; let music: Music = Music { meta, loc, url, - cover_url, + cover, lyric, }; Ok(Some(music)) diff --git a/rust-libs/ease-client-backend/src/services/player/mod.rs b/rust-libs/ease-client-backend/src/services/player/mod.rs index 477a13d0..5ae053de 100644 --- a/rust-libs/ease-client-backend/src/services/player/mod.rs +++ b/rust-libs/ease-client-backend/src/services/player/mod.rs @@ -5,6 +5,7 @@ use ease_client_shared::backends::{ music::{MusicAbstract, MusicId}, player::{ConnectorPlayerAction, PlayMode, PlayerCurrentPlaying}, playlist::PlaylistId, + storage::DataSourceKey, }; use crate::{ @@ -19,7 +20,7 @@ pub struct MusicToPlay { pub id: MusicId, pub title: String, pub url: String, - pub cover_url: String, + pub has_cover: bool, } pub trait IPlayerDelegate: Send + Sync + 'static { @@ -74,16 +75,16 @@ impl PlayerState { } } - pub fn cover(&self) -> String { + pub fn cover(&self) -> Option { if let Some(PlayerMedia { queue, index, .. }) = self.current.read().unwrap().as_ref() { if let Some(music) = queue.get(*index) { - return music.cover_url.clone(); + return music.cover.clone(); } } - String::new() + Default::default() } - pub fn prev_cover(&self) -> String { + pub fn prev_cover(&self) -> Option { if let Some(PlayerMedia { queue, index, .. }) = self.current.read().unwrap().as_ref() { if self.can_play_previous() { let prev_index = if *index == 0 { @@ -92,14 +93,14 @@ impl PlayerState { *index - 1 }; if let Some(music) = queue.get(prev_index) { - return music.cover_url.clone(); + return music.cover.clone(); } } } - String::new() + Default::default() } - pub fn next_cover(&self) -> String { + pub fn next_cover(&self) -> Option { if let Some(PlayerMedia { queue, index, .. }) = self.current.read().unwrap().as_ref() { if self.can_play_next() { let next_index = if *index + 1 >= queue.len() { @@ -108,11 +109,11 @@ impl PlayerState { *index + 1 }; if let Some(music) = queue.get(next_index) { - return music.cover_url.clone(); + return music.cover.clone(); } } } - String::new() + Default::default() } } @@ -152,7 +153,7 @@ pub(crate) async fn player_request_play( id: to_play.id, title: music.title().to_string(), url, - cover_url: music.cover_url, + has_cover: music.cover.is_some(), }; item }; @@ -294,7 +295,7 @@ pub(crate) fn get_player_current( can_next: player_state.can_play_next(), prev_cover: player_state.prev_cover(), next_cover: player_state.next_cover(), - cover: state.queue[state.index].cover_url.clone(), + cover: state.queue[state.index].cover.clone(), }; Ok(Some(current)) } diff --git a/rust-libs/ease-client-backend/src/services/server/mod.rs b/rust-libs/ease-client-backend/src/services/server/mod.rs index 23d81392..c4d7bb8f 100644 --- a/rust-libs/ease-client-backend/src/services/server/mod.rs +++ b/rust-libs/ease-client-backend/src/services/server/mod.rs @@ -5,8 +5,10 @@ use std::sync::Arc; use ease_client_shared::backends::storage::DataSourceKey; use ease_remote_storage::StreamFile; -use serve::get_stream_file_cover_by_music_id; pub use serve::start_server; +use serve::{ + get_stream_file_by_loc, get_stream_file_by_music_id, get_stream_file_cover_by_music_id, +}; use crate::{ctx::BackendContext, error::BResult}; @@ -15,12 +17,8 @@ pub(crate) async fn load_asset( key: DataSourceKey, ) -> BResult> { match key { - DataSourceKey::Music { id } => { - unimplemented!() - } + DataSourceKey::Music { id } => get_stream_file_by_music_id(cx, id).await, DataSourceKey::Cover { id } => get_stream_file_cover_by_music_id(cx, id).await, - DataSourceKey::AnyEntry { entry } => { - unimplemented!() - } + DataSourceKey::AnyEntry { entry } => get_stream_file_by_loc(cx, entry).await, } } diff --git a/rust-libs/ease-client-backend/src/services/server/serve.rs b/rust-libs/ease-client-backend/src/services/server/serve.rs index 745bfc54..529375f8 100644 --- a/rust-libs/ease-client-backend/src/services/server/serve.rs +++ b/rust-libs/ease-client-backend/src/services/server/serve.rs @@ -22,7 +22,7 @@ use crate::{ }, }; -async fn get_stream_file_by_loc( +pub(crate) async fn get_stream_file_by_loc( cx: &Arc, loc: StorageEntryLoc, ) -> BResult> { @@ -35,7 +35,7 @@ async fn get_stream_file_by_loc( Ok(Some(stream_file)) } -async fn get_stream_file_by_music_id( +pub(crate) async fn get_stream_file_by_music_id( cx: &Arc, id: MusicId, ) -> BResult> { diff --git a/rust-libs/ease-client-shared/src/backends/music.rs b/rust-libs/ease-client-shared/src/backends/music.rs index 8c4752f4..bde65316 100644 --- a/rust-libs/ease-client-shared/src/backends/music.rs +++ b/rust-libs/ease-client-shared/src/backends/music.rs @@ -1,10 +1,11 @@ - use serde::{Deserialize, Serialize}; use crate::define_id; use super::{ - lyric::Lyrics, music_duration::MusicDuration, storage::StorageEntryLoc, + lyric::Lyrics, + music_duration::MusicDuration, + storage::{DataSourceKey, StorageEntryLoc}, }; define_id!(MusicId); @@ -19,7 +20,7 @@ pub struct MusicMeta { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct MusicAbstract { pub meta: MusicMeta, - pub cover_url: String, + pub cover: Option, } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)] @@ -43,7 +44,7 @@ pub struct Music { pub meta: MusicMeta, pub loc: StorageEntryLoc, pub url: String, - pub cover_url: String, + pub cover: Option, pub lyric: Option, } @@ -60,7 +61,7 @@ impl Music { pub fn music_abstract(&self) -> MusicAbstract { MusicAbstract { meta: self.meta.clone(), - cover_url: self.cover_url.clone(), + cover: self.cover.clone(), } } } diff --git a/rust-libs/ease-client-shared/src/backends/player.rs b/rust-libs/ease-client-shared/src/backends/player.rs index 86e58187..530a7e9c 100644 --- a/rust-libs/ease-client-shared/src/backends/player.rs +++ b/rust-libs/ease-client-shared/src/backends/player.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use super::{ music::{MusicAbstract, MusicId}, playlist::PlaylistId, + storage::DataSourceKey, }; #[derive(Debug, Serialize, Deserialize)] @@ -21,9 +22,9 @@ pub struct PlayerCurrentPlaying { pub mode: PlayMode, pub can_prev: bool, pub can_next: bool, - pub cover: String, - pub prev_cover: String, - pub next_cover: String, + pub cover: Option, + pub prev_cover: Option, + pub next_cover: Option, } #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Enum)] diff --git a/rust-libs/ease-client-shared/src/backends/playlist.rs b/rust-libs/ease-client-shared/src/backends/playlist.rs index dcddf9fe..db9e81f7 100644 --- a/rust-libs/ease-client-shared/src/backends/playlist.rs +++ b/rust-libs/ease-client-shared/src/backends/playlist.rs @@ -72,7 +72,7 @@ impl Playlist { pub fn cover(&self) -> &Option { &self.abstr.cover() } - pub fn show_cover_url(&self) -> &Option { + pub fn show_cover(&self) -> &Option { self.abstr.show_cover() } pub fn duration(&self) -> &Option { diff --git a/rust-libs/ease-client-test/src/lib.rs b/rust-libs/ease-client-test/src/lib.rs index 88e69e48..85b95a56 100644 --- a/rust-libs/ease-client-test/src/lib.rs +++ b/rust-libs/ease-client-test/src/lib.rs @@ -14,7 +14,7 @@ use ease_client_backend::Backend; use ease_client_shared::backends::app::ArgInitializeApp; use ease_client_shared::backends::music::MusicId; use ease_client_shared::backends::playlist::{CreatePlaylistMode, PlaylistId}; -use ease_client_shared::backends::storage::{StorageId, StorageType}; +use ease_client_shared::backends::storage::{DataSourceKey, StorageId, StorageType}; use ease_client_shared::backends::MessagePayload; use event_loop::EventLoop; use fake_permission::FakePermissionService; @@ -336,6 +336,15 @@ impl TestApp { self.server.load_resource(url).await } + pub async fn load_resource_by_key(&self, key: DataSourceKey) -> Vec { + let v = self.backend.load_asset(key.into()).await.unwrap(); + if let Some(v) = v { + v.bytes().await.unwrap().to_vec() + } else { + Default::default() + } + } + async fn wait(&self, mut ms: u64) { tokio::time::sleep(Duration::from_millis(0)).await; loop { diff --git a/rust-libs/ease-client-test/tests/music.rs b/rust-libs/ease-client-test/tests/music.rs index 4b5a29a2..3328dac2 100644 --- a/rust-libs/ease-client-test/tests/music.rs +++ b/rust-libs/ease-client-test/tests/music.rs @@ -625,18 +625,18 @@ async fn music_cover_1() { app.dispatch_click(PlaylistDetailWidget::Music { id: a_music_id }); app.wait_network().await; let state = app.latest_state().current_music.unwrap(); - let cover_url = state.cover.clone(); - let picture = app.load_resource(&cover_url).await; + let cover = state.cover.clone().unwrap(); + let picture = app.load_resource_by_key(cover).await; assert_eq!(picture.len(), 15025); let state = app.latest_state().playlist_list.unwrap(); assert_eq!(state.playlist_list.len(), 1); - let cover_url = state.playlist_list[0].cover_url.clone(); - let picture = app.load_resource(&cover_url).await; + let cover = state.playlist_list[0].cover.clone().unwrap(); + let picture = app.load_resource_by_key(cover).await; assert_eq!(picture.len(), 15025); let state = app.latest_state().current_playlist.unwrap(); - let cover_url = state.cover_url.clone(); - let picture = app.load_resource(&cover_url).await; + let cover = state.cover.clone().unwrap(); + let picture = app.load_resource_by_key(cover).await; assert_eq!(picture.len(), 15025); } diff --git a/rust-libs/ease-client-test/tests/playlist_cover.rs b/rust-libs/ease-client-test/tests/playlist_cover.rs index 6bbd877a..b3a5812a 100644 --- a/rust-libs/ease-client-test/tests/playlist_cover.rs +++ b/rust-libs/ease-client-test/tests/playlist_cover.rs @@ -21,7 +21,7 @@ async fn create_playlist_cover_1() { app.dispatch_click(StorageImportWidget::Import); app.wait_network().await; let state = app.latest_state().create_playlist.unwrap(); - let picture = app.load_resource(state.picture).await; + let picture = app.load_resource_by_key(state.picture.unwrap()).await; assert_eq!(picture.len(), 82580); app.dispatch_click(PlaylistCreateWidget::FinishCreate); app.wait_network().await; @@ -34,7 +34,7 @@ async fn create_playlist_cover_1() { app.dispatch_click(PlaylistEditWidget::ClearCover); app.dispatch_click(PlaylistEditWidget::FinishEdit); let state = app.latest_state().edit_playlist.unwrap(); - assert_eq!(state.picture, ""); + assert_eq!(state.picture, None); } #[tokio::test] @@ -54,13 +54,13 @@ async fn edit_playlist_cover_1() { app.dispatch_click(StorageImportWidget::Import); app.wait_network().await; let state = app.latest_state().edit_playlist.unwrap(); - let picture = app.load_resource(state.picture).await; + let picture = app.load_resource_by_key(state.picture.unwrap()).await; assert_eq!(picture.len(), 82580); app.dispatch_click(PlaylistEditWidget::ClearCover); app.dispatch_click(PlaylistEditWidget::FinishEdit); let state = app.latest_state().edit_playlist.unwrap(); - assert_eq!(state.picture, ""); + assert_eq!(state.picture, None); } #[tokio::test] @@ -91,7 +91,7 @@ async fn edit_playlist_cover_2() { let state = app.latest_state().playlist_list.unwrap(); assert_eq!(state.playlist_list.len(), 1); let picture = app - .load_resource(state.playlist_list[0].cover_url.clone()) + .load_resource_by_key(state.playlist_list[0].cover.clone().unwrap()) .await; assert_eq!(picture.len(), 82580); @@ -100,7 +100,7 @@ async fn edit_playlist_cover_2() { let state = app.latest_state().playlist_list.unwrap(); assert_eq!(state.playlist_list.len(), 1); let picture = app - .load_resource(state.playlist_list[0].cover_url.clone()) + .load_resource_by_key(state.playlist_list[0].cover.clone().unwrap()) .await; assert_eq!(picture.len(), 82580); } diff --git a/rust-libs/ease-client-test/tests/playlist_create.rs b/rust-libs/ease-client-test/tests/playlist_create.rs index 021f6ba1..0c2012ae 100644 --- a/rust-libs/ease-client-test/tests/playlist_create.rs +++ b/rust-libs/ease-client-test/tests/playlist_create.rs @@ -32,7 +32,7 @@ async fn create_playlist_full_1() { assert_eq!(state.mode, CreatePlaylistMode::Full); assert_eq!(state.music_count, 1); assert_eq!(state.recommend_playlist_names.len(), 0); - let picture = app.load_resource(&state.picture).await; + let picture = app.load_resource_by_key(state.picture.unwrap()).await; assert_eq!(picture.len(), 82580); app.dispatch_change_text(PlaylistCreateWidget::Name, "ABC"); @@ -42,7 +42,7 @@ async fn create_playlist_full_1() { let state = app.latest_state().playlist_list.unwrap(); assert_eq!(state.playlist_list.len(), 1); assert_eq!(state.playlist_list[0].title, "ABC".to_string()); - assert_ne!(state.playlist_list[0].cover_url, "") + assert_ne!(state.playlist_list[0].cover.clone(), None) } #[tokio::test] @@ -77,7 +77,7 @@ async fn create_playlist_full_2() { assert_eq!(state.mode, CreatePlaylistMode::Full); assert_eq!(state.music_count, 1); assert_eq!(state.recommend_playlist_names, vec!["musics".to_string()]); - assert_eq!(state.picture, ""); + assert_eq!(state.picture, None); app.dispatch_change_text(PlaylistCreateWidget::Name, "ABC"); app.dispatch_click(PlaylistCreateWidget::FinishCreate); @@ -86,7 +86,7 @@ async fn create_playlist_full_2() { let state = app.latest_state().playlist_list.unwrap(); assert_eq!(state.playlist_list.len(), 1); assert_eq!(state.playlist_list[0].title, "ABC".to_string()); - assert_eq!(state.playlist_list[0].cover_url, "".to_string()) + assert_eq!(state.playlist_list[0].cover.clone(), None) } #[tokio::test] @@ -106,7 +106,7 @@ async fn create_playlist_empty_1() { let state = app.latest_state().playlist_list.unwrap(); assert_eq!(state.playlist_list.len(), 1); assert_eq!(state.playlist_list[0].title, "ABC".to_string()); - assert_eq!(state.playlist_list[0].cover_url, "") + assert_eq!(state.playlist_list[0].cover.clone(), None) } #[tokio::test] @@ -134,6 +134,6 @@ async fn create_playlist_only_cover_1() { assert_eq!(state.mode, CreatePlaylistMode::Full); assert_eq!(state.music_count, 0); assert_eq!(state.recommend_playlist_names.len(), 0); - let picture = app.load_resource(&state.picture).await; + let picture = app.load_resource_by_key(state.picture.unwrap()).await; assert_eq!(picture.len(), 82580); } diff --git a/rust-libs/ease-client/src/view_models/view_state/mod.rs b/rust-libs/ease-client/src/view_models/view_state/mod.rs index 2184fe26..d5a9c93d 100644 --- a/rust-libs/ease-client/src/view_models/view_state/mod.rs +++ b/rust-libs/ease-client/src/view_models/view_state/mod.rs @@ -95,38 +95,10 @@ impl ViewStateVM { vsb!(self, cx, root, self.time_to_pause, time_to_pause_vs); vsb!(self, cx, root, self.current_music, current_music_lyric_vs); // Playlist - vsb!( - self, - cx, - root, - self.all_playlist, - self.connector, - playlist_list_vs - ); - vsb!( - self, - cx, - root, - self.current_playlist, - self.connector, - current_playlist_vs - ); - vsb!( - self, - cx, - root, - self.edit_playlist, - self.connector, - edit_playlist_vs - ); - vsb!( - self, - cx, - root, - self.create_playlist, - self.connector, - create_playlist_vs - ); + vsb!(self, cx, root, self.all_playlist, playlist_list_vs); + vsb!(self, cx, root, self.current_playlist, current_playlist_vs); + vsb!(self, cx, root, self.edit_playlist, edit_playlist_vs); + vsb!(self, cx, root, self.create_playlist, create_playlist_vs); // Main vsb!(self, cx, root, self.router, main_vs); // Storage diff --git a/rust-libs/ease-client/src/view_models/view_state/views/music.rs b/rust-libs/ease-client/src/view_models/view_state/views/music.rs index d6a7fd9b..bf5c0130 100644 --- a/rust-libs/ease-client/src/view_models/view_state/views/music.rs +++ b/rust-libs/ease-client/src/view_models/view_state/views/music.rs @@ -2,6 +2,7 @@ use ease_client_shared::backends::{ music::{LyricLoadState, MusicId}, music_duration::MusicDuration, player::PlayMode, + storage::DataSourceKey, }; use serde::Serialize; @@ -23,9 +24,9 @@ pub struct VCurrentMusicState { pub can_change_position: bool, pub can_play_next: bool, pub can_play_previous: bool, - pub previous_cover: String, - pub next_cover: String, - pub cover: String, + pub previous_cover: Option, + pub next_cover: Option, + pub cover: Option, pub play_mode: PlayMode, pub playing: bool, pub lyric_index: i32, diff --git a/rust-libs/ease-client/src/view_models/view_state/views/playlist.rs b/rust-libs/ease-client/src/view_models/view_state/views/playlist.rs index 77089e2b..3c9c04d1 100644 --- a/rust-libs/ease-client/src/view_models/view_state/views/playlist.rs +++ b/rust-libs/ease-client/src/view_models/view_state/views/playlist.rs @@ -1,6 +1,7 @@ use ease_client_shared::backends::{ music::MusicId, playlist::{CreatePlaylistMode, PlaylistId}, + storage::DataSourceKey, }; use serde::Serialize; @@ -22,7 +23,7 @@ pub struct VPlaylistAbstractItem { pub title: String, pub count: i32, pub duration: String, - pub cover_url: String, + pub cover: Option, } #[derive(Debug, Clone, Serialize, uniffi::Record)] @@ -43,12 +44,12 @@ pub struct VCurrentPlaylistState { pub items: Vec, pub title: String, pub duration: String, - pub cover_url: String, + pub cover: Option, } #[derive(Debug, Clone, Default, Serialize, uniffi::Record)] pub struct VEditPlaylistState { - pub picture: String, + pub picture: Option, pub name: String, pub modal_open: bool, } @@ -57,7 +58,7 @@ pub struct VEditPlaylistState { pub struct VCreatePlaylistState { pub mode: CreatePlaylistMode, pub name: String, - pub picture: String, + pub picture: Option, pub music_count: u32, pub recommend_playlist_names: Vec, pub full_imported: bool, @@ -65,10 +66,7 @@ pub struct VCreatePlaylistState { pub can_submit: bool, } -pub(crate) fn playlist_list_vs( - (state, connector_state): (&AllPlaylistState, &ConnectorState), - root: &mut RootViewModelState, -) { +pub(crate) fn playlist_list_vs((state): (&AllPlaylistState), root: &mut RootViewModelState) { let mut list: Vec<_> = { state.playlists.iter().map(|item| item.clone()).collect() }; list.sort_by(|lhs, rhs| { rhs.created_time() @@ -85,7 +83,7 @@ pub(crate) fn playlist_list_vs( title: playlist.title().to_string(), count: playlist.music_count as i32, duration: get_display_duration(&duration), - cover_url: connector_state.serve_asset_url_opt_key(playlist.show_cover().clone()), + cover: playlist.show_cover().clone(), }); } @@ -95,7 +93,7 @@ pub(crate) fn playlist_list_vs( } pub(crate) fn current_playlist_vs( - (current_playlist, connector_state): (&CurrentPlaylistState, &ConnectorState), + current_playlist: &CurrentPlaylistState, root: &mut RootViewModelState, ) { let playlist = current_playlist.playlist.clone(); @@ -123,35 +121,37 @@ pub(crate) fn current_playlist_vs( items, title: playlist.title().to_string(), duration: get_display_duration(&playlist.duration()), - cover_url: connector_state.serve_asset_url_opt_key(playlist.show_cover_url().clone()), + cover: playlist.show_cover().clone(), }; root.current_playlist = Some(current_playlist_state); } -pub(crate) fn edit_playlist_vs( - (edit_playlist, connector_state): (&EditPlaylistState, &ConnectorState), - root: &mut RootViewModelState, -) { +pub(crate) fn edit_playlist_vs(edit_playlist: &EditPlaylistState, root: &mut RootViewModelState) { root.edit_playlist = Some(VEditPlaylistState { - picture: connector_state.serve_asset_url_opt(edit_playlist.cover.clone()), + picture: edit_playlist + .cover + .clone() + .map(|loc| DataSourceKey::AnyEntry { entry: loc }), name: edit_playlist.playlist_name.clone(), modal_open: edit_playlist.modal_open, }); } pub(crate) fn create_playlist_vs( - (create_playlist, connector_state): (&CreatePlaylistState, &ConnectorState), + create_playlist: &CreatePlaylistState, root: &mut RootViewModelState, ) { let mode = create_playlist.mode; - let cover = connector_state.serve_asset_url_opt(create_playlist.cover.clone()); + let cover = create_playlist.cover.clone(); let music_count = create_playlist.entries.len(); root.create_playlist = Some(VCreatePlaylistState { mode, music_count: music_count as u32, - picture: cover, + picture: cover + .clone() + .map(|loc| DataSourceKey::AnyEntry { entry: loc }), recommend_playlist_names: create_playlist.recommend_playlist_names.clone(), name: decode_component_or_origin(create_playlist.playlist_name.clone()), full_imported: create_playlist.mode == CreatePlaylistMode::Full