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
5 changes: 3 additions & 2 deletions desktopApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,15 @@ dependencies {
implementation(libs.reorderable)
implementation(libs.platformtools.darkmodedetector)
implementation(libs.jna)
implementation("it.sauronsoftware:junique:1.0.4")
}

compose.desktop {
application {
mainClass = "dev.dimension.flare.MainKt"

nativeDistributions {
targetFormats(TargetFormat.Pkg, TargetFormat.Exe)
targetFormats(TargetFormat.Pkg, TargetFormat.Exe, TargetFormat.Deb)
packageName = "Flare"
val buildVersion = System.getenv("BUILD_VERSION")?.toString()?.takeIf {
// match semantic versioning
Expand Down Expand Up @@ -84,7 +85,7 @@ compose.desktop {
iconFile.set(project.file("resources/ic_launcher.ico"))
}
linux {
modules("jdk.security.auth")
iconFile.set(project.file("resources/ic_launcher.png"))
}
appResourcesRootDir.set(file("resources"))
}
Expand Down
Binary file added desktopApp/resources/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
80 changes: 76 additions & 4 deletions desktopApp/src/main/kotlin/dev/dimension/flare/Main.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package dev.dimension.flare

import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
Expand All @@ -16,7 +14,9 @@ import coil3.ImageLoader
import coil3.compose.setSingletonImageLoaderFactory
import coil3.network.ktor3.KtorNetworkFetcherFactory
import coil3.request.crossfade
import dev.dimension.flare.common.APPSCHEMA
import dev.dimension.flare.common.DeeplinkHandler
import dev.dimension.flare.common.FlareWindowManager
import dev.dimension.flare.common.NativeWindowBridge
import dev.dimension.flare.common.NoopIPC
import dev.dimension.flare.common.SandboxHelper
Expand All @@ -31,15 +31,29 @@ import dev.dimension.flare.ui.route.WindowRouter
import dev.dimension.flare.ui.theme.FlareTheme
import dev.dimension.flare.ui.theme.ProvideThemeSettings
import io.github.kdroidfilter.platformtools.darkmodedetector.windows.setWindowsAdaptiveTitleBar
import it.sauronsoftware.junique.AlreadyLockedException
import it.sauronsoftware.junique.JUnique
import org.apache.commons.lang3.SystemUtils
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.koinInject
import org.koin.core.context.startKoin
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
import java.awt.Desktop
import java.io.File
import java.nio.file.Files
import java.nio.file.Paths
import kotlin.io.path.absolutePathString

fun main(args: Array<String>) {
if (SystemUtils.IS_OS_LINUX && isRunning(args)) {
return
}
if (SystemUtils.IS_OS_LINUX) {
ensureMimeInfo()
ensureDesktopEntry()
}
SandboxHelper.configureSandboxArgs()
val ports = WindowsIPC.parsePorts(args)
val platformIPC =
Expand All @@ -58,6 +72,7 @@ fun main(args: Array<String>) {
desktopModule + KoinHelper.modules() + composeUiModule +
module {
single { platformIPC }
singleOf(::FlareWindowManager)
},
)
}
Expand All @@ -82,7 +97,7 @@ fun main(args: Array<String>) {
}.crossfade(true)
.build()
}
val extraWindowRoutes = remember { mutableStateMapOf<String, FloatingWindowState>() }
val extraWindowRoutes = koinInject<FlareWindowManager>()
val nativeWindowBridge = koinInject<NativeWindowBridge>()

fun openWindow(
Expand Down Expand Up @@ -116,7 +131,9 @@ fun main(args: Array<String>) {
size = DpSize(520.dp, 840.dp),
),
) {
window.setWindowsAdaptiveTitleBar()
if (SystemUtils.IS_OS_WINDOWS) {
window.setWindowsAdaptiveTitleBar()
}
FlareTheme {
FlareApp(
onWindowRoute = {
Expand Down Expand Up @@ -170,3 +187,58 @@ fun main(args: Array<String>) {
}
}
}

private const val ENTRY_FILE_NAME = "flare.desktop"
private const val LOCK_ID = "dev.dimensiondev.flare"

private fun ensureDesktopEntry() {
val entryFile =
File("${System.getProperty("user.home")}/.local/share/applications/$ENTRY_FILE_NAME")
if (!entryFile.exists()) {
entryFile.createNewFile()
}
val path = Files.readSymbolicLink(Paths.get("/proc/self/exe"))
entryFile.writeText(
"[Desktop Entry]${System.lineSeparator()}" +
"Type=Application${System.lineSeparator()}" +
"Name=Flare${System.lineSeparator()}" +
"Icon=\"${path.parent.parent.absolutePathString() + "/lib/Flare.png" + "\""}${System.lineSeparator()}" +
"Exec=\"${path.absolutePathString() + "\" %u"}${System.lineSeparator()}" +
"Terminal=false${System.lineSeparator()}" +
"Categories=Network;Internet;${System.lineSeparator()}" +
"MimeType=application/x-$APPSCHEMA;x-scheme-handler/$APPSCHEMA;",
)
}

private fun ensureMimeInfo() {
val file = File("${System.getProperty("user.home")}/.local/share/applications/mimeinfo.cache")
if (!file.exists()) {
file.createNewFile()
}
val text = file.readText()
if (text.isEmpty() || text.isBlank()) {
file.writeText("[MIME Cache]${System.lineSeparator()}")
}
if (!file.readText().contains("x-scheme-handler/$APPSCHEMA=$ENTRY_FILE_NAME;")) {
file.appendText("${System.lineSeparator()}x-scheme-handler/$APPSCHEMA=$ENTRY_FILE_NAME;")
}
}

private fun isRunning(args: Array<String>): Boolean {
val running =
try {
JUnique.acquireLock(LOCK_ID) {
DeeplinkHandler.handleDeeplink(it)
null
}
false
} catch (e: AlreadyLockedException) {
true
}
if (running) {
args.forEach {
JUnique.sendMessage(LOCK_ID, it)
}
}
return running
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package dev.dimension.flare.common

import androidx.compose.runtime.mutableStateMapOf
import dev.dimension.flare.ui.route.FloatingWindowState
import dev.dimension.flare.ui.route.Route

internal class FlareWindowManager {
private val windows = mutableStateMapOf<String, FloatingWindowState>()

fun containsKey(key: String): Boolean = windows.containsKey(key)

operator fun get(key: String): FloatingWindowState? = windows[key]

fun put(
key: String,
state: FloatingWindowState,
) {
windows[key] = state
}

fun put(
key: String,
route: Route.WindowRoute,
) {
windows[key] = FloatingWindowState(route)
}

fun remove(key: String) {
windows.remove(key)
}

fun clear() {
windows.clear()
}

inline fun forEach(action: (Map.Entry<String, FloatingWindowState>) -> Unit) {
windows.forEach(action)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package dev.dimension.flare.common
import dev.dimension.flare.common.macos.MacosBridge
import dev.dimension.flare.common.windows.WindowsBridge
import dev.dimension.flare.ui.model.UiTimeline
import dev.dimension.flare.ui.model.map
import dev.dimension.flare.ui.model.takeSuccess
import dev.dimension.flare.ui.presenter.status.StatusPresenter
import dev.dimension.flare.ui.route.Route
Expand All @@ -18,12 +17,15 @@ import org.apache.commons.lang3.SystemUtils
internal class NativeWindowBridge(
private val scope: CoroutineScope,
private val windowsBridge: WindowsBridge,
private val windowManager: FlareWindowManager,
) {
fun openImageImageViewer(url: String) {
if (SystemUtils.IS_OS_MAC_OSX) {
MacosBridge.openImageViewer(url)
} else if (SystemUtils.IS_OS_WINDOWS) {
windowsBridge.openImageViewer(url)
} else if (SystemUtils.IS_OS_LINUX) {
windowManager.put(url, Route.RawImage(url))
} else {
// TODO: Implement for other platforms
}
Expand Down Expand Up @@ -60,6 +62,17 @@ internal class NativeWindowBridge(
statusKey = statusKey,
userHandle = userHandle,
)
} else if (SystemUtils.IS_OS_LINUX) {
windowManager.put(
key = "${route.statusKey}-${route.index}",
route =
Route.StatusMedia(
statusKey = route.statusKey,
index = route.index,
accountType = route.accountType,
preview = route.preview,
),
)
} else {
// TODO: Implement for other platforms
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package dev.dimension.flare.common

import dev.dimension.flare.common.macos.MacosBridge
import dev.dimension.flare.common.windows.WindowsBridge
import dev.dimension.flare.ui.route.Route
import org.apache.commons.lang3.SystemUtils

internal class WebViewBridge(
private val windowsBridge: WindowsBridge,
private val windowManager: FlareWindowManager,
) {
fun openAndWaitCookies(
url: String,
Expand All @@ -27,6 +29,19 @@ internal class WebViewBridge(
callback(it)
},
)
} else if (SystemUtils.IS_OS_LINUX) {
windowManager.put(
key = url,
route =
Route.WebViewLogin(
url = url,
cookieCallback = {
if (callback(it)) {
windowManager.remove(url)
}
},
),
)
} else {
// TODO: Implement for other platforms
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,11 @@ internal sealed interface Route {
val userKey: MicroBlogKey,
) : ScreenRoute

data class WebViewLogin(
val url: String,
val cookieCallback: ((cookies: String?) -> Unit)?,
) : WindowRoute

companion object {
public fun parse(url: String): Route? {
val deeplinkRoute = DeeplinkRoute.parse(url) ?: return null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import dev.dimension.flare.ui.screen.rss.RssListScreen
import dev.dimension.flare.ui.screen.serviceselect.ServiceSelectScreen
import dev.dimension.flare.ui.screen.settings.LocalCacheScreen
import dev.dimension.flare.ui.screen.settings.SettingsScreen
import dev.dimension.flare.ui.screen.settings.WebViewLoginScreen
import dev.dimension.flare.ui.screen.status.StatusScreen
import dev.dimension.flare.ui.screen.status.VVOCommentScreen
import dev.dimension.flare.ui.screen.status.VVOStatusScreen
Expand Down Expand Up @@ -613,5 +614,8 @@ internal fun WindowScope.RouteContent(
)
},
)

is Route.WebViewLogin ->
WebViewLoginScreen(route = route)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package dev.dimension.flare.ui.screen.settings

import androidx.compose.runtime.Composable
import dev.dimension.flare.ui.route.Route

@Composable
internal fun WebViewLoginScreen(route: Route.WebViewLogin) {
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencyResolutionManagement {
mavenCentral()
maven("https://jitpack.io")
maven("https://central.sonatype.com/repository/maven-snapshots/")
maven("https://github.com/poolborges/maven/raw/master/thirdparty/")
}
}

Expand Down
Loading