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
|
*.iml
|
||||||
.gradle
|
.gradle
|
||||||
/local.properties
|
/local.properties
|
||||||
/.idea/caches
|
.idea
|
||||||
/.idea/libraries
|
|
||||||
/.idea/modules.xml
|
|
||||||
/.idea/workspace.xml
|
|
||||||
/.idea/navEditor.xml
|
|
||||||
/.idea/assetWizardSettings.xml
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/build
|
/build
|
||||||
/captures
|
/captures
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
.cxx
|
.cxx
|
||||||
keystore.properties
|
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">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
<option name="testRunner" value="PLATFORM" />
|
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/app" />
|
||||||
<option value="$PROJECT_DIR$/piholeclient" />
|
<option value="$PROJECT_DIR$/desktop" />
|
||||||
|
<option value="$PROJECT_DIR$/shared" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
<option name="resolveModulePerSourceSet" value="false" />
|
<option name="resolveExternalAnnotations" value="false" />
|
||||||
</GradleProjectSettings>
|
</GradleProjectSettings>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</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" />
|
<configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" />
|
||||||
</configurations>
|
</configurations>
|
||||||
</component>
|
</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" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<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">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
<mapping directory="$PROJECT_DIR$/desktop" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
|
@ -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.
|
# Add project specific ProGuard rules here.
|
||||||
# You can control the set of applied configuration files using the
|
# 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
|
# For more details, see
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
# 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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
package="com.wbrawner.pihelper">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".PiHelperApplication"
|
android:name=".PiHelperApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:fullBackupContent="@xml/backup_descriptor"
|
android:fullBackupContent="@xml/backup_descriptor"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/Theme.App.Starting">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<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
|
package com.wbrawner.pihelper
|
||||||
|
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.animation.ObjectAnimator
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
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.appcompat.app.AppCompatActivity
|
||||||
import androidx.navigation.NavController
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.navigation.findNavController
|
import androidx.compose.runtime.*
|
||||||
import com.wbrawner.pihelper.MainFragment.Companion.ACTION_DISABLE
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import com.wbrawner.pihelper.MainFragment.Companion.ACTION_ENABLE
|
import androidx.core.animation.doOnEnd
|
||||||
import com.wbrawner.pihelper.MainFragment.Companion.EXTRA_DURATION
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import org.koin.android.ext.android.inject
|
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() {
|
class MainActivity : AppCompatActivity() {
|
||||||
private val addPiHoleViewModel: AddPiHelperViewModel by inject()
|
@Inject
|
||||||
private val navController: NavController by lazy {
|
lateinit var store: Store
|
||||||
findNavController(R.id.content_main)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
installSplashScreen()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
enableEdgeToEdge()
|
||||||
window.setBackgroundDrawable(ColorDrawable(getColor(R.color.colorSurface)))
|
setContent {
|
||||||
val analyticsBundle = Bundle()
|
BackHandler {
|
||||||
analyticsBundle.putString("intent_action", intent.action)
|
store.dispatch(Action.Back)
|
||||||
val args = when (intent.action) {
|
}
|
||||||
ACTION_ENABLE -> {
|
val launchIntent = remember { intent }
|
||||||
if (addPiHoleViewModel.apiKey == null) {
|
LaunchedEffect(launchIntent) {
|
||||||
Toast.makeText(this, R.string.configure_pihelper, Toast.LENGTH_SHORT).show()
|
ShortcutActions.fromIntentAction(launchIntent.action)?.let { action ->
|
||||||
null
|
if (action == ShortcutActions.DISABLE) {
|
||||||
|
val duration = launchIntent.getIntExtra(DURATION, 0)
|
||||||
|
store.dispatch(Action.Disable(duration.toLong()))
|
||||||
} else {
|
} else {
|
||||||
Bundle().apply { putBoolean(ACTION_ENABLE, true) }
|
store.dispatch(Action.Enable)
|
||||||
}
|
|
||||||
}
|
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ACTION_FORGET_PIHOLE -> {
|
val state by store.state.collectAsState()
|
||||||
if (intent.component?.packageName == packageName) {
|
val navController = rememberNavController()
|
||||||
while (navController.popBackStack()) {
|
LaunchedEffect(state.route) {
|
||||||
// Do nothing, just pop all the items off the back stack
|
navController.navigate(state.route.name)
|
||||||
}
|
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
val effect by store.effects.collectAsState(initial = Effect.Empty)
|
||||||
|
LaunchedEffect(effect) {
|
||||||
|
when (effect) {
|
||||||
|
is Effect.Exit -> finish()
|
||||||
else -> {
|
else -> {
|
||||||
navController.navigate(R.id.mainFragment, args)
|
// no-op
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
PihelperTheme {
|
||||||
override fun onBackPressed() {
|
NavHost(navController, startDestination = state.initialRoute.name) {
|
||||||
if (!navController.navigateUp()) {
|
composable(Route.CONNECT.name) {
|
||||||
finish()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
package com.wbrawner.pihelper
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
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
|
|
||||||
|
|
||||||
@Suppress("unused")
|
@HiltAndroidApp
|
||||||
class PiHelperApplication: Application() {
|
class PiHelperApplication : Application()
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
startKoin{
|
|
||||||
androidLogger()
|
|
||||||
androidContext(this@PiHelperApplication)
|
|
||||||
modules(listOf(
|
|
||||||
piHoleClientModule,
|
|
||||||
piHelperModule
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,33 +1,38 @@
|
||||||
package com.wbrawner.pihelper
|
package com.wbrawner.pihelper
|
||||||
|
|
||||||
import androidx.security.crypto.EncryptedSharedPreferences
|
import com.wbrawner.pihelper.shared.*
|
||||||
import com.wbrawner.piholeclient.NAME_BASE_URL
|
import dagger.Module
|
||||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
import dagger.Provides
|
||||||
import org.koin.core.qualifier.named
|
import dagger.hilt.InstallIn
|
||||||
import org.koin.dsl.module
|
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 {
|
@Provides
|
||||||
single {
|
@Singleton
|
||||||
EncryptedSharedPreferences.create(
|
fun providesAnalyticsHelper(): AnalyticsHelper = object : AnalyticsHelper {
|
||||||
ENCRYPTED_SHARED_PREFS_FILE_NAME,
|
override fun pageView(route: Route) {
|
||||||
"pihelper",
|
// Not implemented
|
||||||
get(),
|
|
||||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
||||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel {
|
override fun event(
|
||||||
AddPiHelperViewModel(get(), get())
|
event: AnalyticsEvent,
|
||||||
|
route: Route
|
||||||
|
) {
|
||||||
|
// Not implemented
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel {
|
@Provides
|
||||||
PiHelperViewModel(get())
|
@Singleton
|
||||||
}
|
fun providesStore(
|
||||||
|
apiService: PiholeAPIService,
|
||||||
single(named(NAME_BASE_URL)) {
|
analyticsHelper: AnalyticsHelper
|
||||||
get<EncryptedSharedPreferences>().getString(KEY_BASE_URL, "")
|
): 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:viewportWidth="24.0"
|
||||||
android:viewportHeight="24.0">
|
android:viewportHeight="24.0">
|
||||||
<path
|
<path
|
||||||
android:fillColor="@color/colorRedDark"
|
android:fillColor="@color/colorWhite"
|
||||||
android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
|
android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
|
||||||
</vector>
|
</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:viewportWidth="24.0"
|
||||||
android:viewportHeight="24.0">
|
android:viewportHeight="24.0">
|
||||||
<path
|
<path
|
||||||
android:fillColor="@color/colorGreenDark"
|
android:fillColor="@color/colorWhite"
|
||||||
android:pathData="M8,5v14l11,-7z"/>
|
android:pathData="M8,5v14l11,-7z"/>
|
||||||
</vector>
|
</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">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item>
|
<item>
|
||||||
<shape
|
<shape
|
||||||
android:shape="oval"
|
android:shape="rectangle"
|
||||||
android:tint="@color/colorWhite" />
|
android:tint="@color/colorSurface" />
|
||||||
</item>
|
</item>
|
||||||
<item
|
<item
|
||||||
android:bottom="0dp"
|
android:bottom="0dp"
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item>
|
<item>
|
||||||
<shape
|
<shape
|
||||||
android:shape="oval"
|
android:shape="rectangle"
|
||||||
android:tint="@color/colorWhite" />
|
android:tint="@color/colorSurface" />
|
||||||
</item>
|
</item>
|
||||||
<item
|
<item
|
||||||
android:bottom="0dp"
|
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">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
|
@ -2,4 +2,5 @@
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
</adaptive-icon>
|
</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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="colorSurface">#333333</color>
|
<color name="colorSurface">#000000</color>
|
||||||
<color name="colorOnSurface">#f1f1f1</color>
|
<color name="colorOnSurface">#f1f1f1</color>
|
||||||
<color name="colorEnabled">@color/colorGreenLight</color>
|
|
||||||
<color name="colorDisabled">@color/colorRedLight</color>
|
|
||||||
<color name="colorButtonSecondary">#999999</color>
|
|
||||||
</resources>
|
</resources>
|
|
@ -1,13 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<style name="AppTheme" parent="BaseTheme">
|
<style name="AppTheme" parent="BaseTheme">
|
||||||
|
<item name="android:statusBarColor">@color/colorTransparent</item>
|
||||||
<item name="android:windowLightStatusBar">false</item>
|
<item name="android:windowLightStatusBar">false</item>
|
||||||
</style>
|
</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>
|
</resources>
|
|
@ -1,18 +1,15 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="colorPrimary">@color/colorRedLight</color>
|
<color name="colorPrimary">@color/colorAccent</color>
|
||||||
<color name="colorPrimaryDark">@color/colorRedDark</color>
|
<color name="colorPrimaryDark">@color/colorRedDark</color>
|
||||||
<color name="colorAccent">@color/colorRedLight</color>
|
<color name="colorAccent">@color/colorRedLight</color>
|
||||||
<color name="colorRedLight">#f60d1a</color>
|
<color name="colorRedLight">#F44336</color>
|
||||||
<color name="colorRedDark">#96060c</color>
|
<color name="colorRedDark">#B71C1C</color>
|
||||||
<color name="colorGreenLight">#30d158</color>
|
<color name="colorGreenLight">#4CAF50</color>
|
||||||
<color name="colorGreenDark">#34c759</color>
|
<color name="colorGreenDark">#1B5E20</color>
|
||||||
|
<color name="colorShortcutBackground">#FFFFFFFF</color>
|
||||||
<color name="colorOnSurface">#000000</color>
|
<color name="colorOnSurface">#000000</color>
|
||||||
<color name="colorWhite">#ffffff</color>
|
<color name="colorWhite">#FFFBFE</color>
|
||||||
<color name="colorEnabled">@color/colorGreenDark</color>
|
|
||||||
<color name="colorUnknown">@color/colorButtonSecondary</color>
|
|
||||||
<color name="colorDisabled">@color/colorRedDark</color>
|
|
||||||
<color name="colorSurface">@color/colorWhite</color>
|
<color name="colorSurface">@color/colorWhite</color>
|
||||||
<color name="colorTransparent">#00000000</color>
|
<color name="colorTransparent">#00000000</color>
|
||||||
<color name="colorButtonSecondary">#666666</color>
|
|
||||||
</resources>
|
</resources>
|
|
@ -1,49 +1,10 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Pi-helper</string>
|
<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_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">Disable for 10 seconds</string>
|
||||||
<string name="action_disable_10_seconds_short">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">Disable for 30 seconds</string>
|
||||||
<string name="action_disable_30_seconds_short">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">Disable for 5 minutes</string>
|
||||||
<string name="action_disable_5_minutes_short">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>
|
</resources>
|
||||||
|
|
|
@ -4,25 +4,17 @@
|
||||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
<item name="colorAccent">@color/colorAccent</item>
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
<item name="android:windowBackground">@drawable/background_splash</item>
|
<item name="android:windowBackground">@drawable/background_splash</item>
|
||||||
<item name="android:statusBarColor">@color/colorTransparent</item>
|
<item name="android:statusBarColor">@color/colorWhite</item>
|
||||||
<item name="android:textColor">@color/colorOnSurface</item>
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppTheme" parent="BaseTheme">
|
<style name="AppTheme" parent="BaseTheme">
|
||||||
<item name="android:windowLightStatusBar">true</item>
|
<item name="android:windowLightStatusBar">true</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppTheme.Button" parent="Widget.MaterialComponents.Button">
|
<style name="Theme.App.Starting" parent="Theme.SplashScreen">
|
||||||
<item name="android:layout_width">match_parent</item>
|
<item name="windowSplashScreenBackground">@color/colorSurface</item>
|
||||||
<item name="android:layout_height">wrap_content</item>
|
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_app_logo</item>
|
||||||
<item name="android:textColor">@color/colorWhite</item>
|
<item name="postSplashScreenTheme">@style/AppTheme</item>
|
||||||
</style>
|
</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>
|
</resources>
|
||||||
|
|
|
@ -7,11 +7,11 @@
|
||||||
android:shortcutLongLabel="@string/action_disable_5_minutes"
|
android:shortcutLongLabel="@string/action_disable_5_minutes"
|
||||||
android:shortcutShortLabel="@string/action_disable_5_minutes_short">
|
android:shortcutShortLabel="@string/action_disable_5_minutes_short">
|
||||||
<intent
|
<intent
|
||||||
android:action="com.wbrawner.pihelper.MainFragment.ACTION_DISABLE"
|
android:action="com.wbrawner.pihelper.ShortcutActions.DISABLE"
|
||||||
android:targetClass="com.wbrawner.pihelper.MainActivity"
|
android:targetClass="com.wbrawner.pihelper.MainActivity"
|
||||||
android:targetPackage="com.wbrawner.pihelper">
|
android:targetPackage="com.wbrawner.pihelper">
|
||||||
<extra
|
<extra
|
||||||
android:name="com.wbrawner.pihelper.MainFragment.EXTRA_DURATION"
|
android:name="com.wbrawner.pihelper.MainActivityKt.DURATION"
|
||||||
android:value="300" />
|
android:value="300" />
|
||||||
</intent>
|
</intent>
|
||||||
</shortcut>
|
</shortcut>
|
||||||
|
@ -23,11 +23,11 @@
|
||||||
android:shortcutLongLabel="@string/action_disable_30_seconds"
|
android:shortcutLongLabel="@string/action_disable_30_seconds"
|
||||||
android:shortcutShortLabel="@string/action_disable_30_seconds_short">
|
android:shortcutShortLabel="@string/action_disable_30_seconds_short">
|
||||||
<intent
|
<intent
|
||||||
android:action="com.wbrawner.pihelper.MainFragment.ACTION_DISABLE"
|
android:action="com.wbrawner.pihelper.ShortcutActions.DISABLE"
|
||||||
android:targetClass="com.wbrawner.pihelper.MainActivity"
|
android:targetClass="com.wbrawner.pihelper.MainActivity"
|
||||||
android:targetPackage="com.wbrawner.pihelper">
|
android:targetPackage="com.wbrawner.pihelper">
|
||||||
<extra
|
<extra
|
||||||
android:name="com.wbrawner.pihelper.MainFragment.EXTRA_DURATION"
|
android:name="com.wbrawner.pihelper.MainActivityKt.DURATION"
|
||||||
android:value="30" />
|
android:value="30" />
|
||||||
</intent>
|
</intent>
|
||||||
</shortcut>
|
</shortcut>
|
||||||
|
@ -39,11 +39,11 @@
|
||||||
android:shortcutLongLabel="@string/action_disable_10_seconds"
|
android:shortcutLongLabel="@string/action_disable_10_seconds"
|
||||||
android:shortcutShortLabel="@string/action_disable_10_seconds_short">
|
android:shortcutShortLabel="@string/action_disable_10_seconds_short">
|
||||||
<intent
|
<intent
|
||||||
android:action="com.wbrawner.pihelper.MainFragment.ACTION_DISABLE"
|
android:action="com.wbrawner.pihelper.ShortcutActions.DISABLE"
|
||||||
android:targetClass="com.wbrawner.pihelper.MainActivity"
|
android:targetClass="com.wbrawner.pihelper.MainActivity"
|
||||||
android:targetPackage="com.wbrawner.pihelper">
|
android:targetPackage="com.wbrawner.pihelper">
|
||||||
<extra
|
<extra
|
||||||
android:name="com.wbrawner.pihelper.MainFragment.EXTRA_DURATION"
|
android:name="com.wbrawner.pihelper.MainActivityKt.DURATION"
|
||||||
android:value="10" />
|
android:value="10" />
|
||||||
</intent>
|
</intent>
|
||||||
</shortcut>
|
</shortcut>
|
||||||
|
@ -55,7 +55,7 @@
|
||||||
android:shortcutLongLabel="@string/action_enable"
|
android:shortcutLongLabel="@string/action_enable"
|
||||||
android:shortcutShortLabel="@string/action_enable">
|
android:shortcutShortLabel="@string/action_enable">
|
||||||
<intent
|
<intent
|
||||||
android:action="com.wbrawner.pihelper.MainFragment.ACTION_ENABLE"
|
android:action="com.wbrawner.pihelper.ShortcutActions.ENABLE"
|
||||||
android:targetClass="com.wbrawner.pihelper.MainActivity"
|
android:targetClass="com.wbrawner.pihelper.MainActivity"
|
||||||
android:targetPackage="com.wbrawner.pihelper" />
|
android:targetPackage="com.wbrawner.pihelper" />
|
||||||
</shortcut>
|
</shortcut>
|
||||||
|
|
|
@ -4,11 +4,11 @@
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:icon="@drawable/ic_shortcut_pause">
|
android:icon="@drawable/ic_shortcut_pause">
|
||||||
<intent
|
<intent
|
||||||
android:action="com.wbrawner.pihelper.MainFragment.ACTION_DISABLE"
|
android:action="com.wbrawner.pihelper.ShortcutActions.DISABLE"
|
||||||
android:targetClass="com.wbrawner.pihelper.MainActivity"
|
android:targetClass="com.wbrawner.pihelper.MainActivity"
|
||||||
android:targetPackage="com.wbrawner.pihelper">
|
android:targetPackage="com.wbrawner.pihelper">
|
||||||
<extra
|
<extra
|
||||||
android:name="com.wbrawner.pihelper.MainFragment.EXTRA_DURATION"
|
android:name="com.wbrawner.pihelper.MainActivityKt.DURATION"
|
||||||
android:value="300" />
|
android:value="300" />
|
||||||
</intent>
|
</intent>
|
||||||
</shortcut>
|
</shortcut>
|
||||||
|
@ -17,11 +17,11 @@
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:icon="@drawable/ic_shortcut_pause">
|
android:icon="@drawable/ic_shortcut_pause">
|
||||||
<intent
|
<intent
|
||||||
android:action="com.wbrawner.pihelper.MainFragment.ACTION_DISABLE"
|
android:action="com.wbrawner.pihelper.ShortcutActions.DISABLE"
|
||||||
android:targetClass="com.wbrawner.pihelper.MainActivity"
|
android:targetClass="com.wbrawner.pihelper.MainActivity"
|
||||||
android:targetPackage="com.wbrawner.pihelper">
|
android:targetPackage="com.wbrawner.pihelper">
|
||||||
<extra
|
<extra
|
||||||
android:name="com.wbrawner.pihelper.MainFragment.EXTRA_DURATION"
|
android:name="com.wbrawner.pihelper.MainActivityKt.DURATION"
|
||||||
android:value="30" />
|
android:value="30" />
|
||||||
</intent>
|
</intent>
|
||||||
</shortcut>
|
</shortcut>
|
||||||
|
@ -30,11 +30,11 @@
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:icon="@drawable/ic_shortcut_pause">
|
android:icon="@drawable/ic_shortcut_pause">
|
||||||
<intent
|
<intent
|
||||||
android:action="com.wbrawner.pihelper.MainFragment.ACTION_DISABLE"
|
android:action="com.wbrawner.pihelper.ShortcutActions.DISABLE"
|
||||||
android:targetClass="com.wbrawner.pihelper.MainActivity"
|
android:targetClass="com.wbrawner.pihelper.MainActivity"
|
||||||
android:targetPackage="com.wbrawner.pihelper">
|
android:targetPackage="com.wbrawner.pihelper">
|
||||||
<extra
|
<extra
|
||||||
android:name="com.wbrawner.pihelper.MainFragment.EXTRA_DURATION"
|
android:name="com.wbrawner.pihelper.MainActivityKt.DURATION"
|
||||||
android:value="10" />
|
android:value="10" />
|
||||||
</intent>
|
</intent>
|
||||||
</shortcut>
|
</shortcut>
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:icon="@drawable/ic_shortcut_enable">
|
android:icon="@drawable/ic_shortcut_enable">
|
||||||
<intent
|
<intent
|
||||||
android:action="com.wbrawner.pihelper.MainFragment.ACTION_ENABLE"
|
android:action="com.wbrawner.pihelper.ShortcutActions.ENABLE"
|
||||||
android:targetClass="com.wbrawner.pihelper.MainActivity"
|
android:targetClass="com.wbrawner.pihelper.MainActivity"
|
||||||
android:targetPackage="com.wbrawner.pihelper" />
|
android:targetPackage="com.wbrawner.pihelper" />
|
||||||
</shortcut>
|
</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
|
android.enableJetifier=true
|
||||||
# Kotlin code style for this project: "official" or "obsolete":
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
kotlin.code.style=official
|
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
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
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
|
fi
|
||||||
else
|
else
|
||||||
JAVACMD="java"
|
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
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
location of your Java installation."
|
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\""
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# For Cygwin, switch paths to Windows format before running java
|
# For Cygwin, switch paths to Windows format before running kotlin
|
||||||
if $cygwin ; then
|
if $cygwin ; then
|
||||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
@ -161,7 +161,7 @@ save () {
|
||||||
}
|
}
|
||||||
APP_ARGS=$(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"
|
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
|
# 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