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">
|
<option name="preferredColumnWidths">
|
||||||
<map>
|
<map>
|
||||||
<entry key="Duration" value="90" />
|
<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="Pixel_3a_API_33_arm64-v8a" value="120" />
|
||||||
<entry key="Tests" value="360" />
|
<entry key="Tests" value="360" />
|
||||||
</map>
|
</map>
|
||||||
|
@ -147,6 +148,20 @@
|
||||||
</AndroidTestResultsTableState>
|
</AndroidTestResultsTableState>
|
||||||
</value>
|
</value>
|
||||||
</entry>
|
</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">
|
<entry key="302221053">
|
||||||
<value>
|
<value>
|
||||||
<AndroidTestResultsTableState>
|
<AndroidTestResultsTableState>
|
||||||
|
@ -160,6 +175,20 @@
|
||||||
</AndroidTestResultsTableState>
|
</AndroidTestResultsTableState>
|
||||||
</value>
|
</value>
|
||||||
</entry>
|
</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">
|
<entry key="730894960">
|
||||||
<value>
|
<value>
|
||||||
<AndroidTestResultsTableState>
|
<AndroidTestResultsTableState>
|
||||||
|
@ -168,6 +197,7 @@
|
||||||
<entry key="2A221FDH200B53" value="120" />
|
<entry key="2A221FDH200B53" value="120" />
|
||||||
<entry key="Duration" value="90" />
|
<entry key="Duration" value="90" />
|
||||||
<entry key="Google Pixel 7" value="120" />
|
<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="Pixel_3a_API_33_arm64-v8a" value="120" />
|
||||||
<entry key="Tests" value="360" />
|
<entry key="Tests" value="360" />
|
||||||
</map>
|
</map>
|
||||||
|
@ -201,6 +231,19 @@
|
||||||
</AndroidTestResultsTableState>
|
</AndroidTestResultsTableState>
|
||||||
</value>
|
</value>
|
||||||
</entry>
|
</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>
|
</map>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</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"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="CompilerConfiguration">
|
<component name="CompilerConfiguration">
|
||||||
<bytecodeTargetLevel target="11" />
|
<bytecodeTargetLevel target="17" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</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">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
<option name="testRunner" value="GRADLE" />
|
|
||||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<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">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/app" />
|
||||||
|
<option value="$PROJECT_DIR$/desktop" />
|
||||||
<option value="$PROJECT_DIR$/shared" />
|
<option value="$PROJECT_DIR$/shared" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
|
<option name="resolveExternalAnnotations" value="false" />
|
||||||
</GradleProjectSettings>
|
</GradleProjectSettings>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|
|
@ -31,5 +31,10 @@
|
||||||
<option name="name" value="maven" />
|
<option name="name" value="maven" />
|
||||||
<option name="url" value="https://s01.oss.sonatype.org/content/repositories/snapshots/" />
|
<option name="url" value="https://s01.oss.sonatype.org/content/repositories/snapshots/" />
|
||||||
</remote-repository>
|
</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>
|
</component>
|
||||||
</project>
|
</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">
|
<project version="4">
|
||||||
<component name="CMakeSettings">
|
<component name="CMakeSettings">
|
||||||
<configurations>
|
<configurations>
|
||||||
|
@ -61,7 +60,11 @@
|
||||||
<item index="0" class="java.lang.String" itemvalue="dagger.hilt.android.testing.BindValue" />
|
<item index="0" class="java.lang.String" itemvalue="dagger.hilt.android.testing.BindValue" />
|
||||||
</list>
|
</list>
|
||||||
</component>
|
</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" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<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">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
<mapping directory="$PROJECT_DIR$/desktop" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
|
@ -4,9 +4,7 @@ import android.content.Context
|
||||||
import androidx.compose.ui.test.*
|
import androidx.compose.ui.test.*
|
||||||
import androidx.compose.ui.test.junit4.ComposeTestRule
|
import androidx.compose.ui.test.junit4.ComposeTestRule
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import com.wbrawner.pihelper.ADD_SCREEN_TAG
|
|
||||||
import com.wbrawner.pihelper.CONNECT_BUTTON_TAG
|
import com.wbrawner.pihelper.CONNECT_BUTTON_TAG
|
||||||
import com.wbrawner.pihelper.HOST_TAG
|
|
||||||
|
|
||||||
fun onAddScreen(testRule: ComposeTestRule, actions: AddScreenRobot.() -> Unit) =
|
fun onAddScreen(testRule: ComposeTestRule, actions: AddScreenRobot.() -> Unit) =
|
||||||
AddScreenRobot(testRule).apply { actions() }
|
AddScreenRobot(testRule).apply { actions() }
|
||||||
|
@ -14,30 +12,22 @@ fun onAddScreen(testRule: ComposeTestRule, actions: AddScreenRobot.() -> Unit) =
|
||||||
class AddScreenRobot(private val testRule: ComposeTestRule) {
|
class AddScreenRobot(private val testRule: ComposeTestRule) {
|
||||||
val context: Context = InstrumentationRegistry.getInstrumentation().context
|
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 {
|
infix fun onAuthScreen(actions: AuthScreenRobot.() -> Unit) = AuthScreenRobot(testRule).run {
|
||||||
actions()
|
actions()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearHost() =
|
fun clearHost() =
|
||||||
testRule.onNode(hasTestTag(HOST_TAG)).performTextClearance()
|
testRule.onNodeWithContentDescription("Pi-hole host input").performTextClearance()
|
||||||
|
|
||||||
fun inputHost(host: String) =
|
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 clickConnect() = testRule.onNode(hasTestTag(CONNECT_BUTTON_TAG)).performClick()
|
||||||
|
|
||||||
fun verifyErrorMessageIsDisplayed(message: String) {
|
fun verifyErrorMessageIsDisplayed(message: String) {
|
||||||
testRule.waitUntil(2_000) {
|
testRule.waitUntil(2_000) {
|
||||||
testRule
|
testRule
|
||||||
.onAllNodesWithText(message, substring = true)
|
.onAllNodesWithContentDescription(message, substring = true)
|
||||||
.fetchSemanticsNodes().size == 1
|
.fetchSemanticsNodes().size == 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import android.content.Context
|
||||||
import androidx.compose.ui.test.*
|
import androidx.compose.ui.test.*
|
||||||
import androidx.compose.ui.test.junit4.ComposeTestRule
|
import androidx.compose.ui.test.junit4.ComposeTestRule
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import com.wbrawner.pihelper.*
|
import com.wbrawner.pihelper.shared.ui.*
|
||||||
|
|
||||||
class AuthScreenRobot(private val testRule: ComposeTestRule) {
|
class AuthScreenRobot(private val testRule: ComposeTestRule) {
|
||||||
val context: Context = InstrumentationRegistry.getInstrumentation().context
|
val context: Context = InstrumentationRegistry.getInstrumentation().context
|
||||||
|
|
|
@ -4,10 +4,10 @@ import android.content.Context
|
||||||
import androidx.compose.ui.test.*
|
import androidx.compose.ui.test.*
|
||||||
import androidx.compose.ui.test.junit4.ComposeTestRule
|
import androidx.compose.ui.test.junit4.ComposeTestRule
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import com.wbrawner.pihelper.DISABLE_PERMANENT_BUTTON_TAG
|
import com.wbrawner.pihelper.shared.ui.DISABLE_PERMANENT_BUTTON_TAG
|
||||||
import com.wbrawner.pihelper.ENABLE_BUTTON_TAG
|
import com.wbrawner.pihelper.shared.ui.ENABLE_BUTTON_TAG
|
||||||
import com.wbrawner.pihelper.MAIN_SCREEN_TAG
|
import com.wbrawner.pihelper.shared.ui.MAIN_SCREEN_TAG
|
||||||
import com.wbrawner.pihelper.STATUS_TEXT_TAG
|
import com.wbrawner.pihelper.shared.ui.STATUS_TEXT_TAG
|
||||||
|
|
||||||
fun onMainScreen(testRule: ComposeTestRule, actions: MainScreenRobot.() -> Unit) =
|
fun onMainScreen(testRule: ComposeTestRule, actions: MainScreenRobot.() -> Unit) =
|
||||||
MainScreenRobot(testRule).apply { actions() }
|
MainScreenRobot(testRule).apply { actions() }
|
||||||
|
|
|
@ -5,28 +5,16 @@ import android.net.ConnectivityManager
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.runtime.getValue
|
||||||
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.ui.platform.LocalContext
|
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.Action
|
||||||
import com.wbrawner.pihelper.shared.Effect
|
import com.wbrawner.pihelper.shared.Effect
|
||||||
import com.wbrawner.pihelper.shared.Store
|
import com.wbrawner.pihelper.shared.Store
|
||||||
import com.wbrawner.pihelper.ui.DayNightPreview
|
import com.wbrawner.pihelper.shared.ui.AddScreen
|
||||||
import com.wbrawner.pihelper.ui.PihelperTheme
|
import com.wbrawner.pihelper.shared.ui.OrDivider
|
||||||
|
import com.wbrawner.pihelper.shared.ui.theme.PihelperTheme
|
||||||
import java.net.Inet4Address
|
import java.net.Inet4Address
|
||||||
|
|
||||||
val emulatorBuildModels = listOf(
|
val emulatorBuildModels = listOf(
|
||||||
|
@ -34,11 +22,6 @@ val emulatorBuildModels = listOf(
|
||||||
"sdk_gphone64_arm64"
|
"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
|
@Composable
|
||||||
fun AddScreen(store: Store) {
|
fun AddScreen(store: Store) {
|
||||||
val effect by store.effects.collectAsState(initial = Effect.Empty)
|
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
|
@Composable
|
||||||
@DayNightPreview
|
@DayNightPreview
|
||||||
fun AddScreen_Preview() {
|
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.activity.compose.setContent
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.animation.core.*
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.*
|
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.compose.ui.tooling.preview.Preview
|
||||||
import androidx.core.animation.doOnEnd
|
import androidx.core.animation.doOnEnd
|
||||||
import androidx.core.content.ContextCompat
|
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.Effect
|
||||||
import com.wbrawner.pihelper.shared.Route
|
import com.wbrawner.pihelper.shared.Route
|
||||||
import com.wbrawner.pihelper.shared.Store
|
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 dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
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
|
@Composable
|
||||||
@Preview
|
@Preview
|
||||||
fun LoadingSpinner_Preview() {
|
fun LoadingSpinner_Preview() {
|
||||||
|
@ -170,3 +147,11 @@ enum class ShortcutActions(val fullName: String) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const val DURATION: String = "com.wbrawner.pihelper.MainActivityKt.DURATION"
|
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.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.wbrawner.pihelper.shared.Store
|
import com.wbrawner.pihelper.shared.Store
|
||||||
import com.wbrawner.pihelper.ui.PihelperTheme
|
import com.wbrawner.pihelper.shared.ui.theme.PihelperTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ScanScreen(store: Store) {
|
fun ScanScreen(store: Store) {
|
||||||
|
|
|
@ -18,5 +18,8 @@ allprojects {
|
||||||
maven {
|
maven {
|
||||||
url = URI("https://s01.oss.sonatype.org/content/repositories/snapshots/")
|
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.mpp.enableGranularSourceSetsMetadata=true
|
||||||
kotlin.native.enableDependencyPropagation=false
|
kotlin.native.enableDependencyPropagation=false
|
||||||
|
org.jetbrains.compose.experimental.uikit.enabled=true
|
|
@ -1,23 +1,24 @@
|
||||||
[versions]
|
[versions]
|
||||||
androidx-core = "1.9.0"
|
androidx-core = "1.10.1"
|
||||||
androidx-appcompat = "1.5.1"
|
androidx-appcompat = "1.6.1"
|
||||||
androidx-splash = "1.0.0"
|
androidx-splash = "1.0.1"
|
||||||
androidx-test-runner = "1.5.1"
|
androidx-test-runner = "1.5.2"
|
||||||
androidx-test-orchestrator = "1.4.2"
|
androidx-test-orchestrator = "1.4.2"
|
||||||
compose = "1.2.1"
|
compose = "1.5.0"
|
||||||
compose-compiler = "1.3.2"
|
compose-compiler = "1.4.2"
|
||||||
compose-material3 = "1.0.1"
|
compose-material = "1.5.0"
|
||||||
espresso = "3.3.0"
|
compose-material3 = "1.1.1"
|
||||||
|
espresso = "3.5.1"
|
||||||
hilt-android = "2.44"
|
hilt-android = "2.44"
|
||||||
kotlin = "1.7.20"
|
kotlin = "1.8.20"
|
||||||
kotlinx-serialization = "1.4.1"
|
kotlinx-serialization = "1.4.1"
|
||||||
kotlinx-coroutines = "1.6.4"
|
kotlinx-coroutines = "1.6.4"
|
||||||
kotlinx-datetime = "0.4.0"
|
kotlinx-datetime = "0.4.0"
|
||||||
ktor = "2.1.2"
|
ktor = "2.1.2"
|
||||||
material = "1.3.0"
|
material = "1.9.0"
|
||||||
maxSdk = "33"
|
maxSdk = "33"
|
||||||
minSdk = "23"
|
minSdk = "23"
|
||||||
navigation = "2.4.1"
|
navigation = "2.7.0"
|
||||||
okhttp = "4.10.0"
|
okhttp = "4.10.0"
|
||||||
plausible = "0.1.0-SNAPSHOT"
|
plausible = "0.1.0-SNAPSHOT"
|
||||||
settings = "0.8.1"
|
settings = "0.8.1"
|
||||||
|
@ -25,16 +26,17 @@ versionCode = "5"
|
||||||
versionName = "1.1.1"
|
versionName = "1.1.1"
|
||||||
|
|
||||||
[libraries]
|
[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-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
||||||
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
|
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
|
||||||
androidx-splash = { module = "androidx.core:core-splashscreen", version.ref = "androidx-splash" }
|
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-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" }
|
||||||
androidx-test-orchestrator = { module = "androidx.test:orchestrator", version.ref = "androidx-test-orchestrator" }
|
androidx-test-orchestrator = { module = "androidx.test:orchestrator", version.ref = "androidx-test-orchestrator" }
|
||||||
compose-activity = { module = "androidx.activity:activity-compose", version = "1.6.0" }
|
compose-activity = { module = "androidx.activity:activity-compose", version = "1.7.2" }
|
||||||
compose-material = { module = "androidx.compose.material:material", version.ref = "compose" }
|
compose-material = { module = "androidx.compose.material:material", version.ref = "compose-material" }
|
||||||
compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "compose-material3" }
|
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-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-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-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose" }
|
||||||
compose-tooling = { module = "androidx.compose.ui:ui-tooling", 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-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-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" }
|
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-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||||
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", 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" }
|
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-ios = { module = "io.ktor:ktor-client-ios", version.ref = "ktor" }
|
||||||
ktor-client-js = { module = "io.ktor:ktor-client-js", 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-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-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" }
|
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" }
|
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-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-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" }
|
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
||||||
kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" }
|
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" }
|
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-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigation" }
|
||||||
navigation-ui = { module = "androidx.navigation:navigation-ui-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" }
|
plausible = { module = "com.wbrawner.plausible:plausible-android", version.ref = "plausible" }
|
||||||
preference = { module = "androidx.preference:preference-ktx", version = "1.1.1" }
|
preference = { module = "androidx.preference:preference-ktx", version = "1.2.0" }
|
||||||
test-ext = { module = "androidx.test.ext:junit", version = "1.1.4" }
|
test-ext = { module = "androidx.test.ext:junit", version = "1.1.5" }
|
||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
compose = ["compose-ui", "compose-material", "compose-material3", "compose-material3-window", "compose-tooling", "compose-activity", "navigation-compose"]
|
compose = ["compose-ui", "compose-material", "compose-material3", "compose-material3-window", "compose-tooling", "compose-activity", "navigation-compose"]
|
||||||
coroutines = ["kotlinx-coroutines-core", "kotlinx-coroutines-android"]
|
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")
|
enableFeaturePreview("VERSION_CATALOGS")
|
||||||
rootProject.name = "Pi-helper"
|
rootProject.name = "Pi-helper"
|
||||||
include(":app")
|
include(":app")
|
||||||
|
include(":desktop")
|
||||||
include(":shared")
|
include(":shared")
|
||||||
|
|
|
@ -2,6 +2,7 @@ plugins {
|
||||||
kotlin("multiplatform")
|
kotlin("multiplatform")
|
||||||
id("com.android.library")
|
id("com.android.library")
|
||||||
kotlin("plugin.serialization")
|
kotlin("plugin.serialization")
|
||||||
|
id("org.jetbrains.compose")
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
@ -11,11 +12,13 @@ kotlin {
|
||||||
baseName = "Pihelper"
|
baseName = "Pihelper"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
jvm("desktop")
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
val commonMain by getting {
|
val commonMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.ktor.client.core)
|
implementation(libs.ktor.client.core)
|
||||||
|
implementation(libs.ktor.client.cio)
|
||||||
implementation(libs.ktor.client.logging)
|
implementation(libs.ktor.client.logging)
|
||||||
implementation(libs.ktor.client.serialization)
|
implementation(libs.ktor.client.serialization)
|
||||||
implementation(libs.ktor.client.content.negotiation)
|
implementation(libs.ktor.client.content.negotiation)
|
||||||
|
@ -23,12 +26,16 @@ kotlin {
|
||||||
implementation(libs.kotlinx.datetime)
|
implementation(libs.kotlinx.datetime)
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
api(libs.multiplatform.settings)
|
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 {
|
val androidMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.ktor.client.android)
|
|
||||||
implementation(libs.plausible)
|
implementation(libs.plausible)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,7 +47,13 @@ kotlin {
|
||||||
iosArm64Main.dependsOn(this)
|
iosArm64Main.dependsOn(this)
|
||||||
iosSimulatorArm64Main.dependsOn(this)
|
iosSimulatorArm64Main.dependsOn(this)
|
||||||
dependencies {
|
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
|
package com.wbrawner.pihelper.shared
|
||||||
|
|
||||||
import io.ktor.client.*
|
|
||||||
import io.ktor.client.engine.android.*
|
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
fun PiholeAPIService.Companion.create() = KtorPiholeAPIService(HttpClient(Android) {
|
|
||||||
commonConfig()
|
|
||||||
})
|
|
||||||
|
|
||||||
actual fun String.hash(): String = BigInteger(
|
actual fun String.hash(): String = BigInteger(
|
||||||
1,
|
1,
|
||||||
|
|
|
@ -3,6 +3,7 @@ package com.wbrawner.pihelper.shared
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
import io.ktor.client.call.*
|
||||||
import io.ktor.client.engine.*
|
import io.ktor.client.engine.*
|
||||||
|
import io.ktor.client.engine.cio.*
|
||||||
import io.ktor.client.plugins.*
|
import io.ktor.client.plugins.*
|
||||||
import io.ktor.client.plugins.contentnegotiation.*
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
|
@ -26,6 +27,10 @@ interface PiholeAPIService {
|
||||||
companion object
|
companion object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun PiholeAPIService.Companion.create() = KtorPiholeAPIService(HttpClient(CIO) {
|
||||||
|
commonConfig()
|
||||||
|
})
|
||||||
|
|
||||||
fun <T : HttpClientEngineConfig> HttpClientConfig<T>.commonConfig() {
|
fun <T : HttpClientEngineConfig> HttpClientConfig<T>.commonConfig() {
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
json(Json {
|
json(Json {
|
||||||
|
|
|
@ -8,41 +8,9 @@ import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
import kotlinx.serialization.encoding.Decoder
|
import kotlinx.serialization.encoding.Decoder
|
||||||
import kotlinx.serialization.encoding.Encoder
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
|
||||||
@Serializable()
|
@Serializable
|
||||||
data class Summary(
|
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,
|
override val status: Status,
|
||||||
@SerialName("gravity_last_updated")
|
|
||||||
val gravity: Gravity? = null,
|
|
||||||
val type: String? = null,
|
val type: String? = null,
|
||||||
val version: Int? = null
|
val version: Int? = null
|
||||||
) : StatusProvider
|
) : 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()
|
@Serializable()
|
||||||
data class VersionResponse(val version: Int)
|
data class VersionResponse(val version: Int)
|
||||||
|
|
||||||
|
|
|
@ -230,6 +230,7 @@ class Store(
|
||||||
route = Route.AUTH,
|
route = Route.AUTH,
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
_state.value = _state.value.copy(loading = false)
|
_state.value = _state.value.copy(loading = false)
|
||||||
if (emitError) {
|
if (emitError) {
|
||||||
_effects.emit(Effect.Error(e.message ?: "Failed to connect to $host"))
|
_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.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
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.LocalAutofillTree
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
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 AUTH_SCREEN_TAG = "authScreen"
|
||||||
const val SUCCESS_TEXT_TAG = "successText"
|
const val SUCCESS_TEXT_TAG = "successText"
|
||||||
|
@ -63,10 +66,7 @@ fun AuthScreen(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
|
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
|
||||||
) {
|
) {
|
||||||
Image(
|
LoadingSpinner(animate = false)
|
||||||
painter = painterResource(id = R.drawable.ic_app_logo),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.testTag(SUCCESS_TEXT_TAG),
|
modifier = Modifier.testTag(SUCCESS_TEXT_TAG),
|
||||||
text = "Pi-helper has successfully connected to your Pi-Hole!",
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
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.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.wbrawner.pihelper.shared.*
|
import com.wbrawner.pihelper.shared.Action
|
||||||
import com.wbrawner.pihelper.ui.PihelperTheme
|
import com.wbrawner.pihelper.shared.Store
|
||||||
|
import com.wbrawner.pihelper.shared.ui.component.LoadingSpinner
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun InfoScreen(store: Store) {
|
fun InfoScreen(store: Store) {
|
||||||
|
@ -107,10 +105,10 @@ fun InfoScreen(onBackClicked: () -> Unit, onForgetPiholeClicked: () -> Unit) {
|
||||||
message.getStringAnnotations(it, it).firstOrNull()?.let { annotation ->
|
message.getStringAnnotations(it, it).firstOrNull()?.let { annotation ->
|
||||||
uriHandler.openUri(annotation.item)
|
uriHandler.openUri(annotation.item)
|
||||||
// TODO: Move this to the store?
|
// TODO: Move this to the store?
|
||||||
PlausibleAnalyticsHelper.event(
|
// PlausibleAnalyticsHelper.event(
|
||||||
AnalyticsEvent.LinkClicked(annotation.item),
|
// AnalyticsEvent.LinkClicked(annotation.item),
|
||||||
Route.ABOUT
|
// Route.ABOUT
|
||||||
)
|
// )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TextButton(onClick = onForgetPiholeClicked) {
|
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)
|
@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.AnimatedContent
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
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.Effect
|
||||||
import com.wbrawner.pihelper.shared.Status
|
import com.wbrawner.pihelper.shared.Status
|
||||||
import com.wbrawner.pihelper.shared.Store
|
import com.wbrawner.pihelper.shared.Store
|
||||||
import com.wbrawner.pihelper.ui.DayNightPreview
|
import com.wbrawner.pihelper.shared.ui.component.LoadingSpinner
|
||||||
import com.wbrawner.pihelper.ui.PihelperTheme
|
import com.wbrawner.pihelper.shared.ui.component.PrimaryButton
|
||||||
import java.util.*
|
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import kotlin.math.roundToLong
|
import kotlin.math.roundToLong
|
||||||
import com.wbrawner.pihelper.shared.State as PihelperState
|
import com.wbrawner.pihelper.shared.State as PihelperState
|
||||||
|
@ -51,7 +50,6 @@ const val DISABLE_PERMANENT_BUTTON_TAG = "disablePermanentButton"
|
||||||
fun MainScreen(store: Store) {
|
fun MainScreen(store: Store) {
|
||||||
val state by store.state.collectAsState()
|
val state by store.state.collectAsState()
|
||||||
val effect by store.effects.collectAsState(initial = Effect.Empty)
|
val effect by store.effects.collectAsState(initial = Effect.Empty)
|
||||||
println(effect)
|
|
||||||
MainScreen(state = state, error = effect as? Effect.Error, dispatch = store::dispatch)
|
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),
|
modifier = Modifier.testTag(STATUS_TEXT_TAG),
|
||||||
color = color,
|
color = color,
|
||||||
fontWeight = FontWeight.Bold,
|
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()) {
|
if (status is Status.Disabled && !status.timeRemaining.isNullOrBlank()) {
|
||||||
Text(
|
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 {
|
enum class Duration {
|
||||||
SECONDS,
|
SECONDS,
|
||||||
MINUTES,
|
MINUTES,
|
||||||
HOURS
|
HOURS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomTimeDialog(
|
fun CustomTimeDialog(
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
|
@ -330,58 +311,58 @@ fun DurationToggle(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
//@Composable
|
||||||
@DayNightPreview
|
//@DayNightPreview
|
||||||
fun CustomTimeDialog_Preview() {
|
//fun CustomTimeDialog_Preview() {
|
||||||
PihelperTheme {
|
// PihelperTheme {
|
||||||
CustomTimeDialog(true, {}) { }
|
// CustomTimeDialog(true, {}) { }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
@Composable
|
//@Composable
|
||||||
@DayNightPreview
|
//@DayNightPreview
|
||||||
fun StatusLabelEnabled_Preview() {
|
//fun StatusLabelEnabled_Preview() {
|
||||||
PihelperTheme {
|
// PihelperTheme {
|
||||||
StatusLabel(Status.Enabled)
|
// StatusLabel(Status.Enabled)
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
@Composable
|
//@Composable
|
||||||
@DayNightPreview
|
//@DayNightPreview
|
||||||
fun StatusLabelDisabled_Preview() {
|
//fun StatusLabelDisabled_Preview() {
|
||||||
PihelperTheme {
|
// PihelperTheme {
|
||||||
StatusLabel(Status.Disabled())
|
// StatusLabel(Status.Disabled())
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
@Composable
|
//@Composable
|
||||||
@DayNightPreview
|
//@DayNightPreview
|
||||||
fun StatusLabelDisabledWithTime_Preview() {
|
//fun StatusLabelDisabledWithTime_Preview() {
|
||||||
PihelperTheme {
|
// PihelperTheme {
|
||||||
StatusLabel(Status.Disabled("12:34:56"))
|
// StatusLabel(Status.Disabled("12:34:56"))
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
@Composable
|
//@Composable
|
||||||
@DayNightPreview
|
//@DayNightPreview
|
||||||
fun PrimaryButton_Preview() {
|
//fun PrimaryButton_Preview() {
|
||||||
PihelperTheme {
|
// PihelperTheme {
|
||||||
PrimaryButton(text = "Disable") {}
|
// PrimaryButton(text = "Disable") {}
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
@Composable
|
//@Composable
|
||||||
@DayNightPreview
|
//@DayNightPreview
|
||||||
fun EnableControls_Preview() {
|
//fun EnableControls_Preview() {
|
||||||
PihelperTheme {
|
// PihelperTheme {
|
||||||
EnableControls {}
|
// EnableControls {}
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
@Composable
|
//@Composable
|
||||||
@DayNightPreview
|
//@DayNightPreview
|
||||||
fun DisableControls_Preview() {
|
//fun DisableControls_Preview() {
|
||||||
PihelperTheme {
|
// PihelperTheme {
|
||||||
DisableControls {}
|
// 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
|
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.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.Shapes
|
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.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.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
|
|
||||||
private val DarkColorPalette = darkColorScheme(
|
private val DarkColorPalette = darkColorScheme(
|
||||||
background = Color.Black,
|
background = Color.Black,
|
||||||
|
@ -31,20 +30,10 @@ private val LightColorPalette = lightColorScheme(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PihelperTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
|
fun PihelperTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
|
||||||
val context = LocalContext.current
|
val colors = if (darkTheme) {
|
||||||
val dynamic = false
|
DarkColorPalette
|
||||||
val colors = if (dynamic) {
|
|
||||||
if (darkTheme) {
|
|
||||||
dynamicDarkColorScheme(context)
|
|
||||||
} else {
|
|
||||||
dynamicLightColorScheme(context)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (darkTheme) {
|
LightColorPalette
|
||||||
DarkColorPalette
|
|
||||||
} else {
|
|
||||||
LightColorPalette
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialTheme(
|
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
|
|
|
@ -1,4 +1,4 @@
|
||||||
package com.wbrawner.pihelper.ui
|
package com.wbrawner.pihelper.shared.ui.theme
|
||||||
|
|
||||||
import androidx.compose.material3.Typography
|
import androidx.compose.material3.Typography
|
||||||
import androidx.compose.ui.text.TextStyle
|
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