Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions tools/idea-plugin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added

- [Web Import] Add error banner for icon loading/download
- [Web Import] Add `css.gg` icons provider
- [Web Import] Add `Octicons` icons provider
- [Web Import] Add `Simple` icons provider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import org.jetbrains.jewel.ui.component.InlineErrorBanner
import org.jetbrains.jewel.ui.component.InlineSuccessBanner
import org.jetbrains.jewel.ui.component.InlineWarningBanner
import org.jetbrains.jewel.ui.component.Text
import org.jetbrains.jewel.ui.component.banner.BannerLinkActionScope

@Composable
fun BannerHost(
Expand Down Expand Up @@ -73,10 +74,23 @@ fun BannerHost(

@Composable
private fun BannerRender(bannerData: BannerData) {
when (val message = bannerData.message) {
is SuccessBanner -> InlineSuccessBanner(text = message.text)
is WarningBanner -> InlineWarningBanner(text = message.text)
is ErrorBanner -> InlineErrorBanner(text = message.text)
val message = bannerData.message
val linkActions: (BannerLinkActionScope.() -> Unit)? = if (message.actions.isEmpty()) {
null
} else {
{
message.actions.forEach { entry ->
action(entry.label) {
entry.onClick()
bannerData.dismiss()
}
}
}
}
when (message) {
is SuccessBanner -> InlineSuccessBanner(text = message.text, linkActions = linkActions)
is WarningBanner -> InlineWarningBanner(text = message.text, linkActions = linkActions)
is ErrorBanner -> InlineErrorBanner(text = message.text, linkActions = linkActions)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,32 @@ package io.github.composegears.valkyrie.jewel.banner
sealed interface BannerMessage {
val text: String
val duration: BannerDuration
val actions: List<BannerAction>

data class SuccessBanner(
override val text: String,
override val duration: BannerDuration = BannerDuration.Short,
override val actions: List<BannerAction> = emptyList(),
) : BannerMessage

data class WarningBanner(
override val text: String,
override val duration: BannerDuration = BannerDuration.Short,
override val actions: List<BannerAction> = emptyList(),
) : BannerMessage

data class ErrorBanner(
override val text: String,
override val duration: BannerDuration = BannerDuration.Short,
override val actions: List<BannerAction> = emptyList(),
) : BannerMessage
}

data class BannerAction(
val label: String,
val onClick: () -> Unit,
)

enum class BannerDuration {
Short,
Long,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
import com.composegears.tiamat.navigation.MutableSavedState
import com.composegears.tiamat.navigation.asStateFlow
import com.composegears.tiamat.navigation.recordOf
import com.intellij.openapi.diagnostic.Logger
import io.github.composegears.valkyrie.parser.unified.util.IconNameFormatter
import io.github.composegears.valkyrie.sdk.core.extensions.safeAs
import io.github.composegears.valkyrie.ui.screen.webimport.common.domain.StandardIconProvider
Expand Down Expand Up @@ -42,6 +43,8 @@ class StandardIconViewModel(
private val _events = Channel<StandardIconEvent>()
val events = _events.receiveAsFlow()

private val logger = Logger.getInstance(StandardIconViewModel::class.java)

private var downloadJob: Job? = null
private var fontLoadJob: Job? = null
private var prefetchJob: Job? = null
Expand Down Expand Up @@ -116,6 +119,7 @@ class StandardIconViewModel(
selectedStyleId = selectedStyle?.id,
)
}.onFailure { error ->
if (error is CancellationException) throw error
stateRecord.value = StandardState.Error(
"Error loading ${provider.providerName} icons: ${error.message}",
)
Expand Down Expand Up @@ -144,7 +148,14 @@ class StandardIconViewModel(
updateSuccess { it.copy(fontByteArray = bytes) }
}.onFailure { error ->
if (error is CancellationException) throw error
logger.warn("Failed to load ${provider.providerName} font", error)
updateSuccess { it.copy(fontByteArray = null) }
_events.send(
StandardIconEvent.FontLoadFailed(
providerName = provider.providerName,
reason = error.toWebFailureReason(),
),
)
}
} else {
updateSuccess { it.copy(fontByteArray = cachedFont) }
Expand All @@ -166,6 +177,15 @@ class StandardIconViewModel(
name = IconNameFormatter.format(icon.exportName),
),
)
}.onFailure { error ->
if (error is CancellationException) throw error
logger.warn("Failed to download ${provider.providerName} icon", error)
_events.send(
StandardIconEvent.IconDownloadFailed(
providerName = provider.providerName,
reason = error.toWebFailureReason(),
),
)
}
}
}
Expand Down Expand Up @@ -243,6 +263,8 @@ class StandardIconViewModel(
if (fontCache[style.id] == null) {
runCatching {
provider.loadFontBytes(style)
}.onFailure { error ->
if (error is CancellationException) throw error
}.onSuccess { bytes ->
fontCache[style.id] = bytes
}
Expand All @@ -268,6 +290,16 @@ sealed interface StandardIconEvent {
val svgContent: String,
val name: String,
) : StandardIconEvent

data class IconDownloadFailed(
val providerName: String,
val reason: WebFailureReason,
) : StandardIconEvent

data class FontLoadFailed(
val providerName: String,
val reason: WebFailureReason,
) : StandardIconEvent
}

@Stable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ import io.github.composegears.valkyrie.jewel.BackAction
import io.github.composegears.valkyrie.jewel.HorizontalDivider
import io.github.composegears.valkyrie.jewel.Title
import io.github.composegears.valkyrie.jewel.Toolbar
import io.github.composegears.valkyrie.jewel.banner.BannerAction
import io.github.composegears.valkyrie.jewel.banner.BannerDuration
import io.github.composegears.valkyrie.jewel.banner.BannerMessage.ErrorBanner
import io.github.composegears.valkyrie.jewel.banner.rememberBannerManager
import io.github.composegears.valkyrie.jewel.ui.placeholder.EmptyPlaceholder
import io.github.composegears.valkyrie.jewel.ui.placeholder.ErrorPlaceholder
import io.github.composegears.valkyrie.jewel.ui.placeholder.LoadingPlaceholder
Expand All @@ -60,6 +64,7 @@ import io.github.composegears.valkyrie.ui.screen.webimport.common.ui.SidePanel
import io.github.composegears.valkyrie.ui.screen.webimport.common.ui.WebIconTopActions
import io.github.composegears.valkyrie.ui.screen.webimport.common.ui.ZOOM_DEFAULT_SCALE
import io.github.composegears.valkyrie.ui.screen.webimport.common.ui.ZoomFloatingBar
import io.github.composegears.valkyrie.util.ValkyrieBundle.message
import io.github.composegears.valkyrie.util.stringResource
import kotlinx.coroutines.launch
import org.jetbrains.jewel.foundation.theme.LocalContentColor
Expand All @@ -78,13 +83,45 @@ internal fun StandardImportScreen(
}
val state by viewModel.state.collectAsState()
val variableFontConfig by provider.variableFontConfig.collectAsState()
val bannerManager = rememberBannerManager()
val currentOnIconDownloaded by rememberUpdatedState(onIconDownload)
var selectedIcon by rememberMutableState<StandardIcon?> { null }

ObserveEvent(viewModel.events) { event ->
when (event) {
is StandardIconEvent.IconDownloaded -> {
currentOnIconDownloaded(event)
}
is StandardIconEvent.IconDownloadFailed -> {
selectedIcon = null
bannerManager.show(
Comment thread
t-regbs marked this conversation as resolved.
message = ErrorBanner(
text = message(
"web.import.error.icon.download",
event.providerName,
message(event.reason.bundleKey),
),
),
)
}
is StandardIconEvent.FontLoadFailed -> {
bannerManager.show(
message = ErrorBanner(
text = message(
"web.import.error.font.load",
event.providerName,
message(event.reason.bundleKey),
),
duration = BannerDuration.Indefinite,
actions = listOf(
BannerAction(
label = message("web.import.error.retry"),
onClick = { viewModel.downloadFont() },
),
),
),
)
}
}
}

Expand All @@ -95,8 +132,12 @@ internal fun StandardImportScreen(
resolveFontWeight = provider::resolveFontWeight,
variableFontConfig = variableFontConfig,
customizationContent = customizationContent,
selectedIcon = selectedIcon,
onBack = onBack,
onSelectIcon = viewModel::downloadIcon,
onSelectIcon = { icon ->
selectedIcon = icon
viewModel.downloadIcon(icon)
},
onSelectCategory = viewModel::selectCategory,
onSelectStyle = viewModel::selectStyle,
onSearchQueryChange = viewModel::updateSearchQuery,
Expand All @@ -113,6 +154,7 @@ private fun StandardImportScreenUI(
resolveFontWeight: (IconStyle?) -> FontWeight,
variableFontConfig: VariableFontConfig?,
customizationContent: (@Composable (onClose: () -> Unit) -> Unit)?,
selectedIcon: StandardIcon?,
onBack: () -> Unit,
onSelectIcon: (StandardIcon) -> Unit,
onSelectCategory: (InferredCategory) -> Unit,
Expand Down Expand Up @@ -164,6 +206,7 @@ private fun StandardImportScreenUI(
resolveFontWeight = resolveFontWeight,
variableFontConfig = variableFontConfig,
customizationContent = customizationContent,
selectedIcon = selectedIcon,
onSelectIcon = onSelectIcon,
onSelectCategory = onSelectCategory,
onSelectStyle = onSelectStyle,
Expand All @@ -183,15 +226,14 @@ private fun IconsContent(
resolveFontWeight: (IconStyle?) -> FontWeight,
variableFontConfig: VariableFontConfig?,
customizationContent: (@Composable (onClose: () -> Unit) -> Unit)?,
selectedIcon: StandardIcon?,
onSelectIcon: (StandardIcon) -> Unit,
onSelectCategory: (InferredCategory) -> Unit,
onSelectStyle: (IconStyle) -> Unit,
onSearchQueryChange: (String) -> Unit,
onSettingsChange: (SizeSettings) -> Unit,
) {
val scope = rememberCoroutineScope()

var selectedIcon by rememberMutableState<StandardIcon?> { null }
var isSidePanelOpen by rememberMutableState { false }
var scaleFactor by rememberMutableState { ZOOM_DEFAULT_SCALE }
val lazyGridState = rememberLazyGridState()
Expand Down Expand Up @@ -281,10 +323,7 @@ private fun IconsContent(
IconCard(
name = icon.displayName,
selected = icon == selectedIcon,
onClick = {
selectedIcon = icon
onSelectIcon(icon)
},
onClick = { onSelectIcon(icon) },
iconContent = {
FontIcon(
modifier = Modifier.size(iconSizeDp),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.github.composegears.valkyrie.ui.screen.webimport.common

import java.io.IOException

enum class WebFailureReason(val bundleKey: String) {
Network("web.import.error.reason.network"),
Server("web.import.error.reason.server"),
Unknown("web.import.error.reason.unknown"),
}

fun Throwable.toWebFailureReason(): WebFailureReason = when (this) {
is IOException -> WebFailureReason.Network
is IllegalStateException -> WebFailureReason.Server
else -> WebFailureReason.Unknown
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
import com.composegears.tiamat.navigation.MutableSavedState
import com.composegears.tiamat.navigation.asStateFlow
import com.composegears.tiamat.navigation.recordOf
import com.intellij.openapi.diagnostic.Logger
import io.github.composegears.valkyrie.parser.unified.util.IconNameFormatter
import io.github.composegears.valkyrie.sdk.core.extensions.safeAs
import io.github.composegears.valkyrie.ui.screen.webimport.common.domain.WebIconProvider
Expand All @@ -16,6 +17,7 @@ import io.github.composegears.valkyrie.ui.screen.webimport.common.domain.icon.St
import io.github.composegears.valkyrie.ui.screen.webimport.common.domain.icon.WebIconConfig
import io.github.composegears.valkyrie.ui.screen.webimport.common.domain.settings.SizeSettings
import io.github.composegears.valkyrie.ui.screen.webimport.common.util.filterByCategory
import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
Expand All @@ -36,6 +38,8 @@ class WebIconViewModel<Icon : StyledWebIcon, Config : WebIconConfig<Icon>>(
private val _events = Channel<WebIconEvent>()
val events = _events.receiveAsFlow()

private val logger = Logger.getInstance(WebIconViewModel::class.java)

private var downloadJob: Job? = null

init {
Expand Down Expand Up @@ -81,6 +85,7 @@ class WebIconViewModel<Icon : StyledWebIcon, Config : WebIconConfig<Icon>>(
selectedStyle = selectedStyle,
)
}.onFailure { error ->
if (error is CancellationException) throw error
stateRecord.value = WebIconState.Error(
"Error loading ${provider.providerName} icons: ${error.message}",
)
Expand All @@ -100,6 +105,15 @@ class WebIconViewModel<Icon : StyledWebIcon, Config : WebIconConfig<Icon>>(
name = IconNameFormatter.format(icon.exportName),
),
)
}.onFailure { error ->
if (error is CancellationException) throw error
logger.warn("Failed to download ${provider.providerName} icon", error)
_events.send(
WebIconEvent.IconDownloadFailed(
providerName = provider.providerName,
reason = error.toWebFailureReason(),
),
)
}
}
}
Expand Down Expand Up @@ -157,6 +171,11 @@ sealed interface WebIconEvent {
val svgContent: String,
val name: String,
) : WebIconEvent

data class IconDownloadFailed(
val providerName: String,
val reason: WebFailureReason,
) : WebIconEvent
}

@Stable
Expand Down
Loading
Loading