Compare commits

...

53 commits

Author SHA1 Message Date
5a3fb94cb2 Update dependency gradle to v8.11.1
All checks were successful
Build & Test / Validate (pull_request) Successful in 20s
Build & Test / Run Unit Tests (pull_request) Successful in 9m9s
Build & Test / Validate (push) Successful in 11s
Build & Test / Run Unit Tests (push) Successful in 13m25s
2024-11-20 18:05:00 +00:00
7bef2be13c Update ktor to v3
All checks were successful
Build & Test / Validate (push) Successful in 18s
Build & Test / Run Unit Tests (push) Successful in 10m27s
2024-11-17 07:12:43 +00:00
d71a7cc2f7 Update dependency com.russhwolf:multiplatform-settings-no-arg to v1
Some checks failed
Build & Test / Validate (pull_request) Successful in 15s
Build & Test / Run Unit Tests (push) Has been cancelled
Build & Test / Run Unit Tests (pull_request) Successful in 5m4s
Build & Test / Validate (push) Successful in 9s
2024-11-17 07:02:53 +00:00
7e1a2113fd Update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.7.3
All checks were successful
Build & Test / Validate (push) Successful in 11s
Build & Test / Run Unit Tests (push) Successful in 11m38s
2024-11-17 06:13:49 +00:00
680f900007 Update hilt.android to v2.52
Some checks failed
Build & Test / Validate (pull_request) Successful in 17s
Build & Test / Run Unit Tests (pull_request) Successful in 8m49s
Build & Test / Run Unit Tests (push) Has been cancelled
Build & Test / Validate (push) Successful in 19s
2024-11-17 06:02:42 +00:00
2b553e8327 Update dependency gradle to v8.11
All checks were successful
Build & Test / Validate (push) Successful in 18s
Build & Test / Run Unit Tests (push) Successful in 12m24s
2024-11-17 05:15:17 +00:00
9493f95eb3 Update dependency com.squareup.okhttp3:mockwebserver to v4.12.0
All checks were successful
Build & Test / Validate (push) Successful in 11s
Build & Test / Validate (pull_request) Successful in 11s
Build & Test / Run Unit Tests (pull_request) Successful in 5m12s
Build & Test / Run Unit Tests (push) Successful in 5m41s
2024-11-17 05:02:49 +00:00
bd9cc3c992 Update dependency com.russhwolf:multiplatform-settings-no-arg to v0.9
All checks were successful
Build & Test / Validate (push) Successful in 16s
Build & Test / Run Unit Tests (push) Successful in 8m53s
2024-11-17 04:11:21 +00:00
ae8cb65b6f Update dependency androidx.test.ext:junit to v1.2.1
Some checks failed
Build & Test / Validate (pull_request) Successful in 11s
Build & Test / Run Unit Tests (pull_request) Successful in 5m8s
Build & Test / Validate (push) Successful in 12s
Build & Test / Run Unit Tests (push) Has been cancelled
2024-11-17 04:02:59 +00:00
85fe3118d0 Update dependency androidx.activity:activity-compose to v1.9.3
All checks were successful
Build & Test / Validate (push) Successful in 17s
Build & Test / Run Unit Tests (push) Successful in 9m22s
2024-11-17 03:11:37 +00:00
15b31a874d Update dependency org.jetbrains.kotlinx:kotlinx-datetime to v0.6.1
Some checks failed
Build & Test / Run Unit Tests (pull_request) Successful in 5m0s
Build & Test / Validate (push) Successful in 11s
Build & Test / Validate (pull_request) Successful in 16s
Build & Test / Run Unit Tests (push) Has been cancelled
2024-11-17 03:02:57 +00:00
8072c38e4d Update dependency androidx.preference:preference-ktx to v1.2.1
All checks were successful
Build & Test / Validate (push) Successful in 12s
Build & Test / Run Unit Tests (push) Successful in 11m41s
2024-11-17 02:15:33 +00:00
badb12812c Update dependency androidx.security:security-crypto to v1.0.0
Some checks failed
Build & Test / Validate (pull_request) Successful in 11s
Build & Test / Run Unit Tests (pull_request) Successful in 8m19s
Build & Test / Validate (push) Successful in 16s
Build & Test / Run Unit Tests (push) Has been cancelled
2024-11-17 02:04:25 +00:00
2bdb4a4974 Add renovate.json
All checks were successful
Build & Test / Validate (push) Successful in 16s
Build & Test / Run Unit Tests (push) Successful in 8m41s
2024-11-17 01:57:36 +00:00
a912cd6cd3
Fix gradle usage for running checks on pull requests
All checks were successful
Build & Test / Validate (push) Successful in 20s
Build & Test / Run Unit Tests (push) Successful in 11m39s
2024-11-16 18:16:19 -07:00
2649e1e6d2
Fix inconsistent Java versions
Some checks failed
Build & Test / Validate (push) Successful in 28s
Build & Test / Run Unit Tests (push) Has been cancelled
2024-11-16 18:12:34 -07:00
0b26a16f1f
Remove Plausible
Some checks failed
Build & Test / Validate (push) Successful in 17s
Build & Test / Run Unit Tests (push) Failing after 6m18s
2024-11-16 17:54:43 -07:00
dba5313c74
fixup! Bump various dependencies
Some checks failed
Build & Test / Run Unit Tests (push) Has been cancelled
Build & Test / Validate (push) Successful in 2m28s
2024-11-16 17:49:53 -07:00
f2d5687a5f
Add pull request workflow
Some checks failed
Build & Test / Validate (push) Successful in 23s
Build & Test / Run Unit Tests (push) Failing after 6m59s
2024-11-16 16:47:42 -07:00
4bf56a5918
fixup! Enable predictive back gesture 2024-11-16 16:45:34 -07:00
d533682af6
Enable predictive back gesture 2024-11-16 16:43:34 -07:00
b5c23fc5ca
Bump various dependencies 2024-11-16 16:40:25 -07:00
7447a5ca6d
Bump max SDK 2024-11-16 15:27:22 -07:00
a4ec4c8aa4
Bump AGP version 2024-11-16 15:23:43 -07:00
a7d0119250
Fix some issues with desktop Linux 2024-07-14 20:32:36 -06:00
5df9a77714 Add icon for Windows 2023-09-06 21:16:16 -06:00
22d81d0fd5 Remove debug code from desktop 2023-09-06 21:15:56 -06:00
a5b1b33d99 Fix icon paths for windows and linux 2023-09-06 21:15:19 -06:00
3ffd663c8d Use Kotlin 1.9.10 2023-09-06 21:13:28 -06:00
3fd8b61043 Use gradle 8.1 2023-09-06 21:13:02 -06:00
6286016da2
fixup! Implement desktop builds 2023-08-22 22:14:47 -06:00
7bbbb022e3
Implement desktop builds 2023-08-22 22:13:17 -06:00
731faf7894 Bump version for release 2022-12-23 22:20:13 -07:00
fc3dbb7fb7 Fix shortcuts 2022-12-23 22:19:31 -07:00
ba830e8b67 Bump version for release 2022-12-23 21:29:50 -07:00
6c18dc6d1d Add Plausible analytics 2022-12-23 21:28:14 -07:00
f2fb3f6c2e Add more UI tests, fix shortcuts icon colors, fixes for Pi-hole FTL 5.18 auth requirements, improvements for error message visibility 2022-12-23 19:37:30 -07:00
c1216a58d3 Add some UI tests 2022-12-22 22:12:20 -07:00
8a2ae66b2f Material3, Disabled duration check, dependency updates
Got a bit carried away here 😅
2022-12-15 08:56:05 -06:00
c40e8f0274 Fix issues for iOS 2022-10-18 19:29:37 -06:00
120c155114 Fix issues for release builds 2022-06-25 14:18:08 -05:00
deff195d1f Add splash screen animation 2022-06-25 13:42:38 -05:00
a44e48d1e1 Migrate to redux-style store in shared module 2022-03-12 21:23:35 -07:00
fc06a2f91d Fix packages in shared module 2022-03-03 19:25:30 -07:00
6165886416 Finish implementation of MainScreen 2022-03-03 19:21:57 -07:00
2a6c469f07 Set navigation bar color according to theme (light or dark) 2022-03-03 18:56:16 -07:00
09f261b034 Migrate networking code to Kotlin Multiplatform compatible module 2022-03-03 18:55:41 -07:00
65838d6905 WIP: Migrate UI to Jetpack Compose 2021-06-10 21:20:31 -06:00
3f98551b9d Replace Koin with Dagger Hilt 2021-06-03 19:47:06 -06:00
bdacaaae34 Migrate dependencies to gradle version catalog 2021-06-04 09:07:06 -06:00
e345afb2b7 Migrate to Gradle Kotlin DSL 2021-06-04 08:58:55 -06:00
09ce5eebf7 Bump AGP version 2021-06-03 18:12:01 -06:00
72be201475 Update gradle version 2021-06-03 18:11:38 -06:00
131 changed files with 3744 additions and 2666 deletions

View 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

10
.gitignore vendored
View file

@ -1,15 +1,11 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.idea
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
keystore.properties
keystore.properties
.kotlin

View file

@ -1 +1 @@
Pi-Helper
Pi-helper

View 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&#10; 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>

View 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>

View file

@ -1,22 +1,6 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="java.util" alias="false" withSubpackages="false" />
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="1.8" />
<bytecodeTargetLevel target="21" />
</component>
</project>

View 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>

View file

@ -4,18 +4,18 @@
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="PLATFORM" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/piholeclient" />
<option value="$PROJECT_DIR$/desktop" />
<option value="$PROJECT_DIR$/shared" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
<option name="useQualifiedModuleNames" value="true" />
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>

View 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>

View file

@ -21,5 +21,20 @@
<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>

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinScriptingSettings">
<scriptDefinition className="org.jetbrains.kotlin.scripting.resolve.KotlinScriptDefinitionFromAnnotatedTemplate" definitionName="KotlinBuildScript">
<order>2147483647</order>
<autoReloadConfigurations>true</autoReloadConfigurations>
</scriptDefinition>
</component>
</project>

6
.idea/kotlinc.xml Normal file
View 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>

View file

@ -5,7 +5,67 @@
<configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" />
</configurations>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="DesignSurface">
<option name="filePathToZoomLevelMap">
<map>
<entry key="../../../../layout/compose-model-1622823449309.xml" value="0.1523829431438127" />
<entry key="../../../../layout/compose-model-1622824814438.xml" value="0.33" />
<entry key="../../../../layout/compose-model-1622917622395.xml" value="0.2170608108108108" />
<entry key="../../../../layout/compose-model-1623014109717.xml" value="0.337037037037037" />
<entry key="../../../../layout/compose-model-1623014799493.xml" value="0.10015558148580318" />
<entry key="../../../../layout/compose-model-1623015496605.xml" value="0.1" />
<entry key="../../../../layout/compose-model-1623015889966.xml" value="0.1072324414715719" />
<entry key="../../../../layout/compose-model-1623016793844.xml" value="0.1" />
<entry key="../../../../layout/compose-model-1623332506723.xml" value="0.1" />
<entry key="../../../../layout/compose-model-1623332556375.xml" value="0.1" />
<entry key="../../../../layout/compose-model-1623353007224.xml" value="0.1" />
<entry key="../../../../layout/compose-model-1623353013891.xml" value="2.0" />
<entry key="../../../../layout/compose-model-1623358629334.xml" value="0.10618436406067679" />
<entry key="../../../../layout/compose-model-1623358815178.xml" value="1.6830769230769231" />
<entry key="../../../../layout/compose-model-1623359670724.xml" value="0.11413043478260869" />
<entry key="../../../../layout/compose-model-1623360495184.xml" value="0.20880752102919348" />
<entry key="../../../../layout/compose-model-1646330539642.xml" value="0.1" />
<entry key="../../../../layout/compose-model-1646331413004.xml" value="0.4506079027355623" />
<entry key="../../../../layout/compose-model-1646332602415.xml" value="0.13775083612040134" />
<entry key="../../../../layout/compose-model-1646358533232.xml" value="0.30153061224489797" />
<entry key="../../../../layout/compose-model-1647060386627.xml" value="0.1" />
<entry key="../../../../layout/compose-model-1647060537103.xml" value="0.12374581939799331" />
<entry key="../../../../layout/compose-model-1647101512079.xml" value="0.12332775919732442" />
<entry key="../../../../layout/compose-model-1647102165810.xml" value="0.1" />
<entry key="app/src/main/res/drawable-v26/ic_shortcut_enable.xml" value="0.27447916666666666" />
<entry key="app/src/main/res/drawable-v26/ic_shortcut_pause.xml" value="0.27447916666666666" />
<entry key="app/src/main/res/drawable/background_splash.xml" value="0.409375" />
<entry key="app/src/main/res/drawable/horizontal_rule.xml" value="0.1" />
<entry key="app/src/main/res/drawable/ic_app_logo.xml" value="0.1065" />
<entry key="app/src/main/res/drawable/ic_launcher_background.xml" value="0.27447916666666666" />
<entry key="app/src/main/res/drawable/ic_pause.xml" value="0.409375" />
<entry key="app/src/main/res/drawable/ic_play.xml" value="0.27447916666666666" />
<entry key="app/src/main/res/drawable/ic_play_arrow.xml" value="0.27447916666666666" />
<entry key="app/src/main/res/drawable/ic_settings.xml" value="0.409375" />
<entry key="app/src/main/res/drawable/ic_shortcut_background.xml" value="0.27447916666666666" />
<entry key="app/src/main/res/drawable/ic_shortcut_enable.xml" value="0.409375" />
<entry key="app/src/main/res/drawable/ic_shortcut_pause.xml" value="0.409375" />
<entry key="app/src/main/res/layout/activity_main.xml" value="0.16111111111111112" />
<entry key="app/src/main/res/layout/dialog_disable_custom_time.xml" value="0.22708333333333333" />
<entry key="app/src/main/res/layout/fragment_add_pi_hole.xml" value="0.3493975903614458" />
<entry key="app/src/main/res/layout/fragment_info.xml" value="0.24010416666666667" />
<entry key="app/src/main/res/layout/fragment_main.xml" value="0.14175724637681159" />
<entry key="app/src/main/res/layout/fragment_retrieve_api_key.xml" value="0.24010416666666667" />
<entry key="app/src/main/res/layout/or_divider.xml" value="0.17147922998986828" />
<entry key="app/src/main/res/menu/main.xml" value="0.3932291666666667" />
</map>
</option>
</component>
<component name="EntryPointsManager">
<list size="1">
<item index="0" class="java.lang.String" itemvalue="dagger.hilt.android.testing.BindValue" />
</list>
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View file

@ -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
View 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>

View file

@ -2,5 +2,6 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/desktop" vcs="Git" />
</component>
</project>

View file

@ -1,75 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
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"
}
buildFeatures {
viewBinding true
}
}
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.2.0'
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.google.android.material:material:1.3.0-alpha04'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.security:security-crypto:1.0.0-rc01'
implementation 'androidx.preference:preference-ktx:1.1.1'
testImplementation 'junit:junit:4.13.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
def navigation_version = '2.3.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
View 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)
}

View file

@ -1,6 +1,6 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
# proguardFiles setting in build.gradle.kts.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

View file

@ -0,0 +1,3 @@
{
"status": "disabled"
}

View file

@ -0,0 +1,3 @@
{
"status": "enabled"
}

View 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
}
}
}

View 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
}
}
}

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View 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
}
}

View file

@ -0,0 +1,3 @@
{
"version": 3
}

View 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)
}
}

View 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)
}
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}
}

View file

@ -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() }

View file

@ -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)
}
}

View file

@ -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
}
}
}

View file

@ -0,0 +1,2 @@
package com.wbrawner.pihelper.util

View file

@ -1,21 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.wbrawner.pihelper">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".PiHelperApplication"
android:allowBackup="true"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_descriptor"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:usesCleartextTraffic="true"
android:supportsRtl="true"
android:theme="@style/AppTheme">
android:theme="@style/Theme.App.Starting">
<activity
android:name=".MainActivity"
android:windowSoftInputMode="adjustResize">
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

View file

@ -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
}
}

View file

@ -1,95 +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.lifecycle.lifecycleScope
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController
import com.wbrawner.pihelper.databinding.FragmentAddPiHoleBinding
import org.koin.android.ext.android.inject
class AddPiHoleFragment : Fragment() {
private val viewModel: AddPiHelperViewModel by inject()
private var _binding: FragmentAddPiHoleBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentAddPiHoleBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val navController = findNavController()
binding.scanNetworkButton.setOnClickListener {
navController.navigate(
R.id.action_addPiHoleFragment_to_scanNetworkFragment,
null,
null,
FragmentNavigatorExtras(binding.piHelperLogo to "piHelperLogo")
)
}
binding.ipAddress.setOnEditorActionListener { _, _, _ ->
binding.connectButton.performClick()
}
binding.connectButton.setSuspendingOnClickListener(lifecycleScope) {
showProgress(true)
if (viewModel.connectToIpAddress(binding.ipAddress.text.toString())) {
navController.navigate(
R.id.action_addPiHoleFragment_to_retrieveApiKeyFragment,
null,
null,
FragmentNavigatorExtras(binding.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) {
binding.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 {
binding.piHelperLogo.clearAnimation()
}
binding.connectionForm.visibility = if (show) View.GONE else View.VISIBLE
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View 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()
}
}

View file

@ -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

View file

@ -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) }
}

View file

@ -1,85 +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 com.wbrawner.pihelper.databinding.FragmentInfoBinding
import org.koin.android.ext.android.inject
class InfoFragment : Fragment() {
private val viewModel: AddPiHelperViewModel by inject()
private var _binding: FragmentInfoBinding? = null
private val binding get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentInfoBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
(activity as? AppCompatActivity)?.setSupportActionBar(binding.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")
binding.infoContent.text = if (Build.VERSION.SDK_INT < 24)
Html.fromHtml(html)
else
Html.fromHtml(html, 0)
binding.infoContent.movementMethod = LinkMovementMethod.getInstance()
binding.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)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View file

@ -1,89 +1,140 @@
package com.wbrawner.pihelper
import android.graphics.drawable.ColorDrawable
import android.animation.ObjectAnimator
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import android.view.View
import android.view.animation.AnticipateInterpolator
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.NavController
import androidx.navigation.findNavController
import com.wbrawner.pihelper.MainFragment.Companion.ACTION_DISABLE
import com.wbrawner.pihelper.MainFragment.Companion.ACTION_ENABLE
import com.wbrawner.pihelper.MainFragment.Companion.EXTRA_DURATION
import com.wbrawner.pihelper.databinding.ActivityMainBinding
import org.koin.android.ext.android.inject
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.runtime.*
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.animation.doOnEnd
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.wbrawner.pihelper.shared.Action
import com.wbrawner.pihelper.shared.Effect
import com.wbrawner.pihelper.shared.Route
import com.wbrawner.pihelper.shared.Store
import com.wbrawner.pihelper.shared.ui.AuthScreen
import com.wbrawner.pihelper.shared.ui.InfoScreen
import com.wbrawner.pihelper.shared.ui.MainScreen
import com.wbrawner.pihelper.shared.ui.component.LoadingSpinner
import com.wbrawner.pihelper.shared.ui.theme.PihelperTheme
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@ExperimentalAnimationApi
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val addPiHoleViewModel: AddPiHelperViewModel by inject()
private val navController: NavController by lazy {
findNavController(R.id.content_main)
}
@Inject
lateinit var store: Store
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
window.setBackgroundDrawable(ColorDrawable(getColor(R.color.colorSurface)))
val args = when (intent.action) {
ACTION_ENABLE -> {
if (addPiHoleViewModel.apiKey == null) {
Toast.makeText(this, R.string.configure_pihelper, Toast.LENGTH_SHORT).show()
null
} else {
Bundle().apply { putBoolean(ACTION_ENABLE, true) }
}
enableEdgeToEdge()
setContent {
BackHandler {
store.dispatch(Action.Back)
}
ACTION_DISABLE -> {
if (addPiHoleViewModel.apiKey == null) {
Toast.makeText(this, R.string.configure_pihelper, Toast.LENGTH_SHORT).show()
null
} else {
Bundle().apply {
putBoolean(ACTION_DISABLE, true)
putLong(EXTRA_DURATION, intent.getIntExtra(EXTRA_DURATION, 10).toLong())
val launchIntent = remember { intent }
LaunchedEffect(launchIntent) {
ShortcutActions.fromIntentAction(launchIntent.action)?.let { action ->
if (action == ShortcutActions.DISABLE) {
val duration = launchIntent.getIntExtra(DURATION, 0)
store.dispatch(Action.Disable(duration.toLong()))
} else {
store.dispatch(Action.Enable)
}
}
}
ACTION_FORGET_PIHOLE -> {
if (intent.component?.packageName == packageName) {
while (navController.popBackStack()) {
// Do nothing, just pop all the items off the back stack
val state by store.state.collectAsState()
val navController = rememberNavController()
LaunchedEffect(state.route) {
navController.navigate(state.route.name)
}
val effect by store.effects.collectAsState(initial = Effect.Empty)
LaunchedEffect(effect) {
when (effect) {
is Effect.Exit -> finish()
else -> {
// no-op
}
// Just return an empty bundle so that the navigation branch below will load
// the correct screen
Bundle()
} else {
null
}
}
else -> null
}
when {
navController.currentDestination?.id != R.id.placeholder && args == null -> {
return
}
addPiHoleViewModel.baseUrl.isNullOrBlank() -> {
navController.navigate(R.id.addPiHoleFragment, args)
}
addPiHoleViewModel.apiKey.isNullOrBlank() -> {
navController.navigate(R.id.addPiHoleFragment)
navController.navigate(R.id.retrieveApiKeyFragment, args)
}
else -> {
navController.navigate(R.id.mainFragment, args)
PihelperTheme {
NavHost(navController, startDestination = state.initialRoute.name) {
composable(Route.CONNECT.name) {
AddScreen(store)
}
composable(Route.SCAN.name) {
ScanScreen(store)
}
composable(Route.AUTH.name) {
AuthScreen(store)
}
composable(Route.HOME.name) {
MainScreen(store)
}
composable(Route.ABOUT.name) {
InfoScreen(store)
}
}
}
}
}
override fun onBackPressed() {
if (!navController.navigateUp()) {
finish()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
splashScreen.setOnExitAnimationListener { splashScreenView ->
listOf(View.SCALE_X, View.SCALE_Y).forEach { axis ->
ObjectAnimator.ofFloat(
splashScreenView,
axis,
1f,
0.45f
).apply {
interpolator = AnticipateInterpolator()
duration = 200L
doOnEnd {
splashScreenView.remove()
}
start()
}
}
}
}
if (navController.currentDestination?.id == R.id.placeholder) {
finish()
}
}
companion object {
const val ACTION_FORGET_PIHOLE = "com.wbrawner.pihelper.ACTION_FORGET_PIHOLE"
}
}
@Composable
@Preview
fun LoadingSpinner_Preview() {
LoadingSpinner()
}
enum class ShortcutActions(val fullName: String) {
ENABLE("com.wbrawner.pihelper.ShortcutActions.ENABLE"),
DISABLE("com.wbrawner.pihelper.ShortcutActions.DISABLE");
companion object {
fun fromIntentAction(action: String?): ShortcutActions? = when (action) {
ENABLE.fullName -> ENABLE
DISABLE.fullName -> DISABLE
else -> null
}
}
}
const val DURATION: String = "com.wbrawner.pihelper.MainActivityKt.DURATION"
@Composable
@DayNightPreview
fun InfoScreen_Preview() {
PihelperTheme {
InfoScreen({}, {})
}
}

View file

@ -1,214 +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 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.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.wbrawner.pihelper.databinding.DialogDisableCustomTimeBinding
import com.wbrawner.pihelper.databinding.FragmentMainBinding
import com.wbrawner.piholeclient.Status
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
class MainFragment : Fragment() {
private val viewModel: PiHelperViewModel by inject()
private var _binding: FragmentMainBinding? = null
private val binding get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
lifecycleScope.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, {
showProgress(false)
val (statusColor, statusText) = when (it) {
Status.DISABLED -> {
binding.enableButton.visibility = View.VISIBLE
binding.disableButtons.visibility = View.GONE
Pair(R.color.colorDisabled, R.string.status_disabled)
}
Status.ENABLED -> {
binding.enableButton.visibility = View.GONE
binding.disableButtons.visibility = View.VISIBLE
Pair(R.color.colorEnabled, R.string.status_enabled)
}
else -> {
binding.enableButton.visibility = View.GONE
binding.disableButtons.visibility = View.GONE
Pair(R.color.colorUnknown, R.string.status_unknown)
}
}
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(binding.status.context, statusColor))
binding.status.text = statusSpan
})
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentMainBinding.inflate(inflater, container, false)
return binding.root
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.main, menu)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
showProgress(true)
binding.enableButton.setSuspendingOnClickListener(lifecycleScope) {
showProgress(true)
try {
viewModel.enablePiHole()
} catch (ignored: Exception) {
Log.e("Pi-helper", "Failed to enable Pi-Hole", ignored)
}
}
binding.disable10SecondsButton.setSuspendingOnClickListener(lifecycleScope) {
showProgress(true)
try {
viewModel.disablePiHole(10)
} catch (ignored: Exception) {
Log.e("Pi-helper", "Failed to disable Pi-Hole", ignored)
}
}
binding.disable30SecondsButton.setSuspendingOnClickListener(lifecycleScope) {
showProgress(true)
try {
viewModel.disablePiHole(30)
} catch (ignored: Exception) {
Log.e("Pi-helper", "Failed to disable Pi-Hole", ignored)
}
}
binding.disable5MinutesButton.setSuspendingOnClickListener(lifecycleScope) {
showProgress(true)
try {
viewModel.disablePiHole(300)
} catch (ignored: Exception) {
Log.e("Pi-helper", "Failed to disable Pi-Hole", ignored)
}
}
binding.disableCustomTimeButton.setOnClickListener {
val dialogView = DialogDisableCustomTimeBinding.inflate(
LayoutInflater.from(it.context),
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.root)
.create()
.apply {
setOnShowListener {
getButton(AlertDialog.BUTTON_POSITIVE)
.setSuspendingOnClickListener(lifecycleScope) {
try {
val rawTime = dialogView.time
.text
.toString()
.toLong()
val computedTime =
when (dialogView.timeUnit.checkedRadioButtonId) {
R.id.seconds -> rawTime
R.id.minutes -> rawTime * 60
else -> rawTime * 3600
}
viewModel.disablePiHole(computedTime)
dismiss()
} catch (e: Exception) {
dialogView.time.error = "Failed to disable Pi-hole"
}
}
}
}
.show()
}
binding.disablePermanentlyButton.setSuspendingOnClickListener(lifecycleScope) {
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) {
binding.progressBar.visibility = if (show) {
binding.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 {
binding.progressBar.clearAnimation()
View.GONE
}
binding.statusContent.visibility = if (show) {
View.GONE
} else {
View.VISIBLE
}
}
override fun onDestroyView() {
_binding = null
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"
}
}

View file

@ -1,23 +1,7 @@
package com.wbrawner.pihelper
import android.app.Application
import android.content.Context
import com.wbrawner.piholeclient.piHoleClientModule
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
import dagger.hilt.android.HiltAndroidApp
@Suppress("unused")
class PiHelperApplication: Application() {
override fun onCreate() {
super.onCreate()
startKoin{
androidLogger()
androidContext(this@PiHelperApplication)
modules(listOf(
piHoleClientModule,
piHelperModule
))
}
}
}
@HiltAndroidApp
class PiHelperApplication : Application()

View file

@ -1,33 +1,38 @@
package com.wbrawner.pihelper
import androidx.security.crypto.EncryptedSharedPreferences
import com.wbrawner.piholeclient.NAME_BASE_URL
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.qualifier.named
import org.koin.dsl.module
import com.wbrawner.pihelper.shared.*
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
const val ENCRYPTED_SHARED_PREFS_FILE_NAME = "pihelper.prefs"
@Module
@InstallIn(SingletonComponent::class)
object PiHelperModule {
@Provides
@Singleton
fun providesPiholeAPIService(): PiholeAPIService = PiholeAPIService.create()
val piHelperModule = module {
single {
EncryptedSharedPreferences.create(
ENCRYPTED_SHARED_PREFS_FILE_NAME,
"pihelper",
get(),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
@Provides
@Singleton
fun providesAnalyticsHelper(): AnalyticsHelper = object : AnalyticsHelper {
override fun pageView(route: Route) {
// Not implemented
}
override fun event(
event: AnalyticsEvent,
route: Route
) {
// Not implemented
}
}
viewModel {
AddPiHelperViewModel(get(), get())
}
viewModel {
PiHelperViewModel(get())
}
single(named(NAME_BASE_URL)) {
get<EncryptedSharedPreferences>().getString(KEY_BASE_URL, "")
}
@Provides
@Singleton
fun providesStore(
apiService: PiholeAPIService,
analyticsHelper: AnalyticsHelper
): Store = Store(apiService, analyticsHelper)
}

View file

@ -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)
}
}
}

View file

@ -1,102 +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.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.transition.TransitionInflater
import com.wbrawner.pihelper.databinding.FragmentRetrieveApiKeyBinding
import org.koin.android.ext.android.inject
class RetrieveApiKeyFragment : Fragment() {
private val viewModel: AddPiHelperViewModel by inject()
private var _binding: FragmentRetrieveApiKeyBinding? = null
private val binding get() = _binding!!
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 {
_binding = FragmentRetrieveApiKeyBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.password.setOnEditorActionListener { _, _, _ ->
binding.connectWithPasswordButton.performClick()
}
binding.connectWithPasswordButton.setSuspendingOnClickListener(lifecycleScope) {
showProgress(true)
try {
viewModel.authenticateWithPassword(binding.password.text.toString())
} catch (ignored: Exception) {
Log.e("Pi-helper", "Failed to authenticate with password", ignored)
binding.password.error = "Failed to authenticate with given password. Please verify " +
"you've entered it correctly and try again."
showProgress(false)
}
}
binding.apiKey.setOnEditorActionListener { _, _, _ ->
binding.connectWithApiKeyButton.performClick()
}
binding.connectWithApiKeyButton.setSuspendingOnClickListener(lifecycleScope) {
showProgress(true)
try {
viewModel.authenticateWithApiKey(binding.apiKey.text.toString())
} catch (ignored: Exception) {
binding.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) {
binding.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 {
binding.piHelperLogo.clearAnimation()
}
binding.authenticationForm.visibility = if (show) View.GONE else View.VISIBLE
}
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
}

View file

@ -1,171 +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.lifecycle.lifecycleScope
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController
import androidx.transition.Transition
import androidx.transition.TransitionInflater
import com.wbrawner.pihelper.databinding.FragmentScanNetworkBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import java.net.Inet4Address
class ScanNetworkFragment : Fragment() {
private val viewModel: AddPiHelperViewModel by inject()
private var _binding: FragmentScanNetworkBinding? = null
private val binding get() = _binding!!
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 {
_binding = FragmentScanNetworkBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.scanningIp.observe(viewLifecycleOwner, Observer {
binding.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
}
binding.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()
})
lifecycleScope.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)
}
}
}
}
lifecycleScope.launch {
delay(500)
if (binding.piHelperLogo.animation == null) {
animatePiHelperLogo()
}
}
}
private fun navigateToApiKeyScreen() {
val extras = FragmentNavigatorExtras(
binding.piHelperLogo to "piHelperLogo"
)
findNavController().navigate(
R.id.action_scanNetworkFragment_to_retrieveApiKeyFragment,
null,
null,
extras
)
}
override fun onDestroyView() {
binding.piHelperLogo.clearAnimation()
_binding = null
super.onDestroyView()
}
private fun animatePiHelperLogo() {
binding.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
}
)
}
}

View 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")
}
}

View 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>

View 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>

View file

@ -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>

View file

@ -4,6 +4,6 @@
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/colorRedDark"
android:fillColor="@color/colorWhite"
android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
</vector>

View 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>

View file

@ -4,6 +4,6 @@
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/colorGreenDark"
android:fillColor="@color/colorWhite"
android:pathData="M8,5v14l11,-7z"/>
</vector>

View 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>

View file

@ -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>

View file

@ -2,8 +2,8 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
android:shape="oval"
android:tint="@color/colorWhite" />
android:shape="rectangle"
android:tint="@color/colorSurface" />
</item>
<item
android:bottom="0dp"

View file

@ -2,8 +2,8 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
android:shape="oval"
android:tint="@color/colorWhite" />
android:shape="rectangle"
android:tint="@color/colorSurface" />
</item>
<item
android:bottom="0dp"

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<fragment 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/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"
tools:background="@color/colorSurface" />

View file

@ -1,50 +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" />
<RadioButton
android:id="@+id/hours"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/duration_hours" />
</RadioGroup>
</LinearLayout>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View file

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View file

@ -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>

View file

@ -1,8 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorSurface">#333333</color>
<color name="colorSurface">#000000</color>
<color name="colorOnSurface">#f1f1f1</color>
<color name="colorEnabled">@color/colorGreenLight</color>
<color name="colorDisabled">@color/colorRedLight</color>
<color name="colorButtonSecondary">#999999</color>
</resources>

View file

@ -1,13 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="BaseTheme">
<item name="android:statusBarColor">@color/colorTransparent</item>
<item name="android:windowLightStatusBar">false</item>
</style>
<style name="AppTheme.Button.Red" parent="Widget.MaterialComponents.Button">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="backgroundTint">@color/colorRedLight</item>
<item name="android:textColor">@color/colorWhite</item>
</style>
</resources>

View file

@ -1,18 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">@color/colorRedLight</color>
<color name="colorPrimary">@color/colorAccent</color>
<color name="colorPrimaryDark">@color/colorRedDark</color>
<color name="colorAccent">@color/colorRedLight</color>
<color name="colorRedLight">#f60d1a</color>
<color name="colorRedDark">#96060c</color>
<color name="colorGreenLight">#30d158</color>
<color name="colorGreenDark">#34c759</color>
<color name="colorRedLight">#F44336</color>
<color name="colorRedDark">#B71C1C</color>
<color name="colorGreenLight">#4CAF50</color>
<color name="colorGreenDark">#1B5E20</color>
<color name="colorShortcutBackground">#FFFFFFFF</color>
<color name="colorOnSurface">#000000</color>
<color name="colorWhite">#ffffff</color>
<color name="colorEnabled">@color/colorGreenDark</color>
<color name="colorUnknown">@color/colorButtonSecondary</color>
<color name="colorDisabled">@color/colorRedDark</color>
<color name="colorWhite">#FFFBFE</color>
<color name="colorSurface">@color/colorWhite</color>
<color name="colorTransparent">#00000000</color>
<color name="colorButtonSecondary">#666666</color>
</resources>
</resources>

View file

@ -1,50 +1,10 @@
<resources>
<string name="app_name">Pi-helper</string>
<string name="scanning_ip_address">Scanning IP Address:</string>
<string name="accessibility_description_pi_helper_logo">Pi-helper Logo</string>
<string name="label_status">Status: %1$s</string>
<string name="action_enable">Enable</string>
<string name="action_disable">Disable</string>
<string name="action_disable_10_seconds">Disable for 10 seconds</string>
<string name="action_disable_10_seconds_short">Disable for 10 seconds</string>
<string name="action_disable_30_seconds">Disable for 30 seconds</string>
<string name="action_disable_30_seconds_short">Disable for 30 seconds</string>
<string name="action_disable_5_minutes">Disable for 5 minutes</string>
<string name="action_disable_5_minutes_short">Disable for 5 minutes</string>
<string name="action_disable_custom">Disable for custom time</string>
<string name="action_disable_permanently">Disable Permanently</string>
<string name="status_disabled">Disabled</string>
<string name="status_enabled">Enabled</string>
<string name="scan_failed">Please ensure you are connected to the same Wi-Fi network that the Pi-Hole is running on and try again, or enter the Pi-Hole\'s IP address manually.</string>
<string name="scan_failed_title">Pi-helper failed to find your Pi-Hole</string>
<string name="connection_failed">Please ensure you are connected to the same Wi-Fi network that the Pi-Hole is running on, and that you\'re using the correct IP address and try again.</string>
<string name="connection_failed_title">Pi-helper failed to connect to your Pi-Hole</string>
<string name="configure_pihelper">Please configure Pi-helper before using shortcuts</string>
<string name="or">or</string>
<string name="action_settings">Settings</string>
<string name="action_forget_pihole">Forget Pi-hole</string>
<string name="content_info"><![CDATA[Pi-helper was made with ❤ by <a href=\"https://wbrawner.com\">William Brawner</a>. You can find the source code or report issues on the <a href=\"https://github.com/wbrawner/PiHelperAndroid\">GitHub page</a> for the project.]]></string>
<string name="confirm_forget_pihole">Are you sure you want to forget your Pi-hole?</string>
<string name="warning_cannot_be_undone">This cannot be undone.</string>
<string name="title_crash_notification">Pi-helper Crashed!</string>
<string name="text_crash_notification">Would you please consider sending the crash report to me?</string>
<string name="channel_crash_notification">Crash Reports</string>
<string name="status_unknown">Unknown</string>
<string name="duration_seconds">Secs</string>
<string name="duration_minutes">Mins</string>
<string name="hint_disable_duration">Time to disable</string>
<string name="info_scan_network">If you\'re not sure what the IP address for your Pi-Hole is, Pi-helper can attempt to find it for you by scanning your network.</string>
<string name="action_scan_network">Scan Network</string>
<string name="info_connect">If you already know the IP address or host of your Pi-Hole, you can also enter it below:</string>
<string name="prompt_ip_address">Pi-Hole IP Address/Host</string>
<string name="action_connect_pihole">Connect to Pi-Hole</string>
<string name="info_connection_success">Pi-helper has successfully connected to your Pi-Hole!</string>
<string name="info_authentication_required">You\'ll need to authenticate in order to enable and disable the Pi-hole.</string>
<string name="prompt_password">Pi-Hole Web Password</string>
<string name="action_authenticate_password">Authenticate with Password</string>
<string name="prompt_api_key">Pi-Hole API Key</string>
<string name="action_authenticate_api_key">Authenticate with API Key</string>
<string name="connecting_to_pihole">Connecting to Pi-hole…</string>
<string name="action_cancel">Cancel</string>
<string name="duration_hours">Hours</string>
</resources>

View file

@ -4,25 +4,17 @@
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:windowBackground">@drawable/background_splash</item>
<item name="android:statusBarColor">@color/colorTransparent</item>
<item name="android:textColor">@color/colorOnSurface</item>
<item name="android:statusBarColor">@color/colorWhite</item>
</style>
<style name="AppTheme" parent="BaseTheme">
<item name="android:windowLightStatusBar">true</item>
</style>
<style name="AppTheme.Button" parent="Widget.MaterialComponents.Button">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textColor">@color/colorWhite</item>
<style name="Theme.App.Starting" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/colorSurface</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_app_logo</item>
<item name="postSplashScreenTheme">@style/AppTheme</item>
</style>
<style name="AppTheme.Button.Green" parent="AppTheme.Button">
<item name="backgroundTint">@color/colorGreenDark</item>
</style>
<style name="AppTheme.Button.Red" parent="AppTheme.Button">
<item name="backgroundTint">@color/colorAccent</item>
</style>
</resources>

View file

@ -7,11 +7,11 @@
android:shortcutLongLabel="@string/action_disable_5_minutes"
android:shortcutShortLabel="@string/action_disable_5_minutes_short">
<intent
android:action="com.wbrawner.pihelper.MainFragment.ACTION_DISABLE"
android:action="com.wbrawner.pihelper.ShortcutActions.DISABLE"
android:targetClass="com.wbrawner.pihelper.MainActivity"
android:targetPackage="com.wbrawner.pihelper">
<extra
android:name="com.wbrawner.pihelper.MainFragment.EXTRA_DURATION"
android:name="com.wbrawner.pihelper.MainActivityKt.DURATION"
android:value="300" />
</intent>
</shortcut>
@ -23,11 +23,11 @@
android:shortcutLongLabel="@string/action_disable_30_seconds"
android:shortcutShortLabel="@string/action_disable_30_seconds_short">
<intent
android:action="com.wbrawner.pihelper.MainFragment.ACTION_DISABLE"
android:action="com.wbrawner.pihelper.ShortcutActions.DISABLE"
android:targetClass="com.wbrawner.pihelper.MainActivity"
android:targetPackage="com.wbrawner.pihelper">
<extra
android:name="com.wbrawner.pihelper.MainFragment.EXTRA_DURATION"
android:name="com.wbrawner.pihelper.MainActivityKt.DURATION"
android:value="30" />
</intent>
</shortcut>
@ -39,11 +39,11 @@
android:shortcutLongLabel="@string/action_disable_10_seconds"
android:shortcutShortLabel="@string/action_disable_10_seconds_short">
<intent
android:action="com.wbrawner.pihelper.MainFragment.ACTION_DISABLE"
android:action="com.wbrawner.pihelper.ShortcutActions.DISABLE"
android:targetClass="com.wbrawner.pihelper.MainActivity"
android:targetPackage="com.wbrawner.pihelper">
<extra
android:name="com.wbrawner.pihelper.MainFragment.EXTRA_DURATION"
android:name="com.wbrawner.pihelper.MainActivityKt.DURATION"
android:value="10" />
</intent>
</shortcut>
@ -55,7 +55,7 @@
android:shortcutLongLabel="@string/action_enable"
android:shortcutShortLabel="@string/action_enable">
<intent
android:action="com.wbrawner.pihelper.MainFragment.ACTION_ENABLE"
android:action="com.wbrawner.pihelper.ShortcutActions.ENABLE"
android:targetClass="com.wbrawner.pihelper.MainActivity"
android:targetPackage="com.wbrawner.pihelper" />
</shortcut>

View file

@ -4,11 +4,11 @@
android:enabled="true"
android:icon="@drawable/ic_shortcut_pause">
<intent
android:action="com.wbrawner.pihelper.MainFragment.ACTION_DISABLE"
android:action="com.wbrawner.pihelper.ShortcutActions.DISABLE"
android:targetClass="com.wbrawner.pihelper.MainActivity"
android:targetPackage="com.wbrawner.pihelper">
<extra
android:name="com.wbrawner.pihelper.MainFragment.EXTRA_DURATION"
android:name="com.wbrawner.pihelper.MainActivityKt.DURATION"
android:value="300" />
</intent>
</shortcut>
@ -17,11 +17,11 @@
android:enabled="true"
android:icon="@drawable/ic_shortcut_pause">
<intent
android:action="com.wbrawner.pihelper.MainFragment.ACTION_DISABLE"
android:action="com.wbrawner.pihelper.ShortcutActions.DISABLE"
android:targetClass="com.wbrawner.pihelper.MainActivity"
android:targetPackage="com.wbrawner.pihelper">
<extra
android:name="com.wbrawner.pihelper.MainFragment.EXTRA_DURATION"
android:name="com.wbrawner.pihelper.MainActivityKt.DURATION"
android:value="30" />
</intent>
</shortcut>
@ -30,11 +30,11 @@
android:enabled="true"
android:icon="@drawable/ic_shortcut_pause">
<intent
android:action="com.wbrawner.pihelper.MainFragment.ACTION_DISABLE"
android:action="com.wbrawner.pihelper.ShortcutActions.DISABLE"
android:targetClass="com.wbrawner.pihelper.MainActivity"
android:targetPackage="com.wbrawner.pihelper">
<extra
android:name="com.wbrawner.pihelper.MainFragment.EXTRA_DURATION"
android:name="com.wbrawner.pihelper.MainActivityKt.DURATION"
android:value="10" />
</intent>
</shortcut>
@ -43,7 +43,7 @@
android:enabled="true"
android:icon="@drawable/ic_shortcut_enable">
<intent
android:action="com.wbrawner.pihelper.MainFragment.ACTION_ENABLE"
android:action="com.wbrawner.pihelper.ShortcutActions.ENABLE"
android:targetClass="com.wbrawner.pihelper.MainActivity"
android:targetPackage="com.wbrawner.pihelper" />
</shortcut>

View file

@ -1,25 +0,0 @@
buildscript {
ext.kotlin_version = '1.4.20'
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:4.1.1'
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
View 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
View file

@ -0,0 +1,2 @@
/build
/release

View 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
View 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"))
}
}
}
}

View 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)
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -19,3 +19,6 @@ android.useAndroidX=true
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false

82
gradle/libs.versions.toml Normal file
View 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.0.21-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" }

View file

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip

6
gradlew vendored
View file

@ -82,7 +82,7 @@ location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
which kotlin >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
@ -109,7 +109,7 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
# For Cygwin, switch paths to Windows format before running kotlin
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
@ -161,7 +161,7 @@ save () {
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
# Collect all arguments for the kotlin command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong

View file

@ -1 +0,0 @@
/build

Some files were not shown because too many files have changed in this diff Show more