diff --git a/desktop/.gitignore b/desktop/.gitignore new file mode 100644 index 0000000..956c004 --- /dev/null +++ b/desktop/.gitignore @@ -0,0 +1,2 @@ +/build +/release \ No newline at end of file diff --git a/desktop/.run/desktop.run.xml b/desktop/.run/desktop.run.xml new file mode 100644 index 0000000..f91b2c2 --- /dev/null +++ b/desktop/.run/desktop.run.xml @@ -0,0 +1,21 @@ + + + + + + + true + + + \ No newline at end of file diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts new file mode 100644 index 0000000..1376d84 --- /dev/null +++ b/desktop/build.gradle.kts @@ -0,0 +1,69 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + +plugins { + kotlin("jvm") + id("org.jetbrains.compose") + java +} + +group = "com.wbrawner.pihelper" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() +} + +val osName = System.getProperty("os.name") +val targetOs = when { + osName == "Mac OS X" -> "macos" + osName.startsWith("Win") -> "windows" + osName.startsWith("Linux") -> "linux" + else -> error("Unsupported OS: $osName") +} + +val targetArch = when (val osArch = System.getProperty("os.arch")) { + "x86_64", "amd64" -> "x64" + "aarch64" -> "arm64" + else -> error("Unsupported arch: $osArch") +} + +val skikoVersion = "0.7.77" // or any more recent version +val target = "${targetOs}-${targetArch}" + + +dependencies { + // Note, if you develop a library, you should use compose.desktop.common. + // compose.desktop.currentOs should be used in launcher-sourceSet + // (in a separate module for demo project and in testMain). + // With compose.desktop.common you will also lose @Preview functionality + implementation(project(":shared")) + implementation(compose.desktop.currentOs) +// implementation("org.jetbrains.skiko:skiko-awt-runtime-$target:$skikoVersion") + implementation(libs.kotlinx.coroutines.jvm) + implementation("ch.qos.logback:logback-classic:1.4.5") +} + +compose.desktop { + application { + mainClass = "MainKt" + + nativeDistributions { + includeAllModules = true + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "Pi-helper" + packageVersion = "1.0.0" + + macOS { + iconFile.set(project.file("src/main/resources/icon.icns")) + } + windows { + iconFile.set(project.file("icon.ico")) + } + linux { + iconFile.set(project.file("icon.png")) + } + } + } +} diff --git a/desktop/src/main/kotlin/Main.kt b/desktop/src/main/kotlin/Main.kt new file mode 100644 index 0000000..2f80de8 --- /dev/null +++ b/desktop/src/main/kotlin/Main.kt @@ -0,0 +1,135 @@ +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.window.Tray +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberTrayState +import com.wbrawner.pihelper.shared.* +import com.wbrawner.pihelper.shared.State +import com.wbrawner.pihelper.shared.ui.AddScreen +import com.wbrawner.pihelper.shared.ui.AuthScreen +import com.wbrawner.pihelper.shared.ui.InfoScreen +import com.wbrawner.pihelper.shared.ui.MainScreen +import com.wbrawner.pihelper.shared.ui.theme.PihelperTheme +import java.net.InetAddress + +val store = Store(PiholeAPIService.create()) + +@OptIn(ExperimentalAnimationApi::class) +@Composable +@Preview +fun App(state: State) { + val error by store.effects.collectAsState(Effect.Empty) + + PihelperTheme { + when (state.route) { + Route.CONNECT -> AddScreen( + scanNetwork = { + store.dispatch(Action.Scan(InetAddress.getLocalHost().hostAddress)) + }, + connectToPihole = { store.dispatch(Action.Connect(it)) }, + loading = state.loading, + error = error as? Effect.Error + ) + + Route.AUTH -> AuthScreen(store) + Route.HOME -> MainScreen(store) + Route.ABOUT -> InfoScreen(store) + else -> { + Text("Not yet implemented") + } + } + } +} + +fun main() = application { + LaunchedEffect(Unit) { + System.loadLibrary("app/Pi-helper.app/Contents/app/libskiko-macos-arm64.dylib") + println("loaded skiko") + System.setProperty("apple.awt.enableTemplateImages", "true") + } + val state by store.state.collectAsState() + val trayState = rememberTrayState() + var isOpen by remember { mutableStateOf(state.apiKey.isNullOrBlank()) } + val statusText = when (val status = state.status) { + is Status.Enabled -> "Enabled" + is Status.Disabled -> status.timeRemaining?.let { "Disabled (${it})" } ?: "Disabled" + else -> "Not connected" + } + Tray( + state = trayState, + tooltip = statusText, + icon = painterResource("IconTemplate.png"), + menu = { + Item( + text = statusText, + enabled = false, + onClick = {} + ) + if (state.status is Status.Disabled) { + Item( + "Enable", + onClick = { + store.dispatch(Action.Enable) + } + ) + } else if (state.status is Status.Enabled) { + Item( + "Disable for 10 seconds", + onClick = { + store.dispatch(Action.Disable(duration = 10)) + } + ) + Item( + "Disable for 30 seconds", + onClick = { + store.dispatch(Action.Disable(duration = 30)) + } + ) + Item( + "Disable for 1 minute", + onClick = { + store.dispatch(Action.Disable(duration = 30)) + } + ) + Item( + "Disable for 5 minutes", + onClick = { + store.dispatch(Action.Disable(duration = 30)) + } + ) + Item( + "Disable permanently", + onClick = { + store.dispatch(Action.Disable()) + } + ) + } + Item( + text = if (isOpen) "Hide window" else "Show window", + onClick = { + isOpen = !isOpen + } + ) + Item( + "Exit", + onClick = { + isOpen = false + } + ) + } + ) + + if (isOpen) { + Window( + title = "Pi-helper", + onCloseRequest = { + isOpen = false + }) { + App(state) + } + } +} diff --git a/desktop/src/main/resources/IconTemplate.png b/desktop/src/main/resources/IconTemplate.png new file mode 100644 index 0000000..352f7dc Binary files /dev/null and b/desktop/src/main/resources/IconTemplate.png differ diff --git a/desktop/src/main/resources/icon.icns b/desktop/src/main/resources/icon.icns new file mode 100644 index 0000000..d1a969d Binary files /dev/null and b/desktop/src/main/resources/icon.icns differ diff --git a/desktop/src/main/resources/icon.png b/desktop/src/main/resources/icon.png new file mode 100644 index 0000000..dc8c839 Binary files /dev/null and b/desktop/src/main/resources/icon.png differ