mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-08-06 04:09:51 +00:00
Compare commits
6 Commits
b26928a8a0
...
a38b9e48cc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a38b9e48cc | ||
|
|
2aae112cbe | ||
|
|
d53ac853a5 | ||
|
|
69a464dcd2 | ||
|
|
941b333a79 | ||
|
|
f6308d0f8b |
94
.github/workflows/spotube-release-binary.yml
vendored
94
.github/workflows/spotube-release-binary.yml
vendored
@ -25,6 +25,7 @@ env:
|
|||||||
COMPOSE_WEBVIEW_REPO: kdroidFilter/ComposeNativeWebview
|
COMPOSE_WEBVIEW_REPO: kdroidFilter/ComposeNativeWebview
|
||||||
COMPOSE_WEBVIEW_COMMIT: 5aeb268a2c37ff4dbfb76924573344a7c4f763ce
|
COMPOSE_WEBVIEW_COMMIT: 5aeb268a2c37ff4dbfb76924573344a7c4f763ce
|
||||||
GRADLE_PLUGIN_REPO: team-spotube/gradle-plugin
|
GRADLE_PLUGIN_REPO: team-spotube/gradle-plugin
|
||||||
|
VLCJ_BUNDLER_REPO: team-spotube/vlcj-bundler-gradle-plugin
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prepare-deps:
|
prepare-deps:
|
||||||
@ -46,6 +47,13 @@ jobs:
|
|||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
path: gradle-plugin
|
path: gradle-plugin
|
||||||
|
|
||||||
|
- name: Checkout vlcj-bundler
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: ${{ env.VLCJ_BUNDLER_REPO }}
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
path: vlcj-bundler
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
@ -73,38 +81,30 @@ jobs:
|
|||||||
chmod +x gradlew
|
chmod +x gradlew
|
||||||
./gradlew publishToMavenLocal
|
./gradlew publishToMavenLocal
|
||||||
|
|
||||||
- name: Upload compose-webview artifacts
|
- name: Publish vlcj-bundler to mavenLocal
|
||||||
uses: actions/upload-artifact@v4
|
working-directory: vlcj-bundler
|
||||||
with:
|
run: |
|
||||||
name: maven-local-composewebview
|
chmod +x gradlew
|
||||||
path: ~/.m2/repository/io/github/kdroidfilter/
|
./gradlew publishToMavenLocal
|
||||||
|
|
||||||
- name: Upload gradle-plugin artifacts
|
- name: Upload maven local
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: maven-local-gradle-plugin
|
name: maven-local
|
||||||
path: ~/.m2/repository/dev/krtirtho/
|
path: ~/.m2/repository/
|
||||||
|
|
||||||
build-android:
|
build-android:
|
||||||
name: Android
|
name: Android
|
||||||
needs: prepare-deps
|
needs: prepare-deps
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
|
||||||
GRADLE_FLAGS: ${{ inputs.release_type == 'nightly' && '-PversionName=nightly' || '' }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download compose-webview artifacts
|
- name: Download maven local
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: maven-local-composewebview
|
name: maven-local
|
||||||
path: ~/.m2/repository/io/github/kdroidfilter/
|
path: ~/.m2/repository/
|
||||||
|
|
||||||
- name: Download gradle-plugin artifacts
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: maven-local-gradle-plugin
|
|
||||||
path: ~/.m2/repository/dev/krtirtho/
|
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
@ -133,7 +133,7 @@ jobs:
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
- name: Assemble Release
|
- name: Assemble Release
|
||||||
run: ./gradlew :composeApp:assembleRelease ${{ env.GRADLE_FLAGS }}
|
run: ./gradlew :composeApp:assembleRelease
|
||||||
|
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
@ -151,22 +151,14 @@ jobs:
|
|||||||
name: Linux Desktop
|
name: Linux Desktop
|
||||||
needs: prepare-deps
|
needs: prepare-deps
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
|
||||||
GRADLE_FLAGS: ${{ inputs.release_type == 'nightly' && '-PversionName=nightly' || '' }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download compose-webview artifacts
|
- name: Download maven local
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: maven-local-composewebview
|
name: maven-local
|
||||||
path: ~/.m2/repository/io/github/kdroidfilter/
|
path: ~/.m2/repository/
|
||||||
|
|
||||||
- name: Download gradle-plugin artifacts
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: maven-local-gradle-plugin
|
|
||||||
path: ~/.m2/repository/dev/krtirtho/
|
|
||||||
|
|
||||||
- name: Install packaging tools
|
- name: Install packaging tools
|
||||||
run: |
|
run: |
|
||||||
@ -186,13 +178,13 @@ jobs:
|
|||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
|
|
||||||
- name: Package DEB
|
- name: Package DEB
|
||||||
run: ./gradlew :composeApp:packageReleaseDeb ${{ env.GRADLE_FLAGS }}
|
run: ./gradlew :composeApp:packageReleaseDeb
|
||||||
|
|
||||||
- name: Package RPM
|
- name: Package RPM
|
||||||
run: ./gradlew :composeApp:packageReleaseRpm ${{ env.GRADLE_FLAGS }}
|
run: ./gradlew :composeApp:packageReleaseRpm
|
||||||
|
|
||||||
- name: Package AppImage
|
- name: Package AppImage
|
||||||
run: ./gradlew :composeApp:packageReleaseAppImage ${{ env.GRADLE_FLAGS }}
|
run: ./gradlew :composeApp:packageReleaseAppImage
|
||||||
|
|
||||||
- name: Upload DEB
|
- name: Upload DEB
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
@ -216,22 +208,14 @@ jobs:
|
|||||||
name: Windows Desktop
|
name: Windows Desktop
|
||||||
needs: prepare-deps
|
needs: prepare-deps
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
env:
|
|
||||||
GRADLE_FLAGS: ${{ inputs.release_type == 'nightly' && '-PversionName=nightly' || '' }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download compose-webview artifacts
|
- name: Download maven local
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: maven-local-composewebview
|
name: maven-local
|
||||||
path: ~/.m2/repository/io/github/kdroidfilter/
|
path: ~/.m2/repository/
|
||||||
|
|
||||||
- name: Download gradle-plugin artifacts
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: maven-local-gradle-plugin
|
|
||||||
path: ~/.m2/repository/dev/krtirtho/
|
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
@ -243,10 +227,10 @@ jobs:
|
|||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v4
|
||||||
|
|
||||||
- name: Package MSI
|
- name: Package MSI
|
||||||
run: ./gradlew :composeApp:packageReleaseMsi ${{ env.GRADLE_FLAGS }}
|
run: ./gradlew :composeApp:packageReleaseMsi
|
||||||
|
|
||||||
- name: Package EXE
|
- name: Package EXE
|
||||||
run: ./gradlew :composeApp:packageReleaseExe ${{ env.GRADLE_FLAGS }}
|
run: ./gradlew :composeApp:packageReleaseExe
|
||||||
|
|
||||||
- name: Upload MSI
|
- name: Upload MSI
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
@ -264,22 +248,14 @@ jobs:
|
|||||||
name: macOS Desktop
|
name: macOS Desktop
|
||||||
needs: prepare-deps
|
needs: prepare-deps
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
env:
|
|
||||||
GRADLE_FLAGS: ${{ inputs.release_type == 'nightly' && '-PversionName=nightly' || '' }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download compose-webview artifacts
|
- name: Download maven local
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: maven-local-composewebview
|
name: maven-local
|
||||||
path: ~/.m2/repository/io/github/kdroidfilter/
|
path: ~/.m2/repository/
|
||||||
|
|
||||||
- name: Download gradle-plugin artifacts
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: maven-local-gradle-plugin
|
|
||||||
path: ~/.m2/repository/dev/krtirtho/
|
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
@ -294,7 +270,7 @@ jobs:
|
|||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
|
|
||||||
- name: Package DMG
|
- name: Package DMG
|
||||||
run: ./gradlew :composeApp:packageReleaseDmg ${{ env.GRADLE_FLAGS }}
|
run: ./gradlew :composeApp:packageReleaseDmg
|
||||||
|
|
||||||
- name: Upload DMG
|
- name: Upload DMG
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
@ -57,6 +57,8 @@ class MediaBrowseHelper(
|
|||||||
companion object {
|
companion object {
|
||||||
const val MEDIA_ID_ROOT = "root"
|
const val MEDIA_ID_ROOT = "root"
|
||||||
const val MEDIA_ID_BROWSE = "browse"
|
const val MEDIA_ID_BROWSE = "browse"
|
||||||
|
const val MEDIA_ID_BROWSE_GENRE = "browse:genre"
|
||||||
|
const val MEDIA_ID_BROWSE_FEATURED = "browse:featured"
|
||||||
const val MEDIA_ID_LIBRARY = "library"
|
const val MEDIA_ID_LIBRARY = "library"
|
||||||
const val MEDIA_ID_SAVED_PLAYLISTS = "library:playlists"
|
const val MEDIA_ID_SAVED_PLAYLISTS = "library:playlists"
|
||||||
const val MEDIA_ID_SAVED_ALBUMS = "library:albums"
|
const val MEDIA_ID_SAVED_ALBUMS = "library:albums"
|
||||||
@ -93,6 +95,11 @@ class MediaBrowseHelper(
|
|||||||
return when {
|
return when {
|
||||||
parentId == MEDIA_ID_ROOT -> buildRootChildren()
|
parentId == MEDIA_ID_ROOT -> buildRootChildren()
|
||||||
parentId == MEDIA_ID_BROWSE -> buildBrowseItems()
|
parentId == MEDIA_ID_BROWSE -> buildBrowseItems()
|
||||||
|
parentId == MEDIA_ID_BROWSE_FEATURED -> buildFeaturedItems()
|
||||||
|
parentId.startsWith("$MEDIA_ID_BROWSE_GENRE:") -> {
|
||||||
|
val genreId = parentId.removePrefix("$MEDIA_ID_BROWSE_GENRE:")
|
||||||
|
buildBrowseGenreItems(genreId)
|
||||||
|
}
|
||||||
parentId == MEDIA_ID_LIBRARY -> buildLibraryRootChildren()
|
parentId == MEDIA_ID_LIBRARY -> buildLibraryRootChildren()
|
||||||
parentId == MEDIA_ID_SAVED_PLAYLISTS -> buildSavedPlaylistsItems()
|
parentId == MEDIA_ID_SAVED_PLAYLISTS -> buildSavedPlaylistsItems()
|
||||||
parentId == MEDIA_ID_SAVED_ALBUMS -> buildSavedAlbumsItems()
|
parentId == MEDIA_ID_SAVED_ALBUMS -> buildSavedAlbumsItems()
|
||||||
@ -214,7 +221,6 @@ class MediaBrowseHelper(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun buildRootChildren(): List<MediaItem> {
|
private fun buildRootChildren(): List<MediaItem> {
|
||||||
val gridExtras = Bundle().apply { putInt(CONTENT_STYLE_BROWSABLE, CONTENT_STYLE_GRID) }
|
|
||||||
return listOf(
|
return listOf(
|
||||||
MediaItem.Builder()
|
MediaItem.Builder()
|
||||||
.setMediaId(MEDIA_ID_BROWSE)
|
.setMediaId(MEDIA_ID_BROWSE)
|
||||||
@ -223,7 +229,6 @@ class MediaBrowseHelper(
|
|||||||
.setTitle("Browse")
|
.setTitle("Browse")
|
||||||
.setIsBrowsable(true)
|
.setIsBrowsable(true)
|
||||||
.setIsPlayable(false)
|
.setIsPlayable(false)
|
||||||
.setExtras(gridExtras)
|
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.build(),
|
.build(),
|
||||||
@ -251,27 +256,67 @@ class MediaBrowseHelper(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun buildBrowseItems(): List<MediaItem> {
|
private suspend fun buildBrowseItems(): List<MediaItem> {
|
||||||
|
val gridExtras = Bundle().apply { putInt(CONTENT_STYLE_BROWSABLE, CONTENT_STYLE_GRID) }
|
||||||
|
val items = mutableListOf<MediaItem>()
|
||||||
|
|
||||||
|
items.add(
|
||||||
|
MediaItem.Builder()
|
||||||
|
.setMediaId(MEDIA_ID_BROWSE_FEATURED)
|
||||||
|
.setMediaMetadata(
|
||||||
|
MediaMetadata.Builder()
|
||||||
|
.setTitle("Featured")
|
||||||
|
.setIsBrowsable(true)
|
||||||
|
.setIsPlayable(false)
|
||||||
|
.setExtras(gridExtras)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
val genres = try {
|
||||||
|
homeScreenRepository.genres() ?: emptyList()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (genre in genres) {
|
||||||
|
items.add(
|
||||||
|
MediaItem.Builder()
|
||||||
|
.setMediaId("$MEDIA_ID_BROWSE_GENRE:${genre.id}")
|
||||||
|
.setMediaMetadata(
|
||||||
|
MediaMetadata.Builder()
|
||||||
|
.setTitle(genre.name)
|
||||||
|
.setIsBrowsable(true)
|
||||||
|
.setIsPlayable(false)
|
||||||
|
.setExtras(gridExtras)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun buildFeaturedItems(): List<MediaItem> {
|
||||||
val featured = try {
|
val featured = try {
|
||||||
homeScreenRepository.featuredItems() ?: emptyList()
|
homeScreenRepository.featuredItems() ?: emptyList()
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return featured.mapNotNull { item -> browseItemToMediaItem(item) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun buildBrowseGenreItems(genreId: String): List<MediaItem> {
|
||||||
val sections = try {
|
val sections = try {
|
||||||
homeScreenRepository.list()?.items ?: emptyList()
|
homeScreenRepository.list(genreId)?.items ?: emptyList()
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
val items = mutableListOf<MediaItem>()
|
val items = mutableListOf<MediaItem>()
|
||||||
|
|
||||||
if (featured.isNotEmpty()) {
|
|
||||||
for (item in featured) {
|
|
||||||
val mediaItem = browseItemToMediaItem(item) ?: continue
|
|
||||||
items.add(mediaItem.withGroupTitle("Featured"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (section in sections) {
|
for (section in sections) {
|
||||||
for (item in section.items) {
|
for (item in section.items) {
|
||||||
val mediaItem = browseItemToMediaItem(
|
val mediaItem = browseItemToMediaItem(
|
||||||
|
|||||||
@ -47,6 +47,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
@ -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 count = audioPlayer.player.mediaItemCount
|
||||||
val items = mutableListOf<Media3MediaItem>()
|
val items = mutableListOf<Media3MediaItem>()
|
||||||
for (i in 0 until count) {
|
for (i in 0 until count) {
|
||||||
@ -381,7 +382,7 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
|
|||||||
items.add(item)
|
items.add(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return items
|
items
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@ -125,12 +125,12 @@ private fun resolveTextFieldState(
|
|||||||
)
|
)
|
||||||
|
|
||||||
else -> ResolvedAutoCompleteTextFieldState(
|
else -> ResolvedAutoCompleteTextFieldState(
|
||||||
theme.background.pressed,
|
theme.background.normal,
|
||||||
theme.highlight.pressed,
|
theme.highlight.normal,
|
||||||
theme.shape.pressed,
|
theme.shape.normal,
|
||||||
theme.border.pressed,
|
theme.border.normal,
|
||||||
theme.shadow.pressed,
|
theme.shadow.normal,
|
||||||
theme.foreground.pressed
|
theme.foreground.normal
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -106,11 +106,11 @@ private fun resolveButtonState(
|
|||||||
)
|
)
|
||||||
|
|
||||||
else -> ResolvedButtonState(
|
else -> ResolvedButtonState(
|
||||||
style.colors.focused,
|
style.colors.normal,
|
||||||
style.shape.focused,
|
style.shape.normal,
|
||||||
style.shadow.focused,
|
style.shadow.normal,
|
||||||
style.border.focused,
|
style.border.normal,
|
||||||
style.padding.focused
|
style.padding.normal
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -311,21 +311,28 @@ fun GhostIconButton(
|
|||||||
val outlineStyle = baseTheme.iconButtons.outline
|
val outlineStyle = baseTheme.iconButtons.outline
|
||||||
val ghostStyle = theme ?: BaseUITheme.ButtonStyle(
|
val ghostStyle = theme ?: BaseUITheme.ButtonStyle(
|
||||||
colors = BaseUITheme.InteractionState(
|
colors = BaseUITheme.InteractionState(
|
||||||
|
normal = BaseUITheme.ButtonColors(
|
||||||
|
background = Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent)),
|
||||||
|
foreground = outlineStyle.colors.normal.foreground,
|
||||||
|
highlight = outlineStyle.colors.normal.highlight,
|
||||||
|
),
|
||||||
hovered = outlineStyle.colors.hovered,
|
hovered = outlineStyle.colors.hovered,
|
||||||
pressed = outlineStyle.colors.pressed,
|
pressed = outlineStyle.colors.pressed,
|
||||||
focused = BaseUITheme.ButtonColors(
|
focused = BaseUITheme.ButtonColors(
|
||||||
background = Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent)),
|
background = Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent)),
|
||||||
foreground = outlineStyle.colors.focused.foreground,
|
foreground = outlineStyle.colors.normal.foreground,
|
||||||
highlight = outlineStyle.colors.focused.highlight,
|
highlight = outlineStyle.colors.normal.highlight,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
shape = outlineStyle.shape,
|
shape = outlineStyle.shape,
|
||||||
shadow = BaseUITheme.InteractionState(
|
shadow = BaseUITheme.InteractionState(
|
||||||
|
normal = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent),
|
||||||
hovered = outlineStyle.shadow.hovered,
|
hovered = outlineStyle.shadow.hovered,
|
||||||
pressed = outlineStyle.shadow.pressed,
|
pressed = outlineStyle.shadow.pressed,
|
||||||
focused = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent),
|
focused = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent),
|
||||||
),
|
),
|
||||||
border = BaseUITheme.InteractionState(
|
border = BaseUITheme.InteractionState(
|
||||||
|
normal = BaseUITheme.Border(Color.Transparent, 0.dp),
|
||||||
hovered = outlineStyle.border.hovered,
|
hovered = outlineStyle.border.hovered,
|
||||||
pressed = outlineStyle.border.pressed,
|
pressed = outlineStyle.border.pressed,
|
||||||
focused = BaseUITheme.Border(Color.Transparent, 0.dp),
|
focused = BaseUITheme.Border(Color.Transparent, 0.dp),
|
||||||
|
|||||||
@ -85,7 +85,7 @@ private fun resolveCheckBoxState(
|
|||||||
return when {
|
return when {
|
||||||
isPressed -> ResolvedCheckBoxState(style.colors.pressed, style.shape.pressed, style.shadow.pressed, style.border.pressed)
|
isPressed -> ResolvedCheckBoxState(style.colors.pressed, style.shape.pressed, style.shadow.pressed, style.border.pressed)
|
||||||
isHovered -> ResolvedCheckBoxState(style.colors.hovered, style.shape.hovered, style.shadow.hovered, style.border.hovered)
|
isHovered -> ResolvedCheckBoxState(style.colors.hovered, style.shape.hovered, style.shadow.hovered, style.border.hovered)
|
||||||
else -> ResolvedCheckBoxState(style.colors.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 {
|
return when {
|
||||||
isPressed -> ResolvedChipState(style.colors.pressed, style.shape.pressed, style.shadow.pressed, style.border.pressed, style.padding.pressed)
|
isPressed -> ResolvedChipState(style.colors.pressed, style.shape.pressed, style.shadow.pressed, style.border.pressed, style.padding.pressed)
|
||||||
isHovered -> ResolvedChipState(style.colors.hovered, style.shape.hovered, style.shadow.hovered, style.border.hovered, style.padding.hovered)
|
isHovered -> ResolvedChipState(style.colors.hovered, style.shape.hovered, style.shadow.hovered, style.border.hovered, style.padding.hovered)
|
||||||
else -> ResolvedChipState(style.colors.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,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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -95,22 +95,22 @@ fun Slider(
|
|||||||
val stateColor = when {
|
val stateColor = when {
|
||||||
isPressed -> sliderTheme.trackActiveColor.pressed
|
isPressed -> sliderTheme.trackActiveColor.pressed
|
||||||
isHovered -> sliderTheme.trackActiveColor.hovered
|
isHovered -> sliderTheme.trackActiveColor.hovered
|
||||||
else -> sliderTheme.trackActiveColor.focused
|
else -> sliderTheme.trackActiveColor.normal
|
||||||
}
|
}
|
||||||
val inactiveColor = when {
|
val inactiveColor = when {
|
||||||
isPressed -> sliderTheme.trackInactiveColor.pressed
|
isPressed -> sliderTheme.trackInactiveColor.pressed
|
||||||
isHovered -> sliderTheme.trackInactiveColor.hovered
|
isHovered -> sliderTheme.trackInactiveColor.hovered
|
||||||
else -> sliderTheme.trackInactiveColor.focused
|
else -> sliderTheme.trackInactiveColor.normal
|
||||||
}
|
}
|
||||||
val thumbColor = when {
|
val thumbColor = when {
|
||||||
isPressed -> sliderTheme.thumbColor.pressed
|
isPressed -> sliderTheme.thumbColor.pressed
|
||||||
isHovered -> sliderTheme.thumbColor.hovered
|
isHovered -> sliderTheme.thumbColor.hovered
|
||||||
else -> sliderTheme.thumbColor.focused
|
else -> sliderTheme.thumbColor.normal
|
||||||
}
|
}
|
||||||
val thumbShadowState = when {
|
val thumbShadowState = when {
|
||||||
isPressed -> sliderTheme.thumbShadow.pressed
|
isPressed -> sliderTheme.thumbShadow.pressed
|
||||||
isHovered -> sliderTheme.thumbShadow.hovered
|
isHovered -> sliderTheme.thumbShadow.hovered
|
||||||
else -> sliderTheme.thumbShadow.focused
|
else -> sliderTheme.thumbShadow.normal
|
||||||
}
|
}
|
||||||
|
|
||||||
val trackActiveColor = if (enabled) stateColor else stateColor.copy(alpha = 0.38f)
|
val trackActiveColor = if (enabled) stateColor else stateColor.copy(alpha = 0.38f)
|
||||||
|
|||||||
@ -81,7 +81,7 @@ private fun resolveTextFieldState(
|
|||||||
return when {
|
return when {
|
||||||
isFocused -> ResolvedTextFieldState(theme.background.focused, theme.highlight.focused, theme.shape.focused, theme.border.focused, theme.shadow.focused, theme.foreground.focused)
|
isFocused -> ResolvedTextFieldState(theme.background.focused, theme.highlight.focused, theme.shape.focused, theme.border.focused, theme.shadow.focused, theme.foreground.focused)
|
||||||
isHovered -> ResolvedTextFieldState(theme.background.hovered, theme.highlight.hovered, theme.shape.hovered, theme.border.hovered, theme.shadow.hovered, theme.foreground.hovered)
|
isHovered -> ResolvedTextFieldState(theme.background.hovered, theme.highlight.hovered, theme.shape.hovered, theme.border.hovered, theme.shadow.hovered, theme.foreground.hovered)
|
||||||
else -> ResolvedTextFieldState(theme.background.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -54,19 +54,21 @@ data class BaseUITheme(
|
|||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
data class InteractionState<T>(
|
data class InteractionState<T>(
|
||||||
|
val normal: T,
|
||||||
val hovered: T,
|
val hovered: T,
|
||||||
val pressed: T,
|
val pressed: T,
|
||||||
val focused: T,
|
val focused: T,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun <T> fromSingleValue(value: T): InteractionState<T> {
|
fun <T> fromSingleValue(value: T): InteractionState<T> {
|
||||||
return InteractionState(value, value, value)
|
return InteractionState(value, value, value, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
data class AdvancedInteractionState<T>(
|
data class AdvancedInteractionState<T>(
|
||||||
|
val normal: T,
|
||||||
val hovered: T,
|
val hovered: T,
|
||||||
val pressed: T,
|
val pressed: T,
|
||||||
val focused: T,
|
val focused: T,
|
||||||
@ -75,7 +77,7 @@ data class BaseUITheme(
|
|||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun <T> fromSingleValue(value: T): AdvancedInteractionState<T> {
|
fun <T> fromSingleValue(value: T): AdvancedInteractionState<T> {
|
||||||
return AdvancedInteractionState(value, value, value, value, value)
|
return AdvancedInteractionState(value, value, value, value, value, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -200,23 +202,30 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
|
|
||||||
val defaultShape = RoundedCornerShape(14.dp)
|
val defaultShape = RoundedCornerShape(14.dp)
|
||||||
val defaultShapeState =
|
val defaultShapeState =
|
||||||
BaseUITheme.InteractionState<Shape>(defaultShape, defaultShape, defaultShape)
|
BaseUITheme.InteractionState<Shape>(defaultShape, defaultShape, defaultShape, defaultShape)
|
||||||
val noBorder = BaseUITheme.Border(Color.Transparent, 0.dp)
|
val noBorder = BaseUITheme.Border(Color.Transparent, 0.dp)
|
||||||
val noBorderState = BaseUITheme.InteractionState(noBorder, noBorder, noBorder)
|
val noBorderState = BaseUITheme.InteractionState(noBorder, noBorder, noBorder, noBorder)
|
||||||
val noShadow = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent)
|
val noShadow = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent)
|
||||||
val noShadowState = BaseUITheme.InteractionState(noShadow, noShadow, noShadow)
|
val noShadowState = BaseUITheme.InteractionState(noShadow, noShadow, noShadow, noShadow)
|
||||||
val defaultButtonPadding = BaseUITheme.InteractionState(
|
val defaultButtonPadding = BaseUITheme.InteractionState(
|
||||||
PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
||||||
PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
||||||
PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
||||||
|
PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
||||||
)
|
)
|
||||||
val iconButtonPadding = BaseUITheme.InteractionState(
|
val iconButtonPadding = BaseUITheme.InteractionState(
|
||||||
PaddingValues(8.dp),
|
PaddingValues(8.dp),
|
||||||
PaddingValues(8.dp),
|
PaddingValues(8.dp),
|
||||||
PaddingValues(8.dp),
|
PaddingValues(8.dp),
|
||||||
|
PaddingValues(8.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
val outlineColors = BaseUITheme.InteractionState(
|
val outlineColors = BaseUITheme.InteractionState(
|
||||||
|
normal = BaseUITheme.ButtonColors(
|
||||||
|
background = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||||
|
foreground = scheme.onSurface,
|
||||||
|
highlight = highlight,
|
||||||
|
),
|
||||||
hovered = BaseUITheme.ButtonColors(
|
hovered = BaseUITheme.ButtonColors(
|
||||||
background = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
background = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||||
foreground = scheme.onSurface,
|
foreground = scheme.onSurface,
|
||||||
@ -234,6 +243,12 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
val outlineShadow = BaseUITheme.InteractionState(
|
val outlineShadow = BaseUITheme.InteractionState(
|
||||||
|
normal = BaseUITheme.Shadow(
|
||||||
|
6.dp,
|
||||||
|
true,
|
||||||
|
shadowColor.copy(alpha = 0.15f),
|
||||||
|
shadowColor.copy(alpha = 0.18f)
|
||||||
|
),
|
||||||
hovered = BaseUITheme.Shadow(
|
hovered = BaseUITheme.Shadow(
|
||||||
9.dp,
|
9.dp,
|
||||||
true,
|
true,
|
||||||
@ -254,12 +269,18 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
val outlineBorder = BaseUITheme.InteractionState(
|
val outlineBorder = BaseUITheme.InteractionState(
|
||||||
|
normal = BaseUITheme.Border(border, 0.5.dp),
|
||||||
hovered = BaseUITheme.Border(border, 0.5.dp),
|
hovered = BaseUITheme.Border(border, 0.5.dp),
|
||||||
pressed = BaseUITheme.Border(border.copy(alpha = 0.7f), 0.5.dp),
|
pressed = BaseUITheme.Border(border.copy(alpha = 0.7f), 0.5.dp),
|
||||||
focused = BaseUITheme.Border(border, 0.5.dp),
|
focused = BaseUITheme.Border(border, 0.5.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
val primaryColors = BaseUITheme.InteractionState(
|
val primaryColors = BaseUITheme.InteractionState(
|
||||||
|
normal = BaseUITheme.ButtonColors(
|
||||||
|
background = Brush.verticalGradient(listOf(scheme.primary, scheme.primary)),
|
||||||
|
foreground = scheme.onPrimary,
|
||||||
|
highlight = Color.White.copy(alpha = 0.25f),
|
||||||
|
),
|
||||||
hovered = BaseUITheme.ButtonColors(
|
hovered = BaseUITheme.ButtonColors(
|
||||||
background = Brush.verticalGradient(listOf(scheme.primary, scheme.primary)),
|
background = Brush.verticalGradient(listOf(scheme.primary, scheme.primary)),
|
||||||
foreground = scheme.onPrimary,
|
foreground = scheme.onPrimary,
|
||||||
@ -282,6 +303,12 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
val primaryShadow = BaseUITheme.InteractionState(
|
val primaryShadow = BaseUITheme.InteractionState(
|
||||||
|
normal = BaseUITheme.Shadow(
|
||||||
|
6.dp,
|
||||||
|
true,
|
||||||
|
scheme.primary.copy(alpha = 0.35f),
|
||||||
|
scheme.primary.copy(alpha = 0.4f)
|
||||||
|
),
|
||||||
hovered = BaseUITheme.Shadow(
|
hovered = BaseUITheme.Shadow(
|
||||||
9.dp,
|
9.dp,
|
||||||
true,
|
true,
|
||||||
@ -302,12 +329,23 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
val primaryBorder = BaseUITheme.InteractionState(
|
val primaryBorder = BaseUITheme.InteractionState(
|
||||||
|
normal = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
||||||
hovered = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
hovered = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
||||||
pressed = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
pressed = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
||||||
focused = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
focused = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
val secondaryColors = BaseUITheme.InteractionState(
|
val secondaryColors = BaseUITheme.InteractionState(
|
||||||
|
normal = BaseUITheme.ButtonColors(
|
||||||
|
background = Brush.verticalGradient(
|
||||||
|
listOf(
|
||||||
|
secondaryContainer,
|
||||||
|
secondaryContainer.copy(alpha = if (secondaryContainer.luminance() > 0.5f) 0.92f else 1f)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
foreground = scheme.onSecondaryContainer,
|
||||||
|
highlight = secondaryHighlight,
|
||||||
|
),
|
||||||
hovered = BaseUITheme.ButtonColors(
|
hovered = BaseUITheme.ButtonColors(
|
||||||
background = Brush.verticalGradient(
|
background = Brush.verticalGradient(
|
||||||
listOf(
|
listOf(
|
||||||
@ -340,6 +378,12 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
val secondaryShadow = BaseUITheme.InteractionState(
|
val secondaryShadow = BaseUITheme.InteractionState(
|
||||||
|
normal = BaseUITheme.Shadow(
|
||||||
|
6.dp,
|
||||||
|
true,
|
||||||
|
shadowColor.copy(alpha = 0.15f),
|
||||||
|
shadowColor.copy(alpha = 0.18f)
|
||||||
|
),
|
||||||
hovered = BaseUITheme.Shadow(
|
hovered = BaseUITheme.Shadow(
|
||||||
9.dp,
|
9.dp,
|
||||||
true,
|
true,
|
||||||
@ -360,12 +404,18 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
val secondaryBorder = BaseUITheme.InteractionState(
|
val secondaryBorder = BaseUITheme.InteractionState(
|
||||||
|
normal = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp),
|
||||||
hovered = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp),
|
hovered = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp),
|
||||||
pressed = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp),
|
pressed = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp),
|
||||||
focused = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp),
|
focused = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
val ghostColors = BaseUITheme.InteractionState(
|
val ghostColors = BaseUITheme.InteractionState(
|
||||||
|
normal = BaseUITheme.ButtonColors(
|
||||||
|
background = Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent)),
|
||||||
|
foreground = scheme.onSurface,
|
||||||
|
highlight = Color.Transparent,
|
||||||
|
),
|
||||||
hovered = BaseUITheme.ButtonColors(
|
hovered = BaseUITheme.ButtonColors(
|
||||||
background = Brush.verticalGradient(
|
background = Brush.verticalGradient(
|
||||||
listOf(
|
listOf(
|
||||||
@ -447,6 +497,7 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val textFieldForeground = BaseUITheme.InteractionState(
|
val textFieldForeground = BaseUITheme.InteractionState(
|
||||||
|
normal = scheme.onSurface,
|
||||||
hovered = scheme.onSurface,
|
hovered = scheme.onSurface,
|
||||||
pressed = scheme.onSurface,
|
pressed = scheme.onSurface,
|
||||||
focused = scheme.onSurface,
|
focused = scheme.onSurface,
|
||||||
@ -454,17 +505,18 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
|
|
||||||
val checkBoxShape = RoundedCornerShape(6.dp)
|
val checkBoxShape = RoundedCornerShape(6.dp)
|
||||||
val checkBoxShapeState =
|
val checkBoxShapeState =
|
||||||
BaseUITheme.InteractionState<Shape>(checkBoxShape, checkBoxShape, checkBoxShape)
|
BaseUITheme.InteractionState<Shape>(checkBoxShape, checkBoxShape, checkBoxShape, checkBoxShape)
|
||||||
val noPadding =
|
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 chipTabShape = RoundedCornerShape(10.dp)
|
||||||
val chipTabShapeState =
|
val chipTabShapeState =
|
||||||
BaseUITheme.InteractionState<Shape>(chipTabShape, chipTabShape, chipTabShape)
|
BaseUITheme.InteractionState<Shape>(chipTabShape, chipTabShape, chipTabShape, chipTabShape)
|
||||||
val chipTabPadding = BaseUITheme.InteractionState(
|
val chipTabPadding = BaseUITheme.InteractionState(
|
||||||
PaddingValues(horizontal = 14.dp, vertical = 4.dp),
|
PaddingValues(horizontal = 14.dp, vertical = 4.dp),
|
||||||
PaddingValues(horizontal = 14.dp, vertical = 4.dp),
|
PaddingValues(horizontal = 14.dp, vertical = 4.dp),
|
||||||
PaddingValues(horizontal = 14.dp, vertical = 4.dp),
|
PaddingValues(horizontal = 14.dp, vertical = 4.dp),
|
||||||
|
PaddingValues(horizontal = 14.dp, vertical = 4.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
return BaseUITheme(
|
return BaseUITheme(
|
||||||
@ -482,26 +534,36 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
),
|
),
|
||||||
textField = TextFieldTheme(
|
textField = TextFieldTheme(
|
||||||
background = BaseUITheme.InteractionState(
|
background = BaseUITheme.InteractionState(
|
||||||
|
normal = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||||
hovered = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
hovered = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||||
pressed = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
pressed = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||||
focused = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
focused = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||||
),
|
),
|
||||||
highlight = BaseUITheme.InteractionState(
|
highlight = BaseUITheme.InteractionState(
|
||||||
|
normal = highlight,
|
||||||
hovered = highlight,
|
hovered = highlight,
|
||||||
pressed = highlight,
|
pressed = highlight,
|
||||||
focused = highlight,
|
focused = highlight,
|
||||||
),
|
),
|
||||||
shape = BaseUITheme.InteractionState(
|
shape = BaseUITheme.InteractionState(
|
||||||
|
normal = defaultShape,
|
||||||
hovered = defaultShape,
|
hovered = defaultShape,
|
||||||
pressed = defaultShape,
|
pressed = defaultShape,
|
||||||
focused = defaultShape,
|
focused = defaultShape,
|
||||||
),
|
),
|
||||||
border = BaseUITheme.InteractionState(
|
border = BaseUITheme.InteractionState(
|
||||||
|
normal = BaseUITheme.Border(border.copy(alpha = 0.85f), 0.5.dp),
|
||||||
hovered = BaseUITheme.Border(border.copy(alpha = 0.85f), 0.5.dp),
|
hovered = BaseUITheme.Border(border.copy(alpha = 0.85f), 0.5.dp),
|
||||||
pressed = BaseUITheme.Border(border, 0.5.dp),
|
pressed = BaseUITheme.Border(border, 0.5.dp),
|
||||||
focused = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
focused = BaseUITheme.Border(scheme.primary, 0.5.dp),
|
||||||
),
|
),
|
||||||
shadow = BaseUITheme.InteractionState(
|
shadow = BaseUITheme.InteractionState(
|
||||||
|
normal = BaseUITheme.Shadow(
|
||||||
|
9.dp,
|
||||||
|
true,
|
||||||
|
shadowColor.copy(alpha = 0.22f),
|
||||||
|
shadowColor.copy(alpha = 0.26f)
|
||||||
|
),
|
||||||
hovered = BaseUITheme.Shadow(
|
hovered = BaseUITheme.Shadow(
|
||||||
9.dp,
|
9.dp,
|
||||||
true,
|
true,
|
||||||
@ -540,21 +602,30 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
),
|
),
|
||||||
slider = SliderTheme(
|
slider = SliderTheme(
|
||||||
trackActiveColor = BaseUITheme.InteractionState(
|
trackActiveColor = BaseUITheme.InteractionState(
|
||||||
|
normal = scheme.primary,
|
||||||
hovered = scheme.primary,
|
hovered = scheme.primary,
|
||||||
pressed = scheme.primary,
|
pressed = scheme.primary,
|
||||||
focused = scheme.primary,
|
focused = scheme.primary,
|
||||||
),
|
),
|
||||||
trackInactiveColor = BaseUITheme.InteractionState(
|
trackInactiveColor = BaseUITheme.InteractionState(
|
||||||
|
normal = border.copy(alpha = 0.6f),
|
||||||
hovered = border.copy(alpha = 0.6f),
|
hovered = border.copy(alpha = 0.6f),
|
||||||
pressed = border.copy(alpha = 0.6f),
|
pressed = border.copy(alpha = 0.6f),
|
||||||
focused = border.copy(alpha = 0.6f),
|
focused = border.copy(alpha = 0.6f),
|
||||||
),
|
),
|
||||||
thumbColor = BaseUITheme.InteractionState(
|
thumbColor = BaseUITheme.InteractionState(
|
||||||
|
normal = scheme.primary,
|
||||||
hovered = scheme.primary,
|
hovered = scheme.primary,
|
||||||
pressed = scheme.primary,
|
pressed = scheme.primary,
|
||||||
focused = scheme.primary,
|
focused = scheme.primary,
|
||||||
),
|
),
|
||||||
thumbShadow = BaseUITheme.InteractionState(
|
thumbShadow = BaseUITheme.InteractionState(
|
||||||
|
normal = BaseUITheme.Shadow(
|
||||||
|
5.dp,
|
||||||
|
true,
|
||||||
|
scheme.primary.copy(alpha = 0.35f),
|
||||||
|
scheme.primary.copy(alpha = 0.4f)
|
||||||
|
),
|
||||||
hovered = BaseUITheme.Shadow(
|
hovered = BaseUITheme.Shadow(
|
||||||
8.dp,
|
8.dp,
|
||||||
true,
|
true,
|
||||||
@ -582,6 +653,12 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
colors = primaryColors,
|
colors = primaryColors,
|
||||||
shape = checkBoxShapeState,
|
shape = checkBoxShapeState,
|
||||||
shadow = BaseUITheme.InteractionState(
|
shadow = BaseUITheme.InteractionState(
|
||||||
|
normal = BaseUITheme.Shadow(
|
||||||
|
6.dp,
|
||||||
|
true,
|
||||||
|
scheme.primary.copy(alpha = 0.3f),
|
||||||
|
scheme.primary.copy(alpha = 0.35f)
|
||||||
|
),
|
||||||
hovered = BaseUITheme.Shadow(
|
hovered = BaseUITheme.Shadow(
|
||||||
8.dp,
|
8.dp,
|
||||||
true,
|
true,
|
||||||
@ -608,6 +685,12 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
colors = outlineColors,
|
colors = outlineColors,
|
||||||
shape = checkBoxShapeState,
|
shape = checkBoxShapeState,
|
||||||
shadow = BaseUITheme.InteractionState(
|
shadow = BaseUITheme.InteractionState(
|
||||||
|
normal = BaseUITheme.Shadow(
|
||||||
|
3.dp,
|
||||||
|
true,
|
||||||
|
shadowColor.copy(alpha = 0.15f),
|
||||||
|
shadowColor.copy(alpha = 0.18f)
|
||||||
|
),
|
||||||
hovered = BaseUITheme.Shadow(
|
hovered = BaseUITheme.Shadow(
|
||||||
5.dp,
|
5.dp,
|
||||||
true,
|
true,
|
||||||
@ -662,6 +745,7 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
),
|
),
|
||||||
listRowTile = BaseUITheme.ListRowTheme(
|
listRowTile = BaseUITheme.ListRowTheme(
|
||||||
background = BaseUITheme.AdvancedInteractionState(
|
background = BaseUITheme.AdvancedInteractionState(
|
||||||
|
normal = Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent)),
|
||||||
hovered = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
hovered = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||||
pressed = Brush.verticalGradient(listOf(containerPressed, containerPressed)),
|
pressed = Brush.verticalGradient(listOf(containerPressed, containerPressed)),
|
||||||
focused = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
focused = Brush.verticalGradient(listOf(containerLighter, containerDarker)),
|
||||||
@ -679,6 +763,7 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
shape = BaseUITheme.AdvancedInteractionState(
|
shape = BaseUITheme.AdvancedInteractionState(
|
||||||
|
normal = RoundedCornerShape(8.dp),
|
||||||
hovered = RoundedCornerShape(8.dp),
|
hovered = RoundedCornerShape(8.dp),
|
||||||
pressed = RoundedCornerShape(8.dp),
|
pressed = RoundedCornerShape(8.dp),
|
||||||
focused = RoundedCornerShape(8.dp),
|
focused = RoundedCornerShape(8.dp),
|
||||||
@ -686,6 +771,7 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
disabled = RoundedCornerShape(8.dp),
|
disabled = RoundedCornerShape(8.dp),
|
||||||
),
|
),
|
||||||
border = BaseUITheme.AdvancedInteractionState(
|
border = BaseUITheme.AdvancedInteractionState(
|
||||||
|
normal = BaseUITheme.Border(Color.Transparent, 0.dp),
|
||||||
hovered = BaseUITheme.Border(Color.Transparent, 0.dp),
|
hovered = BaseUITheme.Border(Color.Transparent, 0.dp),
|
||||||
pressed = BaseUITheme.Border(Color.Transparent, 0.dp),
|
pressed = BaseUITheme.Border(Color.Transparent, 0.dp),
|
||||||
focused = BaseUITheme.Border(Color.Transparent, 0.dp),
|
focused = BaseUITheme.Border(Color.Transparent, 0.dp),
|
||||||
@ -693,6 +779,7 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
disabled = BaseUITheme.Border(Color.Transparent, 0.dp),
|
disabled = BaseUITheme.Border(Color.Transparent, 0.dp),
|
||||||
),
|
),
|
||||||
shadow = BaseUITheme.AdvancedInteractionState(
|
shadow = BaseUITheme.AdvancedInteractionState(
|
||||||
|
normal = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent),
|
||||||
hovered = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent),
|
hovered = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent),
|
||||||
pressed = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent),
|
pressed = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent),
|
||||||
focused = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent),
|
focused = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent),
|
||||||
@ -701,6 +788,7 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
),
|
),
|
||||||
padding = PaddingValues(horizontal = 8.dp, vertical = 6.dp),
|
padding = PaddingValues(horizontal = 8.dp, vertical = 6.dp),
|
||||||
textStyle = BaseUITheme.AdvancedInteractionState(
|
textStyle = BaseUITheme.AdvancedInteractionState(
|
||||||
|
normal = TextStyle.Default,
|
||||||
hovered = TextStyle.Default,
|
hovered = TextStyle.Default,
|
||||||
pressed = TextStyle.Default,
|
pressed = TextStyle.Default,
|
||||||
focused = TextStyle.Default,
|
focused = TextStyle.Default,
|
||||||
@ -708,6 +796,7 @@ fun rememberBaseUITheme(): BaseUITheme {
|
|||||||
disabled = TextStyle.Default,
|
disabled = TextStyle.Default,
|
||||||
),
|
),
|
||||||
foreground = BaseUITheme.AdvancedInteractionState(
|
foreground = BaseUITheme.AdvancedInteractionState(
|
||||||
|
normal = scheme.onSurface,
|
||||||
hovered = scheme.onSurface,
|
hovered = scheme.onSurface,
|
||||||
pressed = scheme.onSurface,
|
pressed = scheme.onSurface,
|
||||||
focused = scheme.onSurface,
|
focused = scheme.onSurface,
|
||||||
@ -766,6 +855,7 @@ fun invertedButtonStyle(): BaseUITheme.ButtonStyle {
|
|||||||
|
|
||||||
return BaseUITheme.ButtonStyle(
|
return BaseUITheme.ButtonStyle(
|
||||||
colors = BaseUITheme.InteractionState(
|
colors = BaseUITheme.InteractionState(
|
||||||
|
normal = invertedColors,
|
||||||
hovered = invertedColors,
|
hovered = invertedColors,
|
||||||
pressed = pressedColors,
|
pressed = pressedColors,
|
||||||
focused = invertedColors,
|
focused = invertedColors,
|
||||||
|
|||||||
@ -562,7 +562,7 @@ private fun TrackListRow(
|
|||||||
isSelected -> rowTheme.background.selected
|
isSelected -> rowTheme.background.selected
|
||||||
isCurrentTrack && isCurrentTrackPlaying -> rowTheme.background.hovered
|
isCurrentTrack && isCurrentTrackPlaying -> rowTheme.background.hovered
|
||||||
isCurrentTrack -> rowTheme.background.focused
|
isCurrentTrack -> rowTheme.background.focused
|
||||||
else -> Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent))
|
else -> rowTheme.background.normal
|
||||||
}
|
}
|
||||||
val highlightColor = Color.White.copy(
|
val highlightColor = Color.White.copy(
|
||||||
alpha = if (isLight) 0.9f else 0.06f
|
alpha = if (isLight) 0.9f else 0.06f
|
||||||
|
|||||||
@ -36,12 +36,8 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.FilledIconButton
|
|
||||||
import androidx.compose.material3.FilledTonalButton
|
|
||||||
import androidx.compose.material3.FilledTonalIconButton
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
@ -49,7 +45,6 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@ -61,10 +56,6 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import compose.icons.FeatherIcons
|
|
||||||
import compose.icons.feathericons.Play
|
|
||||||
import compose.icons.feathericons.PlusSquare
|
|
||||||
import compose.icons.feathericons.User
|
|
||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.album.MetadataAlbum
|
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.album.MetadataAlbum
|
||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtist
|
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtist
|
||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.playlist.MetadataPlaylist
|
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.playlist.MetadataPlaylist
|
||||||
@ -76,6 +67,10 @@ import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
|||||||
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
||||||
import dev.krtirtho.spotube.core.navigation.Routes
|
import dev.krtirtho.spotube.core.navigation.Routes
|
||||||
import dev.krtirtho.spotube.core.share.ShareService
|
import dev.krtirtho.spotube.core.share.ShareService
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.PrimaryIconButton
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.SecondaryButton
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.SecondaryIconButton
|
||||||
import dev.krtirtho.spotube.core.ui.component.AlbumCard
|
import dev.krtirtho.spotube.core.ui.component.AlbumCard
|
||||||
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
|
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
|
||||||
import dev.krtirtho.spotube.core.ui.component.ArtistCard
|
import dev.krtirtho.spotube.core.ui.component.ArtistCard
|
||||||
@ -89,6 +84,10 @@ import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
|
|||||||
import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer
|
import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer
|
||||||
import dev.krtirtho.spotube.core.ui.misc.shimmerApply
|
import dev.krtirtho.spotube.core.ui.misc.shimmerApply
|
||||||
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
||||||
|
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||||
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
||||||
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxPlay
|
||||||
|
import dev.krtirtho.spotube.resources.iconsax.User
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
@ -349,7 +348,7 @@ private fun ArtistErrorContent(
|
|||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
)
|
)
|
||||||
Button(onClick = onRetry) {
|
PrimaryButton(onClick = onRetry) {
|
||||||
Text("Retry")
|
Text("Retry")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -452,7 +451,7 @@ private fun ArtistAvatar(
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = FeatherIcons.User,
|
imageVector = Iconsax.User,
|
||||||
contentDescription = artist.name,
|
contentDescription = artist.name,
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.size(size * 0.4f),
|
modifier = Modifier.size(size * 0.4f),
|
||||||
@ -516,11 +515,11 @@ private fun ArtistHeaderActions(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
if (isSaved) {
|
if (isSaved) {
|
||||||
FilledTonalButton(onClick = onFollowClick) {
|
SecondaryButton(onClick = onFollowClick) {
|
||||||
Text("Following", modifier = Modifier.width(65.dp), textAlign = TextAlign.Center)
|
Text("Following", modifier = Modifier.width(65.dp), textAlign = TextAlign.Center)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Button(onClick = onFollowClick) {
|
PrimaryButton(onClick = onFollowClick) {
|
||||||
Text("Follow", modifier = Modifier.width(65.dp), textAlign = TextAlign.Center)
|
Text("Follow", modifier = Modifier.width(65.dp), textAlign = TextAlign.Center)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -546,15 +545,15 @@ private fun TopTracksHeader(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
FilledIconButton(onClick = onPlay) {
|
PrimaryIconButton(onClick = onPlay) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = FeatherIcons.Play,
|
imageVector = Iconsax.IconsaxPlay,
|
||||||
contentDescription = "Play top tracks",
|
contentDescription = "Play top tracks",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
FilledTonalIconButton(onClick = onAddToQueue) {
|
SecondaryIconButton(onClick = onAddToQueue) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = FeatherIcons.PlusSquare,
|
imageVector = Iconsax.IconsaxAddSquare,
|
||||||
contentDescription = "Add top tracks to queue",
|
contentDescription = "Add top tracks to queue",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,8 @@ package dev.krtirtho.spotube.modules.shell.player_queue
|
|||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.hoverable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@ -53,22 +55,28 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import compose.icons.FeatherIcons
|
|
||||||
import compose.icons.feathericons.MoreVertical
|
|
||||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
||||||
import dev.krtirtho.spotube.core.di.rememberLogger
|
import dev.krtirtho.spotube.core.di.rememberLogger
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.Card
|
||||||
import dev.krtirtho.spotube.core.ui.base.GhostIconButton
|
import dev.krtirtho.spotube.core.ui.base.GhostIconButton
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.IconButton
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.ListRowTile
|
||||||
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
||||||
import dev.krtirtho.spotube.core.ui.base.SecondaryIconButton
|
import dev.krtirtho.spotube.core.ui.base.SecondaryIconButton
|
||||||
import dev.krtirtho.spotube.core.ui.base.TextField
|
import dev.krtirtho.spotube.core.ui.base.TextField
|
||||||
import dev.krtirtho.spotube.core.ui.base.copyShape
|
import dev.krtirtho.spotube.core.ui.base.copyShape
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.highlight
|
||||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||||
|
import dev.krtirtho.spotube.resources.iconsax.Iconsax3DotsMore
|
||||||
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxDragHandle
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxFilterSearch
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxFilterSearch
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicSquareRemove
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicSquareRemove
|
||||||
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxSetting
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import sh.calvin.reorderable.ReorderableItem
|
import sh.calvin.reorderable.ReorderableItem
|
||||||
@ -188,9 +196,9 @@ fun PlayerQueueContent(
|
|||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
SecondaryIconButton(
|
IconButton(
|
||||||
onClick = viewModel::clearQueue,
|
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")
|
Icon(Iconsax.IconsaxTrash, contentDescription = "Clear Queue")
|
||||||
}
|
}
|
||||||
@ -203,19 +211,17 @@ fun PlayerQueueContent(
|
|||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
Card {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
contentPadding = PaddingValues(bottom = 8.dp),
|
contentPadding = PaddingValues(bottom = 8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
|
||||||
) {
|
) {
|
||||||
items(displayList, key = { item -> item.id }) { item ->
|
items(displayList, key = { item -> item.id }) { item ->
|
||||||
ReorderableItem(reorderableLazyListState, key = item.id) { isDragging ->
|
ReorderableItem(reorderableLazyListState, key = item.id) { isDragging ->
|
||||||
val elevation by animateDpAsState(if (isDragging) 8.dp else 0.dp)
|
val elevation by animateDpAsState(if (isDragging) 8.dp else 0.dp)
|
||||||
QueueItemRow(
|
QueueItemRow(
|
||||||
item = item,
|
item = item,
|
||||||
isDragging = isDragging,
|
|
||||||
elevation = elevation,
|
|
||||||
reorderScope = if (isFiltered) null else this,
|
reorderScope = if (isFiltered) null else this,
|
||||||
onPlayClick = { viewModel.playQueueItem(item.originalIndex) },
|
onPlayClick = { viewModel.playQueueItem(item.originalIndex) },
|
||||||
onRemoveClick = { viewModel.removeQueueItem(item.originalIndex) },
|
onRemoveClick = { viewModel.removeQueueItem(item.originalIndex) },
|
||||||
@ -227,13 +233,12 @@ fun PlayerQueueContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun QueueItemRow(
|
private fun QueueItemRow(
|
||||||
item: QueueItemUi,
|
item: QueueItemUi,
|
||||||
isDragging: Boolean,
|
|
||||||
elevation: androidx.compose.ui.unit.Dp,
|
|
||||||
reorderScope: sh.calvin.reorderable.ReorderableCollectionItemScope?,
|
reorderScope: sh.calvin.reorderable.ReorderableCollectionItemScope?,
|
||||||
onPlayClick: () -> Unit,
|
onPlayClick: () -> Unit,
|
||||||
onRemoveClick: () -> Unit,
|
onRemoveClick: () -> Unit,
|
||||||
@ -241,30 +246,18 @@ private fun QueueItemRow(
|
|||||||
) {
|
) {
|
||||||
var showMenu by remember { mutableStateOf(false) }
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val rowColor = if (item.isCurrent) {
|
ListRowTile(
|
||||||
MaterialTheme.colorScheme.secondaryContainer
|
onClick = onPlayClick,
|
||||||
} else {
|
selected = item.isCurrent,
|
||||||
MaterialTheme.colorScheme.surfaceContainerHigh
|
modifier = Modifier,
|
||||||
}
|
leading = {
|
||||||
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(MaterialTheme.shapes.medium)
|
|
||||||
.clickable(onClick = onPlayClick),
|
|
||||||
shadowElevation = elevation,
|
|
||||||
tonalElevation = if (item.isCurrent) 2.dp else 0.dp,
|
|
||||||
) {
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.height(72.dp),
|
||||||
.height(72.dp)
|
|
||||||
.background(rowColor)
|
|
||||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
FeatherIcons.MoreVertical,
|
Iconsax.IconsaxDragHandle,
|
||||||
contentDescription = if (reorderScope != null) "Reorder" else null,
|
contentDescription = if (reorderScope != null) "Reorder" else null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
@ -300,10 +293,9 @@ private fun QueueItemRow(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
},
|
||||||
|
title = {
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(
|
Text(
|
||||||
text = item.title,
|
text = item.title,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
@ -315,6 +307,8 @@ private fun QueueItemRow(
|
|||||||
MaterialTheme.colorScheme.onSurface
|
MaterialTheme.colorScheme.onSurface
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
subtitle = {
|
||||||
Text(
|
Text(
|
||||||
text = item.subtitle,
|
text = item.subtitle,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
@ -322,10 +316,8 @@ private fun QueueItemRow(
|
|||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
trailing = {
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = item.durationMs.toDurationString(),
|
text = item.durationMs.toDurationString(),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
@ -340,7 +332,7 @@ private fun QueueItemRow(
|
|||||||
modifier = Modifier.size(36.dp),
|
modifier = Modifier.size(36.dp),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
FeatherIcons.MoreVertical,
|
Iconsax.Iconsax3DotsMore,
|
||||||
contentDescription = "More options",
|
contentDescription = "More options",
|
||||||
modifier = Modifier.size(18.dp),
|
modifier = Modifier.size(18.dp),
|
||||||
)
|
)
|
||||||
@ -362,7 +354,7 @@ private fun QueueItemRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun QueueEntry.toQueueDisplayData(): Tuple4<String, String, Long, String?> {
|
private fun QueueEntry.toQueueDisplayData(): Tuple4<String, String, Long, String?> {
|
||||||
|
|||||||
@ -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
|
||||||
Loading…
Reference in New Issue
Block a user