mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-08-05 19:59:51 +00:00
feat: add adaptive dialog bottom sheet component and enhance plugin installation UI
This commit is contained in:
parent
7d1a6750c3
commit
c3852a1c32
@ -29,7 +29,10 @@
|
|||||||
- `plugin_interfaces` exports `zipline.core` and `semver` as API. `composeApp` depends on it for the plugin system.
|
- `plugin_interfaces` exports `zipline.core` and `semver` as API. `composeApp` depends on it for the plugin system.
|
||||||
- `plugin_interfaces` also has a JS target (`browser()`), used by the plugin system.
|
- `plugin_interfaces` also has a JS target (`browser()`), used by the plugin system.
|
||||||
|
|
||||||
## Desktop JVM specifics
|
## UI component patterns
|
||||||
|
- **AdaptiveDropdownBottomSheet** (`commonMain/.../core/ui/component/AdaptiveDropdownBottomSheet.kt`): switches between `DropdownMenu` (large screen) and `ModalBottomSheet` (small screen) via `currentWindowAdaptiveInfo()`. Do NOT use expect/actual — all adaptive components that rely ONLY on Compose/Material3 APIs belong in commonMain.
|
||||||
|
- **AdaptiveDialogBottomSheet** (`commonMain/.../core/ui/component/AdaptiveDialogBottomSheet.kt`): switches between `ThemedDialog` (large screen) and `ModalBottomSheet` (small screen) via `currentWindowAdaptiveInfo()`. Same rule — keep in commonMain unless platform-specific APIs are required.
|
||||||
|
- Use `expect`/`actual` only when the component MUST use platform-specific APIs (e.g. `WindowState` for desktop window controls, native scrollbars). Pure Compose/Material3 adaptivity stays in commonMain.
|
||||||
- JavaFX is required; `--add-opens` flags in `compose.desktop.application.jvmArgs` must be preserved: `javafx.graphics/javafx.scene`, `javafx.graphics/com.sun.javafx.sg.prism`, `javafx.graphics/com.sun.javafx.scene`, `javafx.web/com.sun.webkit`, `javafx.media/com.sun.media.jfxmedia`, `javafx.media/com.sun.media.jfxmedia.events`.
|
- JavaFX is required; `--add-opens` flags in `compose.desktop.application.jvmArgs` must be preserved: `javafx.graphics/javafx.scene`, `javafx.graphics/com.sun.javafx.sg.prism`, `javafx.graphics/com.sun.javafx.scene`, `javafx.web/com.sun.webkit`, `javafx.media/com.sun.media.jfxmedia`, `javafx.media/com.sun.media.jfxmedia.events`.
|
||||||
- JavaFX dependencies are loaded from OpenJFX with platform classifiers (win/mac/linux) resolved at configuration time via `System.getProperty("os.name")`.
|
- JavaFX dependencies are loaded from OpenJFX with platform classifiers (win/mac/linux) resolved at configuration time via `System.getProperty("os.name")`.
|
||||||
|
|
||||||
|
|||||||
@ -126,6 +126,7 @@
|
|||||||
<string name="settings_plugins_plugin_content_description">%1$s plugin</string>
|
<string name="settings_plugins_plugin_content_description">%1$s plugin</string>
|
||||||
<string name="settings_plugins_clear_content_description">Clear</string>
|
<string name="settings_plugins_clear_content_description">Clear</string>
|
||||||
|
|
||||||
|
<string name="plugin_configure_title">Configure</string>
|
||||||
<string name="plugin_screen_title">Plugin Manager</string>
|
<string name="plugin_screen_title">Plugin Manager</string>
|
||||||
<string name="plugin_error_enter_url">Please enter a URL</string>
|
<string name="plugin_error_enter_url">Please enter a URL</string>
|
||||||
<string name="plugin_error_url_scheme">URL must start with http:// or https://</string>
|
<string name="plugin_error_url_scheme">URL must start with http:// or https://</string>
|
||||||
|
|||||||
@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* 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.component
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.ThemedDialog
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun AdaptiveDialogBottomSheet(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
title: @Composable (() -> Unit)? = null,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
breakpointDp: Float = 600f,
|
||||||
|
actions: @Composable (() -> Unit)? = null,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val adaptiveInfo = currentWindowAdaptiveInfo()
|
||||||
|
val isLargeScreen = adaptiveInfo.windowSizeClass.minWidthDp >= breakpointDp
|
||||||
|
|
||||||
|
if (isLargeScreen) {
|
||||||
|
ThemedDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = title,
|
||||||
|
actions = actions,
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
dragHandle = null,
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
) {
|
||||||
|
title?.let {
|
||||||
|
it()
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
}
|
||||||
|
content()
|
||||||
|
actions?.let {
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
it()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -26,9 +26,11 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
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.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
@ -55,22 +57,30 @@ import dev.krtirtho.spotube.core.ui.base.Card
|
|||||||
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
|
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
|
||||||
import dev.krtirtho.spotube.PlatformType
|
import dev.krtirtho.spotube.PlatformType
|
||||||
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.SecondaryIconButton
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.TextField
|
||||||
import dev.krtirtho.spotube.core.ui.component.AdaptiveDropdownBottomSheet
|
import dev.krtirtho.spotube.core.ui.component.AdaptiveDropdownBottomSheet
|
||||||
|
import dev.krtirtho.spotube.core.ui.component.AdaptiveDialogBottomSheet
|
||||||
import dev.krtirtho.spotube.core.ui.component.AdaptiveMenuItem
|
import dev.krtirtho.spotube.core.ui.component.AdaptiveMenuItem
|
||||||
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
|
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.modules.plugin.components.InstallSection
|
|
||||||
import dev.krtirtho.spotube.modules.plugin.components.PluginCard
|
import dev.krtirtho.spotube.modules.plugin.components.PluginCard
|
||||||
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.resources.iconsax.Iconsax
|
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||||
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxAdd
|
||||||
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
||||||
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.IconsaxCheckSquare
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxCheckSquare
|
||||||
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.IconsaxEdit
|
||||||
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxExportArrowBulk
|
||||||
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxImportArrow2Bulk
|
||||||
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxLink
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusic
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusic
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSound
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxSound
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxTextalignLeft
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxTextalignLeft
|
||||||
@ -83,6 +93,9 @@ import org.koin.compose.koinInject
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import spotube.composeapp.generated.resources.Res
|
import spotube.composeapp.generated.resources.Res
|
||||||
import spotube.composeapp.generated.resources.plugin_empty_subtitle
|
import spotube.composeapp.generated.resources.plugin_empty_subtitle
|
||||||
|
import spotube.composeapp.generated.resources.plugin_action_download
|
||||||
|
import spotube.composeapp.generated.resources.plugin_action_install_from_file
|
||||||
|
import spotube.composeapp.generated.resources.plugin_configure_title
|
||||||
import spotube.composeapp.generated.resources.plugin_empty_title
|
import spotube.composeapp.generated.resources.plugin_empty_title
|
||||||
import spotube.composeapp.generated.resources.plugin_error_download_failed
|
import spotube.composeapp.generated.resources.plugin_error_download_failed
|
||||||
import spotube.composeapp.generated.resources.plugin_error_enter_url
|
import spotube.composeapp.generated.resources.plugin_error_enter_url
|
||||||
@ -90,7 +103,9 @@ import spotube.composeapp.generated.resources.plugin_error_url_scheme
|
|||||||
import spotube.composeapp.generated.resources.plugin_installed_count
|
import spotube.composeapp.generated.resources.plugin_installed_count
|
||||||
import spotube.composeapp.generated.resources.plugin_installed_plural
|
import spotube.composeapp.generated.resources.plugin_installed_plural
|
||||||
import spotube.composeapp.generated.resources.plugin_installed_singular
|
import spotube.composeapp.generated.resources.plugin_installed_singular
|
||||||
|
import spotube.composeapp.generated.resources.plugin_install_section_title
|
||||||
import spotube.composeapp.generated.resources.plugin_screen_title
|
import spotube.composeapp.generated.resources.plugin_screen_title
|
||||||
|
import spotube.composeapp.generated.resources.plugin_url_placeholder
|
||||||
import spotube.composeapp.generated.resources.settings_plugins_action_change
|
import spotube.composeapp.generated.resources.settings_plugins_action_change
|
||||||
import spotube.composeapp.generated.resources.settings_plugins_action_select
|
import spotube.composeapp.generated.resources.settings_plugins_action_select
|
||||||
import spotube.composeapp.generated.resources.settings_plugins_ability_audio
|
import spotube.composeapp.generated.resources.settings_plugins_ability_audio
|
||||||
@ -120,6 +135,7 @@ fun PluginScreen(
|
|||||||
var urlInput by remember { mutableStateOf("") }
|
var urlInput by remember { mutableStateOf("") }
|
||||||
var urlError by remember { mutableStateOf<String?>(null) }
|
var urlError by remember { mutableStateOf<String?>(null) }
|
||||||
var isLoadingUrl by remember { mutableStateOf(false) }
|
var isLoadingUrl by remember { mutableStateOf(false) }
|
||||||
|
var showInstallSheet 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)
|
||||||
@ -175,6 +191,90 @@ fun PluginScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showInstallSheet) {
|
||||||
|
AdaptiveDialogBottomSheet(
|
||||||
|
onDismiss = { showInstallSheet = false },
|
||||||
|
title = {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Iconsax.IconsaxImportArrow2Bulk,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(Res.string.plugin_install_section_title),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.Top,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
TextField(
|
||||||
|
value = urlInput,
|
||||||
|
onValueChange = { urlInput = it; urlError = null },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
stringResource(Res.string.plugin_url_placeholder),
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
Iconsax.IconsaxLink,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
isError = urlError != null,
|
||||||
|
singleLine = true,
|
||||||
|
)
|
||||||
|
SecondaryIconButton(
|
||||||
|
onClick = { submitUrl() },
|
||||||
|
enabled = !isLoadingUrl,
|
||||||
|
) {
|
||||||
|
if (isLoadingUrl) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimary
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
Iconsax.IconsaxImportArrow2Bulk,
|
||||||
|
contentDescription = stringResource(Res.string.plugin_action_download),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
|
||||||
|
|
||||||
|
OutlineButton(
|
||||||
|
onClick = { launcher.launch() },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Iconsax.IconsaxExportArrowBulk,
|
||||||
|
contentDescription = stringResource(Res.string.plugin_action_install_from_file)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text(stringResource(Res.string.plugin_action_install_from_file))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
ApplicationMainBar(title = { Text(stringResource(Res.string.plugin_screen_title)) })
|
ApplicationMainBar(title = { Text(stringResource(Res.string.plugin_screen_title)) })
|
||||||
@ -205,16 +305,28 @@ fun PluginScreen(
|
|||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
// ── Install section ───────────────────────────────────
|
// ── Configure header ──────────────────────────────
|
||||||
item {
|
item {
|
||||||
InstallSection(
|
Row(
|
||||||
urlInput = urlInput,
|
modifier = Modifier
|
||||||
onUrlChange = { urlInput = it; urlError = null },
|
.fillMaxWidth()
|
||||||
urlError = urlError,
|
.padding(horizontal = 4.dp, vertical = 4.dp),
|
||||||
isLoadingUrl = isLoadingUrl,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
onSubmitUrl = { submitUrl() },
|
verticalAlignment = Alignment.CenterVertically
|
||||||
onPickFile = { launcher.launch() }
|
) {
|
||||||
)
|
Text(
|
||||||
|
stringResource(Res.string.plugin_configure_title),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
PrimaryButton(onClick = { showInstallSheet = true }) {
|
||||||
|
Icon(
|
||||||
|
Iconsax.IconsaxAdd,
|
||||||
|
contentDescription = "Install a plugin",
|
||||||
|
)
|
||||||
|
Text(stringResource(Res.string.plugin_install_section_title))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Default ability plugin selectors ─────────────────
|
// ── Default ability plugin selectors ─────────────────
|
||||||
|
|||||||
@ -1,151 +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.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.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.Card
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.TextField
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.SecondaryIconButton
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxExportArrowBulk
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxImportArrow2Bulk
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxLink
|
|
||||||
import org.jetbrains.compose.resources.stringResource
|
|
||||||
import spotube.composeapp.generated.resources.Res
|
|
||||||
import spotube.composeapp.generated.resources.plugin_action_download
|
|
||||||
import spotube.composeapp.generated.resources.plugin_action_install_from_file
|
|
||||||
import spotube.composeapp.generated.resources.plugin_install_section_title
|
|
||||||
import spotube.composeapp.generated.resources.plugin_url_placeholder
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
internal fun InstallSection(
|
|
||||||
urlInput: String,
|
|
||||||
onUrlChange: (String) -> Unit,
|
|
||||||
urlError: String?,
|
|
||||||
isLoadingUrl: Boolean,
|
|
||||||
onSubmitUrl: () -> Unit,
|
|
||||||
onPickFile: () -> Unit,
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Iconsax.IconsaxImportArrow2Bulk,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(16.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
stringResource(Res.string.plugin_install_section_title),
|
|
||||||
style = MaterialTheme.typography.titleSmall,
|
|
||||||
fontWeight = FontWeight.SemiBold
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// URL row
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.Top,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
TextField(
|
|
||||||
value = urlInput,
|
|
||||||
onValueChange = onUrlChange,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
placeholder = {
|
|
||||||
Text(
|
|
||||||
stringResource(Res.string.plugin_url_placeholder),
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
},
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(
|
|
||||||
Iconsax.IconsaxLink,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(16.dp)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
isError = urlError != null,
|
|
||||||
// supportingText = urlError?.let { { Text(it) } },
|
|
||||||
// shape = RoundedCornerShape(10.dp),
|
|
||||||
singleLine = true,
|
|
||||||
)
|
|
||||||
SecondaryIconButton(
|
|
||||||
onClick = onSubmitUrl,
|
|
||||||
enabled = !isLoadingUrl,
|
|
||||||
) {
|
|
||||||
if (isLoadingUrl) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.size(16.dp),
|
|
||||||
strokeWidth = 2.dp,
|
|
||||||
color = MaterialTheme.colorScheme.onPrimary
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Icon(
|
|
||||||
Iconsax.IconsaxImportArrow2Bulk,
|
|
||||||
contentDescription = stringResource(Res.string.plugin_action_download),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
|
|
||||||
|
|
||||||
// File picker
|
|
||||||
OutlineButton(
|
|
||||||
onClick = onPickFile,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Iconsax.IconsaxExportArrowBulk,
|
|
||||||
contentDescription = stringResource(Res.string.plugin_action_install_from_file)
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
Text(stringResource(Res.string.plugin_action_install_from_file))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
package dev.krtirtho.spotube.resources.iconsax
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
|
import androidx.compose.ui.graphics.StrokeJoin
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.graphics.vector.PathData
|
||||||
|
import androidx.compose.ui.graphics.vector.group
|
||||||
|
import androidx.compose.ui.graphics.vector.path
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
val Iconsax.IconsaxAdd: ImageVector
|
||||||
|
get() {
|
||||||
|
if (_IncosaxAdd != null) {
|
||||||
|
return _IncosaxAdd!!
|
||||||
|
}
|
||||||
|
_IncosaxAdd = ImageVector.Builder(
|
||||||
|
name = "IncosaxAdd",
|
||||||
|
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(
|
||||||
|
fillAlpha = 0.4f,
|
||||||
|
stroke = SolidColor(Color.White),
|
||||||
|
strokeAlpha = 0.4f,
|
||||||
|
strokeLineWidth = 1.5f,
|
||||||
|
strokeLineCap = StrokeCap.Round,
|
||||||
|
strokeLineJoin = StrokeJoin.Round
|
||||||
|
) {
|
||||||
|
moveTo(6f, 12f)
|
||||||
|
horizontalLineTo(18f)
|
||||||
|
}
|
||||||
|
path(
|
||||||
|
stroke = SolidColor(Color.White),
|
||||||
|
strokeLineWidth = 1.5f,
|
||||||
|
strokeLineCap = StrokeCap.Round,
|
||||||
|
strokeLineJoin = StrokeJoin.Round
|
||||||
|
) {
|
||||||
|
moveTo(12f, 18f)
|
||||||
|
verticalLineTo(6f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return _IncosaxAdd!!
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("ObjectPropertyName")
|
||||||
|
private var _IncosaxAdd: ImageVector? = null
|
||||||
Loading…
Reference in New Issue
Block a user