diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml
index af6a4f9..5c15dcf 100644
--- a/.idea/androidTestResultsUserPreferences.xml
+++ b/.idea/androidTestResultsUserPreferences.xml
@@ -126,6 +126,7 @@
diff --git a/.idea/artifacts/shared_desktop.xml b/.idea/artifacts/shared_desktop.xml
new file mode 100644
index 0000000..c9e4707
--- /dev/null
+++ b/.idea/artifacts/shared_desktop.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/shared/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index fb7f4a8..b589d56 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
new file mode 100644
index 0000000..99d4b26
--- /dev/null
+++ b/.idea/deploymentTargetDropDown.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index e900b00..2586163 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -4,17 +4,17 @@
-
-
-
+
+
+
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
index 147f384..e2c417b 100644
--- a/.idea/jarRepositories.xml
+++ b/.idea/jarRepositories.xml
@@ -31,5 +31,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinScripting.xml b/.idea/kotlinScripting.xml
deleted file mode 100644
index e56386c..0000000
--- a/.idea/kotlinScripting.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
- 2147483647
- true
-
-
-
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 0000000..69e8615
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 284e372..1defa5b 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,3 @@
-
@@ -61,7 +60,11 @@
-
+
+
+
+
+
diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml
new file mode 100644
index 0000000..2b63946
--- /dev/null
+++ b/.idea/uiDesigner.xml
@@ -0,0 +1,124 @@
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 94a25f7..08ebc54 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -2,5 +2,6 @@
+
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/wbrawner/pihelper/util/AddScreenRobot.kt b/app/src/androidTest/java/com/wbrawner/pihelper/util/AddScreenRobot.kt
index 9a3d2c7..900979c 100644
--- a/app/src/androidTest/java/com/wbrawner/pihelper/util/AddScreenRobot.kt
+++ b/app/src/androidTest/java/com/wbrawner/pihelper/util/AddScreenRobot.kt
@@ -4,9 +4,7 @@ import android.content.Context
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.test.platform.app.InstrumentationRegistry
-import com.wbrawner.pihelper.ADD_SCREEN_TAG
import com.wbrawner.pihelper.CONNECT_BUTTON_TAG
-import com.wbrawner.pihelper.HOST_TAG
fun onAddScreen(testRule: ComposeTestRule, actions: AddScreenRobot.() -> Unit) =
AddScreenRobot(testRule).apply { actions() }
@@ -14,30 +12,22 @@ fun onAddScreen(testRule: ComposeTestRule, actions: AddScreenRobot.() -> Unit) =
class AddScreenRobot(private val testRule: ComposeTestRule) {
val context: Context = InstrumentationRegistry.getInstrumentation().context
- init {
- testRule.waitUntil {
- testRule
- .onAllNodesWithTag(ADD_SCREEN_TAG)
- .fetchSemanticsNodes().size == 1
- }
- }
-
infix fun onAuthScreen(actions: AuthScreenRobot.() -> Unit) = AuthScreenRobot(testRule).run {
actions()
}
fun clearHost() =
- testRule.onNode(hasTestTag(HOST_TAG)).performTextClearance()
+ testRule.onNodeWithContentDescription("Pi-hole host input").performTextClearance()
fun inputHost(host: String) =
- testRule.onNode(hasTestTag(HOST_TAG)).performTextInput(host)
+ testRule.onNodeWithContentDescription("Pi-hole host input").performTextInput(host)
fun clickConnect() = testRule.onNode(hasTestTag(CONNECT_BUTTON_TAG)).performClick()
fun verifyErrorMessageIsDisplayed(message: String) {
testRule.waitUntil(2_000) {
testRule
- .onAllNodesWithText(message, substring = true)
+ .onAllNodesWithContentDescription(message, substring = true)
.fetchSemanticsNodes().size == 1
}
}
diff --git a/app/src/androidTest/java/com/wbrawner/pihelper/util/AuthScreenRobot.kt b/app/src/androidTest/java/com/wbrawner/pihelper/util/AuthScreenRobot.kt
index e052509..bbb8a3a 100644
--- a/app/src/androidTest/java/com/wbrawner/pihelper/util/AuthScreenRobot.kt
+++ b/app/src/androidTest/java/com/wbrawner/pihelper/util/AuthScreenRobot.kt
@@ -4,7 +4,7 @@ import android.content.Context
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.test.platform.app.InstrumentationRegistry
-import com.wbrawner.pihelper.*
+import com.wbrawner.pihelper.shared.ui.*
class AuthScreenRobot(private val testRule: ComposeTestRule) {
val context: Context = InstrumentationRegistry.getInstrumentation().context
diff --git a/app/src/androidTest/java/com/wbrawner/pihelper/util/MainScreenRobot.kt b/app/src/androidTest/java/com/wbrawner/pihelper/util/MainScreenRobot.kt
index 0c0e392..4b38dc4 100644
--- a/app/src/androidTest/java/com/wbrawner/pihelper/util/MainScreenRobot.kt
+++ b/app/src/androidTest/java/com/wbrawner/pihelper/util/MainScreenRobot.kt
@@ -4,10 +4,10 @@ import android.content.Context
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.test.platform.app.InstrumentationRegistry
-import com.wbrawner.pihelper.DISABLE_PERMANENT_BUTTON_TAG
-import com.wbrawner.pihelper.ENABLE_BUTTON_TAG
-import com.wbrawner.pihelper.MAIN_SCREEN_TAG
-import com.wbrawner.pihelper.STATUS_TEXT_TAG
+import com.wbrawner.pihelper.shared.ui.DISABLE_PERMANENT_BUTTON_TAG
+import com.wbrawner.pihelper.shared.ui.ENABLE_BUTTON_TAG
+import com.wbrawner.pihelper.shared.ui.MAIN_SCREEN_TAG
+import com.wbrawner.pihelper.shared.ui.STATUS_TEXT_TAG
fun onMainScreen(testRule: ComposeTestRule, actions: MainScreenRobot.() -> Unit) =
MainScreenRobot(testRule).apply { actions() }
diff --git a/app/src/main/java/com/wbrawner/pihelper/AddScreen.kt b/app/src/main/java/com/wbrawner/pihelper/AddScreen.kt
index 57a5183..ef22b98 100644
--- a/app/src/main/java/com/wbrawner/pihelper/AddScreen.kt
+++ b/app/src/main/java/com/wbrawner/pihelper/AddScreen.kt
@@ -5,28 +5,16 @@ import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.widget.Toast
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.Text
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalSoftwareKeyboardController
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
import com.wbrawner.pihelper.shared.Action
import com.wbrawner.pihelper.shared.Effect
import com.wbrawner.pihelper.shared.Store
-import com.wbrawner.pihelper.ui.DayNightPreview
-import com.wbrawner.pihelper.ui.PihelperTheme
+import com.wbrawner.pihelper.shared.ui.AddScreen
+import com.wbrawner.pihelper.shared.ui.OrDivider
+import com.wbrawner.pihelper.shared.ui.theme.PihelperTheme
import java.net.Inet4Address
val emulatorBuildModels = listOf(
@@ -34,11 +22,6 @@ val emulatorBuildModels = listOf(
"sdk_gphone64_arm64"
)
-const val ADD_SCREEN_TAG = "addScreen"
-const val CONNECT_BUTTON_TAG = "connectButton"
-const val HOST_TAG = "hostInput"
-const val SCAN_BUTTON_TAG = "scanButton"
-
@Composable
fun AddScreen(store: Store) {
val effect by store.effects.collectAsState(initial = Effect.Empty)
@@ -83,96 +66,6 @@ fun AddScreen(store: Store) {
)
}
-@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
-@Composable
-fun AddScreen(
- scanNetwork: () -> Unit,
- connectToPihole: (String) -> Unit,
- loading: Boolean = false,
- error: Effect.Error? = null
-) {
- val (host: String, setHost: (String) -> Unit) = remember { mutableStateOf("pi.hole") }
- val keyboardController = LocalSoftwareKeyboardController.current
- Column(
- modifier = Modifier
- .testTag(ADD_SCREEN_TAG)
- .padding(16.dp)
- .fillMaxSize(),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
- ) {
- LoadingSpinner(loading)
- Text(
- text = "If you're not sure what the IP address for your Pi-hole is, Pi-helper can " +
- "attempt to find it for you by scanning your network.",
- textAlign = TextAlign.Center
- )
- PrimaryButton(
- modifier = Modifier.testTag(SCAN_BUTTON_TAG),
- text = "Scan Network",
- onClick = scanNetwork
- )
- OrDivider()
- Text(
- text = "If you already know the IP address or host of your Pi-hole, you can also " +
- "enter it below:",
- textAlign = TextAlign.Center
- )
- OutlinedTextField(
- modifier = Modifier
- .testTag(HOST_TAG)
- .fillMaxWidth(),
- value = host,
- onValueChange = setHost,
- label = { Text("Pi-hole Host") }
- )
- PrimaryButton(
- modifier = Modifier.testTag(CONNECT_BUTTON_TAG),
- text = "Connect to Pi-hole",
- onClick = {
- keyboardController?.hide()
- connectToPihole(host)
- }
- )
- error?.let {
- Text(
- color = MaterialTheme.colorScheme.primary,
- textAlign = TextAlign.Center,
- text = "Connection failed: ${it.message}"
- )
- }
- }
-}
-
-@Composable
-fun OrDivider() {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 8.dp),
- horizontalArrangement = Arrangement.Center,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Box(
- modifier = Modifier
- .height(2.dp)
- .weight(1f)
- .padding(end = 8.dp)
- .clip(RectangleShape)
- .background(MaterialTheme.colorScheme.onSurface),
- )
- Text("OR")
- Box(
- modifier = Modifier
- .height(2.dp)
- .weight(1f)
- .padding(start = 8.dp)
- .clip(RectangleShape)
- .background(MaterialTheme.colorScheme.onSurface),
- )
- }
-}
-
@Composable
@DayNightPreview
fun AddScreen_Preview() {
diff --git a/app/src/main/java/com/wbrawner/pihelper/DayNightPreview.kt b/app/src/main/java/com/wbrawner/pihelper/DayNightPreview.kt
new file mode 100644
index 0000000..64063b4
--- /dev/null
+++ b/app/src/main/java/com/wbrawner/pihelper/DayNightPreview.kt
@@ -0,0 +1,8 @@
+package com.wbrawner.pihelper
+
+import android.content.res.Configuration
+import androidx.compose.ui.tooling.preview.Preview
+
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
+annotation class DayNightPreview
\ No newline at end of file
diff --git a/app/src/main/java/com/wbrawner/pihelper/MainActivity.kt b/app/src/main/java/com/wbrawner/pihelper/MainActivity.kt
index b8c191c..0120eca 100644
--- a/app/src/main/java/com/wbrawner/pihelper/MainActivity.kt
+++ b/app/src/main/java/com/wbrawner/pihelper/MainActivity.kt
@@ -9,15 +9,8 @@ import android.view.animation.AnticipateInterpolator
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.animation.core.*
-import androidx.compose.foundation.Image
import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.rotate
-import androidx.compose.ui.graphics.ColorFilter
-import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.animation.doOnEnd
import androidx.core.content.ContextCompat
@@ -29,7 +22,11 @@ import com.wbrawner.pihelper.shared.Action
import com.wbrawner.pihelper.shared.Effect
import com.wbrawner.pihelper.shared.Route
import com.wbrawner.pihelper.shared.Store
-import com.wbrawner.pihelper.ui.PihelperTheme
+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.component.LoadingSpinner
+import com.wbrawner.pihelper.shared.ui.theme.PihelperTheme
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@@ -130,26 +127,6 @@ class MainActivity : AppCompatActivity() {
}
}
-@Composable
-fun LoadingSpinner(animate: Boolean = false) {
- val animation = rememberInfiniteTransition()
- val rotation by animation.animateValue(
- initialValue = 0f,
- targetValue = 360f,
- typeConverter = Float.VectorConverter,
- animationSpec = infiniteRepeatable(
- animation = tween(1000, easing = LinearEasing),
- repeatMode = RepeatMode.Restart
- )
- )
- Image(
- modifier = Modifier.rotate(if (animate) rotation else 0f),
- painter = painterResource(id = R.drawable.ic_app_logo),
- contentDescription = "Loading",
- colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground)
- )
-}
-
@Composable
@Preview
fun LoadingSpinner_Preview() {
@@ -170,3 +147,11 @@ enum class ShortcutActions(val fullName: String) {
}
const val DURATION: String = "com.wbrawner.pihelper.MainActivityKt.DURATION"
+
+@Composable
+@DayNightPreview
+fun InfoScreen_Preview() {
+ PihelperTheme {
+ InfoScreen({}, {})
+ }
+}
diff --git a/app/src/main/java/com/wbrawner/pihelper/ScanScreen.kt b/app/src/main/java/com/wbrawner/pihelper/ScanScreen.kt
index 004cc34..fecb0b2 100644
--- a/app/src/main/java/com/wbrawner/pihelper/ScanScreen.kt
+++ b/app/src/main/java/com/wbrawner/pihelper/ScanScreen.kt
@@ -12,7 +12,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.wbrawner.pihelper.shared.Store
-import com.wbrawner.pihelper.ui.PihelperTheme
+import com.wbrawner.pihelper.shared.ui.theme.PihelperTheme
@Composable
fun ScanScreen(store: Store) {
diff --git a/build.gradle.kts b/build.gradle.kts
index 323a0bf..5b6eb7f 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -18,5 +18,8 @@ allprojects {
maven {
url = URI("https://s01.oss.sonatype.org/content/repositories/snapshots/")
}
+ maven {
+ url = URI("https://maven.pkg.jetbrains.space/public/p/compose/dev/")
+ }
}
}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 40f6cb9..2ae54a4 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -22,3 +22,4 @@ kotlin.code.style=official
kotlin.mpp.enableGranularSourceSetsMetadata=true
kotlin.native.enableDependencyPropagation=false
+org.jetbrains.compose.experimental.uikit.enabled=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 849732d..08009bc 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,23 +1,24 @@
[versions]
-androidx-core = "1.9.0"
-androidx-appcompat = "1.5.1"
-androidx-splash = "1.0.0"
-androidx-test-runner = "1.5.1"
+androidx-core = "1.10.1"
+androidx-appcompat = "1.6.1"
+androidx-splash = "1.0.1"
+androidx-test-runner = "1.5.2"
androidx-test-orchestrator = "1.4.2"
-compose = "1.2.1"
-compose-compiler = "1.3.2"
-compose-material3 = "1.0.1"
-espresso = "3.3.0"
+compose = "1.5.0"
+compose-compiler = "1.4.2"
+compose-material = "1.5.0"
+compose-material3 = "1.1.1"
+espresso = "3.5.1"
hilt-android = "2.44"
-kotlin = "1.7.20"
+kotlin = "1.8.20"
kotlinx-serialization = "1.4.1"
kotlinx-coroutines = "1.6.4"
kotlinx-datetime = "0.4.0"
ktor = "2.1.2"
-material = "1.3.0"
+material = "1.9.0"
maxSdk = "33"
minSdk = "23"
-navigation = "2.4.1"
+navigation = "2.7.0"
okhttp = "4.10.0"
plausible = "0.1.0-SNAPSHOT"
settings = "0.8.1"
@@ -25,16 +26,17 @@ versionCode = "5"
versionName = "1.1.1"
[libraries]
-android-gradle = { module = "com.android.tools.build:gradle", version = "7.3.1" }
+android-gradle = { module = "com.android.tools.build:gradle", version = "7.4.2" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
androidx-splash = { module = "androidx.core:core-splashscreen", version.ref = "androidx-splash" }
androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" }
androidx-test-orchestrator = { module = "androidx.test:orchestrator", version.ref = "androidx-test-orchestrator" }
-compose-activity = { module = "androidx.activity:activity-compose", version = "1.6.0" }
-compose-material = { module = "androidx.compose.material:material", version.ref = "compose" }
+compose-activity = { module = "androidx.activity:activity-compose", version = "1.7.2" }
+compose-material = { module = "androidx.compose.material:material", version.ref = "compose-material" }
compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "compose-material3" }
compose-material3-window = { module = "androidx.compose.material3:material3-window-size-class", version.ref = "compose-material3" }
+compose-plugin-jetbrains = { module = "org.jetbrains.compose:compose-gradle-plugin", version = "1.5.0-rc04" }
compose-test-junit = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" }
compose-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose" }
compose-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
@@ -45,12 +47,13 @@ hilt-android-core = { module = "com.google.dagger:hilt-android", version.ref = "
hilt-android-kapt = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt-android" }
hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt-android" }
hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.0.0" }
-junit = { module = "junit:junit", version = "4.12" }
+junit = { module = "junit:junit", version = "4.13.2" }
kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
ktor-client-ios = { module = "io.ktor:ktor-client-ios", version.ref = "ktor" }
ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" }
+ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-client-serialization = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
@@ -58,6 +61,7 @@ ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
+kotlinx-coroutines-jvm = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
@@ -68,10 +72,10 @@ navigation-compose = { module = "androidx.navigation:navigation-compose", versio
navigation-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigation" }
navigation-ui = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigation" }
plausible = { module = "com.wbrawner.plausible:plausible-android", version.ref = "plausible" }
-preference = { module = "androidx.preference:preference-ktx", version = "1.1.1" }
-test-ext = { module = "androidx.test.ext:junit", version = "1.1.4" }
+preference = { module = "androidx.preference:preference-ktx", version = "1.2.0" }
+test-ext = { module = "androidx.test.ext:junit", version = "1.1.5" }
[bundles]
compose = ["compose-ui", "compose-material", "compose-material3", "compose-material3-window", "compose-tooling", "compose-activity", "navigation-compose"]
coroutines = ["kotlinx-coroutines-core", "kotlinx-coroutines-android"]
-plugins = ["android-gradle", "kotlin-gradle", "dagger-hilt", "kotlin-serialization"]
\ No newline at end of file
+plugins = ["android-gradle", "compose-plugin-jetbrains", "kotlin-gradle", "dagger-hilt", "kotlin-serialization"]
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 6088553..f2af5a2 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,4 +1,5 @@
enableFeaturePreview("VERSION_CATALOGS")
rootProject.name = "Pi-helper"
include(":app")
+include(":desktop")
include(":shared")
diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts
index 311bbfa..2bde9ec 100644
--- a/shared/build.gradle.kts
+++ b/shared/build.gradle.kts
@@ -2,6 +2,7 @@ plugins {
kotlin("multiplatform")
id("com.android.library")
kotlin("plugin.serialization")
+ id("org.jetbrains.compose")
}
kotlin {
@@ -11,11 +12,13 @@ kotlin {
baseName = "Pihelper"
}
}
+ jvm("desktop")
sourceSets {
val commonMain by getting {
dependencies {
implementation(libs.ktor.client.core)
+ implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.logging)
implementation(libs.ktor.client.serialization)
implementation(libs.ktor.client.content.negotiation)
@@ -23,12 +26,16 @@ kotlin {
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
api(libs.multiplatform.settings)
+ api(compose.runtime)
+ api(compose.foundation)
+ api(compose.material3)
+ @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
+ implementation(compose.components.resources)
}
}
val androidMain by getting {
dependencies {
- implementation(libs.ktor.client.android)
implementation(libs.plausible)
}
}
@@ -40,7 +47,13 @@ kotlin {
iosArm64Main.dependsOn(this)
iosSimulatorArm64Main.dependsOn(this)
dependencies {
- implementation(libs.ktor.client.ios)
+ }
+ }
+
+ val desktopMain by getting {
+ dependencies {
+ implementation(compose.desktop.common)
+ implementation(compose.uiTooling)
}
}
}
diff --git a/shared/src/androidMain/kotlin/com/wbrawner/pihelper/shared/Android.kt b/shared/src/androidMain/kotlin/com/wbrawner/pihelper/shared/Android.kt
index ae1d372..7e9099d 100644
--- a/shared/src/androidMain/kotlin/com/wbrawner/pihelper/shared/Android.kt
+++ b/shared/src/androidMain/kotlin/com/wbrawner/pihelper/shared/Android.kt
@@ -1,13 +1,8 @@
package com.wbrawner.pihelper.shared
-import io.ktor.client.*
-import io.ktor.client.engine.android.*
import java.math.BigInteger
import java.security.MessageDigest
-fun PiholeAPIService.Companion.create() = KtorPiholeAPIService(HttpClient(Android) {
- commonConfig()
-})
actual fun String.hash(): String = BigInteger(
1,
diff --git a/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/PiholeAPIService.kt b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/PiholeAPIService.kt
index 5b766c3..610b594 100644
--- a/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/PiholeAPIService.kt
+++ b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/PiholeAPIService.kt
@@ -3,6 +3,7 @@ package com.wbrawner.pihelper.shared
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.*
+import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
@@ -26,6 +27,10 @@ interface PiholeAPIService {
companion object
}
+fun PiholeAPIService.Companion.create() = KtorPiholeAPIService(HttpClient(CIO) {
+ commonConfig()
+})
+
fun HttpClientConfig.commonConfig() {
install(ContentNegotiation) {
json(Json {
diff --git a/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/Responses.kt b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/Responses.kt
index 0ba9ea1..dfd9ad8 100644
--- a/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/Responses.kt
+++ b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/Responses.kt
@@ -8,41 +8,9 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
-@Serializable()
+@Serializable
data class Summary(
- @SerialName("domains_being_blocked")
- val domainsBeingBlocked: String? = null,
- @SerialName("dns_queries_today")
- val dnsQueriesToday: String? = null,
- @SerialName("ads_blocked_today")
- val adsBlockedToday: String? = null,
- @SerialName("ads_percentage_today")
- val adsPercentageToday: String? = null,
- @SerialName("unique_domains")
- val uniqueDomains: String? = null,
- @SerialName("queries_forwarded")
- val queriesForwarded: String? = null,
- @SerialName("clients_ever_seen")
- val clientsEverSeen: String? = null,
- @SerialName("unique_clients")
- val uniqueClients: String? = null,
- @SerialName("dns_queries_all_types")
- val dnsQueriesAllTypes: String? = null,
- @SerialName("queries_cached")
- val queriesCached: String? = null,
- @SerialName("no_data_replies")
- val noDataReplies: String? = null,
- @SerialName("nx_domain_replies")
- val nxDomainReplies: String? = null,
- @SerialName("cname_replies")
- val cnameReplies: String? = null,
- @SerialName("in_replies")
- val ipReplies: String? = null,
- @SerialName("privacy_level")
- val privacyLevel: String,
override val status: Status,
- @SerialName("gravity_last_updated")
- val gravity: Gravity? = null,
val type: String? = null,
val version: Int? = null
) : StatusProvider
@@ -77,21 +45,6 @@ sealed class Status(val name: String) {
}
}
-@Serializable()
-data class Gravity(
- @SerialName("file_exists")
- val fileExists: Boolean,
- val absolute: Int,
- val relative: Relative
-)
-
-@Serializable()
-data class Relative(
- val days: String,
- val hours: String,
- val minutes: String
-)
-
@Serializable()
data class VersionResponse(val version: Int)
diff --git a/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/Store.kt b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/Store.kt
index f01e1c8..24ae9bb 100644
--- a/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/Store.kt
+++ b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/Store.kt
@@ -230,6 +230,7 @@ class Store(
route = Route.AUTH,
)
} catch (e: Exception) {
+ e.printStackTrace()
_state.value = _state.value.copy(loading = false)
if (emitError) {
_effects.emit(Effect.Error(e.message ?: "Failed to connect to $host"))
diff --git a/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/AddScreen.kt b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/AddScreen.kt
new file mode 100644
index 0000000..50f8b33
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/AddScreen.kt
@@ -0,0 +1,120 @@
+package com.wbrawner.pihelper.shared.ui
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.wbrawner.pihelper.shared.Effect
+import com.wbrawner.pihelper.shared.ui.component.LoadingSpinner
+import com.wbrawner.pihelper.shared.ui.component.PrimaryButton
+
+const val ADD_SCREEN_TAG = "addScreen"
+const val CONNECT_BUTTON_TAG = "connectButton"
+const val HOST_TAG = "hostInput"
+const val SCAN_BUTTON_TAG = "scanButton"
+
+@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
+@Composable
+fun AddScreen(
+ scanNetwork: () -> Unit,
+ connectToPihole: (String) -> Unit,
+ loading: Boolean = false,
+ error: Effect.Error? = null
+) {
+ val (host: String, setHost: (String) -> Unit) = remember { mutableStateOf("pi.hole") }
+ val keyboardController = LocalSoftwareKeyboardController.current
+ Column(
+ modifier = Modifier
+ .testTag(ADD_SCREEN_TAG)
+ .padding(16.dp)
+ .fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
+ ) {
+ LoadingSpinner(loading)
+ Text(
+ text = "If you're not sure what the IP address for your Pi-hole is, Pi-helper can " +
+ "attempt to find it for you by scanning your network.",
+ textAlign = TextAlign.Center
+ )
+ PrimaryButton(
+ modifier = Modifier.semantics { contentDescription = "Scan Network button" },
+ text = "Scan Network",
+ onClick = scanNetwork
+ )
+ OrDivider()
+ Text(
+ text = "If you already know the IP address or host of your Pi-hole, you can also " +
+ "enter it below:",
+ textAlign = TextAlign.Center
+ )
+ OutlinedTextField(
+ modifier = Modifier
+ .semantics { contentDescription = "Pi-hole host input" }
+ .fillMaxWidth(),
+ value = host,
+ onValueChange = setHost,
+ label = { Text("Pi-hole Host") }
+ )
+ PrimaryButton(
+ modifier = Modifier.testTag(CONNECT_BUTTON_TAG),
+ text = "Connect to Pi-hole",
+ onClick = {
+ keyboardController?.hide()
+ connectToPihole(host)
+ }
+ )
+ error?.let {
+ Text(
+ color = MaterialTheme.colorScheme.primary,
+ textAlign = TextAlign.Center,
+ text = "Connection failed: ${it.message}"
+ )
+ }
+ }
+}
+
+@Composable
+fun OrDivider() {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(
+ modifier = Modifier
+ .height(2.dp)
+ .weight(1f)
+ .padding(end = 8.dp)
+ .clip(RectangleShape)
+ .background(MaterialTheme.colorScheme.onSurface),
+ )
+ Text("OR")
+ Box(
+ modifier = Modifier
+ .height(2.dp)
+ .weight(1f)
+ .padding(start = 8.dp)
+ .clip(RectangleShape)
+ .background(MaterialTheme.colorScheme.onSurface),
+ )
+ }
+}
diff --git a/app/src/main/java/com/wbrawner/pihelper/AuthScreen.kt b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/AuthScreen.kt
similarity index 92%
rename from app/src/main/java/com/wbrawner/pihelper/AuthScreen.kt
rename to shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/AuthScreen.kt
index fbe5263..a23c9e1 100644
--- a/app/src/main/java/com/wbrawner/pihelper/AuthScreen.kt
+++ b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/AuthScreen.kt
@@ -1,6 +1,5 @@
-package com.wbrawner.pihelper
+package com.wbrawner.pihelper.shared.ui
-import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@@ -22,11 +21,15 @@ import androidx.compose.ui.platform.LocalAutofill
import androidx.compose.ui.platform.LocalAutofillTree
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
-import com.wbrawner.pihelper.shared.*
+import com.wbrawner.pihelper.shared.Action
+import com.wbrawner.pihelper.shared.AuthenticationString
+import com.wbrawner.pihelper.shared.Effect
+import com.wbrawner.pihelper.shared.Store
+import com.wbrawner.pihelper.shared.ui.component.LoadingSpinner
+import com.wbrawner.pihelper.shared.ui.component.PrimaryButton
const val AUTH_SCREEN_TAG = "authScreen"
const val SUCCESS_TEXT_TAG = "successText"
@@ -63,10 +66,7 @@ fun AuthScreen(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
) {
- Image(
- painter = painterResource(id = R.drawable.ic_app_logo),
- contentDescription = null
- )
+ LoadingSpinner(animate = false)
Text(
modifier = Modifier.testTag(SUCCESS_TEXT_TAG),
text = "Pi-helper has successfully connected to your Pi-Hole!",
diff --git a/app/src/main/java/com/wbrawner/pihelper/InfoScreen.kt b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/InfoScreen.kt
similarity index 86%
rename from app/src/main/java/com/wbrawner/pihelper/InfoScreen.kt
rename to shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/InfoScreen.kt
index 9d513e3..9bebf9d 100644
--- a/app/src/main/java/com/wbrawner/pihelper/InfoScreen.kt
+++ b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/InfoScreen.kt
@@ -1,7 +1,5 @@
-package com.wbrawner.pihelper
+package com.wbrawner.pihelper.shared.ui
-import android.content.res.Configuration.UI_MODE_NIGHT_NO
-import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@@ -20,10 +18,10 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import com.wbrawner.pihelper.shared.*
-import com.wbrawner.pihelper.ui.PihelperTheme
+import com.wbrawner.pihelper.shared.Action
+import com.wbrawner.pihelper.shared.Store
+import com.wbrawner.pihelper.shared.ui.component.LoadingSpinner
@Composable
fun InfoScreen(store: Store) {
@@ -107,10 +105,10 @@ fun InfoScreen(onBackClicked: () -> Unit, onForgetPiholeClicked: () -> Unit) {
message.getStringAnnotations(it, it).firstOrNull()?.let { annotation ->
uriHandler.openUri(annotation.item)
// TODO: Move this to the store?
- PlausibleAnalyticsHelper.event(
- AnalyticsEvent.LinkClicked(annotation.item),
- Route.ABOUT
- )
+// PlausibleAnalyticsHelper.event(
+// AnalyticsEvent.LinkClicked(annotation.item),
+// Route.ABOUT
+// )
}
}
TextButton(onClick = onForgetPiholeClicked) {
@@ -119,12 +117,3 @@ fun InfoScreen(onBackClicked: () -> Unit, onForgetPiholeClicked: () -> Unit) {
}
}
}
-
-@Composable
-@Preview(showSystemUi = true, uiMode = UI_MODE_NIGHT_NO)
-@Preview(showSystemUi = true, uiMode = UI_MODE_NIGHT_YES)
-fun InfoScreen_Preview() {
- PihelperTheme {
- InfoScreen({}, {})
- }
-}
diff --git a/app/src/main/java/com/wbrawner/pihelper/MainScreen.kt b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/MainScreen.kt
similarity index 87%
rename from app/src/main/java/com/wbrawner/pihelper/MainScreen.kt
rename to shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/MainScreen.kt
index 95b4ff7..bd755f9 100644
--- a/app/src/main/java/com/wbrawner/pihelper/MainScreen.kt
+++ b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/MainScreen.kt
@@ -1,6 +1,6 @@
@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class)
-package com.wbrawner.pihelper
+package com.wbrawner.pihelper.shared.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
@@ -24,9 +24,8 @@ import com.wbrawner.pihelper.shared.Action
import com.wbrawner.pihelper.shared.Effect
import com.wbrawner.pihelper.shared.Status
import com.wbrawner.pihelper.shared.Store
-import com.wbrawner.pihelper.ui.DayNightPreview
-import com.wbrawner.pihelper.ui.PihelperTheme
-import java.util.*
+import com.wbrawner.pihelper.shared.ui.component.LoadingSpinner
+import com.wbrawner.pihelper.shared.ui.component.PrimaryButton
import kotlin.math.pow
import kotlin.math.roundToLong
import com.wbrawner.pihelper.shared.State as PihelperState
@@ -51,7 +50,6 @@ const val DISABLE_PERMANENT_BUTTON_TAG = "disablePermanentButton"
fun MainScreen(store: Store) {
val state by store.state.collectAsState()
val effect by store.effects.collectAsState(initial = Effect.Empty)
- println(effect)
MainScreen(state = state, error = effect as? Effect.Error, dispatch = store::dispatch)
}
@@ -137,7 +135,7 @@ fun StatusLabel(status: Status) {
modifier = Modifier.testTag(STATUS_TEXT_TAG),
color = color,
fontWeight = FontWeight.Bold,
- text = status.name.capitalize(Locale.US)
+ text = status.name.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
)
if (status is Status.Disabled && !status.timeRemaining.isNullOrBlank()) {
Text(
@@ -201,30 +199,13 @@ fun DisableControls(disable: (duration: Long?) -> Unit) {
}
}
-@Composable
-fun PrimaryButton(
- modifier: Modifier = Modifier,
- text: String,
- onClick: () -> Unit
-) {
- Button(
- modifier = modifier.fillMaxWidth(),
- colors = ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.primary,
- contentColor = MaterialTheme.colorScheme.onPrimary
- ),
- onClick = onClick
- ) {
- Text(text)
- }
-}
-
enum class Duration {
SECONDS,
MINUTES,
HOURS
}
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomTimeDialog(
visible: Boolean,
@@ -330,58 +311,58 @@ fun DurationToggle(
}
}
-@Composable
-@DayNightPreview
-fun CustomTimeDialog_Preview() {
- PihelperTheme {
- CustomTimeDialog(true, {}) { }
- }
-}
-
-@Composable
-@DayNightPreview
-fun StatusLabelEnabled_Preview() {
- PihelperTheme {
- StatusLabel(Status.Enabled)
- }
-}
-
-@Composable
-@DayNightPreview
-fun StatusLabelDisabled_Preview() {
- PihelperTheme {
- StatusLabel(Status.Disabled())
- }
-}
-
-@Composable
-@DayNightPreview
-fun StatusLabelDisabledWithTime_Preview() {
- PihelperTheme {
- StatusLabel(Status.Disabled("12:34:56"))
- }
-}
-
-@Composable
-@DayNightPreview
-fun PrimaryButton_Preview() {
- PihelperTheme {
- PrimaryButton(text = "Disable") {}
- }
-}
-
-@Composable
-@DayNightPreview
-fun EnableControls_Preview() {
- PihelperTheme {
- EnableControls {}
- }
-}
-
-@Composable
-@DayNightPreview
-fun DisableControls_Preview() {
- PihelperTheme {
- DisableControls {}
- }
-}
+//@Composable
+//@DayNightPreview
+//fun CustomTimeDialog_Preview() {
+// PihelperTheme {
+// CustomTimeDialog(true, {}) { }
+// }
+//}
+//
+//@Composable
+//@DayNightPreview
+//fun StatusLabelEnabled_Preview() {
+// PihelperTheme {
+// StatusLabel(Status.Enabled)
+// }
+//}
+//
+//@Composable
+//@DayNightPreview
+//fun StatusLabelDisabled_Preview() {
+// PihelperTheme {
+// StatusLabel(Status.Disabled())
+// }
+//}
+//
+//@Composable
+//@DayNightPreview
+//fun StatusLabelDisabledWithTime_Preview() {
+// PihelperTheme {
+// StatusLabel(Status.Disabled("12:34:56"))
+// }
+//}
+//
+//@Composable
+//@DayNightPreview
+//fun PrimaryButton_Preview() {
+// PihelperTheme {
+// PrimaryButton(text = "Disable") {}
+// }
+//}
+//
+//@Composable
+//@DayNightPreview
+//fun EnableControls_Preview() {
+// PihelperTheme {
+// EnableControls {}
+// }
+//}
+//
+//@Composable
+//@DayNightPreview
+//fun DisableControls_Preview() {
+// PihelperTheme {
+// DisableControls {}
+// }
+//}
diff --git a/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/component/LoadingSpinner.kt b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/component/LoadingSpinner.kt
new file mode 100644
index 0000000..bcea158
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/component/LoadingSpinner.kt
@@ -0,0 +1,33 @@
+package com.wbrawner.pihelper.shared.ui.component
+
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.Image
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.graphics.ColorFilter
+import org.jetbrains.compose.resources.ExperimentalResourceApi
+import org.jetbrains.compose.resources.painterResource
+
+@OptIn(ExperimentalResourceApi::class)
+@Composable
+fun LoadingSpinner(animate: Boolean = false) {
+ val animation = rememberInfiniteTransition()
+ val rotation by animation.animateValue(
+ initialValue = 0f,
+ targetValue = 360f,
+ typeConverter = Float.VectorConverter,
+ animationSpec = infiniteRepeatable(
+ animation = tween(1000, easing = LinearEasing),
+ repeatMode = RepeatMode.Restart
+ )
+ )
+ Image(
+ modifier = Modifier.rotate(if (animate) rotation else 0f),
+ painter = painterResource("img/ic_app_logo.xml"),
+ contentDescription = "Loading",
+ colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground)
+ )
+}
diff --git a/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/component/PrimaryButton.kt b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/component/PrimaryButton.kt
new file mode 100644
index 0000000..e17a7fe
--- /dev/null
+++ b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/component/PrimaryButton.kt
@@ -0,0 +1,27 @@
+package com.wbrawner.pihelper.shared.ui.component
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+@Composable
+fun PrimaryButton(
+ modifier: Modifier = Modifier,
+ text: String,
+ onClick: () -> Unit
+) {
+ Button(
+ modifier = modifier.fillMaxWidth(),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary,
+ contentColor = MaterialTheme.colorScheme.onPrimary
+ ),
+ onClick = onClick
+ ) {
+ Text(text)
+ }
+}
diff --git a/app/src/main/java/com/wbrawner/pihelper/ui/Color.kt b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/theme/Color.kt
similarity index 78%
rename from app/src/main/java/com/wbrawner/pihelper/ui/Color.kt
rename to shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/theme/Color.kt
index 19502cf..515c249 100644
--- a/app/src/main/java/com/wbrawner/pihelper/ui/Color.kt
+++ b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/theme/Color.kt
@@ -1,4 +1,4 @@
-package com.wbrawner.pihelper.ui
+package com.wbrawner.pihelper.shared.ui.theme
import androidx.compose.ui.graphics.Color
diff --git a/app/src/main/java/com/wbrawner/pihelper/ui/Shape.kt b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/theme/Shape.kt
similarity index 86%
rename from app/src/main/java/com/wbrawner/pihelper/ui/Shape.kt
rename to shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/theme/Shape.kt
index a9354b7..fc160fd 100644
--- a/app/src/main/java/com/wbrawner/pihelper/ui/Shape.kt
+++ b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/theme/Shape.kt
@@ -1,4 +1,4 @@
-package com.wbrawner.pihelper.ui
+package com.wbrawner.pihelper.shared.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
diff --git a/app/src/main/java/com/wbrawner/pihelper/ui/Theme.kt b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/theme/Theme.kt
similarity index 57%
rename from app/src/main/java/com/wbrawner/pihelper/ui/Theme.kt
rename to shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/theme/Theme.kt
index 479b66f..da2a69d 100644
--- a/app/src/main/java/com/wbrawner/pihelper/ui/Theme.kt
+++ b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/theme/Theme.kt
@@ -1,13 +1,12 @@
-package com.wbrawner.pihelper.ui
+package com.wbrawner.pihelper.shared.ui.theme
-import android.content.res.Configuration.UI_MODE_NIGHT_NO
-import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material3.*
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.tooling.preview.Preview
private val DarkColorPalette = darkColorScheme(
background = Color.Black,
@@ -31,20 +30,10 @@ private val LightColorPalette = lightColorScheme(
@Composable
fun PihelperTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
- val context = LocalContext.current
- val dynamic = false
- val colors = if (dynamic) {
- if (darkTheme) {
- dynamicDarkColorScheme(context)
- } else {
- dynamicLightColorScheme(context)
- }
+ val colors = if (darkTheme) {
+ DarkColorPalette
} else {
- if (darkTheme) {
- DarkColorPalette
- } else {
- LightColorPalette
- }
+ LightColorPalette
}
MaterialTheme(
@@ -56,7 +45,3 @@ fun PihelperTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composab
}
)
}
-
-@Preview(uiMode = UI_MODE_NIGHT_NO)
-@Preview(uiMode = UI_MODE_NIGHT_YES)
-annotation class DayNightPreview
\ No newline at end of file
diff --git a/app/src/main/java/com/wbrawner/pihelper/ui/Type.kt b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/theme/Type.kt
similarity index 90%
rename from app/src/main/java/com/wbrawner/pihelper/ui/Type.kt
rename to shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/theme/Type.kt
index 5385789..0e088a2 100644
--- a/app/src/main/java/com/wbrawner/pihelper/ui/Type.kt
+++ b/shared/src/commonMain/kotlin/com/wbrawner/pihelper/shared/ui/theme/Type.kt
@@ -1,4 +1,4 @@
-package com.wbrawner.pihelper.ui
+package com.wbrawner.pihelper.shared.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
diff --git a/shared/src/commonMain/resources/img/ic_app_logo.xml b/shared/src/commonMain/resources/img/ic_app_logo.xml
new file mode 100644
index 0000000..d119308
--- /dev/null
+++ b/shared/src/commonMain/resources/img/ic_app_logo.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/shared/src/desktopMain/kotlin/com/wbrawner/pihelper/shared/Desktop.kt b/shared/src/desktopMain/kotlin/com/wbrawner/pihelper/shared/Desktop.kt
new file mode 100644
index 0000000..cef6cdb
--- /dev/null
+++ b/shared/src/desktopMain/kotlin/com/wbrawner/pihelper/shared/Desktop.kt
@@ -0,0 +1,23 @@
+package com.wbrawner.pihelper.shared
+
+import androidx.compose.desktop.ui.tooling.preview.Preview
+import androidx.compose.runtime.Composable
+import com.wbrawner.pihelper.shared.ui.OrDivider
+import com.wbrawner.pihelper.shared.ui.theme.PihelperTheme
+import io.ktor.client.*
+import io.ktor.client.engine.*
+import java.math.BigInteger
+import java.security.MessageDigest
+
+actual fun String.hash(): String = BigInteger(
+ 1,
+ MessageDigest.getInstance("SHA-256").digest(this.toByteArray())
+).toString(16).padStart(64, '0')
+
+@Composable
+@Preview
+fun OrDivider_Preview() {
+ PihelperTheme {
+ OrDivider()
+ }
+}
\ No newline at end of file