mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-08-05 19:59:51 +00:00
Compare commits
16 Commits
6550beceb4
...
9ad2c5c5c3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ad2c5c5c3 | ||
|
|
16d4adf59c | ||
|
|
3d7d15541f | ||
|
|
70cabedc9e | ||
|
|
0b3d21f255 | ||
|
|
9d79d055ba | ||
|
|
a590fa59f9 | ||
|
|
1ef1eff3cb | ||
|
|
6ac934ad19 | ||
|
|
49e969cc0f | ||
|
|
8692c344d6 | ||
|
|
a38b9e48cc | ||
|
|
2aae112cbe | ||
|
|
d53ac853a5 | ||
|
|
69a464dcd2 | ||
|
|
941b333a79 |
70
.github/workflows/spotube-release-binary.yml
vendored
70
.github/workflows/spotube-release-binary.yml
vendored
@ -97,8 +97,6 @@ jobs:
|
||||
name: Android
|
||||
needs: prepare-deps
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GRADLE_FLAGS: ${{ inputs.release_type == 'nightly' && '-PversionName=nightly' || '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@ -135,26 +133,23 @@ jobs:
|
||||
EOF
|
||||
|
||||
- name: Assemble Release
|
||||
run: ./gradlew :composeApp:assembleRelease ${{ env.GRADLE_FLAGS }}
|
||||
run: ./gradlew :composeApp:assembleRelease
|
||||
|
||||
- name: Rename APK
|
||||
run: |
|
||||
APK_FILE=$(find composeApp/build/outputs/apk/release -name "*.apk" -type f | head -1)
|
||||
cp "$APK_FILE" Spotube-android-all-arch.apk
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: android-apk
|
||||
path: composeApp/build/outputs/apk/release/*.apk
|
||||
|
||||
- name: Upload AAB
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: android-aab
|
||||
path: composeApp/build/outputs/bundle/release/*.aab
|
||||
path: Spotube-android-all-arch.apk
|
||||
|
||||
build-linux:
|
||||
name: Linux Desktop
|
||||
needs: prepare-deps
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GRADLE_FLAGS: ${{ inputs.release_type == 'nightly' && '-PversionName=nightly' || '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@ -182,38 +177,45 @@ jobs:
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Package DEB
|
||||
run: ./gradlew :composeApp:packageReleaseDeb ${{ env.GRADLE_FLAGS }}
|
||||
run: ./gradlew :composeApp:packageReleaseDeb
|
||||
|
||||
- name: Package RPM
|
||||
run: ./gradlew :composeApp:packageReleaseRpm ${{ env.GRADLE_FLAGS }}
|
||||
run: ./gradlew :composeApp:packageReleaseRpm
|
||||
|
||||
- name: Package AppImage
|
||||
run: ./gradlew :composeApp:packageReleaseAppImage ${{ env.GRADLE_FLAGS }}
|
||||
run: ./gradlew :composeApp:packageReleaseAppImage
|
||||
|
||||
- name: Rename Linux artifacts
|
||||
run: |
|
||||
DEB_FILE=$(find composeApp/build -name "*.deb" -type f | head -1)
|
||||
RPM_FILE=$(find composeApp/build -name "*.rpm" -type f | head -1)
|
||||
APPIMAGE_FILE=$(find composeApp/build -name "*.AppImage" -type f | head -1)
|
||||
cp "$DEB_FILE" Spotube-linux-x86_64.deb
|
||||
cp "$RPM_FILE" Spotube-linux-x86_64.rpm
|
||||
cp "$APPIMAGE_FILE" Spotube-linux-x86_64.AppImage
|
||||
|
||||
- name: Upload DEB
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux-deb
|
||||
path: composeApp/build/compose/binaries/main-release/**/*.deb
|
||||
path: Spotube-linux-x86_64.deb
|
||||
|
||||
- name: Upload RPM
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux-rpm
|
||||
path: composeApp/build/compose/binaries/main-release/**/*.rpm
|
||||
path: Spotube-linux-x86_64.rpm
|
||||
|
||||
- name: Upload AppImage
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux-appimage
|
||||
path: composeApp/build/compose/binaries/main-release/**/*.AppImage
|
||||
path: Spotube-linux-x86_64.AppImage
|
||||
|
||||
build-windows:
|
||||
name: Windows Desktop
|
||||
needs: prepare-deps
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
GRADLE_FLAGS: ${{ inputs.release_type == 'nightly' && '-PversionName=nightly' || '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@ -232,30 +234,25 @@ jobs:
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: Package MSI
|
||||
run: ./gradlew :composeApp:packageReleaseMsi ${{ env.GRADLE_FLAGS }}
|
||||
|
||||
- name: Package EXE
|
||||
run: ./gradlew :composeApp:packageReleaseExe ${{ env.GRADLE_FLAGS }}
|
||||
run: ./gradlew :composeApp:packageReleaseExe
|
||||
|
||||
- name: Upload MSI
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-msi
|
||||
path: composeApp/build/compose/binaries/main-release/**/*.msi
|
||||
- name: Rename Windows artifacts
|
||||
shell: pwsh
|
||||
run: |
|
||||
$EXE_FILE = Get-ChildItem -Path composeApp/build -Filter "*.exe" -Recurse | Select-Object -First 1
|
||||
Copy-Item $EXE_FILE.FullName Spotube-windows-x86_64-setup.exe
|
||||
|
||||
- name: Upload EXE
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-exe
|
||||
path: composeApp/build/compose/binaries/main-release/**/*.exe
|
||||
path: Spotube-windows-x86_64-setup.exe
|
||||
|
||||
build-macos:
|
||||
name: macOS Desktop
|
||||
needs: prepare-deps
|
||||
runs-on: macos-latest
|
||||
env:
|
||||
GRADLE_FLAGS: ${{ inputs.release_type == 'nightly' && '-PversionName=nightly' || '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@ -278,13 +275,18 @@ jobs:
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Package DMG
|
||||
run: ./gradlew :composeApp:packageReleaseDmg ${{ env.GRADLE_FLAGS }}
|
||||
run: ./gradlew :composeApp:packageReleaseDmg
|
||||
|
||||
- name: Rename macOS artifact
|
||||
run: |
|
||||
DMG_FILE=$(find composeApp/build -name "*.dmg" -type f | head -1)
|
||||
cp "$DMG_FILE" Spotube-macos-universal.dmg
|
||||
|
||||
- name: Upload DMG
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: macos-dmg
|
||||
path: composeApp/build/compose/binaries/main-release/**/*.dmg
|
||||
path: Spotube-macos-universal.dmg
|
||||
|
||||
create-release:
|
||||
name: Create GitHub Release
|
||||
|
||||
@ -1,96 +0,0 @@
|
||||
-------------------------------
|
||||
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.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -57,6 +57,8 @@ class MediaBrowseHelper(
|
||||
companion object {
|
||||
const val MEDIA_ID_ROOT = "root"
|
||||
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_SAVED_PLAYLISTS = "library:playlists"
|
||||
const val MEDIA_ID_SAVED_ALBUMS = "library:albums"
|
||||
@ -93,6 +95,11 @@ class MediaBrowseHelper(
|
||||
return when {
|
||||
parentId == MEDIA_ID_ROOT -> buildRootChildren()
|
||||
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_SAVED_PLAYLISTS -> buildSavedPlaylistsItems()
|
||||
parentId == MEDIA_ID_SAVED_ALBUMS -> buildSavedAlbumsItems()
|
||||
@ -214,7 +221,6 @@ class MediaBrowseHelper(
|
||||
}
|
||||
|
||||
private fun buildRootChildren(): List<MediaItem> {
|
||||
val gridExtras = Bundle().apply { putInt(CONTENT_STYLE_BROWSABLE, CONTENT_STYLE_GRID) }
|
||||
return listOf(
|
||||
MediaItem.Builder()
|
||||
.setMediaId(MEDIA_ID_BROWSE)
|
||||
@ -223,7 +229,6 @@ class MediaBrowseHelper(
|
||||
.setTitle("Browse")
|
||||
.setIsBrowsable(true)
|
||||
.setIsPlayable(false)
|
||||
.setExtras(gridExtras)
|
||||
.build()
|
||||
)
|
||||
.build(),
|
||||
@ -251,27 +256,67 @@ class MediaBrowseHelper(
|
||||
}
|
||||
|
||||
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 {
|
||||
homeScreenRepository.featuredItems() ?: emptyList()
|
||||
} catch (_: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
return featured.mapNotNull { item -> browseItemToMediaItem(item) }
|
||||
}
|
||||
|
||||
private suspend fun buildBrowseGenreItems(genreId: String): List<MediaItem> {
|
||||
val sections = try {
|
||||
homeScreenRepository.list()?.items ?: emptyList()
|
||||
homeScreenRepository.list(genreId)?.items ?: emptyList()
|
||||
} catch (_: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
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 (item in section.items) {
|
||||
val mediaItem = browseItemToMediaItem(
|
||||
|
||||
@ -47,6 +47,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
@ -372,7 +373,7 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildCurrentMediaItemList(): List<Media3MediaItem> {
|
||||
private suspend fun buildCurrentMediaItemList(): List<Media3MediaItem> = withContext(Dispatchers.Main) {
|
||||
val count = audioPlayer.player.mediaItemCount
|
||||
val items = mutableListOf<Media3MediaItem>()
|
||||
for (i in 0 until count) {
|
||||
@ -381,7 +382,7 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
|
||||
items.add(item)
|
||||
}
|
||||
}
|
||||
return items
|
||||
items
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@ -85,7 +85,7 @@ val navigationModule = module {
|
||||
LibraryScreen()
|
||||
}
|
||||
navigation<Routes.Settings> {
|
||||
SettingsScreen(pluginManager = get(), settingsViewModel = koinViewModel())
|
||||
SettingsScreen(settingsViewModel = koinViewModel())
|
||||
}
|
||||
navigation<Routes.Plugins> {
|
||||
PluginScreen(pluginManager = get())
|
||||
|
||||
@ -125,12 +125,12 @@ private fun resolveTextFieldState(
|
||||
)
|
||||
|
||||
else -> ResolvedAutoCompleteTextFieldState(
|
||||
theme.background.pressed,
|
||||
theme.highlight.pressed,
|
||||
theme.shape.pressed,
|
||||
theme.border.pressed,
|
||||
theme.shadow.pressed,
|
||||
theme.foreground.pressed
|
||||
theme.background.normal,
|
||||
theme.highlight.normal,
|
||||
theme.shape.normal,
|
||||
theme.border.normal,
|
||||
theme.shadow.normal,
|
||||
theme.foreground.normal
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,11 +106,11 @@ private fun resolveButtonState(
|
||||
)
|
||||
|
||||
else -> ResolvedButtonState(
|
||||
style.colors.focused,
|
||||
style.shape.focused,
|
||||
style.shadow.focused,
|
||||
style.border.focused,
|
||||
style.padding.focused
|
||||
style.colors.normal,
|
||||
style.shape.normal,
|
||||
style.shadow.normal,
|
||||
style.border.normal,
|
||||
style.padding.normal
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -299,6 +299,26 @@ fun IconButton(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GhostButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
theme: BaseUITheme.ButtonStyle? = null,
|
||||
content: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
val baseTheme = LocalBaseUITheme.current
|
||||
val style = theme ?: baseTheme.buttons.ghost
|
||||
OutlineButton(
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
hoverOnly = true,
|
||||
theme = style,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GhostIconButton(
|
||||
onClick: () -> Unit,
|
||||
@ -308,36 +328,13 @@ fun GhostIconButton(
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val baseTheme = LocalBaseUITheme.current
|
||||
val outlineStyle = baseTheme.iconButtons.outline
|
||||
val ghostStyle = theme ?: BaseUITheme.ButtonStyle(
|
||||
colors = BaseUITheme.InteractionState(
|
||||
hovered = outlineStyle.colors.hovered,
|
||||
pressed = outlineStyle.colors.pressed,
|
||||
focused = BaseUITheme.ButtonColors(
|
||||
background = Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent)),
|
||||
foreground = outlineStyle.colors.focused.foreground,
|
||||
highlight = outlineStyle.colors.focused.highlight,
|
||||
),
|
||||
),
|
||||
shape = outlineStyle.shape,
|
||||
shadow = BaseUITheme.InteractionState(
|
||||
hovered = outlineStyle.shadow.hovered,
|
||||
pressed = outlineStyle.shadow.pressed,
|
||||
focused = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent),
|
||||
),
|
||||
border = BaseUITheme.InteractionState(
|
||||
hovered = outlineStyle.border.hovered,
|
||||
pressed = outlineStyle.border.pressed,
|
||||
focused = BaseUITheme.Border(Color.Transparent, 0.dp),
|
||||
),
|
||||
padding = outlineStyle.padding,
|
||||
)
|
||||
val style = theme ?: baseTheme.iconButtons.ghost
|
||||
OutlineButton(
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
hoverOnly = true,
|
||||
theme = ghostStyle,
|
||||
theme = style,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
|
||||
@ -85,7 +85,7 @@ private fun resolveCheckBoxState(
|
||||
return when {
|
||||
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)
|
||||
else -> ResolvedCheckBoxState(style.colors.focused, style.shape.focused, style.shadow.focused, style.border.focused)
|
||||
else -> ResolvedCheckBoxState(style.colors.normal, style.shape.normal, style.shadow.normal, style.border.normal)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -70,7 +70,7 @@ private fun resolveChipState(
|
||||
return when {
|
||||
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)
|
||||
else -> ResolvedChipState(style.colors.focused, style.shape.focused, style.shadow.focused, style.border.focused, style.padding.focused)
|
||||
else -> ResolvedChipState(style.colors.normal, style.shape.normal, style.shadow.normal, style.border.normal, style.padding.normal)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,186 @@
|
||||
/*
|
||||
* 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.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
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.widthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
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.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
|
||||
private val DefaultDialogMaxWidth: Dp = 440.dp
|
||||
|
||||
@Composable
|
||||
fun ThemedDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
title: @Composable (() -> Unit)? = null,
|
||||
actions: @Composable (() -> Unit)? = null,
|
||||
theme: BaseUITheme.DialogTheme? = null,
|
||||
properties: DialogProperties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val dialogTheme = theme ?: LocalBaseUITheme.current.dialog
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
properties = properties,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = true,
|
||||
enter = scaleIn(
|
||||
initialScale = 0.92f,
|
||||
animationSpec = tween(280),
|
||||
) + fadeIn(animationSpec = tween(200)),
|
||||
exit = scaleOut(
|
||||
targetScale = 0.92f,
|
||||
animationSpec = tween(180),
|
||||
) + fadeOut(animationSpec = tween(150)),
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.widthIn(max = DefaultDialogMaxWidth)
|
||||
.shadow(
|
||||
elevation = dialogTheme.shadow.elevation,
|
||||
shape = dialogTheme.shadow.let { if (it.clip) dialogTheme.shape else RoundedCornerShape(0.dp) },
|
||||
ambientColor = dialogTheme.shadow.ambientColor,
|
||||
spotColor = dialogTheme.shadow.spotColor,
|
||||
)
|
||||
.clip(dialogTheme.shape)
|
||||
.background(dialogTheme.background, dialogTheme.shape)
|
||||
.border(
|
||||
BorderStroke(dialogTheme.border.width, dialogTheme.border.color),
|
||||
dialogTheme.shape
|
||||
)
|
||||
.highlight(Color.White.copy(alpha = 0.08f)),
|
||||
) {
|
||||
Column(modifier = Modifier.widthIn(max = DefaultDialogMaxWidth)) {
|
||||
if (title != null) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 24.dp, end = 24.dp, top = 24.dp),
|
||||
) {
|
||||
title()
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(
|
||||
start = 24.dp,
|
||||
end = 24.dp,
|
||||
top = if (title == null) 24.dp else 0.dp,
|
||||
),
|
||||
) {
|
||||
content()
|
||||
}
|
||||
|
||||
if (actions != null) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
|
||||
) {
|
||||
actions()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Spacer(Modifier.height(24.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ThemedDialogPreview() {
|
||||
MaterialTheme {
|
||||
val theme = rememberBaseUITheme()
|
||||
CompositionLocalProvider(LocalBaseUITheme provides theme) {
|
||||
var show by remember { mutableStateOf(true) }
|
||||
androidx.compose.material3.Surface(
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
) {
|
||||
PrimaryButton(onClick = { show = true }) {
|
||||
Text("Open Dialog")
|
||||
}
|
||||
if (show) {
|
||||
ThemedDialog(onDismissRequest = { show = false }) {
|
||||
Text(
|
||||
"This is a themed dialog with glass-like gradient, shadow, and scale animation.",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,172 @@
|
||||
/*
|
||||
* 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.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.hoverable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.layout.widthIn
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
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.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun DropdownMenu(
|
||||
expanded: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
offset: DpOffset = DpOffset(0.dp, 4.dp),
|
||||
theme: BaseUITheme.DropdownMenuTheme? = null,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val menuTheme = theme ?: LocalBaseUITheme.current.dropdownMenu
|
||||
|
||||
androidx.compose.material3.DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
offset = offset,
|
||||
modifier = modifier
|
||||
.widthIn(min = 180.dp)
|
||||
.shadow(
|
||||
elevation = menuTheme.shadow.elevation,
|
||||
shape = menuTheme.shape,
|
||||
ambientColor = menuTheme.shadow.ambientColor,
|
||||
spotColor = menuTheme.shadow.spotColor,
|
||||
)
|
||||
.clip(menuTheme.shape)
|
||||
.background(menuTheme.background, menuTheme.shape)
|
||||
.border(
|
||||
BorderStroke(menuTheme.border.width, menuTheme.border.color),
|
||||
menuTheme.shape,
|
||||
),
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DropdownMenuItem(
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
leadingIcon: ImageVector? = null,
|
||||
selected: Boolean = false,
|
||||
theme: BaseUITheme.DropdownMenuTheme? = null,
|
||||
) {
|
||||
val menuTheme = theme ?: LocalBaseUITheme.current.dropdownMenu
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(menuTheme.shape)
|
||||
.hoverable(interactionSource)
|
||||
.then(
|
||||
if (isHovered && enabled) {
|
||||
Modifier.background(menuTheme.itemHoverBackground)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.then(
|
||||
if (isHovered && enabled) {
|
||||
Modifier.highlight(menuTheme.itemHighlight)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.clickable(
|
||||
enabled = enabled,
|
||||
interactionSource = interactionSource,
|
||||
indication = null,
|
||||
onClick = onClick,
|
||||
)
|
||||
.padding(menuTheme.itemPadding),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
if (leadingIcon != null) {
|
||||
Icon(
|
||||
imageVector = leadingIcon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = if (enabled) {
|
||||
menuTheme.itemForeground
|
||||
} else {
|
||||
menuTheme.itemForeground.copy(alpha = 0.38f)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (enabled) {
|
||||
menuTheme.itemForeground
|
||||
} else {
|
||||
menuTheme.itemForeground.copy(alpha = 0.38f)
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DropdownMenuDivider(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(0.5.dp)
|
||||
.background(MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,177 @@
|
||||
/*
|
||||
* 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.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.hoverable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
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.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
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.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
private val RadioSize = 22.dp
|
||||
private val RadioDotSize = 10.dp
|
||||
|
||||
private data class ResolvedRadioState(
|
||||
val colors: BaseUITheme.ButtonColors,
|
||||
val shape: Shape,
|
||||
val shadow: BaseUITheme.Shadow,
|
||||
val border: BaseUITheme.Border,
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun resolveRadioState(
|
||||
style: BaseUITheme.ButtonStyle,
|
||||
isPressed: Boolean,
|
||||
isHovered: Boolean,
|
||||
): ResolvedRadioState {
|
||||
return when {
|
||||
isPressed -> ResolvedRadioState(style.colors.pressed, style.shape.pressed, style.shadow.pressed, style.border.pressed)
|
||||
isHovered -> ResolvedRadioState(style.colors.hovered, style.shape.hovered, style.shadow.hovered, style.border.hovered)
|
||||
else -> ResolvedRadioState(style.colors.normal, style.shape.normal, style.shadow.normal, style.border.normal)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Radio(
|
||||
selected: Boolean,
|
||||
onClick: (() -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
theme: BaseUITheme.CheckBoxTheme? = null,
|
||||
) {
|
||||
val baseTheme = LocalBaseUITheme.current.checkBox
|
||||
val radioTheme = theme ?: baseTheme
|
||||
val source = remember { MutableInteractionSource() }
|
||||
val isPressed by source.collectIsPressedAsState()
|
||||
val isHovered by source.collectIsHoveredAsState()
|
||||
|
||||
val style = if (selected) radioTheme.selected else radioTheme.unselected
|
||||
val resolved = resolveRadioState(style, isPressed, isHovered)
|
||||
val lift = if (isHovered && !isPressed && onClick != null) (-1).dp else 0.dp
|
||||
|
||||
val dotProgress by animateFloatAsState(
|
||||
targetValue = if (selected) 1f else 0f,
|
||||
animationSpec = tween(durationMillis = 180),
|
||||
label = "radioDot",
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(RadioSize)
|
||||
.graphicsLayer {
|
||||
translationY = lift.toPx()
|
||||
shape = CircleShape
|
||||
clip = true
|
||||
}
|
||||
.then(
|
||||
if (resolved.shadow.elevation > 0.dp) {
|
||||
Modifier.shadow(
|
||||
elevation = resolved.shadow.elevation,
|
||||
shape = CircleShape,
|
||||
ambientColor = resolved.shadow.ambientColor,
|
||||
spotColor = resolved.shadow.spotColor,
|
||||
)
|
||||
} else Modifier
|
||||
)
|
||||
.clip(CircleShape)
|
||||
.background(resolved.colors.background, CircleShape)
|
||||
.border(BorderStroke(0.5.dp, resolved.border.color), CircleShape)
|
||||
.highlight(resolved.colors.highlight)
|
||||
.then(
|
||||
if (onClick != null) {
|
||||
Modifier
|
||||
.hoverable(interactionSource = source, enabled = enabled)
|
||||
.clickable(
|
||||
interactionSource = source,
|
||||
indication = ripple(bounded = true, radius = RadioSize / 2),
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(RadioDotSize * dotProgress)
|
||||
.clip(CircleShape)
|
||||
.background(radioTheme.checkmarkColor, CircleShape),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun RadioPreview() {
|
||||
MaterialTheme {
|
||||
val theme = rememberBaseUITheme()
|
||||
CompositionLocalProvider(LocalBaseUITheme provides theme) {
|
||||
androidx.compose.material3.Surface(
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
modifier = Modifier.padding(24.dp),
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Radio(selected = true, onClick = {})
|
||||
Text("Selected", style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Radio(selected = false, onClick = {})
|
||||
Text("Unselected", style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -95,22 +95,22 @@ fun Slider(
|
||||
val stateColor = when {
|
||||
isPressed -> sliderTheme.trackActiveColor.pressed
|
||||
isHovered -> sliderTheme.trackActiveColor.hovered
|
||||
else -> sliderTheme.trackActiveColor.focused
|
||||
else -> sliderTheme.trackActiveColor.normal
|
||||
}
|
||||
val inactiveColor = when {
|
||||
isPressed -> sliderTheme.trackInactiveColor.pressed
|
||||
isHovered -> sliderTheme.trackInactiveColor.hovered
|
||||
else -> sliderTheme.trackInactiveColor.focused
|
||||
else -> sliderTheme.trackInactiveColor.normal
|
||||
}
|
||||
val thumbColor = when {
|
||||
isPressed -> sliderTheme.thumbColor.pressed
|
||||
isHovered -> sliderTheme.thumbColor.hovered
|
||||
else -> sliderTheme.thumbColor.focused
|
||||
else -> sliderTheme.thumbColor.normal
|
||||
}
|
||||
val thumbShadowState = when {
|
||||
isPressed -> sliderTheme.thumbShadow.pressed
|
||||
isHovered -> sliderTheme.thumbShadow.hovered
|
||||
else -> sliderTheme.thumbShadow.focused
|
||||
else -> sliderTheme.thumbShadow.normal
|
||||
}
|
||||
|
||||
val trackActiveColor = if (enabled) stateColor else stateColor.copy(alpha = 0.38f)
|
||||
|
||||
@ -81,7 +81,7 @@ private fun resolveTextFieldState(
|
||||
return when {
|
||||
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)
|
||||
else -> ResolvedTextFieldState(theme.background.pressed, theme.highlight.pressed, theme.shape.pressed, theme.border.pressed, theme.shadow.pressed, theme.foreground.pressed)
|
||||
else -> ResolvedTextFieldState(theme.background.normal, theme.highlight.normal, theme.shape.normal, theme.border.normal, theme.shadow.normal, theme.foreground.normal)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -48,25 +48,30 @@ data class BaseUITheme(
|
||||
val slider: SliderTheme,
|
||||
val checkBox: CheckBoxTheme,
|
||||
val chipTab: ChipTabTheme,
|
||||
val toggle: ToggleTheme,
|
||||
val card: CardTheme,
|
||||
val listRowTile: ListRowTheme,
|
||||
val dialog: DialogTheme,
|
||||
val dropdownMenu: DropdownMenuTheme,
|
||||
) {
|
||||
|
||||
@Stable
|
||||
data class InteractionState<T>(
|
||||
val normal: T,
|
||||
val hovered: T,
|
||||
val pressed: T,
|
||||
val focused: T,
|
||||
) {
|
||||
companion object {
|
||||
fun <T> fromSingleValue(value: T): InteractionState<T> {
|
||||
return InteractionState(value, value, value)
|
||||
return InteractionState(value, value, value, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
data class AdvancedInteractionState<T>(
|
||||
val normal: T,
|
||||
val hovered: T,
|
||||
val pressed: T,
|
||||
val focused: T,
|
||||
@ -75,7 +80,7 @@ data class BaseUITheme(
|
||||
) {
|
||||
companion object {
|
||||
fun <T> fromSingleValue(value: T): AdvancedInteractionState<T> {
|
||||
return AdvancedInteractionState(value, value, value, value, value)
|
||||
return AdvancedInteractionState(value, value, value, value, value, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -156,6 +161,27 @@ data class BaseUITheme(
|
||||
val shadowElevation: Dp,
|
||||
)
|
||||
|
||||
data class DialogTheme(
|
||||
val background: Brush,
|
||||
val scrim: Color,
|
||||
val shape: Shape,
|
||||
val border: Border,
|
||||
val shadow: Shadow,
|
||||
val padding: PaddingValues,
|
||||
)
|
||||
|
||||
data class DropdownMenuTheme(
|
||||
val background: Brush,
|
||||
val shape: Shape,
|
||||
val border: Border,
|
||||
val shadow: Shadow,
|
||||
val itemHoverBackground: Brush,
|
||||
val itemForeground: Color,
|
||||
val itemHighlight: Color,
|
||||
val padding: PaddingValues,
|
||||
val itemPadding: PaddingValues,
|
||||
)
|
||||
|
||||
data class SliderTheme(
|
||||
val trackActiveColor: InteractionState<Color>,
|
||||
val trackInactiveColor: InteractionState<Color>,
|
||||
@ -175,6 +201,13 @@ data class BaseUITheme(
|
||||
val selected: ButtonStyle,
|
||||
val unselected: ButtonStyle,
|
||||
)
|
||||
|
||||
data class ToggleTheme(
|
||||
val checked: ButtonStyle,
|
||||
val unchecked: ButtonStyle,
|
||||
val thumb: InteractionState<Color>,
|
||||
val thumbShadow: InteractionState<Shadow>,
|
||||
)
|
||||
}
|
||||
|
||||
val LocalBaseUITheme = staticCompositionLocalOf<BaseUITheme> {
|
||||
@ -200,23 +233,30 @@ fun rememberBaseUITheme(): BaseUITheme {
|
||||
|
||||
val defaultShape = RoundedCornerShape(14.dp)
|
||||
val defaultShapeState =
|
||||
BaseUITheme.InteractionState<Shape>(defaultShape, defaultShape, defaultShape)
|
||||
BaseUITheme.InteractionState<Shape>(defaultShape, defaultShape, defaultShape, defaultShape)
|
||||
val noBorder = BaseUITheme.Border(Color.Transparent, 0.dp)
|
||||
val noBorderState = BaseUITheme.InteractionState(noBorder, noBorder, noBorder)
|
||||
val noBorderState = BaseUITheme.InteractionState(noBorder, noBorder, noBorder, noBorder)
|
||||
val noShadow = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent)
|
||||
val noShadowState = BaseUITheme.InteractionState(noShadow, noShadow, noShadow)
|
||||
val noShadowState = BaseUITheme.InteractionState(noShadow, noShadow, noShadow, noShadow)
|
||||
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),
|
||||
)
|
||||
val iconButtonPadding = BaseUITheme.InteractionState(
|
||||
PaddingValues(8.dp),
|
||||
PaddingValues(8.dp),
|
||||
PaddingValues(8.dp),
|
||||
PaddingValues(8.dp),
|
||||
)
|
||||
|
||||
val outlineColors = BaseUITheme.InteractionState(
|
||||
normal = BaseUITheme.ButtonColors(
|
||||
background = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||
foreground = scheme.onSurface,
|
||||
highlight = highlight,
|
||||
),
|
||||
hovered = BaseUITheme.ButtonColors(
|
||||
background = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||
foreground = scheme.onSurface,
|
||||
@ -234,6 +274,12 @@ fun rememberBaseUITheme(): BaseUITheme {
|
||||
),
|
||||
)
|
||||
val outlineShadow = BaseUITheme.InteractionState(
|
||||
normal = BaseUITheme.Shadow(
|
||||
6.dp,
|
||||
true,
|
||||
shadowColor.copy(alpha = 0.15f),
|
||||
shadowColor.copy(alpha = 0.18f)
|
||||
),
|
||||
hovered = BaseUITheme.Shadow(
|
||||
9.dp,
|
||||
true,
|
||||
@ -254,12 +300,18 @@ fun rememberBaseUITheme(): BaseUITheme {
|
||||
),
|
||||
)
|
||||
val outlineBorder = BaseUITheme.InteractionState(
|
||||
normal = BaseUITheme.Border(border, 0.5.dp),
|
||||
hovered = BaseUITheme.Border(border, 0.5.dp),
|
||||
pressed = BaseUITheme.Border(border.copy(alpha = 0.7f), 0.5.dp),
|
||||
focused = BaseUITheme.Border(border, 0.5.dp),
|
||||
)
|
||||
|
||||
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(
|
||||
background = Brush.verticalGradient(listOf(scheme.primary, scheme.primary)),
|
||||
foreground = scheme.onPrimary,
|
||||
@ -282,6 +334,12 @@ fun rememberBaseUITheme(): BaseUITheme {
|
||||
),
|
||||
)
|
||||
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(
|
||||
9.dp,
|
||||
true,
|
||||
@ -302,12 +360,23 @@ fun rememberBaseUITheme(): BaseUITheme {
|
||||
),
|
||||
)
|
||||
val primaryBorder = BaseUITheme.InteractionState(
|
||||
normal = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
||||
hovered = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
||||
pressed = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
||||
focused = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
||||
)
|
||||
|
||||
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(
|
||||
background = Brush.verticalGradient(
|
||||
listOf(
|
||||
@ -340,6 +409,12 @@ fun rememberBaseUITheme(): BaseUITheme {
|
||||
),
|
||||
)
|
||||
val secondaryShadow = BaseUITheme.InteractionState(
|
||||
normal = BaseUITheme.Shadow(
|
||||
6.dp,
|
||||
true,
|
||||
shadowColor.copy(alpha = 0.15f),
|
||||
shadowColor.copy(alpha = 0.18f)
|
||||
),
|
||||
hovered = BaseUITheme.Shadow(
|
||||
9.dp,
|
||||
true,
|
||||
@ -360,12 +435,18 @@ fun rememberBaseUITheme(): BaseUITheme {
|
||||
),
|
||||
)
|
||||
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),
|
||||
pressed = 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(
|
||||
normal = BaseUITheme.ButtonColors(
|
||||
background = Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent)),
|
||||
foreground = scheme.onSurface,
|
||||
highlight = Color.Transparent,
|
||||
),
|
||||
hovered = BaseUITheme.ButtonColors(
|
||||
background = Brush.verticalGradient(
|
||||
listOf(
|
||||
@ -447,6 +528,7 @@ fun rememberBaseUITheme(): BaseUITheme {
|
||||
)
|
||||
|
||||
val textFieldForeground = BaseUITheme.InteractionState(
|
||||
normal = scheme.onSurface,
|
||||
hovered = scheme.onSurface,
|
||||
pressed = scheme.onSurface,
|
||||
focused = scheme.onSurface,
|
||||
@ -454,17 +536,28 @@ fun rememberBaseUITheme(): BaseUITheme {
|
||||
|
||||
val checkBoxShape = RoundedCornerShape(6.dp)
|
||||
val checkBoxShapeState =
|
||||
BaseUITheme.InteractionState<Shape>(checkBoxShape, checkBoxShape, checkBoxShape)
|
||||
BaseUITheme.InteractionState<Shape>(checkBoxShape, checkBoxShape, checkBoxShape, checkBoxShape)
|
||||
val noPadding =
|
||||
BaseUITheme.InteractionState(PaddingValues(0.dp), PaddingValues(0.dp), PaddingValues(0.dp))
|
||||
BaseUITheme.InteractionState(PaddingValues(0.dp), PaddingValues(0.dp), PaddingValues(0.dp), PaddingValues(0.dp))
|
||||
|
||||
val chipTabShape = RoundedCornerShape(10.dp)
|
||||
val chipTabShapeState =
|
||||
BaseUITheme.InteractionState<Shape>(chipTabShape, chipTabShape, chipTabShape)
|
||||
BaseUITheme.InteractionState<Shape>(chipTabShape, chipTabShape, chipTabShape, chipTabShape)
|
||||
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),
|
||||
)
|
||||
|
||||
val toggleShape = RoundedCornerShape(50)
|
||||
val toggleShapeState =
|
||||
BaseUITheme.InteractionState<Shape>(toggleShape, toggleShape, toggleShape, toggleShape)
|
||||
val togglePadding = BaseUITheme.InteractionState(
|
||||
PaddingValues(2.dp),
|
||||
PaddingValues(2.dp),
|
||||
PaddingValues(2.dp),
|
||||
PaddingValues(2.dp),
|
||||
)
|
||||
|
||||
return BaseUITheme(
|
||||
@ -482,26 +575,36 @@ fun rememberBaseUITheme(): BaseUITheme {
|
||||
),
|
||||
textField = TextFieldTheme(
|
||||
background = BaseUITheme.InteractionState(
|
||||
normal = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||
hovered = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||
pressed = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||
focused = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||
),
|
||||
highlight = BaseUITheme.InteractionState(
|
||||
normal = highlight,
|
||||
hovered = highlight,
|
||||
pressed = highlight,
|
||||
focused = highlight,
|
||||
),
|
||||
shape = BaseUITheme.InteractionState(
|
||||
normal = defaultShape,
|
||||
hovered = defaultShape,
|
||||
pressed = defaultShape,
|
||||
focused = defaultShape,
|
||||
),
|
||||
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),
|
||||
pressed = BaseUITheme.Border(border, 0.5.dp),
|
||||
focused = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
||||
),
|
||||
shadow = BaseUITheme.InteractionState(
|
||||
normal = BaseUITheme.Shadow(
|
||||
9.dp,
|
||||
true,
|
||||
shadowColor.copy(alpha = 0.22f),
|
||||
shadowColor.copy(alpha = 0.26f)
|
||||
),
|
||||
hovered = BaseUITheme.Shadow(
|
||||
9.dp,
|
||||
true,
|
||||
@ -540,21 +643,30 @@ fun rememberBaseUITheme(): BaseUITheme {
|
||||
),
|
||||
slider = SliderTheme(
|
||||
trackActiveColor = BaseUITheme.InteractionState(
|
||||
normal = scheme.primary,
|
||||
hovered = scheme.primary,
|
||||
pressed = scheme.primary,
|
||||
focused = scheme.primary,
|
||||
),
|
||||
trackInactiveColor = BaseUITheme.InteractionState(
|
||||
normal = border.copy(alpha = 0.6f),
|
||||
hovered = border.copy(alpha = 0.6f),
|
||||
pressed = border.copy(alpha = 0.6f),
|
||||
focused = border.copy(alpha = 0.6f),
|
||||
),
|
||||
thumbColor = BaseUITheme.InteractionState(
|
||||
normal = scheme.primary,
|
||||
hovered = scheme.primary,
|
||||
pressed = scheme.primary,
|
||||
focused = scheme.primary,
|
||||
),
|
||||
thumbShadow = BaseUITheme.InteractionState(
|
||||
normal = BaseUITheme.Shadow(
|
||||
5.dp,
|
||||
true,
|
||||
scheme.primary.copy(alpha = 0.35f),
|
||||
scheme.primary.copy(alpha = 0.4f)
|
||||
),
|
||||
hovered = BaseUITheme.Shadow(
|
||||
8.dp,
|
||||
true,
|
||||
@ -582,6 +694,12 @@ fun rememberBaseUITheme(): BaseUITheme {
|
||||
colors = primaryColors,
|
||||
shape = checkBoxShapeState,
|
||||
shadow = BaseUITheme.InteractionState(
|
||||
normal = BaseUITheme.Shadow(
|
||||
6.dp,
|
||||
true,
|
||||
scheme.primary.copy(alpha = 0.3f),
|
||||
scheme.primary.copy(alpha = 0.35f)
|
||||
),
|
||||
hovered = BaseUITheme.Shadow(
|
||||
8.dp,
|
||||
true,
|
||||
@ -608,6 +726,12 @@ fun rememberBaseUITheme(): BaseUITheme {
|
||||
colors = outlineColors,
|
||||
shape = checkBoxShapeState,
|
||||
shadow = BaseUITheme.InteractionState(
|
||||
normal = BaseUITheme.Shadow(
|
||||
3.dp,
|
||||
true,
|
||||
shadowColor.copy(alpha = 0.15f),
|
||||
shadowColor.copy(alpha = 0.18f)
|
||||
),
|
||||
hovered = BaseUITheme.Shadow(
|
||||
5.dp,
|
||||
true,
|
||||
@ -648,6 +772,161 @@ fun rememberBaseUITheme(): BaseUITheme {
|
||||
padding = chipTabPadding,
|
||||
),
|
||||
),
|
||||
toggle = BaseUITheme.ToggleTheme(
|
||||
checked = BaseUITheme.ButtonStyle(
|
||||
colors = 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(
|
||||
background = Brush.verticalGradient(listOf(scheme.primary, scheme.primary)),
|
||||
foreground = scheme.onPrimary,
|
||||
highlight = Color.White.copy(alpha = 0.25f),
|
||||
),
|
||||
pressed = BaseUITheme.ButtonColors(
|
||||
background = Brush.verticalGradient(
|
||||
listOf(
|
||||
scheme.primary.copy(alpha = 0.85f),
|
||||
scheme.primary
|
||||
)
|
||||
),
|
||||
foreground = scheme.onPrimary,
|
||||
highlight = Color.White.copy(alpha = 0.25f),
|
||||
),
|
||||
focused = BaseUITheme.ButtonColors(
|
||||
background = Brush.verticalGradient(listOf(scheme.primary, scheme.primary)),
|
||||
foreground = scheme.onPrimary,
|
||||
highlight = Color.White.copy(alpha = 0.25f),
|
||||
),
|
||||
),
|
||||
shape = toggleShapeState,
|
||||
shadow = BaseUITheme.InteractionState(
|
||||
normal = BaseUITheme.Shadow(
|
||||
4.dp,
|
||||
true,
|
||||
scheme.primary.copy(alpha = 0.3f),
|
||||
scheme.primary.copy(alpha = 0.35f)
|
||||
),
|
||||
hovered = BaseUITheme.Shadow(
|
||||
6.dp,
|
||||
true,
|
||||
scheme.primary.copy(alpha = 0.4f),
|
||||
scheme.primary.copy(alpha = 0.45f)
|
||||
),
|
||||
pressed = BaseUITheme.Shadow(
|
||||
1.dp,
|
||||
true,
|
||||
scheme.primary.copy(alpha = 0.2f),
|
||||
scheme.primary.copy(alpha = 0.25f)
|
||||
),
|
||||
focused = BaseUITheme.Shadow(
|
||||
4.dp,
|
||||
true,
|
||||
scheme.primary.copy(alpha = 0.3f),
|
||||
scheme.primary.copy(alpha = 0.35f)
|
||||
),
|
||||
),
|
||||
border = BaseUITheme.InteractionState(
|
||||
normal = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
||||
hovered = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
||||
pressed = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
||||
focused = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
||||
),
|
||||
padding = togglePadding,
|
||||
),
|
||||
unchecked = BaseUITheme.ButtonStyle(
|
||||
colors = BaseUITheme.InteractionState(
|
||||
normal = BaseUITheme.ButtonColors(
|
||||
background = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||
foreground = scheme.onSurface,
|
||||
highlight = highlight,
|
||||
),
|
||||
hovered = BaseUITheme.ButtonColors(
|
||||
background = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||
foreground = scheme.onSurface,
|
||||
highlight = highlight,
|
||||
),
|
||||
pressed = BaseUITheme.ButtonColors(
|
||||
background = Brush.verticalGradient(listOf(containerPressed, containerPressed)),
|
||||
foreground = scheme.onSurface,
|
||||
highlight = highlight,
|
||||
),
|
||||
focused = BaseUITheme.ButtonColors(
|
||||
background = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||
foreground = scheme.onSurface,
|
||||
highlight = highlight,
|
||||
),
|
||||
),
|
||||
shape = toggleShapeState,
|
||||
shadow = BaseUITheme.InteractionState(
|
||||
normal = BaseUITheme.Shadow(
|
||||
4.dp,
|
||||
true,
|
||||
shadowColor.copy(alpha = 0.15f),
|
||||
shadowColor.copy(alpha = 0.18f)
|
||||
),
|
||||
hovered = BaseUITheme.Shadow(
|
||||
6.dp,
|
||||
true,
|
||||
shadowColor.copy(alpha = 0.22f),
|
||||
shadowColor.copy(alpha = 0.26f)
|
||||
),
|
||||
pressed = BaseUITheme.Shadow(
|
||||
1.dp,
|
||||
true,
|
||||
shadowColor.copy(alpha = 0.08f),
|
||||
shadowColor.copy(alpha = 0.1f)
|
||||
),
|
||||
focused = BaseUITheme.Shadow(
|
||||
4.dp,
|
||||
true,
|
||||
shadowColor.copy(alpha = 0.15f),
|
||||
shadowColor.copy(alpha = 0.18f)
|
||||
),
|
||||
),
|
||||
border = BaseUITheme.InteractionState(
|
||||
normal = BaseUITheme.Border(border, 0.5.dp),
|
||||
hovered = BaseUITheme.Border(border, 0.5.dp),
|
||||
pressed = BaseUITheme.Border(border.copy(alpha = 0.7f), 0.5.dp),
|
||||
focused = BaseUITheme.Border(border, 0.5.dp),
|
||||
),
|
||||
padding = togglePadding,
|
||||
),
|
||||
thumb = BaseUITheme.InteractionState(
|
||||
normal = Color.White,
|
||||
hovered = Color.White,
|
||||
pressed = Color.White,
|
||||
focused = Color.White,
|
||||
),
|
||||
thumbShadow = BaseUITheme.InteractionState(
|
||||
normal = BaseUITheme.Shadow(
|
||||
3.dp,
|
||||
true,
|
||||
shadowColor.copy(alpha = 0.2f),
|
||||
shadowColor.copy(alpha = 0.25f)
|
||||
),
|
||||
hovered = BaseUITheme.Shadow(
|
||||
4.dp,
|
||||
true,
|
||||
shadowColor.copy(alpha = 0.28f),
|
||||
shadowColor.copy(alpha = 0.32f)
|
||||
),
|
||||
pressed = BaseUITheme.Shadow(
|
||||
1.dp,
|
||||
true,
|
||||
shadowColor.copy(alpha = 0.12f),
|
||||
shadowColor.copy(alpha = 0.15f)
|
||||
),
|
||||
focused = BaseUITheme.Shadow(
|
||||
3.dp,
|
||||
true,
|
||||
shadowColor.copy(alpha = 0.2f),
|
||||
shadowColor.copy(alpha = 0.25f)
|
||||
),
|
||||
),
|
||||
),
|
||||
card = BaseUITheme.CardTheme(
|
||||
background = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
@ -662,6 +941,7 @@ fun rememberBaseUITheme(): BaseUITheme {
|
||||
),
|
||||
listRowTile = BaseUITheme.ListRowTheme(
|
||||
background = BaseUITheme.AdvancedInteractionState(
|
||||
normal = Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent)),
|
||||
hovered = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||
pressed = Brush.verticalGradient(listOf(containerPressed, containerPressed)),
|
||||
focused = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||
@ -679,6 +959,7 @@ fun rememberBaseUITheme(): BaseUITheme {
|
||||
),
|
||||
),
|
||||
shape = BaseUITheme.AdvancedInteractionState(
|
||||
normal = RoundedCornerShape(8.dp),
|
||||
hovered = RoundedCornerShape(8.dp),
|
||||
pressed = RoundedCornerShape(8.dp),
|
||||
focused = RoundedCornerShape(8.dp),
|
||||
@ -686,6 +967,7 @@ fun rememberBaseUITheme(): BaseUITheme {
|
||||
disabled = RoundedCornerShape(8.dp),
|
||||
),
|
||||
border = BaseUITheme.AdvancedInteractionState(
|
||||
normal = BaseUITheme.Border(Color.Transparent, 0.dp),
|
||||
hovered = BaseUITheme.Border(Color.Transparent, 0.dp),
|
||||
pressed = BaseUITheme.Border(Color.Transparent, 0.dp),
|
||||
focused = BaseUITheme.Border(Color.Transparent, 0.dp),
|
||||
@ -693,6 +975,7 @@ fun rememberBaseUITheme(): BaseUITheme {
|
||||
disabled = BaseUITheme.Border(Color.Transparent, 0.dp),
|
||||
),
|
||||
shadow = BaseUITheme.AdvancedInteractionState(
|
||||
normal = 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),
|
||||
focused = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent),
|
||||
@ -701,6 +984,7 @@ fun rememberBaseUITheme(): BaseUITheme {
|
||||
),
|
||||
padding = PaddingValues(horizontal = 8.dp, vertical = 6.dp),
|
||||
textStyle = BaseUITheme.AdvancedInteractionState(
|
||||
normal = TextStyle.Default,
|
||||
hovered = TextStyle.Default,
|
||||
pressed = TextStyle.Default,
|
||||
focused = TextStyle.Default,
|
||||
@ -708,6 +992,7 @@ fun rememberBaseUITheme(): BaseUITheme {
|
||||
disabled = TextStyle.Default,
|
||||
),
|
||||
foreground = BaseUITheme.AdvancedInteractionState(
|
||||
normal = scheme.onSurface,
|
||||
hovered = scheme.onSurface,
|
||||
pressed = scheme.onSurface,
|
||||
focused = scheme.onSurface,
|
||||
@ -715,6 +1000,47 @@ fun rememberBaseUITheme(): BaseUITheme {
|
||||
disabled = scheme.onSurface.copy(alpha = 0.38f),
|
||||
),
|
||||
),
|
||||
dialog = BaseUITheme.DialogTheme(
|
||||
background = Brush.verticalGradient(
|
||||
listOf(
|
||||
scheme.surface,
|
||||
scheme.surfaceContainerHigh,
|
||||
)
|
||||
),
|
||||
scrim = Color.Black.copy(alpha = 0.42f),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
border = BaseUITheme.Border(border, 0.5.dp),
|
||||
shadow = BaseUITheme.Shadow(
|
||||
24.dp,
|
||||
true,
|
||||
shadowColor.copy(alpha = 0.3f),
|
||||
shadowColor.copy(alpha = 0.4f)
|
||||
),
|
||||
padding = PaddingValues(24.dp),
|
||||
),
|
||||
dropdownMenu = BaseUITheme.DropdownMenuTheme(
|
||||
background = Brush.verticalGradient(
|
||||
listOf(
|
||||
scheme.surfaceContainerHigh,
|
||||
scheme.surfaceContainer,
|
||||
)
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
border = BaseUITheme.Border(border, 0.5.dp),
|
||||
shadow = BaseUITheme.Shadow(
|
||||
12.dp,
|
||||
true,
|
||||
shadowColor.copy(alpha = 0.2f),
|
||||
shadowColor.copy(alpha = 0.25f)
|
||||
),
|
||||
itemHoverBackground = Brush.verticalGradient(
|
||||
listOf(containerLighter, containerDarker)
|
||||
),
|
||||
itemForeground = scheme.onSurface,
|
||||
itemHighlight = highlight,
|
||||
padding = PaddingValues(4.dp),
|
||||
itemPadding = PaddingValues(horizontal = 14.dp, vertical = 10.dp),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@ -766,6 +1092,7 @@ fun invertedButtonStyle(): BaseUITheme.ButtonStyle {
|
||||
|
||||
return BaseUITheme.ButtonStyle(
|
||||
colors = BaseUITheme.InteractionState(
|
||||
normal = invertedColors,
|
||||
hovered = invertedColors,
|
||||
pressed = pressedColors,
|
||||
focused = invertedColors,
|
||||
|
||||
@ -0,0 +1,264 @@
|
||||
/*
|
||||
* 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.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.hoverable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
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.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private val ToggleWidth = 44.dp
|
||||
private val ToggleHeight = 24.dp
|
||||
private val ThumbSize = 20.dp
|
||||
|
||||
private data class ResolvedToggleState(
|
||||
val colors: BaseUITheme.ButtonColors,
|
||||
val shape: Shape,
|
||||
val shadow: BaseUITheme.Shadow,
|
||||
val border: BaseUITheme.Border,
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun resolveToggleState(
|
||||
style: BaseUITheme.ButtonStyle,
|
||||
isPressed: Boolean,
|
||||
isHovered: Boolean,
|
||||
): ResolvedToggleState {
|
||||
return when {
|
||||
isPressed -> ResolvedToggleState(
|
||||
style.colors.pressed,
|
||||
style.shape.pressed,
|
||||
style.shadow.pressed,
|
||||
style.border.pressed,
|
||||
)
|
||||
|
||||
isHovered -> ResolvedToggleState(
|
||||
style.colors.hovered,
|
||||
style.shape.hovered,
|
||||
style.shadow.hovered,
|
||||
style.border.hovered,
|
||||
)
|
||||
|
||||
else -> ResolvedToggleState(
|
||||
style.colors.normal,
|
||||
style.shape.normal,
|
||||
style.shadow.normal,
|
||||
style.border.normal,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Toggle(
|
||||
checked: Boolean,
|
||||
onCheckedChange: ((Boolean) -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
theme: BaseUITheme.ToggleTheme? = null,
|
||||
) {
|
||||
val baseTheme = LocalBaseUITheme.current.toggle
|
||||
val toggleTheme = theme ?: baseTheme
|
||||
val source = interactionSource ?: remember { MutableInteractionSource() }
|
||||
val isPressed by source.collectIsPressedAsState()
|
||||
val isHovered by source.collectIsHoveredAsState()
|
||||
|
||||
val style = if (checked) toggleTheme.checked else toggleTheme.unchecked
|
||||
val resolved = resolveToggleState(style, isPressed, isHovered)
|
||||
|
||||
val thumbColor = when {
|
||||
isPressed -> toggleTheme.thumb.pressed
|
||||
isHovered -> toggleTheme.thumb.hovered
|
||||
else -> toggleTheme.thumb.normal
|
||||
}
|
||||
|
||||
val thumbShadowState = when {
|
||||
isPressed -> toggleTheme.thumbShadow.pressed
|
||||
isHovered -> toggleTheme.thumbShadow.hovered
|
||||
else -> toggleTheme.thumbShadow.normal
|
||||
}
|
||||
|
||||
val borderColor = when {
|
||||
!enabled -> resolved.colors.foreground.copy(alpha = 0.38f)
|
||||
else -> resolved.border.color
|
||||
}
|
||||
|
||||
val thumbProgress by animateFloatAsState(
|
||||
targetValue = if (checked) 1f else 0f,
|
||||
animationSpec = tween(durationMillis = 200),
|
||||
label = "thumbProgress",
|
||||
)
|
||||
|
||||
val thumbScale by animateFloatAsState(
|
||||
targetValue = when {
|
||||
isPressed -> 0.92f
|
||||
isHovered -> 1.05f
|
||||
else -> 1f
|
||||
},
|
||||
animationSpec = tween(durationMillis = 150),
|
||||
label = "thumbScale",
|
||||
)
|
||||
|
||||
val lift = if (isHovered && !isPressed && onCheckedChange != null) (-1).dp else 0.dp
|
||||
val density = LocalDensity.current
|
||||
val thumbOffsetXPx by animateDpAsState(
|
||||
targetValue = with(density) {
|
||||
val trackWidth = ToggleWidth - ThumbSize
|
||||
max((thumbProgress * trackWidth.toPx()).toDp() - 4.dp, 2.dp)
|
||||
},
|
||||
animationSpec = tween(durationMillis = 200),
|
||||
label = "thumbOffsetX",
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.width(ToggleWidth)
|
||||
.height(ToggleHeight)
|
||||
.graphicsLayer {
|
||||
translationY = lift.toPx()
|
||||
}
|
||||
.then(
|
||||
if (resolved.shadow.elevation > 0.dp) {
|
||||
Modifier.shadow(
|
||||
elevation = resolved.shadow.elevation,
|
||||
shape = resolved.shape,
|
||||
ambientColor = resolved.shadow.ambientColor,
|
||||
spotColor = resolved.shadow.spotColor,
|
||||
)
|
||||
} else Modifier
|
||||
)
|
||||
.clip(resolved.shape)
|
||||
.background(resolved.colors.background, resolved.shape)
|
||||
.border(BorderStroke(resolved.border.width, borderColor), resolved.shape)
|
||||
.highlight(resolved.colors.highlight)
|
||||
.then(
|
||||
if (onCheckedChange != null) {
|
||||
Modifier
|
||||
.hoverable(interactionSource = source, enabled = enabled)
|
||||
.clickable(
|
||||
enabled = enabled,
|
||||
interactionSource = source,
|
||||
indication = null,
|
||||
onClick = { onCheckedChange(!checked) },
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.offset(x = thumbOffsetXPx)
|
||||
.size(ThumbSize)
|
||||
.graphicsLayer {
|
||||
scaleX = thumbScale
|
||||
scaleY = thumbScale
|
||||
}
|
||||
.shadow(
|
||||
elevation = thumbShadowState.elevation,
|
||||
shape = CircleShape,
|
||||
ambientColor = thumbShadowState.ambientColor,
|
||||
spotColor = thumbShadowState.spotColor,
|
||||
)
|
||||
.background(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color.White.copy(alpha = 0.6f),
|
||||
thumbColor.copy(alpha = 0.95f),
|
||||
thumbColor,
|
||||
),
|
||||
center = Offset(0f, -0.55f),
|
||||
radius = 1.1f,
|
||||
),
|
||||
shape = CircleShape,
|
||||
)
|
||||
.border(
|
||||
width = 0.8.dp,
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.White.copy(alpha = 0.55f),
|
||||
Color.White.copy(alpha = 0.08f),
|
||||
),
|
||||
),
|
||||
shape = CircleShape,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun TogglePreview() {
|
||||
MaterialTheme {
|
||||
val theme = rememberBaseUITheme()
|
||||
CompositionLocalProvider(LocalBaseUITheme provides theme) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
modifier = Modifier.padding(24.dp),
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Toggle(checked = true, onCheckedChange = {})
|
||||
Toggle(checked = false, onCheckedChange = {})
|
||||
Toggle(checked = true, onCheckedChange = null)
|
||||
Toggle(checked = false, onCheckedChange = null, enabled = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -17,6 +17,7 @@
|
||||
|
||||
package dev.krtirtho.spotube.core.ui.component
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
@ -33,10 +34,10 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
@ -64,6 +65,9 @@ import androidx.compose.ui.unit.dp
|
||||
import compose.icons.FeatherIcons
|
||||
import compose.icons.feathericons.Check
|
||||
import dev.krtirtho.spotube.core.ui.base.TextField
|
||||
import dev.krtirtho.spotube.core.ui.base.DropdownMenu
|
||||
import dev.krtirtho.spotube.core.ui.base.DropdownMenuItem
|
||||
import dev.krtirtho.spotube.core.ui.base.DropdownMenuDivider
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax3DotsMore
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxFilterSearch
|
||||
@ -105,10 +109,9 @@ fun AdaptiveDropdownBottomSheet(
|
||||
trigger { expanded = true }
|
||||
|
||||
if (isLargeScreen) {
|
||||
ShadcnDropdownMenu(
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
minWidth = menuMinWidth,
|
||||
) {
|
||||
if (header != null && (headerDisplayMode == HeaderDisplayMode.Always || headerDisplayMode == HeaderDisplayMode.OnlyInDropdown)) {
|
||||
header()
|
||||
@ -137,26 +140,22 @@ fun AdaptiveDropdownBottomSheet(
|
||||
)
|
||||
}
|
||||
|
||||
val hasSelection = items.any { it.selected }
|
||||
|
||||
items
|
||||
.forEach { item ->
|
||||
items.forEach { item ->
|
||||
if (query.isNotBlank() && filter != null && !filter(item, query)) {
|
||||
return@forEach
|
||||
}
|
||||
if (item.dividerBefore) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f),
|
||||
)
|
||||
DropdownMenuDivider()
|
||||
}
|
||||
ShadcnDropdownMenuItem(
|
||||
item = item,
|
||||
DropdownMenuItem(
|
||||
text = item.label,
|
||||
onClick = {
|
||||
item.onClick()
|
||||
expanded = false
|
||||
},
|
||||
hasSelection = hasSelection,
|
||||
enabled = item.enabled,
|
||||
selected = item.selected,
|
||||
leadingIcon = item.icon,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -177,117 +176,6 @@ fun AdaptiveDropdownBottomSheet(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShadcnDropdownMenu(
|
||||
expanded: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
minWidth: Dp,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val shape = RoundedCornerShape(6.dp)
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
offset = DpOffset(x = 0.dp, y = 4.dp),
|
||||
modifier = Modifier
|
||||
.width(minWidth)
|
||||
.shadow(
|
||||
elevation = 2.dp,
|
||||
shape = shape,
|
||||
ambientColor = Color.Black.copy(alpha = 0.06f),
|
||||
spotColor = Color.Black.copy(alpha = 0.1f),
|
||||
)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f),
|
||||
shape = shape,
|
||||
)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
shape = shape,
|
||||
)
|
||||
.clip(shape),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShadcnDropdownMenuItem(
|
||||
item: AdaptiveMenuItem,
|
||||
onClick: () -> Unit,
|
||||
hasSelection: Boolean,
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||
|
||||
val hoverColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.hoverable(interactionSource)
|
||||
.background(
|
||||
color = if (isHovered && item.enabled) hoverColor else Color.Transparent,
|
||||
)
|
||||
.clickable(
|
||||
enabled = item.enabled,
|
||||
interactionSource = interactionSource,
|
||||
indication = null,
|
||||
onClick = onClick,
|
||||
)
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
if (item.icon != null) {
|
||||
Icon(
|
||||
imageVector = item.icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = if (item.enabled) {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||
},
|
||||
)
|
||||
} else if (item.selected) {
|
||||
Icon(
|
||||
imageVector = FeatherIcons.Check,
|
||||
contentDescription = "Selected",
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
} else if (hasSelection) {
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
}
|
||||
|
||||
Text(
|
||||
text = item.label,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (item.enabled) {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
|
||||
if (item.selected && item.icon != null) {
|
||||
Icon(
|
||||
imageVector = FeatherIcons.Check,
|
||||
contentDescription = "Selected",
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun AdaptiveBottomSheetContent(
|
||||
|
||||
@ -53,6 +53,7 @@ import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer
|
||||
import dev.krtirtho.spotube.core.ui.misc.shimmerApply
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
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.IconsaxHeart2
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxPauseCircle
|
||||
@ -80,6 +81,7 @@ fun CollectionDetails(
|
||||
isFollowing: Boolean = false,
|
||||
onFollowClick: () -> Unit = { },
|
||||
showFollowButton: Boolean = true,
|
||||
onEdit: (() -> Unit)? = null,
|
||||
) {
|
||||
|
||||
|
||||
@ -151,6 +153,15 @@ fun CollectionDetails(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (onEdit != null) {
|
||||
ButtonGroupDivider()
|
||||
GroupIconButton(onClick = onEdit) {
|
||||
Icon(
|
||||
imageVector = Iconsax.IconsaxEdit,
|
||||
contentDescription = "Edit playlist",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val ownerInfo = @Composable {
|
||||
|
||||
@ -103,6 +103,7 @@ import dev.krtirtho.spotube.resources.iconsax.Iconsax3DotsMore
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxDirectboxReceive
|
||||
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.IconsaxPause
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxPlay
|
||||
@ -140,6 +141,7 @@ fun TrackList(
|
||||
onBulkDownload: (List<MetadataTrack>) -> Unit = {},
|
||||
onBulkAddToQueue: (List<MetadataTrack>) -> Unit = {},
|
||||
onBulkPlayNext: (List<MetadataTrack>) -> Unit = {},
|
||||
onBulkAddToPlaylist: (List<MetadataTrack>) -> Unit = {},
|
||||
currentTrackId: String? = null,
|
||||
isCurrentTrackPlaying: Boolean = false,
|
||||
trackOptionsState: (MetadataTrack) -> TrackOptionsState = { TrackOptionsState() },
|
||||
@ -433,6 +435,11 @@ fun TrackList(
|
||||
label = if (isAll) "Play All Next" else "Play $trackCount Next",
|
||||
onClick = { onBulkPlayNext(targetTracks) },
|
||||
),
|
||||
AdaptiveMenuItem(
|
||||
icon = Iconsax.IconsaxMusicPlaylist,
|
||||
label = if (isAll) "Add All to Playlist" else "Add $trackCount to Playlist",
|
||||
onClick = { onBulkAddToPlaylist(targetTracks) },
|
||||
),
|
||||
),
|
||||
trigger = { onClick ->
|
||||
GroupIconButton(
|
||||
@ -562,7 +569,7 @@ private fun TrackListRow(
|
||||
isSelected -> rowTheme.background.selected
|
||||
isCurrentTrack && isCurrentTrackPlaying -> rowTheme.background.hovered
|
||||
isCurrentTrack -> rowTheme.background.focused
|
||||
else -> Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent))
|
||||
else -> rowTheme.background.normal
|
||||
}
|
||||
val highlightColor = Color.White.copy(
|
||||
alpha = if (isLight) 0.9f else 0.06f
|
||||
|
||||
@ -49,6 +49,7 @@ import dev.krtirtho.spotube.resources.iconsax.IconsaxDirectboxReceive
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart2
|
||||
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.IconsaxNext
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxShare
|
||||
@ -59,6 +60,7 @@ sealed interface TrackOptionsAction {
|
||||
data object AddToQueue : TrackOptionsAction
|
||||
data object RemoveFromQueue : TrackOptionsAction
|
||||
data object ToggleFavorite : TrackOptionsAction
|
||||
data object AddToPlaylist : TrackOptionsAction
|
||||
data object Download : TrackOptionsAction
|
||||
data object ToggleBlacklist : TrackOptionsAction
|
||||
data object Share : TrackOptionsAction
|
||||
@ -274,6 +276,14 @@ private fun buildTrackMenuItems(
|
||||
),
|
||||
)
|
||||
|
||||
add(
|
||||
AdaptiveMenuItem(
|
||||
icon = Iconsax.IconsaxMusicPlaylist,
|
||||
label = "Add to playlist",
|
||||
onClick = { onAction(TrackOptionsAction.AddToPlaylist) },
|
||||
),
|
||||
)
|
||||
|
||||
add(
|
||||
AdaptiveMenuItem(
|
||||
icon = Iconsax.IconsaxDirectboxReceive,
|
||||
|
||||
@ -20,7 +20,12 @@ package dev.krtirtho.spotube.modules.album
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
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.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
||||
@ -40,6 +45,8 @@ import dev.krtirtho.spotube.core.ui.component.TrackOptionsState
|
||||
import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
|
||||
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.koin.compose.koinInject
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
@ -50,6 +57,8 @@ fun AlbumScreen(albumId: String) {
|
||||
val audioPlayer: AudioPlayer = koinInject()
|
||||
val shareService: ShareService = koinInject()
|
||||
val downloadsViewModel: DownloadsViewModel = koinViewModel()
|
||||
val libraryRepository: LibraryRepository = koinInject()
|
||||
val scope = rememberCoroutineScope()
|
||||
val viewModel = koinViewModel<AlbumViewModel>(
|
||||
key = albumId,
|
||||
parameters = { parametersOf(albumId) }
|
||||
@ -62,6 +71,13 @@ fun AlbumScreen(albumId: String) {
|
||||
val playerState by audioPlayer.playerStateFlow.collectAsStateWithLifecycle()
|
||||
val savedTrackIds by viewModel.savedTrackIds.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 {
|
||||
val currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id
|
||||
@ -87,6 +103,10 @@ fun AlbumScreen(albumId: String) {
|
||||
if (action is TrackOptionsAction.Download) {
|
||||
downloadsViewModel.downloadTrack(track)
|
||||
}
|
||||
if (action is TrackOptionsAction.AddToPlaylist) {
|
||||
tracksToAddToPlaylist = listOf(track)
|
||||
showAddToPlaylistPicker = true
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
@ -194,8 +214,26 @@ fun AlbumScreen(albumId: String) {
|
||||
onBulkPlayNext = { 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,6 +222,7 @@ class AlbumViewModel(
|
||||
is TrackOptionsAction.Download -> {}
|
||||
is TrackOptionsAction.ToggleBlacklist -> {}
|
||||
is TrackOptionsAction.Share -> {}
|
||||
is TrackOptionsAction.AddToPlaylist -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,12 +36,8 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
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.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
@ -49,7 +45,10 @@ import androidx.compose.material3.Text
|
||||
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.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@ -61,10 +60,6 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
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.artist.MetadataArtist
|
||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.playlist.MetadataPlaylist
|
||||
@ -76,6 +71,10 @@ import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
||||
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
||||
import dev.krtirtho.spotube.core.navigation.Routes
|
||||
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.ApplicationMainBar
|
||||
import dev.krtirtho.spotube.core.ui.component.ArtistCard
|
||||
@ -89,8 +88,15 @@ import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
|
||||
import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer
|
||||
import dev.krtirtho.spotube.core.ui.misc.shimmerApply
|
||||
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.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.compose.koinInject
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
@ -102,6 +108,8 @@ fun ArtistScreen(artistId: String) {
|
||||
val audioPlayer: AudioPlayer = koinInject()
|
||||
val shareService: ShareService = koinInject()
|
||||
val downloadsViewModel: DownloadsViewModel = koinViewModel()
|
||||
val libraryRepository: LibraryRepository = koinInject()
|
||||
val scope = rememberCoroutineScope()
|
||||
val viewModel = koinViewModel<ArtistViewModel>(
|
||||
key = artistId,
|
||||
parameters = { parametersOf(artistId) }
|
||||
@ -114,6 +122,13 @@ fun ArtistScreen(artistId: String) {
|
||||
val playerState by audioPlayer.playerStateFlow.collectAsStateWithLifecycle()
|
||||
val savedTrackIds by viewModel.savedTrackIds.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 {
|
||||
val currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id
|
||||
@ -139,6 +154,10 @@ fun ArtistScreen(artistId: String) {
|
||||
if (action is TrackOptionsAction.Download) {
|
||||
downloadsViewModel.downloadTrack(track)
|
||||
}
|
||||
if (action is TrackOptionsAction.AddToPlaylist) {
|
||||
tracksToAddToPlaylist = listOf(track)
|
||||
showAddToPlaylistPicker = true
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
@ -214,6 +233,10 @@ fun ArtistScreen(artistId: String) {
|
||||
onBulkDownload = { tracks -> downloadsViewModel.downloadTracks(tracks) },
|
||||
onBulkAddToQueue = { tracks -> viewModel.addTracksToQueue(tracks) },
|
||||
onBulkPlayNext = { tracks -> viewModel.playTracksNext(tracks) },
|
||||
onBulkAddToPlaylist = { tracks ->
|
||||
tracksToAddToPlaylist = tracks
|
||||
showAddToPlaylistPicker = true
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@ -267,6 +290,20 @@ fun ArtistScreen(artistId: String) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AddToPlaylistPicker(
|
||||
visible = showAddToPlaylistPicker,
|
||||
currentUserId = currentUserId,
|
||||
onDismiss = { showAddToPlaylistPicker = false },
|
||||
onPlaylistSelected = { playlistId ->
|
||||
scope.launch {
|
||||
libraryRepository.addTracksToPlaylist(
|
||||
playlistId,
|
||||
tracksToAddToPlaylist.map { it.id }
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -349,7 +386,7 @@ private fun ArtistErrorContent(
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
Button(onClick = onRetry) {
|
||||
PrimaryButton(onClick = onRetry) {
|
||||
Text("Retry")
|
||||
}
|
||||
}
|
||||
@ -452,7 +489,7 @@ private fun ArtistAvatar(
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = FeatherIcons.User,
|
||||
imageVector = Iconsax.User,
|
||||
contentDescription = artist.name,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(size * 0.4f),
|
||||
@ -516,11 +553,11 @@ private fun ArtistHeaderActions(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (isSaved) {
|
||||
FilledTonalButton(onClick = onFollowClick) {
|
||||
SecondaryButton(onClick = onFollowClick) {
|
||||
Text("Following", modifier = Modifier.width(65.dp), textAlign = TextAlign.Center)
|
||||
}
|
||||
} else {
|
||||
Button(onClick = onFollowClick) {
|
||||
PrimaryButton(onClick = onFollowClick) {
|
||||
Text("Follow", modifier = Modifier.width(65.dp), textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
@ -546,15 +583,15 @@ private fun TopTracksHeader(
|
||||
)
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
FilledIconButton(onClick = onPlay) {
|
||||
PrimaryIconButton(onClick = onPlay) {
|
||||
Icon(
|
||||
imageVector = FeatherIcons.Play,
|
||||
imageVector = Iconsax.IconsaxPlay,
|
||||
contentDescription = "Play top tracks",
|
||||
)
|
||||
}
|
||||
FilledTonalIconButton(onClick = onAddToQueue) {
|
||||
SecondaryIconButton(onClick = onAddToQueue) {
|
||||
Icon(
|
||||
imageVector = FeatherIcons.PlusSquare,
|
||||
imageVector = Iconsax.IconsaxAddSquare,
|
||||
contentDescription = "Add top tracks to queue",
|
||||
)
|
||||
}
|
||||
|
||||
@ -269,6 +269,7 @@ class ArtistViewModel(
|
||||
is TrackOptionsAction.Download -> {}
|
||||
is TrackOptionsAction.ToggleBlacklist -> {}
|
||||
is TrackOptionsAction.Share -> {}
|
||||
is TrackOptionsAction.AddToPlaylist -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ 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.PaginationStrategy
|
||||
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 io.github.reactivecircus.cache4k.Cache
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@ -276,4 +277,80 @@ class LibraryRepository(
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,257 @@
|
||||
/*
|
||||
* 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,10 +22,12 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
@ -35,12 +37,15 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.base.TextField
|
||||
import dev.krtirtho.spotube.core.ui.component.LikedTracksCard
|
||||
@ -49,6 +54,7 @@ import dev.krtirtho.spotube.core.ui.component.cards.PlayableCard
|
||||
import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
|
||||
import dev.krtirtho.spotube.modules.shell.LocalAppShellBottomInset
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxFilterSearch
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
@ -62,6 +68,7 @@ fun LibraryPlaylistsScreen(
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val gridState = rememberLazyGridState()
|
||||
var showCreatePlaylist by remember { mutableStateOf(false) }
|
||||
|
||||
|
||||
LaunchedEffect(gridState) {
|
||||
@ -84,8 +91,14 @@ fun LibraryPlaylistsScreen(
|
||||
}
|
||||
|
||||
Column(modifier = modifier) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = searchMode,
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
TextField(
|
||||
value = (state as? LibraryPlaylistsState.Data)?.query ?: "",
|
||||
@ -101,6 +114,16 @@ fun LibraryPlaylistsScreen(
|
||||
}
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = { showCreatePlaylist = true },
|
||||
modifier = Modifier.size(48.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Iconsax.IconsaxAddSquare,
|
||||
contentDescription = "Create Playlist",
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
when (state) {
|
||||
is LibraryPlaylistsState.Loading -> {
|
||||
@ -187,5 +210,20 @@ 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,4 +152,31 @@ class LibraryPlaylistsViewModel(
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,335 @@
|
||||
/*
|
||||
* 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.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 dev.krtirtho.spotube.core.ui.base.Toggle
|
||||
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,
|
||||
)
|
||||
}
|
||||
Toggle(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,
|
||||
)
|
||||
}
|
||||
Toggle(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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,325 @@
|
||||
/*
|
||||
* 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,11 +17,21 @@
|
||||
|
||||
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.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
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.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayer
|
||||
@ -31,6 +41,7 @@ import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
||||
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
||||
import dev.krtirtho.spotube.core.navigation.Routes
|
||||
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.CollectionDetails
|
||||
import dev.krtirtho.spotube.core.ui.component.ErrorDisplay
|
||||
@ -40,6 +51,13 @@ import dev.krtirtho.spotube.core.ui.component.TrackOptionsState
|
||||
import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
|
||||
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.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.viewmodel.koinViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
@ -50,6 +68,8 @@ fun PlaylistScreen(playlistId: String) {
|
||||
val audioPlayer: AudioPlayer = koinInject()
|
||||
val shareService: ShareService = koinInject()
|
||||
val downloadsViewModel: DownloadsViewModel = koinViewModel()
|
||||
val libraryRepository: LibraryRepository = koinInject()
|
||||
val scope = rememberCoroutineScope()
|
||||
val viewModel = koinViewModel<PlaylistViewModel>(
|
||||
key = playlistId,
|
||||
parameters = { parametersOf(playlistId) }
|
||||
@ -61,6 +81,11 @@ fun PlaylistScreen(playlistId: String) {
|
||||
val currentCollectionEntry by audioPlayerQueue.currentCollectionEntryFlow.collectAsStateWithLifecycle()
|
||||
val playerState by audioPlayer.playerStateFlow.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) {
|
||||
viewModel.handleTrackOptionsAction(track, action)
|
||||
@ -73,6 +98,10 @@ fun PlaylistScreen(playlistId: String) {
|
||||
if (action is TrackOptionsAction.Download) {
|
||||
downloadsViewModel.downloadTrack(track)
|
||||
}
|
||||
if (action is TrackOptionsAction.AddToPlaylist) {
|
||||
tracksToAddToPlaylist = listOf(track)
|
||||
showAddToPlaylistPicker = true
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
@ -131,6 +160,8 @@ fun PlaylistScreen(playlistId: String) {
|
||||
val ownerName = owner?.displayName ?: owner?.username ?: "Unknown"
|
||||
val ownerImageURL = owner?.thumbnails?.firstOrNull()?.url
|
||||
|
||||
val isOwner = currentUserId != null && playlist?.owner?.id == currentUserId
|
||||
|
||||
val savedTrackIds by viewModel.savedTrackIds.collectAsStateWithLifecycle()
|
||||
|
||||
|
||||
@ -166,8 +197,27 @@ fun PlaylistScreen(playlistId: String) {
|
||||
isFollowing = savedPlaylistIds.contains(playlistId),
|
||||
onFollowClick = viewModel::toggleSavedPlaylist,
|
||||
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,
|
||||
error = null,
|
||||
hasMore = dataState.nextPagination != null,
|
||||
@ -190,8 +240,61 @@ fun PlaylistScreen(playlistId: String) {
|
||||
onBulkPlayNext = { 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,6 +94,9 @@ class PlaylistViewModel(
|
||||
val savedPlaylistIds
|
||||
get() = libraryRepository.savedPlaylistIdsFlow
|
||||
|
||||
private val _currentUserId = MutableStateFlow<String?>(null)
|
||||
val currentUserId: StateFlow<String?> = _currentUserId.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
repository.pluginManager.selectedMetadataPlugin
|
||||
@ -102,10 +105,15 @@ class PlaylistViewModel(
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
loadInitialData()
|
||||
loadCurrentUser()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadCurrentUser() {
|
||||
_currentUserId.value = libraryRepository.currentUser()?.id
|
||||
}
|
||||
|
||||
private suspend fun loadInitialData() = runCatching {
|
||||
_state.value = PlaylistScreenState.Loading
|
||||
val playlistInfo = repository.getPlaylistInfo(playlistId)
|
||||
@ -179,6 +187,33 @@ 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) {
|
||||
viewModelScope.launch {
|
||||
when (action) {
|
||||
@ -224,6 +259,7 @@ class PlaylistViewModel(
|
||||
is TrackOptionsAction.Download -> {}
|
||||
is TrackOptionsAction.ToggleBlacklist -> {}
|
||||
is TrackOptionsAction.Share -> {}
|
||||
is TrackOptionsAction.AddToPlaylist -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,16 +21,17 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
@ -46,25 +47,40 @@ 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.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import compose.icons.FeatherIcons
|
||||
import compose.icons.feathericons.Package
|
||||
import dev.krtirtho.spotube.core.ui.base.Card
|
||||
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
|
||||
import dev.krtirtho.spotube.PlatformType
|
||||
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
||||
import dev.krtirtho.spotube.core.ui.component.AdaptiveDropdownBottomSheet
|
||||
import dev.krtirtho.spotube.core.ui.component.AdaptiveMenuItem
|
||||
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
|
||||
import dev.krtirtho.spotube.core.ui.component.HeaderDisplayMode
|
||||
import dev.krtirtho.spotube.core.webview.WebViewController
|
||||
import dev.krtirtho.spotube.getPlatform
|
||||
import dev.krtirtho.spotube.modules.plugin.components.InstallSection
|
||||
import dev.krtirtho.spotube.modules.plugin.components.PluginCard
|
||||
import dev.krtirtho.spotube.modules.plugin.components.PluginPermissionDialog
|
||||
import dev.krtirtho.spotube.modules.shell.LocalAppShellBottomInset
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxArrowDown4
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxBox
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxCheckSquare
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxDocumentText
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxEdit
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusic
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSound
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxTextalignLeft
|
||||
import io.github.vinceglb.filekit.dialogs.FileKitType
|
||||
import io.github.vinceglb.filekit.dialogs.compose.rememberFilePickerLauncher
|
||||
import io.github.vinceglb.filekit.readBytes
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.koinInject
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import spotube.composeapp.generated.resources.Res
|
||||
import spotube.composeapp.generated.resources.plugin_empty_subtitle
|
||||
import spotube.composeapp.generated.resources.plugin_empty_title
|
||||
@ -75,6 +91,18 @@ import spotube.composeapp.generated.resources.plugin_installed_count
|
||||
import spotube.composeapp.generated.resources.plugin_installed_plural
|
||||
import spotube.composeapp.generated.resources.plugin_installed_singular
|
||||
import spotube.composeapp.generated.resources.plugin_screen_title
|
||||
import spotube.composeapp.generated.resources.settings_plugins_action_change
|
||||
import spotube.composeapp.generated.resources.settings_plugins_action_select
|
||||
import spotube.composeapp.generated.resources.settings_plugins_ability_audio
|
||||
import spotube.composeapp.generated.resources.settings_plugins_ability_lyrics
|
||||
import spotube.composeapp.generated.resources.settings_plugins_ability_metadata
|
||||
import spotube.composeapp.generated.resources.settings_plugins_ability_scrobble
|
||||
import spotube.composeapp.generated.resources.settings_plugins_clear
|
||||
import spotube.composeapp.generated.resources.settings_plugins_default_ability_title
|
||||
import spotube.composeapp.generated.resources.settings_plugins_manage_title
|
||||
import spotube.composeapp.generated.resources.settings_plugins_no_plugins
|
||||
import spotube.composeapp.generated.resources.settings_plugins_no_selection
|
||||
import spotube.composeapp.generated.resources.settings_plugins_plugin_content_description
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@ -189,6 +217,45 @@ fun PluginScreen(
|
||||
)
|
||||
}
|
||||
|
||||
// ── Default ability plugin selectors ─────────────────
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 4.dp, bottom = 4.dp)
|
||||
) {
|
||||
PluginAbility.entries.forEachIndexed { index, ability ->
|
||||
if (index > 0) {
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.outlineVariant.copy(
|
||||
alpha = 0.5f
|
||||
),
|
||||
)
|
||||
}
|
||||
val selectedPlugin = state.selectedPlugins[ability]
|
||||
DefaultAbilityPluginSelector(
|
||||
ability = ability,
|
||||
selectedPlugin = selectedPlugin,
|
||||
state = when (ability) {
|
||||
PluginAbility.METADATA -> pluginManager.metadataPlugins
|
||||
PluginAbility.AUDIO -> pluginManager.audioPlugins
|
||||
PluginAbility.LYRICS -> pluginManager.lyricsPlugins
|
||||
PluginAbility.SCROBBLE -> pluginManager.scrobblePlugins
|
||||
},
|
||||
onSelected = { plugin ->
|
||||
pluginManager.setSelectedPlugin(ability, plugin)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.plugins.isEmpty()) {
|
||||
item {
|
||||
Box(
|
||||
@ -206,7 +273,7 @@ fun PluginScreen(
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(
|
||||
FeatherIcons.Package,
|
||||
Iconsax.IconsaxBox,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
@ -245,8 +312,25 @@ fun PluginScreen(
|
||||
modifier = Modifier.padding(horizontal = 4.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
items(state.plugins) { plugin ->
|
||||
val isSelected = state.selectedPlugins.containsValue(plugin)
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
state.plugins.forEachIndexed { index, plugin ->
|
||||
if (index > 0) {
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.outlineVariant.copy(
|
||||
alpha = 0.5f
|
||||
),
|
||||
)
|
||||
}
|
||||
val isSelected =
|
||||
state.selectedPlugins.containsValue(plugin)
|
||||
val selectedAbility = state.selectedPlugins
|
||||
.entries
|
||||
.firstOrNull { (_, selectedPlugin) -> selectedPlugin.id == plugin.id }
|
||||
@ -255,7 +339,10 @@ fun PluginScreen(
|
||||
activeServices?.get(ability)
|
||||
}
|
||||
|
||||
var requiresAuth by remember(plugin.id, selectedService) {
|
||||
var requiresAuth by remember(
|
||||
plugin.id,
|
||||
selectedService
|
||||
) {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var isLoggedIn by remember(plugin.id, selectedService) {
|
||||
@ -265,11 +352,13 @@ fun PluginScreen(
|
||||
LaunchedEffect(plugin.id, selectedService) {
|
||||
requiresAuth = false
|
||||
isLoggedIn = false
|
||||
val service = selectedService ?: return@LaunchedEffect
|
||||
val service =
|
||||
selectedService ?: return@LaunchedEffect
|
||||
|
||||
|
||||
service.use {
|
||||
val pluginRequiresAuth = coreAPI.requiresAuthentication
|
||||
val pluginRequiresAuth =
|
||||
coreAPI.requiresAuthentication
|
||||
requiresAuth = pluginRequiresAuth
|
||||
if (!pluginRequiresAuth) return@use
|
||||
|
||||
@ -282,7 +371,9 @@ fun PluginScreen(
|
||||
PluginCard(
|
||||
plugin = plugin,
|
||||
isSelected = isSelected,
|
||||
onRemove = { scope.launch { pluginManager.removePlugin(plugin) } },
|
||||
onRemove = {
|
||||
scope.launch { pluginManager.removePlugin(plugin) }
|
||||
},
|
||||
isLoggedIn = isLoggedIn,
|
||||
onLogin = if (requiresAuth && selectedService != null) {
|
||||
{
|
||||
@ -312,5 +403,195 @@ fun PluginScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DefaultAbilityPluginSelector(
|
||||
ability: PluginAbility,
|
||||
state: StateFlow<List<PluginEntry>>,
|
||||
selectedPlugin: PluginEntry? = null,
|
||||
onSelected: (PluginEntry?) -> Unit = { },
|
||||
) {
|
||||
val plugins by state.collectAsStateWithLifecycle()
|
||||
val noPluginsText = stringResource(Res.string.settings_plugins_no_plugins)
|
||||
|
||||
val menuItems = buildList {
|
||||
if (plugins.isNotEmpty()) {
|
||||
plugins.forEach { plugin ->
|
||||
val isSelected = selectedPlugin?.name == plugin.name
|
||||
add(
|
||||
AdaptiveMenuItem(
|
||||
label = plugin.name,
|
||||
onClick = { onSelected(plugin) },
|
||||
selected = isSelected,
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
add(
|
||||
AdaptiveMenuItem(
|
||||
label = noPluginsText,
|
||||
onClick = { },
|
||||
enabled = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.clip(RoundedCornerShape(8.dp)),
|
||||
color = when (ability) {
|
||||
PluginAbility.METADATA -> Color(0xFF4CAF50).copy(alpha = 0.1f)
|
||||
PluginAbility.AUDIO -> Color(0xFF2196F3).copy(alpha = 0.1f)
|
||||
PluginAbility.LYRICS -> Color(0xFFFFC107).copy(alpha = 0.1f)
|
||||
PluginAbility.SCROBBLE -> Color(0xFF9C27B0).copy(alpha = 0.1f)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (ability) {
|
||||
PluginAbility.METADATA -> Iconsax.IconsaxDocumentText
|
||||
PluginAbility.AUDIO -> Iconsax.IconsaxMusic
|
||||
PluginAbility.LYRICS -> Iconsax.IconsaxTextalignLeft
|
||||
PluginAbility.SCROBBLE -> Iconsax.IconsaxSound
|
||||
},
|
||||
contentDescription = stringResource(
|
||||
Res.string.settings_plugins_plugin_content_description,
|
||||
ability.displayLabel()
|
||||
),
|
||||
modifier = Modifier.padding(8.dp),
|
||||
tint = when (ability) {
|
||||
PluginAbility.METADATA -> Color(0xFF4CAF50)
|
||||
PluginAbility.AUDIO -> Color(0xFF2196F3)
|
||||
PluginAbility.LYRICS -> Color(0xFFFFC107)
|
||||
PluginAbility.SCROBBLE -> Color(0xFF9C27B0)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
stringResource(
|
||||
Res.string.settings_plugins_default_ability_title,
|
||||
ability.displayLabel()
|
||||
),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
if (selectedPlugin != null) {
|
||||
Text(
|
||||
selectedPlugin.name,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
stringResource(Res.string.settings_plugins_no_selection),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AdaptiveDropdownBottomSheet(
|
||||
items = menuItems,
|
||||
headerDisplayMode = HeaderDisplayMode.OnlyInBottomSheet,
|
||||
header = {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.clip(RoundedCornerShape(8.dp)),
|
||||
color = when (ability) {
|
||||
PluginAbility.METADATA -> Color(0xFF4CAF50).copy(alpha = 0.1f)
|
||||
PluginAbility.AUDIO -> Color(0xFF2196F3).copy(alpha = 0.1f)
|
||||
PluginAbility.LYRICS -> Color(0xFFFFC107).copy(alpha = 0.1f)
|
||||
PluginAbility.SCROBBLE -> Color(0xFF9C27B0).copy(alpha = 0.1f)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (ability) {
|
||||
PluginAbility.METADATA -> Iconsax.IconsaxDocumentText
|
||||
PluginAbility.AUDIO -> Iconsax.IconsaxMusic
|
||||
PluginAbility.LYRICS -> Iconsax.IconsaxTextalignLeft
|
||||
PluginAbility.SCROBBLE -> Iconsax.IconsaxSound
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(8.dp),
|
||||
tint = when (ability) {
|
||||
PluginAbility.METADATA -> Color(0xFF4CAF50)
|
||||
PluginAbility.AUDIO -> Color(0xFF2196F3)
|
||||
PluginAbility.LYRICS -> Color(0xFFFFC107)
|
||||
PluginAbility.SCROBBLE -> Color(0xFF9C27B0)
|
||||
}
|
||||
)
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
stringResource(
|
||||
Res.string.settings_plugins_default_ability_title,
|
||||
ability.displayLabel()
|
||||
),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
selectedPlugin?.let {
|
||||
Text(
|
||||
it.name,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
trigger = { onClick ->
|
||||
OutlineButton(onClick = onClick) {
|
||||
Text(
|
||||
if (selectedPlugin != null) {
|
||||
stringResource(Res.string.settings_plugins_action_change)
|
||||
} else {
|
||||
stringResource(Res.string.settings_plugins_action_select) + " "
|
||||
},
|
||||
)
|
||||
Icon(
|
||||
imageVector = if (selectedPlugin != null) Iconsax.IconsaxEdit else Iconsax.IconsaxArrowDown4,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(14.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PluginAbility.displayLabel(): String {
|
||||
return when (this) {
|
||||
PluginAbility.METADATA -> stringResource(Res.string.settings_plugins_ability_metadata)
|
||||
PluginAbility.AUDIO -> stringResource(Res.string.settings_plugins_ability_audio)
|
||||
PluginAbility.LYRICS -> stringResource(Res.string.settings_plugins_ability_lyrics)
|
||||
PluginAbility.SCROBBLE -> stringResource(Res.string.settings_plugins_ability_scrobble)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -17,36 +17,34 @@
|
||||
|
||||
package dev.krtirtho.spotube.modules.plugin.components
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
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.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import compose.icons.FeatherIcons
|
||||
import compose.icons.feathericons.Download
|
||||
import compose.icons.feathericons.Link
|
||||
import compose.icons.feathericons.Upload
|
||||
import dev.krtirtho.spotube.core.ui.base.Card
|
||||
import dev.krtirtho.spotube.core.ui.base.TextField
|
||||
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
||||
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
|
||||
import dev.krtirtho.spotube.core.ui.base.SecondaryIconButton
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxExportArrowBulk
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxImportArrow2Bulk
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxLink
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import spotube.composeapp.generated.resources.Res
|
||||
import spotube.composeapp.generated.resources.plugin_action_download
|
||||
@ -65,11 +63,6 @@ internal fun InstallSection(
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||
),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
@ -80,7 +73,7 @@ internal fun InstallSection(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
FeatherIcons.Download,
|
||||
Iconsax.IconsaxImportArrow2Bulk,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
@ -98,7 +91,7 @@ internal fun InstallSection(
|
||||
verticalAlignment = Alignment.Top,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
TextField(
|
||||
value = urlInput,
|
||||
onValueChange = onUrlChange,
|
||||
modifier = Modifier.weight(1f),
|
||||
@ -110,22 +103,19 @@ internal fun InstallSection(
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
FeatherIcons.Link,
|
||||
Iconsax.IconsaxLink,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
},
|
||||
isError = urlError != null,
|
||||
supportingText = urlError?.let { { Text(it) } },
|
||||
// supportingText = urlError?.let { { Text(it) } },
|
||||
// shape = RoundedCornerShape(10.dp),
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
textStyle = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
Button(
|
||||
SecondaryIconButton(
|
||||
onClick = onSubmitUrl,
|
||||
enabled = !isLoadingUrl,
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
modifier = Modifier.height(56.dp)
|
||||
) {
|
||||
if (isLoadingUrl) {
|
||||
CircularProgressIndicator(
|
||||
@ -135,9 +125,8 @@ internal fun InstallSection(
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
FeatherIcons.Download,
|
||||
Iconsax.IconsaxImportArrow2Bulk,
|
||||
contentDescription = stringResource(Res.string.plugin_action_download),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -146,15 +135,13 @@ internal fun InstallSection(
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
|
||||
|
||||
// File picker
|
||||
OutlinedButton(
|
||||
OutlineButton(
|
||||
onClick = onPickFile,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(10.dp)
|
||||
) {
|
||||
Icon(
|
||||
FeatherIcons.Upload,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
Iconsax.IconsaxExportArrowBulk,
|
||||
contentDescription = stringResource(Res.string.plugin_action_install_from_file)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(Res.string.plugin_action_install_from_file))
|
||||
@ -162,4 +149,3 @@ internal fun InstallSection(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -27,13 +27,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
@ -42,15 +38,17 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import compose.icons.FeatherIcons
|
||||
import compose.icons.feathericons.Check
|
||||
import compose.icons.feathericons.Package
|
||||
import compose.icons.feathericons.Tag
|
||||
import compose.icons.feathericons.Trash2
|
||||
import compose.icons.feathericons.User
|
||||
import dev.krtirtho.spotube.core.ui.base.GhostIconButton
|
||||
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
||||
import dev.krtirtho.spotube.modules.plugin.BUILT_IN_PLUGINS
|
||||
import dev.krtirtho.spotube.modules.plugin.PluginAbility
|
||||
import dev.krtirtho.spotube.modules.plugin.PluginEntry
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxBox
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxCheckSquare
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxTag
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash
|
||||
import dev.krtirtho.spotube.resources.iconsax.User
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import spotube.composeapp.generated.resources.Res
|
||||
import spotube.composeapp.generated.resources.plugin_action_remove
|
||||
@ -73,20 +71,6 @@ internal fun PluginCard(
|
||||
onLogin: (() -> Unit)? = null,
|
||||
onLogout: (() -> Unit)? = null,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||
),
|
||||
border = BorderStroke(
|
||||
width = 1.dp,
|
||||
color = if (isSelected)
|
||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.4f)
|
||||
else
|
||||
MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@ -103,7 +87,7 @@ internal fun PluginCard(
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(
|
||||
FeatherIcons.Package,
|
||||
Iconsax.IconsaxBox,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(22.dp)
|
||||
@ -140,7 +124,7 @@ internal fun PluginCard(
|
||||
horizontalArrangement = Arrangement.spacedBy(3.dp)
|
||||
) {
|
||||
Icon(
|
||||
FeatherIcons.Check,
|
||||
Iconsax.IconsaxCheckSquare,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(10.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
@ -175,7 +159,7 @@ internal fun PluginCard(
|
||||
horizontalArrangement = Arrangement.spacedBy(3.dp)
|
||||
) {
|
||||
Icon(
|
||||
FeatherIcons.User,
|
||||
Iconsax.User,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(11.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
@ -192,7 +176,7 @@ internal fun PluginCard(
|
||||
horizontalArrangement = Arrangement.spacedBy(3.dp)
|
||||
) {
|
||||
Icon(
|
||||
FeatherIcons.Tag,
|
||||
Iconsax.IconsaxTag,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(11.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
@ -247,12 +231,11 @@ internal fun PluginCard(
|
||||
.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||
)
|
||||
} else {
|
||||
IconButton(onClick = onRemove) {
|
||||
GhostIconButton(onClick = onRemove) {
|
||||
Icon(
|
||||
FeatherIcons.Trash2,
|
||||
Iconsax.IconsaxTrash,
|
||||
contentDescription = stringResource(Res.string.plugin_action_remove),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -263,7 +246,7 @@ internal fun PluginCard(
|
||||
else -> null
|
||||
}
|
||||
authAction?.let { (action, label) ->
|
||||
TextButton(onClick = action) {
|
||||
OutlineButton(onClick = action) {
|
||||
Text(
|
||||
text = stringResource(label),
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
@ -272,7 +255,6 @@ internal fun PluginCard(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ -284,4 +266,3 @@ private fun PluginAbility.displayLabel(): String {
|
||||
PluginAbility.SCROBBLE -> stringResource(Res.string.settings_plugins_ability_scrobble)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -25,18 +25,11 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
@ -45,16 +38,18 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import compose.icons.FeatherIcons
|
||||
import compose.icons.feathericons.Database
|
||||
import compose.icons.feathericons.Eye
|
||||
import compose.icons.feathericons.Package
|
||||
import compose.icons.feathericons.User
|
||||
import compose.icons.feathericons.Wifi
|
||||
import dev.krtirtho.spotube.core.ui.base.Card
|
||||
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
||||
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
|
||||
import dev.krtirtho.spotube.core.ui.base.ThemedDialog
|
||||
import dev.krtirtho.spotube.modules.plugin.PluginCapability
|
||||
import dev.krtirtho.spotube.modules.plugin.PluginEntry
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxBoxAdd
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxEye
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxWifiSquare
|
||||
import dev.krtirtho.spotube.resources.iconsax.PhosphorDatabaseDuotone
|
||||
import dev.krtirtho.spotube.resources.iconsax.User
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import spotube.composeapp.generated.resources.Res
|
||||
import spotube.composeapp.generated.resources.plugin_permissions_api_diff
|
||||
@ -84,48 +79,28 @@ fun PluginPermissionDialog(
|
||||
onConfirm: (() -> Unit)? = null,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
Dialog(
|
||||
ThemedDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.widthIn(min = 320.dp, max = 480.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f))
|
||||
.padding(24.dp)
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
title = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp)
|
||||
) {
|
||||
Surface(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(RoundedCornerShape(12.dp)),
|
||||
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.12f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(
|
||||
imageVector = FeatherIcons.Package,
|
||||
imageVector = Iconsax.IconsaxBoxAdd,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
Column {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
@ -143,10 +118,11 @@ fun PluginPermissionDialog(
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
) {
|
||||
Icon(
|
||||
FeatherIcons.User,
|
||||
Iconsax.User,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(13.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
@ -165,7 +141,27 @@ fun PluginPermissionDialog(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
},
|
||||
actions = {
|
||||
OutlineButton(onClick = onDismiss) {
|
||||
Text(
|
||||
if (confirmLabel == null) {
|
||||
stringResource(Res.string.settings_action_close)
|
||||
} else {
|
||||
stringResource(Res.string.settings_action_cancel)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (confirmLabel != null && onConfirm != null) {
|
||||
PrimaryButton(onClick = onConfirm) {
|
||||
Text(confirmLabel)
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
if (pluginInfo.description.isNotBlank()) {
|
||||
Text(
|
||||
pluginInfo.description,
|
||||
@ -175,22 +171,8 @@ fun PluginPermissionDialog(
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.55f)
|
||||
) {
|
||||
Card {
|
||||
Text(
|
||||
message,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
@ -225,41 +207,6 @@ fun PluginPermissionDialog(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.End)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = onDismiss,
|
||||
shape = RoundedCornerShape(10.dp)
|
||||
) {
|
||||
Text(
|
||||
if (confirmLabel == null) {
|
||||
stringResource(Res.string.settings_action_close)
|
||||
} else {
|
||||
stringResource(Res.string.settings_action_cancel)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (confirmLabel != null && onConfirm != null) {
|
||||
Button(
|
||||
onClick = onConfirm,
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Text(confirmLabel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -268,10 +215,8 @@ private fun InstalledComparisonCard(
|
||||
installedPlugin: PluginEntry,
|
||||
incomingPlugin: PluginEntry,
|
||||
) {
|
||||
Surface(
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.45f)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(14.dp),
|
||||
@ -328,28 +273,29 @@ private fun VersionStat(label: String, value: String) {
|
||||
private fun CapabilityRow(capability: PluginCapability) {
|
||||
val (icon, label, description) = when (capability) {
|
||||
PluginCapability.PERSISTENT_STORAGE -> Triple(
|
||||
FeatherIcons.Database,
|
||||
Iconsax.PhosphorDatabaseDuotone,
|
||||
stringResource(Res.string.plugin_permissions_capability_storage_title),
|
||||
stringResource(Res.string.plugin_permissions_capability_storage_desc)
|
||||
)
|
||||
PluginCapability.NETWORK_REQUESTS -> Triple(
|
||||
FeatherIcons.Wifi,
|
||||
Iconsax.IconsaxWifiSquare,
|
||||
stringResource(Res.string.plugin_permissions_capability_network_title),
|
||||
stringResource(Res.string.plugin_permissions_capability_network_desc)
|
||||
)
|
||||
PluginCapability.WEBVIEW -> Triple(
|
||||
FeatherIcons.Eye,
|
||||
Iconsax.IconsaxEye,
|
||||
stringResource(Res.string.plugin_permissions_capability_webview_title),
|
||||
stringResource(Res.string.plugin_permissions_capability_webview_desc)
|
||||
)
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp)
|
||||
) {
|
||||
@ -380,4 +326,5 @@ private fun CapabilityRow(capability: PluginCapability) {
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -20,7 +20,12 @@ package dev.krtirtho.spotube.modules.saved_tracks
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
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.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
||||
@ -40,6 +45,9 @@ import dev.krtirtho.spotube.core.ui.component.TrackOptionsAction
|
||||
import dev.krtirtho.spotube.core.ui.component.TrackOptionsState
|
||||
import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
|
||||
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.koin.compose.koinInject
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
@ -53,6 +61,8 @@ fun SavedTracksScreen() {
|
||||
val audioPlayer: AudioPlayer = koinInject()
|
||||
val shareService: ShareService = koinInject()
|
||||
val downloadsViewModel: DownloadsViewModel = koinViewModel()
|
||||
val libraryRepository: LibraryRepository = koinInject()
|
||||
val scope = rememberCoroutineScope()
|
||||
val viewModel = koinViewModel<SavedTracksViewModel>(
|
||||
key = SAVED_TRACKS_COLLECTION_ID,
|
||||
parameters = { parametersOf() }
|
||||
@ -63,6 +73,13 @@ fun SavedTracksScreen() {
|
||||
val currentQueueEntry by audioPlayerQueue.currentQueueEntryFlow.collectAsStateWithLifecycle()
|
||||
val currentCollectionEntry by audioPlayerQueue.currentCollectionEntryFlow.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 {
|
||||
val currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id
|
||||
@ -88,6 +105,10 @@ fun SavedTracksScreen() {
|
||||
if (action is TrackOptionsAction.Download) {
|
||||
downloadsViewModel.downloadTrack(track)
|
||||
}
|
||||
if (action is TrackOptionsAction.AddToPlaylist) {
|
||||
tracksToAddToPlaylist = listOf(track)
|
||||
showAddToPlaylistPicker = true
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
@ -187,8 +208,26 @@ fun SavedTracksScreen() {
|
||||
onBulkPlayNext = { 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,6 +197,7 @@ class SavedTracksViewModel(
|
||||
is TrackOptionsAction.Download -> {}
|
||||
is TrackOptionsAction.ToggleBlacklist -> {}
|
||||
is TrackOptionsAction.Share -> {}
|
||||
is TrackOptionsAction.AddToPlaylist -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,8 +57,10 @@ import androidx.compose.material3.Text
|
||||
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.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@ -97,6 +99,8 @@ import dev.krtirtho.spotube.core.ui.component.UserCard
|
||||
import dev.krtirtho.spotube.core.ui.component.cards.PlayableCard
|
||||
import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
|
||||
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.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken
|
||||
@ -115,10 +119,18 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
|
||||
val audioPlayerQueue: AudioPlayerQueue = koinInject()
|
||||
val shareService: ShareService = koinInject()
|
||||
val downloadsViewModel: DownloadsViewModel = koinViewModel()
|
||||
val libraryRepository: LibraryRepository = koinInject()
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val selectedType = state.selectedSearchType
|
||||
val scope = rememberCoroutineScope()
|
||||
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
|
||||
|
||||
@ -175,6 +187,10 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
|
||||
shareService.share(uri, track.title)
|
||||
}
|
||||
}
|
||||
is TrackOptionsAction.AddToPlaylist -> {
|
||||
tracksToAddToPlaylist = listOf(track)
|
||||
showAddToPlaylistPicker = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -295,6 +311,10 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
|
||||
onBulkDownload = { tracks -> downloadsViewModel.downloadTracks(tracks) },
|
||||
onBulkAddToQueue = ::bulkAddToQueue,
|
||||
onBulkPlayNext = ::bulkPlayNext,
|
||||
onBulkAddToPlaylist = { tracks ->
|
||||
tracksToAddToPlaylist = tracks
|
||||
showAddToPlaylistPicker = true
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
)
|
||||
@ -311,6 +331,10 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
|
||||
onBulkDownload = { tracks -> downloadsViewModel.downloadTracks(tracks) },
|
||||
onBulkAddToQueue = ::bulkAddToQueue,
|
||||
onBulkPlayNext = ::bulkPlayNext,
|
||||
onBulkAddToPlaylist = { tracks ->
|
||||
tracksToAddToPlaylist = tracks
|
||||
showAddToPlaylistPicker = true
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
)
|
||||
@ -357,6 +381,20 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AddToPlaylistPicker(
|
||||
visible = showAddToPlaylistPicker,
|
||||
currentUserId = currentUserId,
|
||||
onDismiss = { showAddToPlaylistPicker = false },
|
||||
onPlaylistSelected = { playlistId ->
|
||||
scope.launch {
|
||||
libraryRepository.addTracksToPlaylist(
|
||||
playlistId,
|
||||
tracksToAddToPlaylist.map { it.id }
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -568,6 +606,7 @@ private fun SearchAllTab(
|
||||
onBulkDownload: (List<MetadataTrack>) -> Unit,
|
||||
onBulkAddToQueue: (List<MetadataTrack>) -> Unit,
|
||||
onBulkPlayNext: (List<MetadataTrack>) -> Unit,
|
||||
onBulkAddToPlaylist: (List<MetadataTrack>) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (query.isBlank()) {
|
||||
@ -621,6 +660,7 @@ private fun SearchAllTab(
|
||||
onBulkDownload = onBulkDownload,
|
||||
onBulkAddToQueue = onBulkAddToQueue,
|
||||
onBulkPlayNext = onBulkPlayNext,
|
||||
onBulkAddToPlaylist = onBulkAddToPlaylist,
|
||||
contentPadding = PaddingValues(top = 12.dp, bottom = 16.dp + bottomInset),
|
||||
headerContent = {
|
||||
if (showTracks) {
|
||||
@ -704,6 +744,7 @@ private fun SearchTracksTab(
|
||||
onBulkDownload: (List<MetadataTrack>) -> Unit,
|
||||
onBulkAddToQueue: (List<MetadataTrack>) -> Unit,
|
||||
onBulkPlayNext: (List<MetadataTrack>) -> Unit,
|
||||
onBulkAddToPlaylist: (List<MetadataTrack>) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (query.isBlank()) {
|
||||
@ -729,6 +770,7 @@ private fun SearchTracksTab(
|
||||
onBulkDownload = onBulkDownload,
|
||||
onBulkAddToQueue = onBulkAddToQueue,
|
||||
onBulkPlayNext = onBulkPlayNext,
|
||||
onBulkAddToPlaylist = onBulkAddToPlaylist,
|
||||
simplified = true,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
||||
@ -37,7 +37,6 @@ import dev.krtirtho.spotube.PlatformType
|
||||
import dev.krtirtho.spotube.getPlatform
|
||||
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
||||
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
|
||||
import dev.krtirtho.spotube.modules.plugin.PluginManager
|
||||
import spotube.composeapp.generated.resources.*
|
||||
import dev.krtirtho.spotube.modules.settings.sections.appearanceSection
|
||||
import dev.krtirtho.spotube.modules.settings.sections.cacheSection
|
||||
@ -54,9 +53,8 @@ import org.koin.compose.koinInject
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(pluginManager: PluginManager, settingsViewModel: SettingsViewModel) {
|
||||
fun SettingsScreen(settingsViewModel: SettingsViewModel) {
|
||||
val navigatorCommands = koinInject<NavigationCommands>()
|
||||
val pluginState by pluginManager.state.collectAsStateWithLifecycle()
|
||||
val settingsState by settingsViewModel.settingsState.collectAsStateWithLifecycle()
|
||||
val platformType = remember { getPlatform().type }
|
||||
val isDesktopPlatform = platformType == PlatformType.Windows ||
|
||||
@ -90,8 +88,6 @@ fun SettingsScreen(pluginManager: PluginManager, settingsViewModel: SettingsView
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
pluginsSection(
|
||||
pluginManager = pluginManager,
|
||||
pluginState = pluginState,
|
||||
navigatorCommands = navigatorCommands,
|
||||
)
|
||||
if (settingsState != null) {
|
||||
|
||||
@ -24,18 +24,13 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
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.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@ -46,8 +41,16 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
||||
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
|
||||
import dev.krtirtho.spotube.core.ui.base.Radio
|
||||
import dev.krtirtho.spotube.core.ui.base.TextField
|
||||
import dev.krtirtho.spotube.core.ui.base.ThemedDialog
|
||||
import dev.krtirtho.spotube.core.ui.base.Toggle
|
||||
import dev.krtirtho.spotube.core.ui.component.AdaptiveDropdownBottomSheet
|
||||
import dev.krtirtho.spotube.core.ui.component.AdaptiveMenuItem
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxArrowDown4
|
||||
import spotube.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@ -64,7 +67,7 @@ internal fun SwitchSettingCard(
|
||||
subtitle = subtitle,
|
||||
icon = icon,
|
||||
trailingContent = {
|
||||
Switch(
|
||||
Toggle(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
@ -107,8 +110,13 @@ internal fun <T> SelectionSettingCard(
|
||||
)
|
||||
},
|
||||
trigger = { onClick ->
|
||||
TextButton(onClick = onClick) {
|
||||
OutlineButton(onClick = onClick) {
|
||||
Text(optionLabel(selectedOption))
|
||||
Icon(
|
||||
imageVector = Iconsax.IconsaxArrowDown4,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(14.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
filter = filter,
|
||||
@ -122,16 +130,18 @@ internal fun <T> SelectionSettingCard(
|
||||
)
|
||||
|
||||
if (!isWideLayout && isDialogOpen) {
|
||||
AlertDialog(
|
||||
ThemedDialog(
|
||||
onDismissRequest = { isDialogOpen = false },
|
||||
title = {
|
||||
Text(dialogTitle)
|
||||
Text(dialogTitle, style = MaterialTheme.typography.titleLarge)
|
||||
},
|
||||
actions = {
|
||||
OutlineButton(onClick = { isDialogOpen = false }) {
|
||||
Text(stringResource(Res.string.settings_action_close))
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
options.forEach { option ->
|
||||
val isSelected = option == selectedOption
|
||||
Row(
|
||||
@ -146,7 +156,7 @@ internal fun <T> SelectionSettingCard(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
RadioButton(
|
||||
Radio(
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
onOptionSelected(option)
|
||||
@ -160,14 +170,8 @@ internal fun <T> SelectionSettingCard(
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { isDialogOpen = false }) {
|
||||
Text(stringResource(Res.string.settings_action_close))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -234,12 +238,27 @@ internal fun TextInputSettingCard(
|
||||
val normalizedValue = normalize(draft)
|
||||
val errorMessage = validate(normalizedValue)
|
||||
|
||||
AlertDialog(
|
||||
ThemedDialog(
|
||||
onDismissRequest = { isDialogOpen = false },
|
||||
title = {
|
||||
Text(dialogTitle)
|
||||
Text(dialogTitle, style = MaterialTheme.typography.titleLarge)
|
||||
},
|
||||
text = {
|
||||
actions = {
|
||||
PrimaryButton(
|
||||
onClick = {
|
||||
if (errorMessage == null) {
|
||||
onValueSaved(normalizedValue)
|
||||
isDialogOpen = false
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(Res.string.settings_action_save))
|
||||
}
|
||||
OutlineButton(onClick = { isDialogOpen = false }) {
|
||||
Text(stringResource(Res.string.settings_action_cancel))
|
||||
}
|
||||
},
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
dialogDescription?.let {
|
||||
Text(
|
||||
@ -264,25 +283,7 @@ internal fun TextInputSettingCard(
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (errorMessage == null) {
|
||||
onValueSaved(normalizedValue)
|
||||
isDialogOpen = false
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(Res.string.settings_action_save))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { isDialogOpen = false }) {
|
||||
Text(stringResource(Res.string.settings_action_cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,26 +17,11 @@
|
||||
|
||||
package dev.krtirtho.spotube.modules.settings.components
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.krtirtho.spotube.core.ui.base.ListRowTile
|
||||
|
||||
/**
|
||||
* Reusable setting card item component for consistent styling
|
||||
@ -48,54 +33,32 @@ fun SettingCardItem(
|
||||
title: String,
|
||||
subtitle: String? = null,
|
||||
icon: (@Composable () -> Unit)? = null,
|
||||
trailingContent: (@Composable RowScope.() -> Unit)? = null,
|
||||
trailingContent: (@Composable () -> Unit)? = null,
|
||||
onClick: () -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.clickable(onClick = onClick, enabled = enabled)
|
||||
.then(if (!enabled) Modifier.alpha(0.5f) else Modifier),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||
),
|
||||
border = BorderStroke(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (icon != null) {
|
||||
icon()
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
ListRowTile(
|
||||
selected = false,
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
leading = icon,
|
||||
title = {
|
||||
Text(
|
||||
title,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
if (subtitle != null) {
|
||||
},
|
||||
subtitle = subtitle?.let {
|
||||
{
|
||||
Text(
|
||||
subtitle,
|
||||
it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.Gray,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
trailingContent?.invoke(this)
|
||||
}
|
||||
}
|
||||
},
|
||||
trailing = trailingContent,
|
||||
)
|
||||
}
|
||||
@ -27,19 +27,14 @@ 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.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@ -53,12 +48,19 @@ import compose.icons.FeatherIcons
|
||||
import compose.icons.feathericons.Droplet
|
||||
import compose.icons.feathericons.Monitor
|
||||
import spotube.composeapp.generated.resources.*
|
||||
import dev.krtirtho.spotube.core.ui.base.ThemedDialog
|
||||
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
||||
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
|
||||
import dev.krtirtho.spotube.core.ui.base.Radio
|
||||
import dev.krtirtho.spotube.modules.settings.AccentColors
|
||||
import dev.krtirtho.spotube.modules.settings.SettingsViewModel
|
||||
import dev.krtirtho.spotube.modules.settings.Theme
|
||||
import dev.krtirtho.spotube.modules.settings.UserSettings
|
||||
import dev.krtirtho.spotube.modules.settings.components.SelectionSettingCard
|
||||
import dev.krtirtho.spotube.modules.settings.components.SettingCardItem
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxColorSwatch
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxColorsSquare
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import spotube.composeapp.generated.resources.*
|
||||
|
||||
@ -67,8 +69,9 @@ internal fun LazyListScope.appearanceSection(
|
||||
settingsViewModel: SettingsViewModel,
|
||||
) {
|
||||
settingsSectionHeader(Res.string.settings_section_appearance)
|
||||
|
||||
item {
|
||||
settingsSectionCard(
|
||||
items = listOf(
|
||||
{
|
||||
SelectionSettingCard(
|
||||
title = stringResource(Res.string.settings_theme_title),
|
||||
subtitle = stringResource(
|
||||
@ -76,7 +79,7 @@ internal fun LazyListScope.appearanceSection(
|
||||
settings.theme.displayLabel()
|
||||
),
|
||||
icon = {
|
||||
SettingsItemIcon(FeatherIcons.Monitor, stringResource(Res.string.settings_theme_title))
|
||||
SettingsItemIcon(Iconsax.IconsaxColorSwatch, stringResource(Res.string.settings_theme_title))
|
||||
},
|
||||
selectedOption = settings.theme,
|
||||
options = Theme.entries,
|
||||
@ -87,13 +90,12 @@ internal fun LazyListScope.appearanceSection(
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
},
|
||||
{
|
||||
AccentColorSettingCard(
|
||||
selectedAccent = settings.accentColor,
|
||||
icon = {
|
||||
SettingsItemIcon(FeatherIcons.Droplet, stringResource(Res.string.settings_accent_title))
|
||||
SettingsItemIcon(Iconsax.IconsaxColorsSquare, stringResource(Res.string.settings_accent_title))
|
||||
},
|
||||
onColorSaved = { accent ->
|
||||
settingsViewModel.updateSettings {
|
||||
@ -101,7 +103,9 @@ internal fun LazyListScope.appearanceSection(
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ -155,19 +159,26 @@ private fun AccentColorSettingCard(
|
||||
if (isDialogOpen) {
|
||||
var draftAccent by remember(selectedAccent, isDialogOpen) { mutableStateOf(selectedAccent) }
|
||||
|
||||
AlertDialog(
|
||||
ThemedDialog(
|
||||
onDismissRequest = { isDialogOpen = false },
|
||||
title = {
|
||||
Text(stringResource(Res.string.settings_accent_dialog_title))
|
||||
Text(stringResource(Res.string.settings_accent_dialog_title), style = MaterialTheme.typography.titleLarge)
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 420.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
actions = {
|
||||
PrimaryButton(
|
||||
onClick = {
|
||||
onColorSaved(draftAccent)
|
||||
isDialogOpen = false
|
||||
}
|
||||
) {
|
||||
Text(stringResource(Res.string.settings_action_save))
|
||||
}
|
||||
OutlineButton(onClick = { isDialogOpen = false }) {
|
||||
Text(stringResource(Res.string.settings_action_cancel))
|
||||
}
|
||||
},
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text(
|
||||
text = stringResource(Res.string.settings_accent_dialog_description),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
@ -204,7 +215,7 @@ private fun AccentColorSettingCard(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
RadioButton(
|
||||
Radio(
|
||||
selected = option == draftAccent,
|
||||
onClick = { draftAccent = option }
|
||||
)
|
||||
@ -221,23 +232,7 @@ private fun AccentColorSettingCard(
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onColorSaved(draftAccent)
|
||||
isDialogOpen = false
|
||||
}
|
||||
) {
|
||||
Text(stringResource(Res.string.settings_action_save))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { isDialogOpen = false }) {
|
||||
Text(stringResource(Res.string.settings_action_cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -19,19 +19,22 @@ package dev.krtirtho.spotube.modules.settings.sections
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import compose.icons.FeatherIcons
|
||||
import compose.icons.feathericons.Folder
|
||||
import compose.icons.feathericons.HardDrive
|
||||
import spotube.composeapp.generated.resources.*
|
||||
import dev.krtirtho.spotube.core.ui.base.GhostIconButton
|
||||
import dev.krtirtho.spotube.modules.settings.SettingsViewModel
|
||||
import dev.krtirtho.spotube.modules.settings.UserSettings
|
||||
import dev.krtirtho.spotube.modules.settings.components.SettingCardItem
|
||||
import dev.krtirtho.spotube.modules.settings.components.SwitchSettingCard
|
||||
import dev.krtirtho.spotube.modules.settings.components.TextInputSettingCard
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxDocumentDownload
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxFolder
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxFolderAdd
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxStopCircle
|
||||
import io.github.vinceglb.filekit.path
|
||||
import io.github.vinceglb.filekit.dialogs.compose.rememberDirectoryPickerLauncher
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
@ -41,14 +44,15 @@ internal fun LazyListScope.cacheSection(
|
||||
settingsViewModel: SettingsViewModel,
|
||||
) {
|
||||
settingsSectionHeader(Res.string.settings_section_caching)
|
||||
|
||||
item {
|
||||
settingsSectionCard(
|
||||
items = listOf(
|
||||
{
|
||||
SwitchSettingCard(
|
||||
title = stringResource(Res.string.settings_enable_music_caching_title),
|
||||
subtitle = stringResource(Res.string.settings_enable_music_caching_subtitle),
|
||||
icon = {
|
||||
SettingsItemIcon(
|
||||
FeatherIcons.HardDrive,
|
||||
Iconsax.IconsaxDocumentDownload,
|
||||
stringResource(Res.string.settings_enable_music_caching_title)
|
||||
)
|
||||
},
|
||||
@ -59,9 +63,8 @@ internal fun LazyListScope.cacheSection(
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
},
|
||||
{
|
||||
CacheFolderSettingCard(
|
||||
enabled = settings.enableMusicCaching,
|
||||
folder = settings.cacheFolder,
|
||||
@ -71,9 +74,8 @@ internal fun LazyListScope.cacheSection(
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
},
|
||||
{
|
||||
val error_whole_number = stringResource(Res.string.settings_error_whole_number)
|
||||
val error_cache_range = stringResource(Res.string.settings_error_cache_size_range)
|
||||
|
||||
@ -89,7 +91,7 @@ internal fun LazyListScope.cacheSection(
|
||||
},
|
||||
icon = {
|
||||
SettingsItemIcon(
|
||||
FeatherIcons.HardDrive,
|
||||
Iconsax.IconsaxStopCircle,
|
||||
stringResource(Res.string.settings_cache_size_limit_title)
|
||||
)
|
||||
},
|
||||
@ -117,7 +119,9 @@ internal fun LazyListScope.cacheSection(
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ -142,17 +146,17 @@ private fun CacheFolderSettingCard(
|
||||
} ?: stringResource(Res.string.settings_cache_folder_default),
|
||||
icon = {
|
||||
SettingsItemIcon(
|
||||
FeatherIcons.Folder,
|
||||
Iconsax.IconsaxFolder,
|
||||
stringResource(Res.string.settings_cache_folder_title)
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
IconButton(
|
||||
GhostIconButton(
|
||||
onClick = { pickerLauncher.launch() },
|
||||
enabled = enabled,
|
||||
) {
|
||||
Icon(
|
||||
FeatherIcons.Folder,
|
||||
Iconsax.IconsaxFolderAdd,
|
||||
contentDescription = stringResource(Res.string.settings_cache_folder_title)
|
||||
)
|
||||
}
|
||||
|
||||
@ -18,13 +18,13 @@
|
||||
package dev.krtirtho.spotube.modules.settings.sections
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import compose.icons.FeatherIcons
|
||||
import compose.icons.feathericons.Activity
|
||||
import compose.icons.feathericons.Minimize2
|
||||
import spotube.composeapp.generated.resources.*
|
||||
import dev.krtirtho.spotube.modules.settings.SettingsViewModel
|
||||
import dev.krtirtho.spotube.modules.settings.UserSettings
|
||||
import dev.krtirtho.spotube.modules.settings.components.SwitchSettingCard
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxImportArrow2
|
||||
import dev.krtirtho.spotube.resources.iconsax.LineDiscord
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
internal fun LazyListScope.desktopSection(
|
||||
@ -32,14 +32,15 @@ internal fun LazyListScope.desktopSection(
|
||||
settingsViewModel: SettingsViewModel,
|
||||
) {
|
||||
settingsSectionHeader(Res.string.settings_section_desktop)
|
||||
|
||||
item {
|
||||
settingsSectionCard(
|
||||
items = listOf(
|
||||
{
|
||||
SwitchSettingCard(
|
||||
title = stringResource(Res.string.settings_desktop_minimize_title),
|
||||
subtitle = stringResource(Res.string.settings_desktop_minimize_subtitle),
|
||||
icon = {
|
||||
SettingsItemIcon(
|
||||
FeatherIcons.Minimize2,
|
||||
Iconsax.IconsaxImportArrow2,
|
||||
stringResource(Res.string.settings_desktop_minimize_title)
|
||||
)
|
||||
},
|
||||
@ -50,15 +51,14 @@ internal fun LazyListScope.desktopSection(
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
},
|
||||
{
|
||||
SwitchSettingCard(
|
||||
title = stringResource(Res.string.settings_desktop_discord_title),
|
||||
subtitle = stringResource(Res.string.settings_desktop_discord_subtitle),
|
||||
icon = {
|
||||
SettingsItemIcon(
|
||||
FeatherIcons.Activity,
|
||||
Iconsax.LineDiscord,
|
||||
stringResource(Res.string.settings_desktop_discord_title)
|
||||
)
|
||||
},
|
||||
@ -69,6 +69,8 @@ internal fun LazyListScope.desktopSection(
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -24,14 +24,9 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
@ -42,20 +37,44 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import compose.icons.FeatherIcons
|
||||
import compose.icons.feathericons.Disc
|
||||
import compose.icons.feathericons.Folder
|
||||
import compose.icons.feathericons.PlusSquare
|
||||
import compose.icons.feathericons.Sliders
|
||||
import compose.icons.feathericons.Trash2
|
||||
import spotube.composeapp.generated.resources.*
|
||||
import dev.krtirtho.spotube.core.ui.base.GhostIconButton
|
||||
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
||||
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
|
||||
import dev.krtirtho.spotube.core.ui.base.ThemedDialog
|
||||
import dev.krtirtho.spotube.modules.settings.SettingsViewModel
|
||||
import dev.krtirtho.spotube.modules.settings.UserSettings
|
||||
import dev.krtirtho.spotube.modules.settings.components.SettingCardItem
|
||||
import dev.krtirtho.spotube.modules.settings.components.SelectionSettingCard
|
||||
import io.github.vinceglb.filekit.path
|
||||
import dev.krtirtho.spotube.modules.settings.components.SettingCardItem
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxDirectboxReceive
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxFolder
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxFolderAdd
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicPlay
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxVerticalSetting
|
||||
import io.github.vinceglb.filekit.dialogs.compose.rememberDirectoryPickerLauncher
|
||||
import io.github.vinceglb.filekit.path
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import spotube.composeapp.generated.resources.Res
|
||||
import spotube.composeapp.generated.resources.settings_action_cancel
|
||||
import spotube.composeapp.generated.resources.settings_action_save
|
||||
import spotube.composeapp.generated.resources.settings_download_folder_current
|
||||
import spotube.composeapp.generated.resources.settings_download_folder_default
|
||||
import spotube.composeapp.generated.resources.settings_download_folder_title
|
||||
import spotube.composeapp.generated.resources.settings_download_format_subtitle_current
|
||||
import spotube.composeapp.generated.resources.settings_download_format_title
|
||||
import spotube.composeapp.generated.resources.settings_download_quality_title
|
||||
import spotube.composeapp.generated.resources.settings_local_media_folders_add_action
|
||||
import spotube.composeapp.generated.resources.settings_local_media_folders_description
|
||||
import spotube.composeapp.generated.resources.settings_local_media_folders_manage
|
||||
import spotube.composeapp.generated.resources.settings_local_media_folders_none_added
|
||||
import spotube.composeapp.generated.resources.settings_local_media_folders_remove_action
|
||||
import spotube.composeapp.generated.resources.settings_local_media_folders_subtitle_current
|
||||
import spotube.composeapp.generated.resources.settings_local_media_folders_subtitle_empty
|
||||
import spotube.composeapp.generated.resources.settings_local_media_folders_title
|
||||
import spotube.composeapp.generated.resources.settings_section_downloads
|
||||
import spotube.composeapp.generated.resources.settings_subtitle_current
|
||||
|
||||
internal fun LazyListScope.downloadsSection(
|
||||
settings: UserSettings,
|
||||
@ -68,8 +87,9 @@ internal fun LazyListScope.downloadsSection(
|
||||
)
|
||||
|
||||
settingsSectionHeader(Res.string.settings_section_downloads)
|
||||
|
||||
item {
|
||||
settingsSectionCard(
|
||||
items = listOf(
|
||||
{
|
||||
DownloadFolderSettingCard(
|
||||
folder = settings.overloadedDownloadFolder,
|
||||
onFolderSelected = { folder ->
|
||||
@ -78,9 +98,8 @@ internal fun LazyListScope.downloadsSection(
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
},
|
||||
{
|
||||
LocalMediaFoldersSettingCard(
|
||||
folders = settings.localMediaFolders,
|
||||
onFoldersSaved = { folders ->
|
||||
@ -89,9 +108,8 @@ internal fun LazyListScope.downloadsSection(
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
},
|
||||
{
|
||||
SelectionSettingCard(
|
||||
title = stringResource(Res.string.settings_download_format_title),
|
||||
subtitle = stringResource(
|
||||
@ -99,7 +117,7 @@ internal fun LazyListScope.downloadsSection(
|
||||
settings.downloadMusicFormat.displayLabel()
|
||||
),
|
||||
icon = {
|
||||
SettingsItemIcon(FeatherIcons.Disc, stringResource(Res.string.settings_download_format_title))
|
||||
SettingsItemIcon(Iconsax.IconsaxMusicPlay, stringResource(Res.string.settings_download_format_title))
|
||||
},
|
||||
selectedOption = settings.downloadMusicFormat,
|
||||
options = downloadFormats,
|
||||
@ -113,9 +131,8 @@ internal fun LazyListScope.downloadsSection(
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
},
|
||||
{
|
||||
SelectionSettingCard(
|
||||
title = stringResource(Res.string.settings_download_quality_title),
|
||||
subtitle = stringResource(
|
||||
@ -123,7 +140,7 @@ internal fun LazyListScope.downloadsSection(
|
||||
settings.downloadMusicQuality.displayLabel()
|
||||
),
|
||||
icon = {
|
||||
SettingsItemIcon(FeatherIcons.Sliders, stringResource(Res.string.settings_download_quality_title))
|
||||
SettingsItemIcon(Iconsax.IconsaxVerticalSetting, stringResource(Res.string.settings_download_quality_title))
|
||||
},
|
||||
selectedOption = settings.downloadMusicQuality,
|
||||
options = downloadQualities,
|
||||
@ -134,7 +151,9 @@ internal fun LazyListScope.downloadsSection(
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ -157,12 +176,12 @@ private fun DownloadFolderSettingCard(
|
||||
stringResource(Res.string.settings_download_folder_current, it)
|
||||
} ?: stringResource(Res.string.settings_download_folder_default),
|
||||
icon = {
|
||||
SettingsItemIcon(FeatherIcons.Folder, stringResource(Res.string.settings_download_folder_title))
|
||||
SettingsItemIcon(Iconsax.IconsaxDirectboxReceive, stringResource(Res.string.settings_download_folder_title))
|
||||
},
|
||||
trailingContent = {
|
||||
IconButton(onClick = { pickerLauncher.launch() }) {
|
||||
GhostIconButton(onClick = { pickerLauncher.launch() }) {
|
||||
Icon(
|
||||
FeatherIcons.Folder,
|
||||
imageVector = Iconsax.IconsaxFolderAdd,
|
||||
contentDescription = stringResource(Res.string.settings_download_folder_title)
|
||||
)
|
||||
}
|
||||
@ -194,12 +213,12 @@ private fun LocalMediaFoldersSettingCard(
|
||||
subtitle = subtitlePreview,
|
||||
icon = {
|
||||
SettingsItemIcon(
|
||||
FeatherIcons.Folder,
|
||||
Iconsax.IconsaxFolder,
|
||||
stringResource(Res.string.settings_local_media_folders_title),
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
TextButton(onClick = { isDialogOpen = true }) {
|
||||
OutlineButton(onClick = { isDialogOpen = true }) {
|
||||
Text(stringResource(Res.string.settings_local_media_folders_manage))
|
||||
}
|
||||
},
|
||||
@ -235,28 +254,41 @@ private fun LocalMediaFoldersDialog(
|
||||
}
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
ThemedDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(stringResource(Res.string.settings_local_media_folders_title))
|
||||
Text(
|
||||
text = stringResource(Res.string.settings_local_media_folders_title),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
actions = {
|
||||
PrimaryButton(
|
||||
onClick = {
|
||||
onFoldersSaved(draftFolders.map(::normalizePath).filter { it.isNotBlank() }.distinct())
|
||||
onDismiss()
|
||||
}
|
||||
) {
|
||||
Text(stringResource(Res.string.settings_action_save))
|
||||
}
|
||||
OutlineButton(onClick = onDismiss) {
|
||||
Text(stringResource(Res.string.settings_action_cancel))
|
||||
}
|
||||
},
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text(
|
||||
text = stringResource(Res.string.settings_local_media_folders_description),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
TextButton(onClick = { pickerLauncher.launch() }) {
|
||||
OutlineButton(onClick = { pickerLauncher.launch() }) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(FeatherIcons.PlusSquare, contentDescription = null)
|
||||
Icon(Iconsax.IconsaxAddSquare, contentDescription = null)
|
||||
Text(stringResource(Res.string.settings_local_media_folders_add_action))
|
||||
}
|
||||
}
|
||||
@ -283,7 +315,7 @@ private fun LocalMediaFoldersDialog(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
IconButton(
|
||||
GhostIconButton(
|
||||
onClick = {
|
||||
if (index in draftFolders.indices) {
|
||||
draftFolders.removeAt(index)
|
||||
@ -291,7 +323,7 @@ private fun LocalMediaFoldersDialog(
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = FeatherIcons.Trash2,
|
||||
imageVector = Iconsax.IconsaxTrash,
|
||||
contentDescription = stringResource(
|
||||
Res.string.settings_local_media_folders_remove_action,
|
||||
),
|
||||
@ -302,22 +334,6 @@ private fun LocalMediaFoldersDialog(
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onFoldersSaved(draftFolders.map(::normalizePath).filter { it.isNotBlank() }.distinct())
|
||||
onDismiss()
|
||||
}
|
||||
) {
|
||||
Text(stringResource(Res.string.settings_action_save))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(Res.string.settings_action_cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -27,6 +27,9 @@ import dev.krtirtho.spotube.modules.settings.SettingsViewModel
|
||||
import dev.krtirtho.spotube.modules.settings.SupportedLanguages
|
||||
import dev.krtirtho.spotube.modules.settings.UserSettings
|
||||
import dev.krtirtho.spotube.modules.settings.components.SelectionSettingCard
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxGlobe
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxLanguageSquare
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
internal fun LazyListScope.languageRegionSection(
|
||||
@ -34,8 +37,9 @@ internal fun LazyListScope.languageRegionSection(
|
||||
settingsViewModel: SettingsViewModel,
|
||||
) {
|
||||
settingsSectionHeader(Res.string.settings_section_language_region)
|
||||
|
||||
item {
|
||||
settingsSectionCard(
|
||||
items = listOf(
|
||||
{
|
||||
SelectionSettingCard(
|
||||
title = stringResource(Res.string.settings_language_title),
|
||||
subtitle = stringResource(
|
||||
@ -44,7 +48,7 @@ internal fun LazyListScope.languageRegionSection(
|
||||
),
|
||||
icon = {
|
||||
SettingsItemIcon(
|
||||
FeatherIcons.Globe,
|
||||
Iconsax.IconsaxLanguageSquare,
|
||||
stringResource(Res.string.settings_language_title)
|
||||
)
|
||||
},
|
||||
@ -60,9 +64,8 @@ internal fun LazyListScope.languageRegionSection(
|
||||
},
|
||||
filter = { item, query -> item.label.contains(query, ignoreCase = true) },
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
},
|
||||
{
|
||||
SelectionSettingCard(
|
||||
title = stringResource(Res.string.settings_country_title),
|
||||
subtitle = stringResource(
|
||||
@ -71,7 +74,7 @@ internal fun LazyListScope.languageRegionSection(
|
||||
),
|
||||
icon = {
|
||||
SettingsItemIcon(
|
||||
FeatherIcons.MapPin,
|
||||
Iconsax.IconsaxGlobe,
|
||||
stringResource(Res.string.settings_country_title)
|
||||
)
|
||||
},
|
||||
@ -87,6 +90,8 @@ internal fun LazyListScope.languageRegionSection(
|
||||
},
|
||||
filter = { item, query -> item.label.contains(query, ignoreCase = true) },
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -30,6 +30,12 @@ import dev.krtirtho.spotube.modules.settings.UserSettings
|
||||
import dev.krtirtho.spotube.modules.settings.components.SelectionSettingCard
|
||||
import dev.krtirtho.spotube.modules.settings.components.SwitchSettingCard
|
||||
import dev.krtirtho.spotube.modules.settings.components.TextInputSettingCard
|
||||
import dev.krtirtho.spotube.resources.iconsax.CustomServer
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMirroringScreen
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicPlay
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxRepeatArrow
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxVerticalSetting
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
internal fun LazyListScope.playbackSection(
|
||||
@ -43,8 +49,9 @@ internal fun LazyListScope.playbackSection(
|
||||
)
|
||||
|
||||
settingsSectionHeader(Res.string.settings_section_playback)
|
||||
|
||||
item {
|
||||
settingsSectionCard(
|
||||
items = listOf(
|
||||
{
|
||||
SelectionSettingCard(
|
||||
title = stringResource(Res.string.settings_streaming_format_title),
|
||||
subtitle = stringResource(
|
||||
@ -52,7 +59,7 @@ internal fun LazyListScope.playbackSection(
|
||||
settings.streamingMusicFormat.displayLabel()
|
||||
),
|
||||
icon = {
|
||||
SettingsItemIcon(FeatherIcons.Radio, stringResource(Res.string.settings_streaming_format_title))
|
||||
SettingsItemIcon(Iconsax.IconsaxMusicPlay, stringResource(Res.string.settings_streaming_format_title))
|
||||
},
|
||||
selectedOption = settings.streamingMusicFormat,
|
||||
options = streamingFormats,
|
||||
@ -66,9 +73,8 @@ internal fun LazyListScope.playbackSection(
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
},
|
||||
{
|
||||
SelectionSettingCard(
|
||||
title = stringResource(Res.string.settings_streaming_quality_title),
|
||||
subtitle = stringResource(
|
||||
@ -76,7 +82,7 @@ internal fun LazyListScope.playbackSection(
|
||||
settings.streamingMusicQuality.displayLabel()
|
||||
),
|
||||
icon = {
|
||||
SettingsItemIcon(FeatherIcons.Sliders, stringResource(Res.string.settings_streaming_quality_title))
|
||||
SettingsItemIcon(Iconsax.IconsaxVerticalSetting, stringResource(Res.string.settings_streaming_quality_title))
|
||||
},
|
||||
selectedOption = settings.streamingMusicQuality,
|
||||
options = streamingQualities,
|
||||
@ -87,15 +93,14 @@ internal fun LazyListScope.playbackSection(
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
},
|
||||
{
|
||||
SwitchSettingCard(
|
||||
title = stringResource(Res.string.settings_enable_endless_playback_title),
|
||||
subtitle = stringResource(Res.string.settings_enable_endless_playback_subtitle),
|
||||
icon = {
|
||||
SettingsItemIcon(
|
||||
FeatherIcons.Repeat,
|
||||
Iconsax.IconsaxRepeatArrow,
|
||||
stringResource(Res.string.settings_enable_endless_playback_title)
|
||||
)
|
||||
},
|
||||
@ -106,14 +111,13 @@ internal fun LazyListScope.playbackSection(
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
},
|
||||
{
|
||||
SwitchSettingCard(
|
||||
title = stringResource(Res.string.settings_enable_connect_title),
|
||||
subtitle = stringResource(Res.string.settings_enable_connect_subtitle),
|
||||
icon = {
|
||||
SettingsItemIcon(FeatherIcons.Cast, stringResource(Res.string.settings_enable_connect_title))
|
||||
SettingsItemIcon(Iconsax.IconsaxMirroringScreen, stringResource(Res.string.settings_enable_connect_title))
|
||||
},
|
||||
checked = settings.enableConnect,
|
||||
onCheckedChange = { enabled ->
|
||||
@ -122,9 +126,8 @@ internal fun LazyListScope.playbackSection(
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
},
|
||||
{
|
||||
val error_whole_number = stringResource(Res.string.settings_error_whole_number)
|
||||
val error_port_range = stringResource(Res.string.settings_error_port_range)
|
||||
|
||||
@ -135,7 +138,7 @@ internal fun LazyListScope.playbackSection(
|
||||
settings.playbackProxyServerPort
|
||||
),
|
||||
icon = {
|
||||
SettingsItemIcon(FeatherIcons.Server, stringResource(Res.string.settings_playback_port_title))
|
||||
SettingsItemIcon(Iconsax.CustomServer, stringResource(Res.string.settings_playback_port_title))
|
||||
},
|
||||
value = settings.playbackProxyServerPort.toString(),
|
||||
dialogDescription = stringResource(Res.string.settings_playback_port_description),
|
||||
@ -155,6 +158,8 @@ internal fun LazyListScope.playbackSection(
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -17,327 +17,38 @@
|
||||
|
||||
package dev.krtirtho.spotube.modules.settings.sections
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
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.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import compose.icons.FeatherIcons
|
||||
import compose.icons.feathericons.Activity
|
||||
import compose.icons.feathericons.AlignLeft
|
||||
import compose.icons.feathericons.Check
|
||||
import compose.icons.feathericons.ChevronRight
|
||||
import compose.icons.feathericons.ExternalLink
|
||||
import compose.icons.feathericons.FileText
|
||||
import compose.icons.feathericons.Music
|
||||
import compose.icons.feathericons.Package
|
||||
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
||||
import dev.krtirtho.spotube.core.navigation.Routes
|
||||
import dev.krtirtho.spotube.core.ui.component.AdaptiveDropdownBottomSheet
|
||||
import dev.krtirtho.spotube.core.ui.component.AdaptiveMenuItem
|
||||
import dev.krtirtho.spotube.core.ui.component.HeaderDisplayMode
|
||||
import spotube.composeapp.generated.resources.*
|
||||
import dev.krtirtho.spotube.modules.plugin.PluginAbility
|
||||
import dev.krtirtho.spotube.modules.plugin.PluginEntry
|
||||
import dev.krtirtho.spotube.modules.plugin.PluginManager
|
||||
import dev.krtirtho.spotube.modules.plugin.PluginManagerStates
|
||||
import dev.krtirtho.spotube.modules.settings.components.SettingCardItem
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxBoxAdd
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@Composable
|
||||
fun DefaultAbilityPluginSelector(
|
||||
ability: PluginAbility,
|
||||
state: StateFlow<List<PluginEntry>>,
|
||||
selectedPlugin: PluginEntry? = null,
|
||||
onSelected: (PluginEntry?) -> Unit = { },
|
||||
onManagePlugins: () -> Unit = { }
|
||||
) {
|
||||
val plugins by state.collectAsStateWithLifecycle()
|
||||
val noPluginsText = stringResource(Res.string.settings_plugins_no_plugins)
|
||||
val clearText = stringResource(Res.string.settings_plugins_clear)
|
||||
val manageText = stringResource(Res.string.settings_plugins_manage_title)
|
||||
|
||||
val menuItems = buildList {
|
||||
if (plugins.isNotEmpty()) {
|
||||
plugins.forEach { plugin ->
|
||||
val isSelected = selectedPlugin?.name == plugin.name
|
||||
add(
|
||||
AdaptiveMenuItem(
|
||||
label = plugin.name,
|
||||
onClick = { onSelected(plugin) },
|
||||
selected = isSelected,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (selectedPlugin != null) {
|
||||
add(
|
||||
AdaptiveMenuItem(
|
||||
icon = FeatherIcons.AlignLeft,
|
||||
label = clearText,
|
||||
onClick = { onSelected(null) },
|
||||
dividerBefore = true,
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
add(
|
||||
AdaptiveMenuItem(
|
||||
label = noPluginsText,
|
||||
onClick = { },
|
||||
enabled = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
add(
|
||||
AdaptiveMenuItem(
|
||||
icon = FeatherIcons.ExternalLink,
|
||||
label = manageText,
|
||||
onClick = onManagePlugins,
|
||||
dividerBefore = true,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||
),
|
||||
border = BorderStroke(
|
||||
width = 1.dp,
|
||||
color = if (selectedPlugin != null) {
|
||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.3f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.outlineVariant
|
||||
}
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.clip(RoundedCornerShape(8.dp)),
|
||||
color = when (ability) {
|
||||
PluginAbility.METADATA -> Color(0xFF4CAF50).copy(alpha = 0.1f)
|
||||
PluginAbility.AUDIO -> Color(0xFF2196F3).copy(alpha = 0.1f)
|
||||
PluginAbility.LYRICS -> Color(0xFFFFC107).copy(alpha = 0.1f)
|
||||
PluginAbility.SCROBBLE -> Color(0xFF9C27B0).copy(alpha = 0.1f)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (ability) {
|
||||
PluginAbility.METADATA -> FeatherIcons.FileText
|
||||
PluginAbility.AUDIO -> FeatherIcons.Music
|
||||
PluginAbility.LYRICS -> FeatherIcons.AlignLeft
|
||||
PluginAbility.SCROBBLE -> FeatherIcons.Activity
|
||||
},
|
||||
contentDescription = stringResource(
|
||||
Res.string.settings_plugins_plugin_content_description,
|
||||
ability.displayLabel()
|
||||
),
|
||||
modifier = Modifier.padding(8.dp),
|
||||
tint = when (ability) {
|
||||
PluginAbility.METADATA -> Color(0xFF4CAF50)
|
||||
PluginAbility.AUDIO -> Color(0xFF2196F3)
|
||||
PluginAbility.LYRICS -> Color(0xFFFFC107)
|
||||
PluginAbility.SCROBBLE -> Color(0xFF9C27B0)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
stringResource(
|
||||
Res.string.settings_plugins_default_ability_title,
|
||||
ability.displayLabel()
|
||||
),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
if (selectedPlugin != null) {
|
||||
Text(
|
||||
selectedPlugin.name,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
stringResource(Res.string.settings_plugins_no_selection),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.Gray,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AdaptiveDropdownBottomSheet(
|
||||
items = menuItems,
|
||||
headerDisplayMode = HeaderDisplayMode.OnlyInBottomSheet,
|
||||
header = {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.clip(RoundedCornerShape(8.dp)),
|
||||
color = when (ability) {
|
||||
PluginAbility.METADATA -> Color(0xFF4CAF50).copy(alpha = 0.1f)
|
||||
PluginAbility.AUDIO -> Color(0xFF2196F3).copy(alpha = 0.1f)
|
||||
PluginAbility.LYRICS -> Color(0xFFFFC107).copy(alpha = 0.1f)
|
||||
PluginAbility.SCROBBLE -> Color(0xFF9C27B0).copy(alpha = 0.1f)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (ability) {
|
||||
PluginAbility.METADATA -> FeatherIcons.FileText
|
||||
PluginAbility.AUDIO -> FeatherIcons.Music
|
||||
PluginAbility.LYRICS -> FeatherIcons.AlignLeft
|
||||
PluginAbility.SCROBBLE -> FeatherIcons.Activity
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(8.dp),
|
||||
tint = when (ability) {
|
||||
PluginAbility.METADATA -> Color(0xFF4CAF50)
|
||||
PluginAbility.AUDIO -> Color(0xFF2196F3)
|
||||
PluginAbility.LYRICS -> Color(0xFFFFC107)
|
||||
PluginAbility.SCROBBLE -> Color(0xFF9C27B0)
|
||||
}
|
||||
)
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
stringResource(
|
||||
Res.string.settings_plugins_default_ability_title,
|
||||
ability.displayLabel()
|
||||
),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
selectedPlugin?.let {
|
||||
Text(
|
||||
it.name,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
trigger = { onClick ->
|
||||
Button(
|
||||
onClick = onClick,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
modifier = Modifier.clip(RoundedCornerShape(8.dp))
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = FeatherIcons.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(0.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
if (selectedPlugin != null) {
|
||||
stringResource(Res.string.settings_plugins_action_change)
|
||||
} else {
|
||||
stringResource(Res.string.settings_plugins_action_select)
|
||||
},
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun LazyListScope.pluginsSection(
|
||||
pluginManager: PluginManager,
|
||||
pluginState: PluginManagerStates,
|
||||
navigatorCommands: NavigationCommands
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
stringResource(Res.string.settings_section_plugins),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = Color.Gray,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
item {
|
||||
settingsSectionHeader(Res.string.settings_section_plugins)
|
||||
settingsSectionCard(
|
||||
items = listOf {
|
||||
SettingCardItem(
|
||||
title = stringResource(Res.string.settings_plugins_manage_title),
|
||||
subtitle = stringResource(Res.string.settings_plugins_manage_subtitle),
|
||||
icon = {
|
||||
Surface(
|
||||
modifier = Modifier.clip(RoundedCornerShape(8.dp)),
|
||||
color = Color(0xFFFF9800).copy(alpha = 0.1f)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = FeatherIcons.Package,
|
||||
contentDescription = stringResource(Res.string.settings_section_plugins),
|
||||
modifier = Modifier.padding(8.dp),
|
||||
tint = Color(0xFFFF9800)
|
||||
SettingsItemIcon(
|
||||
Iconsax.IconsaxBoxAdd,
|
||||
stringResource(Res.string.settings_section_plugins),
|
||||
Color(0xFFFF9800)
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
Icon(
|
||||
@ -351,35 +62,5 @@ internal fun LazyListScope.pluginsSection(
|
||||
}
|
||||
)
|
||||
}
|
||||
items(PluginAbility.entries.size) { index ->
|
||||
val ability = PluginAbility.entries[index]
|
||||
val selectedPlugin =
|
||||
(pluginState as? PluginManagerStates.Data)?.selectedPlugins?.get(ability)
|
||||
DefaultAbilityPluginSelector(
|
||||
ability = ability,
|
||||
selectedPlugin = selectedPlugin,
|
||||
state = when (ability) {
|
||||
PluginAbility.METADATA -> pluginManager.metadataPlugins
|
||||
PluginAbility.AUDIO -> pluginManager.audioPlugins
|
||||
PluginAbility.LYRICS -> pluginManager.lyricsPlugins
|
||||
PluginAbility.SCROBBLE -> pluginManager.scrobblePlugins
|
||||
},
|
||||
onSelected = { plugin ->
|
||||
pluginManager.setSelectedPlugin(ability, plugin)
|
||||
},
|
||||
onManagePlugins = {
|
||||
navigatorCommands.navigateTo(Routes.Plugins)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PluginAbility.displayLabel(): String {
|
||||
return when (this) {
|
||||
PluginAbility.METADATA -> stringResource(Res.string.settings_plugins_ability_metadata)
|
||||
PluginAbility.AUDIO -> stringResource(Res.string.settings_plugins_ability_audio)
|
||||
PluginAbility.LYRICS -> stringResource(Res.string.settings_plugins_ability_lyrics)
|
||||
PluginAbility.SCROBBLE -> stringResource(Res.string.settings_plugins_ability_scrobble)
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,29 +18,30 @@
|
||||
package dev.krtirtho.spotube.modules.settings.sections
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import compose.icons.FeatherIcons
|
||||
import compose.icons.feathericons.RefreshCw
|
||||
import spotube.composeapp.generated.resources.*
|
||||
import dev.krtirtho.spotube.modules.settings.SettingsViewModel
|
||||
import dev.krtirtho.spotube.modules.settings.UserSettings
|
||||
import dev.krtirtho.spotube.modules.settings.components.SwitchSettingCard
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxRefreshArrow2
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import spotube.composeapp.generated.resources.Res
|
||||
import spotube.composeapp.generated.resources.settings_section_updates
|
||||
import spotube.composeapp.generated.resources.settings_updates_auto_check_subtitle
|
||||
import spotube.composeapp.generated.resources.settings_updates_auto_check_title
|
||||
|
||||
internal fun LazyListScope.updatesSection(
|
||||
settings: UserSettings,
|
||||
settingsViewModel: SettingsViewModel,
|
||||
) {
|
||||
settingsSectionHeader(Res.string.settings_section_updates)
|
||||
|
||||
item {
|
||||
settingsSectionCard(
|
||||
items = listOf {
|
||||
SwitchSettingCard(
|
||||
title = stringResource(Res.string.settings_updates_auto_check_title),
|
||||
subtitle = stringResource(Res.string.settings_updates_auto_check_subtitle),
|
||||
icon = {
|
||||
SettingsItemIcon(
|
||||
FeatherIcons.RefreshCw,
|
||||
Iconsax.IconsaxRefreshArrow2,
|
||||
stringResource(Res.string.settings_updates_auto_check_title)
|
||||
)
|
||||
},
|
||||
@ -52,5 +53,6 @@ internal fun LazyListScope.updatesSection(
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -18,10 +18,12 @@
|
||||
package dev.krtirtho.spotube.modules.settings.sections
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
@ -35,6 +37,7 @@ import dev.krtirtho.plugin_interfaces.plugin_apis.audio.AudioFormat
|
||||
import dev.krtirtho.plugin_interfaces.plugin_apis.audio.AudioQuality
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import dev.krtirtho.spotube.core.ui.base.Card
|
||||
|
||||
private val standardLossyQualities = listOf(
|
||||
AudioQuality.Lossy(bitrate = 44_000),
|
||||
@ -172,3 +175,30 @@ internal fun normalizePath(path: String): String {
|
||||
return path.trim().trimEnd('/', '\\')
|
||||
}
|
||||
|
||||
internal fun LazyListScope.settingsSectionCard(
|
||||
items: List<@Composable () -> Unit>,
|
||||
) {
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 4.dp, bottom = 4.dp)
|
||||
) {
|
||||
items.forEachIndexed { index, content ->
|
||||
if (index > 0) {
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f),
|
||||
)
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,7 +91,7 @@ fun AppBottombar(
|
||||
contentDescription = label,
|
||||
tint = if (selected) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(20.dp),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
if (screen == Routes.Library) {
|
||||
DownloadBadgeIndicator(
|
||||
|
||||
@ -25,6 +25,7 @@ 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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@ -33,10 +34,8 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedIconButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -69,6 +68,9 @@ import org.koin.compose.koinInject
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
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
|
||||
//
|
||||
@ -185,15 +187,17 @@ fun AppFloatingPlayer(
|
||||
audioPlayerQueue = audioPlayerQueue,
|
||||
savedTracksViewModel = savedTracksViewModel,
|
||||
)
|
||||
OutlinedIconButton(
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
IconButton(
|
||||
onClick = ::onPlayPause,
|
||||
enabled = playerUiState.queue.isNotEmpty()
|
||||
enabled = playerUiState.queue.isNotEmpty(),
|
||||
theme = LocalBaseUITheme.current.iconButtons.outline.copy(
|
||||
shape = BaseUITheme.InteractionState.fromSingleValue(CircleShape)
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay,
|
||||
contentDescription = if (playerUiState.isPlaying) "Pause" else "Play",
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.inverseOnSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,7 +22,6 @@ import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@ -40,18 +39,22 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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 dev.krtirtho.spotube.core.navigation.NavigationState
|
||||
import dev.krtirtho.spotube.core.navigation.Navigator
|
||||
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.OutlineButton
|
||||
import dev.krtirtho.spotube.core.ui.base.SecondaryButton
|
||||
@ -63,7 +66,10 @@ import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSidebarLeftBroken
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSidebarRightBroken
|
||||
import dev.krtirtho.spotube.tabs
|
||||
import org.jetbrains.compose.resources.Font
|
||||
import org.koin.compose.koinInject
|
||||
import spotube.composeapp.generated.resources.Res
|
||||
import spotube.composeapp.generated.resources.cookie_regular
|
||||
|
||||
@Composable
|
||||
fun AppSidebar(
|
||||
@ -93,19 +99,26 @@ fun AppSidebar(
|
||||
AnimatedVisibility(visible = expanded, enter = fadeIn(), exit = fadeOut()) {
|
||||
Text(
|
||||
text = "Spotube",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
style = MaterialTheme.typography.headlineLarge.copy(
|
||||
fontFamily = FontFamily(
|
||||
listOf(
|
||||
Font(Res.font.cookie_regular, weight = FontWeight.Normal)
|
||||
)
|
||||
)
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
GhostIconButton(
|
||||
onClick = { expanded = !expanded }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (expanded) Iconsax.IconsaxSidebarLeftBroken else Iconsax.IconsaxSidebarRightBroken,
|
||||
contentDescription = if (expanded) "Collapse sidebar" else "Expand sidebar",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.size(22.dp)
|
||||
.clickable { expanded = !expanded }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
@ -189,7 +202,7 @@ fun SidebarItem(
|
||||
},
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(if (expanded) 1f else 0f))
|
||||
Spacer(modifier = if (expanded) Modifier.weight(1f) else Modifier)
|
||||
}
|
||||
|
||||
val buttonModifier = Modifier
|
||||
|
||||
@ -20,6 +20,8 @@ package dev.krtirtho.spotube.modules.shell.player_queue
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.background
|
||||
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.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@ -53,22 +55,28 @@ 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.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.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.IconButton
|
||||
import dev.krtirtho.spotube.core.ui.base.ListRowTile
|
||||
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
||||
import dev.krtirtho.spotube.core.ui.base.SecondaryIconButton
|
||||
import dev.krtirtho.spotube.core.ui.base.TextField
|
||||
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.Iconsax3DotsMore
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxDragHandle
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxFilterSearch
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicSquareRemove
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSetting
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
@ -188,9 +196,9 @@ fun PlayerQueueContent(
|
||||
singleLine = true,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
SecondaryIconButton(
|
||||
IconButton(
|
||||
onClick = viewModel::clearQueue,
|
||||
theme = LocalBaseUITheme.current.iconButtons.secondary.copyShape(MaterialTheme.shapes.small),
|
||||
theme = LocalBaseUITheme.current.iconButtons.outline.copyShape(MaterialTheme.shapes.small),
|
||||
) {
|
||||
Icon(Iconsax.IconsaxTrash, contentDescription = "Clear Queue")
|
||||
}
|
||||
@ -203,19 +211,17 @@ fun PlayerQueueContent(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
} else {
|
||||
Card {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = lazyListState,
|
||||
contentPadding = PaddingValues(bottom = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
items(displayList, key = { item -> item.id }) { item ->
|
||||
ReorderableItem(reorderableLazyListState, key = item.id) { isDragging ->
|
||||
val elevation by animateDpAsState(if (isDragging) 8.dp else 0.dp)
|
||||
QueueItemRow(
|
||||
item = item,
|
||||
isDragging = isDragging,
|
||||
elevation = elevation,
|
||||
reorderScope = if (isFiltered) null else this,
|
||||
onPlayClick = { viewModel.playQueueItem(item.originalIndex) },
|
||||
onRemoveClick = { viewModel.removeQueueItem(item.originalIndex) },
|
||||
@ -227,13 +233,12 @@ fun PlayerQueueContent(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun QueueItemRow(
|
||||
item: QueueItemUi,
|
||||
isDragging: Boolean,
|
||||
elevation: androidx.compose.ui.unit.Dp,
|
||||
reorderScope: sh.calvin.reorderable.ReorderableCollectionItemScope?,
|
||||
onPlayClick: () -> Unit,
|
||||
onRemoveClick: () -> Unit,
|
||||
@ -241,30 +246,18 @@ private fun QueueItemRow(
|
||||
) {
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
|
||||
val rowColor = if (item.isCurrent) {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.clickable(onClick = onPlayClick),
|
||||
shadowElevation = elevation,
|
||||
tonalElevation = if (item.isCurrent) 2.dp else 0.dp,
|
||||
) {
|
||||
ListRowTile(
|
||||
onClick = onPlayClick,
|
||||
selected = item.isCurrent,
|
||||
modifier = Modifier,
|
||||
leading = {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(72.dp)
|
||||
.background(rowColor)
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
.height(72.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
FeatherIcons.MoreVertical,
|
||||
Iconsax.IconsaxDragHandle,
|
||||
contentDescription = if (reorderScope != null) "Reorder" else null,
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
@ -300,10 +293,9 @@ private fun QueueItemRow(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = item.title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
@ -315,6 +307,8 @@ private fun QueueItemRow(
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
},
|
||||
)
|
||||
},
|
||||
subtitle = {
|
||||
Text(
|
||||
text = item.subtitle,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
@ -322,10 +316,8 @@ private fun QueueItemRow(
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
},
|
||||
trailing = {
|
||||
Text(
|
||||
text = item.durationMs.toDurationString(),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
@ -340,7 +332,7 @@ private fun QueueItemRow(
|
||||
modifier = Modifier.size(36.dp),
|
||||
) {
|
||||
Icon(
|
||||
FeatherIcons.MoreVertical,
|
||||
Iconsax.Iconsax3DotsMore,
|
||||
contentDescription = "More options",
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
@ -362,7 +354,7 @@ private fun QueueItemRow(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun QueueEntry.toQueueDisplayData(): Tuple4<String, String, Long, String?> {
|
||||
|
||||
@ -0,0 +1,150 @@
|
||||
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.CustomServer: ImageVector
|
||||
get() {
|
||||
if (_CustomServer != null) {
|
||||
return _CustomServer!!
|
||||
}
|
||||
_CustomServer = ImageVector.Builder(
|
||||
name = "CustomServer",
|
||||
defaultWidth = 24.dp,
|
||||
defaultHeight = 24.dp,
|
||||
viewportWidth = 24f,
|
||||
viewportHeight = 24f
|
||||
).apply {
|
||||
group(
|
||||
clipPathData = PathData {
|
||||
moveTo(0f, 0f)
|
||||
horizontalLineToRelative(24f)
|
||||
verticalLineToRelative(24f)
|
||||
horizontalLineToRelative(-24f)
|
||||
close()
|
||||
}
|
||||
) {
|
||||
// Layer 1: The solid, bright Status Check Badge (bottom right)
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(18.5f, 11f)
|
||||
curveTo(14.91f, 11f, 12f, 13.91f, 12f, 17.5f)
|
||||
curveTo(12f, 21.09f, 14.91f, 24f, 18.5f, 24f)
|
||||
curveTo(22.09f, 24f, 25f, 21.09f, 25f, 17.5f)
|
||||
curveTo(25f, 13.91f, 22.09f, 11f, 18.5f, 11f)
|
||||
close()
|
||||
// The cutout shape of the checkmark
|
||||
moveTo(17.2f, 20.3f)
|
||||
lineTo(14.7f, 17.8f)
|
||||
curveTo(14.31f, 17.41f, 14.31f, 16.78f, 14.7f, 16.39f)
|
||||
curveTo(15.09f, 16f, 15.72f, 16f, 16.11f, 16.39f)
|
||||
lineTo(17.9f, 18.18f)
|
||||
lineTo(21.39f, 14.69f)
|
||||
curveTo(21.78f, 14.3f, 22.41f, 14.3f, 22.8f, 14.69f)
|
||||
curveTo(23.19f, 15.08f, 23.19f, 15.71f, 22.8f, 16.1f)
|
||||
lineTo(18.6f, 20.3f)
|
||||
curveTo(18.21f, 20.69f, 17.59f, 20.69f, 17.2f, 20.3f)
|
||||
close()
|
||||
}
|
||||
|
||||
// Layer 2: Main Server Rack Chassis (Top & Bottom shells with 40% opacity)
|
||||
path(
|
||||
fill = SolidColor(Color.White),
|
||||
fillAlpha = 0.4f,
|
||||
strokeAlpha = 0.4f
|
||||
) {
|
||||
// Outer border of the double-slot server rack structure
|
||||
moveTo(20.5f, 8.5f)
|
||||
verticalLineTo(7f)
|
||||
curveTo(20.5f, 3.5f, 18f, 1.5f, 13f, 1.5f)
|
||||
horizontalLineTo(7f)
|
||||
curveTo(3f, 1.5f, 1.5f, 3.5f, 1.5f, 7f)
|
||||
verticalLineTo(9.5f)
|
||||
curveTo(1.5f, 11.5f, 3f, 12f, 4.5f, 12f)
|
||||
curveTo(3f, 12f, 1.5f, 12.5f, 1.5f, 14.5f)
|
||||
verticalLineTo(17f)
|
||||
curveTo(1.5f, 20.5f, 3f, 22.5f, 7f, 22.5f)
|
||||
horizontalLineTo(10f)
|
||||
curveTo(10.55f, 22.5f, 11f, 22.05f, 11f, 21.5f)
|
||||
curveTo(11f, 20.95f, 10.55f, 20.5f, 10f, 20.5f)
|
||||
horizontalLineTo(7f)
|
||||
curveTo(4.5f, 20.5f, 3.5f, 19.5f, 3.5f, 17f)
|
||||
verticalLineTo(14.5f)
|
||||
curveTo(3.5f, 13f, 4.5f, 13f, 6f, 13f)
|
||||
curveTo(6.55f, 13f, 7f, 12.55f, 7f, 12f)
|
||||
curveTo(7f, 11.45f, 6.55f, 11f, 6f, 11f)
|
||||
curveTo(4.5f, 11f, 3.5f, 11f, 3.5f, 9.5f)
|
||||
verticalLineTo(7f)
|
||||
curveTo(3.5f, 4.5f, 4.5f, 3.5f, 7f, 3.5f)
|
||||
horizontalLineTo(13f)
|
||||
curveTo(16.5f, 3.5f, 18.5f, 4.5f, 18.5f, 7f)
|
||||
verticalLineTo(8.5f)
|
||||
curveTo(18.5f, 9.05f, 18.95f, 9.5f, 19.5f, 9.5f)
|
||||
curveTo(20.05f, 9.5f, 20.5f, 9.05f, 20.5f, 8.5f)
|
||||
close()
|
||||
}
|
||||
|
||||
// Layer 3: Server Front Panel Slots, Buttons, and LEDs (100% white)
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
// Top Slot LED 1
|
||||
moveTo(5.5f, 5.5f)
|
||||
curveTo(6.05f, 5.5f, 6.5f, 5.95f, 6.5f, 6.5f)
|
||||
verticalLineTo(7.5f)
|
||||
curveTo(6.5f, 8.05f, 6.05f, 8.5f, 5.5f, 8.5f)
|
||||
curveTo(4.95f, 8.5f, 4.5f, 8.05f, 4.5f, 7.5f)
|
||||
verticalLineTo(6.5f)
|
||||
curveTo(4.5f, 5.95f, 4.95f, 5.5f, 5.5f, 5.5f)
|
||||
close()
|
||||
|
||||
// Top Slot LED 2
|
||||
moveTo(8.5f, 5.5f)
|
||||
curveTo(9.05f, 5.5f, 9.5f, 5.95f, 9.5f, 6.5f)
|
||||
verticalLineTo(7.5f)
|
||||
curveTo(9.5f, 8.05f, 9.05f, 8.5f, 8.5f, 8.5f)
|
||||
curveTo(7.95f, 8.5f, 7.5f, 8.05f, 7.5f, 7.5f)
|
||||
verticalLineTo(6.5f)
|
||||
curveTo(7.5f, 5.95f, 7.95f, 5.5f, 8.5f, 5.5f)
|
||||
close()
|
||||
|
||||
// Top Slot Button Strip
|
||||
moveTo(11.5f, 6f)
|
||||
horizontalLineTo(16.5f)
|
||||
curveTo(17.05f, 6f, 17.5f, 6.45f, 17.5f, 7f)
|
||||
curveTo(17.5f, 7.55f, 17.05f, 8f, 16.5f, 8f)
|
||||
horizontalLineTo(11.5f)
|
||||
curveTo(10.95f, 8f, 10.5f, 7.55f, 10.5f, 7f)
|
||||
curveTo(10.5f, 6.45f, 10.95f, 6f, 11.5f, 6f)
|
||||
close()
|
||||
|
||||
// Bottom Slot LED 1
|
||||
moveTo(5.5f, 14.5f)
|
||||
curveTo(6.05f, 14.5f, 6.5f, 14.95f, 6.5f, 15.5f)
|
||||
verticalLineTo(16.5f)
|
||||
curveTo(6.5f, 17.05f, 6.05f, 17.5f, 5.5f, 17.5f)
|
||||
curveTo(4.95f, 17.5f, 4.5f, 17.05f, 4.5f, 16.5f)
|
||||
verticalLineTo(15.5f)
|
||||
curveTo(4.5f, 14.95f, 4.95f, 14.5f, 5.5f, 14.5f)
|
||||
close()
|
||||
|
||||
// Bottom Slot LED 2
|
||||
moveTo(8.5f, 14.5f)
|
||||
curveTo(9.05f, 14.5f, 9.5f, 14.95f, 9.5f, 15.5f)
|
||||
verticalLineTo(16.5f)
|
||||
curveTo(9.5f, 17.05f, 9.05f, 17.5f, 8.5f, 17.5f)
|
||||
curveTo(7.95f, 17.5f, 7.5f, 17.05f, 7.5f, 16.5f)
|
||||
verticalLineTo(15.5f)
|
||||
curveTo(7.5f, 14.95f, 7.95f, 14.5f, 8.5f, 14.5f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
|
||||
return _CustomServer!!
|
||||
}
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
private var _CustomServer: ImageVector? = null
|
||||
@ -1,20 +1,3 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
object Iconsax
|
||||
|
||||
@ -0,0 +1,120 @@
|
||||
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.IconsaxBox: ImageVector
|
||||
get() {
|
||||
if (_IconsaxBox != null) {
|
||||
return _IconsaxBox!!
|
||||
}
|
||||
_IconsaxBox = ImageVector.Builder(
|
||||
name = "IconsaxBox",
|
||||
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(20.1f, 6.94f)
|
||||
curveTo(20.1f, 7.48f, 19.81f, 7.97f, 19.35f, 8.22f)
|
||||
lineTo(17.61f, 9.16f)
|
||||
lineTo(16.13f, 9.95f)
|
||||
lineTo(13.06f, 11.61f)
|
||||
curveTo(12.73f, 11.79f, 12.37f, 11.88f, 12f, 11.88f)
|
||||
curveTo(11.63f, 11.88f, 11.27f, 11.79f, 10.94f, 11.61f)
|
||||
lineTo(4.65f, 8.22f)
|
||||
curveTo(4.19f, 7.97f, 3.9f, 7.48f, 3.9f, 6.94f)
|
||||
curveTo(3.9f, 6.4f, 4.19f, 5.91f, 4.65f, 5.66f)
|
||||
lineTo(6.62f, 4.6f)
|
||||
lineTo(8.19f, 3.75f)
|
||||
lineTo(10.94f, 2.27f)
|
||||
curveTo(11.6f, 1.91f, 12.4f, 1.91f, 13.06f, 2.27f)
|
||||
lineTo(19.35f, 5.66f)
|
||||
curveTo(19.81f, 5.91f, 20.1f, 6.4f, 20.1f, 6.94f)
|
||||
close()
|
||||
}
|
||||
path(
|
||||
fill = SolidColor(Color.White),
|
||||
fillAlpha = 0.4f,
|
||||
strokeAlpha = 0.4f
|
||||
) {
|
||||
moveTo(9.9f, 12.79f)
|
||||
lineTo(4.05f, 9.86f)
|
||||
curveTo(3.6f, 9.63f, 3.08f, 9.66f, 2.65f, 9.92f)
|
||||
curveTo(2.22f, 10.18f, 1.97f, 10.64f, 1.97f, 11.14f)
|
||||
verticalLineTo(16.67f)
|
||||
curveTo(1.97f, 17.63f, 2.5f, 18.49f, 3.36f, 18.92f)
|
||||
lineTo(9.21f, 21.84f)
|
||||
curveTo(9.41f, 21.94f, 9.63f, 21.99f, 9.85f, 21.99f)
|
||||
curveTo(10.11f, 21.99f, 10.37f, 21.92f, 10.6f, 21.77f)
|
||||
curveTo(11.03f, 21.51f, 11.28f, 21.05f, 11.28f, 20.55f)
|
||||
verticalLineTo(15.02f)
|
||||
curveTo(11.29f, 14.08f, 10.76f, 13.22f, 9.9f, 12.79f)
|
||||
close()
|
||||
}
|
||||
path(
|
||||
fill = SolidColor(Color.White),
|
||||
fillAlpha = 0.4f,
|
||||
strokeAlpha = 0.4f
|
||||
) {
|
||||
moveTo(22.03f, 11.15f)
|
||||
verticalLineTo(16.68f)
|
||||
curveTo(22.03f, 17.63f, 21.5f, 18.49f, 20.64f, 18.92f)
|
||||
lineTo(14.79f, 21.85f)
|
||||
curveTo(14.59f, 21.95f, 14.37f, 22f, 14.15f, 22f)
|
||||
curveTo(13.89f, 22f, 13.63f, 21.93f, 13.39f, 21.78f)
|
||||
curveTo(12.97f, 21.52f, 12.71f, 21.06f, 12.71f, 20.56f)
|
||||
verticalLineTo(15.04f)
|
||||
curveTo(12.71f, 14.08f, 13.24f, 13.22f, 14.1f, 12.79f)
|
||||
lineTo(16.25f, 11.72f)
|
||||
lineTo(17.75f, 10.97f)
|
||||
lineTo(19.95f, 9.87f)
|
||||
curveTo(20.4f, 9.64f, 20.92f, 9.66f, 21.35f, 9.93f)
|
||||
curveTo(21.77f, 10.19f, 22.03f, 10.65f, 22.03f, 11.15f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(17.61f, 9.16f)
|
||||
lineTo(16.13f, 9.95f)
|
||||
lineTo(6.62f, 4.6f)
|
||||
lineTo(8.19f, 3.75f)
|
||||
lineTo(17.37f, 8.93f)
|
||||
curveTo(17.47f, 8.99f, 17.55f, 9.07f, 17.61f, 9.16f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(17.75f, 10.971f)
|
||||
verticalLineTo(13.241f)
|
||||
curveTo(17.75f, 13.651f, 17.41f, 13.991f, 17f, 13.991f)
|
||||
curveTo(16.59f, 13.991f, 16.25f, 13.651f, 16.25f, 13.241f)
|
||||
verticalLineTo(11.721f)
|
||||
lineTo(17.75f, 10.971f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
|
||||
return _IconsaxBox!!
|
||||
}
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
private var _IconsaxBox: ImageVector? = null
|
||||
@ -0,0 +1,109 @@
|
||||
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.IconsaxColorSwatch: ImageVector
|
||||
get() {
|
||||
if (_IconsaxColorSwatch != null) {
|
||||
return _IconsaxColorSwatch!!
|
||||
}
|
||||
_IconsaxColorSwatch = ImageVector.Builder(
|
||||
name = "IconsaxColorSwatch",
|
||||
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(22f, 16.5f)
|
||||
verticalLineTo(19.5f)
|
||||
curveTo(22f, 21f, 21f, 22f, 19.5f, 22f)
|
||||
horizontalLineTo(6f)
|
||||
curveTo(6.41f, 22f, 6.83f, 21.94f, 7.22f, 21.81f)
|
||||
curveTo(7.33f, 21.77f, 7.44f, 21.73f, 7.55f, 21.68f)
|
||||
curveTo(7.9f, 21.54f, 8.24f, 21.34f, 8.54f, 21.08f)
|
||||
curveTo(8.63f, 21.01f, 8.73f, 20.92f, 8.82f, 20.83f)
|
||||
lineTo(8.86f, 20.79f)
|
||||
lineTo(15.66f, 14f)
|
||||
horizontalLineTo(19.5f)
|
||||
curveTo(21f, 14f, 22f, 15f, 22f, 16.5f)
|
||||
close()
|
||||
}
|
||||
path(
|
||||
fill = SolidColor(Color.White),
|
||||
fillAlpha = 0.6f,
|
||||
strokeAlpha = 0.6f
|
||||
) {
|
||||
moveTo(18.37f, 11.291f)
|
||||
lineTo(15.66f, 14.001f)
|
||||
lineTo(8.86f, 20.791f)
|
||||
curveTo(9.56f, 20.071f, 10f, 19.081f, 10f, 18.001f)
|
||||
verticalLineTo(8.341f)
|
||||
lineTo(12.71f, 5.631f)
|
||||
curveTo(13.77f, 4.571f, 15.19f, 4.571f, 16.25f, 5.631f)
|
||||
lineTo(18.37f, 7.751f)
|
||||
curveTo(19.43f, 8.811f, 19.43f, 10.231f, 18.37f, 11.291f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(7.5f, 2f)
|
||||
horizontalLineTo(4.5f)
|
||||
curveTo(3f, 2f, 2f, 3f, 2f, 4.5f)
|
||||
verticalLineTo(18f)
|
||||
curveTo(2f, 18.27f, 2.03f, 18.54f, 2.08f, 18.8f)
|
||||
curveTo(2.11f, 18.93f, 2.14f, 19.06f, 2.18f, 19.19f)
|
||||
curveTo(2.23f, 19.34f, 2.28f, 19.49f, 2.34f, 19.63f)
|
||||
curveTo(2.35f, 19.64f, 2.35f, 19.65f, 2.35f, 19.65f)
|
||||
curveTo(2.36f, 19.65f, 2.36f, 19.65f, 2.35f, 19.66f)
|
||||
curveTo(2.49f, 19.94f, 2.65f, 20.21f, 2.84f, 20.46f)
|
||||
curveTo(2.95f, 20.59f, 3.06f, 20.71f, 3.17f, 20.83f)
|
||||
curveTo(3.28f, 20.95f, 3.4f, 21.05f, 3.53f, 21.15f)
|
||||
lineTo(3.54f, 21.16f)
|
||||
curveTo(3.79f, 21.35f, 4.06f, 21.51f, 4.34f, 21.65f)
|
||||
curveTo(4.35f, 21.64f, 4.35f, 21.64f, 4.35f, 21.65f)
|
||||
curveTo(4.5f, 21.72f, 4.65f, 21.77f, 4.81f, 21.82f)
|
||||
curveTo(4.94f, 21.86f, 5.07f, 21.89f, 5.2f, 21.92f)
|
||||
curveTo(5.46f, 21.97f, 5.73f, 22f, 6f, 22f)
|
||||
curveTo(6.41f, 22f, 6.83f, 21.94f, 7.22f, 21.81f)
|
||||
curveTo(7.33f, 21.77f, 7.44f, 21.73f, 7.55f, 21.68f)
|
||||
curveTo(7.9f, 21.54f, 8.24f, 21.34f, 8.54f, 21.08f)
|
||||
curveTo(8.63f, 21.01f, 8.73f, 20.92f, 8.82f, 20.83f)
|
||||
lineTo(8.86f, 20.79f)
|
||||
curveTo(9.56f, 20.07f, 10f, 19.08f, 10f, 18f)
|
||||
verticalLineTo(4.5f)
|
||||
curveTo(10f, 3f, 9f, 2f, 7.5f, 2f)
|
||||
close()
|
||||
moveTo(6f, 19.5f)
|
||||
curveTo(5.17f, 19.5f, 4.5f, 18.83f, 4.5f, 18f)
|
||||
curveTo(4.5f, 17.17f, 5.17f, 16.5f, 6f, 16.5f)
|
||||
curveTo(6.83f, 16.5f, 7.5f, 17.17f, 7.5f, 18f)
|
||||
curveTo(7.5f, 18.83f, 6.83f, 19.5f, 6f, 19.5f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
|
||||
return _IconsaxColorSwatch!!
|
||||
}
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
private var _IconsaxColorSwatch: ImageVector? = null
|
||||
@ -1,20 +1,3 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
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.IconsaxDocumentDownload: ImageVector
|
||||
get() {
|
||||
if (_IconsaxDocumentDownload != null) {
|
||||
return _IconsaxDocumentDownload!!
|
||||
}
|
||||
_IconsaxDocumentDownload = ImageVector.Builder(
|
||||
name = "IconsaxDocumentDownload",
|
||||
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(20.5f, 10.19f)
|
||||
horizontalLineTo(17.61f)
|
||||
curveTo(15.24f, 10.19f, 13.31f, 8.26f, 13.31f, 5.89f)
|
||||
verticalLineTo(3f)
|
||||
curveTo(13.31f, 2.45f, 12.86f, 2f, 12.31f, 2f)
|
||||
horizontalLineTo(8.07f)
|
||||
curveTo(4.99f, 2f, 2.5f, 4f, 2.5f, 7.57f)
|
||||
verticalLineTo(16.43f)
|
||||
curveTo(2.5f, 20f, 4.99f, 22f, 8.07f, 22f)
|
||||
horizontalLineTo(15.93f)
|
||||
curveTo(19.01f, 22f, 21.5f, 20f, 21.5f, 16.43f)
|
||||
verticalLineTo(11.19f)
|
||||
curveTo(21.5f, 10.64f, 21.05f, 10.19f, 20.5f, 10.19f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(15.8f, 2.21f)
|
||||
curveTo(15.39f, 1.8f, 14.68f, 2.08f, 14.68f, 2.65f)
|
||||
verticalLineTo(6.14f)
|
||||
curveTo(14.68f, 7.6f, 15.92f, 8.81f, 17.43f, 8.81f)
|
||||
curveTo(18.38f, 8.82f, 19.7f, 8.82f, 20.83f, 8.82f)
|
||||
curveTo(21.4f, 8.82f, 21.7f, 8.15f, 21.3f, 7.75f)
|
||||
curveTo(19.86f, 6.3f, 17.28f, 3.69f, 15.8f, 2.21f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(12.28f, 14.72f)
|
||||
curveTo(11.99f, 14.43f, 11.51f, 14.43f, 11.22f, 14.72f)
|
||||
lineTo(10.5f, 15.44f)
|
||||
verticalLineTo(11.25f)
|
||||
curveTo(10.5f, 10.84f, 10.16f, 10.5f, 9.75f, 10.5f)
|
||||
curveTo(9.34f, 10.5f, 9f, 10.84f, 9f, 11.25f)
|
||||
verticalLineTo(15.44f)
|
||||
lineTo(8.28f, 14.72f)
|
||||
curveTo(7.99f, 14.43f, 7.51f, 14.43f, 7.22f, 14.72f)
|
||||
curveTo(6.93f, 15.01f, 6.93f, 15.49f, 7.22f, 15.78f)
|
||||
lineTo(9.22f, 17.78f)
|
||||
curveTo(9.23f, 17.79f, 9.24f, 17.79f, 9.24f, 17.8f)
|
||||
curveTo(9.3f, 17.86f, 9.38f, 17.91f, 9.46f, 17.95f)
|
||||
curveTo(9.56f, 17.98f, 9.65f, 18f, 9.75f, 18f)
|
||||
curveTo(9.85f, 18f, 9.94f, 17.98f, 10.03f, 17.94f)
|
||||
curveTo(10.12f, 17.9f, 10.2f, 17.85f, 10.28f, 17.78f)
|
||||
lineTo(12.28f, 15.78f)
|
||||
curveTo(12.57f, 15.49f, 12.57f, 15.01f, 12.28f, 14.72f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
|
||||
return _IconsaxDocumentDownload!!
|
||||
}
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
private var _IconsaxDocumentDownload: ImageVector? = null
|
||||
@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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
|
||||
@ -0,0 +1,92 @@
|
||||
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
|
||||
@ -0,0 +1,61 @@
|
||||
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.path
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val Iconsax.IconsaxExportArrowBulk: ImageVector
|
||||
get() {
|
||||
if (_IconsaxExportArrowBulk != null) {
|
||||
return _IconsaxExportArrowBulk!!
|
||||
}
|
||||
_IconsaxExportArrowBulk = ImageVector.Builder(
|
||||
name = "IconsaxExportArrowBulk",
|
||||
defaultWidth = 24.dp,
|
||||
defaultHeight = 24.dp,
|
||||
viewportWidth = 24f,
|
||||
viewportHeight = 24f
|
||||
).apply {
|
||||
path(
|
||||
fill = SolidColor(Color.White),
|
||||
fillAlpha = 0.4f,
|
||||
strokeAlpha = 0.4f
|
||||
) {
|
||||
moveTo(16.8f, 9f)
|
||||
horizontalLineTo(7.2f)
|
||||
curveTo(4f, 9f, 2f, 11f, 2f, 14.2f)
|
||||
verticalLineTo(16.79f)
|
||||
curveTo(2f, 20f, 4f, 22f, 7.2f, 22f)
|
||||
horizontalLineTo(16.79f)
|
||||
curveTo(19.99f, 22f, 21.99f, 20f, 21.99f, 16.8f)
|
||||
verticalLineTo(14.2f)
|
||||
curveTo(22f, 11f, 20f, 9f, 16.8f, 9f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(15.88f, 5.57f)
|
||||
lineTo(12.53f, 2.22f)
|
||||
curveTo(12.24f, 1.93f, 11.76f, 1.93f, 11.47f, 2.22f)
|
||||
lineTo(8.12f, 5.57f)
|
||||
curveTo(7.83f, 5.86f, 7.83f, 6.34f, 8.12f, 6.63f)
|
||||
curveTo(8.41f, 6.92f, 8.89f, 6.92f, 9.18f, 6.63f)
|
||||
lineTo(11.25f, 4.56f)
|
||||
verticalLineTo(15.25f)
|
||||
curveTo(11.25f, 15.66f, 11.59f, 16f, 12f, 16f)
|
||||
curveTo(12.41f, 16f, 12.75f, 15.66f, 12.75f, 15.25f)
|
||||
verticalLineTo(4.56f)
|
||||
lineTo(14.82f, 6.63f)
|
||||
curveTo(14.97f, 6.78f, 15.16f, 6.85f, 15.35f, 6.85f)
|
||||
curveTo(15.54f, 6.85f, 15.73f, 6.78f, 15.88f, 6.63f)
|
||||
curveTo(16.18f, 6.34f, 16.18f, 5.87f, 15.88f, 5.57f)
|
||||
close()
|
||||
}
|
||||
}.build()
|
||||
|
||||
return _IconsaxExportArrowBulk!!
|
||||
}
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
private var _IconsaxExportArrowBulk: ImageVector? = null
|
||||
@ -0,0 +1,69 @@
|
||||
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.IconsaxEye: ImageVector
|
||||
get() {
|
||||
if (_IconsaxEye != null) {
|
||||
return _IconsaxEye!!
|
||||
}
|
||||
_IconsaxEye = ImageVector.Builder(
|
||||
name = "IconsaxEye",
|
||||
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(21.25f, 9.15f)
|
||||
curveTo(18.94f, 5.52f, 15.56f, 3.43f, 12f, 3.43f)
|
||||
curveTo(10.22f, 3.43f, 8.49f, 3.95f, 6.91f, 4.92f)
|
||||
curveTo(5.33f, 5.9f, 3.91f, 7.33f, 2.75f, 9.15f)
|
||||
curveTo(1.75f, 10.72f, 1.75f, 13.27f, 2.75f, 14.84f)
|
||||
curveTo(5.06f, 18.48f, 8.44f, 20.56f, 12f, 20.56f)
|
||||
curveTo(13.78f, 20.56f, 15.51f, 20.04f, 17.09f, 19.07f)
|
||||
curveTo(18.67f, 18.09f, 20.09f, 16.66f, 21.25f, 14.84f)
|
||||
curveTo(22.25f, 13.28f, 22.25f, 10.72f, 21.25f, 9.15f)
|
||||
close()
|
||||
moveTo(12f, 16.04f)
|
||||
curveTo(9.76f, 16.04f, 7.96f, 14.23f, 7.96f, 12f)
|
||||
curveTo(7.96f, 9.77f, 9.76f, 7.96f, 12f, 7.96f)
|
||||
curveTo(14.24f, 7.96f, 16.04f, 9.77f, 16.04f, 12f)
|
||||
curveTo(16.04f, 14.23f, 14.24f, 16.04f, 12f, 16.04f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(12f, 9.141f)
|
||||
curveTo(10.43f, 9.141f, 9.15f, 10.421f, 9.15f, 12.001f)
|
||||
curveTo(9.15f, 13.571f, 10.43f, 14.851f, 12f, 14.851f)
|
||||
curveTo(13.57f, 14.851f, 14.86f, 13.571f, 14.86f, 12.001f)
|
||||
curveTo(14.86f, 10.431f, 13.57f, 9.141f, 12f, 9.141f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
|
||||
return _IconsaxEye!!
|
||||
}
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
private var _IconsaxEye: ImageVector? = null
|
||||
@ -0,0 +1,68 @@
|
||||
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.IconsaxFolder: ImageVector
|
||||
get() {
|
||||
if (_IconsaxFolder != null) {
|
||||
return _IconsaxFolder!!
|
||||
}
|
||||
_IconsaxFolder = ImageVector.Builder(
|
||||
name = "IconsaxFolder",
|
||||
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)) {
|
||||
moveTo(22f, 11.07f)
|
||||
verticalLineTo(16.65f)
|
||||
curveTo(22f, 19.6f, 19.6f, 22f, 16.65f, 22f)
|
||||
horizontalLineTo(7.35f)
|
||||
curveTo(4.4f, 22f, 2f, 19.6f, 2f, 16.65f)
|
||||
verticalLineTo(9.44f)
|
||||
horizontalLineTo(21.74f)
|
||||
curveTo(21.89f, 9.89f, 21.97f, 10.35f, 21.99f, 10.84f)
|
||||
curveTo(22f, 10.91f, 22f, 11f, 22f, 11.07f)
|
||||
close()
|
||||
}
|
||||
path(
|
||||
fill = SolidColor(Color.White),
|
||||
fillAlpha = 0.4f,
|
||||
strokeAlpha = 0.4f
|
||||
) {
|
||||
moveTo(21.74f, 9.44f)
|
||||
horizontalLineTo(2f)
|
||||
verticalLineTo(6.42f)
|
||||
curveTo(2f, 3.98f, 3.98f, 2f, 6.42f, 2f)
|
||||
horizontalLineTo(8.75f)
|
||||
curveTo(10.38f, 2f, 10.89f, 2.53f, 11.54f, 3.4f)
|
||||
lineTo(12.94f, 5.26f)
|
||||
curveTo(13.25f, 5.67f, 13.29f, 5.73f, 13.87f, 5.73f)
|
||||
horizontalLineTo(16.66f)
|
||||
curveTo(19.03f, 5.72f, 21.05f, 7.28f, 21.74f, 9.44f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
|
||||
return _IconsaxFolder!!
|
||||
}
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
private var _IconsaxFolder: ImageVector? = null
|
||||
@ -0,0 +1,86 @@
|
||||
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.IconsaxFolderAdd: ImageVector
|
||||
get() {
|
||||
if (_IconsaxFolderAdd != null) {
|
||||
return _IconsaxFolderAdd!!
|
||||
}
|
||||
_IconsaxFolderAdd = ImageVector.Builder(
|
||||
name = "IconsaxFolderAdd",
|
||||
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(21.74f, 9.44f)
|
||||
horizontalLineTo(2f)
|
||||
verticalLineTo(6.42f)
|
||||
curveTo(2f, 3.98f, 3.98f, 2f, 6.42f, 2f)
|
||||
horizontalLineTo(8.75f)
|
||||
curveTo(10.38f, 2f, 10.89f, 2.53f, 11.54f, 3.4f)
|
||||
lineTo(12.94f, 5.26f)
|
||||
curveTo(13.25f, 5.67f, 13.29f, 5.73f, 13.87f, 5.73f)
|
||||
horizontalLineTo(16.66f)
|
||||
curveTo(19.03f, 5.72f, 21.05f, 7.28f, 21.74f, 9.44f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(21.99f, 10.84f)
|
||||
curveTo(21.97f, 10.35f, 21.89f, 9.89f, 21.74f, 9.44f)
|
||||
horizontalLineTo(2f)
|
||||
verticalLineTo(16.65f)
|
||||
curveTo(2f, 19.6f, 4.4f, 22f, 7.35f, 22f)
|
||||
horizontalLineTo(16.65f)
|
||||
curveTo(19.6f, 22f, 22f, 19.6f, 22f, 16.65f)
|
||||
verticalLineTo(11.07f)
|
||||
curveTo(22f, 11f, 22f, 10.91f, 21.99f, 10.84f)
|
||||
close()
|
||||
moveTo(14.5f, 16.25f)
|
||||
horizontalLineTo(12.81f)
|
||||
verticalLineTo(18f)
|
||||
curveTo(12.81f, 18.41f, 12.47f, 18.75f, 12.06f, 18.75f)
|
||||
curveTo(11.65f, 18.75f, 11.31f, 18.41f, 11.31f, 18f)
|
||||
verticalLineTo(16.25f)
|
||||
horizontalLineTo(9.5f)
|
||||
curveTo(9.09f, 16.25f, 8.75f, 15.91f, 8.75f, 15.5f)
|
||||
curveTo(8.75f, 15.09f, 9.09f, 14.75f, 9.5f, 14.75f)
|
||||
horizontalLineTo(11.31f)
|
||||
verticalLineTo(13f)
|
||||
curveTo(11.31f, 12.59f, 11.65f, 12.25f, 12.06f, 12.25f)
|
||||
curveTo(12.47f, 12.25f, 12.81f, 12.59f, 12.81f, 13f)
|
||||
verticalLineTo(14.75f)
|
||||
horizontalLineTo(14.5f)
|
||||
curveTo(14.91f, 14.75f, 15.25f, 15.09f, 15.25f, 15.5f)
|
||||
curveTo(15.25f, 15.91f, 14.91f, 16.25f, 14.5f, 16.25f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
|
||||
return _IconsaxFolderAdd!!
|
||||
}
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
private var _IconsaxFolderAdd: ImageVector? = null
|
||||
@ -0,0 +1,140 @@
|
||||
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.IconsaxGlobe: ImageVector
|
||||
get() {
|
||||
if (_IconsaxGlobe != null) {
|
||||
return _IconsaxGlobe!!
|
||||
}
|
||||
_IconsaxGlobe = ImageVector.Builder(
|
||||
name = "IconsaxGlobe",
|
||||
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(7.65f, 20.91f)
|
||||
curveTo(7.62f, 20.91f, 7.58f, 20.93f, 7.55f, 20.93f)
|
||||
curveTo(5.61f, 19.97f, 4.03f, 18.38f, 3.06f, 16.44f)
|
||||
curveTo(3.06f, 16.41f, 3.08f, 16.37f, 3.08f, 16.34f)
|
||||
curveTo(4.3f, 16.7f, 5.56f, 16.97f, 6.81f, 17.18f)
|
||||
curveTo(7.03f, 18.44f, 7.29f, 19.69f, 7.65f, 20.91f)
|
||||
close()
|
||||
}
|
||||
path(
|
||||
fill = SolidColor(Color.White),
|
||||
fillAlpha = 0.4f,
|
||||
strokeAlpha = 0.4f
|
||||
) {
|
||||
moveTo(20.94f, 16.45f)
|
||||
curveTo(19.95f, 18.44f, 18.3f, 20.05f, 16.29f, 21.02f)
|
||||
curveTo(16.67f, 19.75f, 16.99f, 18.47f, 17.2f, 17.18f)
|
||||
curveTo(18.46f, 16.97f, 19.7f, 16.7f, 20.92f, 16.34f)
|
||||
curveTo(20.91f, 16.38f, 20.94f, 16.42f, 20.94f, 16.45f)
|
||||
close()
|
||||
}
|
||||
path(
|
||||
fill = SolidColor(Color.White),
|
||||
fillAlpha = 0.4f,
|
||||
strokeAlpha = 0.4f
|
||||
) {
|
||||
moveTo(21.02f, 7.71f)
|
||||
curveTo(19.76f, 7.33f, 18.49f, 7.02f, 17.2f, 6.8f)
|
||||
curveTo(16.99f, 5.51f, 16.68f, 4.23f, 16.29f, 2.98f)
|
||||
curveTo(18.36f, 3.97f, 20.03f, 5.64f, 21.02f, 7.71f)
|
||||
close()
|
||||
}
|
||||
path(
|
||||
fill = SolidColor(Color.White),
|
||||
fillAlpha = 0.4f,
|
||||
strokeAlpha = 0.4f
|
||||
) {
|
||||
moveTo(7.65f, 3.091f)
|
||||
curveTo(7.29f, 4.311f, 7.03f, 5.551f, 6.82f, 6.811f)
|
||||
curveTo(5.53f, 7.011f, 4.25f, 7.331f, 2.98f, 7.711f)
|
||||
curveTo(3.95f, 5.701f, 5.56f, 4.051f, 7.55f, 3.061f)
|
||||
curveTo(7.58f, 3.061f, 7.62f, 3.091f, 7.65f, 3.091f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(15.49f, 6.59f)
|
||||
curveTo(13.17f, 6.33f, 10.83f, 6.33f, 8.51f, 6.59f)
|
||||
curveTo(8.76f, 5.22f, 9.08f, 3.85f, 9.53f, 2.53f)
|
||||
curveTo(9.55f, 2.45f, 9.54f, 2.39f, 9.55f, 2.31f)
|
||||
curveTo(10.34f, 2.12f, 11.15f, 2f, 12f, 2f)
|
||||
curveTo(12.84f, 2f, 13.66f, 2.12f, 14.44f, 2.31f)
|
||||
curveTo(14.45f, 2.39f, 14.45f, 2.45f, 14.47f, 2.53f)
|
||||
curveTo(14.92f, 3.86f, 15.24f, 5.22f, 15.49f, 6.59f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(6.59f, 15.49f)
|
||||
curveTo(5.21f, 15.24f, 3.85f, 14.92f, 2.53f, 14.47f)
|
||||
curveTo(2.45f, 14.45f, 2.39f, 14.46f, 2.31f, 14.45f)
|
||||
curveTo(2.12f, 13.66f, 2f, 12.85f, 2f, 12f)
|
||||
curveTo(2f, 11.16f, 2.12f, 10.34f, 2.31f, 9.56f)
|
||||
curveTo(2.39f, 9.55f, 2.45f, 9.55f, 2.53f, 9.53f)
|
||||
curveTo(3.86f, 9.09f, 5.21f, 8.76f, 6.59f, 8.51f)
|
||||
curveTo(6.34f, 10.83f, 6.34f, 13.17f, 6.59f, 15.49f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(22f, 12f)
|
||||
curveTo(22f, 12.85f, 21.88f, 13.66f, 21.69f, 14.45f)
|
||||
curveTo(21.61f, 14.46f, 21.55f, 14.45f, 21.47f, 14.47f)
|
||||
curveTo(20.14f, 14.91f, 18.78f, 15.24f, 17.41f, 15.49f)
|
||||
curveTo(17.67f, 13.17f, 17.67f, 10.83f, 17.41f, 8.51f)
|
||||
curveTo(18.78f, 8.76f, 20.15f, 9.08f, 21.47f, 9.53f)
|
||||
curveTo(21.55f, 9.55f, 21.61f, 9.56f, 21.69f, 9.56f)
|
||||
curveTo(21.88f, 10.35f, 22f, 11.16f, 22f, 12f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(15.49f, 17.41f)
|
||||
curveTo(15.24f, 18.79f, 14.92f, 20.15f, 14.47f, 21.47f)
|
||||
curveTo(14.45f, 21.55f, 14.45f, 21.61f, 14.44f, 21.69f)
|
||||
curveTo(13.66f, 21.88f, 12.84f, 22f, 12f, 22f)
|
||||
curveTo(11.15f, 22f, 10.34f, 21.88f, 9.55f, 21.69f)
|
||||
curveTo(9.54f, 21.61f, 9.55f, 21.55f, 9.53f, 21.47f)
|
||||
curveTo(9.09f, 20.14f, 8.76f, 18.79f, 8.51f, 17.41f)
|
||||
curveTo(9.67f, 17.54f, 10.83f, 17.63f, 12f, 17.63f)
|
||||
curveTo(13.17f, 17.63f, 14.34f, 17.54f, 15.49f, 17.41f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(15.763f, 15.763f)
|
||||
curveTo(13.262f, 16.079f, 10.738f, 16.079f, 8.237f, 15.763f)
|
||||
curveTo(7.921f, 13.262f, 7.921f, 10.738f, 8.237f, 8.237f)
|
||||
curveTo(10.738f, 7.921f, 13.262f, 7.921f, 15.763f, 8.237f)
|
||||
curveTo(16.079f, 10.738f, 16.079f, 13.262f, 15.763f, 15.763f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
|
||||
return _IconsaxGlobe!!
|
||||
}
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
private var _IconsaxGlobe: ImageVector? = null
|
||||
@ -0,0 +1,67 @@
|
||||
package dev.krtirtho.spotube.resources.iconsax
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.StrokeJoin
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val Iconsax.IconsaxImportArrow2: ImageVector
|
||||
get() {
|
||||
if (_IconsaxImportArrow2 != null) {
|
||||
return _IconsaxImportArrow2!!
|
||||
}
|
||||
_IconsaxImportArrow2 = ImageVector.Builder(
|
||||
name = "IconsaxImportArrow2",
|
||||
defaultWidth = 24.dp,
|
||||
defaultHeight = 24.dp,
|
||||
viewportWidth = 24f,
|
||||
viewportHeight = 24f
|
||||
).apply {
|
||||
path(
|
||||
fillAlpha = 0.4f,
|
||||
stroke = SolidColor(Color.White),
|
||||
strokeAlpha = 0.4f,
|
||||
strokeLineWidth = 1.5f,
|
||||
strokeLineCap = StrokeCap.Round,
|
||||
strokeLineJoin = StrokeJoin.Round
|
||||
) {
|
||||
moveTo(22.001f, 2f)
|
||||
lineTo(13.801f, 10.2f)
|
||||
}
|
||||
path(
|
||||
fillAlpha = 0.4f,
|
||||
stroke = SolidColor(Color.White),
|
||||
strokeAlpha = 0.4f,
|
||||
strokeLineWidth = 1.5f,
|
||||
strokeLineCap = StrokeCap.Round,
|
||||
strokeLineJoin = StrokeJoin.Round
|
||||
) {
|
||||
moveTo(13f, 6.17f)
|
||||
verticalLineTo(11f)
|
||||
horizontalLineTo(17.83f)
|
||||
}
|
||||
path(
|
||||
stroke = SolidColor(Color.White),
|
||||
strokeLineWidth = 1.5f,
|
||||
strokeLineCap = StrokeCap.Round,
|
||||
strokeLineJoin = StrokeJoin.Round
|
||||
) {
|
||||
moveTo(11f, 2f)
|
||||
horizontalLineTo(9f)
|
||||
curveTo(4f, 2f, 2f, 4f, 2f, 9f)
|
||||
verticalLineTo(15f)
|
||||
curveTo(2f, 20f, 4f, 22f, 9f, 22f)
|
||||
horizontalLineTo(15f)
|
||||
curveTo(20f, 22f, 22f, 20f, 22f, 15f)
|
||||
verticalLineTo(13f)
|
||||
}
|
||||
}.build()
|
||||
|
||||
return _IconsaxImportArrow2!!
|
||||
}
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
private var _IconsaxImportArrow2: ImageVector? = null
|
||||
@ -0,0 +1,61 @@
|
||||
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.path
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val Iconsax.IconsaxImportArrow2Bulk: ImageVector
|
||||
get() {
|
||||
if (_IconsaxImportArrow2Bulk != null) {
|
||||
return _IconsaxImportArrow2Bulk!!
|
||||
}
|
||||
_IconsaxImportArrow2Bulk = ImageVector.Builder(
|
||||
name = "IconsaxImportArrow2Bulk",
|
||||
defaultWidth = 24.dp,
|
||||
defaultHeight = 24.dp,
|
||||
viewportWidth = 24f,
|
||||
viewportHeight = 24f
|
||||
).apply {
|
||||
path(
|
||||
fill = SolidColor(Color.White),
|
||||
fillAlpha = 0.4f,
|
||||
strokeAlpha = 0.4f
|
||||
) {
|
||||
moveTo(16.8f, 9f)
|
||||
horizontalLineTo(7.2f)
|
||||
curveTo(4f, 9f, 2f, 11f, 2f, 14.2f)
|
||||
verticalLineTo(16.79f)
|
||||
curveTo(2f, 20f, 4f, 22f, 7.2f, 22f)
|
||||
horizontalLineTo(16.79f)
|
||||
curveTo(19.99f, 22f, 21.99f, 20f, 21.99f, 16.8f)
|
||||
verticalLineTo(14.2f)
|
||||
curveTo(22f, 11f, 20f, 9f, 16.8f, 9f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(15.88f, 12.43f)
|
||||
lineTo(12.53f, 15.78f)
|
||||
curveTo(12.24f, 16.07f, 11.76f, 16.07f, 11.47f, 15.78f)
|
||||
lineTo(8.12f, 12.43f)
|
||||
curveTo(7.83f, 12.14f, 7.83f, 11.66f, 8.12f, 11.37f)
|
||||
curveTo(8.41f, 11.08f, 8.89f, 11.08f, 9.18f, 11.37f)
|
||||
lineTo(11.25f, 13.44f)
|
||||
verticalLineTo(2.75f)
|
||||
curveTo(11.25f, 2.34f, 11.59f, 2f, 12f, 2f)
|
||||
curveTo(12.41f, 2f, 12.75f, 2.34f, 12.75f, 2.75f)
|
||||
verticalLineTo(13.44f)
|
||||
lineTo(14.82f, 11.37f)
|
||||
curveTo(14.97f, 11.22f, 15.16f, 11.15f, 15.35f, 11.15f)
|
||||
curveTo(15.54f, 11.15f, 15.73f, 11.22f, 15.88f, 11.37f)
|
||||
curveTo(16.18f, 11.66f, 16.18f, 12.13f, 15.88f, 12.43f)
|
||||
close()
|
||||
}
|
||||
}.build()
|
||||
|
||||
return _IconsaxImportArrow2Bulk!!
|
||||
}
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
private var _IconsaxImportArrow2Bulk: ImageVector? = null
|
||||
@ -0,0 +1,65 @@
|
||||
package dev.krtirtho.spotube.resources.iconsax
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.StrokeJoin
|
||||
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.IconsaxLink: ImageVector
|
||||
get() {
|
||||
if (_IconsaxLink != null) {
|
||||
return _IconsaxLink!!
|
||||
}
|
||||
_IconsaxLink = ImageVector.Builder(
|
||||
name = "IconsaxLink",
|
||||
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(
|
||||
stroke = SolidColor(Color.White),
|
||||
strokeLineWidth = 1.5f,
|
||||
strokeLineCap = StrokeCap.Round,
|
||||
strokeLineJoin = StrokeJoin.Round
|
||||
) {
|
||||
moveTo(13.06f, 10.94f)
|
||||
curveTo(15.31f, 13.19f, 15.31f, 16.83f, 13.06f, 19.07f)
|
||||
curveTo(10.81f, 21.31f, 7.17f, 21.32f, 4.93f, 19.07f)
|
||||
curveTo(2.69f, 16.82f, 2.68f, 13.18f, 4.93f, 10.94f)
|
||||
}
|
||||
path(
|
||||
fillAlpha = 0.4f,
|
||||
stroke = SolidColor(Color.White),
|
||||
strokeAlpha = 0.4f,
|
||||
strokeLineWidth = 1.5f,
|
||||
strokeLineCap = StrokeCap.Round,
|
||||
strokeLineJoin = StrokeJoin.Round
|
||||
) {
|
||||
moveTo(10.59f, 13.41f)
|
||||
curveTo(8.25f, 11.07f, 8.25f, 7.27f, 10.59f, 4.92f)
|
||||
curveTo(12.93f, 2.57f, 16.73f, 2.58f, 19.08f, 4.92f)
|
||||
curveTo(21.43f, 7.26f, 21.42f, 11.06f, 19.08f, 13.41f)
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
|
||||
return _IconsaxLink!!
|
||||
}
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
private var _IconsaxLink: ImageVector? = null
|
||||
@ -0,0 +1,90 @@
|
||||
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.IconsaxMirroringScreen: ImageVector
|
||||
get() {
|
||||
if (_IconsaxMirroringScreen != null) {
|
||||
return _IconsaxMirroringScreen!!
|
||||
}
|
||||
_IconsaxMirroringScreen = ImageVector.Builder(
|
||||
name = "IconsaxMirroringScreen",
|
||||
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(2f, 9f)
|
||||
verticalLineTo(8f)
|
||||
curveTo(2f, 5.24f, 4.24f, 3f, 7f, 3f)
|
||||
horizontalLineTo(17f)
|
||||
curveTo(19.76f, 3f, 22f, 5.24f, 22f, 8f)
|
||||
verticalLineTo(16f)
|
||||
curveTo(22f, 18.76f, 19.76f, 21f, 17f, 21f)
|
||||
horizontalLineTo(16f)
|
||||
horizontalLineTo(7f)
|
||||
curveTo(4.24f, 21f, 2f, 18.76f, 2f, 16f)
|
||||
verticalLineTo(9f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(16.14f, 15.439f)
|
||||
curveTo(15.77f, 15.439f, 15.45f, 15.159f, 15.4f, 14.789f)
|
||||
curveTo(14.97f, 11.409f, 12.51f, 8.949f, 9.13f, 8.519f)
|
||||
curveTo(8.72f, 8.469f, 8.43f, 8.089f, 8.48f, 7.679f)
|
||||
curveTo(8.53f, 7.269f, 8.91f, 6.979f, 9.32f, 7.029f)
|
||||
curveTo(13.39f, 7.549f, 16.36f, 10.519f, 16.89f, 14.599f)
|
||||
curveTo(16.94f, 15.009f, 16.65f, 15.389f, 16.24f, 15.439f)
|
||||
curveTo(16.21f, 15.439f, 16.18f, 15.439f, 16.14f, 15.439f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(13.44f, 16.298f)
|
||||
curveTo(13.07f, 16.298f, 12.75f, 16.018f, 12.7f, 15.648f)
|
||||
curveTo(12.39f, 13.268f, 10.66f, 11.528f, 8.27f, 11.218f)
|
||||
curveTo(7.86f, 11.168f, 7.57f, 10.788f, 7.62f, 10.378f)
|
||||
curveTo(7.67f, 9.968f, 8.05f, 9.678f, 8.46f, 9.728f)
|
||||
curveTo(11.54f, 10.128f, 13.79f, 12.368f, 14.19f, 15.458f)
|
||||
curveTo(14.24f, 15.868f, 13.95f, 16.248f, 13.54f, 16.298f)
|
||||
curveTo(13.51f, 16.298f, 13.47f, 16.298f, 13.44f, 16.298f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(10.39f, 16.809f)
|
||||
curveTo(10.02f, 16.809f, 9.7f, 16.529f, 9.65f, 16.159f)
|
||||
curveTo(9.52f, 15.139f, 8.78f, 14.399f, 7.76f, 14.269f)
|
||||
curveTo(7.35f, 14.219f, 7.06f, 13.839f, 7.11f, 13.429f)
|
||||
curveTo(7.16f, 13.019f, 7.54f, 12.729f, 7.95f, 12.779f)
|
||||
curveTo(9.64f, 12.999f, 10.92f, 14.279f, 11.14f, 15.969f)
|
||||
curveTo(11.19f, 16.379f, 10.9f, 16.759f, 10.49f, 16.809f)
|
||||
curveTo(10.46f, 16.809f, 10.42f, 16.809f, 10.39f, 16.809f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
|
||||
return _IconsaxMirroringScreen!!
|
||||
}
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
private var _IconsaxMirroringScreen: ImageVector? = null
|
||||
@ -0,0 +1,109 @@
|
||||
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.IconsaxMusicPlay: ImageVector
|
||||
get() {
|
||||
if (_IconsaxMusicPlay != null) {
|
||||
return _IconsaxMusicPlay!!
|
||||
}
|
||||
_IconsaxMusicPlay = ImageVector.Builder(
|
||||
name = "IconsaxMusicPlay",
|
||||
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)) {
|
||||
moveTo(13.18f, 11.861f)
|
||||
curveTo(12.78f, 11.861f, 12.42f, 11.641f, 12.25f, 11.281f)
|
||||
lineTo(10.8f, 8.391f)
|
||||
lineTo(10.38f, 9.171f)
|
||||
curveTo(10.15f, 9.601f, 9.69f, 9.871f, 9.2f, 9.871f)
|
||||
horizontalLineTo(8.47f)
|
||||
curveTo(8.06f, 9.871f, 7.72f, 9.531f, 7.72f, 9.121f)
|
||||
curveTo(7.72f, 8.711f, 8.06f, 8.371f, 8.47f, 8.371f)
|
||||
horizontalLineTo(9.11f)
|
||||
lineTo(9.9f, 6.911f)
|
||||
curveTo(10.09f, 6.571f, 10.47f, 6.341f, 10.83f, 6.361f)
|
||||
curveTo(11.22f, 6.361f, 11.57f, 6.591f, 11.75f, 6.931f)
|
||||
lineTo(13.18f, 9.791f)
|
||||
lineTo(13.52f, 9.101f)
|
||||
curveTo(13.75f, 8.641f, 14.2f, 8.361f, 14.72f, 8.361f)
|
||||
horizontalLineTo(15.53f)
|
||||
curveTo(15.94f, 8.361f, 16.28f, 8.701f, 16.28f, 9.111f)
|
||||
curveTo(16.28f, 9.521f, 15.94f, 9.861f, 15.53f, 9.861f)
|
||||
horizontalLineTo(14.82f)
|
||||
lineTo(14.11f, 11.271f)
|
||||
curveTo(13.93f, 11.641f, 13.58f, 11.861f, 13.18f, 11.861f)
|
||||
close()
|
||||
}
|
||||
path(
|
||||
fill = SolidColor(Color.White),
|
||||
fillAlpha = 0.4f,
|
||||
strokeAlpha = 0.4f
|
||||
) {
|
||||
moveTo(2.75f, 18.651f)
|
||||
curveTo(2.34f, 18.651f, 2f, 18.311f, 2f, 17.901f)
|
||||
verticalLineTo(12.201f)
|
||||
curveTo(1.95f, 9.491f, 2.96f, 6.931f, 4.84f, 5.011f)
|
||||
curveTo(6.72f, 3.101f, 9.24f, 2.051f, 11.95f, 2.051f)
|
||||
curveTo(17.49f, 2.051f, 22f, 6.561f, 22f, 12.101f)
|
||||
verticalLineTo(17.801f)
|
||||
curveTo(22f, 18.211f, 21.66f, 18.551f, 21.25f, 18.551f)
|
||||
curveTo(20.84f, 18.551f, 20.5f, 18.211f, 20.5f, 17.801f)
|
||||
verticalLineTo(12.101f)
|
||||
curveTo(20.5f, 7.391f, 16.67f, 3.551f, 11.95f, 3.551f)
|
||||
curveTo(9.64f, 3.551f, 7.5f, 4.441f, 5.91f, 6.061f)
|
||||
curveTo(4.31f, 7.691f, 3.46f, 9.861f, 3.5f, 12.181f)
|
||||
verticalLineTo(17.891f)
|
||||
curveTo(3.5f, 18.311f, 3.17f, 18.651f, 2.75f, 18.651f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(5.94f, 12.451f)
|
||||
horizontalLineTo(5.81f)
|
||||
curveTo(3.71f, 12.451f, 2f, 14.161f, 2f, 16.261f)
|
||||
verticalLineTo(18.141f)
|
||||
curveTo(2f, 20.241f, 3.71f, 21.951f, 5.81f, 21.951f)
|
||||
horizontalLineTo(5.94f)
|
||||
curveTo(8.04f, 21.951f, 9.75f, 20.241f, 9.75f, 18.141f)
|
||||
verticalLineTo(16.261f)
|
||||
curveTo(9.75f, 14.161f, 8.04f, 12.451f, 5.94f, 12.451f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(18.19f, 12.451f)
|
||||
horizontalLineTo(18.06f)
|
||||
curveTo(15.96f, 12.451f, 14.25f, 14.161f, 14.25f, 16.261f)
|
||||
verticalLineTo(18.141f)
|
||||
curveTo(14.25f, 20.241f, 15.96f, 21.951f, 18.06f, 21.951f)
|
||||
horizontalLineTo(18.19f)
|
||||
curveTo(20.29f, 21.951f, 22f, 20.241f, 22f, 18.141f)
|
||||
verticalLineTo(16.261f)
|
||||
curveTo(22f, 14.161f, 20.29f, 12.451f, 18.19f, 12.451f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
|
||||
return _IconsaxMusicPlay!!
|
||||
}
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
private var _IconsaxMusicPlay: ImageVector? = null
|
||||
@ -0,0 +1,83 @@
|
||||
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.path
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val Iconsax.IconsaxRefreshArrow2: ImageVector
|
||||
get() {
|
||||
if (_IconsaxRefreshArrow2 != null) {
|
||||
return _IconsaxRefreshArrow2!!
|
||||
}
|
||||
_IconsaxRefreshArrow2 = ImageVector.Builder(
|
||||
name = "IconsaxRefreshArrow2",
|
||||
defaultWidth = 24.dp,
|
||||
defaultHeight = 24.dp,
|
||||
viewportWidth = 24f,
|
||||
viewportHeight = 24f
|
||||
).apply {
|
||||
path(
|
||||
fill = SolidColor(Color.White),
|
||||
fillAlpha = 0.4f,
|
||||
strokeAlpha = 0.4f
|
||||
) {
|
||||
moveTo(22f, 7.81f)
|
||||
verticalLineTo(16.18f)
|
||||
curveTo(22f, 19.82f, 19.83f, 21.99f, 16.19f, 21.99f)
|
||||
horizontalLineTo(7.81f)
|
||||
curveTo(4.17f, 22f, 2f, 19.83f, 2f, 16.19f)
|
||||
verticalLineTo(7.81f)
|
||||
curveTo(2f, 4.17f, 4.17f, 2f, 7.81f, 2f)
|
||||
horizontalLineTo(16.18f)
|
||||
curveTo(19.83f, 2f, 22f, 4.17f, 22f, 7.81f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(12f, 18.25f)
|
||||
curveTo(10.21f, 18.25f, 8.81f, 17.36f, 7.86f, 16.48f)
|
||||
verticalLineTo(17.19f)
|
||||
curveTo(7.86f, 17.6f, 7.52f, 17.94f, 7.11f, 17.94f)
|
||||
curveTo(6.7f, 17.94f, 6.36f, 17.6f, 6.36f, 17.19f)
|
||||
verticalLineTo(14.44f)
|
||||
curveTo(6.36f, 14.03f, 6.7f, 13.69f, 7.11f, 13.69f)
|
||||
horizontalLineTo(9.59f)
|
||||
curveTo(10f, 13.69f, 10.34f, 14.03f, 10.34f, 14.44f)
|
||||
curveTo(10.34f, 14.85f, 10f, 15.19f, 9.59f, 15.19f)
|
||||
horizontalLineTo(8.69f)
|
||||
curveTo(9.43f, 15.93f, 10.58f, 16.75f, 12f, 16.75f)
|
||||
curveTo(14.62f, 16.75f, 16.75f, 14.62f, 16.75f, 12f)
|
||||
curveTo(16.75f, 11.59f, 17.09f, 11.25f, 17.5f, 11.25f)
|
||||
curveTo(17.91f, 11.25f, 18.25f, 11.59f, 18.25f, 12f)
|
||||
curveTo(18.25f, 15.45f, 15.45f, 18.25f, 12f, 18.25f)
|
||||
close()
|
||||
moveTo(6.5f, 12.75f)
|
||||
curveTo(6.09f, 12.75f, 5.75f, 12.41f, 5.75f, 12f)
|
||||
curveTo(5.75f, 8.55f, 8.55f, 5.75f, 12f, 5.75f)
|
||||
curveTo(14.15f, 5.75f, 15.73f, 6.68f, 16.75f, 7.57f)
|
||||
verticalLineTo(6.81f)
|
||||
curveTo(16.75f, 6.4f, 17.09f, 6.06f, 17.5f, 6.06f)
|
||||
curveTo(17.91f, 6.06f, 18.25f, 6.4f, 18.25f, 6.81f)
|
||||
verticalLineTo(9.55f)
|
||||
curveTo(18.25f, 9.58f, 18.25f, 9.61f, 18.25f, 9.63f)
|
||||
curveTo(18.24f, 9.74f, 18.21f, 9.84f, 18.16f, 9.93f)
|
||||
curveTo(18.11f, 10.02f, 18.04f, 10.1f, 17.95f, 10.17f)
|
||||
curveTo(17.88f, 10.22f, 17.8f, 10.26f, 17.71f, 10.29f)
|
||||
curveTo(17.64f, 10.31f, 17.57f, 10.32f, 17.5f, 10.32f)
|
||||
horizontalLineTo(15.07f)
|
||||
curveTo(14.66f, 10.32f, 14.32f, 9.98f, 14.32f, 9.57f)
|
||||
curveTo(14.32f, 9.16f, 14.66f, 8.82f, 15.07f, 8.82f)
|
||||
horizontalLineTo(15.9f)
|
||||
curveTo(15.1f, 8.08f, 13.81f, 7.26f, 12.02f, 7.26f)
|
||||
curveTo(9.4f, 7.26f, 7.27f, 9.39f, 7.27f, 12.01f)
|
||||
curveTo(7.25f, 12.41f, 6.91f, 12.75f, 6.5f, 12.75f)
|
||||
close()
|
||||
}
|
||||
}.build()
|
||||
|
||||
return _IconsaxRefreshArrow2!!
|
||||
}
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
private var _IconsaxRefreshArrow2: ImageVector? = null
|
||||
@ -0,0 +1,63 @@
|
||||
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.IconsaxStopCircle: ImageVector
|
||||
get() {
|
||||
if (_IconsaxStopCircle != null) {
|
||||
return _IconsaxStopCircle!!
|
||||
}
|
||||
_IconsaxStopCircle = ImageVector.Builder(
|
||||
name = "IconsaxStopCircle",
|
||||
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(11.97f, 22f)
|
||||
curveTo(17.493f, 22f, 21.97f, 17.523f, 21.97f, 12f)
|
||||
curveTo(21.97f, 6.477f, 17.493f, 2f, 11.97f, 2f)
|
||||
curveTo(6.447f, 2f, 1.97f, 6.477f, 1.97f, 12f)
|
||||
curveTo(1.97f, 17.523f, 6.447f, 22f, 11.97f, 22f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(10.77f, 16.229f)
|
||||
horizontalLineTo(13.23f)
|
||||
curveTo(14.89f, 16.229f, 16.23f, 14.889f, 16.23f, 13.229f)
|
||||
verticalLineTo(10.769f)
|
||||
curveTo(16.23f, 9.11f, 14.89f, 7.77f, 13.23f, 7.77f)
|
||||
horizontalLineTo(10.77f)
|
||||
curveTo(9.11f, 7.77f, 7.77f, 9.11f, 7.77f, 10.769f)
|
||||
verticalLineTo(13.229f)
|
||||
curveTo(7.77f, 14.889f, 9.11f, 16.229f, 10.77f, 16.229f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
|
||||
return _IconsaxStopCircle!!
|
||||
}
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
private var _IconsaxStopCircle: ImageVector? = null
|
||||
@ -0,0 +1,65 @@
|
||||
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.IconsaxTag: ImageVector
|
||||
get() {
|
||||
if (_IconsaxTag != null) {
|
||||
return _IconsaxTag!!
|
||||
}
|
||||
_IconsaxTag = ImageVector.Builder(
|
||||
name = "IconsaxTag",
|
||||
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(4.17f, 15.3f)
|
||||
lineTo(8.7f, 19.83f)
|
||||
curveTo(10.56f, 21.69f, 13.58f, 21.69f, 15.45f, 19.83f)
|
||||
lineTo(19.84f, 15.44f)
|
||||
curveTo(21.7f, 13.58f, 21.7f, 10.56f, 19.84f, 8.69f)
|
||||
lineTo(15.3f, 4.17f)
|
||||
curveTo(14.35f, 3.22f, 13.04f, 2.71f, 11.7f, 2.78f)
|
||||
lineTo(6.7f, 3.02f)
|
||||
curveTo(4.7f, 3.11f, 3.11f, 4.7f, 3.01f, 6.69f)
|
||||
lineTo(2.77f, 11.69f)
|
||||
curveTo(2.71f, 13.04f, 3.22f, 14.35f, 4.17f, 15.3f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(9.5f, 12.38f)
|
||||
curveTo(11.091f, 12.38f, 12.38f, 11.091f, 12.38f, 9.5f)
|
||||
curveTo(12.38f, 7.91f, 11.091f, 6.62f, 9.5f, 6.62f)
|
||||
curveTo(7.91f, 6.62f, 6.62f, 7.91f, 6.62f, 9.5f)
|
||||
curveTo(6.62f, 11.091f, 7.91f, 12.38f, 9.5f, 12.38f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
|
||||
return _IconsaxTag!!
|
||||
}
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
private var _IconsaxTag: ImageVector? = null
|
||||
@ -0,0 +1,87 @@
|
||||
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.IconsaxTextalignLeft: ImageVector
|
||||
get() {
|
||||
if (_IconsaxTextalignLeft != null) {
|
||||
return _IconsaxTextalignLeft!!
|
||||
}
|
||||
_IconsaxTextalignLeft = ImageVector.Builder(
|
||||
name = "IconsaxTextalignLeft",
|
||||
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)) {
|
||||
moveTo(21f, 5.25f)
|
||||
horizontalLineTo(3f)
|
||||
curveTo(2.59f, 5.25f, 2.25f, 4.91f, 2.25f, 4.5f)
|
||||
curveTo(2.25f, 4.09f, 2.59f, 3.75f, 3f, 3.75f)
|
||||
horizontalLineTo(21f)
|
||||
curveTo(21.41f, 3.75f, 21.75f, 4.09f, 21.75f, 4.5f)
|
||||
curveTo(21.75f, 4.91f, 21.41f, 5.25f, 21f, 5.25f)
|
||||
close()
|
||||
}
|
||||
path(
|
||||
fill = SolidColor(Color.White),
|
||||
fillAlpha = 0.4f,
|
||||
strokeAlpha = 0.4f
|
||||
) {
|
||||
moveTo(12.47f, 10.25f)
|
||||
horizontalLineTo(3f)
|
||||
curveTo(2.59f, 10.25f, 2.25f, 9.91f, 2.25f, 9.5f)
|
||||
curveTo(2.25f, 9.09f, 2.59f, 8.75f, 3f, 8.75f)
|
||||
horizontalLineTo(12.47f)
|
||||
curveTo(12.88f, 8.75f, 13.22f, 9.09f, 13.22f, 9.5f)
|
||||
curveTo(13.22f, 9.91f, 12.89f, 10.25f, 12.47f, 10.25f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(21f, 15.25f)
|
||||
horizontalLineTo(3f)
|
||||
curveTo(2.59f, 15.25f, 2.25f, 14.91f, 2.25f, 14.5f)
|
||||
curveTo(2.25f, 14.09f, 2.59f, 13.75f, 3f, 13.75f)
|
||||
horizontalLineTo(21f)
|
||||
curveTo(21.41f, 13.75f, 21.75f, 14.09f, 21.75f, 14.5f)
|
||||
curveTo(21.75f, 14.91f, 21.41f, 15.25f, 21f, 15.25f)
|
||||
close()
|
||||
}
|
||||
path(
|
||||
fill = SolidColor(Color.White),
|
||||
fillAlpha = 0.4f,
|
||||
strokeAlpha = 0.4f
|
||||
) {
|
||||
moveTo(12.47f, 20.25f)
|
||||
horizontalLineTo(3f)
|
||||
curveTo(2.59f, 20.25f, 2.25f, 19.91f, 2.25f, 19.5f)
|
||||
curveTo(2.25f, 19.09f, 2.59f, 18.75f, 3f, 18.75f)
|
||||
horizontalLineTo(12.47f)
|
||||
curveTo(12.88f, 18.75f, 13.22f, 19.09f, 13.22f, 19.5f)
|
||||
curveTo(13.22f, 19.91f, 12.89f, 20.25f, 12.47f, 20.25f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
|
||||
return _IconsaxTextalignLeft!!
|
||||
}
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
private var _IconsaxTextalignLeft: ImageVector? = null
|
||||
@ -0,0 +1,130 @@
|
||||
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.IconsaxVerticalSetting: ImageVector
|
||||
get() {
|
||||
if (_IconsaxVerticalSetting != null) {
|
||||
return _IconsaxVerticalSetting!!
|
||||
}
|
||||
_IconsaxVerticalSetting = ImageVector.Builder(
|
||||
name = "IconsaxVerticalSetting",
|
||||
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(18.51f, 1.801f)
|
||||
curveTo(18.01f, 1.801f, 17.61f, 2.201f, 17.61f, 2.701f)
|
||||
verticalLineTo(7.351f)
|
||||
curveTo(17.61f, 7.851f, 18.01f, 8.251f, 18.51f, 8.251f)
|
||||
curveTo(19.01f, 8.251f, 19.41f, 7.851f, 19.41f, 7.351f)
|
||||
verticalLineTo(2.701f)
|
||||
curveTo(19.41f, 2.201f, 19.01f, 1.801f, 18.51f, 1.801f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(12f, 15.75f)
|
||||
curveTo(11.5f, 15.75f, 11.1f, 16.15f, 11.1f, 16.65f)
|
||||
verticalLineTo(21.3f)
|
||||
curveTo(11.1f, 21.8f, 11.5f, 22.2f, 12f, 22.2f)
|
||||
curveTo(12.5f, 22.2f, 12.9f, 21.8f, 12.9f, 21.3f)
|
||||
verticalLineTo(16.65f)
|
||||
curveTo(12.9f, 16.16f, 12.5f, 15.75f, 12f, 15.75f)
|
||||
close()
|
||||
}
|
||||
path(
|
||||
fill = SolidColor(Color.White),
|
||||
fillAlpha = 0.4f,
|
||||
strokeAlpha = 0.4f
|
||||
) {
|
||||
moveTo(5.49f, 1.801f)
|
||||
curveTo(4.99f, 1.801f, 4.59f, 2.201f, 4.59f, 2.701f)
|
||||
verticalLineTo(7.351f)
|
||||
curveTo(4.59f, 7.851f, 4.99f, 8.251f, 5.49f, 8.251f)
|
||||
curveTo(5.99f, 8.251f, 6.39f, 7.851f, 6.39f, 7.351f)
|
||||
verticalLineTo(2.701f)
|
||||
curveTo(6.39f, 2.201f, 5.98f, 1.801f, 5.49f, 1.801f)
|
||||
close()
|
||||
}
|
||||
path(
|
||||
fill = SolidColor(Color.White),
|
||||
fillAlpha = 0.4f,
|
||||
strokeAlpha = 0.4f
|
||||
) {
|
||||
moveTo(7.35f, 10.17f)
|
||||
horizontalLineTo(3.63f)
|
||||
curveTo(3.13f, 10.17f, 2.73f, 10.57f, 2.73f, 11.07f)
|
||||
curveTo(2.73f, 11.57f, 3.13f, 11.97f, 3.63f, 11.97f)
|
||||
horizontalLineTo(4.59f)
|
||||
verticalLineTo(21.3f)
|
||||
curveTo(4.59f, 21.8f, 4.99f, 22.2f, 5.49f, 22.2f)
|
||||
curveTo(5.99f, 22.2f, 6.39f, 21.8f, 6.39f, 21.3f)
|
||||
verticalLineTo(11.97f)
|
||||
horizontalLineTo(7.35f)
|
||||
curveTo(7.85f, 11.97f, 8.25f, 11.57f, 8.25f, 11.07f)
|
||||
curveTo(8.25f, 10.57f, 7.84f, 10.17f, 7.35f, 10.17f)
|
||||
close()
|
||||
}
|
||||
path(
|
||||
fill = SolidColor(Color.White),
|
||||
fillAlpha = 0.4f,
|
||||
strokeAlpha = 0.4f
|
||||
) {
|
||||
moveTo(20.37f, 10.17f)
|
||||
horizontalLineTo(16.65f)
|
||||
curveTo(16.15f, 10.17f, 15.75f, 10.57f, 15.75f, 11.07f)
|
||||
curveTo(15.75f, 11.57f, 16.15f, 11.97f, 16.65f, 11.97f)
|
||||
horizontalLineTo(17.61f)
|
||||
verticalLineTo(21.3f)
|
||||
curveTo(17.61f, 21.8f, 18.01f, 22.2f, 18.51f, 22.2f)
|
||||
curveTo(19.01f, 22.2f, 19.41f, 21.8f, 19.41f, 21.3f)
|
||||
verticalLineTo(11.97f)
|
||||
horizontalLineTo(20.37f)
|
||||
curveTo(20.87f, 11.97f, 21.27f, 11.57f, 21.27f, 11.07f)
|
||||
curveTo(21.27f, 10.57f, 20.87f, 10.17f, 20.37f, 10.17f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(13.86f, 12.031f)
|
||||
horizontalLineTo(12.9f)
|
||||
verticalLineTo(2.701f)
|
||||
curveTo(12.9f, 2.201f, 12.5f, 1.801f, 12f, 1.801f)
|
||||
curveTo(11.5f, 1.801f, 11.1f, 2.201f, 11.1f, 2.701f)
|
||||
verticalLineTo(12.031f)
|
||||
horizontalLineTo(10.14f)
|
||||
curveTo(9.64f, 12.031f, 9.24f, 12.431f, 9.24f, 12.931f)
|
||||
curveTo(9.24f, 13.431f, 9.64f, 13.831f, 10.14f, 13.831f)
|
||||
horizontalLineTo(13.86f)
|
||||
curveTo(14.36f, 13.831f, 14.76f, 13.431f, 14.76f, 12.931f)
|
||||
curveTo(14.76f, 12.431f, 14.36f, 12.031f, 13.86f, 12.031f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
|
||||
return _IconsaxVerticalSetting!!
|
||||
}
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
private var _IconsaxVerticalSetting: ImageVector? = null
|
||||
@ -0,0 +1,88 @@
|
||||
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.IconsaxWifiSquare: ImageVector
|
||||
get() {
|
||||
if (_IconsaxWifiSquare != null) {
|
||||
return _IconsaxWifiSquare!!
|
||||
}
|
||||
_IconsaxWifiSquare = ImageVector.Builder(
|
||||
name = "IconsaxWifiSquare",
|
||||
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(16.19f, 2f)
|
||||
horizontalLineTo(7.82f)
|
||||
curveTo(4.17f, 2f, 2f, 4.17f, 2f, 7.81f)
|
||||
verticalLineTo(16.18f)
|
||||
curveTo(2f, 19.82f, 4.17f, 21.99f, 7.81f, 21.99f)
|
||||
horizontalLineTo(16.18f)
|
||||
curveTo(19.82f, 21.99f, 21.99f, 19.82f, 21.99f, 16.18f)
|
||||
verticalLineTo(7.81f)
|
||||
curveTo(22f, 4.17f, 19.83f, 2f, 16.19f, 2f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(18f, 10.71f)
|
||||
curveTo(17.84f, 10.71f, 17.68f, 10.66f, 17.54f, 10.55f)
|
||||
curveTo(14.17f, 7.95f, 9.82f, 7.95f, 6.46f, 10.55f)
|
||||
curveTo(6.13f, 10.8f, 5.66f, 10.74f, 5.41f, 10.42f)
|
||||
curveTo(5.16f, 10.09f, 5.22f, 9.62f, 5.54f, 9.37f)
|
||||
curveTo(9.46f, 6.34f, 14.53f, 6.34f, 18.46f, 9.37f)
|
||||
curveTo(18.79f, 9.62f, 18.85f, 10.09f, 18.59f, 10.42f)
|
||||
curveTo(18.45f, 10.61f, 18.22f, 10.71f, 18f, 10.71f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(16.4f, 13.8f)
|
||||
curveTo(16.24f, 13.8f, 16.08f, 13.75f, 15.94f, 13.64f)
|
||||
curveTo(13.54f, 11.79f, 10.45f, 11.79f, 8.05f, 13.64f)
|
||||
curveTo(7.72f, 13.89f, 7.25f, 13.83f, 7f, 13.51f)
|
||||
curveTo(6.75f, 13.19f, 6.81f, 12.71f, 7.13f, 12.46f)
|
||||
curveTo(10.08f, 10.18f, 13.9f, 10.18f, 16.85f, 12.46f)
|
||||
curveTo(17.18f, 12.71f, 17.24f, 13.18f, 16.98f, 13.51f)
|
||||
curveTo(16.85f, 13.7f, 16.63f, 13.8f, 16.4f, 13.8f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.White)) {
|
||||
moveTo(14.2f, 16.889f)
|
||||
curveTo(14.04f, 16.889f, 13.88f, 16.84f, 13.74f, 16.729f)
|
||||
curveTo(12.68f, 15.91f, 11.31f, 15.91f, 10.25f, 16.729f)
|
||||
curveTo(9.92f, 16.979f, 9.45f, 16.92f, 9.2f, 16.6f)
|
||||
curveTo(8.95f, 16.27f, 9.01f, 15.8f, 9.33f, 15.55f)
|
||||
curveTo(10.92f, 14.319f, 13.06f, 14.319f, 14.65f, 15.55f)
|
||||
curveTo(14.98f, 15.8f, 15.04f, 16.27f, 14.78f, 16.6f)
|
||||
curveTo(14.65f, 16.789f, 14.43f, 16.889f, 14.2f, 16.889f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
|
||||
return _IconsaxWifiSquare!!
|
||||
}
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
private var _IconsaxWifiSquare: ImageVector? = null
|
||||
@ -0,0 +1,59 @@
|
||||
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.path
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val Iconsax.LineDiscord: ImageVector
|
||||
get() {
|
||||
if (_LineDiscord != null) {
|
||||
return _LineDiscord!!
|
||||
}
|
||||
_LineDiscord = ImageVector.Builder(
|
||||
name = "LineDiscord",
|
||||
defaultWidth = 24.dp,
|
||||
defaultHeight = 24.dp,
|
||||
viewportWidth = 24f,
|
||||
viewportHeight = 24f
|
||||
).apply {
|
||||
path(fill = SolidColor(Color.Black)) {
|
||||
moveTo(18.942f, 5.555f)
|
||||
curveTo(17.647f, 4.95f, 16.263f, 4.509f, 14.816f, 4.259f)
|
||||
curveTo(14.638f, 4.58f, 14.43f, 5.012f, 14.287f, 5.356f)
|
||||
curveTo(12.749f, 5.125f, 11.224f, 5.125f, 9.714f, 5.356f)
|
||||
curveTo(9.571f, 5.012f, 9.359f, 4.58f, 9.179f, 4.259f)
|
||||
curveTo(7.731f, 4.509f, 6.345f, 4.951f, 5.05f, 5.559f)
|
||||
curveTo(2.439f, 9.505f, 1.731f, 13.353f, 2.085f, 17.146f)
|
||||
curveTo(3.817f, 18.439f, 5.495f, 19.225f, 7.145f, 19.739f)
|
||||
curveTo(7.553f, 19.178f, 7.916f, 18.582f, 8.229f, 17.954f)
|
||||
curveTo(7.633f, 17.728f, 7.062f, 17.448f, 6.523f, 17.124f)
|
||||
curveTo(6.666f, 17.017f, 6.806f, 16.907f, 6.941f, 16.792f)
|
||||
curveTo(10.232f, 18.332f, 13.807f, 18.332f, 17.059f, 16.792f)
|
||||
curveTo(17.196f, 16.907f, 17.336f, 17.017f, 17.477f, 17.124f)
|
||||
curveTo(16.936f, 17.45f, 16.364f, 17.729f, 15.767f, 17.956f)
|
||||
curveTo(16.08f, 18.582f, 16.442f, 19.18f, 16.851f, 19.741f)
|
||||
curveTo(18.503f, 19.227f, 20.183f, 18.441f, 21.915f, 17.146f)
|
||||
curveTo(22.33f, 12.748f, 21.206f, 8.936f, 18.942f, 5.555f)
|
||||
close()
|
||||
moveTo(8.678f, 14.813f)
|
||||
curveTo(7.69f, 14.813f, 6.88f, 13.891f, 6.88f, 12.768f)
|
||||
curveTo(6.88f, 11.645f, 7.672f, 10.721f, 8.678f, 10.721f)
|
||||
curveTo(9.683f, 10.721f, 10.493f, 11.643f, 10.476f, 12.768f)
|
||||
curveTo(10.477f, 13.891f, 9.683f, 14.813f, 8.678f, 14.813f)
|
||||
close()
|
||||
moveTo(15.322f, 14.813f)
|
||||
curveTo(14.334f, 14.813f, 13.524f, 13.891f, 13.524f, 12.768f)
|
||||
curveTo(13.524f, 11.645f, 14.317f, 10.721f, 15.322f, 10.721f)
|
||||
curveTo(16.327f, 10.721f, 17.138f, 11.643f, 17.12f, 12.768f)
|
||||
curveTo(17.12f, 13.891f, 16.327f, 14.813f, 15.322f, 14.813f)
|
||||
close()
|
||||
}
|
||||
}.build()
|
||||
|
||||
return _LineDiscord!!
|
||||
}
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
private var _LineDiscord: ImageVector? = null
|
||||
@ -0,0 +1,78 @@
|
||||
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.path
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val Iconsax.PhosphorDatabaseDuotone: ImageVector
|
||||
get() {
|
||||
if (_PhosphorDatabaseDuotone != null) {
|
||||
return _PhosphorDatabaseDuotone!!
|
||||
}
|
||||
_PhosphorDatabaseDuotone = ImageVector.Builder(
|
||||
name = "PhosphorDatabaseDuotone",
|
||||
defaultWidth = 24.dp,
|
||||
defaultHeight = 24.dp,
|
||||
viewportWidth = 256f,
|
||||
viewportHeight = 256f
|
||||
).apply {
|
||||
path(
|
||||
fill = SolidColor(Color.Black),
|
||||
fillAlpha = 0.2f,
|
||||
strokeAlpha = 0.2f
|
||||
) {
|
||||
moveTo(216f, 80f)
|
||||
curveToRelative(0f, 26.51f, -39.4f, 48f, -88f, 48f)
|
||||
reflectiveCurveTo(40f, 106.51f, 40f, 80f)
|
||||
reflectiveCurveToRelative(39.4f, -48f, 88f, -48f)
|
||||
reflectiveCurveTo(216f, 53.49f, 216f, 80f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color.Black)) {
|
||||
moveTo(128f, 24f)
|
||||
curveTo(74.17f, 24f, 32f, 48.6f, 32f, 80f)
|
||||
verticalLineToRelative(96f)
|
||||
curveToRelative(0f, 31.4f, 42.17f, 56f, 96f, 56f)
|
||||
reflectiveCurveToRelative(96f, -24.6f, 96f, -56f)
|
||||
lineTo(224f, 80f)
|
||||
curveTo(224f, 48.6f, 181.83f, 24f, 128f, 24f)
|
||||
close()
|
||||
moveTo(208f, 128f)
|
||||
curveToRelative(0f, 9.62f, -7.88f, 19.43f, -21.61f, 26.92f)
|
||||
curveTo(170.93f, 163.35f, 150.19f, 168f, 128f, 168f)
|
||||
reflectiveCurveToRelative(-42.93f, -4.65f, -58.39f, -13.08f)
|
||||
curveTo(55.88f, 147.43f, 48f, 137.62f, 48f, 128f)
|
||||
lineTo(48f, 111.36f)
|
||||
curveToRelative(17.06f, 15f, 46.23f, 24.64f, 80f, 24.64f)
|
||||
reflectiveCurveToRelative(62.94f, -9.68f, 80f, -24.64f)
|
||||
close()
|
||||
moveTo(69.61f, 53.08f)
|
||||
curveTo(85.07f, 44.65f, 105.81f, 40f, 128f, 40f)
|
||||
reflectiveCurveToRelative(42.93f, 4.65f, 58.39f, 13.08f)
|
||||
curveTo(200.12f, 60.57f, 208f, 70.38f, 208f, 80f)
|
||||
reflectiveCurveToRelative(-7.88f, 19.43f, -21.61f, 26.92f)
|
||||
curveTo(170.93f, 115.35f, 150.19f, 120f, 128f, 120f)
|
||||
reflectiveCurveToRelative(-42.93f, -4.65f, -58.39f, -13.08f)
|
||||
curveTo(55.88f, 99.43f, 48f, 89.62f, 48f, 80f)
|
||||
reflectiveCurveTo(55.88f, 60.57f, 69.61f, 53.08f)
|
||||
close()
|
||||
moveTo(186.39f, 202.92f)
|
||||
curveTo(170.93f, 211.35f, 150.19f, 216f, 128f, 216f)
|
||||
reflectiveCurveToRelative(-42.93f, -4.65f, -58.39f, -13.08f)
|
||||
curveTo(55.88f, 195.43f, 48f, 185.62f, 48f, 176f)
|
||||
lineTo(48f, 159.36f)
|
||||
curveToRelative(17.06f, 15f, 46.23f, 24.64f, 80f, 24.64f)
|
||||
reflectiveCurveToRelative(62.94f, -9.68f, 80f, -24.64f)
|
||||
lineTo(208f, 176f)
|
||||
curveTo(208f, 185.62f, 200.12f, 195.43f, 186.39f, 202.92f)
|
||||
close()
|
||||
}
|
||||
}.build()
|
||||
|
||||
return _PhosphorDatabaseDuotone!!
|
||||
}
|
||||
|
||||
@Suppress("ObjectPropertyName")
|
||||
private var _PhosphorDatabaseDuotone: ImageVector? = null
|
||||
@ -104,6 +104,8 @@ object FakeMetadataStore {
|
||||
|
||||
fun getUser(id: String): MetadataUser? = users[id]?.toModel()
|
||||
|
||||
fun getCurrentUser(): MetadataUser? = users[CURRENT_USER_ID]?.toModel()
|
||||
|
||||
fun getTrack(id: String): MetadataTrack = tracks[id]?.toModel()
|
||||
?: error("Track not found: $id")
|
||||
|
||||
|
||||
@ -23,4 +23,8 @@ class RealMetadataUserAPI : MetadataUserAPI {
|
||||
override suspend fun getUser(id: String): MetadataUser? {
|
||||
return FakeMetadataStore.getUser(id)
|
||||
}
|
||||
|
||||
override suspend fun me(): MetadataUser? {
|
||||
return FakeMetadataStore.getCurrentUser()
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user