mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-08-05 19:59:51 +00:00
Compare commits
1 Commits
192bdebbbc
...
0ead2e6b8a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ead2e6b8a |
96
.github/workflows/spotube-release-binary.yml
vendored
96
.github/workflows/spotube-release-binary.yml
vendored
@ -25,7 +25,6 @@ env:
|
|||||||
COMPOSE_WEBVIEW_REPO: kdroidFilter/ComposeNativeWebview
|
COMPOSE_WEBVIEW_REPO: kdroidFilter/ComposeNativeWebview
|
||||||
COMPOSE_WEBVIEW_COMMIT: 5aeb268a2c37ff4dbfb76924573344a7c4f763ce
|
COMPOSE_WEBVIEW_COMMIT: 5aeb268a2c37ff4dbfb76924573344a7c4f763ce
|
||||||
GRADLE_PLUGIN_REPO: team-spotube/gradle-plugin
|
GRADLE_PLUGIN_REPO: team-spotube/gradle-plugin
|
||||||
VLCJ_BUNDLER_REPO: team-spotube/vlcj-bundler-gradle-plugin
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prepare-deps:
|
prepare-deps:
|
||||||
@ -47,13 +46,6 @@ jobs:
|
|||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
path: gradle-plugin
|
path: gradle-plugin
|
||||||
|
|
||||||
- name: Checkout vlcj-bundler
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
repository: ${{ env.VLCJ_BUNDLER_REPO }}
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
path: vlcj-bundler
|
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
@ -81,30 +73,38 @@ jobs:
|
|||||||
chmod +x gradlew
|
chmod +x gradlew
|
||||||
./gradlew publishToMavenLocal
|
./gradlew publishToMavenLocal
|
||||||
|
|
||||||
- name: Publish vlcj-bundler to mavenLocal
|
- name: Upload compose-webview artifacts
|
||||||
working-directory: vlcj-bundler
|
|
||||||
run: |
|
|
||||||
chmod +x gradlew
|
|
||||||
./gradlew publishToMavenLocal
|
|
||||||
|
|
||||||
- name: Upload maven local
|
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: maven-local
|
name: maven-local-composewebview
|
||||||
path: ~/.m2/repository/
|
path: ~/.m2/repository/io/github/kdroidfilter/
|
||||||
|
|
||||||
|
- name: Upload gradle-plugin artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: maven-local-gradle-plugin
|
||||||
|
path: ~/.m2/repository/dev/krtirtho/
|
||||||
|
|
||||||
build-android:
|
build-android:
|
||||||
name: Android
|
name: Android
|
||||||
needs: prepare-deps
|
needs: prepare-deps
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GRADLE_FLAGS: ${{ inputs.release_type == 'nightly' && '-PversionName=nightly' || '' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download maven local
|
- name: Download compose-webview artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: maven-local
|
name: maven-local-composewebview
|
||||||
path: ~/.m2/repository/
|
path: ~/.m2/repository/io/github/kdroidfilter/
|
||||||
|
|
||||||
|
- name: Download gradle-plugin artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: maven-local-gradle-plugin
|
||||||
|
path: ~/.m2/repository/dev/krtirtho/
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
@ -133,7 +133,7 @@ jobs:
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
- name: Assemble Release
|
- name: Assemble Release
|
||||||
run: ./gradlew :composeApp:assembleRelease
|
run: ./gradlew :composeApp:assembleRelease ${{ env.GRADLE_FLAGS }}
|
||||||
|
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
@ -151,14 +151,22 @@ jobs:
|
|||||||
name: Linux Desktop
|
name: Linux Desktop
|
||||||
needs: prepare-deps
|
needs: prepare-deps
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GRADLE_FLAGS: ${{ inputs.release_type == 'nightly' && '-PversionName=nightly' || '' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download maven local
|
- name: Download compose-webview artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: maven-local
|
name: maven-local-composewebview
|
||||||
path: ~/.m2/repository/
|
path: ~/.m2/repository/io/github/kdroidfilter/
|
||||||
|
|
||||||
|
- name: Download gradle-plugin artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: maven-local-gradle-plugin
|
||||||
|
path: ~/.m2/repository/dev/krtirtho/
|
||||||
|
|
||||||
- name: Install packaging tools
|
- name: Install packaging tools
|
||||||
run: |
|
run: |
|
||||||
@ -178,13 +186,13 @@ jobs:
|
|||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
|
|
||||||
- name: Package DEB
|
- name: Package DEB
|
||||||
run: ./gradlew :composeApp:packageReleaseDeb
|
run: ./gradlew :composeApp:packageReleaseDeb ${{ env.GRADLE_FLAGS }}
|
||||||
|
|
||||||
- name: Package RPM
|
- name: Package RPM
|
||||||
run: ./gradlew :composeApp:packageReleaseRpm
|
run: ./gradlew :composeApp:packageReleaseRpm ${{ env.GRADLE_FLAGS }}
|
||||||
|
|
||||||
- name: Package AppImage
|
- name: Package AppImage
|
||||||
run: ./gradlew :composeApp:packageReleaseAppImage
|
run: ./gradlew :composeApp:packageReleaseAppImage ${{ env.GRADLE_FLAGS }}
|
||||||
|
|
||||||
- name: Upload DEB
|
- name: Upload DEB
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
@ -208,14 +216,22 @@ jobs:
|
|||||||
name: Windows Desktop
|
name: Windows Desktop
|
||||||
needs: prepare-deps
|
needs: prepare-deps
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
env:
|
||||||
|
GRADLE_FLAGS: ${{ inputs.release_type == 'nightly' && '-PversionName=nightly' || '' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download maven local
|
- name: Download compose-webview artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: maven-local
|
name: maven-local-composewebview
|
||||||
path: ~/.m2/repository/
|
path: ~/.m2/repository/io/github/kdroidfilter/
|
||||||
|
|
||||||
|
- name: Download gradle-plugin artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: maven-local-gradle-plugin
|
||||||
|
path: ~/.m2/repository/dev/krtirtho/
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
@ -227,10 +243,10 @@ jobs:
|
|||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v4
|
||||||
|
|
||||||
- name: Package MSI
|
- name: Package MSI
|
||||||
run: ./gradlew :composeApp:packageReleaseMsi
|
run: ./gradlew :composeApp:packageReleaseMsi ${{ env.GRADLE_FLAGS }}
|
||||||
|
|
||||||
- name: Package EXE
|
- name: Package EXE
|
||||||
run: ./gradlew :composeApp:packageReleaseExe
|
run: ./gradlew :composeApp:packageReleaseExe ${{ env.GRADLE_FLAGS }}
|
||||||
|
|
||||||
- name: Upload MSI
|
- name: Upload MSI
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
@ -248,14 +264,22 @@ jobs:
|
|||||||
name: macOS Desktop
|
name: macOS Desktop
|
||||||
needs: prepare-deps
|
needs: prepare-deps
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
|
env:
|
||||||
|
GRADLE_FLAGS: ${{ inputs.release_type == 'nightly' && '-PversionName=nightly' || '' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download maven local
|
- name: Download compose-webview artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: maven-local
|
name: maven-local-composewebview
|
||||||
path: ~/.m2/repository/
|
path: ~/.m2/repository/io/github/kdroidfilter/
|
||||||
|
|
||||||
|
- name: Download gradle-plugin artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: maven-local-gradle-plugin
|
||||||
|
path: ~/.m2/repository/dev/krtirtho/
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
@ -270,7 +294,7 @@ jobs:
|
|||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
|
|
||||||
- name: Package DMG
|
- name: Package DMG
|
||||||
run: ./gradlew :composeApp:packageReleaseDmg
|
run: ./gradlew :composeApp:packageReleaseDmg ${{ env.GRADLE_FLAGS }}
|
||||||
|
|
||||||
- name: Upload DMG
|
- name: Upload DMG
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
96
assets/fonts/Ubuntu_Mono/UFL.txt
Normal file
96
assets/fonts/Ubuntu_Mono/UFL.txt
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
-------------------------------
|
||||||
|
UBUNTU FONT LICENCE Version 1.0
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
This licence allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely. The fonts, including any derivative works, can be
|
||||||
|
bundled, embedded, and redistributed provided the terms of this licence
|
||||||
|
are met. The fonts and derivatives, however, cannot be released under
|
||||||
|
any other licence. The requirement for fonts to remain under this
|
||||||
|
licence does not require any document created using the fonts or their
|
||||||
|
derivatives to be published under this licence, as long as the primary
|
||||||
|
purpose of the document is not to be a vehicle for the distribution of
|
||||||
|
the fonts.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this licence and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components
|
||||||
|
as received under this licence.
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to
|
||||||
|
a new environment.
|
||||||
|
|
||||||
|
"Copyright Holder(s)" refers to all individuals and companies who have a
|
||||||
|
copyright ownership of the Font Software.
|
||||||
|
|
||||||
|
"Substantially Changed" refers to Modified Versions which can be easily
|
||||||
|
identified as dissimilar to the Font Software by users of the Font
|
||||||
|
Software comparing the Original Version with the Modified Version.
|
||||||
|
|
||||||
|
To "Propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification and with or without charging
|
||||||
|
a redistribution fee), making available to the public, and in some
|
||||||
|
countries other activities as well.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
This licence does not grant any rights under trademark law and all such
|
||||||
|
rights are reserved.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of the Font Software, to propagate the Font Software, subject to
|
||||||
|
the below conditions:
|
||||||
|
|
||||||
|
1) Each copy of the Font Software must contain the above copyright
|
||||||
|
notice and this licence. These can be included either as stand-alone
|
||||||
|
text files, human-readable headers or in the appropriate machine-
|
||||||
|
readable metadata fields within text or binary files as long as those
|
||||||
|
fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
2) The font name complies with the following:
|
||||||
|
(a) The Original Version must retain its name, unmodified.
|
||||||
|
(b) Modified Versions which are Substantially Changed must be renamed to
|
||||||
|
avoid use of the name of the Original Version or similar names entirely.
|
||||||
|
(c) Modified Versions which are not Substantially Changed must be
|
||||||
|
renamed to both (i) retain the name of the Original Version and (ii) add
|
||||||
|
additional naming elements to distinguish the Modified Version from the
|
||||||
|
Original Version. The name of such Modified Versions must be the name of
|
||||||
|
the Original Version, with "derivative X" where X represents the name of
|
||||||
|
the new work, appended to that name.
|
||||||
|
|
||||||
|
3) The name(s) of the Copyright Holder(s) and any contributor to the
|
||||||
|
Font Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except (i) as required by this licence, (ii) to
|
||||||
|
acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
|
||||||
|
their explicit written permission.
|
||||||
|
|
||||||
|
4) The Font Software, modified or unmodified, in part or in whole, must
|
||||||
|
be distributed entirely under this licence, and must not be distributed
|
||||||
|
under any other licence. The requirement for fonts to remain under this
|
||||||
|
licence does not affect any document created using the Font Software,
|
||||||
|
except any version of the Font Software extracted from a document
|
||||||
|
created using the Font Software may only be distributed under this
|
||||||
|
licence.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This licence becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
|
||||||
|
COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
|
||||||
|
DEALINGS IN THE FONT SOFTWARE.
|
||||||
BIN
assets/fonts/Ubuntu_Mono/UbuntuMono-Bold.ttf
Normal file
BIN
assets/fonts/Ubuntu_Mono/UbuntuMono-Bold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Ubuntu_Mono/UbuntuMono-BoldItalic.ttf
Normal file
BIN
assets/fonts/Ubuntu_Mono/UbuntuMono-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Ubuntu_Mono/UbuntuMono-Italic.ttf
Normal file
BIN
assets/fonts/Ubuntu_Mono/UbuntuMono-Italic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf
Normal file
BIN
assets/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf
Normal file
Binary file not shown.
@ -57,8 +57,6 @@ class MediaBrowseHelper(
|
|||||||
companion object {
|
companion object {
|
||||||
const val MEDIA_ID_ROOT = "root"
|
const val MEDIA_ID_ROOT = "root"
|
||||||
const val MEDIA_ID_BROWSE = "browse"
|
const val MEDIA_ID_BROWSE = "browse"
|
||||||
const val MEDIA_ID_BROWSE_GENRE = "browse:genre"
|
|
||||||
const val MEDIA_ID_BROWSE_FEATURED = "browse:featured"
|
|
||||||
const val MEDIA_ID_LIBRARY = "library"
|
const val MEDIA_ID_LIBRARY = "library"
|
||||||
const val MEDIA_ID_SAVED_PLAYLISTS = "library:playlists"
|
const val MEDIA_ID_SAVED_PLAYLISTS = "library:playlists"
|
||||||
const val MEDIA_ID_SAVED_ALBUMS = "library:albums"
|
const val MEDIA_ID_SAVED_ALBUMS = "library:albums"
|
||||||
@ -95,11 +93,6 @@ class MediaBrowseHelper(
|
|||||||
return when {
|
return when {
|
||||||
parentId == MEDIA_ID_ROOT -> buildRootChildren()
|
parentId == MEDIA_ID_ROOT -> buildRootChildren()
|
||||||
parentId == MEDIA_ID_BROWSE -> buildBrowseItems()
|
parentId == MEDIA_ID_BROWSE -> buildBrowseItems()
|
||||||
parentId == MEDIA_ID_BROWSE_FEATURED -> buildFeaturedItems()
|
|
||||||
parentId.startsWith("$MEDIA_ID_BROWSE_GENRE:") -> {
|
|
||||||
val genreId = parentId.removePrefix("$MEDIA_ID_BROWSE_GENRE:")
|
|
||||||
buildBrowseGenreItems(genreId)
|
|
||||||
}
|
|
||||||
parentId == MEDIA_ID_LIBRARY -> buildLibraryRootChildren()
|
parentId == MEDIA_ID_LIBRARY -> buildLibraryRootChildren()
|
||||||
parentId == MEDIA_ID_SAVED_PLAYLISTS -> buildSavedPlaylistsItems()
|
parentId == MEDIA_ID_SAVED_PLAYLISTS -> buildSavedPlaylistsItems()
|
||||||
parentId == MEDIA_ID_SAVED_ALBUMS -> buildSavedAlbumsItems()
|
parentId == MEDIA_ID_SAVED_ALBUMS -> buildSavedAlbumsItems()
|
||||||
@ -221,6 +214,7 @@ class MediaBrowseHelper(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun buildRootChildren(): List<MediaItem> {
|
private fun buildRootChildren(): List<MediaItem> {
|
||||||
|
val gridExtras = Bundle().apply { putInt(CONTENT_STYLE_BROWSABLE, CONTENT_STYLE_GRID) }
|
||||||
return listOf(
|
return listOf(
|
||||||
MediaItem.Builder()
|
MediaItem.Builder()
|
||||||
.setMediaId(MEDIA_ID_BROWSE)
|
.setMediaId(MEDIA_ID_BROWSE)
|
||||||
@ -229,6 +223,7 @@ class MediaBrowseHelper(
|
|||||||
.setTitle("Browse")
|
.setTitle("Browse")
|
||||||
.setIsBrowsable(true)
|
.setIsBrowsable(true)
|
||||||
.setIsPlayable(false)
|
.setIsPlayable(false)
|
||||||
|
.setExtras(gridExtras)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.build(),
|
.build(),
|
||||||
@ -256,67 +251,27 @@ class MediaBrowseHelper(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun buildBrowseItems(): List<MediaItem> {
|
private suspend fun buildBrowseItems(): List<MediaItem> {
|
||||||
val gridExtras = Bundle().apply { putInt(CONTENT_STYLE_BROWSABLE, CONTENT_STYLE_GRID) }
|
|
||||||
val items = mutableListOf<MediaItem>()
|
|
||||||
|
|
||||||
items.add(
|
|
||||||
MediaItem.Builder()
|
|
||||||
.setMediaId(MEDIA_ID_BROWSE_FEATURED)
|
|
||||||
.setMediaMetadata(
|
|
||||||
MediaMetadata.Builder()
|
|
||||||
.setTitle("Featured")
|
|
||||||
.setIsBrowsable(true)
|
|
||||||
.setIsPlayable(false)
|
|
||||||
.setExtras(gridExtras)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
val genres = try {
|
|
||||||
homeScreenRepository.genres() ?: emptyList()
|
|
||||||
} catch (_: Exception) {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
for (genre in genres) {
|
|
||||||
items.add(
|
|
||||||
MediaItem.Builder()
|
|
||||||
.setMediaId("$MEDIA_ID_BROWSE_GENRE:${genre.id}")
|
|
||||||
.setMediaMetadata(
|
|
||||||
MediaMetadata.Builder()
|
|
||||||
.setTitle(genre.name)
|
|
||||||
.setIsBrowsable(true)
|
|
||||||
.setIsPlayable(false)
|
|
||||||
.setExtras(gridExtras)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun buildFeaturedItems(): List<MediaItem> {
|
|
||||||
val featured = try {
|
val featured = try {
|
||||||
homeScreenRepository.featuredItems() ?: emptyList()
|
homeScreenRepository.featuredItems() ?: emptyList()
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
return featured.mapNotNull { item -> browseItemToMediaItem(item) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun buildBrowseGenreItems(genreId: String): List<MediaItem> {
|
|
||||||
val sections = try {
|
val sections = try {
|
||||||
homeScreenRepository.list(genreId)?.items ?: emptyList()
|
homeScreenRepository.list()?.items ?: emptyList()
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
val items = mutableListOf<MediaItem>()
|
val items = mutableListOf<MediaItem>()
|
||||||
|
|
||||||
|
if (featured.isNotEmpty()) {
|
||||||
|
for (item in featured) {
|
||||||
|
val mediaItem = browseItemToMediaItem(item) ?: continue
|
||||||
|
items.add(mediaItem.withGroupTitle("Featured"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (section in sections) {
|
for (section in sections) {
|
||||||
for (item in section.items) {
|
for (item in section.items) {
|
||||||
val mediaItem = browseItemToMediaItem(
|
val mediaItem = browseItemToMediaItem(
|
||||||
|
|||||||
@ -47,7 +47,6 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
@ -373,7 +372,7 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun buildCurrentMediaItemList(): List<Media3MediaItem> = withContext(Dispatchers.Main) {
|
private fun buildCurrentMediaItemList(): List<Media3MediaItem> {
|
||||||
val count = audioPlayer.player.mediaItemCount
|
val count = audioPlayer.player.mediaItemCount
|
||||||
val items = mutableListOf<Media3MediaItem>()
|
val items = mutableListOf<Media3MediaItem>()
|
||||||
for (i in 0 until count) {
|
for (i in 0 until count) {
|
||||||
@ -382,7 +381,7 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
|
|||||||
items.add(item)
|
items.add(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@ -125,12 +125,12 @@ private fun resolveTextFieldState(
|
|||||||
)
|
)
|
||||||
|
|
||||||
else -> ResolvedAutoCompleteTextFieldState(
|
else -> ResolvedAutoCompleteTextFieldState(
|
||||||
theme.background.normal,
|
theme.background.pressed,
|
||||||
theme.highlight.normal,
|
theme.highlight.pressed,
|
||||||
theme.shape.normal,
|
theme.shape.pressed,
|
||||||
theme.border.normal,
|
theme.border.pressed,
|
||||||
theme.shadow.normal,
|
theme.shadow.pressed,
|
||||||
theme.foreground.normal
|
theme.foreground.pressed
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -106,11 +106,11 @@ private fun resolveButtonState(
|
|||||||
)
|
)
|
||||||
|
|
||||||
else -> ResolvedButtonState(
|
else -> ResolvedButtonState(
|
||||||
style.colors.normal,
|
style.colors.focused,
|
||||||
style.shape.normal,
|
style.shape.focused,
|
||||||
style.shadow.normal,
|
style.shadow.focused,
|
||||||
style.border.normal,
|
style.border.focused,
|
||||||
style.padding.normal
|
style.padding.focused
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -311,28 +311,21 @@ fun GhostIconButton(
|
|||||||
val outlineStyle = baseTheme.iconButtons.outline
|
val outlineStyle = baseTheme.iconButtons.outline
|
||||||
val ghostStyle = theme ?: BaseUITheme.ButtonStyle(
|
val ghostStyle = theme ?: BaseUITheme.ButtonStyle(
|
||||||
colors = BaseUITheme.InteractionState(
|
colors = BaseUITheme.InteractionState(
|
||||||
normal = BaseUITheme.ButtonColors(
|
|
||||||
background = Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent)),
|
|
||||||
foreground = outlineStyle.colors.normal.foreground,
|
|
||||||
highlight = outlineStyle.colors.normal.highlight,
|
|
||||||
),
|
|
||||||
hovered = outlineStyle.colors.hovered,
|
hovered = outlineStyle.colors.hovered,
|
||||||
pressed = outlineStyle.colors.pressed,
|
pressed = outlineStyle.colors.pressed,
|
||||||
focused = BaseUITheme.ButtonColors(
|
focused = BaseUITheme.ButtonColors(
|
||||||
background = Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent)),
|
background = Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent)),
|
||||||
foreground = outlineStyle.colors.normal.foreground,
|
foreground = outlineStyle.colors.focused.foreground,
|
||||||
highlight = outlineStyle.colors.normal.highlight,
|
highlight = outlineStyle.colors.focused.highlight,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
shape = outlineStyle.shape,
|
shape = outlineStyle.shape,
|
||||||
shadow = BaseUITheme.InteractionState(
|
shadow = BaseUITheme.InteractionState(
|
||||||
normal = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent),
|
|
||||||
hovered = outlineStyle.shadow.hovered,
|
hovered = outlineStyle.shadow.hovered,
|
||||||
pressed = outlineStyle.shadow.pressed,
|
pressed = outlineStyle.shadow.pressed,
|
||||||
focused = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent),
|
focused = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent),
|
||||||
),
|
),
|
||||||
border = BaseUITheme.InteractionState(
|
border = BaseUITheme.InteractionState(
|
||||||
normal = BaseUITheme.Border(Color.Transparent, 0.dp),
|
|
||||||
hovered = outlineStyle.border.hovered,
|
hovered = outlineStyle.border.hovered,
|
||||||
pressed = outlineStyle.border.pressed,
|
pressed = outlineStyle.border.pressed,
|
||||||
focused = BaseUITheme.Border(Color.Transparent, 0.dp),
|
focused = BaseUITheme.Border(Color.Transparent, 0.dp),
|
||||||
|
|||||||
@ -85,7 +85,7 @@ private fun resolveCheckBoxState(
|
|||||||
return when {
|
return when {
|
||||||
isPressed -> ResolvedCheckBoxState(style.colors.pressed, style.shape.pressed, style.shadow.pressed, style.border.pressed)
|
isPressed -> ResolvedCheckBoxState(style.colors.pressed, style.shape.pressed, style.shadow.pressed, style.border.pressed)
|
||||||
isHovered -> ResolvedCheckBoxState(style.colors.hovered, style.shape.hovered, style.shadow.hovered, style.border.hovered)
|
isHovered -> ResolvedCheckBoxState(style.colors.hovered, style.shape.hovered, style.shadow.hovered, style.border.hovered)
|
||||||
else -> ResolvedCheckBoxState(style.colors.normal, style.shape.normal, style.shadow.normal, style.border.normal)
|
else -> ResolvedCheckBoxState(style.colors.focused, style.shape.focused, style.shadow.focused, style.border.focused)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -70,7 +70,7 @@ private fun resolveChipState(
|
|||||||
return when {
|
return when {
|
||||||
isPressed -> ResolvedChipState(style.colors.pressed, style.shape.pressed, style.shadow.pressed, style.border.pressed, style.padding.pressed)
|
isPressed -> ResolvedChipState(style.colors.pressed, style.shape.pressed, style.shadow.pressed, style.border.pressed, style.padding.pressed)
|
||||||
isHovered -> ResolvedChipState(style.colors.hovered, style.shape.hovered, style.shadow.hovered, style.border.hovered, style.padding.hovered)
|
isHovered -> ResolvedChipState(style.colors.hovered, style.shape.hovered, style.shadow.hovered, style.border.hovered, style.padding.hovered)
|
||||||
else -> ResolvedChipState(style.colors.normal, style.shape.normal, style.shadow.normal, style.border.normal, style.padding.normal)
|
else -> ResolvedChipState(style.colors.focused, style.shape.focused, style.shadow.focused, style.border.focused, style.padding.focused)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,132 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package dev.krtirtho.spotube.core.ui.base
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
|
||||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
|
||||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.draw.shadow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ListRowTile(
|
|
||||||
selected: Boolean = false,
|
|
||||||
enabled: Boolean = true,
|
|
||||||
onClick: (() -> Unit)? = null,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
leading: @Composable (() -> Unit)? = null,
|
|
||||||
title: @Composable (() -> Unit)? = null,
|
|
||||||
subtitle: @Composable (() -> Unit)? = null,
|
|
||||||
trailing: @Composable (() -> Unit)? = null,
|
|
||||||
) {
|
|
||||||
val rowTheme = LocalBaseUITheme.current.listRowTile
|
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
|
||||||
val isPressed by interactionSource.collectIsPressedAsState()
|
|
||||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
|
||||||
val isFocused by interactionSource.collectIsFocusedAsState()
|
|
||||||
|
|
||||||
val background = when {
|
|
||||||
isPressed -> rowTheme.background.pressed
|
|
||||||
isHovered -> rowTheme.background.hovered
|
|
||||||
isFocused -> rowTheme.background.focused
|
|
||||||
selected -> rowTheme.background.selected
|
|
||||||
else -> rowTheme.background.normal
|
|
||||||
}
|
|
||||||
val shape = when {
|
|
||||||
isPressed -> rowTheme.shape.pressed
|
|
||||||
isHovered -> rowTheme.shape.hovered
|
|
||||||
isFocused -> rowTheme.shape.focused
|
|
||||||
selected -> rowTheme.shape.selected
|
|
||||||
else -> rowTheme.shape.normal
|
|
||||||
}
|
|
||||||
val borderDef = when {
|
|
||||||
isPressed -> rowTheme.border.pressed
|
|
||||||
isHovered -> rowTheme.border.hovered
|
|
||||||
isFocused -> rowTheme.border.focused
|
|
||||||
selected -> rowTheme.border.selected
|
|
||||||
else -> rowTheme.border.normal
|
|
||||||
}
|
|
||||||
val shadowDef = when {
|
|
||||||
isPressed -> rowTheme.shadow.pressed
|
|
||||||
isHovered -> rowTheme.shadow.hovered
|
|
||||||
isFocused -> rowTheme.shadow.focused
|
|
||||||
selected -> rowTheme.shadow.selected
|
|
||||||
else -> rowTheme.shadow.normal
|
|
||||||
}
|
|
||||||
|
|
||||||
val mod = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.shadow(shadowDef.elevation, shape, shadowDef.clip, shadowDef.ambientColor, shadowDef.spotColor)
|
|
||||||
.clip(shape)
|
|
||||||
.border(borderDef.width, borderDef.color, shape)
|
|
||||||
.background(background, shape)
|
|
||||||
.then(
|
|
||||||
if (onClick != null) {
|
|
||||||
Modifier.clickable(
|
|
||||||
enabled = enabled,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
indication = null,
|
|
||||||
onClick = onClick,
|
|
||||||
)
|
|
||||||
} else Modifier
|
|
||||||
)
|
|
||||||
.padding(rowTheme.padding)
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = mod,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
if (leading != null) {
|
|
||||||
leading()
|
|
||||||
Spacer(Modifier.width(12.dp))
|
|
||||||
}
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
) {
|
|
||||||
if (title != null) {
|
|
||||||
title()
|
|
||||||
}
|
|
||||||
if (subtitle != null) {
|
|
||||||
Spacer(Modifier.width(2.dp))
|
|
||||||
subtitle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (trailing != null) {
|
|
||||||
Spacer(Modifier.width(12.dp))
|
|
||||||
trailing()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -95,22 +95,22 @@ fun Slider(
|
|||||||
val stateColor = when {
|
val stateColor = when {
|
||||||
isPressed -> sliderTheme.trackActiveColor.pressed
|
isPressed -> sliderTheme.trackActiveColor.pressed
|
||||||
isHovered -> sliderTheme.trackActiveColor.hovered
|
isHovered -> sliderTheme.trackActiveColor.hovered
|
||||||
else -> sliderTheme.trackActiveColor.normal
|
else -> sliderTheme.trackActiveColor.focused
|
||||||
}
|
}
|
||||||
val inactiveColor = when {
|
val inactiveColor = when {
|
||||||
isPressed -> sliderTheme.trackInactiveColor.pressed
|
isPressed -> sliderTheme.trackInactiveColor.pressed
|
||||||
isHovered -> sliderTheme.trackInactiveColor.hovered
|
isHovered -> sliderTheme.trackInactiveColor.hovered
|
||||||
else -> sliderTheme.trackInactiveColor.normal
|
else -> sliderTheme.trackInactiveColor.focused
|
||||||
}
|
}
|
||||||
val thumbColor = when {
|
val thumbColor = when {
|
||||||
isPressed -> sliderTheme.thumbColor.pressed
|
isPressed -> sliderTheme.thumbColor.pressed
|
||||||
isHovered -> sliderTheme.thumbColor.hovered
|
isHovered -> sliderTheme.thumbColor.hovered
|
||||||
else -> sliderTheme.thumbColor.normal
|
else -> sliderTheme.thumbColor.focused
|
||||||
}
|
}
|
||||||
val thumbShadowState = when {
|
val thumbShadowState = when {
|
||||||
isPressed -> sliderTheme.thumbShadow.pressed
|
isPressed -> sliderTheme.thumbShadow.pressed
|
||||||
isHovered -> sliderTheme.thumbShadow.hovered
|
isHovered -> sliderTheme.thumbShadow.hovered
|
||||||
else -> sliderTheme.thumbShadow.normal
|
else -> sliderTheme.thumbShadow.focused
|
||||||
}
|
}
|
||||||
|
|
||||||
val trackActiveColor = if (enabled) stateColor else stateColor.copy(alpha = 0.38f)
|
val trackActiveColor = if (enabled) stateColor else stateColor.copy(alpha = 0.38f)
|
||||||
|
|||||||
@ -81,7 +81,7 @@ private fun resolveTextFieldState(
|
|||||||
return when {
|
return when {
|
||||||
isFocused -> ResolvedTextFieldState(theme.background.focused, theme.highlight.focused, theme.shape.focused, theme.border.focused, theme.shadow.focused, theme.foreground.focused)
|
isFocused -> ResolvedTextFieldState(theme.background.focused, theme.highlight.focused, theme.shape.focused, theme.border.focused, theme.shadow.focused, theme.foreground.focused)
|
||||||
isHovered -> ResolvedTextFieldState(theme.background.hovered, theme.highlight.hovered, theme.shape.hovered, theme.border.hovered, theme.shadow.hovered, theme.foreground.hovered)
|
isHovered -> ResolvedTextFieldState(theme.background.hovered, theme.highlight.hovered, theme.shape.hovered, theme.border.hovered, theme.shadow.hovered, theme.foreground.hovered)
|
||||||
else -> ResolvedTextFieldState(theme.background.normal, theme.highlight.normal, theme.shape.normal, theme.border.normal, theme.shadow.normal, theme.foreground.normal)
|
else -> ResolvedTextFieldState(theme.background.pressed, theme.highlight.pressed, theme.shape.pressed, theme.border.pressed, theme.shadow.pressed, theme.foreground.pressed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -54,21 +54,19 @@ data class BaseUITheme(
|
|||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
data class InteractionState<T>(
|
data class InteractionState<T>(
|
||||||
val normal: T,
|
|
||||||
val hovered: T,
|
val hovered: T,
|
||||||
val pressed: T,
|
val pressed: T,
|
||||||
val focused: T,
|
val focused: T,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun <T> fromSingleValue(value: T): InteractionState<T> {
|
fun <T> fromSingleValue(value: T): InteractionState<T> {
|
||||||
return InteractionState(value, value, value, value)
|
return InteractionState(value, value, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
data class AdvancedInteractionState<T>(
|
data class AdvancedInteractionState<T>(
|
||||||
val normal: T,
|
|
||||||
val hovered: T,
|
val hovered: T,
|
||||||
val pressed: T,
|
val pressed: T,
|
||||||
val focused: T,
|
val focused: T,
|
||||||
@ -77,7 +75,7 @@ data class BaseUITheme(
|
|||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun <T> fromSingleValue(value: T): AdvancedInteractionState<T> {
|
fun <T> fromSingleValue(value: T): AdvancedInteractionState<T> {
|
||||||
return AdvancedInteractionState(value, value, value, value, value, value)
|
return AdvancedInteractionState(value, value, value, value, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -202,30 +200,23 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
|
|
||||||
val defaultShape = RoundedCornerShape(14.dp)
|
val defaultShape = RoundedCornerShape(14.dp)
|
||||||
val defaultShapeState =
|
val defaultShapeState =
|
||||||
BaseUITheme.InteractionState<Shape>(defaultShape, defaultShape, defaultShape, defaultShape)
|
BaseUITheme.InteractionState<Shape>(defaultShape, defaultShape, defaultShape)
|
||||||
val noBorder = BaseUITheme.Border(Color.Transparent, 0.dp)
|
val noBorder = BaseUITheme.Border(Color.Transparent, 0.dp)
|
||||||
val noBorderState = BaseUITheme.InteractionState(noBorder, noBorder, noBorder, noBorder)
|
val noBorderState = BaseUITheme.InteractionState(noBorder, noBorder, noBorder)
|
||||||
val noShadow = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent)
|
val noShadow = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent)
|
||||||
val noShadowState = BaseUITheme.InteractionState(noShadow, noShadow, noShadow, noShadow)
|
val noShadowState = BaseUITheme.InteractionState(noShadow, noShadow, noShadow)
|
||||||
val defaultButtonPadding = BaseUITheme.InteractionState(
|
val defaultButtonPadding = BaseUITheme.InteractionState(
|
||||||
PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
||||||
PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
||||||
PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
||||||
PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
|
||||||
)
|
)
|
||||||
val iconButtonPadding = BaseUITheme.InteractionState(
|
val iconButtonPadding = BaseUITheme.InteractionState(
|
||||||
PaddingValues(8.dp),
|
PaddingValues(8.dp),
|
||||||
PaddingValues(8.dp),
|
PaddingValues(8.dp),
|
||||||
PaddingValues(8.dp),
|
PaddingValues(8.dp),
|
||||||
PaddingValues(8.dp),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val outlineColors = BaseUITheme.InteractionState(
|
val outlineColors = BaseUITheme.InteractionState(
|
||||||
normal = BaseUITheme.ButtonColors(
|
|
||||||
background = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
|
||||||
foreground = scheme.onSurface,
|
|
||||||
highlight = highlight,
|
|
||||||
),
|
|
||||||
hovered = BaseUITheme.ButtonColors(
|
hovered = BaseUITheme.ButtonColors(
|
||||||
background = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
background = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||||
foreground = scheme.onSurface,
|
foreground = scheme.onSurface,
|
||||||
@ -243,12 +234,6 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
val outlineShadow = BaseUITheme.InteractionState(
|
val outlineShadow = BaseUITheme.InteractionState(
|
||||||
normal = BaseUITheme.Shadow(
|
|
||||||
6.dp,
|
|
||||||
true,
|
|
||||||
shadowColor.copy(alpha = 0.15f),
|
|
||||||
shadowColor.copy(alpha = 0.18f)
|
|
||||||
),
|
|
||||||
hovered = BaseUITheme.Shadow(
|
hovered = BaseUITheme.Shadow(
|
||||||
9.dp,
|
9.dp,
|
||||||
true,
|
true,
|
||||||
@ -269,18 +254,12 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
val outlineBorder = BaseUITheme.InteractionState(
|
val outlineBorder = BaseUITheme.InteractionState(
|
||||||
normal = BaseUITheme.Border(border, 0.5.dp),
|
|
||||||
hovered = BaseUITheme.Border(border, 0.5.dp),
|
hovered = BaseUITheme.Border(border, 0.5.dp),
|
||||||
pressed = BaseUITheme.Border(border.copy(alpha = 0.7f), 0.5.dp),
|
pressed = BaseUITheme.Border(border.copy(alpha = 0.7f), 0.5.dp),
|
||||||
focused = BaseUITheme.Border(border, 0.5.dp),
|
focused = BaseUITheme.Border(border, 0.5.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
val primaryColors = BaseUITheme.InteractionState(
|
val primaryColors = BaseUITheme.InteractionState(
|
||||||
normal = BaseUITheme.ButtonColors(
|
|
||||||
background = Brush.verticalGradient(listOf(scheme.primary, scheme.primary)),
|
|
||||||
foreground = scheme.onPrimary,
|
|
||||||
highlight = Color.White.copy(alpha = 0.25f),
|
|
||||||
),
|
|
||||||
hovered = BaseUITheme.ButtonColors(
|
hovered = BaseUITheme.ButtonColors(
|
||||||
background = Brush.verticalGradient(listOf(scheme.primary, scheme.primary)),
|
background = Brush.verticalGradient(listOf(scheme.primary, scheme.primary)),
|
||||||
foreground = scheme.onPrimary,
|
foreground = scheme.onPrimary,
|
||||||
@ -303,12 +282,6 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
val primaryShadow = BaseUITheme.InteractionState(
|
val primaryShadow = BaseUITheme.InteractionState(
|
||||||
normal = BaseUITheme.Shadow(
|
|
||||||
6.dp,
|
|
||||||
true,
|
|
||||||
scheme.primary.copy(alpha = 0.35f),
|
|
||||||
scheme.primary.copy(alpha = 0.4f)
|
|
||||||
),
|
|
||||||
hovered = BaseUITheme.Shadow(
|
hovered = BaseUITheme.Shadow(
|
||||||
9.dp,
|
9.dp,
|
||||||
true,
|
true,
|
||||||
@ -329,23 +302,12 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
val primaryBorder = BaseUITheme.InteractionState(
|
val primaryBorder = BaseUITheme.InteractionState(
|
||||||
normal = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
|
||||||
hovered = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
hovered = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
||||||
pressed = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
pressed = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
||||||
focused = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
focused = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
val secondaryColors = BaseUITheme.InteractionState(
|
val secondaryColors = BaseUITheme.InteractionState(
|
||||||
normal = BaseUITheme.ButtonColors(
|
|
||||||
background = Brush.verticalGradient(
|
|
||||||
listOf(
|
|
||||||
secondaryContainer,
|
|
||||||
secondaryContainer.copy(alpha = if (secondaryContainer.luminance() > 0.5f) 0.92f else 1f)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
foreground = scheme.onSecondaryContainer,
|
|
||||||
highlight = secondaryHighlight,
|
|
||||||
),
|
|
||||||
hovered = BaseUITheme.ButtonColors(
|
hovered = BaseUITheme.ButtonColors(
|
||||||
background = Brush.verticalGradient(
|
background = Brush.verticalGradient(
|
||||||
listOf(
|
listOf(
|
||||||
@ -378,12 +340,6 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
val secondaryShadow = BaseUITheme.InteractionState(
|
val secondaryShadow = BaseUITheme.InteractionState(
|
||||||
normal = BaseUITheme.Shadow(
|
|
||||||
6.dp,
|
|
||||||
true,
|
|
||||||
shadowColor.copy(alpha = 0.15f),
|
|
||||||
shadowColor.copy(alpha = 0.18f)
|
|
||||||
),
|
|
||||||
hovered = BaseUITheme.Shadow(
|
hovered = BaseUITheme.Shadow(
|
||||||
9.dp,
|
9.dp,
|
||||||
true,
|
true,
|
||||||
@ -404,18 +360,12 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
val secondaryBorder = BaseUITheme.InteractionState(
|
val secondaryBorder = BaseUITheme.InteractionState(
|
||||||
normal = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp),
|
|
||||||
hovered = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp),
|
hovered = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp),
|
||||||
pressed = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp),
|
pressed = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp),
|
||||||
focused = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp),
|
focused = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
val ghostColors = BaseUITheme.InteractionState(
|
val ghostColors = BaseUITheme.InteractionState(
|
||||||
normal = BaseUITheme.ButtonColors(
|
|
||||||
background = Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent)),
|
|
||||||
foreground = scheme.onSurface,
|
|
||||||
highlight = Color.Transparent,
|
|
||||||
),
|
|
||||||
hovered = BaseUITheme.ButtonColors(
|
hovered = BaseUITheme.ButtonColors(
|
||||||
background = Brush.verticalGradient(
|
background = Brush.verticalGradient(
|
||||||
listOf(
|
listOf(
|
||||||
@ -497,7 +447,6 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val textFieldForeground = BaseUITheme.InteractionState(
|
val textFieldForeground = BaseUITheme.InteractionState(
|
||||||
normal = scheme.onSurface,
|
|
||||||
hovered = scheme.onSurface,
|
hovered = scheme.onSurface,
|
||||||
pressed = scheme.onSurface,
|
pressed = scheme.onSurface,
|
||||||
focused = scheme.onSurface,
|
focused = scheme.onSurface,
|
||||||
@ -505,18 +454,17 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
|
|
||||||
val checkBoxShape = RoundedCornerShape(6.dp)
|
val checkBoxShape = RoundedCornerShape(6.dp)
|
||||||
val checkBoxShapeState =
|
val checkBoxShapeState =
|
||||||
BaseUITheme.InteractionState<Shape>(checkBoxShape, checkBoxShape, checkBoxShape, checkBoxShape)
|
BaseUITheme.InteractionState<Shape>(checkBoxShape, checkBoxShape, checkBoxShape)
|
||||||
val noPadding =
|
val noPadding =
|
||||||
BaseUITheme.InteractionState(PaddingValues(0.dp), PaddingValues(0.dp), PaddingValues(0.dp), PaddingValues(0.dp))
|
BaseUITheme.InteractionState(PaddingValues(0.dp), PaddingValues(0.dp), PaddingValues(0.dp))
|
||||||
|
|
||||||
val chipTabShape = RoundedCornerShape(10.dp)
|
val chipTabShape = RoundedCornerShape(10.dp)
|
||||||
val chipTabShapeState =
|
val chipTabShapeState =
|
||||||
BaseUITheme.InteractionState<Shape>(chipTabShape, chipTabShape, chipTabShape, chipTabShape)
|
BaseUITheme.InteractionState<Shape>(chipTabShape, chipTabShape, chipTabShape)
|
||||||
val chipTabPadding = BaseUITheme.InteractionState(
|
val chipTabPadding = BaseUITheme.InteractionState(
|
||||||
PaddingValues(horizontal = 14.dp, vertical = 4.dp),
|
PaddingValues(horizontal = 14.dp, vertical = 4.dp),
|
||||||
PaddingValues(horizontal = 14.dp, vertical = 4.dp),
|
PaddingValues(horizontal = 14.dp, vertical = 4.dp),
|
||||||
PaddingValues(horizontal = 14.dp, vertical = 4.dp),
|
PaddingValues(horizontal = 14.dp, vertical = 4.dp),
|
||||||
PaddingValues(horizontal = 14.dp, vertical = 4.dp),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return BaseUITheme(
|
return BaseUITheme(
|
||||||
@ -534,36 +482,26 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
),
|
),
|
||||||
textField = TextFieldTheme(
|
textField = TextFieldTheme(
|
||||||
background = BaseUITheme.InteractionState(
|
background = BaseUITheme.InteractionState(
|
||||||
normal = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
|
||||||
hovered = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
hovered = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||||
pressed = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
pressed = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||||
focused = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
focused = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||||
),
|
),
|
||||||
highlight = BaseUITheme.InteractionState(
|
highlight = BaseUITheme.InteractionState(
|
||||||
normal = highlight,
|
|
||||||
hovered = highlight,
|
hovered = highlight,
|
||||||
pressed = highlight,
|
pressed = highlight,
|
||||||
focused = highlight,
|
focused = highlight,
|
||||||
),
|
),
|
||||||
shape = BaseUITheme.InteractionState(
|
shape = BaseUITheme.InteractionState(
|
||||||
normal = defaultShape,
|
|
||||||
hovered = defaultShape,
|
hovered = defaultShape,
|
||||||
pressed = defaultShape,
|
pressed = defaultShape,
|
||||||
focused = defaultShape,
|
focused = defaultShape,
|
||||||
),
|
),
|
||||||
border = BaseUITheme.InteractionState(
|
border = BaseUITheme.InteractionState(
|
||||||
normal = BaseUITheme.Border(border.copy(alpha = 0.85f), 0.5.dp),
|
|
||||||
hovered = BaseUITheme.Border(border.copy(alpha = 0.85f), 0.5.dp),
|
hovered = BaseUITheme.Border(border.copy(alpha = 0.85f), 0.5.dp),
|
||||||
pressed = BaseUITheme.Border(border, 0.5.dp),
|
pressed = BaseUITheme.Border(border, 0.5.dp),
|
||||||
focused = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
focused = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
||||||
),
|
),
|
||||||
shadow = BaseUITheme.InteractionState(
|
shadow = BaseUITheme.InteractionState(
|
||||||
normal = BaseUITheme.Shadow(
|
|
||||||
9.dp,
|
|
||||||
true,
|
|
||||||
shadowColor.copy(alpha = 0.22f),
|
|
||||||
shadowColor.copy(alpha = 0.26f)
|
|
||||||
),
|
|
||||||
hovered = BaseUITheme.Shadow(
|
hovered = BaseUITheme.Shadow(
|
||||||
9.dp,
|
9.dp,
|
||||||
true,
|
true,
|
||||||
@ -602,30 +540,21 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
),
|
),
|
||||||
slider = SliderTheme(
|
slider = SliderTheme(
|
||||||
trackActiveColor = BaseUITheme.InteractionState(
|
trackActiveColor = BaseUITheme.InteractionState(
|
||||||
normal = scheme.primary,
|
|
||||||
hovered = scheme.primary,
|
hovered = scheme.primary,
|
||||||
pressed = scheme.primary,
|
pressed = scheme.primary,
|
||||||
focused = scheme.primary,
|
focused = scheme.primary,
|
||||||
),
|
),
|
||||||
trackInactiveColor = BaseUITheme.InteractionState(
|
trackInactiveColor = BaseUITheme.InteractionState(
|
||||||
normal = border.copy(alpha = 0.6f),
|
|
||||||
hovered = border.copy(alpha = 0.6f),
|
hovered = border.copy(alpha = 0.6f),
|
||||||
pressed = border.copy(alpha = 0.6f),
|
pressed = border.copy(alpha = 0.6f),
|
||||||
focused = border.copy(alpha = 0.6f),
|
focused = border.copy(alpha = 0.6f),
|
||||||
),
|
),
|
||||||
thumbColor = BaseUITheme.InteractionState(
|
thumbColor = BaseUITheme.InteractionState(
|
||||||
normal = scheme.primary,
|
|
||||||
hovered = scheme.primary,
|
hovered = scheme.primary,
|
||||||
pressed = scheme.primary,
|
pressed = scheme.primary,
|
||||||
focused = scheme.primary,
|
focused = scheme.primary,
|
||||||
),
|
),
|
||||||
thumbShadow = BaseUITheme.InteractionState(
|
thumbShadow = BaseUITheme.InteractionState(
|
||||||
normal = BaseUITheme.Shadow(
|
|
||||||
5.dp,
|
|
||||||
true,
|
|
||||||
scheme.primary.copy(alpha = 0.35f),
|
|
||||||
scheme.primary.copy(alpha = 0.4f)
|
|
||||||
),
|
|
||||||
hovered = BaseUITheme.Shadow(
|
hovered = BaseUITheme.Shadow(
|
||||||
8.dp,
|
8.dp,
|
||||||
true,
|
true,
|
||||||
@ -653,12 +582,6 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
colors = primaryColors,
|
colors = primaryColors,
|
||||||
shape = checkBoxShapeState,
|
shape = checkBoxShapeState,
|
||||||
shadow = BaseUITheme.InteractionState(
|
shadow = BaseUITheme.InteractionState(
|
||||||
normal = BaseUITheme.Shadow(
|
|
||||||
6.dp,
|
|
||||||
true,
|
|
||||||
scheme.primary.copy(alpha = 0.3f),
|
|
||||||
scheme.primary.copy(alpha = 0.35f)
|
|
||||||
),
|
|
||||||
hovered = BaseUITheme.Shadow(
|
hovered = BaseUITheme.Shadow(
|
||||||
8.dp,
|
8.dp,
|
||||||
true,
|
true,
|
||||||
@ -685,12 +608,6 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
colors = outlineColors,
|
colors = outlineColors,
|
||||||
shape = checkBoxShapeState,
|
shape = checkBoxShapeState,
|
||||||
shadow = BaseUITheme.InteractionState(
|
shadow = BaseUITheme.InteractionState(
|
||||||
normal = BaseUITheme.Shadow(
|
|
||||||
3.dp,
|
|
||||||
true,
|
|
||||||
shadowColor.copy(alpha = 0.15f),
|
|
||||||
shadowColor.copy(alpha = 0.18f)
|
|
||||||
),
|
|
||||||
hovered = BaseUITheme.Shadow(
|
hovered = BaseUITheme.Shadow(
|
||||||
5.dp,
|
5.dp,
|
||||||
true,
|
true,
|
||||||
@ -745,7 +662,6 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
),
|
),
|
||||||
listRowTile = BaseUITheme.ListRowTheme(
|
listRowTile = BaseUITheme.ListRowTheme(
|
||||||
background = BaseUITheme.AdvancedInteractionState(
|
background = BaseUITheme.AdvancedInteractionState(
|
||||||
normal = Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent)),
|
|
||||||
hovered = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
hovered = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||||
pressed = Brush.verticalGradient(listOf(containerPressed, containerPressed)),
|
pressed = Brush.verticalGradient(listOf(containerPressed, containerPressed)),
|
||||||
focused = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
focused = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||||
@ -763,7 +679,6 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
shape = BaseUITheme.AdvancedInteractionState(
|
shape = BaseUITheme.AdvancedInteractionState(
|
||||||
normal = RoundedCornerShape(8.dp),
|
|
||||||
hovered = RoundedCornerShape(8.dp),
|
hovered = RoundedCornerShape(8.dp),
|
||||||
pressed = RoundedCornerShape(8.dp),
|
pressed = RoundedCornerShape(8.dp),
|
||||||
focused = RoundedCornerShape(8.dp),
|
focused = RoundedCornerShape(8.dp),
|
||||||
@ -771,7 +686,6 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
disabled = RoundedCornerShape(8.dp),
|
disabled = RoundedCornerShape(8.dp),
|
||||||
),
|
),
|
||||||
border = BaseUITheme.AdvancedInteractionState(
|
border = BaseUITheme.AdvancedInteractionState(
|
||||||
normal = BaseUITheme.Border(Color.Transparent, 0.dp),
|
|
||||||
hovered = BaseUITheme.Border(Color.Transparent, 0.dp),
|
hovered = BaseUITheme.Border(Color.Transparent, 0.dp),
|
||||||
pressed = BaseUITheme.Border(Color.Transparent, 0.dp),
|
pressed = BaseUITheme.Border(Color.Transparent, 0.dp),
|
||||||
focused = BaseUITheme.Border(Color.Transparent, 0.dp),
|
focused = BaseUITheme.Border(Color.Transparent, 0.dp),
|
||||||
@ -779,7 +693,6 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
disabled = BaseUITheme.Border(Color.Transparent, 0.dp),
|
disabled = BaseUITheme.Border(Color.Transparent, 0.dp),
|
||||||
),
|
),
|
||||||
shadow = BaseUITheme.AdvancedInteractionState(
|
shadow = BaseUITheme.AdvancedInteractionState(
|
||||||
normal = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent),
|
|
||||||
hovered = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent),
|
hovered = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent),
|
||||||
pressed = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent),
|
pressed = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent),
|
||||||
focused = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent),
|
focused = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent),
|
||||||
@ -788,7 +701,6 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
),
|
),
|
||||||
padding = PaddingValues(horizontal = 8.dp, vertical = 6.dp),
|
padding = PaddingValues(horizontal = 8.dp, vertical = 6.dp),
|
||||||
textStyle = BaseUITheme.AdvancedInteractionState(
|
textStyle = BaseUITheme.AdvancedInteractionState(
|
||||||
normal = TextStyle.Default,
|
|
||||||
hovered = TextStyle.Default,
|
hovered = TextStyle.Default,
|
||||||
pressed = TextStyle.Default,
|
pressed = TextStyle.Default,
|
||||||
focused = TextStyle.Default,
|
focused = TextStyle.Default,
|
||||||
@ -796,7 +708,6 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
disabled = TextStyle.Default,
|
disabled = TextStyle.Default,
|
||||||
),
|
),
|
||||||
foreground = BaseUITheme.AdvancedInteractionState(
|
foreground = BaseUITheme.AdvancedInteractionState(
|
||||||
normal = scheme.onSurface,
|
|
||||||
hovered = scheme.onSurface,
|
hovered = scheme.onSurface,
|
||||||
pressed = scheme.onSurface,
|
pressed = scheme.onSurface,
|
||||||
focused = scheme.onSurface,
|
focused = scheme.onSurface,
|
||||||
@ -855,7 +766,6 @@ fun invertedButtonStyle(): BaseUITheme.ButtonStyle {
|
|||||||
|
|
||||||
return BaseUITheme.ButtonStyle(
|
return BaseUITheme.ButtonStyle(
|
||||||
colors = BaseUITheme.InteractionState(
|
colors = BaseUITheme.InteractionState(
|
||||||
normal = invertedColors,
|
|
||||||
hovered = invertedColors,
|
hovered = invertedColors,
|
||||||
pressed = pressedColors,
|
pressed = pressedColors,
|
||||||
focused = invertedColors,
|
focused = invertedColors,
|
||||||
|
|||||||
@ -53,7 +53,6 @@ import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer
|
|||||||
import dev.krtirtho.spotube.core.ui.misc.shimmerApply
|
import dev.krtirtho.spotube.core.ui.misc.shimmerApply
|
||||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxEdit
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart2
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart2
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxPauseCircle
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxPauseCircle
|
||||||
@ -81,7 +80,6 @@ fun CollectionDetails(
|
|||||||
isFollowing: Boolean = false,
|
isFollowing: Boolean = false,
|
||||||
onFollowClick: () -> Unit = { },
|
onFollowClick: () -> Unit = { },
|
||||||
showFollowButton: Boolean = true,
|
showFollowButton: Boolean = true,
|
||||||
onEdit: (() -> Unit)? = null,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
||||||
@ -153,15 +151,6 @@ fun CollectionDetails(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (onEdit != null) {
|
|
||||||
ButtonGroupDivider()
|
|
||||||
GroupIconButton(onClick = onEdit) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Iconsax.IconsaxEdit,
|
|
||||||
contentDescription = "Edit playlist",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val ownerInfo = @Composable {
|
val ownerInfo = @Composable {
|
||||||
|
|||||||
@ -103,7 +103,6 @@ import dev.krtirtho.spotube.resources.iconsax.Iconsax3DotsMore
|
|||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxDirectboxReceive
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxDirectboxReceive
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxFilterSearch
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxFilterSearch
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicPlaylist
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxNext
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxNext
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxPause
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxPause
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxPlay
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxPlay
|
||||||
@ -141,7 +140,6 @@ fun TrackList(
|
|||||||
onBulkDownload: (List<MetadataTrack>) -> Unit = {},
|
onBulkDownload: (List<MetadataTrack>) -> Unit = {},
|
||||||
onBulkAddToQueue: (List<MetadataTrack>) -> Unit = {},
|
onBulkAddToQueue: (List<MetadataTrack>) -> Unit = {},
|
||||||
onBulkPlayNext: (List<MetadataTrack>) -> Unit = {},
|
onBulkPlayNext: (List<MetadataTrack>) -> Unit = {},
|
||||||
onBulkAddToPlaylist: (List<MetadataTrack>) -> Unit = {},
|
|
||||||
currentTrackId: String? = null,
|
currentTrackId: String? = null,
|
||||||
isCurrentTrackPlaying: Boolean = false,
|
isCurrentTrackPlaying: Boolean = false,
|
||||||
trackOptionsState: (MetadataTrack) -> TrackOptionsState = { TrackOptionsState() },
|
trackOptionsState: (MetadataTrack) -> TrackOptionsState = { TrackOptionsState() },
|
||||||
@ -435,11 +433,6 @@ fun TrackList(
|
|||||||
label = if (isAll) "Play All Next" else "Play $trackCount Next",
|
label = if (isAll) "Play All Next" else "Play $trackCount Next",
|
||||||
onClick = { onBulkPlayNext(targetTracks) },
|
onClick = { onBulkPlayNext(targetTracks) },
|
||||||
),
|
),
|
||||||
AdaptiveMenuItem(
|
|
||||||
icon = Iconsax.IconsaxMusicPlaylist,
|
|
||||||
label = if (isAll) "Add All to Playlist" else "Add $trackCount to Playlist",
|
|
||||||
onClick = { onBulkAddToPlaylist(targetTracks) },
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
trigger = { onClick ->
|
trigger = { onClick ->
|
||||||
GroupIconButton(
|
GroupIconButton(
|
||||||
@ -569,7 +562,7 @@ private fun TrackListRow(
|
|||||||
isSelected -> rowTheme.background.selected
|
isSelected -> rowTheme.background.selected
|
||||||
isCurrentTrack && isCurrentTrackPlaying -> rowTheme.background.hovered
|
isCurrentTrack && isCurrentTrackPlaying -> rowTheme.background.hovered
|
||||||
isCurrentTrack -> rowTheme.background.focused
|
isCurrentTrack -> rowTheme.background.focused
|
||||||
else -> rowTheme.background.normal
|
else -> Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent))
|
||||||
}
|
}
|
||||||
val highlightColor = Color.White.copy(
|
val highlightColor = Color.White.copy(
|
||||||
alpha = if (isLight) 0.9f else 0.06f
|
alpha = if (isLight) 0.9f else 0.06f
|
||||||
|
|||||||
@ -49,7 +49,6 @@ import dev.krtirtho.spotube.resources.iconsax.IconsaxDirectboxReceive
|
|||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart2
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart2
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicCircle
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicCircle
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicPlaylist
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicSquareRemove
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicSquareRemove
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxNext
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxNext
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxShare
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxShare
|
||||||
@ -60,7 +59,6 @@ sealed interface TrackOptionsAction {
|
|||||||
data object AddToQueue : TrackOptionsAction
|
data object AddToQueue : TrackOptionsAction
|
||||||
data object RemoveFromQueue : TrackOptionsAction
|
data object RemoveFromQueue : TrackOptionsAction
|
||||||
data object ToggleFavorite : TrackOptionsAction
|
data object ToggleFavorite : TrackOptionsAction
|
||||||
data object AddToPlaylist : TrackOptionsAction
|
|
||||||
data object Download : TrackOptionsAction
|
data object Download : TrackOptionsAction
|
||||||
data object ToggleBlacklist : TrackOptionsAction
|
data object ToggleBlacklist : TrackOptionsAction
|
||||||
data object Share : TrackOptionsAction
|
data object Share : TrackOptionsAction
|
||||||
@ -276,14 +274,6 @@ private fun buildTrackMenuItems(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
add(
|
|
||||||
AdaptiveMenuItem(
|
|
||||||
icon = Iconsax.IconsaxMusicPlaylist,
|
|
||||||
label = "Add to playlist",
|
|
||||||
onClick = { onAction(TrackOptionsAction.AddToPlaylist) },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
add(
|
add(
|
||||||
AdaptiveMenuItem(
|
AdaptiveMenuItem(
|
||||||
icon = Iconsax.IconsaxDirectboxReceive,
|
icon = Iconsax.IconsaxDirectboxReceive,
|
||||||
|
|||||||
@ -20,12 +20,7 @@ package dev.krtirtho.spotube.modules.album
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
||||||
@ -45,8 +40,6 @@ import dev.krtirtho.spotube.core.ui.component.TrackOptionsState
|
|||||||
import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
|
import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
|
||||||
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
||||||
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
||||||
import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
@ -57,8 +50,6 @@ fun AlbumScreen(albumId: String) {
|
|||||||
val audioPlayer: AudioPlayer = koinInject()
|
val audioPlayer: AudioPlayer = koinInject()
|
||||||
val shareService: ShareService = koinInject()
|
val shareService: ShareService = koinInject()
|
||||||
val downloadsViewModel: DownloadsViewModel = koinViewModel()
|
val downloadsViewModel: DownloadsViewModel = koinViewModel()
|
||||||
val libraryRepository: LibraryRepository = koinInject()
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val viewModel = koinViewModel<AlbumViewModel>(
|
val viewModel = koinViewModel<AlbumViewModel>(
|
||||||
key = albumId,
|
key = albumId,
|
||||||
parameters = { parametersOf(albumId) }
|
parameters = { parametersOf(albumId) }
|
||||||
@ -71,13 +62,6 @@ fun AlbumScreen(albumId: String) {
|
|||||||
val playerState by audioPlayer.playerStateFlow.collectAsStateWithLifecycle()
|
val playerState by audioPlayer.playerStateFlow.collectAsStateWithLifecycle()
|
||||||
val savedTrackIds by viewModel.savedTrackIds.collectAsStateWithLifecycle()
|
val savedTrackIds by viewModel.savedTrackIds.collectAsStateWithLifecycle()
|
||||||
val savedAlbumIds by viewModel.savedAlbumIds.collectAsStateWithLifecycle()
|
val savedAlbumIds by viewModel.savedAlbumIds.collectAsStateWithLifecycle()
|
||||||
var showAddToPlaylistPicker by remember { mutableStateOf(false) }
|
|
||||||
var tracksToAddToPlaylist by remember { mutableStateOf<List<MetadataTrack>>(emptyList()) }
|
|
||||||
var currentUserId by remember { mutableStateOf<String?>(null) }
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
currentUserId = libraryRepository.currentUser()?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getTrackOptionsState(track: MetadataTrack): TrackOptionsState {
|
fun getTrackOptionsState(track: MetadataTrack): TrackOptionsState {
|
||||||
val currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id
|
val currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id
|
||||||
@ -103,10 +87,6 @@ fun AlbumScreen(albumId: String) {
|
|||||||
if (action is TrackOptionsAction.Download) {
|
if (action is TrackOptionsAction.Download) {
|
||||||
downloadsViewModel.downloadTrack(track)
|
downloadsViewModel.downloadTrack(track)
|
||||||
}
|
}
|
||||||
if (action is TrackOptionsAction.AddToPlaylist) {
|
|
||||||
tracksToAddToPlaylist = listOf(track)
|
|
||||||
showAddToPlaylistPicker = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@ -214,26 +194,8 @@ fun AlbumScreen(albumId: String) {
|
|||||||
onBulkPlayNext = { tracks ->
|
onBulkPlayNext = { tracks ->
|
||||||
viewModel.playTracksNext(tracks)
|
viewModel.playTracksNext(tracks)
|
||||||
},
|
},
|
||||||
onBulkAddToPlaylist = { tracks ->
|
|
||||||
tracksToAddToPlaylist = tracks
|
|
||||||
showAddToPlaylistPicker = true
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AddToPlaylistPicker(
|
|
||||||
visible = showAddToPlaylistPicker,
|
|
||||||
currentUserId = currentUserId,
|
|
||||||
onDismiss = { showAddToPlaylistPicker = false },
|
|
||||||
onPlaylistSelected = { playlistId ->
|
|
||||||
scope.launch {
|
|
||||||
libraryRepository.addTracksToPlaylist(
|
|
||||||
playlistId,
|
|
||||||
tracksToAddToPlaylist.map { it.id }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -222,7 +222,6 @@ class AlbumViewModel(
|
|||||||
is TrackOptionsAction.Download -> {}
|
is TrackOptionsAction.Download -> {}
|
||||||
is TrackOptionsAction.ToggleBlacklist -> {}
|
is TrackOptionsAction.ToggleBlacklist -> {}
|
||||||
is TrackOptionsAction.Share -> {}
|
is TrackOptionsAction.Share -> {}
|
||||||
is TrackOptionsAction.AddToPlaylist -> {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,8 +36,12 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.FilledIconButton
|
||||||
|
import androidx.compose.material3.FilledTonalButton
|
||||||
|
import androidx.compose.material3.FilledTonalIconButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
@ -45,10 +49,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@ -60,6 +61,10 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
|
import compose.icons.FeatherIcons
|
||||||
|
import compose.icons.feathericons.Play
|
||||||
|
import compose.icons.feathericons.PlusSquare
|
||||||
|
import compose.icons.feathericons.User
|
||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.album.MetadataAlbum
|
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.album.MetadataAlbum
|
||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtist
|
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtist
|
||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.playlist.MetadataPlaylist
|
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.playlist.MetadataPlaylist
|
||||||
@ -71,10 +76,6 @@ import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
|||||||
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
||||||
import dev.krtirtho.spotube.core.navigation.Routes
|
import dev.krtirtho.spotube.core.navigation.Routes
|
||||||
import dev.krtirtho.spotube.core.share.ShareService
|
import dev.krtirtho.spotube.core.share.ShareService
|
||||||
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.PrimaryIconButton
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.SecondaryButton
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.SecondaryIconButton
|
|
||||||
import dev.krtirtho.spotube.core.ui.component.AlbumCard
|
import dev.krtirtho.spotube.core.ui.component.AlbumCard
|
||||||
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
|
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
|
||||||
import dev.krtirtho.spotube.core.ui.component.ArtistCard
|
import dev.krtirtho.spotube.core.ui.component.ArtistCard
|
||||||
@ -88,15 +89,8 @@ import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
|
|||||||
import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer
|
import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer
|
||||||
import dev.krtirtho.spotube.core.ui.misc.shimmerApply
|
import dev.krtirtho.spotube.core.ui.misc.shimmerApply
|
||||||
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
||||||
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
|
||||||
import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxPlay
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.User
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
@ -108,8 +102,6 @@ fun ArtistScreen(artistId: String) {
|
|||||||
val audioPlayer: AudioPlayer = koinInject()
|
val audioPlayer: AudioPlayer = koinInject()
|
||||||
val shareService: ShareService = koinInject()
|
val shareService: ShareService = koinInject()
|
||||||
val downloadsViewModel: DownloadsViewModel = koinViewModel()
|
val downloadsViewModel: DownloadsViewModel = koinViewModel()
|
||||||
val libraryRepository: LibraryRepository = koinInject()
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val viewModel = koinViewModel<ArtistViewModel>(
|
val viewModel = koinViewModel<ArtistViewModel>(
|
||||||
key = artistId,
|
key = artistId,
|
||||||
parameters = { parametersOf(artistId) }
|
parameters = { parametersOf(artistId) }
|
||||||
@ -122,13 +114,6 @@ fun ArtistScreen(artistId: String) {
|
|||||||
val playerState by audioPlayer.playerStateFlow.collectAsStateWithLifecycle()
|
val playerState by audioPlayer.playerStateFlow.collectAsStateWithLifecycle()
|
||||||
val savedTrackIds by viewModel.savedTrackIds.collectAsStateWithLifecycle()
|
val savedTrackIds by viewModel.savedTrackIds.collectAsStateWithLifecycle()
|
||||||
val savedArtistIds by viewModel.savedArtistIds.collectAsStateWithLifecycle()
|
val savedArtistIds by viewModel.savedArtistIds.collectAsStateWithLifecycle()
|
||||||
var showAddToPlaylistPicker by remember { mutableStateOf(false) }
|
|
||||||
var tracksToAddToPlaylist by remember { mutableStateOf<List<MetadataTrack>>(emptyList()) }
|
|
||||||
var currentUserId by remember { mutableStateOf<String?>(null) }
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
currentUserId = libraryRepository.currentUser()?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getTrackOptionsState(track: MetadataTrack): TrackOptionsState {
|
fun getTrackOptionsState(track: MetadataTrack): TrackOptionsState {
|
||||||
val currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id
|
val currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id
|
||||||
@ -154,10 +139,6 @@ fun ArtistScreen(artistId: String) {
|
|||||||
if (action is TrackOptionsAction.Download) {
|
if (action is TrackOptionsAction.Download) {
|
||||||
downloadsViewModel.downloadTrack(track)
|
downloadsViewModel.downloadTrack(track)
|
||||||
}
|
}
|
||||||
if (action is TrackOptionsAction.AddToPlaylist) {
|
|
||||||
tracksToAddToPlaylist = listOf(track)
|
|
||||||
showAddToPlaylistPicker = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@ -233,10 +214,6 @@ fun ArtistScreen(artistId: String) {
|
|||||||
onBulkDownload = { tracks -> downloadsViewModel.downloadTracks(tracks) },
|
onBulkDownload = { tracks -> downloadsViewModel.downloadTracks(tracks) },
|
||||||
onBulkAddToQueue = { tracks -> viewModel.addTracksToQueue(tracks) },
|
onBulkAddToQueue = { tracks -> viewModel.addTracksToQueue(tracks) },
|
||||||
onBulkPlayNext = { tracks -> viewModel.playTracksNext(tracks) },
|
onBulkPlayNext = { tracks -> viewModel.playTracksNext(tracks) },
|
||||||
onBulkAddToPlaylist = { tracks ->
|
|
||||||
tracksToAddToPlaylist = tracks
|
|
||||||
showAddToPlaylistPicker = true
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -290,20 +267,6 @@ fun ArtistScreen(artistId: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AddToPlaylistPicker(
|
|
||||||
visible = showAddToPlaylistPicker,
|
|
||||||
currentUserId = currentUserId,
|
|
||||||
onDismiss = { showAddToPlaylistPicker = false },
|
|
||||||
onPlaylistSelected = { playlistId ->
|
|
||||||
scope.launch {
|
|
||||||
libraryRepository.addTracksToPlaylist(
|
|
||||||
playlistId,
|
|
||||||
tracksToAddToPlaylist.map { it.id }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -386,7 +349,7 @@ private fun ArtistErrorContent(
|
|||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
)
|
)
|
||||||
PrimaryButton(onClick = onRetry) {
|
Button(onClick = onRetry) {
|
||||||
Text("Retry")
|
Text("Retry")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -489,7 +452,7 @@ private fun ArtistAvatar(
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Iconsax.User,
|
imageVector = FeatherIcons.User,
|
||||||
contentDescription = artist.name,
|
contentDescription = artist.name,
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.size(size * 0.4f),
|
modifier = Modifier.size(size * 0.4f),
|
||||||
@ -553,11 +516,11 @@ private fun ArtistHeaderActions(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
if (isSaved) {
|
if (isSaved) {
|
||||||
SecondaryButton(onClick = onFollowClick) {
|
FilledTonalButton(onClick = onFollowClick) {
|
||||||
Text("Following", modifier = Modifier.width(65.dp), textAlign = TextAlign.Center)
|
Text("Following", modifier = Modifier.width(65.dp), textAlign = TextAlign.Center)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
PrimaryButton(onClick = onFollowClick) {
|
Button(onClick = onFollowClick) {
|
||||||
Text("Follow", modifier = Modifier.width(65.dp), textAlign = TextAlign.Center)
|
Text("Follow", modifier = Modifier.width(65.dp), textAlign = TextAlign.Center)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -583,15 +546,15 @@ private fun TopTracksHeader(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
PrimaryIconButton(onClick = onPlay) {
|
FilledIconButton(onClick = onPlay) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Iconsax.IconsaxPlay,
|
imageVector = FeatherIcons.Play,
|
||||||
contentDescription = "Play top tracks",
|
contentDescription = "Play top tracks",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
SecondaryIconButton(onClick = onAddToQueue) {
|
FilledTonalIconButton(onClick = onAddToQueue) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Iconsax.IconsaxAddSquare,
|
imageVector = FeatherIcons.PlusSquare,
|
||||||
contentDescription = "Add top tracks to queue",
|
contentDescription = "Add top tracks to queue",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -269,7 +269,6 @@ class ArtistViewModel(
|
|||||||
is TrackOptionsAction.Download -> {}
|
is TrackOptionsAction.Download -> {}
|
||||||
is TrackOptionsAction.ToggleBlacklist -> {}
|
is TrackOptionsAction.ToggleBlacklist -> {}
|
||||||
is TrackOptionsAction.Share -> {}
|
is TrackOptionsAction.Share -> {}
|
||||||
is TrackOptionsAction.AddToPlaylist -> {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,6 @@ import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtist
|
|||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.common.PaginationResult
|
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.common.PaginationResult
|
||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.common.PaginationStrategy
|
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.common.PaginationStrategy
|
||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.playlist.MetadataPlaylist
|
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.playlist.MetadataPlaylist
|
||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.user.MetadataUser
|
|
||||||
import dev.krtirtho.spotube.modules.plugin.PluginManager
|
import dev.krtirtho.spotube.modules.plugin.PluginManager
|
||||||
import io.github.reactivecircus.cache4k.Cache
|
import io.github.reactivecircus.cache4k.Cache
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@ -277,80 +276,4 @@ class LibraryRepository(
|
|||||||
savedArtistIds.value -= ids.toSet()
|
savedArtistIds.value -= ids.toSet()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun currentUser(): MetadataUser? {
|
|
||||||
return plugin?.let { plugin ->
|
|
||||||
pluginManager.withScope {
|
|
||||||
plugin.use {
|
|
||||||
metadataUserAPI.getUser("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun createPlaylist(
|
|
||||||
name: String,
|
|
||||||
description: String?,
|
|
||||||
isPublic: Boolean,
|
|
||||||
isCollaborating: Boolean,
|
|
||||||
imageBase64: String,
|
|
||||||
trackIds: List<String>,
|
|
||||||
): MetadataPlaylist? {
|
|
||||||
return plugin?.let { plugin ->
|
|
||||||
pluginManager.withScope {
|
|
||||||
plugin.use {
|
|
||||||
val result = metadataPlaylistAPI.createPlaylist(
|
|
||||||
name = name,
|
|
||||||
description = description,
|
|
||||||
isPublic = isPublic,
|
|
||||||
isCollaborating = isCollaborating,
|
|
||||||
imageBase64 = imageBase64,
|
|
||||||
trackIds = trackIds,
|
|
||||||
)
|
|
||||||
playlistCache.invalidateAll()
|
|
||||||
savedPlaylistIds.value += result.id
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun updatePlaylist(
|
|
||||||
id: String,
|
|
||||||
name: String?,
|
|
||||||
description: String?,
|
|
||||||
isPublic: Boolean?,
|
|
||||||
isCollaborating: Boolean?,
|
|
||||||
imageBase64: String?,
|
|
||||||
trackIds: List<String>?,
|
|
||||||
): MetadataPlaylist? {
|
|
||||||
return plugin?.let { plugin ->
|
|
||||||
pluginManager.withScope {
|
|
||||||
plugin.use {
|
|
||||||
val result = metadataPlaylistAPI.updatePlaylist(
|
|
||||||
id = id,
|
|
||||||
name = name,
|
|
||||||
description = description,
|
|
||||||
isPublic = isPublic,
|
|
||||||
isCollaborating = isCollaborating,
|
|
||||||
imageBase64 = imageBase64,
|
|
||||||
trackIds = trackIds,
|
|
||||||
)
|
|
||||||
playlistCache.invalidateAll()
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun addTracksToPlaylist(playlistId: String, trackIds: List<String>) {
|
|
||||||
plugin?.let { plugin ->
|
|
||||||
pluginManager.withScope {
|
|
||||||
plugin.use {
|
|
||||||
metadataPlaylistAPI.addTracksToPlaylist(playlistId, trackIds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
playlistCache.invalidateAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,257 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2026 Kingmor Roy Tirtho and Spotube Contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package dev.krtirtho.spotube.modules.library.playlist
|
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
|
||||||
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.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import coil3.compose.AsyncImage
|
|
||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.playlist.MetadataPlaylist
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.TextField
|
|
||||||
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxFilterSearch
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.compose.koinInject
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun AddToPlaylistPicker(
|
|
||||||
visible: Boolean,
|
|
||||||
currentUserId: String?,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
onPlaylistSelected: (String) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
libraryRepository: LibraryRepository = koinInject(),
|
|
||||||
breakpointDp: Float = 600f,
|
|
||||||
) {
|
|
||||||
if (!visible) return
|
|
||||||
|
|
||||||
val adaptiveInfo = currentWindowAdaptiveInfo()
|
|
||||||
val isLargeScreen = adaptiveInfo.windowSizeClass.minWidthDp >= breakpointDp
|
|
||||||
|
|
||||||
var playlists by remember { mutableStateOf<List<MetadataPlaylist>>(emptyList()) }
|
|
||||||
var isLoading by remember { mutableStateOf(true) }
|
|
||||||
var searchQuery by remember { mutableStateOf("") }
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
LaunchedEffect(visible, currentUserId) {
|
|
||||||
if (visible) {
|
|
||||||
isLoading = true
|
|
||||||
scope.launch {
|
|
||||||
val result = libraryRepository.savedPlaylists()
|
|
||||||
playlists = result?.items?.filter {
|
|
||||||
currentUserId != null && it.owner?.id == currentUserId
|
|
||||||
} ?: emptyList()
|
|
||||||
isLoading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val filteredPlaylists = remember(playlists, searchQuery) {
|
|
||||||
if (searchQuery.isBlank()) {
|
|
||||||
playlists
|
|
||||||
} else {
|
|
||||||
playlists.filter {
|
|
||||||
it.title.contains(searchQuery, ignoreCase = true) ||
|
|
||||||
it.description?.contains(searchQuery, ignoreCase = true) == true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val content: @Composable () -> Unit = {
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
|
||||||
TextField(
|
|
||||||
value = searchQuery,
|
|
||||||
onValueChange = { searchQuery = it },
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(bottom = 12.dp),
|
|
||||||
placeholder = { Text("Search your playlists...") },
|
|
||||||
singleLine = true,
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(
|
|
||||||
imageVector = Iconsax.IconsaxFilterSearch,
|
|
||||||
contentDescription = "Search",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
PlaylistPickerContent(
|
|
||||||
playlists = filteredPlaylists,
|
|
||||||
isLoading = isLoading,
|
|
||||||
onPlaylistSelected = { playlistId ->
|
|
||||||
onPlaylistSelected(playlistId)
|
|
||||||
onDismiss()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLargeScreen) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
modifier = modifier,
|
|
||||||
title = {
|
|
||||||
Text("Add to Your Playlist")
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Box(modifier = Modifier.fillMaxWidth().height(400.dp)) {
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
OutlineButton(onClick = onDismiss) {
|
|
||||||
Text("Cancel")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
ModalBottomSheet(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
dragHandle = null,
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Add to Your Playlist",
|
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
|
||||||
modifier = Modifier.padding(bottom = 16.dp),
|
|
||||||
)
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun PlaylistPickerContent(
|
|
||||||
playlists: List<MetadataPlaylist>,
|
|
||||||
isLoading: Boolean,
|
|
||||||
onPlaylistSelected: (String) -> Unit,
|
|
||||||
) {
|
|
||||||
if (isLoading) {
|
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
|
||||||
Text("Loading your playlists...")
|
|
||||||
}
|
|
||||||
} else if (playlists.isEmpty()) {
|
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
|
||||||
Text("You don't own any playlists yet.\nCreate one to add tracks.")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
) {
|
|
||||||
items(playlists, key = { it.id }) { playlist ->
|
|
||||||
PlaylistPickerRow(
|
|
||||||
playlist = playlist,
|
|
||||||
onClick = { onPlaylistSelected(playlist.id) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun PlaylistPickerRow(
|
|
||||||
playlist: MetadataPlaylist,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(RoundedCornerShape(12.dp))
|
|
||||||
.clickable(onClick = onClick)
|
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
) {
|
|
||||||
val imageUrl = playlist.thumbnails.firstOrNull()?.url
|
|
||||||
if (imageUrl != null) {
|
|
||||||
AsyncImage(
|
|
||||||
model = imageUrl,
|
|
||||||
contentDescription = playlist.title,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(48.dp)
|
|
||||||
.clip(RoundedCornerShape(8.dp)),
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(48.dp)
|
|
||||||
.clip(RoundedCornerShape(8.dp)),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = playlist.title.take(1).uppercase(),
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(
|
|
||||||
text = playlist.title,
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "${playlist.trackCount} tracks",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -22,12 +22,10 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
@ -37,15 +35,12 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import dev.krtirtho.spotube.core.ui.base.IconButton
|
|
||||||
import dev.krtirtho.spotube.core.ui.component.ErrorDisplay
|
import dev.krtirtho.spotube.core.ui.component.ErrorDisplay
|
||||||
import dev.krtirtho.spotube.core.ui.base.TextField
|
import dev.krtirtho.spotube.core.ui.base.TextField
|
||||||
import dev.krtirtho.spotube.core.ui.component.LikedTracksCard
|
import dev.krtirtho.spotube.core.ui.component.LikedTracksCard
|
||||||
@ -54,7 +49,6 @@ import dev.krtirtho.spotube.core.ui.component.cards.PlayableCard
|
|||||||
import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
|
import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
|
||||||
import dev.krtirtho.spotube.modules.shell.LocalAppShellBottomInset
|
import dev.krtirtho.spotube.modules.shell.LocalAppShellBottomInset
|
||||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxFilterSearch
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxFilterSearch
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@ -68,7 +62,6 @@ fun LibraryPlaylistsScreen(
|
|||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val gridState = rememberLazyGridState()
|
val gridState = rememberLazyGridState()
|
||||||
var showCreatePlaylist by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
|
|
||||||
LaunchedEffect(gridState) {
|
LaunchedEffect(gridState) {
|
||||||
@ -91,38 +84,22 @@ fun LibraryPlaylistsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
Row(
|
AnimatedVisibility(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
visible = searchMode,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
TextField(
|
||||||
visible = searchMode,
|
value = (state as? LibraryPlaylistsState.Data)?.query ?: "",
|
||||||
modifier = Modifier.weight(1f),
|
onValueChange = viewModel::onQueryChange,
|
||||||
) {
|
modifier = Modifier.fillMaxWidth(),
|
||||||
TextField(
|
placeholder = { Text("Search saved playlists...") },
|
||||||
value = (state as? LibraryPlaylistsState.Data)?.query ?: "",
|
singleLine = true,
|
||||||
onValueChange = viewModel::onQueryChange,
|
leadingIcon = {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
Icon(
|
||||||
placeholder = { Text("Search saved playlists...") },
|
imageVector = Iconsax.IconsaxFilterSearch,
|
||||||
singleLine = true,
|
contentDescription = "Search"
|
||||||
leadingIcon = {
|
)
|
||||||
Icon(
|
}
|
||||||
imageVector = Iconsax.IconsaxFilterSearch,
|
)
|
||||||
contentDescription = "Search"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IconButton(
|
|
||||||
onClick = { showCreatePlaylist = true },
|
|
||||||
modifier = Modifier.size(48.dp),
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Iconsax.IconsaxAddSquare,
|
|
||||||
contentDescription = "Create Playlist",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
when (state) {
|
when (state) {
|
||||||
@ -210,20 +187,5 @@ fun LibraryPlaylistsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaylistFormSheet(
|
|
||||||
visible = showCreatePlaylist,
|
|
||||||
onDismiss = { showCreatePlaylist = false },
|
|
||||||
onSubmit = { data ->
|
|
||||||
viewModel.createPlaylist(
|
|
||||||
name = data.name,
|
|
||||||
description = data.description.ifBlank { null },
|
|
||||||
isPublic = data.isPublic,
|
|
||||||
isCollaborating = data.isCollaborating,
|
|
||||||
imageBase64 = data.imageBase64,
|
|
||||||
trackIds = emptyList(),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -152,31 +152,4 @@ class LibraryPlaylistsViewModel(
|
|||||||
loadInitialData()
|
loadInitialData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createPlaylist(
|
|
||||||
name: String,
|
|
||||||
description: String?,
|
|
||||||
isPublic: Boolean,
|
|
||||||
isCollaborating: Boolean,
|
|
||||||
imageBase64: String,
|
|
||||||
trackIds: List<String>,
|
|
||||||
) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
runCatching {
|
|
||||||
repository.createPlaylist(
|
|
||||||
name = name,
|
|
||||||
description = description,
|
|
||||||
isPublic = isPublic,
|
|
||||||
isCollaborating = isCollaborating,
|
|
||||||
imageBase64 = imageBase64,
|
|
||||||
trackIds = trackIds,
|
|
||||||
)
|
|
||||||
}.onFailure { e ->
|
|
||||||
logger.e(e) { "Failed to create playlist" }
|
|
||||||
}.onSuccess {
|
|
||||||
repository.invalidateCaches()
|
|
||||||
loadInitialData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,335 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package dev.krtirtho.spotube.modules.library.playlist
|
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.Switch
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import coil3.compose.AsyncImage
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.TextField
|
|
||||||
import io.github.vinceglb.filekit.dialogs.FileKitType
|
|
||||||
import io.github.vinceglb.filekit.dialogs.compose.rememberFilePickerLauncher
|
|
||||||
import io.github.vinceglb.filekit.readBytes
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import okio.ByteString.Companion.toByteString
|
|
||||||
|
|
||||||
data class PlaylistFormData(
|
|
||||||
val name: String = "",
|
|
||||||
val description: String = "",
|
|
||||||
val isPublic: Boolean = true,
|
|
||||||
val isCollaborating: Boolean = false,
|
|
||||||
val imageBase64: String = "",
|
|
||||||
val imagePreviewUrl: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun PlaylistFormSheet(
|
|
||||||
visible: Boolean,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
onSubmit: (PlaylistFormData) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
initialData: PlaylistFormData? = null,
|
|
||||||
isEditing: Boolean = false,
|
|
||||||
breakpointDp: Float = 600f,
|
|
||||||
) {
|
|
||||||
if (!visible) return
|
|
||||||
|
|
||||||
val adaptiveInfo = currentWindowAdaptiveInfo()
|
|
||||||
val isLargeScreen = adaptiveInfo.windowSizeClass.minWidthDp >= breakpointDp
|
|
||||||
|
|
||||||
var name by remember(initialData) { mutableStateOf(initialData?.name ?: "") }
|
|
||||||
var description by remember(initialData) { mutableStateOf(initialData?.description ?: "") }
|
|
||||||
var isPublic by remember(initialData) { mutableStateOf(initialData?.isPublic ?: true) }
|
|
||||||
var isCollaborating by remember(initialData) { mutableStateOf(initialData?.isCollaborating ?: false) }
|
|
||||||
var imageBase64 by remember(initialData) { mutableStateOf(initialData?.imageBase64 ?: "") }
|
|
||||||
var imagePreviewUrl by remember(initialData) { mutableStateOf(initialData?.imagePreviewUrl) }
|
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val imagePicker = rememberFilePickerLauncher(
|
|
||||||
type = FileKitType.Image
|
|
||||||
) { file ->
|
|
||||||
if (file != null) {
|
|
||||||
scope.launch {
|
|
||||||
val bytes = file.readBytes()
|
|
||||||
val base64 = withContext(Dispatchers.Default) {
|
|
||||||
bytes.toByteString().base64()
|
|
||||||
}
|
|
||||||
imageBase64 = base64
|
|
||||||
imagePreviewUrl = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentData = PlaylistFormData(
|
|
||||||
name = name,
|
|
||||||
description = description,
|
|
||||||
isPublic = isPublic,
|
|
||||||
isCollaborating = isCollaborating,
|
|
||||||
imageBase64 = imageBase64,
|
|
||||||
imagePreviewUrl = imagePreviewUrl,
|
|
||||||
)
|
|
||||||
|
|
||||||
val handleImagePick = { imagePicker.launch() }
|
|
||||||
|
|
||||||
if (isLargeScreen) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
modifier = modifier,
|
|
||||||
title = {
|
|
||||||
Text(if (isEditing) "Edit Playlist" else "Create Playlist")
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
PlaylistFormFields(
|
|
||||||
name = name,
|
|
||||||
onNameChange = { name = it },
|
|
||||||
description = description,
|
|
||||||
onDescriptionChange = { description = it },
|
|
||||||
isPublic = isPublic,
|
|
||||||
onIsPublicChange = { isPublic = it },
|
|
||||||
isCollaborating = isCollaborating,
|
|
||||||
onIsCollaboratingChange = { isCollaborating = it },
|
|
||||||
imageBase64 = imageBase64,
|
|
||||||
imagePreviewUrl = imagePreviewUrl,
|
|
||||||
onPickImage = handleImagePick,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
PrimaryButton(
|
|
||||||
onClick = {
|
|
||||||
onSubmit(currentData)
|
|
||||||
onDismiss()
|
|
||||||
},
|
|
||||||
enabled = name.isNotBlank(),
|
|
||||||
) {
|
|
||||||
Text(if (isEditing) "Save" else "Create")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
OutlineButton(onClick = onDismiss) {
|
|
||||||
Text("Cancel")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
ModalBottomSheet(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
dragHandle = null,
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = if (isEditing) "Edit Playlist" else "Create Playlist",
|
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
|
||||||
modifier = Modifier.padding(bottom = 16.dp),
|
|
||||||
)
|
|
||||||
PlaylistFormFields(
|
|
||||||
name = name,
|
|
||||||
onNameChange = { name = it },
|
|
||||||
description = description,
|
|
||||||
onDescriptionChange = { description = it },
|
|
||||||
isPublic = isPublic,
|
|
||||||
onIsPublicChange = { isPublic = it },
|
|
||||||
isCollaborating = isCollaborating,
|
|
||||||
onIsCollaboratingChange = { isCollaborating = it },
|
|
||||||
imageBase64 = imageBase64,
|
|
||||||
imagePreviewUrl = imagePreviewUrl,
|
|
||||||
onPickImage = handleImagePick,
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
|
|
||||||
) {
|
|
||||||
OutlineButton(onClick = onDismiss) {
|
|
||||||
Text("Cancel")
|
|
||||||
}
|
|
||||||
PrimaryButton(
|
|
||||||
onClick = {
|
|
||||||
onSubmit(currentData)
|
|
||||||
onDismiss()
|
|
||||||
},
|
|
||||||
enabled = name.isNotBlank(),
|
|
||||||
) {
|
|
||||||
Text(if (isEditing) "Save" else "Create")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun PlaylistFormFields(
|
|
||||||
name: String,
|
|
||||||
onNameChange: (String) -> Unit,
|
|
||||||
description: String,
|
|
||||||
onDescriptionChange: (String) -> Unit,
|
|
||||||
isPublic: Boolean,
|
|
||||||
onIsPublicChange: (Boolean) -> Unit,
|
|
||||||
isCollaborating: Boolean,
|
|
||||||
onIsCollaboratingChange: (Boolean) -> Unit,
|
|
||||||
imageBase64: String,
|
|
||||||
imagePreviewUrl: String?,
|
|
||||||
onPickImage: () -> Unit,
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.verticalScroll(rememberScrollState()),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
) {
|
|
||||||
TextField(
|
|
||||||
value = name,
|
|
||||||
onValueChange = onNameChange,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
placeholder = { Text("Playlist name") },
|
|
||||||
singleLine = true,
|
|
||||||
label = { Text("Name") },
|
|
||||||
)
|
|
||||||
|
|
||||||
TextField(
|
|
||||||
value = description,
|
|
||||||
onValueChange = onDescriptionChange,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
placeholder = { Text("Description (optional)") },
|
|
||||||
maxLines = 3,
|
|
||||||
label = { Text("Description") },
|
|
||||||
)
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(RoundedCornerShape(12.dp))
|
|
||||||
.clickable { onIsPublicChange(!isPublic) }
|
|
||||||
.padding(horizontal = 4.dp, vertical = 8.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text("Public", style = MaterialTheme.typography.bodyLarge)
|
|
||||||
Text(
|
|
||||||
"Anyone can see this playlist",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Switch(checked = isPublic, onCheckedChange = onIsPublicChange)
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(RoundedCornerShape(12.dp))
|
|
||||||
.clickable { onIsCollaboratingChange(!isCollaborating) }
|
|
||||||
.padding(horizontal = 4.dp, vertical = 8.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text("Collaborative", style = MaterialTheme.typography.bodyLarge)
|
|
||||||
Text(
|
|
||||||
"Others can add tracks to this playlist",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Switch(checked = isCollaborating, onCheckedChange = onIsCollaboratingChange)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
|
||||||
Text("Cover Image", style = MaterialTheme.typography.bodyLarge)
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(64.dp)
|
|
||||||
.clip(RoundedCornerShape(8.dp)),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
when {
|
|
||||||
imagePreviewUrl != null -> AsyncImage(
|
|
||||||
model = imagePreviewUrl,
|
|
||||||
contentDescription = "Playlist cover",
|
|
||||||
modifier = Modifier.size(64.dp),
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
)
|
|
||||||
imageBase64.isNotBlank() -> {
|
|
||||||
Text(
|
|
||||||
"✓",
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
Text(
|
|
||||||
"No image",
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PrimaryButton(onClick = onPickImage) {
|
|
||||||
Text(if (imageBase64.isNotBlank() || imagePreviewUrl != null) "Change Image" else "Pick Image")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,325 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2026 Kingmor Roy Tirtho and Spotube Contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package dev.krtirtho.spotube.modules.playlist
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
|
||||||
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.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import coil3.compose.AsyncImage
|
|
||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.IconButton
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.TextField
|
|
||||||
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
|
||||||
import dev.krtirtho.spotube.modules.search.SearchRepository
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxCheckCircle
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxFilterSearch
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearch
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.compose.koinInject
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun AddTracksToPlaylistDialog(
|
|
||||||
visible: Boolean,
|
|
||||||
playlistId: String,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
searchRepository: SearchRepository = koinInject(),
|
|
||||||
libraryRepository: LibraryRepository = koinInject(),
|
|
||||||
breakpointDp: Float = 600f,
|
|
||||||
) {
|
|
||||||
if (!visible) return
|
|
||||||
|
|
||||||
val adaptiveInfo = currentWindowAdaptiveInfo()
|
|
||||||
val isLargeScreen = adaptiveInfo.windowSizeClass.minWidthDp >= breakpointDp
|
|
||||||
|
|
||||||
var query by remember { mutableStateOf("") }
|
|
||||||
var searchResults by remember { mutableStateOf<List<MetadataTrack>>(emptyList()) }
|
|
||||||
var isSearching by remember { mutableStateOf(false) }
|
|
||||||
var hasSearched by remember { mutableStateOf(false) }
|
|
||||||
var addingTrackIds by remember { mutableStateOf<Set<String>>(emptySet()) }
|
|
||||||
var addedTrackIds by remember { mutableStateOf<Set<String>>(emptySet()) }
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
LaunchedEffect(visible) {
|
|
||||||
if (!visible) {
|
|
||||||
query = ""
|
|
||||||
searchResults = emptyList()
|
|
||||||
hasSearched = false
|
|
||||||
addingTrackIds = emptySet()
|
|
||||||
addedTrackIds = emptySet()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val handleSearch = {
|
|
||||||
if (query.isNotBlank() && !isSearching) {
|
|
||||||
isSearching = true
|
|
||||||
hasSearched = true
|
|
||||||
scope.launch {
|
|
||||||
val result = searchRepository.searchTracks(query)
|
|
||||||
searchResults = result.items.map { it.data }
|
|
||||||
isSearching = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val handleAddTrack = { track: MetadataTrack ->
|
|
||||||
if (!addingTrackIds.contains(track.id) && !addedTrackIds.contains(track.id)) {
|
|
||||||
addingTrackIds = addingTrackIds + track.id
|
|
||||||
scope.launch {
|
|
||||||
libraryRepository.addTracksToPlaylist(playlistId, listOf(track.id))
|
|
||||||
addingTrackIds = addingTrackIds - track.id
|
|
||||||
addedTrackIds = addedTrackIds + track.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val content: @Composable () -> Unit = {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
TextField(
|
|
||||||
value = query,
|
|
||||||
onValueChange = { query = it },
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
placeholder = { Text("Search tracks...") },
|
|
||||||
singleLine = true,
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(
|
|
||||||
imageVector = Iconsax.IconsaxFilterSearch,
|
|
||||||
contentDescription = "Search",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
IconButton(onClick = handleSearch) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Iconsax.IconsaxSearch,
|
|
||||||
contentDescription = "Search",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
when {
|
|
||||||
isSearching -> {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.weight(1f),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hasSearched && searchResults.isEmpty() -> {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.weight(1f),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
Text("No tracks found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
searchResults.isNotEmpty() -> {
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.weight(1f),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
) {
|
|
||||||
items(searchResults, key = { it.id }) { track ->
|
|
||||||
TrackSearchResultRow(
|
|
||||||
track = track,
|
|
||||||
isAdding = addingTrackIds.contains(track.id),
|
|
||||||
isAdded = addedTrackIds.contains(track.id),
|
|
||||||
onAdd = { handleAddTrack(track) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.weight(1f),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
Text("Search for tracks to add")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLargeScreen) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
modifier = modifier,
|
|
||||||
title = {
|
|
||||||
Text("Add Tracks to Playlist")
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Box(modifier = Modifier.fillMaxWidth().height(500.dp)) {
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
PrimaryButton(onClick = onDismiss) {
|
|
||||||
Text("Done")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
ModalBottomSheet(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
dragHandle = null,
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
.height(600.dp),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Add Tracks to Playlist",
|
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
|
||||||
modifier = Modifier.padding(bottom = 16.dp),
|
|
||||||
)
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun TrackSearchResultRow(
|
|
||||||
track: MetadataTrack,
|
|
||||||
isAdding: Boolean,
|
|
||||||
isAdded: Boolean,
|
|
||||||
onAdd: () -> Unit,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(RoundedCornerShape(8.dp))
|
|
||||||
.padding(horizontal = 8.dp, vertical = 6.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
) {
|
|
||||||
val imageUrl = track.album?.thumbnails?.firstOrNull()?.url
|
|
||||||
if (imageUrl != null) {
|
|
||||||
AsyncImage(
|
|
||||||
model = imageUrl,
|
|
||||||
contentDescription = track.title,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(48.dp)
|
|
||||||
.clip(RoundedCornerShape(6.dp)),
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(48.dp)
|
|
||||||
.clip(RoundedCornerShape(6.dp)),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = track.title.take(1).uppercase(),
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(
|
|
||||||
text = track.title,
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = track.artists.joinToString(", ") { it.name },
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
when {
|
|
||||||
isAdded -> {
|
|
||||||
Icon(
|
|
||||||
imageVector = Iconsax.IconsaxCheckCircle,
|
|
||||||
contentDescription = "Added",
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
isAdding -> {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
strokeWidth = 2.dp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
OutlineButton(onClick = onAdd) {
|
|
||||||
Text("Add")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -17,21 +17,11 @@
|
|||||||
|
|
||||||
package dev.krtirtho.spotube.modules.playlist
|
package dev.krtirtho.spotube.modules.playlist
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
||||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayer
|
import dev.krtirtho.spotube.core.audioplayer.AudioPlayer
|
||||||
@ -41,7 +31,6 @@ import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
|||||||
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
||||||
import dev.krtirtho.spotube.core.navigation.Routes
|
import dev.krtirtho.spotube.core.navigation.Routes
|
||||||
import dev.krtirtho.spotube.core.share.ShareService
|
import dev.krtirtho.spotube.core.share.ShareService
|
||||||
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
|
||||||
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
|
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
|
||||||
import dev.krtirtho.spotube.core.ui.component.CollectionDetails
|
import dev.krtirtho.spotube.core.ui.component.CollectionDetails
|
||||||
import dev.krtirtho.spotube.core.ui.component.ErrorDisplay
|
import dev.krtirtho.spotube.core.ui.component.ErrorDisplay
|
||||||
@ -51,13 +40,6 @@ import dev.krtirtho.spotube.core.ui.component.TrackOptionsState
|
|||||||
import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
|
import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
|
||||||
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
||||||
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
||||||
import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker
|
|
||||||
import dev.krtirtho.spotube.modules.library.playlist.PlaylistFormData
|
|
||||||
import dev.krtirtho.spotube.modules.library.playlist.PlaylistFormSheet
|
|
||||||
import dev.krtirtho.spotube.modules.playlist.AddTracksToPlaylistDialog
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
@ -68,8 +50,6 @@ fun PlaylistScreen(playlistId: String) {
|
|||||||
val audioPlayer: AudioPlayer = koinInject()
|
val audioPlayer: AudioPlayer = koinInject()
|
||||||
val shareService: ShareService = koinInject()
|
val shareService: ShareService = koinInject()
|
||||||
val downloadsViewModel: DownloadsViewModel = koinViewModel()
|
val downloadsViewModel: DownloadsViewModel = koinViewModel()
|
||||||
val libraryRepository: LibraryRepository = koinInject()
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val viewModel = koinViewModel<PlaylistViewModel>(
|
val viewModel = koinViewModel<PlaylistViewModel>(
|
||||||
key = playlistId,
|
key = playlistId,
|
||||||
parameters = { parametersOf(playlistId) }
|
parameters = { parametersOf(playlistId) }
|
||||||
@ -81,11 +61,6 @@ fun PlaylistScreen(playlistId: String) {
|
|||||||
val currentCollectionEntry by audioPlayerQueue.currentCollectionEntryFlow.collectAsStateWithLifecycle()
|
val currentCollectionEntry by audioPlayerQueue.currentCollectionEntryFlow.collectAsStateWithLifecycle()
|
||||||
val playerState by audioPlayer.playerStateFlow.collectAsStateWithLifecycle()
|
val playerState by audioPlayer.playerStateFlow.collectAsStateWithLifecycle()
|
||||||
val savedPlaylistIds by viewModel.savedPlaylistIds.collectAsStateWithLifecycle()
|
val savedPlaylistIds by viewModel.savedPlaylistIds.collectAsStateWithLifecycle()
|
||||||
val currentUserId by viewModel.currentUserId.collectAsStateWithLifecycle()
|
|
||||||
var showEditPlaylist by remember { mutableStateOf(false) }
|
|
||||||
var showAddToPlaylistPicker by remember { mutableStateOf(false) }
|
|
||||||
var showAddTracksDialog by remember { mutableStateOf(false) }
|
|
||||||
var tracksToAddToPlaylist by remember { mutableStateOf<List<MetadataTrack>>(emptyList()) }
|
|
||||||
|
|
||||||
fun handleTrackOptionsAction(track: MetadataTrack, action: TrackOptionsAction) {
|
fun handleTrackOptionsAction(track: MetadataTrack, action: TrackOptionsAction) {
|
||||||
viewModel.handleTrackOptionsAction(track, action)
|
viewModel.handleTrackOptionsAction(track, action)
|
||||||
@ -98,10 +73,6 @@ fun PlaylistScreen(playlistId: String) {
|
|||||||
if (action is TrackOptionsAction.Download) {
|
if (action is TrackOptionsAction.Download) {
|
||||||
downloadsViewModel.downloadTrack(track)
|
downloadsViewModel.downloadTrack(track)
|
||||||
}
|
}
|
||||||
if (action is TrackOptionsAction.AddToPlaylist) {
|
|
||||||
tracksToAddToPlaylist = listOf(track)
|
|
||||||
showAddToPlaylistPicker = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@ -160,8 +131,6 @@ fun PlaylistScreen(playlistId: String) {
|
|||||||
val ownerName = owner?.displayName ?: owner?.username ?: "Unknown"
|
val ownerName = owner?.displayName ?: owner?.username ?: "Unknown"
|
||||||
val ownerImageURL = owner?.thumbnails?.firstOrNull()?.url
|
val ownerImageURL = owner?.thumbnails?.firstOrNull()?.url
|
||||||
|
|
||||||
val isOwner = currentUserId != null && playlist?.owner?.id == currentUserId
|
|
||||||
|
|
||||||
val savedTrackIds by viewModel.savedTrackIds.collectAsStateWithLifecycle()
|
val savedTrackIds by viewModel.savedTrackIds.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
|
||||||
@ -197,27 +166,8 @@ fun PlaylistScreen(playlistId: String) {
|
|||||||
isFollowing = savedPlaylistIds.contains(playlistId),
|
isFollowing = savedPlaylistIds.contains(playlistId),
|
||||||
onFollowClick = viewModel::toggleSavedPlaylist,
|
onFollowClick = viewModel::toggleSavedPlaylist,
|
||||||
showFollowButton = playlistId != "saved_tracks",
|
showFollowButton = playlistId != "saved_tracks",
|
||||||
onEdit = if (isOwner) { { showEditPlaylist = true } } else null,
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
footerContent = if (isOwner) {
|
|
||||||
{
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 16.dp),
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
) {
|
|
||||||
OutlineButton(onClick = { showAddTracksDialog = true }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Iconsax.IconsaxAddSquare,
|
|
||||||
contentDescription = null,
|
|
||||||
)
|
|
||||||
Text("Add Tracks", modifier = Modifier.padding(start = 8.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else null,
|
|
||||||
tracks = dataState.tracks,
|
tracks = dataState.tracks,
|
||||||
error = null,
|
error = null,
|
||||||
hasMore = dataState.nextPagination != null,
|
hasMore = dataState.nextPagination != null,
|
||||||
@ -240,61 +190,8 @@ fun PlaylistScreen(playlistId: String) {
|
|||||||
onBulkPlayNext = { tracks ->
|
onBulkPlayNext = { tracks ->
|
||||||
viewModel.playTracksNext(tracks)
|
viewModel.playTracksNext(tracks)
|
||||||
},
|
},
|
||||||
onBulkAddToPlaylist = { tracks ->
|
|
||||||
tracksToAddToPlaylist = tracks
|
|
||||||
showAddToPlaylistPicker = true
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val dataState = state as? PlaylistScreenState.Data
|
|
||||||
val playlist = (dataState as? PlaylistScreenState.Data.Loaded)?.playlist
|
|
||||||
PlaylistFormSheet(
|
|
||||||
visible = showEditPlaylist,
|
|
||||||
onDismiss = { showEditPlaylist = false },
|
|
||||||
isEditing = true,
|
|
||||||
initialData = playlist?.let {
|
|
||||||
PlaylistFormData(
|
|
||||||
name = it.title,
|
|
||||||
description = it.description.orEmpty(),
|
|
||||||
isPublic = true,
|
|
||||||
isCollaborating = false,
|
|
||||||
imagePreviewUrl = it.thumbnails.firstOrNull()?.url,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onSubmit = { data ->
|
|
||||||
viewModel.updatePlaylist(
|
|
||||||
name = data.name,
|
|
||||||
description = data.description.ifBlank { null },
|
|
||||||
isPublic = data.isPublic,
|
|
||||||
isCollaborating = data.isCollaborating,
|
|
||||||
imageBase64 = data.imageBase64.ifBlank { null },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
AddToPlaylistPicker(
|
|
||||||
visible = showAddToPlaylistPicker,
|
|
||||||
currentUserId = currentUserId,
|
|
||||||
onDismiss = { showAddToPlaylistPicker = false },
|
|
||||||
onPlaylistSelected = { selectedPlaylistId ->
|
|
||||||
scope.launch {
|
|
||||||
libraryRepository.addTracksToPlaylist(
|
|
||||||
selectedPlaylistId,
|
|
||||||
tracksToAddToPlaylist.map { it.id }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
AddTracksToPlaylistDialog(
|
|
||||||
visible = showAddTracksDialog,
|
|
||||||
playlistId = playlistId,
|
|
||||||
onDismiss = {
|
|
||||||
showAddTracksDialog = false
|
|
||||||
viewModel.refresh()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -94,9 +94,6 @@ class PlaylistViewModel(
|
|||||||
val savedPlaylistIds
|
val savedPlaylistIds
|
||||||
get() = libraryRepository.savedPlaylistIdsFlow
|
get() = libraryRepository.savedPlaylistIdsFlow
|
||||||
|
|
||||||
private val _currentUserId = MutableStateFlow<String?>(null)
|
|
||||||
val currentUserId: StateFlow<String?> = _currentUserId.asStateFlow()
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repository.pluginManager.selectedMetadataPlugin
|
repository.pluginManager.selectedMetadataPlugin
|
||||||
@ -105,15 +102,10 @@ class PlaylistViewModel(
|
|||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.collect {
|
.collect {
|
||||||
loadInitialData()
|
loadInitialData()
|
||||||
loadCurrentUser()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadCurrentUser() {
|
|
||||||
_currentUserId.value = libraryRepository.currentUser()?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun loadInitialData() = runCatching {
|
private suspend fun loadInitialData() = runCatching {
|
||||||
_state.value = PlaylistScreenState.Loading
|
_state.value = PlaylistScreenState.Loading
|
||||||
val playlistInfo = repository.getPlaylistInfo(playlistId)
|
val playlistInfo = repository.getPlaylistInfo(playlistId)
|
||||||
@ -187,33 +179,6 @@ class PlaylistViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updatePlaylist(
|
|
||||||
name: String?,
|
|
||||||
description: String?,
|
|
||||||
isPublic: Boolean?,
|
|
||||||
isCollaborating: Boolean?,
|
|
||||||
imageBase64: String?,
|
|
||||||
) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
runCatching {
|
|
||||||
libraryRepository.updatePlaylist(
|
|
||||||
id = playlistId,
|
|
||||||
name = name,
|
|
||||||
description = description,
|
|
||||||
isPublic = isPublic,
|
|
||||||
isCollaborating = isCollaborating,
|
|
||||||
imageBase64 = imageBase64,
|
|
||||||
trackIds = null,
|
|
||||||
)
|
|
||||||
}.onFailure { e ->
|
|
||||||
logger.e(e) { "Failed to update playlist" }
|
|
||||||
}.onSuccess {
|
|
||||||
repository.invalidateCaches()
|
|
||||||
loadInitialData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleTrackOptionsAction(track: MetadataTrack, action: TrackOptionsAction) {
|
fun handleTrackOptionsAction(track: MetadataTrack, action: TrackOptionsAction) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (action) {
|
when (action) {
|
||||||
@ -259,7 +224,6 @@ class PlaylistViewModel(
|
|||||||
is TrackOptionsAction.Download -> {}
|
is TrackOptionsAction.Download -> {}
|
||||||
is TrackOptionsAction.ToggleBlacklist -> {}
|
is TrackOptionsAction.ToggleBlacklist -> {}
|
||||||
is TrackOptionsAction.Share -> {}
|
is TrackOptionsAction.Share -> {}
|
||||||
is TrackOptionsAction.AddToPlaylist -> {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,12 +20,7 @@ package dev.krtirtho.spotube.modules.saved_tracks
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
||||||
@ -45,9 +40,6 @@ import dev.krtirtho.spotube.core.ui.component.TrackOptionsAction
|
|||||||
import dev.krtirtho.spotube.core.ui.component.TrackOptionsState
|
import dev.krtirtho.spotube.core.ui.component.TrackOptionsState
|
||||||
import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
|
import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
|
||||||
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
||||||
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
|
||||||
import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.jetbrains.compose.resources.DrawableResource
|
import org.jetbrains.compose.resources.DrawableResource
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
@ -61,8 +53,6 @@ fun SavedTracksScreen() {
|
|||||||
val audioPlayer: AudioPlayer = koinInject()
|
val audioPlayer: AudioPlayer = koinInject()
|
||||||
val shareService: ShareService = koinInject()
|
val shareService: ShareService = koinInject()
|
||||||
val downloadsViewModel: DownloadsViewModel = koinViewModel()
|
val downloadsViewModel: DownloadsViewModel = koinViewModel()
|
||||||
val libraryRepository: LibraryRepository = koinInject()
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val viewModel = koinViewModel<SavedTracksViewModel>(
|
val viewModel = koinViewModel<SavedTracksViewModel>(
|
||||||
key = SAVED_TRACKS_COLLECTION_ID,
|
key = SAVED_TRACKS_COLLECTION_ID,
|
||||||
parameters = { parametersOf() }
|
parameters = { parametersOf() }
|
||||||
@ -73,13 +63,6 @@ fun SavedTracksScreen() {
|
|||||||
val currentQueueEntry by audioPlayerQueue.currentQueueEntryFlow.collectAsStateWithLifecycle()
|
val currentQueueEntry by audioPlayerQueue.currentQueueEntryFlow.collectAsStateWithLifecycle()
|
||||||
val currentCollectionEntry by audioPlayerQueue.currentCollectionEntryFlow.collectAsStateWithLifecycle()
|
val currentCollectionEntry by audioPlayerQueue.currentCollectionEntryFlow.collectAsStateWithLifecycle()
|
||||||
val playerState by audioPlayer.playerStateFlow.collectAsStateWithLifecycle()
|
val playerState by audioPlayer.playerStateFlow.collectAsStateWithLifecycle()
|
||||||
var showAddToPlaylistPicker by remember { mutableStateOf(false) }
|
|
||||||
var tracksToAddToPlaylist by remember { mutableStateOf<List<MetadataTrack>>(emptyList()) }
|
|
||||||
var currentUserId by remember { mutableStateOf<String?>(null) }
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
currentUserId = libraryRepository.currentUser()?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getTrackOptionsState(track: MetadataTrack): TrackOptionsState {
|
fun getTrackOptionsState(track: MetadataTrack): TrackOptionsState {
|
||||||
val currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id
|
val currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id
|
||||||
@ -105,10 +88,6 @@ fun SavedTracksScreen() {
|
|||||||
if (action is TrackOptionsAction.Download) {
|
if (action is TrackOptionsAction.Download) {
|
||||||
downloadsViewModel.downloadTrack(track)
|
downloadsViewModel.downloadTrack(track)
|
||||||
}
|
}
|
||||||
if (action is TrackOptionsAction.AddToPlaylist) {
|
|
||||||
tracksToAddToPlaylist = listOf(track)
|
|
||||||
showAddToPlaylistPicker = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@ -208,26 +187,8 @@ fun SavedTracksScreen() {
|
|||||||
onBulkPlayNext = { tracks ->
|
onBulkPlayNext = { tracks ->
|
||||||
viewModel.playTracksNext(tracks)
|
viewModel.playTracksNext(tracks)
|
||||||
},
|
},
|
||||||
onBulkAddToPlaylist = { tracks ->
|
|
||||||
tracksToAddToPlaylist = tracks
|
|
||||||
showAddToPlaylistPicker = true
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AddToPlaylistPicker(
|
|
||||||
visible = showAddToPlaylistPicker,
|
|
||||||
currentUserId = currentUserId,
|
|
||||||
onDismiss = { showAddToPlaylistPicker = false },
|
|
||||||
onPlaylistSelected = { playlistId ->
|
|
||||||
scope.launch {
|
|
||||||
libraryRepository.addTracksToPlaylist(
|
|
||||||
playlistId,
|
|
||||||
tracksToAddToPlaylist.map { it.id }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -197,7 +197,6 @@ class SavedTracksViewModel(
|
|||||||
is TrackOptionsAction.Download -> {}
|
is TrackOptionsAction.Download -> {}
|
||||||
is TrackOptionsAction.ToggleBlacklist -> {}
|
is TrackOptionsAction.ToggleBlacklist -> {}
|
||||||
is TrackOptionsAction.Share -> {}
|
is TrackOptionsAction.Share -> {}
|
||||||
is TrackOptionsAction.AddToPlaylist -> {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,10 +57,8 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@ -99,8 +97,6 @@ import dev.krtirtho.spotube.core.ui.component.UserCard
|
|||||||
import dev.krtirtho.spotube.core.ui.component.cards.PlayableCard
|
import dev.krtirtho.spotube.core.ui.component.cards.PlayableCard
|
||||||
import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
|
import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
|
||||||
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
||||||
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
|
||||||
import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker
|
|
||||||
import dev.krtirtho.spotube.modules.shell.LocalAppShellBottomInset
|
import dev.krtirtho.spotube.modules.shell.LocalAppShellBottomInset
|
||||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken
|
||||||
@ -119,18 +115,10 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
|
|||||||
val audioPlayerQueue: AudioPlayerQueue = koinInject()
|
val audioPlayerQueue: AudioPlayerQueue = koinInject()
|
||||||
val shareService: ShareService = koinInject()
|
val shareService: ShareService = koinInject()
|
||||||
val downloadsViewModel: DownloadsViewModel = koinViewModel()
|
val downloadsViewModel: DownloadsViewModel = koinViewModel()
|
||||||
val libraryRepository: LibraryRepository = koinInject()
|
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
val selectedType = state.selectedSearchType
|
val selectedType = state.selectedSearchType
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val savedTrackIds by viewModel.savedTrackIds.collectAsStateWithLifecycle()
|
val savedTrackIds by viewModel.savedTrackIds.collectAsStateWithLifecycle()
|
||||||
var showAddToPlaylistPicker by remember { mutableStateOf(false) }
|
|
||||||
var tracksToAddToPlaylist by remember { mutableStateOf<List<MetadataTrack>>(emptyList()) }
|
|
||||||
var currentUserId by remember { mutableStateOf<String?>(null) }
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
currentUserId = libraryRepository.currentUser()?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
@ -187,10 +175,6 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
|
|||||||
shareService.share(uri, track.title)
|
shareService.share(uri, track.title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is TrackOptionsAction.AddToPlaylist -> {
|
|
||||||
tracksToAddToPlaylist = listOf(track)
|
|
||||||
showAddToPlaylistPicker = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -311,10 +295,6 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
|
|||||||
onBulkDownload = { tracks -> downloadsViewModel.downloadTracks(tracks) },
|
onBulkDownload = { tracks -> downloadsViewModel.downloadTracks(tracks) },
|
||||||
onBulkAddToQueue = ::bulkAddToQueue,
|
onBulkAddToQueue = ::bulkAddToQueue,
|
||||||
onBulkPlayNext = ::bulkPlayNext,
|
onBulkPlayNext = ::bulkPlayNext,
|
||||||
onBulkAddToPlaylist = { tracks ->
|
|
||||||
tracksToAddToPlaylist = tracks
|
|
||||||
showAddToPlaylistPicker = true
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
)
|
)
|
||||||
@ -331,10 +311,6 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
|
|||||||
onBulkDownload = { tracks -> downloadsViewModel.downloadTracks(tracks) },
|
onBulkDownload = { tracks -> downloadsViewModel.downloadTracks(tracks) },
|
||||||
onBulkAddToQueue = ::bulkAddToQueue,
|
onBulkAddToQueue = ::bulkAddToQueue,
|
||||||
onBulkPlayNext = ::bulkPlayNext,
|
onBulkPlayNext = ::bulkPlayNext,
|
||||||
onBulkAddToPlaylist = { tracks ->
|
|
||||||
tracksToAddToPlaylist = tracks
|
|
||||||
showAddToPlaylistPicker = true
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
)
|
)
|
||||||
@ -381,20 +357,6 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AddToPlaylistPicker(
|
|
||||||
visible = showAddToPlaylistPicker,
|
|
||||||
currentUserId = currentUserId,
|
|
||||||
onDismiss = { showAddToPlaylistPicker = false },
|
|
||||||
onPlaylistSelected = { playlistId ->
|
|
||||||
scope.launch {
|
|
||||||
libraryRepository.addTracksToPlaylist(
|
|
||||||
playlistId,
|
|
||||||
tracksToAddToPlaylist.map { it.id }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -606,7 +568,6 @@ private fun SearchAllTab(
|
|||||||
onBulkDownload: (List<MetadataTrack>) -> Unit,
|
onBulkDownload: (List<MetadataTrack>) -> Unit,
|
||||||
onBulkAddToQueue: (List<MetadataTrack>) -> Unit,
|
onBulkAddToQueue: (List<MetadataTrack>) -> Unit,
|
||||||
onBulkPlayNext: (List<MetadataTrack>) -> Unit,
|
onBulkPlayNext: (List<MetadataTrack>) -> Unit,
|
||||||
onBulkAddToPlaylist: (List<MetadataTrack>) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if (query.isBlank()) {
|
if (query.isBlank()) {
|
||||||
@ -660,7 +621,6 @@ private fun SearchAllTab(
|
|||||||
onBulkDownload = onBulkDownload,
|
onBulkDownload = onBulkDownload,
|
||||||
onBulkAddToQueue = onBulkAddToQueue,
|
onBulkAddToQueue = onBulkAddToQueue,
|
||||||
onBulkPlayNext = onBulkPlayNext,
|
onBulkPlayNext = onBulkPlayNext,
|
||||||
onBulkAddToPlaylist = onBulkAddToPlaylist,
|
|
||||||
contentPadding = PaddingValues(top = 12.dp, bottom = 16.dp + bottomInset),
|
contentPadding = PaddingValues(top = 12.dp, bottom = 16.dp + bottomInset),
|
||||||
headerContent = {
|
headerContent = {
|
||||||
if (showTracks) {
|
if (showTracks) {
|
||||||
@ -744,7 +704,6 @@ private fun SearchTracksTab(
|
|||||||
onBulkDownload: (List<MetadataTrack>) -> Unit,
|
onBulkDownload: (List<MetadataTrack>) -> Unit,
|
||||||
onBulkAddToQueue: (List<MetadataTrack>) -> Unit,
|
onBulkAddToQueue: (List<MetadataTrack>) -> Unit,
|
||||||
onBulkPlayNext: (List<MetadataTrack>) -> Unit,
|
onBulkPlayNext: (List<MetadataTrack>) -> Unit,
|
||||||
onBulkAddToPlaylist: (List<MetadataTrack>) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if (query.isBlank()) {
|
if (query.isBlank()) {
|
||||||
@ -770,7 +729,6 @@ private fun SearchTracksTab(
|
|||||||
onBulkDownload = onBulkDownload,
|
onBulkDownload = onBulkDownload,
|
||||||
onBulkAddToQueue = onBulkAddToQueue,
|
onBulkAddToQueue = onBulkAddToQueue,
|
||||||
onBulkPlayNext = onBulkPlayNext,
|
onBulkPlayNext = onBulkPlayNext,
|
||||||
onBulkAddToPlaylist = onBulkAddToPlaylist,
|
|
||||||
simplified = true,
|
simplified = true,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -91,7 +91,7 @@ fun AppBottombar(
|
|||||||
contentDescription = label,
|
contentDescription = label,
|
||||||
tint = if (selected) MaterialTheme.colorScheme.primary
|
tint = if (selected) MaterialTheme.colorScheme.primary
|
||||||
else MaterialTheme.colorScheme.onSurfaceVariant,
|
else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp),
|
||||||
)
|
)
|
||||||
if (screen == Routes.Library) {
|
if (screen == Routes.Library) {
|
||||||
DownloadBadgeIndicator(
|
DownloadBadgeIndicator(
|
||||||
|
|||||||
@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@ -34,8 +33,10 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedIconButton
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@ -68,9 +69,6 @@ import org.koin.compose.koinInject
|
|||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import dev.krtirtho.spotube.core.ui.base.BaseUITheme
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.IconButton
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
|
||||||
|
|
||||||
// The floating player on small screens that appears above the floating AppBottombar
|
// The floating player on small screens that appears above the floating AppBottombar
|
||||||
//
|
//
|
||||||
@ -187,17 +185,15 @@ fun AppFloatingPlayer(
|
|||||||
audioPlayerQueue = audioPlayerQueue,
|
audioPlayerQueue = audioPlayerQueue,
|
||||||
savedTracksViewModel = savedTracksViewModel,
|
savedTracksViewModel = savedTracksViewModel,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.size(8.dp))
|
OutlinedIconButton(
|
||||||
IconButton(
|
|
||||||
onClick = ::onPlayPause,
|
onClick = ::onPlayPause,
|
||||||
enabled = playerUiState.queue.isNotEmpty(),
|
enabled = playerUiState.queue.isNotEmpty()
|
||||||
theme = LocalBaseUITheme.current.iconButtons.outline.copy(
|
|
||||||
shape = BaseUITheme.InteractionState.fromSingleValue(CircleShape)
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay,
|
if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay,
|
||||||
contentDescription = if (playerUiState.isPlaying) "Pause" else "Play",
|
contentDescription = if (playerUiState.isPlaying) "Pause" else "Play",
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.inverseOnSurface,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import androidx.compose.animation.core.animateDpAsState
|
|||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@ -39,22 +40,18 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.text.font.Font
|
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.krtirtho.spotube.core.navigation.NavigationState
|
import dev.krtirtho.spotube.core.navigation.NavigationState
|
||||||
import dev.krtirtho.spotube.core.navigation.Navigator
|
import dev.krtirtho.spotube.core.navigation.Navigator
|
||||||
import dev.krtirtho.spotube.core.navigation.Routes
|
import dev.krtirtho.spotube.core.navigation.Routes
|
||||||
import dev.krtirtho.spotube.core.ui.base.GhostIconButton
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
||||||
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
||||||
import dev.krtirtho.spotube.core.ui.base.SecondaryButton
|
import dev.krtirtho.spotube.core.ui.base.SecondaryButton
|
||||||
@ -66,10 +63,7 @@ import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
|||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSidebarLeftBroken
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxSidebarLeftBroken
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSidebarRightBroken
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxSidebarRightBroken
|
||||||
import dev.krtirtho.spotube.tabs
|
import dev.krtirtho.spotube.tabs
|
||||||
import org.jetbrains.compose.resources.Font
|
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
import spotube.composeapp.generated.resources.Res
|
|
||||||
import spotube.composeapp.generated.resources.cookie_regular
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppSidebar(
|
fun AppSidebar(
|
||||||
@ -99,25 +93,18 @@ fun AppSidebar(
|
|||||||
AnimatedVisibility(visible = expanded, enter = fadeIn(), exit = fadeOut()) {
|
AnimatedVisibility(visible = expanded, enter = fadeIn(), exit = fadeOut()) {
|
||||||
Text(
|
Text(
|
||||||
text = "Spotube",
|
text = "Spotube",
|
||||||
style = MaterialTheme.typography.headlineLarge.copy(
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontFamily = FontFamily(
|
|
||||||
listOf(
|
|
||||||
Font(Res.font.cookie_regular, weight = FontWeight.Normal)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
GhostIconButton(
|
Icon(
|
||||||
onClick = { expanded = !expanded }
|
imageVector = if (expanded) Iconsax.IconsaxSidebarLeftBroken else Iconsax.IconsaxSidebarRightBroken,
|
||||||
) {
|
contentDescription = if (expanded) "Collapse sidebar" else "Expand sidebar",
|
||||||
Icon(
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
imageVector = if (expanded) Iconsax.IconsaxSidebarLeftBroken else Iconsax.IconsaxSidebarRightBroken,
|
modifier = Modifier
|
||||||
contentDescription = if (expanded) "Collapse sidebar" else "Expand sidebar",
|
.size(22.dp)
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
.clickable { expanded = !expanded }
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
@ -202,7 +189,7 @@ fun SidebarItem(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = if (expanded) Modifier.weight(1f) else Modifier)
|
Spacer(modifier = Modifier.weight(if (expanded) 1f else 0f))
|
||||||
}
|
}
|
||||||
|
|
||||||
val buttonModifier = Modifier
|
val buttonModifier = Modifier
|
||||||
|
|||||||
@ -20,8 +20,6 @@ package dev.krtirtho.spotube.modules.shell.player_queue
|
|||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.hoverable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@ -55,28 +53,22 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
|
import compose.icons.FeatherIcons
|
||||||
|
import compose.icons.feathericons.MoreVertical
|
||||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
||||||
import dev.krtirtho.spotube.core.di.rememberLogger
|
import dev.krtirtho.spotube.core.di.rememberLogger
|
||||||
import dev.krtirtho.spotube.core.ui.base.Card
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.GhostIconButton
|
import dev.krtirtho.spotube.core.ui.base.GhostIconButton
|
||||||
import dev.krtirtho.spotube.core.ui.base.IconButton
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.ListRowTile
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
||||||
import dev.krtirtho.spotube.core.ui.base.SecondaryIconButton
|
import dev.krtirtho.spotube.core.ui.base.SecondaryIconButton
|
||||||
import dev.krtirtho.spotube.core.ui.base.TextField
|
import dev.krtirtho.spotube.core.ui.base.TextField
|
||||||
import dev.krtirtho.spotube.core.ui.base.copyShape
|
import dev.krtirtho.spotube.core.ui.base.copyShape
|
||||||
import dev.krtirtho.spotube.core.ui.base.highlight
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax3DotsMore
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxDragHandle
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxFilterSearch
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxFilterSearch
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicSquareRemove
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicSquareRemove
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSetting
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import sh.calvin.reorderable.ReorderableItem
|
import sh.calvin.reorderable.ReorderableItem
|
||||||
@ -135,7 +127,7 @@ fun PlayerQueueContent(
|
|||||||
val filtered = if (isFiltered) {
|
val filtered = if (isFiltered) {
|
||||||
sourceItems.filter { item ->
|
sourceItems.filter { item ->
|
||||||
item.title.lowercase().contains(normalizedFilter) ||
|
item.title.lowercase().contains(normalizedFilter) ||
|
||||||
item.subtitle.lowercase().contains(normalizedFilter)
|
item.subtitle.lowercase().contains(normalizedFilter)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sourceItems
|
sourceItems
|
||||||
@ -196,9 +188,9 @@ fun PlayerQueueContent(
|
|||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
IconButton(
|
SecondaryIconButton(
|
||||||
onClick = viewModel::clearQueue,
|
onClick = viewModel::clearQueue,
|
||||||
theme = LocalBaseUITheme.current.iconButtons.outline.copyShape(MaterialTheme.shapes.small),
|
theme = LocalBaseUITheme.current.iconButtons.secondary.copyShape(MaterialTheme.shapes.small),
|
||||||
) {
|
) {
|
||||||
Icon(Iconsax.IconsaxTrash, contentDescription = "Clear Queue")
|
Icon(Iconsax.IconsaxTrash, contentDescription = "Clear Queue")
|
||||||
}
|
}
|
||||||
@ -211,23 +203,24 @@ fun PlayerQueueContent(
|
|||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Card {
|
LazyColumn(
|
||||||
LazyColumn(
|
modifier = Modifier.fillMaxSize(),
|
||||||
modifier = Modifier.fillMaxSize(),
|
state = lazyListState,
|
||||||
state = lazyListState,
|
contentPadding = PaddingValues(bottom = 8.dp),
|
||||||
contentPadding = PaddingValues(bottom = 8.dp),
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
) {
|
) {
|
||||||
items(displayList, key = { item -> item.id }) { item ->
|
items(displayList, key = { item -> item.id }) { item ->
|
||||||
ReorderableItem(reorderableLazyListState, key = item.id) { isDragging ->
|
ReorderableItem(reorderableLazyListState, key = item.id) { isDragging ->
|
||||||
val elevation by animateDpAsState(if (isDragging) 8.dp else 0.dp)
|
val elevation by animateDpAsState(if (isDragging) 8.dp else 0.dp)
|
||||||
QueueItemRow(
|
QueueItemRow(
|
||||||
item = item,
|
item = item,
|
||||||
reorderScope = if (isFiltered) null else this,
|
isDragging = isDragging,
|
||||||
onPlayClick = { viewModel.playQueueItem(item.originalIndex) },
|
elevation = elevation,
|
||||||
onRemoveClick = { viewModel.removeQueueItem(item.originalIndex) },
|
reorderScope = if (isFiltered) null else this,
|
||||||
onDragStopped = ::finalizeReorder,
|
onPlayClick = { viewModel.playQueueItem(item.originalIndex) },
|
||||||
)
|
onRemoveClick = { viewModel.removeQueueItem(item.originalIndex) },
|
||||||
}
|
onDragStopped = ::finalizeReorder,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -239,6 +232,8 @@ fun PlayerQueueContent(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun QueueItemRow(
|
private fun QueueItemRow(
|
||||||
item: QueueItemUi,
|
item: QueueItemUi,
|
||||||
|
isDragging: Boolean,
|
||||||
|
elevation: androidx.compose.ui.unit.Dp,
|
||||||
reorderScope: sh.calvin.reorderable.ReorderableCollectionItemScope?,
|
reorderScope: sh.calvin.reorderable.ReorderableCollectionItemScope?,
|
||||||
onPlayClick: () -> Unit,
|
onPlayClick: () -> Unit,
|
||||||
onRemoveClick: () -> Unit,
|
onRemoveClick: () -> Unit,
|
||||||
@ -246,78 +241,91 @@ private fun QueueItemRow(
|
|||||||
) {
|
) {
|
||||||
var showMenu by remember { mutableStateOf(false) }
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
ListRowTile(
|
val rowColor = if (item.isCurrent) {
|
||||||
onClick = onPlayClick,
|
MaterialTheme.colorScheme.secondaryContainer
|
||||||
selected = item.isCurrent,
|
} else {
|
||||||
modifier = Modifier,
|
MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
leading = {
|
}
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(72.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Iconsax.IconsaxDragHandle,
|
|
||||||
contentDescription = if (reorderScope != null) "Reorder" else null,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(24.dp)
|
|
||||||
.then(
|
|
||||||
if (reorderScope != null) {
|
|
||||||
with(reorderScope) { Modifier.draggableHandle(onDragStopped = onDragStopped) }
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
},
|
|
||||||
),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
|
|
||||||
Box(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(48.dp)
|
.fillMaxWidth()
|
||||||
.clip(MaterialTheme.shapes.small)
|
.clip(MaterialTheme.shapes.medium)
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
.clickable(onClick = onPlayClick),
|
||||||
contentAlignment = Alignment.Center,
|
shadowElevation = elevation,
|
||||||
) {
|
tonalElevation = if (item.isCurrent) 2.dp else 0.dp,
|
||||||
if (item.imageUrl != null) {
|
) {
|
||||||
AsyncImage(
|
Row(
|
||||||
model = item.imageUrl,
|
modifier = Modifier
|
||||||
contentDescription = null,
|
.fillMaxWidth()
|
||||||
modifier = Modifier.fillMaxSize(),
|
.height(72.dp)
|
||||||
contentScale = ContentScale.Crop,
|
.background(rowColor)
|
||||||
)
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
} else {
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
Text(
|
) {
|
||||||
text = "${item.originalIndex + 1}",
|
Icon(
|
||||||
style = MaterialTheme.typography.labelMedium,
|
FeatherIcons.MoreVertical,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
contentDescription = if (reorderScope != null) "Reorder" else null,
|
||||||
)
|
modifier = Modifier
|
||||||
}
|
.size(24.dp)
|
||||||
|
.then(
|
||||||
|
if (reorderScope != null) {
|
||||||
|
with(reorderScope) { Modifier.draggableHandle(onDragStopped = onDragStopped) }
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
},
|
||||||
|
),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.clip(MaterialTheme.shapes.small)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
if (item.imageUrl != null) {
|
||||||
|
AsyncImage(
|
||||||
|
model = item.imageUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "${item.originalIndex + 1}",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
title = {
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
Text(
|
|
||||||
text = item.title,
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
Text(
|
||||||
maxLines = 1,
|
text = item.title,
|
||||||
overflow = TextOverflow.Ellipsis,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = if (item.isCurrent) {
|
maxLines = 1,
|
||||||
MaterialTheme.colorScheme.onSecondaryContainer
|
overflow = TextOverflow.Ellipsis,
|
||||||
} else {
|
color = if (item.isCurrent) {
|
||||||
MaterialTheme.colorScheme.onSurface
|
MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
},
|
} else {
|
||||||
)
|
MaterialTheme.colorScheme.onSurface
|
||||||
},
|
},
|
||||||
subtitle = {
|
)
|
||||||
Text(
|
Text(
|
||||||
text = item.subtitle,
|
text = item.subtitle,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
trailing = {
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = item.durationMs.toDurationString(),
|
text = item.durationMs.toDurationString(),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
@ -332,7 +340,7 @@ private fun QueueItemRow(
|
|||||||
modifier = Modifier.size(36.dp),
|
modifier = Modifier.size(36.dp),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Iconsax.Iconsax3DotsMore,
|
FeatherIcons.MoreVertical,
|
||||||
contentDescription = "More options",
|
contentDescription = "More options",
|
||||||
modifier = Modifier.size(18.dp),
|
modifier = Modifier.size(18.dp),
|
||||||
)
|
)
|
||||||
@ -354,7 +362,7 @@ private fun QueueItemRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun QueueEntry.toQueueDisplayData(): Tuple4<String, String, Long, String?> {
|
private fun QueueEntry.toQueueDisplayData(): Tuple4<String, String, Long, String?> {
|
||||||
|
|||||||
@ -1,3 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package dev.krtirtho.spotube.resources.iconsax
|
package dev.krtirtho.spotube.resources.iconsax
|
||||||
|
|
||||||
object Iconsax
|
object Iconsax
|
||||||
|
|||||||
@ -1,128 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package dev.krtirtho.spotube.resources.iconsax
|
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
|
||||||
import androidx.compose.ui.graphics.vector.group
|
|
||||||
import androidx.compose.ui.graphics.vector.path
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
|
|
||||||
val Iconsax.IconsaxDragHandle: ImageVector
|
|
||||||
get() {
|
|
||||||
if (_IconsaxDragHandle != null) {
|
|
||||||
return _IconsaxDragHandle!!
|
|
||||||
}
|
|
||||||
_IconsaxDragHandle = ImageVector.Builder(
|
|
||||||
name = "IconsaxDragHandle",
|
|
||||||
defaultWidth = 24.dp,
|
|
||||||
defaultHeight = 24.dp,
|
|
||||||
viewportWidth = 24f,
|
|
||||||
viewportHeight = 24f
|
|
||||||
).apply {
|
|
||||||
group(
|
|
||||||
clipPathData = PathData {
|
|
||||||
moveTo(0f, 0f)
|
|
||||||
horizontalLineToRelative(24f)
|
|
||||||
verticalLineToRelative(24f)
|
|
||||||
horizontalLineToRelative(-24f)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
path(
|
|
||||||
fill = SolidColor(Color.White),
|
|
||||||
fillAlpha = 0.4f,
|
|
||||||
strokeAlpha = 0.4f
|
|
||||||
) {
|
|
||||||
moveTo(8f, 4f)
|
|
||||||
curveTo(6.9f, 4f, 6f, 4.9f, 6f, 6f)
|
|
||||||
curveTo(6f, 7.1f, 6.9f, 8f, 8f, 8f)
|
|
||||||
curveTo(9.1f, 8f, 10f, 7.1f, 10f, 6f)
|
|
||||||
curveTo(10f, 4.9f, 9.1f, 4f, 8f, 4f)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
path(
|
|
||||||
fill = SolidColor(Color.White),
|
|
||||||
fillAlpha = 0.4f,
|
|
||||||
strokeAlpha = 0.4f
|
|
||||||
) {
|
|
||||||
moveTo(16f, 4f)
|
|
||||||
curveTo(14.9f, 4f, 14f, 4.9f, 14f, 6f)
|
|
||||||
curveTo(14f, 7.1f, 14.9f, 8f, 16f, 8f)
|
|
||||||
curveTo(17.1f, 8f, 18f, 7.1f, 18f, 6f)
|
|
||||||
curveTo(18f, 4.9f, 17.1f, 4f, 16f, 4f)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
path(
|
|
||||||
fill = SolidColor(Color.White),
|
|
||||||
fillAlpha = 0.4f,
|
|
||||||
strokeAlpha = 0.4f
|
|
||||||
) {
|
|
||||||
moveTo(8f, 10f)
|
|
||||||
curveTo(6.9f, 10f, 6f, 10.9f, 6f, 12f)
|
|
||||||
curveTo(6f, 13.1f, 6.9f, 14f, 8f, 14f)
|
|
||||||
curveTo(9.1f, 14f, 10f, 13.1f, 10f, 12f)
|
|
||||||
curveTo(10f, 10.9f, 9.1f, 10f, 8f, 10f)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
path(
|
|
||||||
fill = SolidColor(Color.White),
|
|
||||||
fillAlpha = 0.4f,
|
|
||||||
strokeAlpha = 0.4f
|
|
||||||
) {
|
|
||||||
moveTo(16f, 10f)
|
|
||||||
curveTo(14.9f, 10f, 14f, 10.9f, 14f, 12f)
|
|
||||||
curveTo(14f, 13.1f, 14.9f, 14f, 16f, 14f)
|
|
||||||
curveTo(17.1f, 14f, 18f, 13.1f, 18f, 12f)
|
|
||||||
curveTo(18f, 10.9f, 17.1f, 10f, 16f, 10f)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
path(
|
|
||||||
fill = SolidColor(Color.White),
|
|
||||||
fillAlpha = 0.4f,
|
|
||||||
strokeAlpha = 0.4f
|
|
||||||
) {
|
|
||||||
moveTo(8f, 16f)
|
|
||||||
curveTo(6.9f, 16f, 6f, 16.9f, 6f, 18f)
|
|
||||||
curveTo(6f, 19.1f, 6.9f, 20f, 8f, 20f)
|
|
||||||
curveTo(9.1f, 20f, 10f, 19.1f, 10f, 18f)
|
|
||||||
curveTo(10f, 16.9f, 9.1f, 16f, 8f, 16f)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
path(
|
|
||||||
fill = SolidColor(Color.White),
|
|
||||||
fillAlpha = 0.4f,
|
|
||||||
strokeAlpha = 0.4f
|
|
||||||
) {
|
|
||||||
moveTo(16f, 16f)
|
|
||||||
curveTo(14.9f, 16f, 14f, 16.9f, 14f, 18f)
|
|
||||||
curveTo(14f, 19.1f, 14.9f, 20f, 16f, 20f)
|
|
||||||
curveTo(17.1f, 20f, 18f, 19.1f, 18f, 18f)
|
|
||||||
curveTo(18f, 16.9f, 17.1f, 16f, 16f, 16f)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return _IconsaxDragHandle!!
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("ObjectPropertyName")
|
|
||||||
private var _IconsaxDragHandle: ImageVector? = null
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
package dev.krtirtho.spotube.resources.iconsax
|
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
|
||||||
import androidx.compose.ui.graphics.vector.group
|
|
||||||
import androidx.compose.ui.graphics.vector.path
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
|
|
||||||
val Iconsax.IconsaxEdit: ImageVector
|
|
||||||
get() {
|
|
||||||
if (_IconsaxEdit != null) {
|
|
||||||
return _IconsaxEdit!!
|
|
||||||
}
|
|
||||||
_IconsaxEdit = ImageVector.Builder(
|
|
||||||
name = "IconsaxEdit",
|
|
||||||
defaultWidth = 24.dp,
|
|
||||||
defaultHeight = 24.dp,
|
|
||||||
viewportWidth = 24f,
|
|
||||||
viewportHeight = 24f
|
|
||||||
).apply {
|
|
||||||
group(
|
|
||||||
clipPathData = PathData {
|
|
||||||
moveTo(0f, 0f)
|
|
||||||
horizontalLineToRelative(24f)
|
|
||||||
verticalLineToRelative(24f)
|
|
||||||
horizontalLineToRelative(-24f)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
path(
|
|
||||||
fill = SolidColor(Color.White),
|
|
||||||
fillAlpha = 0.4f,
|
|
||||||
strokeAlpha = 0.4f
|
|
||||||
) {
|
|
||||||
moveTo(15.48f, 3f)
|
|
||||||
horizontalLineTo(7.52f)
|
|
||||||
curveTo(4.07f, 3f, 2f, 5.06f, 2f, 8.52f)
|
|
||||||
verticalLineTo(16.47f)
|
|
||||||
curveTo(2f, 19.94f, 4.07f, 22f, 7.52f, 22f)
|
|
||||||
horizontalLineTo(15.47f)
|
|
||||||
curveTo(18.93f, 22f, 20.99f, 19.94f, 20.99f, 16.48f)
|
|
||||||
verticalLineTo(8.52f)
|
|
||||||
curveTo(21f, 5.06f, 18.93f, 3f, 15.48f, 3f)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
path(fill = SolidColor(Color.White)) {
|
|
||||||
moveTo(21.02f, 2.98f)
|
|
||||||
curveTo(19.23f, 1.18f, 17.48f, 1.14f, 15.64f, 2.98f)
|
|
||||||
lineTo(14.51f, 4.1f)
|
|
||||||
curveTo(14.41f, 4.2f, 14.38f, 4.34f, 14.42f, 4.47f)
|
|
||||||
curveTo(15.12f, 6.92f, 17.08f, 8.88f, 19.53f, 9.58f)
|
|
||||||
curveTo(19.56f, 9.59f, 19.61f, 9.59f, 19.64f, 9.59f)
|
|
||||||
curveTo(19.74f, 9.59f, 19.84f, 9.55f, 19.91f, 9.48f)
|
|
||||||
lineTo(21.02f, 8.36f)
|
|
||||||
curveTo(21.93f, 7.45f, 22.38f, 6.58f, 22.38f, 5.69f)
|
|
||||||
curveTo(22.38f, 4.79f, 21.93f, 3.9f, 21.02f, 2.98f)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
path(fill = SolidColor(Color.White)) {
|
|
||||||
moveTo(17.86f, 10.42f)
|
|
||||||
curveTo(17.59f, 10.29f, 17.33f, 10.16f, 17.09f, 10.01f)
|
|
||||||
curveTo(16.89f, 9.89f, 16.69f, 9.76f, 16.5f, 9.62f)
|
|
||||||
curveTo(16.34f, 9.52f, 16.16f, 9.37f, 15.98f, 9.22f)
|
|
||||||
curveTo(15.96f, 9.21f, 15.9f, 9.16f, 15.82f, 9.08f)
|
|
||||||
curveTo(15.51f, 8.83f, 15.18f, 8.49f, 14.87f, 8.12f)
|
|
||||||
curveTo(14.85f, 8.1f, 14.79f, 8.04f, 14.74f, 7.95f)
|
|
||||||
curveTo(14.64f, 7.84f, 14.49f, 7.65f, 14.36f, 7.44f)
|
|
||||||
curveTo(14.25f, 7.3f, 14.12f, 7.1f, 14f, 6.89f)
|
|
||||||
curveTo(13.85f, 6.64f, 13.72f, 6.39f, 13.6f, 6.13f)
|
|
||||||
curveTo(13.47f, 5.85f, 13.37f, 5.59f, 13.28f, 5.34f)
|
|
||||||
lineTo(7.9f, 10.72f)
|
|
||||||
curveTo(7.55f, 11.07f, 7.21f, 11.73f, 7.14f, 12.22f)
|
|
||||||
lineTo(6.71f, 15.2f)
|
|
||||||
curveTo(6.62f, 15.83f, 6.79f, 16.42f, 7.18f, 16.81f)
|
|
||||||
curveTo(7.51f, 17.14f, 7.96f, 17.31f, 8.46f, 17.31f)
|
|
||||||
curveTo(8.57f, 17.31f, 8.68f, 17.3f, 8.79f, 17.29f)
|
|
||||||
lineTo(11.76f, 16.87f)
|
|
||||||
curveTo(12.25f, 16.8f, 12.91f, 16.47f, 13.26f, 16.11f)
|
|
||||||
lineTo(18.64f, 10.73f)
|
|
||||||
curveTo(18.39f, 10.65f, 18.14f, 10.54f, 17.86f, 10.42f)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return _IconsaxEdit!!
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("ObjectPropertyName")
|
|
||||||
private var _IconsaxEdit: ImageVector? = null
|
|
||||||
@ -104,8 +104,6 @@ object FakeMetadataStore {
|
|||||||
|
|
||||||
fun getUser(id: String): MetadataUser? = users[id]?.toModel()
|
fun getUser(id: String): MetadataUser? = users[id]?.toModel()
|
||||||
|
|
||||||
fun getCurrentUser(): MetadataUser? = users[CURRENT_USER_ID]?.toModel()
|
|
||||||
|
|
||||||
fun getTrack(id: String): MetadataTrack = tracks[id]?.toModel()
|
fun getTrack(id: String): MetadataTrack = tracks[id]?.toModel()
|
||||||
?: error("Track not found: $id")
|
?: error("Track not found: $id")
|
||||||
|
|
||||||
|
|||||||
@ -23,8 +23,4 @@ class RealMetadataUserAPI : MetadataUserAPI {
|
|||||||
override suspend fun getUser(id: String): MetadataUser? {
|
override suspend fun getUser(id: String): MetadataUser? {
|
||||||
return FakeMetadataStore.getUser(id)
|
return FakeMetadataStore.getUser(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun me(): MetadataUser? {
|
|
||||||
return FakeMetadataStore.getCurrentUser()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user