Compare commits
56 commits
Author | SHA1 | Date | |
---|---|---|---|
b230f723b4 | |||
5a3fb94cb2 | |||
7bef2be13c | |||
d71a7cc2f7 | |||
7e1a2113fd | |||
680f900007 | |||
2b553e8327 | |||
9493f95eb3 | |||
bd9cc3c992 | |||
ae8cb65b6f | |||
85fe3118d0 | |||
15b31a874d | |||
8072c38e4d | |||
badb12812c | |||
2bdb4a4974 | |||
a912cd6cd3 | |||
2649e1e6d2 | |||
0b26a16f1f | |||
dba5313c74 | |||
f2d5687a5f | |||
4bf56a5918 | |||
d533682af6 | |||
b5c23fc5ca | |||
7447a5ca6d | |||
a4ec4c8aa4 | |||
a7d0119250 | |||
5df9a77714 | |||
22d81d0fd5 | |||
a5b1b33d99 | |||
3ffd663c8d | |||
3fd8b61043 | |||
6286016da2 | |||
7bbbb022e3 | |||
731faf7894 | |||
fc3dbb7fb7 | |||
ba830e8b67 | |||
6c18dc6d1d | |||
f2fb3f6c2e | |||
c1216a58d3 | |||
8a2ae66b2f | |||
c40e8f0274 | |||
120c155114 | |||
deff195d1f | |||
a44e48d1e1 | |||
fc06a2f91d | |||
6165886416 | |||
2a6c469f07 | |||
09f261b034 | |||
65838d6905 | |||
3f98551b9d | |||
bdacaaae34 | |||
e345afb2b7 | |||
09ce5eebf7 | |||
72be201475 | |||
b6bf045fd0 | |||
53cd8c7312 |
129 changed files with 3774 additions and 2623 deletions
74
.forgejo/workflows/pull_request.yml
Normal file
74
.forgejo/workflows/pull_request.yml
Normal file
|
@ -0,0 +1,74 @@
|
|||
name: Build & Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
name: Validate
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: set up JDK
|
||||
uses: https://git.wbrawner.com/actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '21'
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: https://git.wbrawner.com/gradle/actions/wrapper-validation@v4
|
||||
unit_tests:
|
||||
name: Run Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- validate
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: set up JDK
|
||||
uses: https://git.wbrawner.com/actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '21'
|
||||
- name: Setup Android SDK
|
||||
uses: https://git.wbrawner.com/android-actions/setup-android@v3
|
||||
- name: Setup Gradle
|
||||
uses: https://git.wbrawner.com/gradle/actions/setup-gradle@v4
|
||||
- name: Run checks
|
||||
run: ./gradlew --no-daemon --console=plain check
|
||||
# TODO: Uncomment the following once I get unit tests written
|
||||
# - name: Publish JUnit Results
|
||||
# uses: actions/upload-artifact@v3
|
||||
# if: always()
|
||||
# with:
|
||||
# name: Unit Test Results
|
||||
# path: "*/build/reports/*"
|
||||
# if-no-files-found: error
|
||||
# TODO: Uncomment the following once I get UI tests written
|
||||
# ui_tests:
|
||||
# runs-on: ubuntu-latest
|
||||
# name: Run UI Tests
|
||||
# needs:
|
||||
# - validate
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# - name: set up JDK
|
||||
# uses: https://git.wbrawner.com/actions/setup-java@v3
|
||||
# with:
|
||||
# distribution: 'zulu'
|
||||
# java-version: '21'
|
||||
# - name: Build with Gradle
|
||||
# uses: https://git.wbrawner.com/gradle/gradle-build-action@v2
|
||||
# with:
|
||||
# arguments: assembleDebug assembleDebugAndroidTest
|
||||
# - name: Grant execute permission for flank_auth.sh
|
||||
# run: chmod +x flank_auth.sh
|
||||
# - name: Add auth for flank
|
||||
# env:
|
||||
# GCLOUD_KEY: ${{ secrets.GCLOUD_KEY }}
|
||||
# run: |
|
||||
# ./flank_auth.sh
|
||||
# - name: Run UI tests
|
||||
# uses: https://git.wbrawner.com/gradle/gradle-build-action@v2
|
||||
# with:
|
||||
# arguments: runFlank
|
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -1,15 +1,11 @@
|
|||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.idea
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
keystore.properties
|
||||
.kotlin
|
||||
|
|
|
@ -1 +1 @@
|
|||
Pi-Helper
|
||||
Pi-helper
|
251
.idea/androidTestResultsUserPreferences.xml
Normal file
251
.idea/androidTestResultsUserPreferences.xml
Normal file
|
@ -0,0 +1,251 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidTestResultsUserPreferences">
|
||||
<option name="androidTestResultsTableState">
|
||||
<map>
|
||||
<entry key="-1681706849">
|
||||
<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>
|
||||
<entry key="-1446219758">
|
||||
<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>
|
||||
<entry key="-1423466859">
|
||||
<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>
|
||||
<entry key="-1235639767">
|
||||
<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>
|
||||
<entry key="-1082288520">
|
||||
<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>
|
||||
<entry key="-1041444055">
|
||||
<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>
|
||||
<entry key="-964027671">
|
||||
<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>
|
||||
<entry key="-797988877">
|
||||
<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>
|
||||
<entry key="-725122147">
|
||||
<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>
|
||||
<entry key="-255309275">
|
||||
<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="20293776">
|
||||
<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="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>
|
||||
<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>
|
||||
<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>
|
||||
<option name="preferredColumnWidths">
|
||||
<map>
|
||||
<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>
|
||||
</option>
|
||||
</AndroidTestResultsTableState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="1517452226">
|
||||
<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>
|
||||
<entry key="2038388625">
|
||||
<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>
|
||||
<entry key="2093109046">
|
||||
<value>
|
||||
<AndroidTestResultsTableState>
|
||||
<option name="preferredColumnWidths">
|
||||
<map>
|
||||
<entry key="Duration" value="90" />
|
||||
<entry key="Medium_Phone_API_35" value="120" />
|
||||
<entry key="Pixel_3a_API_33_arm64-v8a" value="120" />
|
||||
<entry key="Tests" value="360" />
|
||||
</map>
|
||||
</option>
|
||||
</AndroidTestResultsTableState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
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>
|
6
.idea/compiler.xml
Normal file
6
.idea/compiler.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="21" />
|
||||
</component>
|
||||
</project>
|
10
.idea/deploymentTargetDropDown.xml
Normal file
10
.idea/deploymentTargetDropDown.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetDropDown">
|
||||
<value>
|
||||
<entry key="app">
|
||||
<State />
|
||||
</entry>
|
||||
</value>
|
||||
</component>
|
||||
</project>
|
|
@ -4,17 +4,18 @@
|
|||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="PLATFORM" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<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$/piholeclient" />
|
||||
<option value="$PROJECT_DIR$/desktop" />
|
||||
<option value="$PROJECT_DIR$/shared" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
<option name="resolveExternalAnnotations" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
|
|
41
.idea/inspectionProfiles/Project_Default.xml
Normal file
41
.idea/inspectionProfiles/Project_Default.xml
Normal file
|
@ -0,0 +1,41 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
40
.idea/jarRepositories.xml
Normal file
40
.idea/jarRepositories.xml
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="BintrayJCenter" />
|
||||
<option name="name" value="BintrayJCenter" />
|
||||
<option name="url" value="https://jcenter.bintray.com/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="Google" />
|
||||
<option name="name" value="Google" />
|
||||
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="MavenRepo" />
|
||||
<option name="name" value="MavenRepo" />
|
||||
<option name="url" value="https://repo.maven.apache.org/maven2/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven" />
|
||||
<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>
|
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="2.0.21" />
|
||||
</component>
|
||||
</project>
|
|
@ -5,7 +5,67 @@
|
|||
<configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" />
|
||||
</configurations>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<component name="DesignSurface">
|
||||
<option name="filePathToZoomLevelMap">
|
||||
<map>
|
||||
<entry key="../../../../layout/compose-model-1622823449309.xml" value="0.1523829431438127" />
|
||||
<entry key="../../../../layout/compose-model-1622824814438.xml" value="0.33" />
|
||||
<entry key="../../../../layout/compose-model-1622917622395.xml" value="0.2170608108108108" />
|
||||
<entry key="../../../../layout/compose-model-1623014109717.xml" value="0.337037037037037" />
|
||||
<entry key="../../../../layout/compose-model-1623014799493.xml" value="0.10015558148580318" />
|
||||
<entry key="../../../../layout/compose-model-1623015496605.xml" value="0.1" />
|
||||
<entry key="../../../../layout/compose-model-1623015889966.xml" value="0.1072324414715719" />
|
||||
<entry key="../../../../layout/compose-model-1623016793844.xml" value="0.1" />
|
||||
<entry key="../../../../layout/compose-model-1623332506723.xml" value="0.1" />
|
||||
<entry key="../../../../layout/compose-model-1623332556375.xml" value="0.1" />
|
||||
<entry key="../../../../layout/compose-model-1623353007224.xml" value="0.1" />
|
||||
<entry key="../../../../layout/compose-model-1623353013891.xml" value="2.0" />
|
||||
<entry key="../../../../layout/compose-model-1623358629334.xml" value="0.10618436406067679" />
|
||||
<entry key="../../../../layout/compose-model-1623358815178.xml" value="1.6830769230769231" />
|
||||
<entry key="../../../../layout/compose-model-1623359670724.xml" value="0.11413043478260869" />
|
||||
<entry key="../../../../layout/compose-model-1623360495184.xml" value="0.20880752102919348" />
|
||||
<entry key="../../../../layout/compose-model-1646330539642.xml" value="0.1" />
|
||||
<entry key="../../../../layout/compose-model-1646331413004.xml" value="0.4506079027355623" />
|
||||
<entry key="../../../../layout/compose-model-1646332602415.xml" value="0.13775083612040134" />
|
||||
<entry key="../../../../layout/compose-model-1646358533232.xml" value="0.30153061224489797" />
|
||||
<entry key="../../../../layout/compose-model-1647060386627.xml" value="0.1" />
|
||||
<entry key="../../../../layout/compose-model-1647060537103.xml" value="0.12374581939799331" />
|
||||
<entry key="../../../../layout/compose-model-1647101512079.xml" value="0.12332775919732442" />
|
||||
<entry key="../../../../layout/compose-model-1647102165810.xml" value="0.1" />
|
||||
<entry key="app/src/main/res/drawable-v26/ic_shortcut_enable.xml" value="0.27447916666666666" />
|
||||
<entry key="app/src/main/res/drawable-v26/ic_shortcut_pause.xml" value="0.27447916666666666" />
|
||||
<entry key="app/src/main/res/drawable/background_splash.xml" value="0.409375" />
|
||||
<entry key="app/src/main/res/drawable/horizontal_rule.xml" value="0.1" />
|
||||
<entry key="app/src/main/res/drawable/ic_app_logo.xml" value="0.1065" />
|
||||
<entry key="app/src/main/res/drawable/ic_launcher_background.xml" value="0.27447916666666666" />
|
||||
<entry key="app/src/main/res/drawable/ic_pause.xml" value="0.409375" />
|
||||
<entry key="app/src/main/res/drawable/ic_play.xml" value="0.27447916666666666" />
|
||||
<entry key="app/src/main/res/drawable/ic_play_arrow.xml" value="0.27447916666666666" />
|
||||
<entry key="app/src/main/res/drawable/ic_settings.xml" value="0.409375" />
|
||||
<entry key="app/src/main/res/drawable/ic_shortcut_background.xml" value="0.27447916666666666" />
|
||||
<entry key="app/src/main/res/drawable/ic_shortcut_enable.xml" value="0.409375" />
|
||||
<entry key="app/src/main/res/drawable/ic_shortcut_pause.xml" value="0.409375" />
|
||||
<entry key="app/src/main/res/layout/activity_main.xml" value="0.16111111111111112" />
|
||||
<entry key="app/src/main/res/layout/dialog_disable_custom_time.xml" value="0.22708333333333333" />
|
||||
<entry key="app/src/main/res/layout/fragment_add_pi_hole.xml" value="0.3493975903614458" />
|
||||
<entry key="app/src/main/res/layout/fragment_info.xml" value="0.24010416666666667" />
|
||||
<entry key="app/src/main/res/layout/fragment_main.xml" value="0.14175724637681159" />
|
||||
<entry key="app/src/main/res/layout/fragment_retrieve_api_key.xml" value="0.24010416666666667" />
|
||||
<entry key="app/src/main/res/layout/or_divider.xml" value="0.17147922998986828" />
|
||||
<entry key="app/src/main/res/menu/main.xml" value="0.3932291666666667" />
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
<component name="EntryPointsManager">
|
||||
<list size="1">
|
||||
<item index="0" class="java.lang.String" itemvalue="dagger.hilt.android.testing.BindValue" />
|
||||
</list>
|
||||
</component>
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
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>
|
|
@ -1,73 +0,0 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
try {
|
||||
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
} catch (FileNotFoundException ignored) {
|
||||
logger.warn("Unable to load keystore properties. Using debug signing configuration instead")
|
||||
keystoreProperties['keyAlias'] = "androiddebugkey"
|
||||
keystoreProperties['keyPassword'] = "android"
|
||||
keystoreProperties['storeFile'] = new File(System.getProperty("user.home"), ".android/debug.keystore").absolutePath
|
||||
keystoreProperties['storePassword'] = "android"
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
defaultConfig {
|
||||
applicationId "com.wbrawner.pihelper"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 29
|
||||
versionCode 2
|
||||
versionName "1.0.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
signingConfigs {
|
||||
release {
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
storeFile file(keystoreProperties['storeFile'])
|
||||
storePassword keystoreProperties['storePassword']
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':piholeclient')
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||
implementation "org.koin:koin-androidx-viewmodel:$koin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.core:core-ktx:1.2.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'com.google.android.material:material:1.2.0-alpha06'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.security:security-crypto:1.0.0-rc01'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
def navigation_version = '2.2.2'
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$navigation_version"
|
||||
def lifecycle_version = '2.2.0'
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
||||
}
|
100
app/build.gradle.kts
Normal file
100
app/build.gradle.kts
Normal file
|
@ -0,0 +1,100 @@
|
|||
import java.io.FileInputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.util.*
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.ksp)
|
||||
alias(libs.plugins.hilt.android)
|
||||
alias(libs.plugins.compose)
|
||||
}
|
||||
|
||||
val keystoreProperties = Properties()
|
||||
try {
|
||||
val keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||
} catch (ignored: FileNotFoundException) {
|
||||
logger.warn("Unable to load keystore properties. Using debug signing configuration instead")
|
||||
keystoreProperties["keyAlias"] = "androiddebugkey"
|
||||
keystoreProperties["keyPassword"] = "android"
|
||||
keystoreProperties["storeFile"] =
|
||||
File(System.getProperty("user.home"), ".android/debug.keystore").absolutePath
|
||||
keystoreProperties["storePassword"] = "android"
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = libs.versions.maxSdk.get().toInt()
|
||||
defaultConfig {
|
||||
applicationId = "com.wbrawner.pihelper"
|
||||
minSdk = libs.versions.minSdk.get().toInt()
|
||||
targetSdk = libs.versions.maxSdk.get().toInt()
|
||||
versionCode = libs.versions.versionCode.get().toInt()
|
||||
versionName = libs.versions.versionName.get()
|
||||
testInstrumentationRunner = "com.wbrawner.pihelper.util.HiltTestRunner"
|
||||
testInstrumentationRunnerArguments["clearPackageData"] = "true"
|
||||
signingConfig = signingConfigs["debug"]
|
||||
}
|
||||
testOptions {
|
||||
execution = "ANDROIDX_TEST_ORCHESTRATOR"
|
||||
}
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
keyAlias = keystoreProperties["keyAlias"].toString()
|
||||
keyPassword = keystoreProperties["keyPassword"].toString()
|
||||
storeFile = file(keystoreProperties["storeFile"].toString())
|
||||
storePassword = keystoreProperties["storePassword"].toString()
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
signingConfig = signingConfigs["release"]
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "21"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
|
||||
}
|
||||
namespace = "com.wbrawner.pihelper"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":shared"))
|
||||
implementation(libs.bundles.coroutines)
|
||||
implementation(libs.bundles.compose)
|
||||
implementation(libs.hilt.android.core)
|
||||
implementation(libs.hilt.navigation.compose)
|
||||
ksp(libs.hilt.android.ksp)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.splash)
|
||||
implementation(libs.material)
|
||||
implementation("androidx.legacy:legacy-support-v4:1.0.0")
|
||||
implementation("androidx.security:security-crypto:1.0.0")
|
||||
implementation(libs.preference)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.mockwebserver)
|
||||
androidTestImplementation(libs.androidx.test.runner)
|
||||
androidTestUtil(libs.androidx.test.orchestrator)
|
||||
androidTestImplementation(libs.test.ext)
|
||||
androidTestImplementation(libs.espresso)
|
||||
androidTestImplementation(libs.hilt.android.testing)
|
||||
kspAndroidTest(libs.hilt.android.ksp)
|
||||
androidTestImplementation(libs.compose.test.junit)
|
||||
debugImplementation(libs.compose.test.manifest)
|
||||
}
|
||||
|
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
|
@ -1,6 +1,6 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
# proguardFiles setting in build.gradle.kts.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
|
3
app/src/androidTest/assets/json/status_disabled.json
Normal file
3
app/src/androidTest/assets/json/status_disabled.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"status": "disabled"
|
||||
}
|
3
app/src/androidTest/assets/json/status_enabled.json
Normal file
3
app/src/androidTest/assets/json/status_enabled.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"status": "enabled"
|
||||
}
|
38
app/src/androidTest/assets/json/summary_disabled.json
Normal file
38
app/src/androidTest/assets/json/summary_disabled.json
Normal file
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"domains_being_blocked": 166010,
|
||||
"dns_queries_today": 71567,
|
||||
"ads_blocked_today": 37254,
|
||||
"ads_percentage_today": 52.054718,
|
||||
"unique_domains": 17559,
|
||||
"queries_forwarded": 29352,
|
||||
"queries_cached": 4864,
|
||||
"clients_ever_seen": 6,
|
||||
"unique_clients": 3,
|
||||
"dns_queries_all_types": 71567,
|
||||
"reply_UNKNOWN": 174,
|
||||
"reply_NODATA": 7023,
|
||||
"reply_NXDOMAIN": 2006,
|
||||
"reply_CNAME": 13863,
|
||||
"reply_IP": 47973,
|
||||
"reply_DOMAIN": 48,
|
||||
"reply_RRNAME": 0,
|
||||
"reply_SERVFAIL": 0,
|
||||
"reply_REFUSED": 0,
|
||||
"reply_NOTIMP": 0,
|
||||
"reply_OTHER": 0,
|
||||
"reply_DNSSEC": 0,
|
||||
"reply_NONE": 0,
|
||||
"reply_BLOB": 480,
|
||||
"dns_queries_all_replies": 71567,
|
||||
"privacy_level": 0,
|
||||
"status": "disabled",
|
||||
"gravity_last_updated": {
|
||||
"file_exists": true,
|
||||
"absolute": 1671361515,
|
||||
"relative": {
|
||||
"days": 3,
|
||||
"hours": 5,
|
||||
"minutes": 23
|
||||
}
|
||||
}
|
||||
}
|
38
app/src/androidTest/assets/json/summary_enabled.json
Normal file
38
app/src/androidTest/assets/json/summary_enabled.json
Normal file
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"domains_being_blocked": 166010,
|
||||
"dns_queries_today": 71567,
|
||||
"ads_blocked_today": 37254,
|
||||
"ads_percentage_today": 52.054718,
|
||||
"unique_domains": 17559,
|
||||
"queries_forwarded": 29352,
|
||||
"queries_cached": 4864,
|
||||
"clients_ever_seen": 6,
|
||||
"unique_clients": 3,
|
||||
"dns_queries_all_types": 71567,
|
||||
"reply_UNKNOWN": 174,
|
||||
"reply_NODATA": 7023,
|
||||
"reply_NXDOMAIN": 2006,
|
||||
"reply_CNAME": 13863,
|
||||
"reply_IP": 47973,
|
||||
"reply_DOMAIN": 48,
|
||||
"reply_RRNAME": 0,
|
||||
"reply_SERVFAIL": 0,
|
||||
"reply_REFUSED": 0,
|
||||
"reply_NOTIMP": 0,
|
||||
"reply_OTHER": 0,
|
||||
"reply_DNSSEC": 0,
|
||||
"reply_NONE": 0,
|
||||
"reply_BLOB": 480,
|
||||
"dns_queries_all_replies": 71567,
|
||||
"privacy_level": 0,
|
||||
"status": "enabled",
|
||||
"gravity_last_updated": {
|
||||
"file_exists": true,
|
||||
"absolute": 1671361515,
|
||||
"relative": {
|
||||
"days": 3,
|
||||
"hours": 5,
|
||||
"minutes": 23
|
||||
}
|
||||
}
|
||||
}
|
1
app/src/androidTest/assets/json/summary_failure.json
Normal file
1
app/src/androidTest/assets/json/summary_failure.json
Normal file
|
@ -0,0 +1 @@
|
|||
[]
|
1
app/src/androidTest/assets/json/top_items_failure.json
Normal file
1
app/src/androidTest/assets/json/top_items_failure.json
Normal file
|
@ -0,0 +1 @@
|
|||
[]
|
56
app/src/androidTest/assets/json/top_items_success.json
Normal file
56
app/src/androidTest/assets/json/top_items_success.json
Normal file
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"top_queries": {
|
||||
"gateway.fe.apple-dns.net": 647,
|
||||
"lb._dns-sd._udp.0.1.168.192.in-addr.arpa": 390,
|
||||
"github.com": 385,
|
||||
"www.belkin.com": 305,
|
||||
"ip.wbrawner.com": 294,
|
||||
"mask.icloud.com": 292,
|
||||
"23.1.168.192.in-addr.arpa": 245,
|
||||
"weather-data.apple.com": 232,
|
||||
"www.google.com": 224,
|
||||
"ocsp2-lb.apple.com.akadns.net": 202,
|
||||
"www.googleapis.com": 198,
|
||||
"e673.dsce9.akamaiedge.net": 196,
|
||||
"domains.google.com": 194,
|
||||
"c1187123150.ip4-d1b183ab.saasprotection.com": 183,
|
||||
"googlehosted.l.googleusercontent.com": 177,
|
||||
"_dns.resolver.arpa": 177,
|
||||
"www.netgear.com": 175,
|
||||
"dns.google": 174,
|
||||
"play.googleapis.com": 170,
|
||||
"ocsp2.g.aaplimg.com": 167,
|
||||
"time.g.aaplimg.com": 161,
|
||||
"userproxypac.aexp.com": 161,
|
||||
"www.gstatic.com": 160,
|
||||
"nrdp.prod.cloud.netflix.com": 155,
|
||||
"mdw-efz.ms-acdc.office.com": 152
|
||||
},
|
||||
"top_ads": {
|
||||
"api2.branch.io": 4173,
|
||||
"cws.conviva.com": 3019,
|
||||
"scribe.logs.roku.com": 1119,
|
||||
"mobile.pipe.aria.microsoft.com": 1083,
|
||||
"ssl.google-analytics.com": 526,
|
||||
"data.emb-api.com": 325,
|
||||
"api.bugfender.com": 200,
|
||||
"googleads.g.doubleclick.net": 174,
|
||||
"app-measurement.com": 157,
|
||||
"config.emb-api.com": 154,
|
||||
"fls-na.amazon.com": 125,
|
||||
"s.youtube.com": 109,
|
||||
"device-api.urbanairship.com": 106,
|
||||
"mobile-collector.newrelic.com": 95,
|
||||
"incoming.telemetry.mozilla.org": 93,
|
||||
"metrics.icloud.com": 74,
|
||||
"www.googleadservices.com": 63,
|
||||
"nova.collect.igodigital.com": 57,
|
||||
"www.googletagmanager.com": 51,
|
||||
"app.adjust.com": 43,
|
||||
"app.adjust.net.in": 43,
|
||||
"app.adjust.world": 43,
|
||||
"iadsdk.apple.com": 41,
|
||||
"api.apptentive.com": 41,
|
||||
"adservice.google.com": 39
|
||||
}
|
||||
}
|
3
app/src/androidTest/assets/json/version_success.json
Normal file
3
app/src/androidTest/assets/json/version_success.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"version": 3
|
||||
}
|
91
app/src/androidTest/java/com/wbrawner/pihelper/MainTests.kt
Normal file
91
app/src/androidTest/java/com/wbrawner/pihelper/MainTests.kt
Normal file
|
@ -0,0 +1,91 @@
|
|||
package com.wbrawner.pihelper
|
||||
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import com.wbrawner.pihelper.shared.PiholeAPIService
|
||||
import com.wbrawner.pihelper.shared.State
|
||||
import com.wbrawner.pihelper.shared.Store
|
||||
import com.wbrawner.pihelper.util.FakeAPIService
|
||||
import com.wbrawner.pihelper.util.onMainScreen
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.testing.BindValue
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import dagger.hilt.android.testing.UninstallModules
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Singleton
|
||||
|
||||
@UninstallModules(PiHelperModule::class)
|
||||
@HiltAndroidTest
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
class MainTests {
|
||||
|
||||
@get:Rule(order = 1)
|
||||
val composeTestRule = createAndroidComposeRule<MainActivity>()
|
||||
|
||||
@get:Rule(order = 0)
|
||||
val hiltTestRule = HiltAndroidRule(this)
|
||||
|
||||
@BindValue
|
||||
@JvmField
|
||||
val apiService: FakeAPIService = FakeAPIService()
|
||||
|
||||
@Test
|
||||
fun testDisable() {
|
||||
onMainScreen(composeTestRule) {
|
||||
apiService.statusEnabled(context)
|
||||
apiService.disableSuccess(context)
|
||||
apiService.statusDisabled(context)
|
||||
apiService.disabledPermanently()
|
||||
verifyStatus("Enabled")
|
||||
val request = apiService.server.takeRequest(1, TimeUnit.SECONDS)
|
||||
assertTrue(request?.requestUrl?.queryParameterNames?.contains("auth") == true)
|
||||
clickDisablePermanentlyButton()
|
||||
verifyStatus("Disabled")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEnable() {
|
||||
onMainScreen(composeTestRule) {
|
||||
apiService.statusDisabled(context)
|
||||
apiService.disabledPermanently()
|
||||
apiService.enableSuccess(context)
|
||||
apiService.statusEnabled(context)
|
||||
verifyStatus("Disabled")
|
||||
val request = apiService.server.takeRequest(1, TimeUnit.SECONDS)
|
||||
assertTrue(request?.requestUrl?.queryParameterNames?.contains("auth") == true)
|
||||
clickEnableButton()
|
||||
verifyStatus("Enabled")
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
inner class TestModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesPiholeAPIService(): PiholeAPIService = apiService
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesInitialState(apiService: FakeAPIService): State = State(
|
||||
apiKey = "key",
|
||||
host = "${apiService.hostName}:${apiService.port}"
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesStore(
|
||||
apiService: PiholeAPIService,
|
||||
initialState: State
|
||||
): Store = Store(apiService, initialState = initialState)
|
||||
}
|
||||
}
|
153
app/src/androidTest/java/com/wbrawner/pihelper/SetupTests.kt
Normal file
153
app/src/androidTest/java/com/wbrawner/pihelper/SetupTests.kt
Normal file
|
@ -0,0 +1,153 @@
|
|||
package com.wbrawner.pihelper
|
||||
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import com.wbrawner.pihelper.shared.PiholeAPIService
|
||||
import com.wbrawner.pihelper.shared.Store
|
||||
import com.wbrawner.pihelper.util.FakeAPIService
|
||||
import com.wbrawner.pihelper.util.onAddScreen
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.testing.BindValue
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import dagger.hilt.android.testing.UninstallModules
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Singleton
|
||||
|
||||
@UninstallModules(PiHelperModule::class)
|
||||
@HiltAndroidTest
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
class SetupTests {
|
||||
|
||||
@get:Rule(order = 1)
|
||||
val composeTestRule = createAndroidComposeRule<MainActivity>()
|
||||
|
||||
@get:Rule(order = 0)
|
||||
val hiltTestRule = HiltAndroidRule(this)
|
||||
|
||||
@BindValue
|
||||
@JvmField
|
||||
val apiService: FakeAPIService = FakeAPIService()
|
||||
|
||||
@Test
|
||||
fun testSuccessfulConnectionWithPassword() {
|
||||
onAddScreen(composeTestRule) {
|
||||
apiService.testConnectionSuccess()
|
||||
clearHost()
|
||||
inputHost("${apiService.hostName}:${apiService.port}")
|
||||
clickConnect()
|
||||
apiService.server.takeRequest(1, TimeUnit.SECONDS)
|
||||
} onAuthScreen {
|
||||
apiService.authenticationSuccess(context)
|
||||
verifyConnectionSuccessMessage()
|
||||
inputPassword("password")
|
||||
clickAuthenticateWithPassword()
|
||||
val request = apiService.server.takeRequest(1, TimeUnit.SECONDS)
|
||||
assertTrue(request?.requestUrl?.queryParameterNames?.contains("topItems") ?: false)
|
||||
assertEquals(
|
||||
"113459eb7bb31bddee85ade5230d6ad5d8b2fb52879e00a84ff6ae1067a210d3",
|
||||
request?.requestUrl?.queryParameter("auth")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSuccessfulConnectionWithAPIKey() {
|
||||
onAddScreen(composeTestRule) {
|
||||
apiService.testConnectionSuccess()
|
||||
clearHost()
|
||||
inputHost("${apiService.hostName}:${apiService.port}")
|
||||
clickConnect()
|
||||
apiService.server.takeRequest(1, TimeUnit.SECONDS)
|
||||
} onAuthScreen {
|
||||
apiService.authenticationSuccess(context)
|
||||
verifyConnectionSuccessMessage()
|
||||
inputAPIKey("113459eb7bb31bddee85ade5230d6ad5d8b2fb52879e00a84ff6ae1067a210d3")
|
||||
clickAuthenticateWithAPIKey()
|
||||
val request = apiService.server.takeRequest(1, TimeUnit.SECONDS)
|
||||
assertTrue(request?.requestUrl?.queryParameterNames?.contains("topItems") ?: false)
|
||||
assertEquals(
|
||||
"113459eb7bb31bddee85ade5230d6ad5d8b2fb52879e00a84ff6ae1067a210d3",
|
||||
request?.requestUrl?.queryParameter("auth")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFailedConnection() {
|
||||
onAddScreen(composeTestRule) {
|
||||
clearHost()
|
||||
inputHost("localhost")
|
||||
clickConnect()
|
||||
verifyErrorMessageIsDisplayed("Failed to connect")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInvalidHost() {
|
||||
onAddScreen(composeTestRule) {
|
||||
apiService.testConnectionFailure()
|
||||
clearHost()
|
||||
inputHost("${apiService.hostName}:${apiService.port}")
|
||||
clickConnect()
|
||||
verifyErrorMessageIsDisplayed("Host does not appear to be a valid Pi-hole")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFailedAuthenticationWithAPIKey() {
|
||||
onAddScreen(composeTestRule) {
|
||||
apiService.testConnectionSuccess()
|
||||
clearHost()
|
||||
inputHost("${apiService.hostName}:${apiService.port}")
|
||||
clickConnect()
|
||||
} onAuthScreen {
|
||||
apiService.authenticationFailure(context)
|
||||
verifyConnectionSuccessMessage()
|
||||
inputAPIKey("113459eb7bb31bddee85ade5230d6ad5d8b2fb52879e00a84ff6ae1067a210d3")
|
||||
clickAuthenticateWithAPIKey()
|
||||
verifyErrorMessageIsDisplayed("Invalid credentials")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFailedAuthenticationWithPassword() {
|
||||
onAddScreen(composeTestRule) {
|
||||
apiService.testConnectionSuccess()
|
||||
clearHost()
|
||||
inputHost("${apiService.hostName}:${apiService.port}")
|
||||
clickConnect()
|
||||
} onAuthScreen {
|
||||
verifyConnectionSuccessMessage()
|
||||
apiService.authenticationFailure(context)
|
||||
inputPassword("password")
|
||||
clickAuthenticateWithPassword()
|
||||
verifyErrorMessageIsDisplayed("Invalid credentials")
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class TestModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindsPiholeAPIService(apiService: FakeAPIService): PiholeAPIService
|
||||
|
||||
companion object {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesStore(
|
||||
apiService: PiholeAPIService
|
||||
): Store = Store(apiService)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package com.wbrawner.pihelper.util
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.ui.test.*
|
||||
import androidx.compose.ui.test.junit4.ComposeTestRule
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
|
||||
fun onAddScreen(testRule: ComposeTestRule, actions: AddScreenRobot.() -> Unit) =
|
||||
AddScreenRobot(testRule).apply { actions() }
|
||||
|
||||
class AddScreenRobot(private val testRule: ComposeTestRule) {
|
||||
val context: Context = InstrumentationRegistry.getInstrumentation().context
|
||||
|
||||
infix fun onAuthScreen(actions: AuthScreenRobot.() -> Unit) = AuthScreenRobot(testRule).run {
|
||||
actions()
|
||||
}
|
||||
|
||||
fun clearHost() =
|
||||
testRule.onNodeWithContentDescription("Pi-hole host input").performTextClearance()
|
||||
|
||||
fun inputHost(host: String) =
|
||||
testRule.onNodeWithContentDescription("Pi-hole host input").performTextInput(host)
|
||||
|
||||
fun clickConnect() = testRule.onNode(hasText("Connect")).performClick()
|
||||
|
||||
fun verifyErrorMessageIsDisplayed(message: String) {
|
||||
testRule.waitUntil(2_000) {
|
||||
testRule
|
||||
.onAllNodesWithContentDescription(message, substring = true)
|
||||
.fetchSemanticsNodes().size == 1
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package com.wbrawner.pihelper.util
|
||||
|
||||
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.shared.ui.*
|
||||
|
||||
class AuthScreenRobot(private val testRule: ComposeTestRule) {
|
||||
val context: Context = InstrumentationRegistry.getInstrumentation().context
|
||||
|
||||
init {
|
||||
testRule.waitUntil {
|
||||
testRule
|
||||
.onAllNodesWithTag(AUTH_SCREEN_TAG)
|
||||
.fetchSemanticsNodes().size == 1
|
||||
}
|
||||
}
|
||||
|
||||
fun verifyConnectionSuccessMessage() =
|
||||
testRule.onNode(hasTestTag(SUCCESS_TEXT_TAG)).assertExists()
|
||||
|
||||
fun inputPassword(password: String) =
|
||||
testRule.onNode(hasTestTag(PASSWORD_INPUT_TAG)).performTextInput(password)
|
||||
|
||||
fun clickAuthenticateWithPassword() = testRule.onNode(hasTestTag(PASSWORD_BUTTON_TAG))
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
fun inputAPIKey(key: String) =
|
||||
testRule.onNode(hasTestTag(API_KEY_INPUT_TAG)).performTextInput(key)
|
||||
|
||||
fun clickAuthenticateWithAPIKey() = testRule.onNode(hasTestTag(API_KEY_BUTTON_TAG))
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
fun verifyErrorMessageIsDisplayed(message: String) {
|
||||
testRule.waitUntil(2_000) {
|
||||
testRule
|
||||
.onAllNodesWithText(message, substring = true)
|
||||
.fetchSemanticsNodes().size == 1
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
package com.wbrawner.pihelper.util
|
||||
|
||||
import android.content.Context
|
||||
import com.wbrawner.pihelper.shared.PiholeAPIService
|
||||
import com.wbrawner.pihelper.shared.create
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
|
||||
class FakeAPIService : PiholeAPIService by PiholeAPIService.create() {
|
||||
val server = MockWebServer().apply {
|
||||
start()
|
||||
}
|
||||
val hostName: String = server.hostName
|
||||
val port = server.port
|
||||
|
||||
fun testConnectionSuccess() {
|
||||
server.enqueue(
|
||||
MockResponse().setHeader(
|
||||
"X-Pi-Hole",
|
||||
"The Pi-hole Web interface is working!"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun testConnectionFailure() {
|
||||
server.enqueue(MockResponse().setResponseCode(204))
|
||||
}
|
||||
|
||||
fun authenticationSuccess(context: Context) {
|
||||
server.enqueue(
|
||||
MockResponse()
|
||||
.setHeader("Content-Type", "application/json")
|
||||
.setBody(
|
||||
context.readAsset("json/top_items_success.json")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun authenticationFailure(context: Context) {
|
||||
server.enqueue(
|
||||
MockResponse()
|
||||
.setHeader("Content-Type", "application/json")
|
||||
.setBody(
|
||||
context.readAsset("json/top_items_failure.json")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun statusEnabled(context: Context) {
|
||||
server.enqueue(
|
||||
MockResponse()
|
||||
.setHeader("Content-Type", "application/json")
|
||||
.setBody(
|
||||
context.readAsset("json/summary_enabled.json")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun statusDisabled(context: Context, duration: Long? = null) {
|
||||
server.enqueue(
|
||||
MockResponse()
|
||||
.setHeader("Content-Type", "application/json")
|
||||
.setBody(
|
||||
context.readAsset("json/summary_disabled.json")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun enableSuccess(context: Context) {
|
||||
server.enqueue(
|
||||
MockResponse()
|
||||
.setHeader("Content-Type", "application/json")
|
||||
.setBody(
|
||||
context.readAsset("json/status_enabled.json")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun disableSuccess(context: Context) {
|
||||
server.enqueue(
|
||||
MockResponse()
|
||||
.setHeader("Content-Type", "application/json")
|
||||
.setBody(
|
||||
context.readAsset("json/status_disabled.json")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun disabledPermanently() {
|
||||
server.enqueue(
|
||||
MockResponse()
|
||||
.setHeader("Content-Type", "text/html")
|
||||
.setBody("<p>Definitely not a number</p>")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.readAsset(path: String) = assets.open(path)
|
||||
.bufferedReader()
|
||||
.use { it.readText() }
|
|
@ -0,0 +1,13 @@
|
|||
package com.wbrawner.pihelper.util
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.test.runner.AndroidJUnitRunner
|
||||
import dagger.hilt.android.testing.HiltTestApplication
|
||||
|
||||
@Suppress("unused")
|
||||
class HiltTestRunner : AndroidJUnitRunner() {
|
||||
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
|
||||
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package com.wbrawner.pihelper.util
|
||||
|
||||
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.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() }
|
||||
|
||||
class MainScreenRobot(private val testRule: ComposeTestRule) {
|
||||
val context: Context = InstrumentationRegistry.getInstrumentation().context
|
||||
|
||||
init {
|
||||
testRule.waitUntil {
|
||||
testRule
|
||||
.onAllNodesWithTag(MAIN_SCREEN_TAG)
|
||||
.fetchSemanticsNodes().size == 1
|
||||
}
|
||||
}
|
||||
|
||||
infix fun onSettingsScreen(actions: AuthScreenRobot.() -> Unit) = AuthScreenRobot(testRule)
|
||||
.run {
|
||||
actions()
|
||||
}
|
||||
|
||||
fun verifyStatus(status: String) {
|
||||
testRule.waitUntil {
|
||||
testRule.onAllNodes(hasTestTag(STATUS_TEXT_TAG).and(hasText(status)))
|
||||
.fetchSemanticsNodes().size == 1
|
||||
}
|
||||
}
|
||||
|
||||
fun clickEnableButton() = testRule.onNode(hasTestTag(ENABLE_BUTTON_TAG)).performClick()
|
||||
|
||||
fun clickDisablePermanentlyButton() = testRule.onNode(hasTestTag(DISABLE_PERMANENT_BUTTON_TAG))
|
||||
.performClick()
|
||||
|
||||
fun verifyErrorMessageIsDisplayed(message: String) {
|
||||
testRule.waitUntil(2_000) {
|
||||
testRule
|
||||
.onAllNodesWithText(message, substring = true)
|
||||
.fetchSemanticsNodes().size == 1
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
package com.wbrawner.pihelper.util
|
||||
|
|
@ -1,21 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.wbrawner.pihelper">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application
|
||||
android:name=".PiHelperApplication"
|
||||
android:allowBackup="true"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="@xml/backup_descriptor"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
android:theme="@style/Theme.App.Starting">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
|
|
@ -1,136 +0,0 @@
|
|||
package com.wbrawner.pihelper
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.core.content.edit
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.wbrawner.piholeclient.PiHoleApiService
|
||||
import com.wbrawner.piholeclient.VersionResponse
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.ConnectException
|
||||
import java.net.SocketTimeoutException
|
||||
|
||||
const val KEY_BASE_URL = "baseUrl"
|
||||
const val KEY_API_KEY = "apiKey"
|
||||
const val IP_MIN = 0
|
||||
const val IP_MAX = 255
|
||||
|
||||
class AddPiHelperViewModel(
|
||||
private val sharedPreferences: SharedPreferences,
|
||||
private val apiService: PiHoleApiService
|
||||
) : ViewModel() {
|
||||
|
||||
@Volatile
|
||||
var baseUrl: String? = sharedPreferences.getString(KEY_BASE_URL, null)
|
||||
set(value) {
|
||||
sharedPreferences.edit {
|
||||
putString(KEY_BASE_URL, value)
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
@Volatile
|
||||
var apiKey: String? = sharedPreferences.getString(KEY_API_KEY, null)
|
||||
set(value) {
|
||||
sharedPreferences.edit {
|
||||
putString(KEY_API_KEY, value)
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
init {
|
||||
apiService.baseUrl = this.baseUrl
|
||||
apiService.apiKey = this.apiKey
|
||||
}
|
||||
|
||||
val piHoleIpAddress = MutableLiveData<String?>()
|
||||
val scanningIp = MutableLiveData<String?>()
|
||||
val authenticated = MutableLiveData<Boolean>()
|
||||
|
||||
suspend fun beginScanning(deviceIpAddress: String) {
|
||||
val addressParts = deviceIpAddress.split(".").toMutableList()
|
||||
var chunks = 1
|
||||
// If the Pi-hole is correctly set up, then there should be a special host for it as
|
||||
// "pi.hole"
|
||||
val ipAddresses = mutableListOf("pi.hole")
|
||||
while (chunks <= IP_MAX) {
|
||||
val chunkSize = (IP_MAX - IP_MIN + 1) / chunks
|
||||
if (chunkSize == 1) {
|
||||
return
|
||||
}
|
||||
for (chunk in 0 until chunks) {
|
||||
val chunkStart = IP_MIN + (chunk * chunkSize)
|
||||
val chunkEnd = IP_MIN + ((chunk + 1) * chunkSize)
|
||||
addressParts[3] = (((chunkEnd - chunkStart) / 2) + chunkStart).toString()
|
||||
ipAddresses.add(addressParts.joinToString("."))
|
||||
}
|
||||
chunks *= 2
|
||||
}
|
||||
scan(ipAddresses)
|
||||
}
|
||||
|
||||
private suspend fun scan(ipAddresses: MutableList<String>) {
|
||||
if (ipAddresses.isEmpty()) {
|
||||
scanningIp.postValue(null)
|
||||
piHoleIpAddress.postValue(null)
|
||||
return
|
||||
}
|
||||
|
||||
val ipAddress = ipAddresses.removeAt(0)
|
||||
scanningIp.postValue(ipAddress)
|
||||
if (!connectToIpAddress(ipAddress)) {
|
||||
scan(ipAddresses)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun connectToIpAddress(ipAddress: String): Boolean {
|
||||
val version: VersionResponse? = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
apiService.baseUrl = ipAddress
|
||||
apiService.getVersion()
|
||||
} catch (ignored: ConnectException) {
|
||||
null
|
||||
} catch (ignored: SocketTimeoutException) {
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
Log.e("Pi-helper", "Failed to load Pi-Hole version at $ipAddress", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
return if (version == null) {
|
||||
false
|
||||
} else {
|
||||
piHoleIpAddress.postValue(ipAddress)
|
||||
baseUrl = ipAddress
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun authenticateWithPassword(password: String) {
|
||||
// The Pi-hole API key is just the web password hashed twice with SHA-256
|
||||
authenticateWithApiKey(password.hash().hash())
|
||||
}
|
||||
|
||||
suspend fun authenticateWithApiKey(apiKey: String) {
|
||||
// This uses the topItems endpoint to test that the API key is working since it requires
|
||||
// authentication and is fairly simple to determine whether or not the request was
|
||||
// successful
|
||||
apiService.apiKey = apiKey
|
||||
try {
|
||||
apiService.getTopItems()
|
||||
this.apiKey = apiKey
|
||||
authenticated.postValue(true)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Pi-helper", "Unable to authenticate with API key", e)
|
||||
authenticated.postValue(false)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
fun forgetPihole() {
|
||||
baseUrl = null
|
||||
apiKey = null
|
||||
}
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
package com.wbrawner.pihelper
|
||||
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.LinearInterpolator
|
||||
import android.view.animation.RotateAnimation
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.FragmentNavigatorExtras
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import kotlinx.android.synthetic.main.fragment_add_pi_hole.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.koin.android.ext.android.inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class AddPiHoleFragment : Fragment(), CoroutineScope {
|
||||
|
||||
override val coroutineContext: CoroutineContext = Dispatchers.Main
|
||||
private val viewModel: AddPiHelperViewModel by inject()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? = inflater.inflate(R.layout.fragment_add_pi_hole, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val navController = findNavController()
|
||||
scanNetworkButton.setOnClickListener {
|
||||
navController.navigate(
|
||||
R.id.action_addPiHoleFragment_to_scanNetworkFragment,
|
||||
null,
|
||||
null,
|
||||
FragmentNavigatorExtras(piHelperLogo to "piHelperLogo")
|
||||
)
|
||||
}
|
||||
ipAddress.setOnEditorActionListener { _, _, _ ->
|
||||
connectButton.performClick()
|
||||
}
|
||||
connectButton.setSuspendingOnClickListener(this) {
|
||||
showProgress(true)
|
||||
if (viewModel.connectToIpAddress(ipAddress.text.toString())) {
|
||||
navController.navigate(
|
||||
R.id.action_addPiHoleFragment_to_retrieveApiKeyFragment,
|
||||
null,
|
||||
null,
|
||||
FragmentNavigatorExtras(piHelperLogo to "piHelperLogo")
|
||||
)
|
||||
} else {
|
||||
AlertDialog.Builder(view.context)
|
||||
.setTitle(R.string.connection_failed_title)
|
||||
.setMessage(R.string.connection_failed)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
showProgress(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showProgress(show: Boolean) {
|
||||
if (show) {
|
||||
piHelperLogo.startAnimation(
|
||||
RotateAnimation(
|
||||
0f,
|
||||
360f,
|
||||
Animation.RELATIVE_TO_SELF,
|
||||
0.5f,
|
||||
Animation.RELATIVE_TO_SELF,
|
||||
0.5f
|
||||
).apply {
|
||||
duration =
|
||||
resources.getInteger(android.R.integer.config_longAnimTime).toLong() * 2
|
||||
repeatMode = Animation.RESTART
|
||||
repeatCount = Animation.INFINITE
|
||||
interpolator = LinearInterpolator()
|
||||
fillAfter = true
|
||||
}
|
||||
)
|
||||
} else {
|
||||
piHelperLogo.clearAnimation()
|
||||
}
|
||||
connectionForm.visibility = if (show) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
cancel()
|
||||
super.onDestroyView()
|
||||
}
|
||||
}
|
84
app/src/main/java/com/wbrawner/pihelper/AddScreen.kt
Normal file
84
app/src/main/java/com/wbrawner/pihelper/AddScreen.kt
Normal file
|
@ -0,0 +1,84 @@
|
|||
package com.wbrawner.pihelper
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.wbrawner.pihelper.shared.Action
|
||||
import com.wbrawner.pihelper.shared.Effect
|
||||
import com.wbrawner.pihelper.shared.Store
|
||||
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(
|
||||
"Android SDK built for x86",
|
||||
"sdk_gphone64_arm64"
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun AddScreen(store: Store) {
|
||||
val state by store.state.collectAsState()
|
||||
val effect by store.effects.collectAsState(initial = Effect.Empty)
|
||||
val context = LocalContext.current
|
||||
AddScreen(
|
||||
scanNetwork = scan@{
|
||||
// TODO: This needs to go in the Store
|
||||
if (BuildConfig.DEBUG && emulatorBuildModels.contains(Build.MODEL)) {
|
||||
// For emulators, just begin scanning the host machine directly
|
||||
store.dispatch(Action.Scan("10.0.2.2"))
|
||||
return@scan
|
||||
}
|
||||
(context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager)
|
||||
?.let { connectivityManager ->
|
||||
connectivityManager.allNetworks
|
||||
.filter {
|
||||
connectivityManager.getNetworkCapabilities(it)
|
||||
?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
|
||||
}
|
||||
.mapNotNull { network ->
|
||||
connectivityManager.getLinkProperties(network)
|
||||
?.linkAddresses
|
||||
?.filter {
|
||||
!it.address.isLoopbackAddress
|
||||
&& !it.address.isLinkLocalAddress
|
||||
&& it.address is Inet4Address
|
||||
}
|
||||
?.mapNotNull { it.address.hostAddress }
|
||||
?.forEach {
|
||||
store.dispatch(Action.Scan(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
?: Toast.makeText(context, "Failed to scan network", Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
connectToPihole = {
|
||||
store.dispatch(Action.Connect(it))
|
||||
},
|
||||
state.loading,
|
||||
error = effect as? Effect.Error
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@DayNightPreview
|
||||
fun AddScreen_Preview() {
|
||||
PihelperTheme {
|
||||
AddScreen({}, {}, error = Effect.Error("Something bad happened"))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@DayNightPreview
|
||||
fun OrDivider_Preview() {
|
||||
PihelperTheme {
|
||||
OrDivider()
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -1,24 +0,0 @@
|
|||
package com.wbrawner.pihelper
|
||||
|
||||
import android.view.View
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigInteger
|
||||
import java.security.MessageDigest
|
||||
|
||||
fun String.hash(): String = BigInteger(
|
||||
1,
|
||||
MessageDigest.getInstance("SHA-256").digest(this.toByteArray())
|
||||
).toString(16).padStart(64, '0')
|
||||
|
||||
fun CoroutineScope.cancel() {
|
||||
coroutineContext[Job]?.cancel()
|
||||
}
|
||||
|
||||
fun View.setSuspendingOnClickListener(
|
||||
coroutineScope: CoroutineScope,
|
||||
clickListener: suspend (v: View) -> Unit
|
||||
) = setOnClickListener { v ->
|
||||
coroutineScope.launch { clickListener(v) }
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
package com.wbrawner.pihelper
|
||||
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.wbrawner.pihelper.MainActivity.Companion.ACTION_FORGET_PIHOLE
|
||||
import kotlinx.android.synthetic.main.fragment_info.*
|
||||
import kotlinx.android.synthetic.main.fragment_main.toolbar
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.koin.android.ext.android.inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class InfoFragment : Fragment(), CoroutineScope {
|
||||
override val coroutineContext: CoroutineContext = Dispatchers.Main
|
||||
private val viewModel: AddPiHelperViewModel by inject()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? = inflater.inflate(R.layout.fragment_info, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
(activity as? AppCompatActivity)?.setSupportActionBar(toolbar)
|
||||
(activity as? AppCompatActivity)?.supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
(activity as? AppCompatActivity)?.supportActionBar?.setTitle(R.string.action_settings)
|
||||
val html = getString(R.string.content_info)
|
||||
@Suppress("DEPRECATION")
|
||||
infoContent?.text = if (Build.VERSION.SDK_INT < 24)
|
||||
Html.fromHtml(html)
|
||||
else
|
||||
Html.fromHtml(html, 0)
|
||||
infoContent.movementMethod = LinkMovementMethod.getInstance()
|
||||
forgetPiHoleButton?.setOnClickListener {
|
||||
AlertDialog.Builder(view.context)
|
||||
.setTitle(R.string.confirm_forget_pihole)
|
||||
.setMessage(R.string.warning_cannot_be_undone)
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.setPositiveButton(R.string.action_forget_pihole) { _, _ ->
|
||||
viewModel.forgetPihole()
|
||||
val refreshIntent = Intent(
|
||||
view.context.applicationContext,
|
||||
MainActivity::class.java
|
||||
).apply {
|
||||
action = ACTION_FORGET_PIHOLE
|
||||
addFlags(
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
and Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
)
|
||||
}
|
||||
activity?.startActivity(refreshIntent)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
findNavController().navigateUp()
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
|
@ -1,89 +1,140 @@
|
|||
package com.wbrawner.pihelper
|
||||
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.animation.ObjectAnimator
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import android.view.View
|
||||
import android.view.animation.AnticipateInterpolator
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.findNavController
|
||||
import com.wbrawner.pihelper.MainFragment.Companion.ACTION_DISABLE
|
||||
import com.wbrawner.pihelper.MainFragment.Companion.ACTION_ENABLE
|
||||
import com.wbrawner.pihelper.MainFragment.Companion.EXTRA_DURATION
|
||||
import org.koin.android.ext.android.inject
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
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.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
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private val addPiHoleViewModel: AddPiHelperViewModel by inject()
|
||||
private val navController: NavController by lazy {
|
||||
findNavController(R.id.content_main)
|
||||
}
|
||||
@Inject
|
||||
lateinit var store: Store
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
window.setBackgroundDrawable(ColorDrawable(getColor(R.color.colorSurface)))
|
||||
val analyticsBundle = Bundle()
|
||||
analyticsBundle.putString("intent_action", intent.action)
|
||||
val args = when (intent.action) {
|
||||
ACTION_ENABLE -> {
|
||||
if (addPiHoleViewModel.apiKey == null) {
|
||||
Toast.makeText(this, R.string.configure_pihelper, Toast.LENGTH_SHORT).show()
|
||||
null
|
||||
} else {
|
||||
Bundle().apply { putBoolean(ACTION_ENABLE, true) }
|
||||
}
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
BackHandler {
|
||||
store.dispatch(Action.Back)
|
||||
}
|
||||
ACTION_DISABLE -> {
|
||||
if (addPiHoleViewModel.apiKey == null) {
|
||||
Toast.makeText(this, R.string.configure_pihelper, Toast.LENGTH_SHORT).show()
|
||||
null
|
||||
} else {
|
||||
Bundle().apply {
|
||||
putBoolean(ACTION_DISABLE, true)
|
||||
putLong(EXTRA_DURATION, intent.getIntExtra(EXTRA_DURATION, 10).toLong())
|
||||
val launchIntent = remember { intent }
|
||||
LaunchedEffect(launchIntent) {
|
||||
ShortcutActions.fromIntentAction(launchIntent.action)?.let { action ->
|
||||
if (action == ShortcutActions.DISABLE) {
|
||||
val duration = launchIntent.getIntExtra(DURATION, 0)
|
||||
store.dispatch(Action.Disable(duration.toLong()))
|
||||
} else {
|
||||
store.dispatch(Action.Enable)
|
||||
}
|
||||
}
|
||||
}
|
||||
ACTION_FORGET_PIHOLE -> {
|
||||
if (intent.component?.packageName == packageName) {
|
||||
while (navController.popBackStack()) {
|
||||
// Do nothing, just pop all the items off the back stack
|
||||
val state by store.state.collectAsState()
|
||||
val navController = rememberNavController()
|
||||
LaunchedEffect(state.route) {
|
||||
navController.navigate(state.route.name)
|
||||
}
|
||||
val effect by store.effects.collectAsState(initial = Effect.Empty)
|
||||
LaunchedEffect(effect) {
|
||||
when (effect) {
|
||||
is Effect.Exit -> finish()
|
||||
else -> {
|
||||
// no-op
|
||||
}
|
||||
// Just return an empty bundle so that the navigation branch below will load
|
||||
// the correct screen
|
||||
Bundle()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
when {
|
||||
navController.currentDestination?.id != R.id.placeholder && args == null -> {
|
||||
return
|
||||
}
|
||||
addPiHoleViewModel.baseUrl.isNullOrBlank() -> {
|
||||
navController.navigate(R.id.addPiHoleFragment, args)
|
||||
}
|
||||
addPiHoleViewModel.apiKey.isNullOrBlank() -> {
|
||||
navController.navigate(R.id.addPiHoleFragment)
|
||||
navController.navigate(R.id.retrieveApiKeyFragment, args)
|
||||
}
|
||||
else -> {
|
||||
navController.navigate(R.id.mainFragment, args)
|
||||
PihelperTheme {
|
||||
NavHost(navController, startDestination = state.initialRoute.name) {
|
||||
composable(Route.CONNECT.name) {
|
||||
AddScreen(store)
|
||||
}
|
||||
composable(Route.SCAN.name) {
|
||||
ScanScreen(store)
|
||||
}
|
||||
composable(Route.AUTH.name) {
|
||||
AuthScreen(store)
|
||||
}
|
||||
composable(Route.HOME.name) {
|
||||
MainScreen(store)
|
||||
}
|
||||
composable(Route.ABOUT.name) {
|
||||
InfoScreen(store)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (!navController.navigateUp()) {
|
||||
finish()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
splashScreen.setOnExitAnimationListener { splashScreenView ->
|
||||
listOf(View.SCALE_X, View.SCALE_Y).forEach { axis ->
|
||||
ObjectAnimator.ofFloat(
|
||||
splashScreenView,
|
||||
axis,
|
||||
1f,
|
||||
0.45f
|
||||
).apply {
|
||||
interpolator = AnticipateInterpolator()
|
||||
duration = 200L
|
||||
doOnEnd {
|
||||
splashScreenView.remove()
|
||||
}
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (navController.currentDestination?.id == R.id.placeholder) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_FORGET_PIHOLE = "com.wbrawner.pihelper.ACTION_FORGET_PIHOLE"
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun LoadingSpinner_Preview() {
|
||||
LoadingSpinner()
|
||||
}
|
||||
|
||||
enum class ShortcutActions(val fullName: String) {
|
||||
ENABLE("com.wbrawner.pihelper.ShortcutActions.ENABLE"),
|
||||
DISABLE("com.wbrawner.pihelper.ShortcutActions.DISABLE");
|
||||
|
||||
companion object {
|
||||
fun fromIntentAction(action: String?): ShortcutActions? = when (action) {
|
||||
ENABLE.fullName -> ENABLE
|
||||
DISABLE.fullName -> DISABLE
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const val DURATION: String = "com.wbrawner.pihelper.MainActivityKt.DURATION"
|
||||
|
||||
@Composable
|
||||
@DayNightPreview
|
||||
fun InfoScreen_Preview() {
|
||||
PihelperTheme {
|
||||
InfoScreen({}, {})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,213 +0,0 @@
|
|||
package com.wbrawner.pihelper
|
||||
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableString
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.LinearInterpolator
|
||||
import android.view.animation.RotateAnimation
|
||||
import android.widget.EditText
|
||||
import android.widget.RadioGroup
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat.getColor
|
||||
import androidx.core.text.set
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.wbrawner.piholeclient.Status
|
||||
import kotlinx.android.synthetic.main.fragment_main.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class MainFragment : Fragment(), CoroutineScope {
|
||||
override val coroutineContext: CoroutineContext = Dispatchers.Main
|
||||
private val viewModel: PiHelperViewModel by inject()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
launch {
|
||||
if (arguments?.getBoolean(ACTION_ENABLE) == true) {
|
||||
viewModel.enablePiHole()
|
||||
} else if (arguments?.getBoolean(ACTION_DISABLE) == true) {
|
||||
viewModel.disablePiHole(arguments?.getLong(EXTRA_DURATION))
|
||||
}
|
||||
viewModel.monitorSummary()
|
||||
}
|
||||
viewModel.status.observe(this, Observer {
|
||||
showProgress(false)
|
||||
val (statusColor, statusText) = when (it) {
|
||||
Status.DISABLED -> {
|
||||
enableButton?.visibility = View.VISIBLE
|
||||
disableButtons?.visibility = View.GONE
|
||||
Pair(R.color.colorDisabled, R.string.status_disabled)
|
||||
}
|
||||
Status.ENABLED -> {
|
||||
enableButton?.visibility = View.GONE
|
||||
disableButtons?.visibility = View.VISIBLE
|
||||
Pair(R.color.colorEnabled, R.string.status_enabled)
|
||||
}
|
||||
else -> {
|
||||
enableButton?.visibility = View.GONE
|
||||
disableButtons?.visibility = View.GONE
|
||||
Pair(R.color.colorUnknown, R.string.status_unknown)
|
||||
}
|
||||
}
|
||||
status?.let { textView ->
|
||||
val status = getString(statusText)
|
||||
val statusLabel = getString(R.string.label_status, status)
|
||||
val start = statusLabel.indexOf(status)
|
||||
val end = start + status.length
|
||||
val statusSpan = SpannableString(statusLabel)
|
||||
statusSpan[start, end] = StyleSpan(Typeface.BOLD)
|
||||
statusSpan[start, end] =
|
||||
ForegroundColorSpan(getColor(textView.context, statusColor))
|
||||
textView.text = statusSpan
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? = inflater.inflate(R.layout.fragment_main, container, false)
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.main, menu)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
(activity as? AppCompatActivity)?.setSupportActionBar(toolbar)
|
||||
showProgress(true)
|
||||
enableButton?.setSuspendingOnClickListener(this) {
|
||||
showProgress(true)
|
||||
try {
|
||||
viewModel.enablePiHole()
|
||||
} catch (ignored: Exception) {
|
||||
Log.e("Pi-helper", "Failed to enable Pi-Hole", ignored)
|
||||
}
|
||||
}
|
||||
disable10SecondsButton?.setSuspendingOnClickListener(this) {
|
||||
showProgress(true)
|
||||
try {
|
||||
viewModel.disablePiHole(10)
|
||||
} catch (ignored: Exception) {
|
||||
Log.e("Pi-helper", "Failed to disable Pi-Hole", ignored)
|
||||
}
|
||||
}
|
||||
disable30SecondsButton?.setSuspendingOnClickListener(this) {
|
||||
showProgress(true)
|
||||
try {
|
||||
viewModel.disablePiHole(30)
|
||||
} catch (ignored: Exception) {
|
||||
Log.e("Pi-helper", "Failed to disable Pi-Hole", ignored)
|
||||
}
|
||||
}
|
||||
disable5MinutesButton?.setSuspendingOnClickListener(this) {
|
||||
showProgress(true)
|
||||
try {
|
||||
viewModel.disablePiHole(300)
|
||||
} catch (ignored: Exception) {
|
||||
Log.e("Pi-helper", "Failed to disable Pi-Hole", ignored)
|
||||
}
|
||||
}
|
||||
disableCustomTimeButton?.setOnClickListener {
|
||||
val dialogView = LayoutInflater.from(it.context)
|
||||
.inflate(R.layout.dialog_disable_custom_time, view as ViewGroup, false)
|
||||
AlertDialog.Builder(it.context)
|
||||
.setTitle(R.string.action_disable_custom)
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.setPositiveButton(R.string.action_disable, null)
|
||||
.setView(dialogView)
|
||||
.create()
|
||||
.apply {
|
||||
setOnShowListener {
|
||||
getButton(AlertDialog.BUTTON_POSITIVE).setSuspendingOnClickListener(this@MainFragment) {
|
||||
try {
|
||||
val rawTime = dialogView.findViewById<EditText>(R.id.time)
|
||||
.text
|
||||
.toString()
|
||||
.toLong()
|
||||
val checkedId =
|
||||
dialogView.findViewById<RadioGroup>(R.id.timeUnit)
|
||||
.checkedRadioButtonId
|
||||
val computedTime = if (checkedId == R.id.seconds) rawTime
|
||||
else rawTime * 60
|
||||
viewModel.disablePiHole(computedTime)
|
||||
dismiss()
|
||||
} catch (e: Exception) {
|
||||
dialogView.findViewById<EditText>(R.id.time)
|
||||
.error = "Failed to disable Pi-hole"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
disablePermanentlyButton?.setSuspendingOnClickListener(this) {
|
||||
showProgress(true)
|
||||
try {
|
||||
viewModel.disablePiHole()
|
||||
} catch (ignored: Exception) {
|
||||
Log.e("Pi-helper", "Failed to disable Pi-Hole", ignored)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.settings) {
|
||||
findNavController().navigate(R.id.action_mainFragment_to_settingsFragment)
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun showProgress(show: Boolean) {
|
||||
progressBar?.visibility = if (show) {
|
||||
progressBar?.startAnimation(RotateAnimation(
|
||||
0f,
|
||||
360f,
|
||||
Animation.RELATIVE_TO_SELF,
|
||||
0.5f,
|
||||
Animation.RELATIVE_TO_SELF,
|
||||
0.5f
|
||||
).apply {
|
||||
duration = resources.getInteger(android.R.integer.config_longAnimTime).toLong() * 2
|
||||
repeatMode = Animation.RESTART
|
||||
repeatCount = Animation.INFINITE
|
||||
interpolator = LinearInterpolator()
|
||||
fillAfter = true
|
||||
})
|
||||
View.VISIBLE
|
||||
} else {
|
||||
progressBar?.clearAnimation()
|
||||
View.GONE
|
||||
}
|
||||
statusContent?.visibility = if (show) {
|
||||
View.GONE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
coroutineContext[Job]?.cancel()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_DISABLE = "com.wbrawner.pihelper.MainFragment.ACTION_DISABLE"
|
||||
const val ACTION_ENABLE = "com.wbrawner.pihelper.MainFragment.ACTION_ENABLE"
|
||||
const val EXTRA_DURATION = "com.wbrawner.pihelper.MainFragment.EXTRA_DURATION"
|
||||
}
|
||||
}
|
|
@ -1,23 +1,7 @@
|
|||
package com.wbrawner.pihelper
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import com.wbrawner.piholeclient.piHoleClientModule
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.android.ext.koin.androidLogger
|
||||
import org.koin.core.context.startKoin
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
@Suppress("unused")
|
||||
class PiHelperApplication: Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
startKoin{
|
||||
androidLogger()
|
||||
androidContext(this@PiHelperApplication)
|
||||
modules(listOf(
|
||||
piHoleClientModule,
|
||||
piHelperModule
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@HiltAndroidApp
|
||||
class PiHelperApplication : Application()
|
|
@ -1,33 +1,38 @@
|
|||
package com.wbrawner.pihelper
|
||||
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import com.wbrawner.piholeclient.NAME_BASE_URL
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
import com.wbrawner.pihelper.shared.*
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
const val ENCRYPTED_SHARED_PREFS_FILE_NAME = "pihelper.prefs"
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object PiHelperModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesPiholeAPIService(): PiholeAPIService = PiholeAPIService.create()
|
||||
|
||||
val piHelperModule = module {
|
||||
single {
|
||||
EncryptedSharedPreferences.create(
|
||||
ENCRYPTED_SHARED_PREFS_FILE_NAME,
|
||||
"pihelper",
|
||||
get(),
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesAnalyticsHelper(): AnalyticsHelper = object : AnalyticsHelper {
|
||||
override fun pageView(route: Route) {
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
override fun event(
|
||||
event: AnalyticsEvent,
|
||||
route: Route
|
||||
) {
|
||||
// Not implemented
|
||||
}
|
||||
}
|
||||
|
||||
viewModel {
|
||||
AddPiHelperViewModel(get(), get())
|
||||
}
|
||||
|
||||
viewModel {
|
||||
PiHelperViewModel(get())
|
||||
}
|
||||
|
||||
single(named(NAME_BASE_URL)) {
|
||||
get<EncryptedSharedPreferences>().getString(KEY_BASE_URL, "")
|
||||
}
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesStore(
|
||||
apiService: PiholeAPIService,
|
||||
analyticsHelper: AnalyticsHelper
|
||||
): Store = Store(apiService, analyticsHelper)
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
package com.wbrawner.pihelper
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.wbrawner.piholeclient.PiHoleApiService
|
||||
import com.wbrawner.piholeclient.Status
|
||||
import com.wbrawner.piholeclient.StatusProvider
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
class PiHelperViewModel(
|
||||
private val apiService: PiHoleApiService
|
||||
) : ViewModel() {
|
||||
val status = MutableLiveData<Status>()
|
||||
private var action: (suspend () -> StatusProvider)? = null
|
||||
get() = field ?: defaultAction
|
||||
private var defaultAction = suspend {
|
||||
apiService.getSummary()
|
||||
}
|
||||
|
||||
suspend fun monitorSummary() {
|
||||
while (coroutineContext.isActive) {
|
||||
try {
|
||||
status.postValue(action!!.invoke().status)
|
||||
action = null
|
||||
} catch (ignored: Exception) {
|
||||
break
|
||||
}
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun enablePiHole() {
|
||||
action = {
|
||||
apiService.enable()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun disablePiHole(duration: Long? = null) {
|
||||
action = {
|
||||
apiService.disable(duration)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
package com.wbrawner.pihelper
|
||||
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.LinearInterpolator
|
||||
import android.view.animation.RotateAnimation
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.transition.TransitionInflater
|
||||
import kotlinx.android.synthetic.main.fragment_retrieve_api_key.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.koin.android.ext.android.inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class RetrieveApiKeyFragment : Fragment(), CoroutineScope {
|
||||
override val coroutineContext: CoroutineContext = Dispatchers.Main
|
||||
private val viewModel: AddPiHelperViewModel by inject()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
sharedElementEnterTransition = TransitionInflater.from(context)
|
||||
.inflateTransition(android.R.transition.move)
|
||||
viewModel.authenticated.observe(this, Observer {
|
||||
if (!it) return@Observer
|
||||
findNavController().navigate(R.id.action_retrieveApiKeyFragment_to_mainFragment)
|
||||
})
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? = inflater.inflate(R.layout.fragment_retrieve_api_key, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
password.setOnEditorActionListener { _, _, _ ->
|
||||
connectWithPasswordButton.performClick()
|
||||
}
|
||||
connectWithPasswordButton.setSuspendingOnClickListener(this) {
|
||||
showProgress(true)
|
||||
try {
|
||||
viewModel.authenticateWithPassword(password.text.toString())
|
||||
} catch (ignored: Exception) {
|
||||
Log.e("Pi-helper", "Failed to authenticate with password", ignored)
|
||||
password.error = "Failed to authenticate with given password. Please verify " +
|
||||
"you've entered it correctly and try again."
|
||||
showProgress(false)
|
||||
}
|
||||
}
|
||||
apiKey.setOnEditorActionListener { _, _, _ ->
|
||||
connectWithApiKeyButton.performClick()
|
||||
}
|
||||
connectWithApiKeyButton.setSuspendingOnClickListener(this) {
|
||||
showProgress(true)
|
||||
try {
|
||||
viewModel.authenticateWithApiKey(apiKey.text.toString())
|
||||
} catch (ignored: Exception) {
|
||||
apiKey.error = "Failed to authenticate with given API key. Please verify " +
|
||||
"you've entered it correctly and try again."
|
||||
showProgress(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showProgress(show: Boolean) {
|
||||
if (show) {
|
||||
piHelperLogo.startAnimation(
|
||||
RotateAnimation(
|
||||
0f,
|
||||
360f,
|
||||
Animation.RELATIVE_TO_SELF,
|
||||
0.5f,
|
||||
Animation.RELATIVE_TO_SELF,
|
||||
0.5f
|
||||
).apply {
|
||||
duration =
|
||||
resources.getInteger(android.R.integer.config_longAnimTime).toLong() * 2
|
||||
repeatMode = Animation.RESTART
|
||||
repeatCount = Animation.INFINITE
|
||||
interpolator = LinearInterpolator()
|
||||
fillAfter = true
|
||||
}
|
||||
)
|
||||
} else {
|
||||
piHelperLogo.clearAnimation()
|
||||
}
|
||||
authenticationForm.visibility = if (show) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
cancel()
|
||||
super.onDestroyView()
|
||||
}
|
||||
}
|
|
@ -1,168 +0,0 @@
|
|||
package com.wbrawner.pihelper
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.LinearInterpolator
|
||||
import android.view.animation.RotateAnimation
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.fragment.FragmentNavigatorExtras
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.transition.Transition
|
||||
import androidx.transition.TransitionInflater
|
||||
import kotlinx.android.synthetic.main.fragment_scan_network.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import java.net.Inet4Address
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class ScanNetworkFragment : Fragment(), CoroutineScope {
|
||||
|
||||
override val coroutineContext: CoroutineContext = Dispatchers.Main
|
||||
private val viewModel: AddPiHelperViewModel by inject()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
sharedElementEnterTransition = TransitionInflater.from(context)
|
||||
.inflateTransition(android.R.transition.move)
|
||||
.addListener(object : Transition.TransitionListener {
|
||||
override fun onTransitionEnd(transition: Transition) {
|
||||
animatePiHelperLogo()
|
||||
}
|
||||
|
||||
override fun onTransitionResume(transition: Transition) {
|
||||
}
|
||||
|
||||
override fun onTransitionPause(transition: Transition) {
|
||||
}
|
||||
|
||||
override fun onTransitionCancel(transition: Transition) {
|
||||
}
|
||||
|
||||
override fun onTransitionStart(transition: Transition) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? = inflater.inflate(R.layout.fragment_scan_network, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel.scanningIp.observe(viewLifecycleOwner, Observer {
|
||||
ipAddress?.text = it
|
||||
})
|
||||
viewModel.piHoleIpAddress.observe(viewLifecycleOwner, Observer { ipAddress ->
|
||||
if (ipAddress == null) {
|
||||
AlertDialog.Builder(view.context)
|
||||
.setTitle(R.string.scan_failed_title)
|
||||
.setMessage(R.string.scan_failed)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
.show()
|
||||
return@Observer
|
||||
}
|
||||
piHelperLogo?.animation?.let {
|
||||
it.setAnimationListener(object : Animation.AnimationListener {
|
||||
override fun onAnimationRepeat(animation: Animation?) {
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animation?) {
|
||||
navigateToApiKeyScreen()
|
||||
}
|
||||
|
||||
override fun onAnimationStart(animation: Animation?) {
|
||||
}
|
||||
})
|
||||
it.repeatCount = 0
|
||||
} ?: navigateToApiKeyScreen()
|
||||
})
|
||||
launch(Dispatchers.IO) {
|
||||
if (BuildConfig.DEBUG && Build.MODEL == "Android SDK built for x86") {
|
||||
// For emulators, just begin scanning the host machine directly
|
||||
viewModel.beginScanning("10.0.2.2")
|
||||
return@launch
|
||||
}
|
||||
(view.context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager)?.let { connectivityManager ->
|
||||
connectivityManager.allNetworks
|
||||
.filter {
|
||||
connectivityManager.getNetworkCapabilities(it)
|
||||
?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
?: false
|
||||
}
|
||||
.forEach { network ->
|
||||
connectivityManager.getLinkProperties(network)
|
||||
?.linkAddresses
|
||||
?.filter { !it.address.isLoopbackAddress && it.address is Inet4Address }
|
||||
?.forEach { address ->
|
||||
Log.d(
|
||||
"Pi-helper",
|
||||
"Found link address: ${address.address.hostName}"
|
||||
)
|
||||
viewModel.beginScanning(address.address.hostAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
launch {
|
||||
delay(500)
|
||||
if (piHelperLogo?.animation == null) {
|
||||
animatePiHelperLogo()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToApiKeyScreen() {
|
||||
val extras = FragmentNavigatorExtras(
|
||||
piHelperLogo to "piHelperLogo"
|
||||
)
|
||||
|
||||
findNavController().navigate(
|
||||
R.id.action_scanNetworkFragment_to_retrieveApiKeyFragment,
|
||||
null,
|
||||
null,
|
||||
extras
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
piHelperLogo.clearAnimation()
|
||||
cancel()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun animatePiHelperLogo() {
|
||||
piHelperLogo?.startAnimation(
|
||||
RotateAnimation(
|
||||
0f,
|
||||
360f,
|
||||
Animation.RELATIVE_TO_SELF,
|
||||
0.5f,
|
||||
Animation.RELATIVE_TO_SELF,
|
||||
0.5f
|
||||
).apply {
|
||||
duration = resources.getInteger(android.R.integer.config_longAnimTime).toLong() * 2
|
||||
repeatMode = Animation.RESTART
|
||||
repeatCount = Animation.INFINITE
|
||||
interpolator = LinearInterpolator()
|
||||
fillAfter = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
62
app/src/main/java/com/wbrawner/pihelper/ScanScreen.kt
Normal file
62
app/src/main/java/com/wbrawner/pihelper/ScanScreen.kt
Normal file
|
@ -0,0 +1,62 @@
|
|||
package com.wbrawner.pihelper
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.shared.ui.component.LoadingSpinner
|
||||
import com.wbrawner.pihelper.shared.ui.theme.PihelperTheme
|
||||
|
||||
@Composable
|
||||
fun ScanScreen(store: Store) {
|
||||
val state by store.state.collectAsState()
|
||||
ScanningStatus(state.scanning?.let { "Scanning $it..." })
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScanningStatus(
|
||||
loadingMessage: String? = null
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
|
||||
) {
|
||||
LoadingSpinner(loadingMessage != null)
|
||||
loadingMessage?.let {
|
||||
Text(
|
||||
text = loadingMessage,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun ScanningStatus_Preview() {
|
||||
PihelperTheme(false) {
|
||||
ScanningStatus(loadingMessage = "Scanning 127.0.0.1")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun ScanningStatus_DarkPreview() {
|
||||
PihelperTheme(true) {
|
||||
ScanningStatus(loadingMessage = "Scanning 127.0.0.1")
|
||||
}
|
||||
}
|
10
app/src/main/res/drawable-v26/ic_shortcut_enable.xml
Normal file
10
app/src/main/res/drawable-v26/ic_shortcut_enable.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground>
|
||||
<inset
|
||||
android:drawable="@drawable/ic_play_arrow_green"
|
||||
android:inset="15%" />
|
||||
</foreground>
|
||||
<background android:drawable="@color/colorSurface" />
|
||||
<monochrome android:drawable="@drawable/ic_play_arrow" />
|
||||
</adaptive-icon>
|
10
app/src/main/res/drawable-v26/ic_shortcut_pause.xml
Normal file
10
app/src/main/res/drawable-v26/ic_shortcut_pause.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground>
|
||||
<inset
|
||||
android:drawable="@drawable/ic_pause_red"
|
||||
android:inset="20%" />
|
||||
</foreground>
|
||||
<background android:drawable="@color/colorSurface" />
|
||||
<monochrome android:drawable="@drawable/ic_pause" />
|
||||
</adaptive-icon>
|
|
@ -1,11 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:width="500dp"
|
||||
android:height="1dp"
|
||||
android:gravity="center">
|
||||
<color android:color="@color/colorOnSurface" />
|
||||
<shape android:shape="rectangle" />
|
||||
</item>
|
||||
|
||||
</layer-list>
|
|
@ -4,6 +4,6 @@
|
|||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@color/colorRedDark"
|
||||
android:fillColor="@color/colorWhite"
|
||||
android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
|
||||
</vector>
|
||||
|
|
9
app/src/main/res/drawable/ic_pause_red.xml
Normal file
9
app/src/main/res/drawable/ic_pause_red.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@color/colorRedDark"
|
||||
android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z" />
|
||||
</vector>
|
|
@ -4,6 +4,6 @@
|
|||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@color/colorGreenDark"
|
||||
android:fillColor="@color/colorWhite"
|
||||
android:pathData="M8,5v14l11,-7z"/>
|
||||
</vector>
|
||||
|
|
9
app/src/main/res/drawable/ic_play_arrow_green.xml
Normal file
9
app/src/main/res/drawable/ic_play_arrow_green.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@color/colorGreenDark"
|
||||
android:pathData="M8,5v14l11,-7z" />
|
||||
</vector>
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/colorOnSurface"
|
||||
android:pathData="M19.1,12.9a2.8,2.8 0,0 0,0.1 -0.9,2.8 2.8,0 0,0 -0.1,-0.9l2.1,-1.6a0.7,0.7 0,0 0,0.1 -0.6L19.4,5.5a0.7,0.7 0,0 0,-0.6 -0.2l-2.4,1a6.5,6.5 0,0 0,-1.6 -0.9l-0.4,-2.6a0.5,0.5 0,0 0,-0.5 -0.4H10.1a0.5,0.5 0,0 0,-0.5 0.4L9.3,5.4a5.6,5.6 0,0 0,-1.7 0.9l-2.4,-1a0.4,0.4 0,0 0,-0.5 0.2l-2,3.4c-0.1,0.2 0,0.4 0.2,0.6l2,1.6a2.8,2.8 0,0 0,-0.1 0.9,2.8 2.8,0 0,0 0.1,0.9L2.8,14.5a0.7,0.7 0,0 0,-0.1 0.6l1.9,3.4a0.7,0.7 0,0 0,0.6 0.2l2.4,-1a6.5,6.5 0,0 0,1.6 0.9l0.4,2.6a0.5,0.5 0,0 0,0.5 0.4h3.8a0.5,0.5 0,0 0,0.5 -0.4l0.3,-2.6a5.6,5.6 0,0 0,1.7 -0.9l2.4,1a0.4,0.4 0,0 0,0.5 -0.2l2,-3.4c0.1,-0.2 0,-0.4 -0.2,-0.6ZM12,15.6A3.6,3.6 0,1 1,15.6 12,3.6 3.6,0 0,1 12,15.6Z"/>
|
||||
</vector>
|
|
@ -2,8 +2,8 @@
|
|||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape
|
||||
android:shape="oval"
|
||||
android:tint="@color/colorWhite" />
|
||||
android:shape="rectangle"
|
||||
android:tint="@color/colorSurface" />
|
||||
</item>
|
||||
<item
|
||||
android:bottom="0dp"
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape
|
||||
android:shape="oval"
|
||||
android:tint="@color/colorWhite" />
|
||||
android:shape="rectangle"
|
||||
android:tint="@color/colorSurface" />
|
||||
</item>
|
||||
<item
|
||||
android:bottom="0dp"
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:width="1dp"
|
||||
android:height="500dp"
|
||||
android:gravity="center">
|
||||
<color android:color="@color/colorOnSurface" />
|
||||
<shape android:shape="rectangle" />
|
||||
</item>
|
||||
|
||||
</layer-list>
|
|
@ -1,118 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="true"
|
||||
android:fillViewport="true"
|
||||
tools:background="@color/colorSurface"
|
||||
tools:context=".AddPiHoleFragment">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:padding="16dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/piHelperLogo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:src="@drawable/ic_app_logo"
|
||||
android:tint="@color/colorOnSurface"
|
||||
android:transitionName="piHelperLogo"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/connectionForm"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/piHelperLogo">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/scanNetwork"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/info_scan_network"
|
||||
android:textAlignment="center"
|
||||
app:layout_constraintBottom_toTopOf="@+id/scanNetworkButton"
|
||||
app:layout_constraintEnd_toStartOf="@+id/orDivider"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/scanNetworkButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/action_scan_network"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/orDivider"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/scanNetwork" />
|
||||
|
||||
<include
|
||||
android:id="@+id/orDivider"
|
||||
layout="@layout/or_divider"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/connectDirectly"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/info_connect"
|
||||
android:textAlignment="center"
|
||||
app:layout_constraintBottom_toTopOf="@+id/scanNetworkButton"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/orDivider"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/ipAddressContainer"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/prompt_ip_address"
|
||||
app:layout_constraintBottom_toTopOf="@+id/connectButton"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/orDivider"
|
||||
app:layout_constraintTop_toBottomOf="@+id/connectDirectly"
|
||||
app:layout_constraintVertical_chainStyle="packed">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/ipAddress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionGo"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:text="pi.hole"
|
||||
tools:ignore="HardcodedText" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/connectButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/action_connect_pihole"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/orDivider"
|
||||
app:layout_constraintTop_toBottomOf="@+id/ipAddressContainer" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
|
@ -1,113 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="true"
|
||||
tools:background="@color/colorSurface">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/statusContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar"
|
||||
tools:visibility="visible">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp"
|
||||
tools:context=".MainFragment">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/status"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="4dp"
|
||||
android:textAlignment="center"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/actionButtons"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Enabled"
|
||||
tools:textColor="@color/colorGreenDark" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/actionButtons"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/status"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/enableButton"
|
||||
style="@style/AppTheme.Button.Green"
|
||||
android:text="@string/action_enable" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/disableButtons"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/disable10SecondsButton"
|
||||
style="@style/AppTheme.Button.Red"
|
||||
android:text="@string/action_disable_10_seconds" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/disable30SecondsButton"
|
||||
style="@style/AppTheme.Button.Red"
|
||||
android:text="@string/action_disable_30_seconds" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/disable5MinutesButton"
|
||||
style="@style/AppTheme.Button.Red"
|
||||
android:text="@string/action_disable_5_minutes" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/disableCustomTimeButton"
|
||||
style="@style/AppTheme.Button.Red"
|
||||
android:text="@string/action_disable_custom" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/disablePermanentlyButton"
|
||||
style="@style/AppTheme.Button.Red"
|
||||
android:text="@string/action_disable_permanently" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:src="@drawable/ic_app_logo"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar"
|
||||
tools:visibility="gone" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,141 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="true"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true"
|
||||
android:padding="16dp"
|
||||
tools:background="@color/colorSurface"
|
||||
tools:context=".RetrieveApiKeyFragment">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="400dp"
|
||||
android:layout_gravity="center">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/piHelperLogo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/accessibility_description_pi_helper_logo"
|
||||
android:src="@drawable/ic_app_logo"
|
||||
android:tint="@color/colorOnSurface"
|
||||
android:transitionName="piHelperLogo"
|
||||
app:layout_constraintBottom_toTopOf="@+id/authenticationForm"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/authenticationForm"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintHeight_min="wrap"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/piHelperLogo">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/connectionSuccess"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/info_connection_success"
|
||||
android:textAlignment="center"
|
||||
app:layout_constraintBottom_toTopOf="@+id/authRequired"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/authRequired"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/info_authentication_required"
|
||||
android:textAlignment="center"
|
||||
app:layout_constraintBottom_toTopOf="@+id/orDivider"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/connectionSuccess" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/passwordContainer"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/prompt_password"
|
||||
app:layout_constraintBottom_toTopOf="@+id/connectWithPasswordButton"
|
||||
app:layout_constraintEnd_toStartOf="@+id/orDivider"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/orDivider"
|
||||
app:layout_constraintVertical_chainStyle="packed">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionGo"
|
||||
android:inputType="textPassword" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/connectWithPasswordButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/action_authenticate_password"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/orDivider"
|
||||
app:layout_constraintEnd_toStartOf="@+id/orDivider"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/passwordContainer" />
|
||||
|
||||
<include
|
||||
android:id="@+id/orDivider"
|
||||
layout="@layout/or_divider"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_min="200dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/authRequired" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/apiKeyContainer"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/prompt_api_key"
|
||||
app:layout_constraintBottom_toTopOf="@+id/connectWithApiKeyButton"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/orDivider"
|
||||
app:layout_constraintTop_toTopOf="@+id/orDivider"
|
||||
app:layout_constraintVertical_chainStyle="packed">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/apiKey"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionGo"
|
||||
android:inputType="textPassword" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/connectWithApiKeyButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/action_authenticate_api_key"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/orDivider"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/orDivider"
|
||||
app:layout_constraintTop_toBottomOf="@+id/apiKeyContainer" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
|
@ -1,36 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:background="@color/colorSurface">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/vertical_rule"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="16dp"
|
||||
android:text="@string/or"
|
||||
android:textAlignment="center"
|
||||
android:textAllCaps="true" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/vertical_rule"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
</LinearLayout>
|
|
@ -1,17 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:background="@color/colorSurface">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/content_main"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
app:defaultNavHost="true"
|
||||
app:navGraph="@navigation/nav_graph" />
|
||||
</merge>
|
|
@ -1,44 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
tools:background="@color/colorSurface">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/hint_disable_duration">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/time"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number|numberSigned" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/timeUnit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/seconds"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="true"
|
||||
android:text="@string/duration_seconds" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/minutes"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/duration_minutes" />
|
||||
</RadioGroup>
|
||||
</LinearLayout>
|
|
@ -1,78 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="true"
|
||||
tools:background="@color/colorSurface"
|
||||
tools:context=".AddPiHoleFragment">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/piHelperLogo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:src="@drawable/ic_app_logo"
|
||||
android:tint="@color/colorOnSurface"
|
||||
android:transitionName="piHelperLogo" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/connectionForm"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/info_scan_network"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/scanNetworkButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/action_scan_network" />
|
||||
|
||||
<include layout="@layout/or_divider" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/info_connect"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/prompt_ip_address">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/ipAddress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionGo"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:text="pi.hole"
|
||||
tools:ignore="HardcodedText" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/connectButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/action_connect_pihole" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
|
@ -1,53 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:background="@color/colorSurface"
|
||||
tools:context=".InfoFragment">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:src="@drawable/ic_app_logo" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/infoContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/content_info"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/forgetPiHoleButton"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/action_forget_pihole" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,96 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="true"
|
||||
tools:background="@color/colorSurface">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/statusContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
tools:context=".MainFragment">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/status"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="4dp"
|
||||
android:textAlignment="center"
|
||||
tools:text="Enabled"
|
||||
tools:textColor="@color/colorGreenDark" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/enableButton"
|
||||
style="@style/AppTheme.Button.Green"
|
||||
android:text="@string/action_enable" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/disableButtons"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/disable10SecondsButton"
|
||||
style="@style/AppTheme.Button.Red"
|
||||
android:text="@string/action_disable_10_seconds" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/disable30SecondsButton"
|
||||
style="@style/AppTheme.Button.Red"
|
||||
android:text="@string/action_disable_30_seconds" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/disable5MinutesButton"
|
||||
style="@style/AppTheme.Button.Red"
|
||||
android:text="@string/action_disable_5_minutes" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/disableCustomTimeButton"
|
||||
style="@style/AppTheme.Button.Red"
|
||||
android:text="@string/action_disable_custom" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/disablePermanentlyButton"
|
||||
style="@style/AppTheme.Button.Red"
|
||||
android:text="@string/action_disable_permanently" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:src="@drawable/ic_app_logo"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,102 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="true"
|
||||
android:fillViewport="true"
|
||||
tools:background="@color/colorSurface"
|
||||
tools:context=".RetrieveApiKeyFragment">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/piHelperLogo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:contentDescription="@string/accessibility_description_pi_helper_logo"
|
||||
android:src="@drawable/ic_app_logo"
|
||||
android:tint="@color/colorOnSurface"
|
||||
android:transitionName="piHelperLogo"
|
||||
app:layout_constraintBottom_toTopOf="@+id/authenticationForm"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/authenticationForm"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/piHelperLogo">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/info_connection_success"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:text="@string/info_authentication_required"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/prompt_password">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionGo"
|
||||
android:inputType="textPassword" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/connectWithPasswordButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/action_authenticate_password" />
|
||||
|
||||
<include layout="@layout/or_divider" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/prompt_api_key">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/apiKey"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionGo"
|
||||
android:inputType="textPassword" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/connectWithApiKeyButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/action_authenticate_api_key" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
|
@ -1,35 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
tools:background="@color/colorSurface"
|
||||
tools:context=".ScanNetworkFragment">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/piHelperLogo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_app_logo"
|
||||
android:tint="@color/colorOnSurface"
|
||||
android:transitionName="piHelperLogo"
|
||||
android:contentDescription="@string/accessibility_description_pi_helper_logo" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ipAddressScanningInfo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/scanning_ip_address"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ipAddress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center" />
|
||||
|
||||
</LinearLayout>
|
|
@ -1,36 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
tools:background="@color/colorSurface">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/horizontal_rule"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="16dp"
|
||||
android:text="@string/or"
|
||||
android:textAlignment="center"
|
||||
android:textAllCaps="true" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/horizontal_rule"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
</LinearLayout>
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/settings"
|
||||
android:icon="@drawable/ic_settings"
|
||||
android:title="@string/action_settings"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
|
@ -2,4 +2,5 @@
|
|||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
|
@ -2,4 +2,5 @@
|
|||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
|
@ -1,78 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/nav_graph"
|
||||
app:startDestination="@id/placeholder">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/addPiHoleFragment"
|
||||
android:name="com.wbrawner.pihelper.AddPiHoleFragment"
|
||||
android:label="fragment_add_pi_hole"
|
||||
tools:layout="@layout/fragment_add_pi_hole" >
|
||||
<action
|
||||
android:id="@+id/action_addPiHoleFragment_to_scanNetworkFragment"
|
||||
app:destination="@id/scanNetworkFragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_addPiHoleFragment_to_retrieveApiKeyFragment"
|
||||
app:destination="@id/retrieveApiKeyFragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim"
|
||||
app:popUpTo="@+id/addPiHoleFragment" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/placeholder"
|
||||
android:name="androidx.fragment.app.Fragment" />
|
||||
<fragment
|
||||
android:id="@+id/scanNetworkFragment"
|
||||
android:name="com.wbrawner.pihelper.ScanNetworkFragment"
|
||||
android:label="fragment_scan_network"
|
||||
tools:layout="@layout/fragment_scan_network" >
|
||||
<action
|
||||
android:id="@+id/action_scanNetworkFragment_to_retrieveApiKeyFragment"
|
||||
app:destination="@id/retrieveApiKeyFragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim"
|
||||
app:popUpTo="@+id/addPiHoleFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/retrieveApiKeyFragment"
|
||||
android:name="com.wbrawner.pihelper.RetrieveApiKeyFragment"
|
||||
android:label="fragment_retrieve_api_key"
|
||||
tools:layout="@layout/fragment_retrieve_api_key" >
|
||||
<action
|
||||
android:id="@+id/action_retrieveApiKeyFragment_to_mainFragment"
|
||||
app:destination="@id/mainFragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/mainFragment"
|
||||
android:name="com.wbrawner.pihelper.MainFragment"
|
||||
android:label="@string/app_name"
|
||||
tools:layout="@layout/fragment_main" >
|
||||
<action
|
||||
android:id="@+id/action_mainFragment_to_settingsFragment"
|
||||
app:destination="@id/settingsFragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/settingsFragment"
|
||||
android:name="com.wbrawner.pihelper.InfoFragment"
|
||||
android:label="@string/action_settings"
|
||||
tools:layout="@layout/fragment_info" />
|
||||
</navigation>
|
|
@ -1,8 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorSurface">#333333</color>
|
||||
<color name="colorSurface">#000000</color>
|
||||
<color name="colorOnSurface">#f1f1f1</color>
|
||||
<color name="colorEnabled">@color/colorGreenLight</color>
|
||||
<color name="colorDisabled">@color/colorRedLight</color>
|
||||
<color name="colorButtonSecondary">#999999</color>
|
||||
</resources>
|
|
@ -1,13 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="AppTheme" parent="BaseTheme">
|
||||
<item name="android:statusBarColor">@color/colorTransparent</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.Button.Red" parent="Widget.MaterialComponents.Button">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="backgroundTint">@color/colorRedLight</item>
|
||||
<item name="android:textColor">@color/colorWhite</item>
|
||||
</style>
|
||||
</resources>
|
|
@ -1,18 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">@color/colorRedLight</color>
|
||||
<color name="colorPrimary">@color/colorAccent</color>
|
||||
<color name="colorPrimaryDark">@color/colorRedDark</color>
|
||||
<color name="colorAccent">@color/colorRedLight</color>
|
||||
<color name="colorRedLight">#f60d1a</color>
|
||||
<color name="colorRedDark">#96060c</color>
|
||||
<color name="colorGreenLight">#30d158</color>
|
||||
<color name="colorGreenDark">#34c759</color>
|
||||
<color name="colorRedLight">#F44336</color>
|
||||
<color name="colorRedDark">#B71C1C</color>
|
||||
<color name="colorGreenLight">#4CAF50</color>
|
||||
<color name="colorGreenDark">#1B5E20</color>
|
||||
<color name="colorShortcutBackground">#FFFFFFFF</color>
|
||||
<color name="colorOnSurface">#000000</color>
|
||||
<color name="colorWhite">#ffffff</color>
|
||||
<color name="colorEnabled">@color/colorGreenDark</color>
|
||||
<color name="colorUnknown">@color/colorButtonSecondary</color>
|
||||
<color name="colorDisabled">@color/colorRedDark</color>
|
||||
<color name="colorWhite">#FFFBFE</color>
|
||||
<color name="colorSurface">@color/colorWhite</color>
|
||||
<color name="colorTransparent">#00000000</color>
|
||||
<color name="colorButtonSecondary">#666666</color>
|
||||
</resources>
|
|
@ -1,49 +1,10 @@
|
|||
<resources>
|
||||
<string name="app_name">Pi-helper</string>
|
||||
<string name="scanning_ip_address">Scanning IP Address:</string>
|
||||
<string name="accessibility_description_pi_helper_logo">Pi-helper Logo</string>
|
||||
<string name="label_status">Status: %1$s</string>
|
||||
<string name="action_enable">Enable</string>
|
||||
<string name="action_disable">Disable</string>
|
||||
<string name="action_disable_10_seconds">Disable for 10 seconds</string>
|
||||
<string name="action_disable_10_seconds_short">Disable for 10 seconds</string>
|
||||
<string name="action_disable_30_seconds">Disable for 30 seconds</string>
|
||||
<string name="action_disable_30_seconds_short">Disable for 30 seconds</string>
|
||||
<string name="action_disable_5_minutes">Disable for 5 minutes</string>
|
||||
<string name="action_disable_5_minutes_short">Disable for 5 minutes</string>
|
||||
<string name="action_disable_custom">Disable for custom time</string>
|
||||
<string name="action_disable_permanently">Disable Permanently</string>
|
||||
<string name="status_disabled">Disabled</string>
|
||||
<string name="status_enabled">Enabled</string>
|
||||
<string name="scan_failed">Please ensure you are connected to the same Wi-Fi network that the Pi-Hole is running on and try again, or enter the Pi-Hole\'s IP address manually.</string>
|
||||
<string name="scan_failed_title">Pi-helper failed to find your Pi-Hole</string>
|
||||
<string name="connection_failed">Please ensure you are connected to the same Wi-Fi network that the Pi-Hole is running on, and that you\'re using the correct IP address and try again.</string>
|
||||
<string name="connection_failed_title">Pi-helper failed to connect to your Pi-Hole</string>
|
||||
<string name="configure_pihelper">Please configure Pi-helper before using shortcuts</string>
|
||||
<string name="or">or</string>
|
||||
<string name="action_settings">Settings</string>
|
||||
<string name="action_forget_pihole">Forget Pi-hole</string>
|
||||
<string name="content_info"><![CDATA[Pi-helper was made with ❤ by <a href=\"https://wbrawner.com\">William Brawner</a>. You can find the source code or report issues on the <a href=\"https://github.com/wbrawner/PiHelperAndroid\">GitHub page</a> for the project.]]></string>
|
||||
<string name="confirm_forget_pihole">Are you sure you want to forget your Pi-hole?</string>
|
||||
<string name="warning_cannot_be_undone">This cannot be undone.</string>
|
||||
<string name="title_crash_notification">Pi-helper Crashed!</string>
|
||||
<string name="text_crash_notification">Would you please consider sending the crash report to me?</string>
|
||||
<string name="channel_crash_notification">Crash Reports</string>
|
||||
<string name="status_unknown">Unknown</string>
|
||||
<string name="duration_seconds">Secs</string>
|
||||
<string name="duration_minutes">Mins</string>
|
||||
<string name="hint_disable_duration">Time to disable</string>
|
||||
<string name="info_scan_network">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.</string>
|
||||
<string name="action_scan_network">Scan Network</string>
|
||||
<string name="info_connect">If you already know the IP address or host of your Pi-Hole, you can also enter it below:</string>
|
||||
<string name="prompt_ip_address">Pi-Hole IP Address/Host</string>
|
||||
<string name="action_connect_pihole">Connect to Pi-Hole</string>
|
||||
<string name="info_connection_success">Pi-helper has successfully connected to your Pi-Hole!</string>
|
||||
<string name="info_authentication_required">You\'ll need to authenticate in order to enable and disable the Pi-hole.</string>
|
||||
<string name="prompt_password">Pi-Hole Web Password</string>
|
||||
<string name="action_authenticate_password">Authenticate with Password</string>
|
||||
<string name="prompt_api_key">Pi-Hole API Key</string>
|
||||
<string name="action_authenticate_api_key">Authenticate with API Key</string>
|
||||
<string name="connecting_to_pihole">Connecting to Pi-hole…</string>
|
||||
<string name="action_cancel">Cancel</string>
|
||||
</resources>
|
||||
|
|
|
@ -4,25 +4,17 @@
|
|||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="android:windowBackground">@drawable/background_splash</item>
|
||||
<item name="android:statusBarColor">@color/colorTransparent</item>
|
||||
<item name="android:textColor">@color/colorOnSurface</item>
|
||||
<item name="android:statusBarColor">@color/colorWhite</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme" parent="BaseTheme">
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.Button" parent="Widget.MaterialComponents.Button">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:textColor">@color/colorWhite</item>
|
||||
<style name="Theme.App.Starting" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">@color/colorSurface</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_app_logo</item>
|
||||
<item name="postSplashScreenTheme">@style/AppTheme</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.Button.Green" parent="AppTheme.Button">
|
||||
<item name="backgroundTint">@color/colorGreenDark</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.Button.Red" parent="AppTheme.Button">
|
||||
<item name="backgroundTint">@color/colorRedDark</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
android:shortcutLongLabel="@string/action_disable_5_minutes"
|
||||
android:shortcutShortLabel="@string/action_disable_5_minutes_short">
|
||||
<intent
|
||||
android:action="com.wbrawner.pihelper.MainFragment.ACTION_DISABLE"
|
||||
android:action="com.wbrawner.pihelper.ShortcutActions.DISABLE"
|
||||
android:targetClass="com.wbrawner.pihelper.MainActivity"
|
||||
android:targetPackage="com.wbrawner.pihelper">
|
||||
<extra
|
||||
android:name="com.wbrawner.pihelper.MainFragment.EXTRA_DURATION"
|
||||
android:name="com.wbrawner.pihelper.MainActivityKt.DURATION"
|
||||
android:value="300" />
|
||||
</intent>
|
||||
</shortcut>
|
||||
|
@ -23,11 +23,11 @@
|
|||
android:shortcutLongLabel="@string/action_disable_30_seconds"
|
||||
android:shortcutShortLabel="@string/action_disable_30_seconds_short">
|
||||
<intent
|
||||
android:action="com.wbrawner.pihelper.MainFragment.ACTION_DISABLE"
|
||||
android:action="com.wbrawner.pihelper.ShortcutActions.DISABLE"
|
||||
android:targetClass="com.wbrawner.pihelper.MainActivity"
|
||||
android:targetPackage="com.wbrawner.pihelper">
|
||||
<extra
|
||||
android:name="com.wbrawner.pihelper.MainFragment.EXTRA_DURATION"
|
||||
android:name="com.wbrawner.pihelper.MainActivityKt.DURATION"
|
||||
android:value="30" />
|
||||
</intent>
|
||||
</shortcut>
|
||||
|
@ -39,11 +39,11 @@
|
|||
android:shortcutLongLabel="@string/action_disable_10_seconds"
|
||||
android:shortcutShortLabel="@string/action_disable_10_seconds_short">
|
||||
<intent
|
||||
android:action="com.wbrawner.pihelper.MainFragment.ACTION_DISABLE"
|
||||
android:action="com.wbrawner.pihelper.ShortcutActions.DISABLE"
|
||||
android:targetClass="com.wbrawner.pihelper.MainActivity"
|
||||
android:targetPackage="com.wbrawner.pihelper">
|
||||
<extra
|
||||
android:name="com.wbrawner.pihelper.MainFragment.EXTRA_DURATION"
|
||||
android:name="com.wbrawner.pihelper.MainActivityKt.DURATION"
|
||||
android:value="10" />
|
||||
</intent>
|
||||
</shortcut>
|
||||
|
@ -55,7 +55,7 @@
|
|||
android:shortcutLongLabel="@string/action_enable"
|
||||
android:shortcutShortLabel="@string/action_enable">
|
||||
<intent
|
||||
android:action="com.wbrawner.pihelper.MainFragment.ACTION_ENABLE"
|
||||
android:action="com.wbrawner.pihelper.ShortcutActions.ENABLE"
|
||||
android:targetClass="com.wbrawner.pihelper.MainActivity"
|
||||
android:targetPackage="com.wbrawner.pihelper" />
|
||||
</shortcut>
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
android:enabled="true"
|
||||
android:icon="@drawable/ic_shortcut_pause">
|
||||
<intent
|
||||
android:action="com.wbrawner.pihelper.MainFragment.ACTION_DISABLE"
|
||||
android:action="com.wbrawner.pihelper.ShortcutActions.DISABLE"
|
||||
android:targetClass="com.wbrawner.pihelper.MainActivity"
|
||||
android:targetPackage="com.wbrawner.pihelper">
|
||||
<extra
|
||||
android:name="com.wbrawner.pihelper.MainFragment.EXTRA_DURATION"
|
||||
android:name="com.wbrawner.pihelper.MainActivityKt.DURATION"
|
||||
android:value="300" />
|
||||
</intent>
|
||||
</shortcut>
|
||||
|
@ -17,11 +17,11 @@
|
|||
android:enabled="true"
|
||||
android:icon="@drawable/ic_shortcut_pause">
|
||||
<intent
|
||||
android:action="com.wbrawner.pihelper.MainFragment.ACTION_DISABLE"
|
||||
android:action="com.wbrawner.pihelper.ShortcutActions.DISABLE"
|
||||
android:targetClass="com.wbrawner.pihelper.MainActivity"
|
||||
android:targetPackage="com.wbrawner.pihelper">
|
||||
<extra
|
||||
android:name="com.wbrawner.pihelper.MainFragment.EXTRA_DURATION"
|
||||
android:name="com.wbrawner.pihelper.MainActivityKt.DURATION"
|
||||
android:value="30" />
|
||||
</intent>
|
||||
</shortcut>
|
||||
|
@ -30,11 +30,11 @@
|
|||
android:enabled="true"
|
||||
android:icon="@drawable/ic_shortcut_pause">
|
||||
<intent
|
||||
android:action="com.wbrawner.pihelper.MainFragment.ACTION_DISABLE"
|
||||
android:action="com.wbrawner.pihelper.ShortcutActions.DISABLE"
|
||||
android:targetClass="com.wbrawner.pihelper.MainActivity"
|
||||
android:targetPackage="com.wbrawner.pihelper">
|
||||
<extra
|
||||
android:name="com.wbrawner.pihelper.MainFragment.EXTRA_DURATION"
|
||||
android:name="com.wbrawner.pihelper.MainActivityKt.DURATION"
|
||||
android:value="10" />
|
||||
</intent>
|
||||
</shortcut>
|
||||
|
@ -43,7 +43,7 @@
|
|||
android:enabled="true"
|
||||
android:icon="@drawable/ic_shortcut_enable">
|
||||
<intent
|
||||
android:action="com.wbrawner.pihelper.MainFragment.ACTION_ENABLE"
|
||||
android:action="com.wbrawner.pihelper.ShortcutActions.ENABLE"
|
||||
android:targetClass="com.wbrawner.pihelper.MainActivity"
|
||||
android:targetPackage="com.wbrawner.pihelper" />
|
||||
</shortcut>
|
||||
|
|
25
build.gradle
25
build.gradle
|
@ -1,25 +0,0 @@
|
|||
buildscript {
|
||||
ext.kotlin_version = '1.3.61'
|
||||
ext.coroutines_version = '1.3.2'
|
||||
ext.koin_version = '2.0.1'
|
||||
ext.okhttp_version = '4.2.2'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.6.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
12
build.gradle.kts
Normal file
12
build.gradle.kts
Normal file
|
@ -0,0 +1,12 @@
|
|||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.android.library) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
alias(libs.plugins.kotlin.jvm) apply false
|
||||
alias(libs.plugins.kotlin.multiplatform) apply false
|
||||
alias(libs.plugins.kotlin.serialization) apply false
|
||||
alias(libs.plugins.compose) apply false
|
||||
alias(libs.plugins.jetbrainsCompose) apply false
|
||||
alias(libs.plugins.hilt.android) apply false
|
||||
alias(libs.plugins.kotlin.ksp) apply false
|
||||
}
|
2
desktop/.gitignore
vendored
Normal file
2
desktop/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/build
|
||||
/release
|
21
desktop/.run/desktop.run.xml
Normal file
21
desktop/.run/desktop.run.xml
Normal file
|
@ -0,0 +1,21 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="desktop" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName"/>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$"/>
|
||||
<option name="externalSystemIdString" value="GRADLE"/>
|
||||
<option name="scriptParameters" value=""/>
|
||||
<option name="taskDescriptions">
|
||||
<list/>
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="run"/>
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" value=""/>
|
||||
</ExternalSystemSettings>
|
||||
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
|
||||
<method v="2"/>
|
||||
</configuration>
|
||||
</component>
|
45
desktop/build.gradle.kts
Normal file
45
desktop/build.gradle.kts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
alias(libs.plugins.compose)
|
||||
alias(libs.plugins.jetbrainsCompose)
|
||||
java
|
||||
}
|
||||
|
||||
group = "com.wbrawner.pihelper"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
dependencies {
|
||||
// Note, if you develop a library, you should use compose.desktop.common.
|
||||
// compose.desktop.currentOs should be used in launcher-sourceSet
|
||||
// (in a separate module for demo project and in testMain).
|
||||
// With compose.desktop.common you will also lose @Preview functionality
|
||||
implementation(project(":shared"))
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation(libs.kotlinx.coroutines.jvm)
|
||||
implementation(libs.logback.classic)
|
||||
}
|
||||
|
||||
compose.desktop {
|
||||
application {
|
||||
mainClass = "MainKt"
|
||||
|
||||
nativeDistributions {
|
||||
includeAllModules = true
|
||||
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
||||
packageName = "Pi-helper"
|
||||
packageVersion = "1.0.0"
|
||||
|
||||
macOS {
|
||||
iconFile.set(project.file("src/main/resources/icon.icns"))
|
||||
}
|
||||
windows {
|
||||
iconFile.set(project.file("src/main/resources/icon.ico"))
|
||||
}
|
||||
linux {
|
||||
iconFile.set(project.file("src/main/resources/icon.png"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
138
desktop/src/main/kotlin/Main.kt
Normal file
138
desktop/src/main/kotlin/Main.kt
Normal file
|
@ -0,0 +1,138 @@
|
|||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.window.Tray
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.application
|
||||
import androidx.compose.ui.window.isTraySupported
|
||||
import androidx.compose.ui.window.rememberTrayState
|
||||
import com.wbrawner.pihelper.shared.*
|
||||
import com.wbrawner.pihelper.shared.State
|
||||
import com.wbrawner.pihelper.shared.ui.AddScreen
|
||||
import com.wbrawner.pihelper.shared.ui.AuthScreen
|
||||
import com.wbrawner.pihelper.shared.ui.InfoScreen
|
||||
import com.wbrawner.pihelper.shared.ui.MainScreen
|
||||
import com.wbrawner.pihelper.shared.ui.theme.PihelperTheme
|
||||
import java.net.InetAddress
|
||||
|
||||
val store = Store(PiholeAPIService.create())
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
@Preview
|
||||
fun App(state: State) {
|
||||
val error by store.effects.collectAsState(Effect.Empty)
|
||||
|
||||
PihelperTheme {
|
||||
when (state.route) {
|
||||
Route.CONNECT -> AddScreen(
|
||||
scanNetwork = {
|
||||
store.dispatch(Action.Scan(InetAddress.getLocalHost().hostAddress))
|
||||
},
|
||||
connectToPihole = { store.dispatch(Action.Connect(it)) },
|
||||
loading = state.loading,
|
||||
error = error as? Effect.Error
|
||||
)
|
||||
|
||||
Route.AUTH -> AuthScreen(store)
|
||||
Route.HOME -> MainScreen(store)
|
||||
Route.ABOUT -> InfoScreen(store)
|
||||
else -> {
|
||||
Text("Not yet implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun main() = application {
|
||||
LaunchedEffect(Unit) {
|
||||
System.setProperty("apple.awt.enableTemplateImages", "true")
|
||||
}
|
||||
val state by store.state.collectAsState()
|
||||
val trayState = rememberTrayState()
|
||||
var isOpen by remember { mutableStateOf(state.apiKey.isNullOrBlank()) }
|
||||
val statusText = when (val status = state.status) {
|
||||
is Status.Enabled -> "Enabled"
|
||||
is Status.Disabled -> status.timeRemaining?.let { "Disabled (${it})" } ?: "Disabled"
|
||||
else -> "Not connected"
|
||||
}
|
||||
Tray(
|
||||
state = trayState,
|
||||
tooltip = statusText,
|
||||
icon = painterResource("IconTemplate.png"),
|
||||
menu = {
|
||||
Item(
|
||||
text = statusText,
|
||||
enabled = false,
|
||||
onClick = {}
|
||||
)
|
||||
if (state.status is Status.Disabled) {
|
||||
Item(
|
||||
"Enable",
|
||||
onClick = {
|
||||
store.dispatch(Action.Enable)
|
||||
}
|
||||
)
|
||||
} else if (state.status is Status.Enabled) {
|
||||
Item(
|
||||
"Disable for 10 seconds",
|
||||
onClick = {
|
||||
store.dispatch(Action.Disable(duration = 10))
|
||||
}
|
||||
)
|
||||
Item(
|
||||
"Disable for 30 seconds",
|
||||
onClick = {
|
||||
store.dispatch(Action.Disable(duration = 30))
|
||||
}
|
||||
)
|
||||
Item(
|
||||
"Disable for 1 minute",
|
||||
onClick = {
|
||||
store.dispatch(Action.Disable(duration = 30))
|
||||
}
|
||||
)
|
||||
Item(
|
||||
"Disable for 5 minutes",
|
||||
onClick = {
|
||||
store.dispatch(Action.Disable(duration = 30))
|
||||
}
|
||||
)
|
||||
Item(
|
||||
"Disable permanently",
|
||||
onClick = {
|
||||
store.dispatch(Action.Disable())
|
||||
}
|
||||
)
|
||||
}
|
||||
Item(
|
||||
text = if (isOpen) "Hide window" else "Show window",
|
||||
onClick = {
|
||||
isOpen = !isOpen
|
||||
}
|
||||
)
|
||||
Item(
|
||||
"Exit",
|
||||
onClick = {
|
||||
isOpen = false
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
if (!isTraySupported || isOpen) {
|
||||
Window(
|
||||
title = "Pi-helper",
|
||||
onCloseRequest = {
|
||||
if (isTraySupported) {
|
||||
isOpen = false
|
||||
} else {
|
||||
exitApplication()
|
||||
}
|
||||
}) {
|
||||
App(state)
|
||||
}
|
||||
}
|
||||
}
|
BIN
desktop/src/main/resources/IconTemplate.png
Normal file
BIN
desktop/src/main/resources/IconTemplate.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
BIN
desktop/src/main/resources/icon.icns
Normal file
BIN
desktop/src/main/resources/icon.icns
Normal file
Binary file not shown.
BIN
desktop/src/main/resources/icon.ico
Normal file
BIN
desktop/src/main/resources/icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 106 KiB |
BIN
desktop/src/main/resources/icon.png
Normal file
BIN
desktop/src/main/resources/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
|
@ -19,3 +19,6 @@ android.useAndroidX=true
|
|||
android.enableJetifier=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonTransitiveRClass=false
|
||||
android.nonFinalResIds=false
|
||||
|
|
82
gradle/libs.versions.toml
Normal file
82
gradle/libs.versions.toml
Normal file
|
@ -0,0 +1,82 @@
|
|||
[versions]
|
||||
androidGradlePlugin = "8.7.2"
|
||||
androidx-core = "1.15.0"
|
||||
androidx-appcompat = "1.7.0"
|
||||
androidx-splash = "1.0.1"
|
||||
androidx-test-runner = "1.6.2"
|
||||
androidx-test-orchestrator = "1.5.1"
|
||||
compose = "1.7.5"
|
||||
compose-multiplatform = "1.7.1"
|
||||
compose-compiler = "1.4.2"
|
||||
compose-material = "1.7.5"
|
||||
compose-material3 = "1.3.1"
|
||||
espresso = "3.6.1"
|
||||
hilt-android = "2.52"
|
||||
kotlin = "2.0.21"
|
||||
kotlin-ksp = "2.1.0-1.0.28"
|
||||
kotlinx-serialization = "1.7.3"
|
||||
kotlinx-coroutines = "1.9.0"
|
||||
kotlinx-datetime = "0.6.1"
|
||||
ktor = "3.0.1"
|
||||
logbackClassic = "1.5.12"
|
||||
material = "1.12.0"
|
||||
maxSdk = "35"
|
||||
minSdk = "23"
|
||||
okhttp = "4.12.0"
|
||||
settings = "1.2.0"
|
||||
versionCode = "5"
|
||||
versionName = "1.1.1"
|
||||
|
||||
[libraries]
|
||||
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.9.3" }
|
||||
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-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" }
|
||||
compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
|
||||
espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" }
|
||||
hilt-android-core = { module = "com.google.dagger:hilt-android", version.ref = "hilt-android" }
|
||||
hilt-android-ksp = { 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.2.0" }
|
||||
junit = { module = "junit:junit", version = "4.13.2" }
|
||||
ktor-client-cio = { module = "io.ktor:ktor-client-cio", 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-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" }
|
||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
||||
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logbackClassic" }
|
||||
material = { module = "com.google.android.material:material", version.ref = "material" }
|
||||
mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" }
|
||||
multiplatform-settings = { module = "com.russhwolf:multiplatform-settings-no-arg", version.ref = "settings" }
|
||||
navigation-compose = { module = "androidx.navigation:navigation-compose", version = "navigation" }
|
||||
preference = { module = "androidx.preference:preference-ktx", version = "1.2.1" }
|
||||
test-ext = { module = "androidx.test.ext:junit", version = "1.2.1" }
|
||||
|
||||
[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-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
|
||||
android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
|
||||
compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "kotlin-ksp" }
|
||||
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt-android" }
|
||||
jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
||||
|
|
6
gradlew
vendored
6
gradlew
vendored
|
@ -82,7 +82,7 @@ location of your Java installation."
|
|||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
which kotlin >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
|
@ -109,7 +109,7 @@ if $darwin; then
|
|||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
# For Cygwin, switch paths to Windows format before running kotlin
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
@ -161,7 +161,7 @@ save () {
|
|||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
# Collect all arguments for the kotlin command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
|
|
1
piholeclient/.gitignore
vendored
1
piholeclient/.gitignore
vendored
|
@ -1 +0,0 @@
|
|||
/build
|
|
@ -1,45 +0,0 @@
|
|||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
defaultConfig {
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 29
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles 'consumer-rules.pro'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||
implementation "org.koin:koin-core:$koin_version"
|
||||
implementation "com.squareup.okhttp3:okhttp:${okhttp_version}"
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:${okhttp_version}"
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.core:core-ktx:1.1.0'
|
||||
implementation "com.squareup.moshi:moshi:1.9.2"
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.9.2"
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
# JSR 305 annotations are for embedding nullability information.
|
||||
-dontwarn javax.annotation.**
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@com.squareup.moshi.* <methods>;
|
||||
}
|
||||
|
||||
-keep @com.squareup.moshi.JsonQualifier interface *
|
||||
|
||||
# Enum field names are used by the integrated EnumJsonAdapter.
|
||||
# values() is synthesized by the Kotlin compiler and is used by EnumJsonAdapter indirectly
|
||||
# Annotate enums with @JsonClass(generateAdapter = false) to use them with Moshi.
|
||||
-keepclassmembers @com.squareup.moshi.JsonClass class * extends java.lang.Enum {
|
||||
<fields>;
|
||||
**[] values();
|
||||
}
|
||||
|
||||
# The name of @JsonClass types is used to look up the generated adapter.
|
||||
-keepnames @com.squareup.moshi.JsonClass class *
|
||||
|
||||
# Retain generated target class's synthetic defaults constructor and keep DefaultConstructorMarker's
|
||||
# name. We will look this up reflectively to invoke the type's constructor.
|
||||
#
|
||||
# We can't _just_ keep the defaults constructor because Proguard/R8's spec doesn't allow wildcard
|
||||
# matching preceding parameters.
|
||||
-keepnames class kotlin.jvm.internal.DefaultConstructorMarker
|
||||
-keepclassmembers @com.squareup.moshi.JsonClass @kotlin.Metadata class * {
|
||||
synthetic <init>(...);
|
||||
}
|
||||
|
||||
# Retain generated JsonAdapters if annotated type is retained.
|
||||
-if @com.squareup.moshi.JsonClass class *
|
||||
-keep class <1>JsonAdapter {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
||||
-if @com.squareup.moshi.JsonClass class **$*
|
||||
-keep class <1>_<2>JsonAdapter {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
||||
-if @com.squareup.moshi.JsonClass class **$*$*
|
||||
-keep class <1>_<2>_<3>JsonAdapter {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
||||
-if @com.squareup.moshi.JsonClass class **$*$*$*
|
||||
-keep class <1>_<2>_<3>_<4>JsonAdapter {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
||||
-if @com.squareup.moshi.JsonClass class **$*$*$*$*
|
||||
-keep class <1>_<2>_<3>_<4>_<5>JsonAdapter {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
||||
-if @com.squareup.moshi.JsonClass class **$*$*$*$*$*
|
||||
-keep class <1>_<2>_<3>_<4>_<5>_<6>JsonAdapter {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
||||
|
||||
-keep class kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoaderImpl
|
||||
|
||||
-keepclassmembers class kotlin.Metadata {
|
||||
public <methods>;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue