Implement desktop builds
This commit is contained in:
parent
731faf7894
commit
7bbbb022e3
39 changed files with 611 additions and 380 deletions
|
@ -126,6 +126,7 @@
|
|||
<option name="preferredColumnWidths">
|
||||
<map>
|
||||
<entry key="Duration" value="90" />
|
||||
<entry key="Pixel_3a_API_31_arm64-v8a" value="120" />
|
||||
<entry key="Pixel_3a_API_33_arm64-v8a" value="120" />
|
||||
<entry key="Tests" value="360" />
|
||||
</map>
|
||||
|
@ -147,6 +148,20 @@
|
|||
</AndroidTestResultsTableState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="28629151">
|
||||
<value>
|
||||
<AndroidTestResultsTableState>
|
||||
<option name="preferredColumnWidths">
|
||||
<map>
|
||||
<entry key="Duration" value="90" />
|
||||
<entry key="Pixel_3a_API_31_arm64-v8a" value="120" />
|
||||
<entry key="Pixel_3a_API_33_arm64-v8a" value="120" />
|
||||
<entry key="Tests" value="360" />
|
||||
</map>
|
||||
</option>
|
||||
</AndroidTestResultsTableState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="302221053">
|
||||
<value>
|
||||
<AndroidTestResultsTableState>
|
||||
|
@ -160,6 +175,20 @@
|
|||
</AndroidTestResultsTableState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="496416765">
|
||||
<value>
|
||||
<AndroidTestResultsTableState>
|
||||
<option name="preferredColumnWidths">
|
||||
<map>
|
||||
<entry key="Duration" value="90" />
|
||||
<entry key="Pixel_3a_API_31_arm64-v8a" value="120" />
|
||||
<entry key="Pixel_3a_API_33_arm64-v8a" value="120" />
|
||||
<entry key="Tests" value="360" />
|
||||
</map>
|
||||
</option>
|
||||
</AndroidTestResultsTableState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="730894960">
|
||||
<value>
|
||||
<AndroidTestResultsTableState>
|
||||
|
@ -168,6 +197,7 @@
|
|||
<entry key="2A221FDH200B53" value="120" />
|
||||
<entry key="Duration" value="90" />
|
||||
<entry key="Google Pixel 7" value="120" />
|
||||
<entry key="Pixel_3a_API_31_arm64-v8a" value="120" />
|
||||
<entry key="Pixel_3a_API_33_arm64-v8a" value="120" />
|
||||
<entry key="Tests" value="360" />
|
||||
</map>
|
||||
|
@ -201,6 +231,19 @@
|
|||
</AndroidTestResultsTableState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="2093109046">
|
||||
<value>
|
||||
<AndroidTestResultsTableState>
|
||||
<option name="preferredColumnWidths">
|
||||
<map>
|
||||
<entry key="Duration" value="90" />
|
||||
<entry key="Pixel_3a_API_33_arm64-v8a" value="120" />
|
||||
<entry key="Tests" value="360" />
|
||||
</map>
|
||||
</option>
|
||||
</AndroidTestResultsTableState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
|
|
8
.idea/artifacts/shared_desktop.xml
Normal file
8
.idea/artifacts/shared_desktop.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="shared-desktop">
|
||||
<output-path>$PROJECT_DIR$/shared/build/libs</output-path>
|
||||
<root id="archive" name="shared-desktop.jar">
|
||||
<element id="module-output" name="Pi-helper.shared.desktopMain" />
|
||||
</root>
|
||||
</artifact>
|
||||
</component>
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="11" />
|
||||
<bytecodeTargetLevel target="17" />
|
||||
</component>
|
||||
</project>
|
22
.idea/deploymentTargetDropDown.xml
Normal file
22
.idea/deploymentTargetDropDown.xml
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetDropDown">
|
||||
<value>
|
||||
<entry key="app">
|
||||
<State />
|
||||
</entry>
|
||||
<entry key="app.androidTest">
|
||||
<State />
|
||||
</entry>
|
||||
<entry key="app.unitTest">
|
||||
<State />
|
||||
</entry>
|
||||
<entry key="pihelper-android.Pi-helper.app">
|
||||
<State />
|
||||
</entry>
|
||||
<entry key="testInvalidHost()">
|
||||
<State />
|
||||
</entry>
|
||||
</value>
|
||||
</component>
|
||||
</project>
|
|
@ -4,17 +4,17 @@
|
|||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="GRADLE" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="Android Studio default JDK" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/desktop" />
|
||||
<option value="$PROJECT_DIR$/shared" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveExternalAnnotations" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
|
|
|
@ -31,5 +31,10 @@
|
|||
<option name="name" value="maven" />
|
||||
<option name="url" value="https://s01.oss.sonatype.org/content/repositories/snapshots/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven" />
|
||||
<option name="name" value="maven" />
|
||||
<option name="url" value="https://maven.pkg.jetbrains.space/public/p/compose/dev" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinScriptingSettings">
|
||||
<scriptDefinition className="org.jetbrains.kotlin.scripting.resolve.KotlinScriptDefinitionFromAnnotatedTemplate" definitionName="KotlinBuildScript">
|
||||
<order>2147483647</order>
|
||||
<autoReloadConfigurations>true</autoReloadConfigurations>
|
||||
</scriptDefinition>
|
||||
</component>
|
||||
</project>
|
6
.idea/kotlinc.xml
Normal file
6
.idea/kotlinc.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.8.20" />
|
||||
</component>
|
||||
</project>
|
|
@ -1,4 +1,3 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CMakeSettings">
|
||||
<configurations>
|
||||
|
@ -61,7 +60,11 @@
|
|||
<item index="0" class="java.lang.String" itemvalue="dagger.hilt.android.testing.BindValue" />
|
||||
</list>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
|
124
.idea/uiDesigner.xml
Normal file
124
.idea/uiDesigner.xml
Normal file
|
@ -0,0 +1,124 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Palette2">
|
||||
<group name="Swing">
|
||||
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
|
||||
</item>
|
||||
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
|
||||
</item>
|
||||
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
|
||||
</item>
|
||||
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
|
||||
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
|
||||
</item>
|
||||
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
|
||||
<initial-values>
|
||||
<property name="text" value="Button" />
|
||||
</initial-values>
|
||||
</item>
|
||||
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
|
||||
<initial-values>
|
||||
<property name="text" value="RadioButton" />
|
||||
</initial-values>
|
||||
</item>
|
||||
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
|
||||
<initial-values>
|
||||
<property name="text" value="CheckBox" />
|
||||
</initial-values>
|
||||
</item>
|
||||
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
|
||||
<initial-values>
|
||||
<property name="text" value="Label" />
|
||||
</initial-values>
|
||||
</item>
|
||||
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||
<preferred-size width="150" height="-1" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||
<preferred-size width="150" height="-1" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||
<preferred-size width="150" height="-1" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
|
||||
<preferred-size width="200" height="200" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
|
||||
<preferred-size width="200" height="200" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
|
||||
</item>
|
||||
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
|
||||
<preferred-size width="-1" height="20" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
|
||||
</item>
|
||||
</group>
|
||||
</component>
|
||||
</project>
|
|
@ -2,5 +2,6 @@
|
|||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/desktop" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() }
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
|
@ -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({}, {})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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/")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -22,3 +22,4 @@ kotlin.code.style=official
|
|||
|
||||
kotlin.mpp.enableGranularSourceSetsMetadata=true
|
||||
kotlin.native.enableDependencyPropagation=false
|
||||
org.jetbrains.compose.experimental.uikit.enabled=true
|
|
@ -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"]
|
||||
plugins = ["android-gradle", "compose-plugin-jetbrains", "kotlin-gradle", "dagger-hilt", "kotlin-serialization"]
|
|
@ -1,4 +1,5 @@
|
|||
enableFeaturePreview("VERSION_CATALOGS")
|
||||
rootProject.name = "Pi-helper"
|
||||
include(":app")
|
||||
include(":desktop")
|
||||
include(":shared")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 <T : HttpClientEngineConfig> HttpClientConfig<T>.commonConfig() {
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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!",
|
|
@ -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({}, {})
|
||||
}
|
||||
}
|
|
@ -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 {}
|
||||
// }
|
||||
//}
|
|
@ -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)
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.wbrawner.pihelper.ui
|
||||
package com.wbrawner.pihelper.shared.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
|
@ -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
|
|
@ -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,21 +30,11 @@ 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)
|
||||
}
|
||||
} else {
|
||||
if (darkTheme) {
|
||||
val colors = if (darkTheme) {
|
||||
DarkColorPalette
|
||||
} else {
|
||||
LightColorPalette
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colors,
|
||||
|
@ -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
|
|
@ -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
|
18
shared/src/commonMain/resources/img/ic_app_logo.xml
Normal file
18
shared/src/commonMain/resources/img/ic_app_logo.xml
Normal file
|
@ -0,0 +1,18 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="404.37808"
|
||||
android:viewportHeight="404.37808">
|
||||
<group android:translateX="66.72238"
|
||||
android:translateY="66.72238">
|
||||
<path
|
||||
android:pathData="m135.467,61.19c-40.93,0 -74.277,33.347 -74.277,74.277 0,12.31 3.02,23.932 8.352,34.169a33.872,33.872 0,0 0,-8.351 22.235,33.872 33.872,0 0,0 33.872,33.872 33.872,33.872 0,0 0,29.24 -16.839,33.872 33.872,0 0,0 0.001,-0.001c3.642,0.552 7.37,0.84 11.163,0.84 40.93,0 74.277,-33.346 74.277,-74.276 0,-40.93 -33.347,-74.277 -74.277,-74.277zM135.467,76.748c32.523,0 58.725,26.195 58.725,58.718 0,32.523 -26.202,58.725 -58.725,58.725 -2.242,0 -4.453,-0.129 -6.628,-0.372a33.872,33.872 0,0 0,0 -0.007,33.872 33.872,0 0,0 0.097,-1.943A33.872,33.872 0,0 0,95.063 157.999,33.872 33.872,0 0,0 82.318,160.512c-0,-0 -0,-0.001 -0.001,-0.001a33.872,33.872 0,0 0,-0.006 0.003c-3.568,-7.591 -5.564,-16.077 -5.564,-25.047 0,-32.523 26.195,-58.718 58.718,-58.718z"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="1.72941"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillAlpha="1"
|
||||
android:strokeLineCap="butt"/>
|
||||
</group>
|
||||
</vector>
|
|
@ -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()
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue