mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-08-05 19:59:51 +00:00
Compare commits
No commits in common. "4bc487782f76d8812c9c1cc405b300851ac88a28" and "ec672d5f8430e6777f06ab29dc6675723c0d7cc4" have entirely different histories.
4bc487782f
...
ec672d5f84
@ -27,11 +27,7 @@ val NEWPIPE_YOUTUBE_BUILT_IN_PLUGIN = PluginEntry(
|
|||||||
PluginCapability.NETWORK_REQUESTS,
|
PluginCapability.NETWORK_REQUESTS,
|
||||||
PluginCapability.PERSISTENT_STORAGE
|
PluginCapability.PERSISTENT_STORAGE
|
||||||
),
|
),
|
||||||
abilities = listOf(PluginAbility.AUDIO),
|
abilities = listOf(PluginAbility.AUDIO)
|
||||||
contact = "",
|
|
||||||
repository = "",
|
|
||||||
bugs = "",
|
|
||||||
license = "",
|
|
||||||
)
|
)
|
||||||
val LRCLIB_BUILT_IN_PLUGIN = PluginEntry(
|
val LRCLIB_BUILT_IN_PLUGIN = PluginEntry(
|
||||||
name = "LRCLib Lyrics",
|
name = "LRCLib Lyrics",
|
||||||
@ -42,11 +38,7 @@ val LRCLIB_BUILT_IN_PLUGIN = PluginEntry(
|
|||||||
capabilities = listOf(
|
capabilities = listOf(
|
||||||
PluginCapability.NETWORK_REQUESTS,
|
PluginCapability.NETWORK_REQUESTS,
|
||||||
),
|
),
|
||||||
abilities = listOf(PluginAbility.LYRICS),
|
abilities = listOf(PluginAbility.LYRICS)
|
||||||
contact = "",
|
|
||||||
repository = "",
|
|
||||||
bugs = "",
|
|
||||||
license = "",
|
|
||||||
)
|
)
|
||||||
val BUILT_IN_PLUGINS = listOf(
|
val BUILT_IN_PLUGINS = listOf(
|
||||||
NEWPIPE_YOUTUBE_BUILT_IN_PLUGIN,
|
NEWPIPE_YOUTUBE_BUILT_IN_PLUGIN,
|
||||||
|
|||||||
@ -55,12 +55,8 @@ data class GitHubOwner(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class GitHubRelease(
|
data class GitHubRelease(
|
||||||
@SerialName("tag_name") val tagName: String,
|
@SerialName("tag_name") val tagName: String,
|
||||||
val name: String? = null,
|
|
||||||
val body: String? = null,
|
|
||||||
val assets: List<GitHubAsset>,
|
val assets: List<GitHubAsset>,
|
||||||
@SerialName("html_url") val htmlUrl: String,
|
@SerialName("html_url") val htmlUrl: String,
|
||||||
val prerelease: Boolean = false,
|
|
||||||
val draft: Boolean = false,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@ -105,20 +101,6 @@ class GitHubPluginRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getReleases(owner: String, repo: String, perPage: Int = 30): List<GitHubRelease> {
|
|
||||||
return try {
|
|
||||||
httpClient.get("https://api.github.com/repos/$owner/$repo/releases") {
|
|
||||||
headers {
|
|
||||||
append("Accept", "application/vnd.github+json")
|
|
||||||
append("X-GitHub-Api-Version", "2022-11-28")
|
|
||||||
}
|
|
||||||
parameter("per_page", perPage)
|
|
||||||
}.body()
|
|
||||||
} catch (_: Exception) {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun close() {
|
fun close() {
|
||||||
httpClient.close()
|
httpClient.close()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,9 +61,9 @@ class PluginDiscoverViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
pluginManager.state.collect { pluginState ->
|
pluginManager.state.collect { pluginState ->
|
||||||
if (pluginState is PluginManagerStates.Data) {
|
if (pluginState is PluginManagerStates.Data) {
|
||||||
val installedUrls = pluginState.plugins.mapNotNull { it.repository.takeIf { r -> r.isNotBlank() } }.toSet()
|
val installedAuthors = pluginState.plugins.map { it.author }.toSet()
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(repos = _allRepos.filter { repo -> repo.htmlUrl !in installedUrls })
|
it.copy(repos = _allRepos.filter { repo -> repo.owner.login !in installedAuthors })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -74,8 +74,8 @@ class PluginDiscoverViewModel(
|
|||||||
private fun filterInstalled(repos: List<GitHubRepo>): List<GitHubRepo> {
|
private fun filterInstalled(repos: List<GitHubRepo>): List<GitHubRepo> {
|
||||||
val pluginState = pluginManager.state.value
|
val pluginState = pluginManager.state.value
|
||||||
if (pluginState !is PluginManagerStates.Data) return repos
|
if (pluginState !is PluginManagerStates.Data) return repos
|
||||||
val installedUrls = pluginState.plugins.mapNotNull { it.repository.takeIf { r -> r.isNotBlank() } }.toSet()
|
val installedAuthors = pluginState.plugins.map { it.author }.toSet()
|
||||||
return repos.filter { it.htmlUrl !in installedUrls }
|
return repos.filter { it.owner.login !in installedAuthors }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFirstPage() {
|
private fun loadFirstPage() {
|
||||||
@ -155,24 +155,6 @@ class PluginDiscoverViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun installPluginFromUrl(url: String, repoId: Long) {
|
|
||||||
if (_state.value.installingRepoId != null) return
|
|
||||||
_state.update { it.copy(installingRepoId = repoId, error = null) }
|
|
||||||
viewModelScope.launch {
|
|
||||||
runCatching {
|
|
||||||
pluginManager.addPluginFromURL(url)
|
|
||||||
}.onFailure { e ->
|
|
||||||
logger.e(e) { "Failed to install plugin" }
|
|
||||||
_state.update { it.copy(error = e.message) }
|
|
||||||
}
|
|
||||||
_state.update { it.copy(installingRepoId = null) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getReleases(owner: String, repo: String): List<GitHubRelease> {
|
|
||||||
return gitHubRepo.getReleases(owner, repo)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
gitHubRepo.close()
|
gitHubRepo.close()
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
|
|||||||
@ -325,15 +325,6 @@ class PluginManager(
|
|||||||
throw IllegalArgumentException("Invalid plugin.json format: ${e.message}")
|
throw IllegalArgumentException("Invalid plugin.json format: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preserve logo.png before deleting temp dir so the permission dialog can show it
|
|
||||||
val logoPngPath = tempDir / "logo.png".toPath()
|
|
||||||
if (FileSystem.SYSTEM.exists(logoPngPath)) {
|
|
||||||
val logoDir = pluginsDirPath / pluginEntry.id.toPath()
|
|
||||||
if (!FileSystem.SYSTEM.exists(logoDir)) FileSystem.SYSTEM.createDirectories(logoDir)
|
|
||||||
val destLogo = logoDir / "logo.png".toPath()
|
|
||||||
FileSystem.SYSTEM.copy(logoPngPath, destLogo)
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingPlugin.value = buildPendingPlugin(pluginEntry, bytes)
|
pendingPlugin.value = buildPendingPlugin(pluginEntry, bytes)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw Exception("Failed to read plugin: ${e.message}", e)
|
throw Exception("Failed to read plugin: ${e.message}", e)
|
||||||
|
|||||||
@ -51,11 +51,7 @@ data class PluginEntry(
|
|||||||
val description: String,
|
val description: String,
|
||||||
val author: String,
|
val author: String,
|
||||||
val capabilities: List<PluginCapability>,
|
val capabilities: List<PluginCapability>,
|
||||||
val abilities: List<PluginAbility>,
|
val abilities: List<PluginAbility>
|
||||||
val contact: String,
|
|
||||||
val repository: String,
|
|
||||||
val bugs: String,
|
|
||||||
val license: String,
|
|
||||||
) {
|
) {
|
||||||
@Suppress("REDUNDANT_CALL_OF_CONVERSION_METHOD")
|
@Suppress("REDUNDANT_CALL_OF_CONVERSION_METHOD")
|
||||||
val id: String = MurmurHash3().hash32x86("$name:$author".encodeToByteArray())
|
val id: String = MurmurHash3().hash32x86("$name:$author".encodeToByteArray())
|
||||||
|
|||||||
@ -17,7 +17,6 @@
|
|||||||
|
|
||||||
package dev.krtirtho.spotube.modules.plugin
|
package dev.krtirtho.spotube.modules.plugin
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@ -31,8 +30,6 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@ -44,7 +41,6 @@ import androidx.compose.material3.Surface
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@ -54,24 +50,16 @@ 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.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
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 androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import coil3.compose.AsyncImage
|
|
||||||
import coil3.compose.LocalPlatformContext
|
|
||||||
import coil3.request.ImageRequest
|
|
||||||
import coil3.request.crossfade
|
|
||||||
import dev.krtirtho.spotube.PlatformType
|
import dev.krtirtho.spotube.PlatformType
|
||||||
import dev.krtirtho.spotube.core.extras.kebabToTitleCase
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.Card
|
import dev.krtirtho.spotube.core.ui.base.Card
|
||||||
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
||||||
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
|
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
|
||||||
import dev.krtirtho.spotube.core.ui.base.SecondaryButton
|
|
||||||
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.ThemedDialog
|
|
||||||
import dev.krtirtho.spotube.core.ui.component.AdaptiveDialogBottomSheet
|
import dev.krtirtho.spotube.core.ui.component.AdaptiveDialogBottomSheet
|
||||||
import dev.krtirtho.spotube.core.ui.component.AdaptiveDropdownBottomSheet
|
import dev.krtirtho.spotube.core.ui.component.AdaptiveDropdownBottomSheet
|
||||||
import dev.krtirtho.spotube.core.ui.component.AdaptiveMenuItem
|
import dev.krtirtho.spotube.core.ui.component.AdaptiveMenuItem
|
||||||
@ -79,22 +67,21 @@ import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
|
|||||||
import dev.krtirtho.spotube.core.ui.component.HeaderDisplayMode
|
import dev.krtirtho.spotube.core.ui.component.HeaderDisplayMode
|
||||||
import dev.krtirtho.spotube.core.webview.WebViewController
|
import dev.krtirtho.spotube.core.webview.WebViewController
|
||||||
import dev.krtirtho.spotube.getPlatform
|
import dev.krtirtho.spotube.getPlatform
|
||||||
|
import dev.krtirtho.spotube.openUrlInBrowser
|
||||||
import dev.krtirtho.spotube.modules.plugin.components.PluginCard
|
import dev.krtirtho.spotube.modules.plugin.components.PluginCard
|
||||||
import dev.krtirtho.spotube.modules.plugin.components.PluginInstallDialog
|
|
||||||
import dev.krtirtho.spotube.modules.plugin.components.PluginPermissionDialog
|
import dev.krtirtho.spotube.modules.plugin.components.PluginPermissionDialog
|
||||||
import dev.krtirtho.spotube.modules.shell.LocalAppShellBottomInset
|
import dev.krtirtho.spotube.modules.shell.LocalAppShellBottomInset
|
||||||
import dev.krtirtho.spotube.openUrlInBrowser
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.CarbonGithubLogo
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxAdd
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxAdd
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxArrowDown4
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxArrowDown4
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxBox
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxBox
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxCheckCircle
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxCheckCircle
|
||||||
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxDocumentDownload
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxDocumentText
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxDocumentText
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxEdit
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxExportArrowBulk
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxGlobe
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxGlobe
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart
|
||||||
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxEdit
|
||||||
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxExportArrowBulk
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxImportArrow2Bulk
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxImportArrow2Bulk
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxLink
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxLink
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusic
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusic
|
||||||
@ -105,9 +92,20 @@ import io.github.vinceglb.filekit.dialogs.compose.rememberFilePickerLauncher
|
|||||||
import io.github.vinceglb.filekit.readBytes
|
import io.github.vinceglb.filekit.readBytes
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import net.swiftzer.semver.SemVer
|
|
||||||
import okio.FileSystem
|
import okio.FileSystem
|
||||||
import okio.Path.Companion.toPath
|
import okio.Path.Companion.toPath
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import coil3.compose.LocalPlatformContext
|
||||||
|
import coil3.request.ImageRequest
|
||||||
|
import coil3.request.crossfade
|
||||||
|
import dev.krtirtho.spotube.core.extras.kebabToTitleCase
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.SecondaryButton
|
||||||
|
import dev.krtirtho.spotube.resources.iconsax.CarbonGithubLogo
|
||||||
import okio.SYSTEM
|
import okio.SYSTEM
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
@ -166,15 +164,6 @@ fun PluginScreen(
|
|||||||
val discoverViewModel: PluginDiscoverViewModel = koinViewModel()
|
val discoverViewModel: PluginDiscoverViewModel = koinViewModel()
|
||||||
val discoverState by discoverViewModel.state.collectAsStateWithLifecycle()
|
val discoverState by discoverViewModel.state.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
var showPluginInfo by remember { mutableStateOf<PluginEntry?>(null) }
|
|
||||||
var showPluginSupport by remember { mutableStateOf<PluginEntry?>(null) }
|
|
||||||
var supportText by remember { mutableStateOf<String?>(null) }
|
|
||||||
var isLoadingSupport by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
var installDialogRepo by remember { mutableStateOf<GitHubRepo?>(null) }
|
|
||||||
var releases by remember { mutableStateOf<List<GitHubRelease>>(emptyList()) }
|
|
||||||
var isLoadingReleases by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val pleaseEnterUrl = stringResource(Res.string.plugin_error_enter_url)
|
val pleaseEnterUrl = stringResource(Res.string.plugin_error_enter_url)
|
||||||
val urlSchemeError = stringResource(Res.string.plugin_error_url_scheme)
|
val urlSchemeError = stringResource(Res.string.plugin_error_url_scheme)
|
||||||
val downloadFailed = stringResource(Res.string.plugin_error_download_failed)
|
val downloadFailed = stringResource(Res.string.plugin_error_download_failed)
|
||||||
@ -214,8 +203,9 @@ fun PluginScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pendingPlugin?.let { pending ->
|
pendingPlugin?.let { pending ->
|
||||||
val logoPath = remember(pending.entry.id) {
|
val logoPath = remember(pending.existingEntry?.id) {
|
||||||
val path = pluginManager.pluginsDirPath / pending.entry.id.toPath() / "logo.png".toPath()
|
val existingId = pending.existingEntry?.id ?: return@remember null
|
||||||
|
val path = pluginManager.pluginsDirPath / existingId.toPath() / "logo.png".toPath()
|
||||||
if (FileSystem.SYSTEM.exists(path)) path else null
|
if (FileSystem.SYSTEM.exists(path)) path else null
|
||||||
}
|
}
|
||||||
PluginPermissionDialog(
|
PluginPermissionDialog(
|
||||||
@ -234,160 +224,6 @@ fun PluginScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
showPluginInfo?.let { plugin ->
|
|
||||||
val logoPath = remember(plugin.id) {
|
|
||||||
val path = pluginManager.pluginsDirPath / plugin.id.toPath() / "logo.png".toPath()
|
|
||||||
if (FileSystem.SYSTEM.exists(path)) path else null
|
|
||||||
}
|
|
||||||
ThemedDialog(
|
|
||||||
onDismissRequest = { showPluginInfo = null },
|
|
||||||
title = {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(48.dp)
|
|
||||||
.clip(RoundedCornerShape(12.dp)),
|
|
||||||
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.12f)
|
|
||||||
) {
|
|
||||||
if (logoPath != null) {
|
|
||||||
val platformContext = LocalPlatformContext.current
|
|
||||||
AsyncImage(
|
|
||||||
model = ImageRequest.Builder(platformContext)
|
|
||||||
.data(logoPath.toString())
|
|
||||||
.crossfade(true)
|
|
||||||
.build(),
|
|
||||||
contentDescription = plugin.name,
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Box(contentAlignment = Alignment.Center) {
|
|
||||||
Icon(
|
|
||||||
Iconsax.IconsaxBox,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
plugin.name,
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.SemiBold
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
PrimaryButton(onClick = { showPluginInfo = null }) {
|
|
||||||
Text("Close")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
if (plugin.description.isNotBlank()) {
|
|
||||||
Text(
|
|
||||||
plugin.description,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
DetailRow("Version", plugin.version)
|
|
||||||
DetailRow("API Version", plugin.apiVersion)
|
|
||||||
DetailRow("Author", plugin.author)
|
|
||||||
if (plugin.license.isNotBlank()) {
|
|
||||||
DetailRow("License", plugin.license)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plugin.capabilities.isNotEmpty()) {
|
|
||||||
DetailChipsRow("Capabilities", plugin.capabilities.map { it.name })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plugin.abilities.isNotEmpty()) {
|
|
||||||
DetailChipsRow("Abilities", plugin.abilities.map { ability ->
|
|
||||||
when (ability) {
|
|
||||||
PluginAbility.METADATA -> "Metadata"
|
|
||||||
PluginAbility.AUDIO -> "Audio"
|
|
||||||
PluginAbility.LYRICS -> "Lyrics"
|
|
||||||
PluginAbility.SCROBBLE -> "Scrobble"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plugin.repository.isNotBlank()) {
|
|
||||||
ClickableDetailRow(label = "Repository", value = plugin.repository) {
|
|
||||||
openUrlInBrowser(plugin.repository)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plugin.contact.isNotBlank()) {
|
|
||||||
DetailRow("Contact", plugin.contact)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plugin.bugs.isNotBlank()) {
|
|
||||||
ClickableDetailRow(label = "Report Bugs", value = plugin.bugs) {
|
|
||||||
openUrlInBrowser(plugin.bugs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showPluginSupport?.let { plugin ->
|
|
||||||
ThemedDialog(
|
|
||||||
onDismissRequest = { showPluginSupport = null; supportText = null },
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
"Support ${plugin.name}",
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.SemiBold
|
|
||||||
)
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
PrimaryButton(onClick = { showPluginSupport = null; supportText = null }) {
|
|
||||||
Text("Close")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
if (isLoadingSupport) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Text(
|
|
||||||
supportText ?: "",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
installDialogRepo?.let { repo ->
|
|
||||||
PluginInstallDialog(
|
|
||||||
repo = repo,
|
|
||||||
releases = releases,
|
|
||||||
isLoadingReleases = isLoadingReleases,
|
|
||||||
onDismiss = { installDialogRepo = null },
|
|
||||||
onInstall = { release ->
|
|
||||||
installDialogRepo = null
|
|
||||||
val smplugUrl = release.assets.firstOrNull { it.name.endsWith(".smplug") }?.browserDownloadUrl
|
|
||||||
if (smplugUrl != null) {
|
|
||||||
discoverViewModel.installPluginFromUrl(smplugUrl, repo.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showInstallSheet) {
|
if (showInstallSheet) {
|
||||||
AdaptiveDialogBottomSheet(
|
AdaptiveDialogBottomSheet(
|
||||||
onDismiss = { showInstallSheet = false },
|
onDismiss = { showInstallSheet = false },
|
||||||
@ -703,23 +539,6 @@ fun PluginScreen(
|
|||||||
},
|
},
|
||||||
isLoggedIn = isLoggedIn,
|
isLoggedIn = isLoggedIn,
|
||||||
logoPath = logoPath,
|
logoPath = logoPath,
|
||||||
onInfo = { showPluginInfo = plugin },
|
|
||||||
onSupport = if (selectedService != null) {
|
|
||||||
{
|
|
||||||
isLoadingSupport = true
|
|
||||||
scope.launch {
|
|
||||||
showPluginSupport = plugin
|
|
||||||
val version = SemVer.parse(plugin.version)
|
|
||||||
selectedService.use {
|
|
||||||
supportText =
|
|
||||||
coreAPI.supportMarkdownText(version)
|
|
||||||
}
|
|
||||||
isLoadingSupport = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
onLogin = if (requiresAuth && selectedService != null) {
|
onLogin = if (requiresAuth && selectedService != null) {
|
||||||
{
|
{
|
||||||
pluginManager.launchTask {
|
pluginManager.launchTask {
|
||||||
@ -933,16 +752,7 @@ fun PluginScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
SecondaryButton(
|
SecondaryButton(
|
||||||
onClick = {
|
onClick = { discoverViewModel.installPlugin(repo) },
|
||||||
installDialogRepo = repo
|
|
||||||
isLoadingReleases = true
|
|
||||||
releases = emptyList()
|
|
||||||
scope.launch {
|
|
||||||
val parts = repo.fullName.split("/")
|
|
||||||
releases = discoverViewModel.getReleases(parts[0], parts[1])
|
|
||||||
isLoadingReleases = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enabled = !isInstalling
|
enabled = !isInstalling
|
||||||
) {
|
) {
|
||||||
if (isInstalling) {
|
if (isInstalling) {
|
||||||
@ -1013,78 +823,6 @@ fun PluginScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun DetailRow(label: String, value: String) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
"$label:",
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.widthIn(min = 90.dp)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
value,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ClickableDetailRow(label: String, value: String, onClick: () -> Unit) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth().clickable(onClick = onClick),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
"$label:",
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.widthIn(min = 90.dp)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
value,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun DetailChipsRow(label: String, chips: List<String>) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
"$label:",
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.widthIn(min = 90.dp)
|
|
||||||
)
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
|
||||||
chips.forEach { chip ->
|
|
||||||
Surface(
|
|
||||||
shape = RoundedCornerShape(4.dp),
|
|
||||||
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.6f)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
chip,
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
|
||||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DefaultAbilityPluginSelector(
|
fun DefaultAbilityPluginSelector(
|
||||||
ability: PluginAbility,
|
ability: PluginAbility,
|
||||||
|
|||||||
@ -52,8 +52,6 @@ import dev.krtirtho.spotube.modules.plugin.PluginEntry
|
|||||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxBox
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxBox
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxCheckSquare
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxCheckSquare
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxInformation
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxTag
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxTag
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash
|
||||||
import dev.krtirtho.spotube.resources.iconsax.User
|
import dev.krtirtho.spotube.resources.iconsax.User
|
||||||
@ -78,8 +76,6 @@ internal fun PluginCard(
|
|||||||
isLoggedIn: Boolean,
|
isLoggedIn: Boolean,
|
||||||
onLogin: (() -> Unit)? = null,
|
onLogin: (() -> Unit)? = null,
|
||||||
onLogout: (() -> Unit)? = null,
|
onLogout: (() -> Unit)? = null,
|
||||||
onInfo: (() -> Unit)? = null,
|
|
||||||
onSupport: (() -> Unit)? = null,
|
|
||||||
logoPath: Path? = null,
|
logoPath: Path? = null,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@ -238,50 +234,28 @@ internal fun PluginCard(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.align(Alignment.Bottom),
|
modifier = Modifier.align(Alignment.Bottom),
|
||||||
horizontalAlignment = Alignment.End,
|
horizontalAlignment = Alignment.End,
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
if (plugin in BUILT_IN_PLUGINS) {
|
||||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
// Built-in plugins cannot be removed
|
||||||
verticalAlignment = Alignment.CenterVertically
|
Text(
|
||||||
) {
|
stringResource(Res.string.plugin_state_builtin),
|
||||||
if (onInfo != null) {
|
style = MaterialTheme.typography.labelSmall,
|
||||||
GhostIconButton(onClick = onInfo) {
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
Icon(
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
Iconsax.IconsaxInformation,
|
.border(
|
||||||
contentDescription = null,
|
BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
shape = RoundedCornerShape(6.dp)
|
||||||
)
|
)
|
||||||
}
|
.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||||
}
|
)
|
||||||
if (onSupport != null) {
|
} else {
|
||||||
GhostIconButton(onClick = onSupport) {
|
GhostIconButton(onClick = onRemove) {
|
||||||
Icon(
|
Icon(
|
||||||
Iconsax.IconsaxHeart,
|
Iconsax.IconsaxTrash,
|
||||||
contentDescription = null,
|
contentDescription = stringResource(Res.string.plugin_action_remove),
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (plugin in BUILT_IN_PLUGINS) {
|
|
||||||
Text(
|
|
||||||
stringResource(Res.string.plugin_state_builtin),
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
|
|
||||||
.border(
|
|
||||||
BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
|
|
||||||
shape = RoundedCornerShape(6.dp)
|
|
||||||
)
|
|
||||||
.padding(horizontal = 6.dp, vertical = 2.dp)
|
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
GhostIconButton(onClick = onRemove) {
|
|
||||||
Icon(
|
|
||||||
Iconsax.IconsaxTrash,
|
|
||||||
contentDescription = stringResource(Res.string.plugin_action_remove),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,256 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package dev.krtirtho.spotube.modules.plugin.components
|
|
||||||
|
|
||||||
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
|
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
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.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
|
||||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.MenuAnchorType
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
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.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
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 coil3.compose.AsyncImage
|
|
||||||
import coil3.compose.LocalPlatformContext
|
|
||||||
import coil3.request.ImageRequest
|
|
||||||
import coil3.request.crossfade
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.ThemedDialog
|
|
||||||
import dev.krtirtho.spotube.modules.plugin.GitHubRepo
|
|
||||||
import dev.krtirtho.spotube.modules.plugin.GitHubRelease
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxBox
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun PluginInstallDialog(
|
|
||||||
repo: GitHubRepo,
|
|
||||||
releases: List<GitHubRelease>,
|
|
||||||
isLoadingReleases: Boolean,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
onInstall: (GitHubRelease) -> Unit,
|
|
||||||
) {
|
|
||||||
var selectedRelease by remember { mutableStateOf<GitHubRelease?>(null) }
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
LaunchedEffect(releases) {
|
|
||||||
if (selectedRelease == null && releases.isNotEmpty()) {
|
|
||||||
selectedRelease = releases.first()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ThemedDialog(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
title = {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(48.dp)
|
|
||||||
.clip(RoundedCornerShape(12.dp)),
|
|
||||||
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.12f)
|
|
||||||
) {
|
|
||||||
Box(contentAlignment = Alignment.Center) {
|
|
||||||
val platformContext = LocalPlatformContext.current
|
|
||||||
AsyncImage(
|
|
||||||
model = ImageRequest.Builder(platformContext)
|
|
||||||
.data(repo.owner.avatarUrl)
|
|
||||||
.crossfade(true)
|
|
||||||
.build(),
|
|
||||||
contentDescription = repo.owner.login,
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(
|
|
||||||
repo.fullName,
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
if (!repo.description.isNullOrBlank()) {
|
|
||||||
Text(
|
|
||||||
repo.description,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
PrimaryButton(
|
|
||||||
onClick = { selectedRelease?.let { onInstall(it) } },
|
|
||||||
enabled = selectedRelease != null && !isLoadingReleases
|
|
||||||
) {
|
|
||||||
Text("Install")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
if (isLoadingReleases) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxWidth().height(120.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator(modifier = Modifier.size(32.dp))
|
|
||||||
}
|
|
||||||
} else if (releases.isEmpty()) {
|
|
||||||
Text(
|
|
||||||
"No releases found",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Text(
|
|
||||||
"Select Release",
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
|
|
||||||
ExposedDropdownMenuBox(
|
|
||||||
expanded = expanded,
|
|
||||||
onExpandedChange = { expanded = it }
|
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.menuAnchor(MenuAnchorType.PrimaryNotEditable),
|
|
||||||
shape = RoundedCornerShape(8.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surfaceVariant
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
selectedRelease?.tagName ?: "Select a release",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ExposedDropdownMenu(
|
|
||||||
expanded = expanded,
|
|
||||||
onDismissRequest = { expanded = false }
|
|
||||||
) {
|
|
||||||
releases.forEach { release ->
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
release.tagName,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
fontWeight = if (release == selectedRelease) FontWeight.SemiBold else FontWeight.Normal
|
|
||||||
)
|
|
||||||
if (release.prerelease) {
|
|
||||||
Text(
|
|
||||||
"Pre-release",
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
color = MaterialTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
selectedRelease = release
|
|
||||||
expanded = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedRelease?.let { release ->
|
|
||||||
if (!release.body.isNullOrBlank()) {
|
|
||||||
Text(
|
|
||||||
"Release Notes",
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
shape = RoundedCornerShape(8.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.heightIn(max = 200.dp)
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.padding(12.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
release.body,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,97 +0,0 @@
|
|||||||
package dev.krtirtho.spotube.resources.iconsax
|
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.graphics.vector.PathData
|
|
||||||
import androidx.compose.ui.graphics.vector.group
|
|
||||||
import androidx.compose.ui.graphics.vector.path
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
|
|
||||||
val Iconsax.IconsaxInformation: ImageVector
|
|
||||||
get() {
|
|
||||||
if (_IconsaxInformation != null) {
|
|
||||||
return _IconsaxInformation!!
|
|
||||||
}
|
|
||||||
_IconsaxInformation = ImageVector.Builder(
|
|
||||||
name = "IconsaxInformation",
|
|
||||||
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(10.75f, 2.45f)
|
|
||||||
curveTo(11.45f, 1.86f, 12.58f, 1.86f, 13.26f, 2.45f)
|
|
||||||
lineTo(14.84f, 3.8f)
|
|
||||||
curveTo(15.14f, 4.05f, 15.71f, 4.26f, 16.11f, 4.26f)
|
|
||||||
horizontalLineTo(17.81f)
|
|
||||||
curveTo(18.87f, 4.26f, 19.74f, 5.13f, 19.74f, 6.19f)
|
|
||||||
verticalLineTo(7.89f)
|
|
||||||
curveTo(19.74f, 8.29f, 19.95f, 8.85f, 20.2f, 9.15f)
|
|
||||||
lineTo(21.55f, 10.73f)
|
|
||||||
curveTo(22.14f, 11.43f, 22.14f, 12.56f, 21.55f, 13.24f)
|
|
||||||
lineTo(20.2f, 14.82f)
|
|
||||||
curveTo(19.95f, 15.12f, 19.74f, 15.68f, 19.74f, 16.08f)
|
|
||||||
verticalLineTo(17.78f)
|
|
||||||
curveTo(19.74f, 18.84f, 18.87f, 19.71f, 17.81f, 19.71f)
|
|
||||||
horizontalLineTo(16.11f)
|
|
||||||
curveTo(15.71f, 19.71f, 15.15f, 19.92f, 14.85f, 20.17f)
|
|
||||||
lineTo(13.27f, 21.52f)
|
|
||||||
curveTo(12.57f, 22.11f, 11.44f, 22.11f, 10.76f, 21.52f)
|
|
||||||
lineTo(9.18f, 20.17f)
|
|
||||||
curveTo(8.88f, 19.92f, 8.31f, 19.71f, 7.92f, 19.71f)
|
|
||||||
horizontalLineTo(6.17f)
|
|
||||||
curveTo(5.11f, 19.71f, 4.24f, 18.84f, 4.24f, 17.78f)
|
|
||||||
verticalLineTo(16.07f)
|
|
||||||
curveTo(4.24f, 15.68f, 4.04f, 15.11f, 3.79f, 14.82f)
|
|
||||||
lineTo(2.44f, 13.23f)
|
|
||||||
curveTo(1.86f, 12.54f, 1.86f, 11.42f, 2.44f, 10.73f)
|
|
||||||
lineTo(3.79f, 9.14f)
|
|
||||||
curveTo(4.04f, 8.84f, 4.24f, 8.28f, 4.24f, 7.89f)
|
|
||||||
verticalLineTo(6.2f)
|
|
||||||
curveTo(4.24f, 5.14f, 5.11f, 4.27f, 6.17f, 4.27f)
|
|
||||||
horizontalLineTo(7.9f)
|
|
||||||
curveTo(8.3f, 4.27f, 8.86f, 4.06f, 9.16f, 3.81f)
|
|
||||||
lineTo(10.75f, 2.45f)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
path(fill = SolidColor(Color.White)) {
|
|
||||||
moveTo(12f, 16.869f)
|
|
||||||
curveTo(11.45f, 16.869f, 11f, 16.419f, 11f, 15.869f)
|
|
||||||
curveTo(11f, 15.319f, 11.44f, 14.869f, 12f, 14.869f)
|
|
||||||
curveTo(12.55f, 14.869f, 13f, 15.319f, 13f, 15.869f)
|
|
||||||
curveTo(13f, 16.419f, 12.56f, 16.869f, 12f, 16.869f)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
path(fill = SolidColor(Color.White)) {
|
|
||||||
moveTo(12f, 13.721f)
|
|
||||||
curveTo(11.59f, 13.721f, 11.25f, 13.381f, 11.25f, 12.971f)
|
|
||||||
verticalLineTo(8.131f)
|
|
||||||
curveTo(11.25f, 7.721f, 11.59f, 7.381f, 12f, 7.381f)
|
|
||||||
curveTo(12.41f, 7.381f, 12.75f, 7.721f, 12.75f, 8.131f)
|
|
||||||
verticalLineTo(12.961f)
|
|
||||||
curveTo(12.75f, 13.381f, 12.42f, 13.721f, 12f, 13.721f)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return _IconsaxInformation!!
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("ObjectPropertyName")
|
|
||||||
private var _IconsaxInformation: ImageVector? = null
|
|
||||||
Loading…
Reference in New Issue
Block a user