diff --git a/FloconDesktop/composeApp/src/androidMain/kotlin/io/github/openflocon/flocondesktop/common/utils/Link.android.kt b/FloconDesktop/composeApp/src/androidMain/kotlin/io/github/openflocon/flocondesktop/common/utils/Link.android.kt new file mode 100644 index 00000000..d96bd61e --- /dev/null +++ b/FloconDesktop/composeApp/src/androidMain/kotlin/io/github/openflocon/flocondesktop/common/utils/Link.android.kt @@ -0,0 +1,7 @@ +package io.github.openflocon.flocondesktop.common.utils + +import java.net.URI + +actual fun openInBrowser(uri: URI) { + // no op +} diff --git a/FloconDesktop/composeApp/src/commonMain/composeResources/drawable/app_icon.png b/FloconDesktop/composeApp/src/commonMain/composeResources/drawable/app_icon.png index 34186295..0aa2724f 100644 Binary files a/FloconDesktop/composeApp/src/commonMain/composeResources/drawable/app_icon.png and b/FloconDesktop/composeApp/src/commonMain/composeResources/drawable/app_icon.png differ diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/utils/Link.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/utils/Link.kt new file mode 100644 index 00000000..4e90f495 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/utils/Link.kt @@ -0,0 +1,5 @@ +package io.github.openflocon.flocondesktop.common.utils + +import java.net.URI + +expect fun openInBrowser(uri: URI) diff --git a/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/Main.kt b/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/Main.kt index 368bc800..f6bc49a9 100644 --- a/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/Main.kt +++ b/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/Main.kt @@ -1,6 +1,9 @@ package io.github.openflocon.flocondesktop +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState @@ -9,6 +12,7 @@ import coil3.compose.setSingletonImageLoaderFactory import coil3.network.ktor3.KtorNetworkFetcherFactory import flocondesktop.composeapp.generated.resources.Res import flocondesktop.composeapp.generated.resources.app_icon_small +import io.github.openflocon.flocondesktop.about.AboutScreen import io.github.openflocon.flocondesktop.window.MIN_WINDOW_HEIGHT import io.github.openflocon.flocondesktop.window.MIN_WINDOW_WIDTH import io.github.openflocon.flocondesktop.window.WindowStateData @@ -16,12 +20,19 @@ import io.github.openflocon.flocondesktop.window.WindowStateSaver import io.github.openflocon.flocondesktop.window.size import io.github.openflocon.flocondesktop.window.windowPosition import org.jetbrains.compose.resources.painterResource +import java.awt.Desktop import java.awt.Dimension fun main() { System.setProperty("apple.awt.application.name", "Flocon") return application { + var openAbout by remember { mutableStateOf(false) } + + Desktop.getDesktop().setAboutHandler { + openAbout = true + } + setSingletonImageLoaderFactory { context -> ImageLoader .Builder(context) @@ -61,6 +72,12 @@ fun main() { window.minimumSize = Dimension(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT) App() + + if (openAbout) { + AboutScreen( + onCloseRequest = { openAbout = false } + ) + } } } } diff --git a/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/about/AboutScreen.kt b/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/about/AboutScreen.kt new file mode 100644 index 00000000..050c79e1 --- /dev/null +++ b/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/about/AboutScreen.kt @@ -0,0 +1,92 @@ +package io.github.openflocon.flocondesktop.about + + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowPlacement +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.rememberWindowState +import flocondesktop.composeapp.generated.resources.Res +import flocondesktop.composeapp.generated.resources.app_icon +import io.github.openflocon.flocondesktop.common.utils.openInBrowser +import io.github.openflocon.library.designsystem.FloconTheme +import io.github.openflocon.library.designsystem.components.FloconSurface +import io.github.openflocon.library.designsystem.components.FloconTextButton +import org.jetbrains.compose.resources.painterResource +import java.net.URI + +@Composable +internal fun AboutScreen( + onCloseRequest: () -> Unit +) { + Window( + title = "About", + onCloseRequest = onCloseRequest, + state = rememberWindowState( + placement = WindowPlacement.Floating, + position = WindowPosition(Alignment.Center), + size = DpSize(Dp.Unspecified, Dp.Unspecified) + ) + ) { + FloconSurface { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .padding(16.dp) + ) { + Image( + painter = painterResource(Res.drawable.app_icon), + contentDescription = null, + modifier = Modifier.size(120.dp) + ) + Text( + text = "Flocon", + style = FloconTheme.typography.titleSmall, + color = FloconTheme.colorPalette.onSurface + ) + FloconTextButton( + onClick = { + openInBrowser(URI.create("https://github.com/openflocon/Flocon")) + } + ) { + Text( + text = "GitHub" + ) + } + FlowRow( + verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically), + horizontalArrangement = Arrangement.spacedBy( + 4.dp, + Alignment.CenterHorizontally + ), + modifier = Modifier + .widthIn(max = 400.dp) + .heightIn(max = 400.dp) + .verticalScroll(rememberScrollState()) + ) { + contributors.fastForEach { + ContributorView(it) + } + } + } + } + } +} diff --git a/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/about/Contributor.kt b/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/about/Contributor.kt new file mode 100644 index 00000000..4d99fbcc --- /dev/null +++ b/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/about/Contributor.kt @@ -0,0 +1,26 @@ +package io.github.openflocon.flocondesktop.about + +import androidx.compose.runtime.Immutable + +@Immutable +internal data class Contributor( + val firstName: String, + val lastName: String, + val profile: String, + val image: String +) + +internal val contributors = listOf( + Contributor( + firstName = "Florent", + lastName = "Champigny", + profile = "https://github.com/florent37", + image = "https://avatars.githubusercontent.com/u/5754972?v=4" + ), + Contributor( + firstName = "Raphael", + lastName = "Teyssandier", + profile = "https://github.com/doTTTTT", + image = "https://avatars.githubusercontent.com/u/13266870?v=4" + ) +) diff --git a/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/about/ContributorView.kt b/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/about/ContributorView.kt new file mode 100644 index 00000000..3621d579 --- /dev/null +++ b/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/about/ContributorView.kt @@ -0,0 +1,86 @@ +package io.github.openflocon.flocondesktop.about + +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import coil3.compose.LocalPlatformContext +import coil3.request.ImageRequest +import io.github.openflocon.flocondesktop.common.utils.openInBrowser +import io.github.openflocon.library.designsystem.FloconTheme +import java.net.URI + +@Composable +internal fun ContributorView( + contributor: Contributor +) { + val interactionSource = remember { MutableInteractionSource() } + val hovered by interactionSource.collectIsHoveredAsState() + val borderColor = if (hovered) FloconTheme.colorPalette.onSurface else Color.Transparent + val shape = RoundedCornerShape(10.dp) + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .width(120.dp) + .height(140.dp) + .clip(shape) + .border(width = 2.dp, color = borderColor, shape = shape) + .clickable( + onClick = { openInBrowser(URI.create(contributor.profile)) }, + interactionSource = interactionSource, + indication = LocalIndication.current + ) + .padding(8.dp) + ) { + AsyncImage( + model = ImageRequest.Builder(LocalPlatformContext.current) + .data(contributor.image) + .listener(onError = { _, error -> + error.throwable.printStackTrace() + }) + .build(), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(80.dp) + .clip(shape) + ) + Spacer(Modifier.weight(1f)) + Text( + text = contributor.firstName, + style = FloconTheme.typography.bodyMedium, + color = FloconTheme.colorPalette.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(Modifier.height(4.dp)) + Text( + text = contributor.lastName, + style = FloconTheme.typography.bodySmall, + color = FloconTheme.colorPalette.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} diff --git a/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/common/utils/Link.desktop.kt b/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/common/utils/Link.desktop.kt new file mode 100644 index 00000000..f9be6f30 --- /dev/null +++ b/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/common/utils/Link.desktop.kt @@ -0,0 +1,16 @@ +package io.github.openflocon.flocondesktop.common.utils + +import java.awt.Desktop +import java.net.URI + +actual fun openInBrowser(uri: URI) { + val osName by lazy(LazyThreadSafetyMode.NONE) { System.getProperty("os.name").lowercase() } + val desktop = Desktop.getDesktop() + + when { + Desktop.isDesktopSupported() && desktop.isSupported(Desktop.Action.BROWSE) -> desktop.browse(uri) + "mac" in osName -> Runtime.getRuntime().exec("open $uri") + "nix" in osName || "nux" in osName -> Runtime.getRuntime().exec("xdg-open $uri") + else -> throw RuntimeException("cannot open $uri") + } +} diff --git a/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconTextButton.kt b/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconTextButton.kt new file mode 100644 index 00000000..b5e8137f --- /dev/null +++ b/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconTextButton.kt @@ -0,0 +1,22 @@ +package io.github.openflocon.library.designsystem.components + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import io.github.openflocon.library.designsystem.FloconTheme + +@Composable +fun FloconTextButton( + onClick: () -> Unit, + content: @Composable RowScope.() -> Unit +) { + TextButton( + onClick = onClick, + colors = ButtonDefaults.textButtonColors( + containerColor = FloconTheme.colorPalette.inverseSurface, + contentColor = FloconTheme.colorPalette.inverseOnSurface + ), + content = content + ) +}