Compare commits
11 Commits
22331ed364
...
feature/sy
| Author | SHA1 | Date | |
|---|---|---|---|
|
b2cb54e32b
|
|||
| b7232b12cd | |||
| 8cd5b93b47 | |||
| 1b07efe291 | |||
| 232b25bed4 | |||
| 2ea6fe0570 | |||
| bb8b742dc4 | |||
| 85b52a9840 | |||
| 774412492f | |||
| 370ddfb8db | |||
| 1567e2c0ad |
6
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "gradle"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "monthly"
|
||||||
7
.github/workflows/main.yml
vendored
@@ -9,20 +9,19 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
Test:
|
Test:
|
||||||
runs-on: self-hosted
|
runs-on: self-hosted
|
||||||
timeout-minutes: 30
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out source code
|
- name: Check out source code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v1
|
||||||
|
|
||||||
- name: Build project
|
- name: Build project
|
||||||
run: ./build.sh build
|
run: ./build.sh build
|
||||||
|
|
||||||
- name: Run Android tests
|
- name: Run Android tests
|
||||||
run: ./build.sh android-tests-parallel 28 29 30 32 33 34
|
run: ./build.sh android-tests-parallel 28 29 30 31 32 33
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: build
|
name: build
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
1
.gitignore
vendored
@@ -17,4 +17,3 @@ node_modules
|
|||||||
*xcuserdata*
|
*xcuserdata*
|
||||||
*.sketch
|
*.sketch
|
||||||
crowdin.yml
|
crowdin.yml
|
||||||
kotlin-js-store
|
|
||||||
|
|||||||
36
CHANGELOG.md
@@ -1,41 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [2.2.0] -- 2024-01-30
|
|
||||||
### Added
|
|
||||||
- Add support for Android 14 (@iSoron, @hiqua)
|
|
||||||
- Allow user to change app language (@leondzn)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Implement workaround to make notifications non-dismissible in Android 14 (@iSoron, #1872)
|
|
||||||
- Fix splash screen background color in dark mode (@SIKV, #1888)
|
|
||||||
|
|
||||||
## [2.1.3] -- 2023-08-28
|
|
||||||
### Fixed
|
|
||||||
- Use text input on Samsung devices (@iSoron, #1719)
|
|
||||||
- Prevent crash if alarm permission is revoked (@iSoron)
|
|
||||||
- Adjust widget colors (@iSoron)
|
|
||||||
- Fix bug preventing screens from updating at midnight (@iSoron)
|
|
||||||
- Fix skip button in locales that use comma instead of dot (@iSoron, #1721)
|
|
||||||
|
|
||||||
## [2.1.2] -- 2023-05-26
|
|
||||||
### Fixed
|
|
||||||
- Fix bug that caused widget to enter checkmark on wrong date (@iSoron, #1541)
|
|
||||||
- Fix widget corners on Android 12 (@iSoron)
|
|
||||||
- Fix bug that caused notes to be lost when editing a checkmark (@iSoron, #1566)
|
|
||||||
- Prevent soft keyboard from covering entry popup (@iSoron)
|
|
||||||
- Accept comma (instead of dot) in certain locales (@iSoron)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Remove update delay after entering a checkmark (@iSoron)
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
- Remove stack widgets (@iSoron)
|
|
||||||
|
|
||||||
|
|
||||||
## [2.1.1] -- 2022-09-24
|
|
||||||
### Fixed
|
|
||||||
- Fix Tasker plugin (@iSoron, #1503)
|
|
||||||
|
|
||||||
## [2.1.0] -- 2022-09-10
|
## [2.1.0] -- 2022-09-10
|
||||||
### Added
|
### Added
|
||||||
- Allow user to add notes to specific dates (@vbh, #1103)
|
- Allow user to add notes to specific dates (@vbh, #1103)
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ show you how your habits improved over time. It is completely ad-free and open
|
|||||||
source.
|
source.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://play.google.com/store/apps/details?id=org.isoron.uhabits&utm_source=global_co&utm_medium=prtnr&utm_content=Mar2515&utm_campaign=PartBadge&pcampaignid=MKT-AC-global-none-all-co-pr-py-PartBadges-Oct1515-1"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" height="80px"/></a>
|
<a href="https://play.google.com/store/apps/details?id=org.isoron.uhabits&utm_source=global_co&utm_medium=prtnr&utm_content=Mar2515&utm_campaign=PartBadge&pcampaignid=MKT-AC-global-none-all-co-pr-py-PartBadges-Oct1515-1"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/apps/en-play-badge-border.png" height="75px"/></a>
|
||||||
<a href="https://f-droid.org/app/org.isoron.uhabits"><img alt="Get it on F-Droid" src="https://f-droid.org/badge/get-it-on.png" height="80px"/></a>
|
<a href="https://f-droid.org/app/org.isoron.uhabits"><img alt="Get it on F-Droid" src="https://i.imgur.com/baSPE7X.png" height="75px"/></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
plugins {
|
plugins {
|
||||||
val kotlinVersion = "1.9.22"
|
val kotlinVersion = "1.7.10"
|
||||||
id("com.android.application") version "8.4.0" apply (false)
|
id("com.android.application") version ("7.3.0-rc01") apply (false)
|
||||||
id("org.jetbrains.kotlin.android") version kotlinVersion apply (false)
|
id("org.jetbrains.kotlin.android") version kotlinVersion apply (false)
|
||||||
id("org.jetbrains.kotlin.kapt") version kotlinVersion apply (false)
|
id("org.jetbrains.kotlin.kapt") version kotlinVersion apply (false)
|
||||||
|
id("org.jetbrains.kotlin.android.extensions") version kotlinVersion apply (false)
|
||||||
id("org.jetbrains.kotlin.multiplatform") version kotlinVersion apply (false)
|
id("org.jetbrains.kotlin.multiplatform") version kotlinVersion apply (false)
|
||||||
id("org.jlleitschuh.gradle.ktlint") version "11.6.1"
|
id("org.jlleitschuh.gradle.ktlint") version "11.0.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply {
|
apply {
|
||||||
|
|||||||
19
build.sh
@@ -64,7 +64,6 @@ fail() {
|
|||||||
core_build() {
|
core_build() {
|
||||||
log_info "Building uhabits-core..."
|
log_info "Building uhabits-core..."
|
||||||
$GRADLE ktlintCheck || fail
|
$GRADLE ktlintCheck || fail
|
||||||
$GRADLE lintDebug || fail
|
|
||||||
$GRADLE :uhabits-core:build || fail
|
$GRADLE :uhabits-core:build || fail
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +181,7 @@ android_test() {
|
|||||||
OUT_INSTRUMENT=${ANDROID_OUTPUTS_DIR}/instrument-${API}.txt
|
OUT_INSTRUMENT=${ANDROID_OUTPUTS_DIR}/instrument-${API}.txt
|
||||||
OUT_LOGCAT=${ANDROID_OUTPUTS_DIR}/logcat-${API}.txt
|
OUT_LOGCAT=${ANDROID_OUTPUTS_DIR}/logcat-${API}.txt
|
||||||
FAILED_TESTS=""
|
FAILED_TESTS=""
|
||||||
for i in {1..10}; do
|
for i in {1..5}; do
|
||||||
log_info "Running $size instrumented tests (attempt $i)..."
|
log_info "Running $size instrumented tests (attempt $i)..."
|
||||||
$ADB shell am instrument \
|
$ADB shell am instrument \
|
||||||
-r -e coverage true -e size "$size" $FAILED_TESTS \
|
-r -e coverage true -e size "$size" $FAILED_TESTS \
|
||||||
@@ -218,28 +217,20 @@ android_test_parallel() {
|
|||||||
(
|
(
|
||||||
LOG=build/android-test-$API.log
|
LOG=build/android-test-$API.log
|
||||||
log_info "API $API: Running tests..."
|
log_info "API $API: Running tests..."
|
||||||
android_test $API 1>$LOG 2>&1
|
if android_test $API 1>$LOG 2>&1; then
|
||||||
ret_code=$?
|
|
||||||
if [ $ret_code = 0 ]; then
|
|
||||||
log_info "API $API: Passed"
|
log_info "API $API: Passed"
|
||||||
else
|
else
|
||||||
log_error "API $API: Failed"
|
log_error "API $API: Failed"
|
||||||
fi
|
fi
|
||||||
pkill -9 -f ${AVD_PREFIX}${API}
|
pkill -9 -f ${AVD_PREFIX}${API}
|
||||||
exit $ret_code
|
|
||||||
)&
|
)&
|
||||||
PIDS+=" $!"
|
PIDS+=" $!"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Check exit codes
|
# Check exit codes
|
||||||
success=0
|
RET_CODE=0
|
||||||
for pid in $PIDS; do
|
for pid in $PIDS; do
|
||||||
wait $pid
|
wait $pid || RET_CODE=1
|
||||||
ret_code=$?
|
|
||||||
if [ $ret_code != 0 ]; then
|
|
||||||
success=1
|
|
||||||
fi
|
|
||||||
echo pid=$pid ret_code=$ret_code success=$success
|
|
||||||
done
|
done
|
||||||
|
|
||||||
# Print all logs
|
# Print all logs
|
||||||
@@ -249,7 +240,7 @@ android_test_parallel() {
|
|||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
done
|
done
|
||||||
|
|
||||||
return $success
|
return $RET_CODE
|
||||||
}
|
}
|
||||||
|
|
||||||
android_build() {
|
android_build() {
|
||||||
|
|||||||
54
docs/TEST.md
@@ -2,25 +2,51 @@
|
|||||||
|
|
||||||
Loop Habit Tracker has a fairly large number of automated tests to reduce the chance of bugs being silently introduced in our code base. The tests are divided into three categories:
|
Loop Habit Tracker has a fairly large number of automated tests to reduce the chance of bugs being silently introduced in our code base. The tests are divided into three categories:
|
||||||
|
|
||||||
- **Unit tests:** These tests run very quickly on the developer's computer, inside a JVM, and do not need an Android emulator or device. They typically test the correctness of core functions of the application, such as the computation of scores and streaks.
|
* **Small tests:** These tests run very quickly on the developer's computer, inside a JVM, and do not need an Android emulator or device. They typically test the correctness of core functions of the application, such as the computation of scores and streaks.
|
||||||
- **Instrumented tests:** These tests require an Android emulator or device. _Medium_ instrumented tests are still quite fast to run, since only individual classes are tested. The app itself does not need to be launched. Examples include _view tests_, which render our custom views on the device and compare them against prerendered images. _Large_ instrumented tests launch the application on an Android emulator and interact with it by touching the screen, much like a regular user.
|
* **Medium tests:** These tests require an Android emulator or device, but they are still quite fast to run, since only individual classes are tested. The app itself does not need to be launched. Examples include *view tests*, which render our custom views on the device and compare them against prerendered images.
|
||||||
|
* **Large tests:** These are end-to-end tests, which launch the application on an Android emulator and interact with it by touching the screen, much like a regular user.
|
||||||
|
|
||||||
## Running unit tests
|
## Running small tests
|
||||||
|
|
||||||
Unit tests can be launched by running `./gradlew test` or by right-clicking a particular class/method in Android Studio and selecting "Run testMethod()" or "Run ClassTest". An alternative way is to use `build.sh`, the script used by our continuous integration server. By running `./build.sh build`, the script will automatically build and run all small tests.
|
Small tests can be launched by running `./gradlew test` or by right-clicking a particular class/method in Android Studio and selecting "Run testMethod()" or "Run ClassTest". An alternative way is to use `build.sh`, the script used by our continuous integration server. By running `./build.sh build`, the script will automatically build and run all small tests.
|
||||||
|
|
||||||
## Running instrumented tests
|
## Running medium tests
|
||||||
|
|
||||||
To run medium tests, it is recommended to use the `build.sh` script.
|
To run medium tests, it is recommended to use the `build.sh` script:
|
||||||
|
|
||||||
1. Run `./build.sh android-setup API` to create the emulator, where `API` is the desired API level.
|
./build.sh build
|
||||||
2. Run `./build.sh android-tests API` to run the tests on a single API.
|
./build.sh medium-tests
|
||||||
3. Run `./build.sh android-tests-parallel API API...` to run the tests on multiple APIs in parallel.
|
|
||||||
|
|
||||||
Note that instrumented tests are designed to run on a clean install, inside an emulator. They will not work on actual devices. All tests are also designed for a particular screen size, namely the Nexus 4 configuration (4.7" 768x1280 xhdpi), and a particular locale, namely English (US). Furthermore:
|
|
||||||
|
|
||||||
- No additional apps should be installed on the device;
|
For this script to succeed, make sure that an emulator is currently running, or that a device (with developer mode activated) is connected via USB.
|
||||||
- The homescreen must look exactly like it was when the emulator was originally created, with no additional icons or widgets;
|
|
||||||
- All animations must be manually disabled.
|
|
||||||
|
|
||||||
If there are failing view tests (that is, if some custom views do not render exactly like the prerendered images we have), then both the actual and expected images will be automatically downloaded from the device to the folder `uhabits-android/build/outputs`. After verifying the differences, if you feel that the actual images are actually fine and should replace the prerendered ones, then run `./build.sh android-accept-images`.
|
**WARNING!** This script will uninstall the app prior to testing it, and therefore delete all user data!
|
||||||
|
|
||||||
|
|
||||||
|
If there are failing view tests (that is, if some custom views do not render exactly like the prerendered images we have), then the script `./build.sh fetch-images` can be used to download both the actual and the expected images from the device. The images will be downloaded from the device into the folder `tmp/`. After verifying the differences, if you feel that the actual images are actually fine and should replace the prerendered ones, then run `./build.sh accept-images`.
|
||||||
|
|
||||||
|
## Running large tests
|
||||||
|
|
||||||
|
Large tests are significantly more complicated to run. In particular, they require:
|
||||||
|
|
||||||
|
* An Android emulator; they will **not** work on actual devices;
|
||||||
|
* A vanilla x86 AOSP image; they will **not** work with Google API images;
|
||||||
|
* A particular screen size, namely the Nexus 4 configuration on Android Studio (4.7 768x1280 xhdpi);
|
||||||
|
* A particular locale, namely English (US).
|
||||||
|
|
||||||
|
Furthermore:
|
||||||
|
|
||||||
|
* No additional apps should be installed on the device;
|
||||||
|
* The homescreen must look exactly like it was when the emulator was originally created, with no additional icons or widgets;
|
||||||
|
* Developer mode must be activated, and all animations must be manually disabled.
|
||||||
|
|
||||||
|
Only the following Android versions are supported by our test suite:
|
||||||
|
|
||||||
|
* Android 7.0 (API 24)
|
||||||
|
* Android 7.1.1 (API 25)
|
||||||
|
* Android 8.0 (API 26)
|
||||||
|
* Android 8.1 (API 27)
|
||||||
|
* Android 9.0 (API 28)
|
||||||
|
* Android 10.0 (API 29)
|
||||||
|
|
||||||
|
After creating an emulator and configuring it exactly as described above, launch it, wait for it to finish booting up, then run `./build.sh large-tests`. As mentioned before, this script will uninstall the app before testing it, and therefore will delete all the user data.
|
||||||
|
|||||||
@@ -3,6 +3,3 @@ org.gradle.daemon=true
|
|||||||
org.gradle.jvmargs=-Xms2048m -Xmx2048m
|
org.gradle.jvmargs=-Xms2048m -Xmx2048m
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
android.defaults.buildfeatures.buildconfig=true
|
|
||||||
android.nonTransitiveRClass=false
|
|
||||||
android.nonFinalResIds=false
|
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -18,10 +18,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.github.triplet.play") version "3.8.6"
|
id("com.github.triplet.play") version "3.7.0"
|
||||||
id("com.android.application") version "8.4.0"
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
id("org.jetbrains.kotlin.kapt")
|
id("org.jetbrains.kotlin.kapt")
|
||||||
|
id("org.jetbrains.kotlin.android.extensions")
|
||||||
id("org.jlleitschuh.gradle.ktlint")
|
id("org.jlleitschuh.gradle.ktlint")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,29 +30,15 @@ tasks.compileLint {
|
|||||||
dependsOn("updateTranslators")
|
dependsOn("updateTranslators")
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
Added on top of kotlinOptions to work around this issue:
|
|
||||||
https://youtrack.jetbrains.com/issue/KTIJ-24311/task-current-target-is-17-and-kaptGenerateStubsProductionDebugKotlin-task-current-target-is-1.8-jvm-target-compatibility-should#focus=Comments-27-6798448.0-0
|
|
||||||
Updating gradle might fix this, so try again in the future to remove this and run:
|
|
||||||
./gradlew --rerun-tasks :uhabits-android:kaptGenerateStubsReleaseKotlin
|
|
||||||
If this doesn't produce any warning, try to remove it.
|
|
||||||
*/
|
|
||||||
kotlin {
|
|
||||||
jvmToolchain(11)
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
||||||
namespace = "org.isoron.uhabits"
|
compileSdk = 32
|
||||||
compileSdk = 34
|
|
||||||
// compileSdkPreview = "VanillaIceCream"
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
versionCode = 20200
|
versionCode = 20100
|
||||||
versionName = "2.2.0"
|
versionName = "2.1.0"
|
||||||
minSdk = 28
|
minSdk = 28
|
||||||
targetSdk = 34
|
targetSdk = 31
|
||||||
// targetSdkPreview = "VanillaIceCream"
|
|
||||||
applicationId = "org.isoron.uhabits"
|
applicationId = "org.isoron.uhabits"
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
@@ -83,28 +70,21 @@ android {
|
|||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
isCoreLibraryDesugaringEnabled = true
|
isCoreLibraryDesugaringEnabled = true
|
||||||
targetCompatibility(JavaVersion.VERSION_11)
|
targetCompatibility(JavaVersion.VERSION_1_8)
|
||||||
sourceCompatibility(JavaVersion.VERSION_11)
|
sourceCompatibility(JavaVersion.VERSION_1_8)
|
||||||
}
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
}
|
}
|
||||||
|
|
||||||
lint {
|
|
||||||
abortOnError = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
val daggerVersion = "2.51.1"
|
val daggerVersion = "2.43.2"
|
||||||
val kotlinVersion = "1.9.22"
|
val kotlinVersion = "1.7.10"
|
||||||
val kxCoroutinesVersion = "1.7.3"
|
val kxCoroutinesVersion = "1.6.4"
|
||||||
val ktorVersion = "1.6.8"
|
val ktorVersion = "1.6.8"
|
||||||
val espressoVersion = "3.5.1"
|
val espressoVersion = "3.4.0"
|
||||||
|
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion")
|
androidTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion")
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:$espressoVersion")
|
androidTestImplementation("androidx.test.espresso:espresso-core:$espressoVersion")
|
||||||
@@ -112,17 +92,17 @@ dependencies {
|
|||||||
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.3")
|
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.3")
|
||||||
androidTestImplementation("io.ktor:ktor-client-mock:$ktorVersion")
|
androidTestImplementation("io.ktor:ktor-client-mock:$ktorVersion")
|
||||||
androidTestImplementation("io.ktor:ktor-jackson:$ktorVersion")
|
androidTestImplementation("io.ktor:ktor-jackson:$ktorVersion")
|
||||||
androidTestImplementation("androidx.annotation:annotation:1.7.1")
|
androidTestImplementation("androidx.annotation:annotation:1.4.0")
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
||||||
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
|
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
|
||||||
androidTestImplementation("androidx.test:rules:1.5.0")
|
androidTestImplementation("androidx.test:rules:1.4.0")
|
||||||
androidTestImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
|
androidTestImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
||||||
compileOnly("javax.annotation:jsr250-api:1.0")
|
compileOnly("javax.annotation:jsr250-api:1.0")
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.2")
|
||||||
implementation("com.github.AppIntro:AppIntro:6.3.1")
|
implementation("com.github.AppIntro:AppIntro:6.2.0")
|
||||||
implementation("com.google.code.findbugs:jsr305:3.0.2")
|
implementation("com.google.code.findbugs:jsr305:3.0.2")
|
||||||
implementation("com.google.dagger:dagger:$daggerVersion")
|
implementation("com.google.dagger:dagger:$daggerVersion")
|
||||||
implementation("com.google.guava:guava:33.1.0-android")
|
implementation("com.google.guava:guava:31.1-android")
|
||||||
implementation("io.ktor:ktor-client-android:$ktorVersion")
|
implementation("io.ktor:ktor-client-android:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-client-core:$ktorVersion")
|
implementation("io.ktor:ktor-client-core:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-client-jackson:$ktorVersion")
|
implementation("io.ktor:ktor-client-jackson:$ktorVersion")
|
||||||
@@ -130,18 +110,17 @@ dependencies {
|
|||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
|
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kxCoroutinesVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kxCoroutinesVersion")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kxCoroutinesVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kxCoroutinesVersion")
|
||||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
implementation("androidx.appcompat:appcompat:1.5.0")
|
||||||
implementation("androidx.legacy:legacy-preference-v14:1.0.0")
|
implementation("androidx.legacy:legacy-preference-v14:1.0.0")
|
||||||
implementation("androidx.legacy:legacy-support-v4:1.0.0")
|
implementation("androidx.legacy:legacy-support-v4:1.0.0")
|
||||||
implementation("com.google.android.material:material:1.11.0")
|
implementation("com.google.android.material:material:1.6.1")
|
||||||
implementation("com.opencsv:opencsv:5.9")
|
implementation("com.opencsv:opencsv:5.6")
|
||||||
implementation("nl.dionsegijn:konfetti-xml:2.0.2")
|
|
||||||
implementation(project(":uhabits-core"))
|
implementation(project(":uhabits-core"))
|
||||||
kapt("com.google.dagger:dagger-compiler:$daggerVersion")
|
kapt("com.google.dagger:dagger-compiler:$daggerVersion")
|
||||||
kaptAndroidTest("com.google.dagger:dagger-compiler:$daggerVersion")
|
kaptAndroidTest("com.google.dagger:dagger-compiler:$daggerVersion")
|
||||||
testImplementation("com.google.dagger:dagger:$daggerVersion")
|
testImplementation("com.google.dagger:dagger:$daggerVersion")
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
|
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
kapt {
|
kapt {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
@@ -39,7 +39,6 @@ open class BaseUserInterfaceTest {
|
|||||||
private lateinit var prefs: Preferences
|
private lateinit var prefs: Preferences
|
||||||
private lateinit var fixtures: HabitFixtures
|
private lateinit var fixtures: HabitFixtures
|
||||||
private lateinit var cache: HabitCardListCache
|
private lateinit var cache: HabitCardListCache
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
|
|||||||
@@ -153,22 +153,18 @@ open class BaseViewTest : BaseAndroidTest() {
|
|||||||
var filename = filename
|
var filename = filename
|
||||||
var dir = getSDCardDir("test-screenshots")
|
var dir = getSDCardDir("test-screenshots")
|
||||||
if (dir == null) dir = AndroidDirFinder(targetContext).getFilesDir("test-screenshots")
|
if (dir == null) dir = AndroidDirFinder(targetContext).getFilesDir("test-screenshots")
|
||||||
if (dir == null) {
|
if (dir == null) throw RuntimeException(
|
||||||
throw RuntimeException(
|
|
||||||
"Could not find suitable dir for screenshots"
|
"Could not find suitable dir for screenshots"
|
||||||
)
|
)
|
||||||
}
|
|
||||||
filename = filename.replace("\\.png$".toRegex(), "$suffix.png")
|
filename = filename.replace("\\.png$".toRegex(), "$suffix.png")
|
||||||
val absolutePath = String.format("%s/%s", dir.absolutePath, filename)
|
val absolutePath = String.format("%s/%s", dir.absolutePath, filename)
|
||||||
val parent = File(absolutePath).parentFile
|
val parent = File(absolutePath).parentFile
|
||||||
if (!parent.exists() && !parent.mkdirs()) {
|
if (!parent.exists() && !parent.mkdirs()) throw RuntimeException(
|
||||||
throw RuntimeException(
|
|
||||||
String.format(
|
String.format(
|
||||||
"Could not create dir: %s",
|
"Could not create dir: %s",
|
||||||
parent.absolutePath
|
parent.absolutePath
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
val out = FileOutputStream(absolutePath)
|
val out = FileOutputStream(absolutePath)
|
||||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||||
return absolutePath
|
return absolutePath
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
package org.isoron.uhabits
|
package org.isoron.uhabits
|
||||||
|
|
||||||
|
import com.nhaarman.mockitokotlin2.mock
|
||||||
import dagger.Component
|
import dagger.Component
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
@@ -34,7 +35,6 @@ import org.isoron.uhabits.inject.ActivityScope
|
|||||||
import org.isoron.uhabits.inject.HabitModule
|
import org.isoron.uhabits.inject.HabitModule
|
||||||
import org.isoron.uhabits.inject.HabitsActivityModule
|
import org.isoron.uhabits.inject.HabitsActivityModule
|
||||||
import org.isoron.uhabits.inject.HabitsApplicationComponent
|
import org.isoron.uhabits.inject.HabitsApplicationComponent
|
||||||
import org.mockito.kotlin.mock
|
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
class TestModule {
|
class TestModule {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ package org.isoron.uhabits.acceptance.steps
|
|||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Build.VERSION.SDK_INT
|
import android.os.Build.VERSION.SDK_INT
|
||||||
|
import android.os.Build.VERSION_CODES
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.test.espresso.Espresso
|
import androidx.test.espresso.Espresso
|
||||||
@@ -32,11 +33,11 @@ import androidx.test.espresso.matcher.ViewMatchers
|
|||||||
import androidx.test.uiautomator.By
|
import androidx.test.uiautomator.By
|
||||||
import androidx.test.uiautomator.UiSelector
|
import androidx.test.uiautomator.UiSelector
|
||||||
import androidx.test.uiautomator.Until
|
import androidx.test.uiautomator.Until
|
||||||
|
import junit.framework.Assert.assertTrue
|
||||||
import org.hamcrest.CoreMatchers
|
import org.hamcrest.CoreMatchers
|
||||||
import org.isoron.uhabits.BaseUserInterfaceTest
|
import org.isoron.uhabits.BaseUserInterfaceTest
|
||||||
import org.isoron.uhabits.R
|
import org.isoron.uhabits.R
|
||||||
import org.isoron.uhabits.activities.habits.list.ListHabitsActivity
|
import org.isoron.uhabits.activities.habits.list.ListHabitsActivity
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
|
|
||||||
object CommonSteps : BaseUserInterfaceTest() {
|
object CommonSteps : BaseUserInterfaceTest() {
|
||||||
fun pressBack() {
|
fun pressBack() {
|
||||||
@@ -147,19 +148,15 @@ object CommonSteps : BaseUserInterfaceTest() {
|
|||||||
Screen.LIST_HABITS ->
|
Screen.LIST_HABITS ->
|
||||||
Espresso.onView(ViewMatchers.withClassName(CoreMatchers.endsWith("ListHabitsRootView")))
|
Espresso.onView(ViewMatchers.withClassName(CoreMatchers.endsWith("ListHabitsRootView")))
|
||||||
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||||
|
|
||||||
Screen.SHOW_HABIT ->
|
Screen.SHOW_HABIT ->
|
||||||
Espresso.onView(ViewMatchers.withId(R.id.subtitleCard))
|
Espresso.onView(ViewMatchers.withId(R.id.subtitleCard))
|
||||||
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||||
|
|
||||||
Screen.EDIT_HABIT ->
|
Screen.EDIT_HABIT ->
|
||||||
Espresso.onView(ViewMatchers.withId(R.id.questionInput))
|
Espresso.onView(ViewMatchers.withId(R.id.questionInput))
|
||||||
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||||
|
|
||||||
Screen.SELECT_HABIT_TYPE ->
|
Screen.SELECT_HABIT_TYPE ->
|
||||||
Espresso.onView(ViewMatchers.withText(R.string.yes_or_no_example))
|
Espresso.onView(ViewMatchers.withText(R.string.yes_or_no_example))
|
||||||
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
|
||||||
|
|
||||||
else -> throw IllegalStateException()
|
else -> throw IllegalStateException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ package org.isoron.uhabits.acceptance.steps
|
|||||||
import android.os.Build.VERSION.SDK_INT
|
import android.os.Build.VERSION.SDK_INT
|
||||||
import androidx.test.uiautomator.UiScrollable
|
import androidx.test.uiautomator.UiScrollable
|
||||||
import androidx.test.uiautomator.UiSelector
|
import androidx.test.uiautomator.UiSelector
|
||||||
|
import junit.framework.Assert.assertFalse
|
||||||
|
import junit.framework.Assert.assertTrue
|
||||||
import org.isoron.uhabits.BaseUserInterfaceTest
|
import org.isoron.uhabits.BaseUserInterfaceTest
|
||||||
import org.junit.Assert.assertFalse
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
|
|
||||||
object WidgetSteps {
|
object WidgetSteps {
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
*/
|
*/
|
||||||
package org.isoron.uhabits.activities.common.views
|
package org.isoron.uhabits.activities.common.views
|
||||||
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
import org.isoron.uhabits.BaseViewTest
|
import org.isoron.uhabits.BaseViewTest
|
||||||
@@ -53,8 +52,7 @@ class FrequencyChartTest : BaseViewTest() {
|
|||||||
@Test
|
@Test
|
||||||
@Throws(Throwable::class)
|
@Throws(Throwable::class)
|
||||||
fun testRender_withDataOffset() {
|
fun testRender_withDataOffset() {
|
||||||
val e = MotionEvent.obtain(0, 0, 0, 0f, 0f, 0)
|
view.onScroll(null, null, -dpToPixels(150), 0f)
|
||||||
view.onScroll(e, e, -dpToPixels(150), 0f)
|
|
||||||
view.invalidate()
|
view.invalidate()
|
||||||
assertRenders(view, BASE_PATH + "renderDataOffset.png")
|
assertRenders(view, BASE_PATH + "renderDataOffset.png")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
*/
|
*/
|
||||||
package org.isoron.uhabits.activities.common.views
|
package org.isoron.uhabits.activities.common.views
|
||||||
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
import org.isoron.uhabits.BaseViewTest
|
import org.isoron.uhabits.BaseViewTest
|
||||||
@@ -45,7 +44,7 @@ class ScoreChartTest : BaseViewTest() {
|
|||||||
habit = habit,
|
habit = habit,
|
||||||
firstWeekday = prefs.firstWeekdayInt,
|
firstWeekday = prefs.firstWeekdayInt,
|
||||||
spinnerPosition = 0,
|
spinnerPosition = 0,
|
||||||
theme = LightTheme()
|
theme = LightTheme(),
|
||||||
)
|
)
|
||||||
view = ScoreChart(targetContext).apply {
|
view = ScoreChart(targetContext).apply {
|
||||||
setScores(state.scores)
|
setScores(state.scores)
|
||||||
@@ -64,8 +63,7 @@ class ScoreChartTest : BaseViewTest() {
|
|||||||
@Test
|
@Test
|
||||||
@Throws(Throwable::class)
|
@Throws(Throwable::class)
|
||||||
fun testRender_withDataOffset() {
|
fun testRender_withDataOffset() {
|
||||||
val e = MotionEvent.obtain(0, 0, 0, 0f, 0f, 0)
|
view.onScroll(null, null, -dpToPixels(150), 0f)
|
||||||
view.onScroll(e, e, -dpToPixels(150), 0f)
|
|
||||||
view.invalidate()
|
view.invalidate()
|
||||||
assertRenders(view, BASE_PATH + "renderDataOffset.png")
|
assertRenders(view, BASE_PATH + "renderDataOffset.png")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import org.junit.runner.RunWith
|
|||||||
@MediumTest
|
@MediumTest
|
||||||
class StreakChartTest : BaseViewTest() {
|
class StreakChartTest : BaseViewTest() {
|
||||||
private lateinit var view: StreakChart
|
private lateinit var view: StreakChart
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
override fun setUp() {
|
override fun setUp() {
|
||||||
super.setUp()
|
super.setUp()
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class EntryButtonViewTest : BaseViewTest() {
|
|||||||
view = component.getEntryButtonViewFactory().create().apply {
|
view = component.getEntryButtonViewFactory().create().apply {
|
||||||
value = Entry.NO
|
value = Entry.NO
|
||||||
color = PaletteUtils.getAndroidTestColor(5)
|
color = PaletteUtils.getAndroidTestColor(5)
|
||||||
onToggle = { _, _ -> toggled = true }
|
onToggle = { _, _, _ -> toggled = true }
|
||||||
onEdit = { edited = true }
|
onEdit = { edited = true }
|
||||||
}
|
}
|
||||||
measureView(view, dpToPixels(48), dpToPixels(48))
|
measureView(view, dpToPixels(48), dpToPixels(48))
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class EntryPanelViewTest : BaseViewTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun testToggle() {
|
fun testToggle() {
|
||||||
val timestamps = mutableListOf<Timestamp>()
|
val timestamps = mutableListOf<Timestamp>()
|
||||||
view.onToggle = { t, _, _ -> timestamps.add(t) }
|
view.onToggle = { t, _, _, _ -> timestamps.add(t) }
|
||||||
view.buttons[0].performLongClick()
|
view.buttons[0].performLongClick()
|
||||||
view.buttons[2].performLongClick()
|
view.buttons[2].performLongClick()
|
||||||
view.buttons[3].performLongClick()
|
view.buttons[3].performLongClick()
|
||||||
@@ -88,7 +88,7 @@ class EntryPanelViewTest : BaseViewTest() {
|
|||||||
fun testToggle_withOffset() {
|
fun testToggle_withOffset() {
|
||||||
val timestamps = mutableListOf<Timestamp>()
|
val timestamps = mutableListOf<Timestamp>()
|
||||||
view.dataOffset = 3
|
view.dataOffset = 3
|
||||||
view.onToggle = { t, _, _ -> timestamps += t }
|
view.onToggle = { t, _, _, _ -> timestamps += t }
|
||||||
view.buttons[0].performLongClick()
|
view.buttons[0].performLongClick()
|
||||||
view.buttons[2].performLongClick()
|
view.buttons[2].performLongClick()
|
||||||
view.buttons[3].performLongClick()
|
view.buttons[3].performLongClick()
|
||||||
|
|||||||
@@ -20,15 +20,15 @@ package org.isoron.uhabits.activities.habits.list.views
|
|||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
|
import com.nhaarman.mockitokotlin2.doReturn
|
||||||
|
import com.nhaarman.mockitokotlin2.mock
|
||||||
|
import com.nhaarman.mockitokotlin2.verify
|
||||||
|
import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
|
||||||
|
import com.nhaarman.mockitokotlin2.whenever
|
||||||
import org.isoron.uhabits.BaseViewTest
|
import org.isoron.uhabits.BaseViewTest
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.mockito.kotlin.doReturn
|
|
||||||
import org.mockito.kotlin.mock
|
|
||||||
import org.mockito.kotlin.verify
|
|
||||||
import org.mockito.kotlin.verifyNoMoreInteractions
|
|
||||||
import org.mockito.kotlin.whenever
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@MediumTest
|
@MediumTest
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ package org.isoron.uhabits.activities.habits.list.views
|
|||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
|
import com.nhaarman.mockitokotlin2.doReturn
|
||||||
|
import com.nhaarman.mockitokotlin2.mock
|
||||||
|
import com.nhaarman.mockitokotlin2.whenever
|
||||||
import org.hamcrest.CoreMatchers.equalTo
|
import org.hamcrest.CoreMatchers.equalTo
|
||||||
import org.hamcrest.MatcherAssert.assertThat
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
import org.isoron.uhabits.BaseViewTest
|
import org.isoron.uhabits.BaseViewTest
|
||||||
@@ -27,9 +30,6 @@ import org.isoron.uhabits.core.ui.screens.habits.list.HintList
|
|||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.mockito.kotlin.doReturn
|
|
||||||
import org.mockito.kotlin.mock
|
|
||||||
import org.mockito.kotlin.whenever
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@MediumTest
|
@MediumTest
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class FrequencyCardViewTest : BaseViewTest() {
|
|||||||
FrequencyCardPresenter.buildState(
|
FrequencyCardPresenter.buildState(
|
||||||
habit = habit,
|
habit = habit,
|
||||||
firstWeekday = 0,
|
firstWeekday = 0,
|
||||||
theme = LightTheme()
|
theme = LightTheme(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
measureView(view, 800f, 600f)
|
measureView(view, 800f, 600f)
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class HistoryCardViewTest : BaseViewTest() {
|
|||||||
HistoryCardPresenter.buildState(
|
HistoryCardPresenter.buildState(
|
||||||
habit = habit,
|
habit = habit,
|
||||||
firstWeekday = SUNDAY,
|
firstWeekday = SUNDAY,
|
||||||
theme = LightTheme()
|
theme = LightTheme(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
measureView(view, 800f, 600f)
|
measureView(view, 800f, 600f)
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class OverviewCardViewTest : BaseViewTest() {
|
|||||||
scoreYearDiff = 0.74f,
|
scoreYearDiff = 0.74f,
|
||||||
totalCount = 44,
|
totalCount = 44,
|
||||||
color = PaletteColor(7),
|
color = PaletteColor(7),
|
||||||
theme = LightTheme()
|
theme = LightTheme(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
measureView(view, 800f, 300f)
|
measureView(view, 800f, 300f)
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class ScoreCardViewTest : BaseViewTest() {
|
|||||||
habit = habit,
|
habit = habit,
|
||||||
firstWeekday = 0,
|
firstWeekday = 0,
|
||||||
spinnerPosition = 0,
|
spinnerPosition = 0,
|
||||||
theme = LightTheme()
|
theme = LightTheme(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
measureView(view, 800f, 600f)
|
measureView(view, 800f, 600f)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class StreakCardViewTest : BaseViewTest() {
|
|||||||
StreakCardState(
|
StreakCardState(
|
||||||
bestStreaks = habit.streaks.getBest(10),
|
bestStreaks = habit.streaks.getBest(10),
|
||||||
color = habit.color,
|
color = habit.color,
|
||||||
theme = LightTheme()
|
theme = LightTheme(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
measureView(view, 800f, 600f)
|
measureView(view, 800f, 600f)
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class SubtitleCardViewTest : BaseViewTest() {
|
|||||||
isNumerical = false,
|
isNumerical = false,
|
||||||
question = "Did you meditate this morning?",
|
question = "Did you meditate this morning?",
|
||||||
reminder = Reminder(8, 30, EVERY_DAY),
|
reminder = Reminder(8, 30, EVERY_DAY),
|
||||||
theme = LightTheme()
|
theme = LightTheme(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
measureView(view, 800f, 200f)
|
measureView(view, 800f, 200f)
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ class ListHabitsRegressionTest : BaseUserInterfaceTest() {
|
|||||||
"Wake up early",
|
"Wake up early",
|
||||||
"Meditate",
|
"Meditate",
|
||||||
"Read books",
|
"Read books",
|
||||||
"Track time"
|
"Track time",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,11 +33,9 @@ import java.io.IOException
|
|||||||
@MediumTest
|
@MediumTest
|
||||||
class CheckmarkWidgetViewTest : BaseViewTest() {
|
class CheckmarkWidgetViewTest : BaseViewTest() {
|
||||||
private lateinit var view: CheckmarkWidgetView
|
private lateinit var view: CheckmarkWidgetView
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
override fun setUp() {
|
override fun setUp() {
|
||||||
super.setUp()
|
super.setUp()
|
||||||
similarityCutoff = 0.00025
|
|
||||||
setTheme(R.style.WidgetTheme)
|
setTheme(R.style.WidgetTheme)
|
||||||
val habit = fixtures.createShortHabit()
|
val habit = fixtures.createShortHabit()
|
||||||
val computedEntries = habit.computedEntries
|
val computedEntries = habit.computedEntries
|
||||||
|
|||||||
@@ -16,13 +16,13 @@
|
|||||||
~ You should have received a copy of the GNU General Public License along
|
~ You should have received a copy of the GNU General Public License along
|
||||||
~ with this program. If not, see <http://www.gnu.org/licenses/>.
|
~ with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
-->
|
-->
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="org.isoron.uhabits">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||||
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
|
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".HabitsApplication"
|
android:name=".HabitsApplication"
|
||||||
@@ -30,7 +30,6 @@
|
|||||||
android:backupAgent=".HabitsBackupAgent"
|
android:backupAgent=".HabitsBackupAgent"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/main_activity_title"
|
android:label="@string/main_activity_title"
|
||||||
android:localeConfig="@xml/locales_config"
|
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppBaseTheme">
|
android:theme="@style/AppBaseTheme">
|
||||||
|
|
||||||
@@ -80,6 +79,14 @@
|
|||||||
android:value=".activities.habits.list.ListHabitsActivity" />
|
android:value=".activities.habits.list.ListHabitsActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".activities.sync.SyncActivity"
|
||||||
|
android:label="@string/device_sync">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
|
android:value=".activities.sync.SyncActivity" />
|
||||||
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.intro.IntroActivity"
|
android:name=".activities.intro.IntroActivity"
|
||||||
android:label=""
|
android:label=""
|
||||||
@@ -271,7 +278,7 @@
|
|||||||
<!-- Locale/Tasker -->
|
<!-- Locale/Tasker -->
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".automation.FireSettingReceiver"
|
android:name=".automation.FireSettingReceiver"
|
||||||
android:exported="true">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
|
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import java.util.Locale;
|
|||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
|||||||
@@ -23,13 +23,14 @@ import android.graphics.Paint.*;
|
|||||||
import android.os.*;
|
import android.os.*;
|
||||||
import androidx.core.view.*;
|
import androidx.core.view.*;
|
||||||
import androidx.core.view.accessibility.*;
|
import androidx.core.view.accessibility.*;
|
||||||
|
import androidx.core.widget.*;
|
||||||
import android.text.format.*;
|
import android.text.format.*;
|
||||||
import android.view.*;
|
import android.view.*;
|
||||||
import android.view.accessibility.*;
|
import android.view.accessibility.*;
|
||||||
|
|
||||||
import androidx.customview.widget.ExploreByTouchHelper;
|
import androidx.customview.widget.ExploreByTouchHelper;
|
||||||
|
|
||||||
|
import com.android.*;
|
||||||
import com.android.datetimepicker.*;
|
import com.android.datetimepicker.*;
|
||||||
import com.android.datetimepicker.date.MonthAdapter.*;
|
import com.android.datetimepicker.date.MonthAdapter.*;
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import android.graphics.Paint.*;
|
|||||||
import android.util.*;
|
import android.util.*;
|
||||||
import android.view.*;
|
import android.view.*;
|
||||||
|
|
||||||
|
import com.android.*;
|
||||||
import com.android.datetimepicker.*;
|
import com.android.datetimepicker.*;
|
||||||
|
|
||||||
import org.isoron.uhabits.R;
|
import org.isoron.uhabits.R;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import android.view.View.*;
|
|||||||
import android.view.accessibility.*;
|
import android.view.accessibility.*;
|
||||||
import android.widget.*;
|
import android.widget.*;
|
||||||
|
|
||||||
|
import com.android.*;
|
||||||
import com.android.datetimepicker.*;
|
import com.android.datetimepicker.*;
|
||||||
|
|
||||||
import org.isoron.uhabits.R;
|
import org.isoron.uhabits.R;
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class AndroidCanvas : Canvas {
|
|||||||
y1.toDp(),
|
y1.toDp(),
|
||||||
x2.toDp(),
|
x2.toDp(),
|
||||||
y2.toDp(),
|
y2.toDp(),
|
||||||
paint
|
paint,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ class AndroidCanvas : Canvas {
|
|||||||
text,
|
text,
|
||||||
x.toDp(),
|
x.toDp(),
|
||||||
y.toDp() + 0.6f * mHeight,
|
y.toDp() + 0.6f * mHeight,
|
||||||
textPaint
|
textPaint,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ class AndroidCanvas : Canvas {
|
|||||||
y: Double,
|
y: Double,
|
||||||
width: Double,
|
width: Double,
|
||||||
height: Double,
|
height: Double,
|
||||||
cornerRadius: Double
|
cornerRadius: Double,
|
||||||
) {
|
) {
|
||||||
paint.style = Paint.Style.FILL
|
paint.style = Paint.Style.FILL
|
||||||
innerCanvas.drawRoundRect(
|
innerCanvas.drawRoundRect(
|
||||||
@@ -93,7 +93,7 @@ class AndroidCanvas : Canvas {
|
|||||||
(y + height).toDp(),
|
(y + height).toDp(),
|
||||||
cornerRadius.toDp(),
|
cornerRadius.toDp(),
|
||||||
cornerRadius.toDp(),
|
cornerRadius.toDp(),
|
||||||
paint
|
paint,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ class AndroidCanvas : Canvas {
|
|||||||
y.toDp(),
|
y.toDp(),
|
||||||
(x + width).toDp(),
|
(x + width).toDp(),
|
||||||
(y + height).toDp(),
|
(y + height).toDp(),
|
||||||
paint
|
paint,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ class AndroidCanvas : Canvas {
|
|||||||
centerY: Double,
|
centerY: Double,
|
||||||
radius: Double,
|
radius: Double,
|
||||||
startAngle: Double,
|
startAngle: Double,
|
||||||
swipeAngle: Double
|
swipeAngle: Double,
|
||||||
) {
|
) {
|
||||||
paint.style = Paint.Style.FILL
|
paint.style = Paint.Style.FILL
|
||||||
innerCanvas.drawArc(
|
innerCanvas.drawArc(
|
||||||
@@ -159,14 +159,14 @@ class AndroidCanvas : Canvas {
|
|||||||
-startAngle.toFloat(),
|
-startAngle.toFloat(),
|
||||||
-swipeAngle.toFloat(),
|
-swipeAngle.toFloat(),
|
||||||
true,
|
true,
|
||||||
paint
|
paint,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fillCircle(
|
override fun fillCircle(
|
||||||
centerX: Double,
|
centerX: Double,
|
||||||
centerY: Double,
|
centerY: Double,
|
||||||
radius: Double
|
radius: Double,
|
||||||
) {
|
) {
|
||||||
paint.style = Paint.Style.FILL
|
paint.style = Paint.Style.FILL
|
||||||
innerCanvas.drawCircle(centerX.toDp(), centerY.toDp(), radius.toDp(), paint)
|
innerCanvas.drawCircle(centerX.toDp(), centerY.toDp(), radius.toDp(), paint)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import kotlin.math.max
|
|||||||
*/
|
*/
|
||||||
class AndroidDataView(
|
class AndroidDataView(
|
||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null
|
attrs: AttributeSet? = null,
|
||||||
) : AndroidView<DataView>(context, attrs),
|
) : AndroidView<DataView>(context, attrs),
|
||||||
GestureDetector.OnGestureListener,
|
GestureDetector.OnGestureListener,
|
||||||
ValueAnimator.AnimatorUpdateListener {
|
ValueAnimator.AnimatorUpdateListener {
|
||||||
@@ -44,23 +44,23 @@ class AndroidDataView(
|
|||||||
addUpdateListener(this@AndroidDataView)
|
addUpdateListener(this@AndroidDataView)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTouchEvent(event: MotionEvent) = detector.onTouchEvent(event)
|
override fun onTouchEvent(event: MotionEvent?) = detector.onTouchEvent(event)
|
||||||
override fun onDown(e: MotionEvent) = true
|
override fun onDown(e: MotionEvent?) = true
|
||||||
override fun onShowPress(e: MotionEvent) = Unit
|
override fun onShowPress(e: MotionEvent?) = Unit
|
||||||
|
|
||||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
override fun onSingleTapUp(e: MotionEvent?): Boolean {
|
||||||
return handleClick(e, true)
|
return handleClick(e, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLongPress(e: MotionEvent) {
|
override fun onLongPress(e: MotionEvent?) {
|
||||||
handleClick(e)
|
handleClick(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScroll(
|
override fun onScroll(
|
||||||
e1: MotionEvent?,
|
e1: MotionEvent?,
|
||||||
e2: MotionEvent,
|
e2: MotionEvent?,
|
||||||
dx: Float,
|
dx: Float,
|
||||||
dy: Float
|
dy: Float,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (abs(dx) > abs(dy)) {
|
if (abs(dx) > abs(dy)) {
|
||||||
val parent = parent
|
val parent = parent
|
||||||
@@ -80,9 +80,9 @@ class AndroidDataView(
|
|||||||
|
|
||||||
override fun onFling(
|
override fun onFling(
|
||||||
e1: MotionEvent?,
|
e1: MotionEvent?,
|
||||||
e2: MotionEvent,
|
e2: MotionEvent?,
|
||||||
velocityX: Float,
|
velocityX: Float,
|
||||||
velocityY: Float
|
velocityY: Float,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
scroller.fling(
|
scroller.fling(
|
||||||
scroller.currX,
|
scroller.currX,
|
||||||
@@ -100,7 +100,7 @@ class AndroidDataView(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationUpdate(animation: ValueAnimator) {
|
override fun onAnimationUpdate(animation: ValueAnimator?) {
|
||||||
if (!scroller.isFinished) {
|
if (!scroller.isFinished) {
|
||||||
scroller.computeScrollOffset()
|
scroller.computeScrollOffset()
|
||||||
updateDataOffset()
|
updateDataOffset()
|
||||||
@@ -127,11 +127,11 @@ class AndroidDataView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleClick(e: MotionEvent, isSingleTap: Boolean = false): Boolean {
|
private fun handleClick(e: MotionEvent?, isSingleTap: Boolean = false): Boolean {
|
||||||
val x: Float
|
val x: Float
|
||||||
val y: Float
|
val y: Float
|
||||||
try {
|
try {
|
||||||
val pointerId = e.getPointerId(0)
|
val pointerId = e!!.getPointerId(0)
|
||||||
x = e.getX(pointerId)
|
x = e.getX(pointerId)
|
||||||
y = e.getY(pointerId)
|
y = e.getY(pointerId)
|
||||||
} catch (ex: RuntimeException) {
|
} catch (ex: RuntimeException) {
|
||||||
@@ -140,11 +140,8 @@ class AndroidDataView(
|
|||||||
// e.getPointerId.
|
// e.getPointerId.
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (isSingleTap) {
|
if (isSingleTap) view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity)
|
||||||
view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity)
|
else view?.onLongClick(x / canvas.innerDensity, y / canvas.innerDensity)
|
||||||
} else {
|
|
||||||
view?.onLongClick(x / canvas.innerDensity, y / canvas.innerDensity)
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,6 @@ fun Color.toInt(): Int {
|
|||||||
(255 * this.alpha).roundToInt(),
|
(255 * this.alpha).roundToInt(),
|
||||||
(255 * this.red).roundToInt(),
|
(255 * this.red).roundToInt(),
|
||||||
(255 * this.green).roundToInt(),
|
(255 * this.green).roundToInt(),
|
||||||
(255 * this.blue).roundToInt()
|
(255 * this.blue).roundToInt(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import android.util.AttributeSet
|
|||||||
|
|
||||||
open class AndroidView<T : View>(
|
open class AndroidView<T : View>(
|
||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null
|
attrs: AttributeSet? = null,
|
||||||
) : android.view.View(context, attrs) {
|
) : android.view.View(context, attrs) {
|
||||||
|
|
||||||
var view: T? = null
|
var view: T? = null
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import org.isoron.uhabits.inject.ActivityScope
|
|||||||
class AndroidThemeSwitcher
|
class AndroidThemeSwitcher
|
||||||
constructor(
|
constructor(
|
||||||
@ActivityContext val context: Context,
|
@ActivityContext val context: Context,
|
||||||
preferences: Preferences
|
preferences: Preferences,
|
||||||
) : ThemeSwitcher(preferences) {
|
) : ThemeSwitcher(preferences) {
|
||||||
|
|
||||||
override var currentTheme: Theme = LightTheme()
|
override var currentTheme: Theme = LightTheme()
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import org.isoron.uhabits.utils.startActivitySafely
|
|||||||
class AboutScreen(
|
class AboutScreen(
|
||||||
private val activity: AboutActivity,
|
private val activity: AboutActivity,
|
||||||
private val intents: IntentFactory,
|
private val intents: IntentFactory,
|
||||||
private val prefs: Preferences
|
private val prefs: Preferences,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private var developerCountdown = 5
|
private var developerCountdown = 5
|
||||||
|
|||||||
@@ -26,14 +26,13 @@ import org.isoron.uhabits.BuildConfig
|
|||||||
import org.isoron.uhabits.R
|
import org.isoron.uhabits.R
|
||||||
import org.isoron.uhabits.core.models.PaletteColor
|
import org.isoron.uhabits.core.models.PaletteColor
|
||||||
import org.isoron.uhabits.databinding.AboutBinding
|
import org.isoron.uhabits.databinding.AboutBinding
|
||||||
import org.isoron.uhabits.utils.applyRootViewInsets
|
|
||||||
import org.isoron.uhabits.utils.currentTheme
|
import org.isoron.uhabits.utils.currentTheme
|
||||||
import org.isoron.uhabits.utils.setupToolbar
|
import org.isoron.uhabits.utils.setupToolbar
|
||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
@SuppressLint("ViewConstructor")
|
||||||
class AboutView(
|
class AboutView(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val screen: AboutScreen
|
private val screen: AboutScreen,
|
||||||
) : FrameLayout(context) {
|
) : FrameLayout(context) {
|
||||||
|
|
||||||
private var binding = AboutBinding.inflate(LayoutInflater.from(context))
|
private var binding = AboutBinding.inflate(LayoutInflater.from(context))
|
||||||
@@ -44,7 +43,7 @@ class AboutView(
|
|||||||
toolbar = binding.toolbar,
|
toolbar = binding.toolbar,
|
||||||
color = PaletteColor(11),
|
color = PaletteColor(11),
|
||||||
title = resources.getString(R.string.about),
|
title = resources.getString(R.string.about),
|
||||||
theme = currentTheme()
|
theme = currentTheme(),
|
||||||
)
|
)
|
||||||
val version = resources.getString(R.string.version_n)
|
val version = resources.getString(R.string.version_n)
|
||||||
binding.tvContributors.setOnClickListener { screen.showCodeContributorsWebsite() }
|
binding.tvContributors.setOnClickListener { screen.showCodeContributorsWebsite() }
|
||||||
@@ -55,6 +54,5 @@ class AboutView(
|
|||||||
binding.tvTranslate.setOnClickListener { screen.showTranslationWebsite() }
|
binding.tvTranslate.setOnClickListener { screen.showTranslationWebsite() }
|
||||||
binding.tvVersion.setOnClickListener { screen.onPressDeveloperCountdown() }
|
binding.tvVersion.setOnClickListener { screen.onPressDeveloperCountdown() }
|
||||||
binding.tvVersion.text = String.format(version, BuildConfig.VERSION_NAME)
|
binding.tvVersion.text = String.format(version, BuildConfig.VERSION_NAME)
|
||||||
applyRootViewInsets()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,63 +20,109 @@
|
|||||||
package org.isoron.uhabits.activities.common.dialogs
|
package org.isoron.uhabits.activities.common.dialogs
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.content.Context
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import android.view.View.GONE
|
import android.view.View.GONE
|
||||||
import android.view.View.VISIBLE
|
import android.view.View.VISIBLE
|
||||||
import androidx.appcompat.app.AppCompatDialogFragment
|
|
||||||
import org.isoron.uhabits.HabitsApplication
|
|
||||||
import org.isoron.uhabits.R
|
import org.isoron.uhabits.R
|
||||||
import org.isoron.uhabits.core.models.Entry.Companion.NO
|
import org.isoron.uhabits.core.models.Entry.Companion.NO
|
||||||
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
|
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
|
||||||
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
|
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
|
||||||
|
import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO
|
||||||
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
|
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
|
||||||
|
import org.isoron.uhabits.core.preferences.Preferences
|
||||||
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
|
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
|
||||||
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
|
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
|
||||||
import org.isoron.uhabits.utils.getCenter
|
import org.isoron.uhabits.utils.dimBehind
|
||||||
|
import org.isoron.uhabits.utils.dismissCurrentAndShow
|
||||||
|
import org.isoron.uhabits.utils.dp
|
||||||
import org.isoron.uhabits.utils.sres
|
import org.isoron.uhabits.utils.sres
|
||||||
|
|
||||||
class CheckmarkDialog : AppCompatDialogFragment() {
|
const val POPUP_WIDTH = 4 * 48f + 16f
|
||||||
var onToggle: (Int, String, Float, Float) -> Unit = { _, _, _, _ -> }
|
const val POPUP_HEIGHT = 48f * 2.5f + 8f
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
class CheckmarkPopup(
|
||||||
val appComponent = (requireActivity().application as HabitsApplication).component
|
private val context: Context,
|
||||||
val prefs = appComponent.preferences
|
private val color: Int,
|
||||||
val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context))
|
private var notes: String,
|
||||||
val color = requireArguments().getInt("color")
|
private var value: Int,
|
||||||
|
private val prefs: Preferences,
|
||||||
|
private val anchor: View,
|
||||||
|
) {
|
||||||
|
var onToggle: (Int, String) -> Unit = { _, _ -> }
|
||||||
|
private lateinit var dialog: Dialog
|
||||||
|
|
||||||
|
private val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context)).apply {
|
||||||
|
// Required for round corners
|
||||||
|
container.clipToOutline = true
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
view.booleanButtons.visibility = VISIBLE
|
||||||
|
initColors()
|
||||||
|
initTypefaces()
|
||||||
|
hideDisabledButtons()
|
||||||
|
populate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initColors() {
|
||||||
arrayOf(view.yesBtn, view.skipBtn).forEach {
|
arrayOf(view.yesBtn, view.skipBtn).forEach {
|
||||||
it.setTextColor(color)
|
it.setTextColor(color)
|
||||||
}
|
}
|
||||||
arrayOf(view.noBtn, view.unknownBtn).forEach {
|
arrayOf(view.noBtn, view.unknownBtn).forEach {
|
||||||
it.setTextColor(view.root.sres.getColor(R.attr.contrast60))
|
it.setTextColor(view.root.sres.getColor(R.attr.contrast60))
|
||||||
}
|
}
|
||||||
arrayOf(view.yesBtn, view.noBtn, view.skipBtn, view.unknownBtn).forEach {
|
|
||||||
it.typeface = getFontAwesome(requireContext())
|
|
||||||
}
|
}
|
||||||
view.notes.setText(requireArguments().getString("notes")!!)
|
|
||||||
|
private fun initTypefaces() {
|
||||||
|
arrayOf(view.yesBtn, view.noBtn, view.skipBtn, view.unknownBtn).forEach {
|
||||||
|
it.typeface = getFontAwesome(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideDisabledButtons() {
|
||||||
if (!prefs.isSkipEnabled) view.skipBtn.visibility = GONE
|
if (!prefs.isSkipEnabled) view.skipBtn.visibility = GONE
|
||||||
if (!prefs.areQuestionMarksEnabled) view.unknownBtn.visibility = GONE
|
if (!prefs.areQuestionMarksEnabled) view.unknownBtn.visibility = GONE
|
||||||
view.booleanButtons.visibility = VISIBLE
|
}
|
||||||
val dialog = Dialog(requireContext())
|
|
||||||
|
private fun populate() {
|
||||||
|
val selectedBtn = when (value) {
|
||||||
|
YES_MANUAL -> view.yesBtn
|
||||||
|
YES_AUTO -> view.noBtn
|
||||||
|
NO -> view.noBtn
|
||||||
|
UNKNOWN -> if (prefs.areQuestionMarksEnabled) view.unknownBtn else view.noBtn
|
||||||
|
SKIP -> if (prefs.isSkipEnabled) view.skipBtn else view.noBtn
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
view.notes.setText(notes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show() {
|
||||||
|
dialog = Dialog(context, android.R.style.Theme_NoTitleBar)
|
||||||
dialog.setContentView(view.root)
|
dialog.setContentView(view.root)
|
||||||
dialog.window?.apply {
|
dialog.window?.apply {
|
||||||
|
setLayout(
|
||||||
|
view.root.dp(POPUP_WIDTH).toInt(),
|
||||||
|
view.root.dp(POPUP_HEIGHT).toInt()
|
||||||
|
)
|
||||||
setBackgroundDrawableResource(android.R.color.transparent)
|
setBackgroundDrawableResource(android.R.color.transparent)
|
||||||
}
|
}
|
||||||
fun onClick(v: Int) {
|
fun onClick(v: Int) {
|
||||||
val notes = view.notes.text.toString().trim()
|
this.value = v
|
||||||
val location = view.yesBtn.getCenter()
|
save()
|
||||||
onToggle(v, notes, location.x, location.y)
|
|
||||||
requireDialog().dismiss()
|
|
||||||
}
|
}
|
||||||
view.yesBtn.setOnClickListener { onClick(YES_MANUAL) }
|
view.yesBtn.setOnClickListener { onClick(YES_MANUAL) }
|
||||||
view.noBtn.setOnClickListener { onClick(NO) }
|
view.noBtn.setOnClickListener { onClick(NO) }
|
||||||
view.skipBtn.setOnClickListener { onClick(SKIP) }
|
view.skipBtn.setOnClickListener { onClick(SKIP) }
|
||||||
view.unknownBtn.setOnClickListener { onClick(UNKNOWN) }
|
view.unknownBtn.setOnClickListener { onClick(UNKNOWN) }
|
||||||
view.notes.setOnEditorActionListener { v, actionId, event ->
|
dialog.setCanceledOnTouchOutside(true)
|
||||||
onClick(requireArguments().getInt("value"))
|
dialog.dimBehind()
|
||||||
true
|
dialog.dismissCurrentAndShow()
|
||||||
}
|
}
|
||||||
|
|
||||||
return dialog
|
fun save() {
|
||||||
|
onToggle(value, view.notes.text.toString().trim())
|
||||||
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,101 +22,97 @@ package org.isoron.uhabits.activities.common.dialogs
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.RadioButton
|
import android.widget.RadioButton
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatDialogFragment
|
import androidx.appcompat.app.AppCompatDialogFragment
|
||||||
|
import kotlinx.android.synthetic.main.frequency_picker_dialog.view.*
|
||||||
import org.isoron.uhabits.R
|
import org.isoron.uhabits.R
|
||||||
import org.isoron.uhabits.databinding.FrequencyPickerDialogBinding
|
|
||||||
|
|
||||||
class FrequencyPickerDialog(
|
class FrequencyPickerDialog(
|
||||||
var freqNumerator: Int,
|
var freqNumerator: Int,
|
||||||
var freqDenominator: Int
|
var freqDenominator: Int
|
||||||
) : AppCompatDialogFragment() {
|
) : AppCompatDialogFragment() {
|
||||||
private var _binding: FrequencyPickerDialogBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
|
lateinit var contentView: View
|
||||||
var onFrequencyPicked: (num: Int, den: Int) -> Unit = { _, _ -> }
|
var onFrequencyPicked: (num: Int, den: Int) -> Unit = { _, _ -> }
|
||||||
|
|
||||||
constructor() : this(1, 1)
|
constructor() : this(1, 1)
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
_binding = FrequencyPickerDialogBinding.inflate(LayoutInflater.from(requireActivity()))
|
val inflater = LayoutInflater.from(requireActivity())
|
||||||
|
contentView = inflater.inflate(R.layout.frequency_picker_dialog, null)
|
||||||
|
|
||||||
addBeforeAfterText(
|
addBeforeAfterText(
|
||||||
this.getString(R.string.every_x_days),
|
this.getString(R.string.every_x_days),
|
||||||
binding.everyXDaysContainer
|
contentView.everyXDaysContainer,
|
||||||
)
|
)
|
||||||
|
|
||||||
addBeforeAfterText(
|
addBeforeAfterText(
|
||||||
this.getString(R.string.x_times_per_week),
|
this.getString(R.string.x_times_per_week),
|
||||||
binding.xTimesPerWeekContainer
|
contentView.xTimesPerWeekContainer,
|
||||||
)
|
)
|
||||||
|
|
||||||
addBeforeAfterText(
|
addBeforeAfterText(
|
||||||
this.getString(R.string.x_times_per_month),
|
this.getString(R.string.x_times_per_month),
|
||||||
binding.xTimesPerMonthContainer
|
contentView.xTimesPerMonthContainer,
|
||||||
)
|
)
|
||||||
|
|
||||||
addBeforeAfterText(
|
addBeforeAfterText(
|
||||||
this.getString(R.string.x_times_per_y_days),
|
this.getString(R.string.x_times_per_y_days),
|
||||||
binding.xTimesPerYDaysContainer
|
contentView.xTimesPerYDaysContainer,
|
||||||
)
|
)
|
||||||
|
|
||||||
binding.everyDayRadioButton.setOnClickListener {
|
contentView.everyDayRadioButton.setOnClickListener {
|
||||||
check(binding.everyDayRadioButton)
|
check(contentView.everyDayRadioButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.everyXDaysRadioButton.setOnClickListener {
|
contentView.everyXDaysRadioButton.setOnClickListener {
|
||||||
check(binding.everyXDaysRadioButton)
|
check(contentView.everyXDaysRadioButton)
|
||||||
val everyXDaysTextView = binding.everyXDaysTextView
|
val everyXDaysTextView = contentView.everyXDaysTextView
|
||||||
selectInputField(everyXDaysTextView)
|
selectInputField(everyXDaysTextView)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.everyXDaysTextView.setOnFocusChangeListener { v, hasFocus ->
|
contentView.everyXDaysTextView.setOnFocusChangeListener { v, hasFocus ->
|
||||||
if (hasFocus) check(binding.everyXDaysRadioButton)
|
if (hasFocus) check(contentView.everyXDaysRadioButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.xTimesPerWeekRadioButton.setOnClickListener {
|
contentView.xTimesPerWeekRadioButton.setOnClickListener {
|
||||||
check(binding.xTimesPerWeekRadioButton)
|
check(contentView.xTimesPerWeekRadioButton)
|
||||||
selectInputField(binding.xTimesPerWeekTextView)
|
selectInputField(contentView.xTimesPerWeekTextView)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.xTimesPerWeekTextView.setOnFocusChangeListener { v, hasFocus ->
|
contentView.xTimesPerWeekTextView.setOnFocusChangeListener { v, hasFocus ->
|
||||||
if (hasFocus) check(binding.xTimesPerWeekRadioButton)
|
if (hasFocus) check(contentView.xTimesPerWeekRadioButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.xTimesPerMonthRadioButton.setOnClickListener {
|
contentView.xTimesPerMonthRadioButton.setOnClickListener {
|
||||||
check(binding.xTimesPerMonthRadioButton)
|
check(contentView.xTimesPerMonthRadioButton)
|
||||||
selectInputField(binding.xTimesPerMonthTextView)
|
selectInputField(contentView.xTimesPerMonthTextView)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.xTimesPerMonthTextView.setOnFocusChangeListener { v, hasFocus ->
|
contentView.xTimesPerMonthTextView.setOnFocusChangeListener { v, hasFocus ->
|
||||||
if (hasFocus) check(binding.xTimesPerMonthRadioButton)
|
if (hasFocus) check(contentView.xTimesPerMonthRadioButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.xTimesPerYDaysRadioButton.setOnClickListener {
|
contentView.xTimesPerYDaysRadioButton.setOnClickListener {
|
||||||
check(binding.xTimesPerYDaysRadioButton)
|
check(contentView.xTimesPerYDaysRadioButton)
|
||||||
selectInputField(binding.xTimesPerYDaysXTextView)
|
selectInputField(contentView.xTimesPerYDaysXTextView)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.xTimesPerYDaysXTextView.setOnFocusChangeListener { v, hasFocus ->
|
contentView.xTimesPerYDaysXTextView.setOnFocusChangeListener { v, hasFocus ->
|
||||||
if (hasFocus) check(binding.xTimesPerYDaysRadioButton)
|
if (hasFocus) check(contentView.xTimesPerYDaysRadioButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.xTimesPerYDaysYTextView.setOnFocusChangeListener { v, hasFocus ->
|
contentView.xTimesPerYDaysYTextView.setOnFocusChangeListener { v, hasFocus ->
|
||||||
if (hasFocus) check(binding.xTimesPerYDaysRadioButton)
|
if (hasFocus) check(contentView.xTimesPerYDaysRadioButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
return AlertDialog.Builder(requireActivity())
|
return AlertDialog.Builder(requireActivity())
|
||||||
.setView(binding.root)
|
.setView(contentView)
|
||||||
.setPositiveButton(R.string.save) { _, _ -> onSaveClicked() }
|
.setPositiveButton(R.string.save) { _, _ -> onSaveClicked() }
|
||||||
.create()
|
.create()
|
||||||
}
|
}
|
||||||
@@ -128,8 +124,7 @@ class FrequencyPickerDialog(
|
|||||||
val parts = str.split("%d")
|
val parts = str.split("%d")
|
||||||
for (i in parts.indices) {
|
for (i in parts.indices) {
|
||||||
container.addView(
|
container.addView(
|
||||||
TextView(activity).apply { text = parts[i].trim() },
|
TextView(activity).apply { text = parts[i].trim() }, 2 * i + 1,
|
||||||
2 * i + 1
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,35 +133,31 @@ class FrequencyPickerDialog(
|
|||||||
var numerator = 1
|
var numerator = 1
|
||||||
var denominator = 1
|
var denominator = 1
|
||||||
when {
|
when {
|
||||||
binding.everyDayRadioButton.isChecked -> {
|
contentView.everyDayRadioButton.isChecked -> {
|
||||||
// NOP
|
// NOP
|
||||||
}
|
}
|
||||||
|
contentView.everyXDaysRadioButton.isChecked -> {
|
||||||
binding.everyXDaysRadioButton.isChecked -> {
|
if (contentView.everyXDaysTextView.text.isNotEmpty()) {
|
||||||
if (binding.everyXDaysTextView.text.isNotEmpty()) {
|
denominator = Integer.parseInt(contentView.everyXDaysTextView.text.toString())
|
||||||
denominator = Integer.parseInt(binding.everyXDaysTextView.text.toString())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
contentView.xTimesPerWeekRadioButton.isChecked -> {
|
||||||
binding.xTimesPerWeekRadioButton.isChecked -> {
|
if (contentView.xTimesPerWeekTextView.text.isNotEmpty()) {
|
||||||
if (binding.xTimesPerWeekTextView.text.isNotEmpty()) {
|
numerator = Integer.parseInt(contentView.xTimesPerWeekTextView.text.toString())
|
||||||
numerator = Integer.parseInt(binding.xTimesPerWeekTextView.text.toString())
|
|
||||||
denominator = 7
|
denominator = 7
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
contentView.xTimesPerYDaysRadioButton.isChecked -> {
|
||||||
binding.xTimesPerYDaysRadioButton.isChecked -> {
|
if (contentView.xTimesPerYDaysXTextView.text.isNotEmpty() && contentView.xTimesPerYDaysYTextView.text.isNotEmpty()) {
|
||||||
if (binding.xTimesPerYDaysXTextView.text.isNotEmpty() && binding.xTimesPerYDaysYTextView.text.isNotEmpty()) {
|
|
||||||
numerator =
|
numerator =
|
||||||
Integer.parseInt(binding.xTimesPerYDaysXTextView.text.toString())
|
Integer.parseInt(contentView.xTimesPerYDaysXTextView.text.toString())
|
||||||
denominator =
|
denominator =
|
||||||
Integer.parseInt(binding.xTimesPerYDaysYTextView.text.toString())
|
Integer.parseInt(contentView.xTimesPerYDaysYTextView.text.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
if (binding.xTimesPerMonthTextView.text.isNotEmpty()) {
|
if (contentView.xTimesPerMonthTextView.text.isNotEmpty()) {
|
||||||
numerator = Integer.parseInt(binding.xTimesPerMonthTextView.text.toString())
|
numerator = Integer.parseInt(contentView.xTimesPerMonthTextView.text.toString())
|
||||||
denominator = 30
|
denominator = 30
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,27 +184,27 @@ class FrequencyPickerDialog(
|
|||||||
private fun populateViews() {
|
private fun populateViews() {
|
||||||
uncheckAll()
|
uncheckAll()
|
||||||
if (freqDenominator == 30 || freqDenominator == 31) {
|
if (freqDenominator == 30 || freqDenominator == 31) {
|
||||||
binding.xTimesPerMonthRadioButton.isChecked = true
|
contentView.xTimesPerMonthRadioButton.isChecked = true
|
||||||
binding.xTimesPerMonthTextView.setText(freqNumerator.toString())
|
contentView.xTimesPerMonthTextView.setText(freqNumerator.toString())
|
||||||
selectInputField(binding.xTimesPerMonthTextView)
|
selectInputField(contentView.xTimesPerMonthTextView)
|
||||||
} else {
|
} else {
|
||||||
if (freqNumerator == 1) {
|
if (freqNumerator == 1) {
|
||||||
if (freqDenominator == 1) {
|
if (freqDenominator == 1) {
|
||||||
binding.everyDayRadioButton.isChecked = true
|
contentView.everyDayRadioButton.isChecked = true
|
||||||
} else {
|
} else {
|
||||||
binding.everyXDaysRadioButton.isChecked = true
|
contentView.everyXDaysRadioButton.isChecked = true
|
||||||
binding.everyXDaysTextView.setText(freqDenominator.toString())
|
contentView.everyXDaysTextView.setText(freqDenominator.toString())
|
||||||
selectInputField(binding.everyXDaysTextView)
|
selectInputField(contentView.everyXDaysTextView)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (freqDenominator == 7) {
|
if (freqDenominator == 7) {
|
||||||
binding.xTimesPerWeekRadioButton.isChecked = true
|
contentView.xTimesPerWeekRadioButton.isChecked = true
|
||||||
binding.xTimesPerWeekTextView.setText(freqNumerator.toString())
|
contentView.xTimesPerWeekTextView.setText(freqNumerator.toString())
|
||||||
selectInputField(binding.xTimesPerWeekTextView)
|
selectInputField(contentView.xTimesPerWeekTextView)
|
||||||
} else {
|
} else {
|
||||||
binding.xTimesPerYDaysRadioButton.isChecked = true
|
contentView.xTimesPerYDaysRadioButton.isChecked = true
|
||||||
binding.xTimesPerYDaysXTextView.setText(freqNumerator.toString())
|
contentView.xTimesPerYDaysXTextView.setText(freqNumerator.toString())
|
||||||
binding.xTimesPerYDaysYTextView.setText(freqDenominator.toString())
|
contentView.xTimesPerYDaysYTextView.setText(freqDenominator.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,10 +215,10 @@ class FrequencyPickerDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun uncheckAll() {
|
private fun uncheckAll() {
|
||||||
binding.everyDayRadioButton.isChecked = false
|
contentView.everyDayRadioButton.isChecked = false
|
||||||
binding.everyXDaysRadioButton.isChecked = false
|
contentView.everyXDaysRadioButton.isChecked = false
|
||||||
binding.xTimesPerWeekRadioButton.isChecked = false
|
contentView.xTimesPerWeekRadioButton.isChecked = false
|
||||||
binding.xTimesPerMonthRadioButton.isChecked = false
|
contentView.xTimesPerMonthRadioButton.isChecked = false
|
||||||
binding.xTimesPerYDaysRadioButton.isChecked = false
|
contentView.xTimesPerYDaysRadioButton.isChecked = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
|
|||||||
theme = themeSwitcher.currentTheme,
|
theme = themeSwitcher.currentTheme,
|
||||||
today = DateUtils.getTodayWithOffset().toLocalDate(),
|
today = DateUtils.getTodayWithOffset().toLocalDate(),
|
||||||
onDateClickedListener = onDateClickedListener ?: object : OnDateClickedListener {},
|
onDateClickedListener = onDateClickedListener ?: object : OnDateClickedListener {},
|
||||||
padding = 10.0
|
padding = 10.0,
|
||||||
)
|
)
|
||||||
dataView = AndroidDataView(requireContext(), null)
|
dataView = AndroidDataView(requireContext(), null)
|
||||||
dataView.view = chart!!
|
dataView.view = chart!!
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
package org.isoron.uhabits.activities.common.dialogs
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.provider.Settings
|
|
||||||
import android.text.method.DigitsKeyListener
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
|
||||||
import android.view.inputmethod.EditorInfo
|
|
||||||
import androidx.appcompat.app.AppCompatDialogFragment
|
|
||||||
import org.isoron.uhabits.HabitsApplication
|
|
||||||
import org.isoron.uhabits.R
|
|
||||||
import org.isoron.uhabits.core.models.Entry
|
|
||||||
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
|
|
||||||
import org.isoron.uhabits.utils.InterfaceUtils
|
|
||||||
import org.isoron.uhabits.utils.getCenter
|
|
||||||
import org.isoron.uhabits.utils.requestFocusWithKeyboard
|
|
||||||
import org.isoron.uhabits.utils.sres
|
|
||||||
import java.text.DecimalFormat
|
|
||||||
import java.text.DecimalFormatSymbols
|
|
||||||
import java.text.NumberFormat
|
|
||||||
import java.text.ParseException
|
|
||||||
|
|
||||||
class NumberDialog : AppCompatDialogFragment() {
|
|
||||||
|
|
||||||
var onToggle: (Double, String, Float, Float) -> Unit = { _, _, _, _ -> }
|
|
||||||
var onDismiss: () -> Unit = {}
|
|
||||||
|
|
||||||
private var originalNotes: String = ""
|
|
||||||
private var originalValue: Double = 0.0
|
|
||||||
private lateinit var view: CheckmarkPopupBinding
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
val appComponent = (requireActivity().application as HabitsApplication).component
|
|
||||||
val prefs = appComponent.preferences
|
|
||||||
view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context))
|
|
||||||
arrayOf(view.yesBtn, view.skipBtn).forEach {
|
|
||||||
it.setTextColor(requireArguments().getInt("color"))
|
|
||||||
}
|
|
||||||
arrayOf(view.noBtn, view.unknownBtn).forEach {
|
|
||||||
it.setTextColor(view.root.sres.getColor(R.attr.contrast60))
|
|
||||||
}
|
|
||||||
arrayOf(view.yesBtn, view.noBtn, view.skipBtn, view.unknownBtn).forEach {
|
|
||||||
it.typeface = InterfaceUtils.getFontAwesome(requireContext())
|
|
||||||
}
|
|
||||||
if (!prefs.isSkipEnabled) view.skipBtnNumber.visibility = View.GONE
|
|
||||||
view.numberButtons.visibility = View.VISIBLE
|
|
||||||
fixDecimalSeparator(view)
|
|
||||||
originalNotes = requireArguments().getString("notes")!!
|
|
||||||
originalValue = requireArguments().getDouble("value")
|
|
||||||
view.notes.setText(originalNotes)
|
|
||||||
view.value.setText(
|
|
||||||
when {
|
|
||||||
originalValue < 0.01 -> "0"
|
|
||||||
else -> DecimalFormat("#.##").format(originalValue)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
view.value.setOnKeyListener { _, keyCode, event ->
|
|
||||||
if (event.action == MotionEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
|
|
||||||
save()
|
|
||||||
return@setOnKeyListener true
|
|
||||||
}
|
|
||||||
return@setOnKeyListener false
|
|
||||||
}
|
|
||||||
view.saveBtn.setOnClickListener {
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
view.skipBtnNumber.setOnClickListener {
|
|
||||||
view.value.setText(DecimalFormat("#.###").format((Entry.SKIP.toDouble() / 1000)))
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
view.notes.setOnEditorActionListener { v, actionId, event ->
|
|
||||||
save()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
view.value.requestFocusWithKeyboard()
|
|
||||||
val dialog = Dialog(requireContext())
|
|
||||||
dialog.setContentView(view.root)
|
|
||||||
dialog.window?.apply {
|
|
||||||
setBackgroundDrawableResource(android.R.color.transparent)
|
|
||||||
}
|
|
||||||
dialog.setOnDismissListener { onDismiss() }
|
|
||||||
return dialog
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fixDecimalSeparator(view: CheckmarkPopupBinding) {
|
|
||||||
// https://stackoverflow.com/a/34256139
|
|
||||||
val separator = DecimalFormatSymbols.getInstance().decimalSeparator
|
|
||||||
view.value.keyListener = DigitsKeyListener.getInstance("0123456789$separator")
|
|
||||||
|
|
||||||
// https://github.com/flutter/flutter/issues/61175
|
|
||||||
val currKeyboard = Settings.Secure.getString(
|
|
||||||
requireContext().contentResolver,
|
|
||||||
Settings.Secure.DEFAULT_INPUT_METHOD
|
|
||||||
)
|
|
||||||
if (currKeyboard.contains("swiftkey") || currKeyboard.contains("samsung")) {
|
|
||||||
view.value.inputType = EditorInfo.TYPE_CLASS_TEXT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun save() {
|
|
||||||
var value = originalValue
|
|
||||||
try {
|
|
||||||
val numberFormat = NumberFormat.getInstance()
|
|
||||||
val valueStr = view.value.text.toString()
|
|
||||||
value = if (valueStr.isNotEmpty()) {
|
|
||||||
numberFormat.parse(valueStr)!!.toDouble()
|
|
||||||
} else {
|
|
||||||
Entry.UNKNOWN.toDouble() / 1000
|
|
||||||
}
|
|
||||||
} catch (e: ParseException) {
|
|
||||||
// NOP
|
|
||||||
}
|
|
||||||
val notes = view.notes.text.toString()
|
|
||||||
val location = view.saveBtn.getCenter()
|
|
||||||
onToggle(value, notes, location.x, location.y)
|
|
||||||
requireDialog().dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.activities.common.dialogs
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.KeyEvent.KEYCODE_ENTER
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MotionEvent.ACTION_DOWN
|
||||||
|
import android.view.View
|
||||||
|
import android.view.View.GONE
|
||||||
|
import android.view.View.VISIBLE
|
||||||
|
import org.isoron.uhabits.core.models.Entry
|
||||||
|
import org.isoron.uhabits.core.preferences.Preferences
|
||||||
|
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
|
||||||
|
import org.isoron.uhabits.utils.dimBehind
|
||||||
|
import org.isoron.uhabits.utils.dismissCurrentAndShow
|
||||||
|
import org.isoron.uhabits.utils.dp
|
||||||
|
import org.isoron.uhabits.utils.requestFocusWithKeyboard
|
||||||
|
import java.text.DecimalFormat
|
||||||
|
|
||||||
|
class NumberPopup(
|
||||||
|
private val context: Context,
|
||||||
|
private var notes: String,
|
||||||
|
private var value: Double,
|
||||||
|
private val prefs: Preferences,
|
||||||
|
private val anchor: View,
|
||||||
|
) {
|
||||||
|
var onToggle: (Double, String) -> Unit = { _, _ -> }
|
||||||
|
var onDismiss: () -> Unit = {}
|
||||||
|
private val originalValue = value
|
||||||
|
private lateinit var dialog: Dialog
|
||||||
|
|
||||||
|
private val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context)).apply {
|
||||||
|
// Required for round corners
|
||||||
|
container.clipToOutline = true
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
view.numberButtons.visibility = VISIBLE
|
||||||
|
hideDisabledButtons()
|
||||||
|
populate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideDisabledButtons() {
|
||||||
|
if (!prefs.isSkipEnabled) view.skipBtnNumber.visibility = GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populate() {
|
||||||
|
view.notes.setText(notes)
|
||||||
|
view.value.setText(
|
||||||
|
when {
|
||||||
|
value < 0.01 -> "0"
|
||||||
|
else -> DecimalFormat("#.##").format(value)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show() {
|
||||||
|
dialog = Dialog(context, android.R.style.Theme_NoTitleBar)
|
||||||
|
dialog.setContentView(view.root)
|
||||||
|
dialog.window?.apply {
|
||||||
|
setLayout(
|
||||||
|
view.root.dp(POPUP_WIDTH).toInt(),
|
||||||
|
view.root.dp(POPUP_HEIGHT).toInt()
|
||||||
|
)
|
||||||
|
setBackgroundDrawableResource(android.R.color.transparent)
|
||||||
|
}
|
||||||
|
dialog.setOnDismissListener {
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
view.value.setOnKeyListener { _, keyCode, event ->
|
||||||
|
if (event.action == ACTION_DOWN && keyCode == KEYCODE_ENTER) {
|
||||||
|
save()
|
||||||
|
return@setOnKeyListener true
|
||||||
|
}
|
||||||
|
return@setOnKeyListener false
|
||||||
|
}
|
||||||
|
view.saveBtn.setOnClickListener {
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
view.skipBtnNumber.setOnClickListener {
|
||||||
|
view.value.setText((Entry.SKIP.toDouble() / 1000).toString())
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
view.value.requestFocusWithKeyboard()
|
||||||
|
dialog.setCanceledOnTouchOutside(true)
|
||||||
|
dialog.dimBehind()
|
||||||
|
dialog.dismissCurrentAndShow()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save() {
|
||||||
|
val value = view.value.text.toString().toDoubleOrNull() ?: originalValue
|
||||||
|
val notes = view.notes.text.toString()
|
||||||
|
onToggle(value, notes)
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -181,9 +181,8 @@ class FrequencyChart : ScrollableChart {
|
|||||||
rect[0f, 0f, baseSize.toFloat()] = baseSize.toFloat()
|
rect[0f, 0f, baseSize.toFloat()] = baseSize.toFloat()
|
||||||
rect.offset(prevRect!!.left, prevRect!!.top + baseSize * j)
|
rect.offset(prevRect!!.left, prevRect!!.top + baseSize * j)
|
||||||
val i = localeWeekdayList[j] % 7
|
val i = localeWeekdayList[j] % 7
|
||||||
if (values != null) {
|
if (values != null)
|
||||||
drawMarker(canvas, rect, values[i], weekDaysInMonth[i])
|
drawMarker(canvas, rect, values[i], weekDaysInMonth[i])
|
||||||
}
|
|
||||||
rect.offset(0f, rowHeight)
|
rect.offset(0f, rowHeight)
|
||||||
}
|
}
|
||||||
drawFooter(canvas, rect, date)
|
drawFooter(canvas, rect, date)
|
||||||
@@ -197,15 +196,13 @@ class FrequencyChart : ScrollableChart {
|
|||||||
rect.centerY() - 0.1f * em,
|
rect.centerY() - 0.1f * em,
|
||||||
pText!!
|
pText!!
|
||||||
)
|
)
|
||||||
if (date[Calendar.MONTH] == 1) {
|
if (date[Calendar.MONTH] == 1) canvas.drawText(
|
||||||
canvas.drawText(
|
|
||||||
dfYear!!.format(time),
|
dfYear!!.format(time),
|
||||||
rect.centerX(),
|
rect.centerX(),
|
||||||
rect.centerY() + 0.9f * em,
|
rect.centerY() + 0.9f * em,
|
||||||
pText!!
|
pText!!
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun drawGrid(canvas: Canvas, rGrid: RectF?) {
|
private fun drawGrid(canvas: Canvas, rGrid: RectF?) {
|
||||||
val nRows = 7
|
val nRows = 7
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ class RingView : View {
|
|||||||
private var em = 0f
|
private var em = 0f
|
||||||
private var text: String?
|
private var text: String?
|
||||||
private var textSize: Float
|
private var textSize: Float
|
||||||
private var isStrokedTextEnabled: Boolean = false
|
|
||||||
private var enableFontAwesome = false
|
private var enableFontAwesome = false
|
||||||
private var internalDrawingCache: Bitmap? = null
|
private var internalDrawingCache: Bitmap? = null
|
||||||
private var cacheCanvas: Canvas? = null
|
private var cacheCanvas: Canvas? = null
|
||||||
@@ -132,10 +131,6 @@ class RingView : View {
|
|||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setIsStrokedTextEnabled(isStroked: Boolean) {
|
|
||||||
this.isStrokedTextEnabled = isStroked
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas) {
|
override fun onDraw(canvas: Canvas) {
|
||||||
super.onDraw(canvas)
|
super.onDraw(canvas)
|
||||||
val activeCanvas: Canvas?
|
val activeCanvas: Canvas?
|
||||||
@@ -153,23 +148,13 @@ class RingView : View {
|
|||||||
pRing!!.color = inactiveColor!!
|
pRing!!.color = inactiveColor!!
|
||||||
activeCanvas.drawArc(rect!!, angle - 90, 360 - angle, true, pRing!!)
|
activeCanvas.drawArc(rect!!, angle - 90, 360 - angle, true, pRing!!)
|
||||||
if (thickness > 0) {
|
if (thickness > 0) {
|
||||||
if (isTransparencyEnabled) {
|
if (isTransparencyEnabled) pRing!!.xfermode = XFERMODE_CLEAR else pRing!!.color =
|
||||||
pRing!!.xfermode = XFERMODE_CLEAR
|
|
||||||
} else {
|
|
||||||
pRing!!.color =
|
|
||||||
backgroundColor!!
|
backgroundColor!!
|
||||||
}
|
|
||||||
rect!!.inset(thickness, thickness)
|
rect!!.inset(thickness, thickness)
|
||||||
activeCanvas.drawArc(rect!!, 0f, 360f, true, pRing!!)
|
activeCanvas.drawArc(rect!!, 0f, 360f, true, pRing!!)
|
||||||
pRing!!.xfermode = null
|
pRing!!.xfermode = null
|
||||||
pRing!!.color = color
|
pRing!!.color = color
|
||||||
pRing!!.textSize = textSize
|
pRing!!.textSize = textSize
|
||||||
|
|
||||||
if (isStrokedTextEnabled) {
|
|
||||||
pRing!!.style = Paint.Style.STROKE
|
|
||||||
pRing!!.strokeWidth = textSize / 15f
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enableFontAwesome) pRing!!.typeface = getFontAwesome(context)
|
if (enableFontAwesome) pRing!!.typeface = getFontAwesome(context)
|
||||||
activeCanvas.drawText(
|
activeCanvas.drawText(
|
||||||
text!!,
|
text!!,
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ abstract class ScrollableChart : View, GestureDetector.OnGestureListener, Animat
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onFling(
|
override fun onFling(
|
||||||
e1: MotionEvent?,
|
e1: MotionEvent,
|
||||||
e2: MotionEvent,
|
e2: MotionEvent,
|
||||||
velocityX: Float,
|
velocityX: Float,
|
||||||
velocityY: Float
|
velocityY: Float
|
||||||
@@ -116,7 +116,7 @@ abstract class ScrollableChart : View, GestureDetector.OnGestureListener, Animat
|
|||||||
return BundleSavedState(superState, bundle)
|
return BundleSavedState(superState, bundle)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, dx: Float, dy: Float): Boolean {
|
override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, dx: Float, dy: Float): Boolean {
|
||||||
var dx = dx
|
var dx = dx
|
||||||
if (scrollerBucketSize == 0) return false
|
if (scrollerBucketSize == 0) return false
|
||||||
if (abs(dx) > abs(dy)) {
|
if (abs(dx) > abs(dy)) {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ package org.isoron.uhabits.activities.habits.edit
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Html
|
import android.text.Html
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
@@ -34,6 +35,11 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.android.datetimepicker.time.RadialPickerLayout
|
import com.android.datetimepicker.time.RadialPickerLayout
|
||||||
import com.android.datetimepicker.time.TimePickerDialog
|
import com.android.datetimepicker.time.TimePickerDialog
|
||||||
|
import kotlinx.android.synthetic.main.activity_edit_habit.nameInput
|
||||||
|
import kotlinx.android.synthetic.main.activity_edit_habit.notesInput
|
||||||
|
import kotlinx.android.synthetic.main.activity_edit_habit.questionInput
|
||||||
|
import kotlinx.android.synthetic.main.activity_edit_habit.targetInput
|
||||||
|
import kotlinx.android.synthetic.main.activity_edit_habit.unitInput
|
||||||
import org.isoron.platform.gui.toInt
|
import org.isoron.platform.gui.toInt
|
||||||
import org.isoron.uhabits.HabitsApplication
|
import org.isoron.uhabits.HabitsApplication
|
||||||
import org.isoron.uhabits.R
|
import org.isoron.uhabits.R
|
||||||
@@ -52,8 +58,7 @@ import org.isoron.uhabits.core.models.PaletteColor
|
|||||||
import org.isoron.uhabits.core.models.Reminder
|
import org.isoron.uhabits.core.models.Reminder
|
||||||
import org.isoron.uhabits.core.models.WeekdayList
|
import org.isoron.uhabits.core.models.WeekdayList
|
||||||
import org.isoron.uhabits.databinding.ActivityEditHabitBinding
|
import org.isoron.uhabits.databinding.ActivityEditHabitBinding
|
||||||
import org.isoron.uhabits.utils.applyRootViewInsets
|
import org.isoron.uhabits.utils.ColorUtils
|
||||||
import org.isoron.uhabits.utils.applyToolbarInsets
|
|
||||||
import org.isoron.uhabits.utils.dismissCurrentAndShow
|
import org.isoron.uhabits.utils.dismissCurrentAndShow
|
||||||
import org.isoron.uhabits.utils.formatTime
|
import org.isoron.uhabits.utils.formatTime
|
||||||
import org.isoron.uhabits.utils.toFormattedString
|
import org.isoron.uhabits.utils.toFormattedString
|
||||||
@@ -94,8 +99,6 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
themeSwitcher.apply()
|
themeSwitcher.apply()
|
||||||
|
|
||||||
binding = ActivityEditHabitBinding.inflate(layoutInflater)
|
binding = ActivityEditHabitBinding.inflate(layoutInflater)
|
||||||
binding.root.applyRootViewInsets()
|
|
||||||
binding.toolbar.applyToolbarInsets()
|
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
if (intent.hasExtra("habitId")) {
|
if (intent.hasExtra("habitId")) {
|
||||||
@@ -268,9 +271,9 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
habit.copyFrom(original)
|
habit.copyFrom(original)
|
||||||
}
|
}
|
||||||
|
|
||||||
habit.name = binding.nameInput.text.trim().toString()
|
habit.name = nameInput.text.trim().toString()
|
||||||
habit.question = binding.questionInput.text.trim().toString()
|
habit.question = questionInput.text.trim().toString()
|
||||||
habit.description = binding.notesInput.text.trim().toString()
|
habit.description = notesInput.text.trim().toString()
|
||||||
habit.color = color
|
habit.color = color
|
||||||
if (reminderHour >= 0) {
|
if (reminderHour >= 0) {
|
||||||
habit.reminder = Reminder(reminderHour, reminderMin, reminderDays)
|
habit.reminder = Reminder(reminderHour, reminderMin, reminderDays)
|
||||||
@@ -280,9 +283,9 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
habit.frequency = Frequency(freqNum, freqDen)
|
habit.frequency = Frequency(freqNum, freqDen)
|
||||||
if (habitType == HabitType.NUMERICAL) {
|
if (habitType == HabitType.NUMERICAL) {
|
||||||
habit.targetValue = binding.targetInput.text.toString().toDouble()
|
habit.targetValue = targetInput.text.toString().toDouble()
|
||||||
habit.targetType = targetType
|
habit.targetType = targetType
|
||||||
habit.unit = binding.unitInput.text.trim().toString()
|
habit.unit = unitInput.text.trim().toString()
|
||||||
}
|
}
|
||||||
habit.type = habitType
|
habit.type = habitType
|
||||||
|
|
||||||
@@ -305,13 +308,13 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun validate(): Boolean {
|
private fun validate(): Boolean {
|
||||||
var isValid = true
|
var isValid = true
|
||||||
if (binding.nameInput.text.isEmpty()) {
|
if (nameInput.text.isEmpty()) {
|
||||||
binding.nameInput.error = getFormattedValidationError(R.string.validation_cannot_be_blank)
|
nameInput.error = getFormattedValidationError(R.string.validation_cannot_be_blank)
|
||||||
isValid = false
|
isValid = false
|
||||||
}
|
}
|
||||||
if (habitType == HabitType.NUMERICAL) {
|
if (habitType == HabitType.NUMERICAL) {
|
||||||
if (binding.targetInput.text.isEmpty()) {
|
if (targetInput.text.isEmpty()) {
|
||||||
binding.targetInput.error = getString(R.string.validation_cannot_be_blank)
|
targetInput.error = getString(R.string.validation_cannot_be_blank)
|
||||||
isValid = false
|
isValid = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -354,7 +357,8 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
androidColor = themeSwitcher.currentTheme.color(color).toInt()
|
androidColor = themeSwitcher.currentTheme.color(color).toInt()
|
||||||
binding.colorButton.backgroundTintList = ColorStateList.valueOf(androidColor)
|
binding.colorButton.backgroundTintList = ColorStateList.valueOf(androidColor)
|
||||||
if (!themeSwitcher.isNightMode) {
|
if (!themeSwitcher.isNightMode) {
|
||||||
window.statusBarColor = androidColor
|
val darkerAndroidColor = ColorUtils.mixColors(Color.BLACK, androidColor, 0.15f)
|
||||||
|
window.statusBarColor = darkerAndroidColor
|
||||||
binding.toolbar.setBackgroundColor(androidColor)
|
binding.toolbar.setBackgroundColor(androidColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,17 +19,12 @@
|
|||||||
|
|
||||||
package org.isoron.uhabits.activities.habits.list
|
package org.isoron.uhabits.activities.habits.list
|
||||||
|
|
||||||
import android.Manifest.permission.POST_NOTIFICATIONS
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat.checkSelfPermission
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import org.isoron.uhabits.BaseExceptionHandler
|
import org.isoron.uhabits.BaseExceptionHandler
|
||||||
@@ -45,8 +40,6 @@ import org.isoron.uhabits.inject.ActivityContextModule
|
|||||||
import org.isoron.uhabits.inject.DaggerHabitsActivityComponent
|
import org.isoron.uhabits.inject.DaggerHabitsActivityComponent
|
||||||
import org.isoron.uhabits.inject.HabitsActivityComponent
|
import org.isoron.uhabits.inject.HabitsActivityComponent
|
||||||
import org.isoron.uhabits.inject.HabitsApplicationComponent
|
import org.isoron.uhabits.inject.HabitsApplicationComponent
|
||||||
import org.isoron.uhabits.utils.applyRootViewInsets
|
|
||||||
import org.isoron.uhabits.utils.dismissCurrentDialog
|
|
||||||
import org.isoron.uhabits.utils.restartWithFade
|
import org.isoron.uhabits.utils.restartWithFade
|
||||||
|
|
||||||
class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
||||||
@@ -62,16 +55,6 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
|||||||
lateinit var midnightTimer: MidnightTimer
|
lateinit var midnightTimer: MidnightTimer
|
||||||
private val scope = CoroutineScope(Dispatchers.Main)
|
private val scope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
private var permissionAlreadyRequested = false
|
|
||||||
private val permissionLauncher =
|
|
||||||
registerForActivityResult(RequestPermission()) { isGranted: Boolean ->
|
|
||||||
if (isGranted) {
|
|
||||||
scheduleReminders()
|
|
||||||
} else {
|
|
||||||
Log.i("ListHabitsActivity", "POST_NOTIFICATIONS denied")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var menu: ListHabitsMenu
|
private lateinit var menu: ListHabitsMenu
|
||||||
|
|
||||||
override fun onQuestionMarksChanged() {
|
override fun onQuestionMarksChanged() {
|
||||||
@@ -101,7 +84,6 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
|||||||
menu = component.listHabitsMenu
|
menu = component.listHabitsMenu
|
||||||
Thread.setDefaultUncaughtExceptionHandler(BaseExceptionHandler(this))
|
Thread.setDefaultUncaughtExceptionHandler(BaseExceptionHandler(this))
|
||||||
component.listHabitsBehavior.onStartup()
|
component.listHabitsBehavior.onStartup()
|
||||||
rootView.applyRootViewInsets()
|
|
||||||
setContentView(rootView)
|
setContentView(rootView)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +91,6 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
|||||||
midnightTimer.onPause()
|
midnightTimer.onPause()
|
||||||
screen.onDetached()
|
screen.onDetached()
|
||||||
adapter.cancelRefresh()
|
adapter.cancelRefresh()
|
||||||
dismissCurrentDialog()
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,26 +99,6 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
|||||||
screen.onAttached()
|
screen.onAttached()
|
||||||
rootView.postInvalidate()
|
rootView.postInvalidate()
|
||||||
midnightTimer.onResume()
|
midnightTimer.onResume()
|
||||||
|
|
||||||
if (appComponent.reminderScheduler.hasHabitsWithReminders()) {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
scheduleReminders()
|
|
||||||
} else {
|
|
||||||
if (checkSelfPermission(this, POST_NOTIFICATIONS) == PERMISSION_GRANTED) {
|
|
||||||
scheduleReminders()
|
|
||||||
} else {
|
|
||||||
// If we have not requested the permission yet, request it. Otherwide do
|
|
||||||
// nothing. This check is necessary to avoid an infinite onResume loop in case
|
|
||||||
// the user denies the permission.
|
|
||||||
if (!permissionAlreadyRequested) {
|
|
||||||
Log.i("ListHabitsActivity", "Requestion permission: POST_NOTIFICATIONS")
|
|
||||||
permissionLauncher.launch(POST_NOTIFICATIONS)
|
|
||||||
permissionAlreadyRequested = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
taskRunner.run {
|
taskRunner.run {
|
||||||
try {
|
try {
|
||||||
AutoBackup(this@ListHabitsActivity).run()
|
AutoBackup(this@ListHabitsActivity).run()
|
||||||
@@ -153,10 +114,6 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scheduleReminders() {
|
|
||||||
appComponent.reminderScheduler.scheduleAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(m: Menu): Boolean {
|
override fun onCreateOptionsMenu(m: Menu): Boolean {
|
||||||
menu.onCreate(menuInflater, m)
|
menu.onCreate(menuInflater, m)
|
||||||
return true
|
return true
|
||||||
@@ -167,7 +124,6 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
|||||||
return menu.onItemSelected(item)
|
return menu.onItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("Deprecated in Java")
|
|
||||||
override fun onActivityResult(request: Int, result: Int, data: Intent?) {
|
override fun onActivityResult(request: Int, result: Int, data: Intent?) {
|
||||||
super.onActivityResult(request, result, data)
|
super.onActivityResult(request, result, data)
|
||||||
screen.onResult(request, result, data)
|
screen.onResult(request, result, data)
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import android.content.Context
|
|||||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import nl.dionsegijn.konfetti.xml.KonfettiView
|
|
||||||
import org.isoron.uhabits.R
|
import org.isoron.uhabits.R
|
||||||
import org.isoron.uhabits.activities.common.views.ScrollableChart
|
import org.isoron.uhabits.activities.common.views.ScrollableChart
|
||||||
import org.isoron.uhabits.activities.common.views.TaskProgressBar
|
import org.isoron.uhabits.activities.common.views.TaskProgressBar
|
||||||
@@ -70,9 +69,6 @@ class ListHabitsRootView @Inject constructor(
|
|||||||
val listView: HabitCardListView = habitCardListViewFactory.create()
|
val listView: HabitCardListView = habitCardListViewFactory.create()
|
||||||
val llEmpty = EmptyListView(context)
|
val llEmpty = EmptyListView(context)
|
||||||
val tbar = buildToolbar()
|
val tbar = buildToolbar()
|
||||||
val konfettiView = KonfettiView(context).apply {
|
|
||||||
translationZ = 10f
|
|
||||||
}
|
|
||||||
val progressBar = TaskProgressBar(context, runner)
|
val progressBar = TaskProgressBar(context, runner)
|
||||||
val hintView: HintView
|
val hintView: HintView
|
||||||
val header = HeaderView(context, preferences, midnightTimer)
|
val header = HeaderView(context, preferences, midnightTimer)
|
||||||
@@ -84,7 +80,6 @@ class ListHabitsRootView @Inject constructor(
|
|||||||
|
|
||||||
val rootView = RelativeLayout(context).apply {
|
val rootView = RelativeLayout(context).apply {
|
||||||
background = sres.getDrawable(R.attr.windowBackgroundColor)
|
background = sres.getDrawable(R.attr.windowBackgroundColor)
|
||||||
addAtTop(konfettiView)
|
|
||||||
addAtTop(tbar)
|
addAtTop(tbar)
|
||||||
addBelow(header, tbar)
|
addBelow(header, tbar)
|
||||||
addBelow(listView, header, height = MATCH_PARENT)
|
addBelow(listView, header, height = MATCH_PARENT)
|
||||||
@@ -99,7 +94,7 @@ class ListHabitsRootView @Inject constructor(
|
|||||||
title = resources.getString(R.string.main_activity_title),
|
title = resources.getString(R.string.main_activity_title),
|
||||||
color = PaletteColor(17),
|
color = PaletteColor(17),
|
||||||
displayHomeAsUpEnabled = false,
|
displayHomeAsUpEnabled = false,
|
||||||
theme = currentTheme()
|
theme = currentTheme(),
|
||||||
)
|
)
|
||||||
addView(rootView, MATCH_PARENT, MATCH_PARENT)
|
addView(rootView, MATCH_PARENT, MATCH_PARENT)
|
||||||
listAdapter.setListView(listView)
|
listAdapter.setListView(listView)
|
||||||
|
|||||||
@@ -22,18 +22,14 @@ package org.isoron.uhabits.activities.habits.list
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
import nl.dionsegijn.konfetti.core.Party
|
|
||||||
import nl.dionsegijn.konfetti.core.Position
|
|
||||||
import nl.dionsegijn.konfetti.core.emitter.Emitter
|
|
||||||
import org.isoron.platform.gui.toInt
|
import org.isoron.platform.gui.toInt
|
||||||
import org.isoron.uhabits.R
|
import org.isoron.uhabits.R
|
||||||
import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog
|
import org.isoron.uhabits.activities.common.dialogs.CheckmarkPopup
|
||||||
import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
|
import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
|
||||||
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
|
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
|
||||||
import org.isoron.uhabits.activities.common.dialogs.NumberDialog
|
import org.isoron.uhabits.activities.common.dialogs.NumberPopup
|
||||||
import org.isoron.uhabits.activities.habits.edit.HabitTypeDialog
|
import org.isoron.uhabits.activities.habits.edit.HabitTypeDialog
|
||||||
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
|
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
|
||||||
import org.isoron.uhabits.core.commands.ArchiveHabitsCommand
|
import org.isoron.uhabits.core.commands.ArchiveHabitsCommand
|
||||||
@@ -66,7 +62,6 @@ import org.isoron.uhabits.intents.IntentFactory
|
|||||||
import org.isoron.uhabits.tasks.ExportDBTaskFactory
|
import org.isoron.uhabits.tasks.ExportDBTaskFactory
|
||||||
import org.isoron.uhabits.tasks.ImportDataTask
|
import org.isoron.uhabits.tasks.ImportDataTask
|
||||||
import org.isoron.uhabits.tasks.ImportDataTaskFactory
|
import org.isoron.uhabits.tasks.ImportDataTaskFactory
|
||||||
import org.isoron.uhabits.utils.ColorUtils
|
|
||||||
import org.isoron.uhabits.utils.copyTo
|
import org.isoron.uhabits.utils.copyTo
|
||||||
import org.isoron.uhabits.utils.currentTheme
|
import org.isoron.uhabits.utils.currentTheme
|
||||||
import org.isoron.uhabits.utils.dismissCurrentAndShow
|
import org.isoron.uhabits.utils.dismissCurrentAndShow
|
||||||
@@ -76,7 +71,6 @@ import org.isoron.uhabits.utils.showSendEmailScreen
|
|||||||
import org.isoron.uhabits.utils.showSendFileScreen
|
import org.isoron.uhabits.utils.showSendFileScreen
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
const val RESULT_IMPORT_DATA = 101
|
const val RESULT_IMPORT_DATA = 101
|
||||||
@@ -101,7 +95,7 @@ class ListHabitsScreen
|
|||||||
private val colorPickerFactory: ColorPickerDialogFactory,
|
private val colorPickerFactory: ColorPickerDialogFactory,
|
||||||
private val behavior: Lazy<ListHabitsBehavior>,
|
private val behavior: Lazy<ListHabitsBehavior>,
|
||||||
private val preferences: Preferences,
|
private val preferences: Preferences,
|
||||||
private val rootView: Lazy<ListHabitsRootView>
|
private val rootView: Lazy<ListHabitsRootView>,
|
||||||
) : CommandRunner.Listener,
|
) : CommandRunner.Listener,
|
||||||
ListHabitsBehavior.Screen,
|
ListHabitsBehavior.Screen,
|
||||||
ListHabitsMenuBehavior.Screen,
|
ListHabitsMenuBehavior.Screen,
|
||||||
@@ -223,28 +217,6 @@ class ListHabitsScreen
|
|||||||
activity.showSendFileScreen(filename)
|
activity.showSendFileScreen(filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showConfetti(color: PaletteColor, x: Float, y: Float) {
|
|
||||||
val baseColor = themeSwitcher.currentTheme!!.color(color).toInt()
|
|
||||||
rootView.get().konfettiView.start(
|
|
||||||
Party(
|
|
||||||
speed = 0f,
|
|
||||||
maxSpeed = 16f,
|
|
||||||
damping = 0.9f,
|
|
||||||
spread = 360,
|
|
||||||
angle = 0,
|
|
||||||
colors = listOf(
|
|
||||||
ColorUtils.changeHue(baseColor, 180f),
|
|
||||||
ColorUtils.changeHue(baseColor, 20f),
|
|
||||||
ColorUtils.changeHue(baseColor, -20f),
|
|
||||||
baseColor
|
|
||||||
),
|
|
||||||
position = Position.Absolute(x, y),
|
|
||||||
emitter = Emitter(duration = 25, TimeUnit.MILLISECONDS).max(25),
|
|
||||||
timeToLive = 0
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showSettingsScreen() {
|
override fun showSettingsScreen() {
|
||||||
val intent = intentFactory.startSettingsActivity(activity)
|
val intent = intentFactory.startSettingsActivity(activity)
|
||||||
activity.startActivityForResult(intent, REQUEST_SETTINGS)
|
activity.startActivityForResult(intent, REQUEST_SETTINGS)
|
||||||
@@ -261,14 +233,17 @@ class ListHabitsScreen
|
|||||||
notes: String,
|
notes: String,
|
||||||
callback: ListHabitsBehavior.NumberPickerCallback
|
callback: ListHabitsBehavior.NumberPickerCallback
|
||||||
) {
|
) {
|
||||||
val fm = (context as AppCompatActivity).supportFragmentManager
|
val view = rootView.get()
|
||||||
val dialog = NumberDialog()
|
NumberPopup(
|
||||||
dialog.arguments = Bundle().apply {
|
context = context,
|
||||||
putDouble("value", value)
|
prefs = preferences,
|
||||||
putString("notes", notes)
|
anchor = view,
|
||||||
|
notes = notes,
|
||||||
|
value = value,
|
||||||
|
).apply {
|
||||||
|
onToggle = { value, notes -> callback.onNumberPicked(value, notes) }
|
||||||
|
show()
|
||||||
}
|
}
|
||||||
dialog.onToggle = { v, n, x, y -> callback.onNumberPicked(v, n, x, y) }
|
|
||||||
dialog.dismissCurrentAndShow(fm, "numberDialog")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showCheckmarkPopup(
|
override fun showCheckmarkPopup(
|
||||||
@@ -277,16 +252,18 @@ class ListHabitsScreen
|
|||||||
color: PaletteColor,
|
color: PaletteColor,
|
||||||
callback: ListHabitsBehavior.CheckMarkDialogCallback
|
callback: ListHabitsBehavior.CheckMarkDialogCallback
|
||||||
) {
|
) {
|
||||||
val theme = rootView.get().currentTheme()
|
val view = rootView.get()
|
||||||
val fm = (context as AppCompatActivity).supportFragmentManager
|
CheckmarkPopup(
|
||||||
val dialog = CheckmarkDialog()
|
context = context,
|
||||||
dialog.arguments = Bundle().apply {
|
prefs = preferences,
|
||||||
putInt("color", theme.color(color).toInt())
|
anchor = view,
|
||||||
putInt("value", selectedValue)
|
color = view.currentTheme().color(color).toInt(),
|
||||||
putString("notes", notes)
|
notes = notes,
|
||||||
|
value = selectedValue,
|
||||||
|
).apply {
|
||||||
|
onToggle = { value, notes -> callback.onNotesSaved(value, notes) }
|
||||||
|
show()
|
||||||
}
|
}
|
||||||
dialog.onToggle = { v, n, x, y -> callback.onNotesSaved(v, n, x, y) }
|
|
||||||
dialog.dismissCurrentAndShow(fm, "checkmarkDialog")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getExecuteString(command: Command): String? {
|
private fun getExecuteString(command: Command): String? {
|
||||||
@@ -348,11 +325,8 @@ class ListHabitsScreen
|
|||||||
private fun onExportDB() {
|
private fun onExportDB() {
|
||||||
taskRunner.execute(
|
taskRunner.execute(
|
||||||
exportDBFactory.create { filename ->
|
exportDBFactory.create { filename ->
|
||||||
if (filename != null) {
|
if (filename != null) activity.showSendFileScreen(filename)
|
||||||
activity.showSendFileScreen(filename)
|
else activity.showMessage(activity.resources.getString(R.string.could_not_export))
|
||||||
} else {
|
|
||||||
activity.showMessage(activity.resources.getString(R.string.could_not_export))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,11 +60,8 @@ abstract class ButtonPanelView<T : View>(
|
|||||||
repeat(buttonCount) { buttons.add(createButton()) }
|
repeat(buttonCount) { buttons.add(createButton()) }
|
||||||
|
|
||||||
removeAllViews()
|
removeAllViews()
|
||||||
if (reverse) {
|
if (reverse) buttons.reversed().forEach { addView(it) }
|
||||||
buttons.reversed().forEach { addView(it) }
|
else buttons.forEach { addView(it) }
|
||||||
} else {
|
|
||||||
buttons.forEach { addView(it) }
|
|
||||||
}
|
|
||||||
setupButtons()
|
setupButtons()
|
||||||
requestLayout()
|
requestLayout()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ import org.isoron.uhabits.utils.sres
|
|||||||
import org.isoron.uhabits.utils.toMeasureSpec
|
import org.isoron.uhabits.utils.toMeasureSpec
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
const val TOGGLE_DELAY_MILLIS = 2000L
|
||||||
|
|
||||||
class CheckmarkButtonViewFactory
|
class CheckmarkButtonViewFactory
|
||||||
@Inject constructor(
|
@Inject constructor(
|
||||||
@ActivityContext val context: Context,
|
@ActivityContext val context: Context,
|
||||||
@@ -77,7 +79,7 @@ class CheckmarkButtonView(
|
|||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
var onToggle: (Int, String) -> Unit = { _, _ -> }
|
var onToggle: (Int, String, Long) -> Unit = { _, _, _ -> }
|
||||||
|
|
||||||
var onEdit: () -> Unit = { }
|
var onEdit: () -> Unit = { }
|
||||||
|
|
||||||
@@ -88,31 +90,25 @@ class CheckmarkButtonView(
|
|||||||
setOnLongClickListener(this)
|
setOnLongClickListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun performToggle() {
|
fun performToggle(delay: Long) {
|
||||||
value = Entry.nextToggleValue(
|
value = Entry.nextToggleValue(
|
||||||
value = value,
|
value = value,
|
||||||
isSkipEnabled = preferences.isSkipEnabled,
|
isSkipEnabled = preferences.isSkipEnabled,
|
||||||
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
|
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
|
||||||
)
|
)
|
||||||
onToggle(value, notes)
|
onToggle(value, notes, delay)
|
||||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
override fun onClick(v: View) {
|
||||||
if (preferences.isShortToggleEnabled) {
|
if (preferences.isShortToggleEnabled) performToggle(TOGGLE_DELAY_MILLIS)
|
||||||
performToggle()
|
else onEdit()
|
||||||
} else {
|
|
||||||
onEdit()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLongClick(v: View): Boolean {
|
override fun onLongClick(v: View): Boolean {
|
||||||
if (preferences.isShortToggleEnabled) {
|
if (preferences.isShortToggleEnabled) onEdit()
|
||||||
onEdit()
|
else performToggle(TOGGLE_DELAY_MILLIS)
|
||||||
} else {
|
|
||||||
performToggle()
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,11 +142,8 @@ class CheckmarkButtonView(
|
|||||||
paint.color = when (value) {
|
paint.color = when (value) {
|
||||||
YES_MANUAL, YES_AUTO, SKIP -> color
|
YES_MANUAL, YES_AUTO, SKIP -> color
|
||||||
NO -> {
|
NO -> {
|
||||||
if (preferences.areQuestionMarksEnabled) {
|
if (preferences.areQuestionMarksEnabled) mediumContrastColor
|
||||||
mediumContrastColor
|
else lowContrastColor
|
||||||
} else {
|
|
||||||
lowContrastColor
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else -> lowContrastColor
|
else -> lowContrastColor
|
||||||
}
|
}
|
||||||
@@ -158,11 +151,8 @@ class CheckmarkButtonView(
|
|||||||
SKIP -> R.string.fa_skipped
|
SKIP -> R.string.fa_skipped
|
||||||
NO -> R.string.fa_times
|
NO -> R.string.fa_times
|
||||||
UNKNOWN -> {
|
UNKNOWN -> {
|
||||||
if (preferences.areQuestionMarksEnabled) {
|
if (preferences.areQuestionMarksEnabled) R.string.fa_question
|
||||||
R.string.fa_question
|
else R.string.fa_times
|
||||||
} else {
|
|
||||||
R.string.fa_times
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else -> R.string.fa_check
|
else -> R.string.fa_check
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class CheckmarkPanelView(
|
|||||||
setupButtons()
|
setupButtons()
|
||||||
}
|
}
|
||||||
|
|
||||||
var onToggle: (Timestamp, Int, String) -> Unit = { _, _, _ -> }
|
var onToggle: (Timestamp, Int, String, Long) -> Unit = { _, _, _, _ -> }
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
setupButtons()
|
setupButtons()
|
||||||
@@ -89,7 +89,7 @@ class CheckmarkPanelView(
|
|||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
button.color = color
|
button.color = color
|
||||||
button.onToggle = { value, notes -> onToggle(timestamp, value, notes) }
|
button.onToggle = { value, notes, delay -> onToggle(timestamp, value, notes, delay) }
|
||||||
button.onEdit = { onEdit(timestamp) }
|
button.onEdit = { onEdit(timestamp) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,11 +156,10 @@ class HabitCardListController @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun notifyListener() {
|
private fun notifyListener() {
|
||||||
if (activeMode is SelectionMode) {
|
if (activeMode is SelectionMode)
|
||||||
selectionMenu.get().onSelectionChange()
|
selectionMenu.get().onSelectionChange()
|
||||||
} else {
|
else
|
||||||
selectionMenu.get().onSelectionFinish()
|
selectionMenu.get().onSelectionFinish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
package org.isoron.uhabits.activities.habits.list.views
|
package org.isoron.uhabits.activities.habits.list.views
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.PointF
|
|
||||||
import android.graphics.text.LineBreaker.BREAK_STRATEGY_BALANCED
|
import android.graphics.text.LineBreaker.BREAK_STRATEGY_BALANCED
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Build.VERSION.SDK_INT
|
import android.os.Build.VERSION.SDK_INT
|
||||||
@@ -58,6 +57,13 @@ class HabitCardViewFactory
|
|||||||
fun create() = HabitCardView(context, checkmarkPanelFactory, numberPanelFactory, behavior)
|
fun create() = HabitCardView(context, checkmarkPanelFactory, numberPanelFactory, behavior)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class DelayedToggle(
|
||||||
|
var habit: Habit,
|
||||||
|
var timestamp: Timestamp,
|
||||||
|
var value: Int,
|
||||||
|
var notes: String
|
||||||
|
)
|
||||||
|
|
||||||
class HabitCardView(
|
class HabitCardView(
|
||||||
@ActivityContext context: Context,
|
@ActivityContext context: Context,
|
||||||
checkmarkPanelFactory: CheckmarkPanelViewFactory,
|
checkmarkPanelFactory: CheckmarkPanelViewFactory,
|
||||||
@@ -130,6 +136,7 @@ class HabitCardView(
|
|||||||
private var scoreRing: RingView
|
private var scoreRing: RingView
|
||||||
|
|
||||||
private var currentToggleTaskId = 0
|
private var currentToggleTaskId = 0
|
||||||
|
private var queuedToggles = mutableListOf<DelayedToggle>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
scoreRing = RingView(context).apply {
|
scoreRing = RingView(context).apply {
|
||||||
@@ -153,18 +160,11 @@ class HabitCardView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkmarkPanel = checkmarkPanelFactory.create().apply {
|
checkmarkPanel = checkmarkPanelFactory.create().apply {
|
||||||
onToggle = { timestamp, value, notes ->
|
onToggle = { timestamp, value, notes, delay ->
|
||||||
triggerRipple(timestamp)
|
if (delay > 0) triggerRipple(timestamp)
|
||||||
val location = getAbsoluteButtonLocation(timestamp)
|
|
||||||
habit?.let {
|
habit?.let {
|
||||||
behavior.onToggle(
|
val taskId = queueToggle(it, timestamp, value, notes);
|
||||||
it,
|
{ runPendingToggles(taskId) }.delay(delay)
|
||||||
timestamp,
|
|
||||||
value,
|
|
||||||
notes,
|
|
||||||
location.x,
|
|
||||||
location.y
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onEdit = { timestamp ->
|
onEdit = { timestamp ->
|
||||||
@@ -205,6 +205,25 @@ class HabitCardView(
|
|||||||
addView(innerFrame)
|
addView(innerFrame)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun runPendingToggles(id: Int) {
|
||||||
|
if (currentToggleTaskId != id) return
|
||||||
|
for ((h, t, v, n) in queuedToggles) behavior.onToggle(h, t, v, n)
|
||||||
|
queuedToggles.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun queueToggle(
|
||||||
|
it: Habit,
|
||||||
|
timestamp: Timestamp,
|
||||||
|
value: Int,
|
||||||
|
notes: String,
|
||||||
|
): Int {
|
||||||
|
currentToggleTaskId += 1
|
||||||
|
queuedToggles.add(DelayedToggle(it, timestamp, value, notes))
|
||||||
|
return currentToggleTaskId
|
||||||
|
}
|
||||||
|
|
||||||
override fun onModelChange() {
|
override fun onModelChange() {
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
habit?.let { copyAttributesFrom(it) }
|
habit?.let { copyAttributesFrom(it) }
|
||||||
@@ -217,27 +236,12 @@ class HabitCardView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun triggerRipple(timestamp: Timestamp) {
|
fun triggerRipple(timestamp: Timestamp) {
|
||||||
val location = getRelativeButtonLocation(timestamp)
|
|
||||||
triggerRipple(location.x, location.y)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getRelativeButtonLocation(timestamp: Timestamp): PointF {
|
|
||||||
val today = DateUtils.getTodayWithOffset()
|
val today = DateUtils.getTodayWithOffset()
|
||||||
val offset = timestamp.daysUntil(today) - dataOffset
|
val offset = timestamp.daysUntil(today) - dataOffset
|
||||||
val button = checkmarkPanel.buttons[offset]
|
val button = checkmarkPanel.buttons[offset]
|
||||||
val y = button.height / 2.0f
|
val y = button.height / 2.0f
|
||||||
val x = checkmarkPanel.x + button.x + (button.width / 2).toFloat()
|
val x = checkmarkPanel.x + button.x + (button.width / 2).toFloat()
|
||||||
return PointF(x, y)
|
triggerRipple(x, y)
|
||||||
}
|
|
||||||
|
|
||||||
private fun getAbsoluteButtonLocation(timestamp: Timestamp): PointF {
|
|
||||||
val containerLocation = IntArray(2)
|
|
||||||
this.getLocationOnScreen(containerLocation)
|
|
||||||
val relButtonLocation = getRelativeButtonLocation(timestamp)
|
|
||||||
return PointF(
|
|
||||||
containerLocation[0].toFloat() + relButtonLocation.x,
|
|
||||||
containerLocation[1].toFloat() - relButtonLocation.y
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttachedToWindow() {
|
override fun onAttachedToWindow() {
|
||||||
@@ -251,6 +255,7 @@ class HabitCardView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun copyAttributesFrom(h: Habit) {
|
private fun copyAttributesFrom(h: Habit) {
|
||||||
|
|
||||||
fun getActiveColor(habit: Habit): Int {
|
fun getActiveColor(habit: Habit): Int {
|
||||||
return when (habit.isArchived) {
|
return when (habit.isArchived) {
|
||||||
true -> sres.getColor(R.attr.contrast60)
|
true -> sres.getColor(R.attr.contrast60)
|
||||||
@@ -296,6 +301,7 @@ class HabitCardView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateBackground(isSelected: Boolean) {
|
private fun updateBackground(isSelected: Boolean) {
|
||||||
|
|
||||||
val background = when (isSelected) {
|
val background = when (isSelected) {
|
||||||
true -> R.drawable.selected_box
|
true -> R.drawable.selected_box
|
||||||
false -> R.drawable.ripple
|
false -> R.drawable.ripple
|
||||||
|
|||||||
@@ -124,20 +124,15 @@ class HeaderView(
|
|||||||
rect.set(0f, 0f, width, height)
|
rect.set(0f, 0f, width, height)
|
||||||
rect.offset(canvas.width.toFloat() - dp(3.0f), 0f)
|
rect.offset(canvas.width.toFloat() - dp(3.0f), 0f)
|
||||||
|
|
||||||
if (isReversed) {
|
if (isReversed) rect.offset(-(index + 1) * width, 0f)
|
||||||
rect.offset(-(index + 1) * width, 0f)
|
else rect.offset((index - buttonCount) * width, 0f)
|
||||||
} else {
|
|
||||||
rect.offset((index - buttonCount) * width, 0f)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRTL()) {
|
if (isRTL()) rect.set(
|
||||||
rect.set(
|
|
||||||
canvas.width - rect.right,
|
canvas.width - rect.right,
|
||||||
rect.top,
|
rect.top,
|
||||||
canvas.width - rect.left,
|
canvas.width - rect.left,
|
||||||
rect.bottom
|
rect.bottom
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
val y1 = rect.centerY() - 0.25 * em
|
val y1 = rect.centerY() - 0.25 * em
|
||||||
val y2 = rect.centerY() + 1.25 * em
|
val y2 = rect.centerY() + 1.25 * em
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ import org.isoron.uhabits.HabitsApplication
|
|||||||
import org.isoron.uhabits.R
|
import org.isoron.uhabits.R
|
||||||
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
||||||
import org.isoron.uhabits.activities.HabitsDirFinder
|
import org.isoron.uhabits.activities.HabitsDirFinder
|
||||||
import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog
|
import org.isoron.uhabits.activities.common.dialogs.CheckmarkPopup
|
||||||
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
|
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
|
||||||
import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog
|
import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog
|
||||||
import org.isoron.uhabits.activities.common.dialogs.NumberDialog
|
import org.isoron.uhabits.activities.common.dialogs.NumberPopup
|
||||||
import org.isoron.uhabits.core.commands.Command
|
import org.isoron.uhabits.core.commands.Command
|
||||||
import org.isoron.uhabits.core.commands.CommandRunner
|
import org.isoron.uhabits.core.commands.CommandRunner
|
||||||
import org.isoron.uhabits.core.models.Habit
|
import org.isoron.uhabits.core.models.Habit
|
||||||
@@ -49,10 +49,8 @@ import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitMenuPresenter
|
|||||||
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitPresenter
|
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitPresenter
|
||||||
import org.isoron.uhabits.core.ui.views.OnDateClickedListener
|
import org.isoron.uhabits.core.ui.views.OnDateClickedListener
|
||||||
import org.isoron.uhabits.intents.IntentFactory
|
import org.isoron.uhabits.intents.IntentFactory
|
||||||
import org.isoron.uhabits.utils.applyRootViewInsets
|
|
||||||
import org.isoron.uhabits.utils.currentTheme
|
import org.isoron.uhabits.utils.currentTheme
|
||||||
import org.isoron.uhabits.utils.dismissCurrentAndShow
|
import org.isoron.uhabits.utils.dismissCurrentAndShow
|
||||||
import org.isoron.uhabits.utils.dismissCurrentDialog
|
|
||||||
import org.isoron.uhabits.utils.showMessage
|
import org.isoron.uhabits.utils.showMessage
|
||||||
import org.isoron.uhabits.utils.showSendFileScreen
|
import org.isoron.uhabits.utils.showSendFileScreen
|
||||||
import org.isoron.uhabits.widgets.WidgetUpdater
|
import org.isoron.uhabits.widgets.WidgetUpdater
|
||||||
@@ -89,7 +87,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
|||||||
habit = habit,
|
habit = habit,
|
||||||
habitList = habitList,
|
habitList = habitList,
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
screen = screen
|
screen = screen,
|
||||||
)
|
)
|
||||||
|
|
||||||
view = ShowHabitView(this)
|
view = ShowHabitView(this)
|
||||||
@@ -100,17 +98,16 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
|||||||
habitList = habitList,
|
habitList = habitList,
|
||||||
screen = screen,
|
screen = screen,
|
||||||
system = HabitsDirFinder(AndroidDirFinder(this)),
|
system = HabitsDirFinder(AndroidDirFinder(this)),
|
||||||
taskRunner = appComponent.taskRunner
|
taskRunner = appComponent.taskRunner,
|
||||||
)
|
)
|
||||||
|
|
||||||
menu = ShowHabitMenu(
|
menu = ShowHabitMenu(
|
||||||
activity = this,
|
activity = this,
|
||||||
presenter = menuPresenter,
|
presenter = menuPresenter,
|
||||||
preferences = preferences
|
preferences = preferences,
|
||||||
)
|
)
|
||||||
|
|
||||||
view.setListener(presenter)
|
view.setListener(presenter)
|
||||||
view.applyRootViewInsets()
|
|
||||||
setContentView(view)
|
setContentView(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +129,6 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
dismissCurrentDialog()
|
|
||||||
commandRunner.removeListener(this)
|
commandRunner.removeListener(this)
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
@@ -152,7 +148,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
|||||||
ShowHabitPresenter.buildState(
|
ShowHabitPresenter.buildState(
|
||||||
habit = habit,
|
habit = habit,
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
theme = themeSwitcher.currentTheme
|
theme = themeSwitcher.currentTheme,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -174,32 +170,41 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
|||||||
override fun showNumberPopup(
|
override fun showNumberPopup(
|
||||||
value: Double,
|
value: Double,
|
||||||
notes: String,
|
notes: String,
|
||||||
|
preferences: Preferences,
|
||||||
callback: ListHabitsBehavior.NumberPickerCallback
|
callback: ListHabitsBehavior.NumberPickerCallback
|
||||||
) {
|
) {
|
||||||
val dialog = NumberDialog()
|
val anchor = getPopupAnchor() ?: return
|
||||||
dialog.arguments = Bundle().apply {
|
NumberPopup(
|
||||||
putDouble("value", value)
|
context = this@ShowHabitActivity,
|
||||||
putString("notes", notes)
|
prefs = preferences,
|
||||||
|
notes = notes,
|
||||||
|
anchor = anchor,
|
||||||
|
value = value,
|
||||||
|
).apply {
|
||||||
|
onToggle = { v, n -> callback.onNumberPicked(v, n) }
|
||||||
|
show()
|
||||||
}
|
}
|
||||||
dialog.onToggle = { v, n, x, y -> callback.onNumberPicked(v, n, x, y) }
|
|
||||||
dialog.dismissCurrentAndShow(supportFragmentManager, "numberDialog")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showCheckmarkPopup(
|
override fun showCheckmarkPopup(
|
||||||
selectedValue: Int,
|
selectedValue: Int,
|
||||||
notes: String,
|
notes: String,
|
||||||
|
preferences: Preferences,
|
||||||
color: PaletteColor,
|
color: PaletteColor,
|
||||||
callback: ListHabitsBehavior.CheckMarkDialogCallback
|
callback: ListHabitsBehavior.CheckMarkDialogCallback
|
||||||
) {
|
) {
|
||||||
val theme = view.currentTheme()
|
val anchor = getPopupAnchor() ?: return
|
||||||
val dialog = CheckmarkDialog()
|
CheckmarkPopup(
|
||||||
dialog.arguments = Bundle().apply {
|
context = this@ShowHabitActivity,
|
||||||
putInt("color", theme.color(color).toInt())
|
prefs = preferences,
|
||||||
putInt("value", selectedValue)
|
notes = notes,
|
||||||
putString("notes", notes)
|
color = view.currentTheme().color(color).toInt(),
|
||||||
|
anchor = anchor,
|
||||||
|
value = selectedValue,
|
||||||
|
).apply {
|
||||||
|
onToggle = { v, n -> callback.onNotesSaved(v, n) }
|
||||||
|
show()
|
||||||
}
|
}
|
||||||
dialog.onToggle = { v, n, x, y -> callback.onNotesSaved(v, n, x, y) }
|
|
||||||
dialog.dismissCurrentAndShow(supportFragmentManager, "checkmarkDialog")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPopupAnchor(): View? {
|
private fun getPopupAnchor(): View? {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitMenuPresenter
|
|||||||
class ShowHabitMenu(
|
class ShowHabitMenu(
|
||||||
val activity: ShowHabitActivity,
|
val activity: ShowHabitActivity,
|
||||||
val presenter: ShowHabitMenuPresenter,
|
val presenter: ShowHabitMenuPresenter,
|
||||||
val preferences: Preferences
|
val preferences: Preferences,
|
||||||
) {
|
) {
|
||||||
fun onCreateOptionsMenu(menu: Menu): Boolean {
|
fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
activity.menuInflater.inflate(R.menu.show_habit, menu)
|
activity.menuInflater.inflate(R.menu.show_habit, menu)
|
||||||
|
|||||||
@@ -25,14 +25,12 @@ import android.widget.FrameLayout
|
|||||||
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitPresenter
|
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitPresenter
|
||||||
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitState
|
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitState
|
||||||
import org.isoron.uhabits.databinding.ShowHabitBinding
|
import org.isoron.uhabits.databinding.ShowHabitBinding
|
||||||
import org.isoron.uhabits.utils.applyToolbarInsets
|
|
||||||
import org.isoron.uhabits.utils.setupToolbar
|
import org.isoron.uhabits.utils.setupToolbar
|
||||||
|
|
||||||
class ShowHabitView(context: Context) : FrameLayout(context) {
|
class ShowHabitView(context: Context) : FrameLayout(context) {
|
||||||
private val binding = ShowHabitBinding.inflate(LayoutInflater.from(context))
|
private val binding = ShowHabitBinding.inflate(LayoutInflater.from(context))
|
||||||
|
|
||||||
init {
|
init {
|
||||||
binding.toolbar.applyToolbarInsets()
|
|
||||||
addView(binding.root)
|
addView(binding.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +39,7 @@ class ShowHabitView(context: Context) : FrameLayout(context) {
|
|||||||
binding.toolbar,
|
binding.toolbar,
|
||||||
title = data.title,
|
title = data.title,
|
||||||
color = data.color,
|
color = data.color,
|
||||||
theme = data.theme
|
theme = data.theme,
|
||||||
)
|
)
|
||||||
binding.subtitleCard.setState(data.subtitle)
|
binding.subtitleCard.setState(data.subtitle)
|
||||||
binding.overviewCard.setState(data.overview)
|
binding.overviewCard.setState(data.overview)
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class BarCardView(context: Context, attrs: AttributeSet) : LinearLayout(context,
|
|||||||
|
|
||||||
fun setState(state: BarCardState) {
|
fun setState(state: BarCardState) {
|
||||||
val androidColor = state.theme.color(state.color).toInt()
|
val androidColor = state.theme.color(state.color).toInt()
|
||||||
binding.chart.view = BarChart(state.theme, JavaLocalDateFormatter(Locale.getDefault())).apply {
|
binding.chart.view = BarChart(state.theme, JavaLocalDateFormatter(Locale.US)).apply {
|
||||||
series = mutableListOf(state.entries.map { it.value / 1000.0 })
|
series = mutableListOf(state.entries.map { it.value / 1000.0 })
|
||||||
colors = mutableListOf(theme.color(state.color.paletteIndex))
|
colors = mutableListOf(theme.color(state.color.paletteIndex))
|
||||||
axis = state.entries.map { it.timestamp.toLocalDate() }
|
axis = state.entries.map { it.timestamp.toLocalDate() }
|
||||||
@@ -63,7 +63,7 @@ class BarCardView(context: Context, attrs: AttributeSet) : LinearLayout(context,
|
|||||||
parent: AdapterView<*>?,
|
parent: AdapterView<*>?,
|
||||||
view: View?,
|
view: View?,
|
||||||
position: Int,
|
position: Int,
|
||||||
id: Long
|
id: Long,
|
||||||
) {
|
) {
|
||||||
presenter.onBoolSpinnerPosition(position)
|
presenter.onBoolSpinnerPosition(position)
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,7 @@ class BarCardView(context: Context, attrs: AttributeSet) : LinearLayout(context,
|
|||||||
parent: AdapterView<*>?,
|
parent: AdapterView<*>?,
|
||||||
view: View?,
|
view: View?,
|
||||||
position: Int,
|
position: Int,
|
||||||
id: Long
|
id: Long,
|
||||||
) {
|
) {
|
||||||
presenter.onNumericalSpinnerPosition(position)
|
presenter.onNumericalSpinnerPosition(position)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class HistoryCardView(context: Context, attrs: AttributeSet) : LinearLayout(cont
|
|||||||
series = state.series,
|
series = state.series,
|
||||||
defaultSquare = state.defaultSquare,
|
defaultSquare = state.defaultSquare,
|
||||||
notesIndicators = state.notesIndicators,
|
notesIndicators = state.notesIndicators,
|
||||||
firstWeekday = state.firstWeekday
|
firstWeekday = state.firstWeekday,
|
||||||
)
|
)
|
||||||
binding.chart.postInvalidate()
|
binding.chart.postInvalidate()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
|
|||||||
binding.frequencyLabel.text = formatFrequency(
|
binding.frequencyLabel.text = formatFrequency(
|
||||||
state.frequency.numerator,
|
state.frequency.numerator,
|
||||||
state.frequency.denominator,
|
state.frequency.denominator,
|
||||||
resources
|
resources,
|
||||||
)
|
)
|
||||||
binding.questionLabel.setTextColor(color)
|
binding.questionLabel.setTextColor(color)
|
||||||
binding.questionLabel.text = state.question
|
binding.questionLabel.text = state.question
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import org.isoron.uhabits.R
|
|||||||
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
||||||
import org.isoron.uhabits.core.models.PaletteColor
|
import org.isoron.uhabits.core.models.PaletteColor
|
||||||
import org.isoron.uhabits.databinding.SettingsActivityBinding
|
import org.isoron.uhabits.databinding.SettingsActivityBinding
|
||||||
import org.isoron.uhabits.utils.applyRootViewInsets
|
|
||||||
import org.isoron.uhabits.utils.setupToolbar
|
import org.isoron.uhabits.utils.setupToolbar
|
||||||
|
|
||||||
class SettingsActivity : AppCompatActivity() {
|
class SettingsActivity : AppCompatActivity() {
|
||||||
@@ -41,9 +40,8 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
toolbar = binding.toolbar,
|
toolbar = binding.toolbar,
|
||||||
title = resources.getString(R.string.settings),
|
title = resources.getString(R.string.settings),
|
||||||
color = PaletteColor(11),
|
color = PaletteColor(11),
|
||||||
theme = themeSwitcher.currentTheme
|
theme = themeSwitcher.currentTheme,
|
||||||
)
|
)
|
||||||
binding.root.applyRootViewInsets()
|
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import android.app.backup.BackupManager
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -44,7 +43,6 @@ import org.isoron.uhabits.core.utils.DateUtils.Companion.getLongWeekdayNames
|
|||||||
import org.isoron.uhabits.notifications.AndroidNotificationTray.Companion.createAndroidNotificationChannel
|
import org.isoron.uhabits.notifications.AndroidNotificationTray.Companion.createAndroidNotificationChannel
|
||||||
import org.isoron.uhabits.notifications.RingtoneManager
|
import org.isoron.uhabits.notifications.RingtoneManager
|
||||||
import org.isoron.uhabits.utils.StyledResources
|
import org.isoron.uhabits.utils.StyledResources
|
||||||
import org.isoron.uhabits.utils.startActivitySafely
|
|
||||||
import org.isoron.uhabits.widgets.WidgetUpdater
|
import org.isoron.uhabits.widgets.WidgetUpdater
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
|
|
||||||
@@ -53,8 +51,6 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
|
|||||||
private var ringtoneManager: RingtoneManager? = null
|
private var ringtoneManager: RingtoneManager? = null
|
||||||
private lateinit var prefs: Preferences
|
private lateinit var prefs: Preferences
|
||||||
private var widgetUpdater: WidgetUpdater? = null
|
private var widgetUpdater: WidgetUpdater? = null
|
||||||
|
|
||||||
@Deprecated("Deprecated in Java")
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
if (requestCode == RINGTONE_REQUEST_CODE) {
|
if (requestCode == RINGTONE_REQUEST_CODE) {
|
||||||
ringtoneManager!!.update(data)
|
ringtoneManager!!.update(data)
|
||||||
@@ -96,12 +92,10 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
|
|||||||
|
|
||||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||||
val key = preference.key ?: return false
|
val key = preference.key ?: return false
|
||||||
when (key) {
|
if (key == "reminderSound") {
|
||||||
"reminderSound" -> {
|
|
||||||
showRingtonePicker()
|
showRingtonePicker()
|
||||||
return true
|
return true
|
||||||
}
|
} else if (key == "reminderCustomize") {
|
||||||
"reminderCustomize" -> {
|
|
||||||
createAndroidNotificationChannel(requireContext())
|
createAndroidNotificationChannel(requireContext())
|
||||||
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
|
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
|
||||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
|
intent.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
|
||||||
@@ -109,12 +103,6 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
|
|||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
"rateApp" -> {
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.playStoreURL)))
|
|
||||||
activity?.startActivitySafely(intent)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.onPreferenceTreeClick(preference)
|
return super.onPreferenceTreeClick(preference)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.activities.sync
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.EditText
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import org.isoron.uhabits.HabitsApplication
|
||||||
|
import org.isoron.uhabits.R
|
||||||
|
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
||||||
|
import org.isoron.uhabits.core.models.PaletteColor
|
||||||
|
import org.isoron.uhabits.databinding.SyncActivityBinding
|
||||||
|
import org.isoron.uhabits.utils.setupToolbar
|
||||||
|
|
||||||
|
class SyncActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: SyncActivityBinding
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
val component = (application as HabitsApplication).component
|
||||||
|
val themeSwitcher = AndroidThemeSwitcher(this, component.preferences)
|
||||||
|
themeSwitcher.apply()
|
||||||
|
|
||||||
|
binding = SyncActivityBinding.inflate(LayoutInflater.from(this))
|
||||||
|
binding.root.setupToolbar(
|
||||||
|
toolbar = binding.toolbar,
|
||||||
|
title = resources.getString(R.string.device_sync),
|
||||||
|
color = PaletteColor(11),
|
||||||
|
theme = themeSwitcher.currentTheme,
|
||||||
|
)
|
||||||
|
binding.generateButton.setOnClickListener { onGenerateCode() }
|
||||||
|
binding.enterButton.setOnClickListener {
|
||||||
|
val et = EditText(this)
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle(R.string.sync_code)
|
||||||
|
.setView(et)
|
||||||
|
.setPositiveButton(R.string.save) { _, _ ->
|
||||||
|
onEnterCode(et.text.toString())
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
binding.disableButton.setOnClickListener {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle(R.string.disable_sync)
|
||||||
|
.setMessage(R.string.disable_sync_description)
|
||||||
|
.setPositiveButton(R.string.disable) { _, _ ->
|
||||||
|
onDisableSync()
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.keep_enabled) { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
setContentView(binding.root)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onGenerateCode() {
|
||||||
|
showCodeScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onEnterCode(code: String) {
|
||||||
|
showCodeScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onDisableSync() {
|
||||||
|
showIntroScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showCodeScreen() {
|
||||||
|
binding.introGroup.visibility = View.GONE
|
||||||
|
binding.codeGroup.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showIntroScreen() {
|
||||||
|
binding.introGroup.visibility = View.VISIBLE
|
||||||
|
binding.codeGroup.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,6 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import org.isoron.uhabits.HabitsApplication
|
import org.isoron.uhabits.HabitsApplication
|
||||||
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
||||||
import org.isoron.uhabits.core.models.HabitMatcher
|
import org.isoron.uhabits.core.models.HabitMatcher
|
||||||
import org.isoron.uhabits.utils.applyRootViewInsets
|
|
||||||
|
|
||||||
class EditSettingActivity : AppCompatActivity() {
|
class EditSettingActivity : AppCompatActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -33,7 +32,7 @@ class EditSettingActivity : AppCompatActivity() {
|
|||||||
val habits = app.component.habitList.getFiltered(
|
val habits = app.component.habitList.getFiltered(
|
||||||
HabitMatcher(
|
HabitMatcher(
|
||||||
isArchivedAllowed = false,
|
isArchivedAllowed = false,
|
||||||
isCompletedAllowed = true
|
isCompletedAllowed = true,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
AndroidThemeSwitcher(this, app.component.preferences).apply()
|
AndroidThemeSwitcher(this, app.component.preferences).apply()
|
||||||
@@ -44,9 +43,8 @@ class EditSettingActivity : AppCompatActivity() {
|
|||||||
context = this,
|
context = this,
|
||||||
habitList = app.component.habitList,
|
habitList = app.component.habitList,
|
||||||
onSave = controller::onSave,
|
onSave = controller::onSave,
|
||||||
args = args
|
args = args,
|
||||||
)
|
)
|
||||||
view.applyRootViewInsets()
|
|
||||||
setContentView(view)
|
setContentView(view)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class EditSettingRootView(
|
|||||||
title = resources.getString(R.string.app_name),
|
title = resources.getString(R.string.app_name),
|
||||||
color = PaletteColor(11),
|
color = PaletteColor(11),
|
||||||
displayHomeAsUpEnabled = false,
|
displayHomeAsUpEnabled = false,
|
||||||
theme = currentTheme()
|
theme = currentTheme(),
|
||||||
)
|
)
|
||||||
populateHabitSpinner()
|
populateHabitSpinner()
|
||||||
binding.habitSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
binding.habitSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
@@ -69,7 +69,7 @@ class EditSettingRootView(
|
|||||||
val habit = habitList.getByPosition(binding.habitSpinner.selectedItemPosition)
|
val habit = habitList.getByPosition(binding.habitSpinner.selectedItemPosition)
|
||||||
val action = mapSpinnerPositionToAction(
|
val action = mapSpinnerPositionToAction(
|
||||||
isNumerical = habit.isNumerical,
|
isNumerical = habit.isNumerical,
|
||||||
itemPosition = binding.actionSpinner.selectedItemPosition
|
itemPosition = binding.actionSpinner.selectedItemPosition,
|
||||||
)
|
)
|
||||||
onSave(habit, action)
|
onSave(habit, action)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,34 +27,22 @@ class AndroidCursor(private val cursor: android.database.Cursor) : Cursor {
|
|||||||
override fun moveToNext() = cursor.moveToNext()
|
override fun moveToNext() = cursor.moveToNext()
|
||||||
|
|
||||||
override fun getInt(index: Int): Int? {
|
override fun getInt(index: Int): Int? {
|
||||||
return if (cursor.isNull(index)) {
|
return if (cursor.isNull(index)) null
|
||||||
null
|
else cursor.getInt(index)
|
||||||
} else {
|
|
||||||
cursor.getInt(index)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLong(index: Int): Long? {
|
override fun getLong(index: Int): Long? {
|
||||||
return if (cursor.isNull(index)) {
|
return if (cursor.isNull(index)) null
|
||||||
null
|
else cursor.getLong(index)
|
||||||
} else {
|
|
||||||
cursor.getLong(index)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDouble(index: Int): Double? {
|
override fun getDouble(index: Int): Double? {
|
||||||
return if (cursor.isNull(index)) {
|
return if (cursor.isNull(index)) null
|
||||||
null
|
else cursor.getDouble(index)
|
||||||
} else {
|
|
||||||
cursor.getDouble(index)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getString(index: Int): String? {
|
override fun getString(index: Int): String? {
|
||||||
return if (cursor.isNull(index)) {
|
return if (cursor.isNull(index)) null
|
||||||
null
|
else cursor.getString(index)
|
||||||
} else {
|
|
||||||
cursor.getString(index)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import java.io.File
|
|||||||
|
|
||||||
class AndroidDatabase(
|
class AndroidDatabase(
|
||||||
private val db: SQLiteDatabase,
|
private val db: SQLiteDatabase,
|
||||||
override val file: File?
|
override val file: File?,
|
||||||
) : Database {
|
) : Database {
|
||||||
|
|
||||||
override fun beginTransaction() = db.beginTransaction()
|
override fun beginTransaction() = db.beginTransaction()
|
||||||
@@ -45,7 +45,7 @@ class AndroidDatabase(
|
|||||||
tableName: String,
|
tableName: String,
|
||||||
values: Map<String, Any?>,
|
values: Map<String, Any?>,
|
||||||
where: String,
|
where: String,
|
||||||
vararg params: String
|
vararg params: String,
|
||||||
): Int {
|
): Int {
|
||||||
val contValues = mapToContentValues(values)
|
val contValues = mapToContentValues(values)
|
||||||
return db.update(tableName, contValues, where, params)
|
return db.update(tableName, contValues, where, params)
|
||||||
@@ -59,7 +59,7 @@ class AndroidDatabase(
|
|||||||
override fun delete(
|
override fun delete(
|
||||||
tableName: String,
|
tableName: String,
|
||||||
where: String,
|
where: String,
|
||||||
vararg params: String
|
vararg params: String,
|
||||||
) {
|
) {
|
||||||
db.delete(tableName, where, params)
|
db.delete(tableName, where, params)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class AndroidDatabaseOpener @Inject constructor() : DatabaseOpener {
|
|||||||
db = SQLiteDatabase.openDatabase(
|
db = SQLiteDatabase.openDatabase(
|
||||||
file.absolutePath,
|
file.absolutePath,
|
||||||
null,
|
null,
|
||||||
SQLiteDatabase.OPEN_READWRITE
|
SQLiteDatabase.OPEN_READWRITE,
|
||||||
),
|
),
|
||||||
file = file
|
file = file
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,6 +25,5 @@ import dagger.Provides
|
|||||||
@Module
|
@Module
|
||||||
class ActivityContextModule(
|
class ActivityContextModule(
|
||||||
@get:Provides
|
@get:Provides
|
||||||
@get:ActivityContext
|
@get:ActivityContext val context: Context
|
||||||
val context: Context
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,6 +26,5 @@ import dagger.Provides
|
|||||||
class AppContextModule(
|
class AppContextModule(
|
||||||
@get:Provides
|
@get:Provides
|
||||||
@get:AppContext
|
@get:AppContext
|
||||||
@param:AppContext
|
@param:AppContext val context: Context
|
||||||
val context: Context
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -53,9 +53,8 @@ class IntentParser
|
|||||||
var timestamp = intent.getLongExtra("timestamp", today)
|
var timestamp = intent.getLongExtra("timestamp", today)
|
||||||
timestamp = DateUtils.getStartOfDay(timestamp)
|
timestamp = DateUtils.getStartOfDay(timestamp)
|
||||||
|
|
||||||
if (timestamp < 0 || timestamp > today) {
|
if (timestamp < 0 || timestamp > today)
|
||||||
throw IllegalArgumentException("timestamp is not valid")
|
throw IllegalArgumentException("timestamp is not valid")
|
||||||
}
|
|
||||||
|
|
||||||
return Timestamp(timestamp)
|
return Timestamp(timestamp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import android.app.AlarmManager.RTC_WAKEUP
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.ALARM_SERVICE
|
import android.content.Context.ALARM_SERVICE
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import org.isoron.uhabits.core.AppScope
|
import org.isoron.uhabits.core.AppScope
|
||||||
import org.isoron.uhabits.core.models.Habit
|
import org.isoron.uhabits.core.models.Habit
|
||||||
@@ -57,10 +56,6 @@ class IntentScheduler
|
|||||||
)
|
)
|
||||||
return SchedulerResult.IGNORED
|
return SchedulerResult.IGNORED
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !manager.canScheduleExactAlarms()) {
|
|
||||||
Log.e("IntentScheduler", "No permission to schedule exact alarms")
|
|
||||||
return SchedulerResult.IGNORED
|
|
||||||
}
|
|
||||||
manager.setExactAndAllowWhileIdle(alarmType, timestamp, intent)
|
manager.setExactAndAllowWhileIdle(alarmType, timestamp, intent)
|
||||||
return SchedulerResult.OK
|
return SchedulerResult.OK
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ class AndroidNotificationTray
|
|||||||
timestamp: Timestamp,
|
timestamp: Timestamp,
|
||||||
disableSound: Boolean = false
|
disableSound: Boolean = false
|
||||||
): Notification {
|
): Notification {
|
||||||
|
|
||||||
val addRepetitionAction = Action(
|
val addRepetitionAction = Action(
|
||||||
R.drawable.ic_action_check,
|
R.drawable.ic_action_check,
|
||||||
context.getString(R.string.yes),
|
context.getString(R.string.yes),
|
||||||
@@ -145,9 +146,8 @@ class AndroidNotificationTray
|
|||||||
.addAction(removeRepetitionAction)
|
.addAction(removeRepetitionAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!disableSound) {
|
if (!disableSound)
|
||||||
builder.setSound(ringtoneManager.getURI())
|
builder.setSound(ringtoneManager.getURI())
|
||||||
}
|
|
||||||
|
|
||||||
if (SDK_INT < Build.VERSION_CODES.S) {
|
if (SDK_INT < Build.VERSION_CODES.S) {
|
||||||
val snoozeAction = Action(
|
val snoozeAction = Action(
|
||||||
|
|||||||
@@ -61,9 +61,8 @@ class RingtoneManager
|
|||||||
"pref_ringtone_uri",
|
"pref_ringtone_uri",
|
||||||
defaultRingtoneUri.toString()
|
defaultRingtoneUri.toString()
|
||||||
)!!
|
)!!
|
||||||
if (prefRingtoneUri.isNotEmpty()) {
|
if (prefRingtoneUri.isNotEmpty())
|
||||||
ringtoneUri = Uri.parse(prefRingtoneUri)
|
ringtoneUri = Uri.parse(prefRingtoneUri)
|
||||||
}
|
|
||||||
|
|
||||||
return ringtoneUri
|
return ringtoneUri
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,8 +33,7 @@ import org.isoron.uhabits.HabitsApplication
|
|||||||
import org.isoron.uhabits.R
|
import org.isoron.uhabits.R
|
||||||
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
||||||
import org.isoron.uhabits.core.models.Habit
|
import org.isoron.uhabits.core.models.Habit
|
||||||
import org.isoron.uhabits.core.ui.views.DarkTheme
|
import org.isoron.uhabits.core.ui.ThemeSwitcher.Companion.THEME_LIGHT
|
||||||
import org.isoron.uhabits.core.ui.views.LightTheme
|
|
||||||
import org.isoron.uhabits.receivers.ReminderController
|
import org.isoron.uhabits.receivers.ReminderController
|
||||||
import org.isoron.uhabits.utils.SystemUtils
|
import org.isoron.uhabits.utils.SystemUtils
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
@@ -52,8 +51,11 @@ class SnoozeDelayPickerActivity : FragmentActivity(), OnItemClickListener {
|
|||||||
val app = applicationContext as HabitsApplication
|
val app = applicationContext as HabitsApplication
|
||||||
val appComponent = app.component
|
val appComponent = app.component
|
||||||
val themeSwitcher = AndroidThemeSwitcher(this, appComponent.preferences)
|
val themeSwitcher = AndroidThemeSwitcher(this, appComponent.preferences)
|
||||||
themeSwitcher.setTheme()
|
if (themeSwitcher.getSystemTheme() == THEME_LIGHT) {
|
||||||
|
setTheme(R.style.BaseDialog)
|
||||||
|
} else {
|
||||||
|
setTheme(R.style.BaseDialogDark)
|
||||||
|
}
|
||||||
val data = intent.data
|
val data = intent.data
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
finish()
|
finish()
|
||||||
@@ -73,16 +75,6 @@ class SnoozeDelayPickerActivity : FragmentActivity(), OnItemClickListener {
|
|||||||
SystemUtils.unlockScreen(this)
|
SystemUtils.unlockScreen(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun AndroidThemeSwitcher.setTheme() {
|
|
||||||
if (this.isNightMode) {
|
|
||||||
setTheme(R.style.BaseDialogDark)
|
|
||||||
this.currentTheme = DarkTheme()
|
|
||||||
} else {
|
|
||||||
setTheme(R.style.BaseDialog)
|
|
||||||
this.currentTheme = LightTheme()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showTimePicker() {
|
private fun showTimePicker() {
|
||||||
val calendar = Calendar.getInstance()
|
val calendar = Calendar.getInstance()
|
||||||
val dialog = TimePickerDialog.newInstance(
|
val dialog = TimePickerDialog.newInstance(
|
||||||
@@ -103,9 +95,7 @@ class SnoozeDelayPickerActivity : FragmentActivity(), OnItemClickListener {
|
|||||||
if (snoozeValues[position] >= 0) {
|
if (snoozeValues[position] >= 0) {
|
||||||
reminderController!!.onSnoozeDelayPicked(habit!!, snoozeValues[position])
|
reminderController!!.onSnoozeDelayPicked(habit!!, snoozeValues[position])
|
||||||
finish()
|
finish()
|
||||||
} else {
|
} else showTimePicker()
|
||||||
showTimePicker()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun finish() {
|
override fun finish() {
|
||||||
|
|||||||
@@ -66,14 +66,8 @@ class ReminderController @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onDismiss(habit: Habit) {
|
fun onDismiss(habit: Habit) {
|
||||||
if (preferences.shouldMakeNotificationsSticky()) {
|
|
||||||
// This is a workaround to keep sticky notifications non-dismissible in Android 14+.
|
|
||||||
// If the notification is dismissed, we immediately reshow it.
|
|
||||||
notificationTray.reshow(habit)
|
|
||||||
} else {
|
|
||||||
notificationTray.cancel(habit)
|
notificationTray.cancel(habit)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun showSnoozeDelayPicker(habit: Habit, context: Context) {
|
private fun showSnoozeDelayPicker(habit: Habit, context: Context) {
|
||||||
context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
|
context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
|
||||||
|
|||||||
@@ -60,14 +60,12 @@ class AndroidTaskRunner : TaskRunner {
|
|||||||
publishProgress(progress)
|
publishProgress(progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("Deprecated in Java")
|
|
||||||
override fun doInBackground(vararg params: Void?): Void? {
|
override fun doInBackground(vararg params: Void?): Void? {
|
||||||
if (isCancelled) return null
|
if (isCancelled) return null
|
||||||
task.doInBackground()
|
task.doInBackground()
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("Deprecated in Java")
|
|
||||||
override fun onPostExecute(aVoid: Void?) {
|
override fun onPostExecute(aVoid: Void?) {
|
||||||
if (isCancelled) return
|
if (isCancelled) return
|
||||||
task.onPostExecute()
|
task.onPostExecute()
|
||||||
@@ -76,7 +74,6 @@ class AndroidTaskRunner : TaskRunner {
|
|||||||
for (l in listeners) l.onTaskFinished(task)
|
for (l in listeners) l.onTaskFinished(task)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("Deprecated in Java")
|
|
||||||
override fun onPreExecute() {
|
override fun onPreExecute() {
|
||||||
if (isCancelled) return
|
if (isCancelled) return
|
||||||
for (l in listeners) l.onTaskStarted(task)
|
for (l in listeners) l.onTaskStarted(task)
|
||||||
@@ -85,7 +82,6 @@ class AndroidTaskRunner : TaskRunner {
|
|||||||
task.onPreExecute()
|
task.onPreExecute()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("Deprecated in Java")
|
|
||||||
override fun onProgressUpdate(vararg values: Int?) {
|
override fun onProgressUpdate(vararg values: Int?) {
|
||||||
values[0]?.let { task.onProgressUpdate(it) }
|
values[0]?.let { task.onProgressUpdate(it) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import javax.inject.Inject
|
|||||||
class ExportDBTaskFactory
|
class ExportDBTaskFactory
|
||||||
@Inject constructor(
|
@Inject constructor(
|
||||||
@AppContext private val context: Context,
|
@AppContext private val context: Context,
|
||||||
private val system: AndroidDirFinder
|
private val system: AndroidDirFinder,
|
||||||
) {
|
) {
|
||||||
fun create(listener: ExportDBTask.Listener) = ExportDBTask(context, system, listener)
|
fun create(listener: ExportDBTask.Listener) = ExportDBTask(context, system, listener)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import javax.inject.Inject
|
|||||||
class ImportDataTaskFactory
|
class ImportDataTaskFactory
|
||||||
@Inject constructor(
|
@Inject constructor(
|
||||||
private val importer: GenericImporter,
|
private val importer: GenericImporter,
|
||||||
private val modelFactory: ModelFactory
|
private val modelFactory: ModelFactory,
|
||||||
) {
|
) {
|
||||||
fun create(file: File, listener: ImportDataTask.Listener) =
|
fun create(file: File, listener: ImportDataTask.Listener) =
|
||||||
ImportDataTask(importer, modelFactory, file, listener)
|
ImportDataTask(importer, modelFactory, file, listener)
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import org.jetbrains.annotations.Contract
|
|||||||
|
|
||||||
object AttributeSetUtils {
|
object AttributeSetUtils {
|
||||||
const val ISORON_NAMESPACE = "http://isoron.org/android"
|
const val ISORON_NAMESPACE = "http://isoron.org/android"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getAttribute(
|
fun getAttribute(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|||||||
@@ -36,13 +36,6 @@ object ColorUtils {
|
|||||||
return a or r or g or b
|
return a or r or g or b
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeHue(color: Int, delta: Float): Int {
|
|
||||||
val hsv = FloatArray(3)
|
|
||||||
Color.colorToHSV(color, hsv)
|
|
||||||
hsv[0] = (hsv[0] + delta).mod(360f)
|
|
||||||
return Color.HSVToColor(hsv)
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun setAlpha(color: Int, newAlpha: Float): Int {
|
fun setAlpha(color: Int, newAlpha: Float): Int {
|
||||||
val intAlpha = (newAlpha * 255).toInt()
|
val intAlpha = (newAlpha * 255).toInt()
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import java.text.SimpleDateFormat
|
|||||||
|
|
||||||
object DatabaseUtils {
|
object DatabaseUtils {
|
||||||
private var opener: HabitsDatabaseOpener? = null
|
private var opener: HabitsDatabaseOpener? = null
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getDatabaseFile(context: Context): File {
|
fun getDatabaseFile(context: Context): File {
|
||||||
val databaseFilename = databaseFilename
|
val databaseFilename = databaseFilename
|
||||||
|
|||||||
@@ -6,24 +6,16 @@ import androidx.fragment.app.FragmentManager
|
|||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
var currentDialog: WeakReference<Dialog> = WeakReference(null)
|
var currentDialog: WeakReference<Dialog> = WeakReference(null)
|
||||||
var currentDialogFragment: WeakReference<DialogFragment> = WeakReference(null)
|
|
||||||
|
|
||||||
fun dismissCurrentDialog() {
|
|
||||||
currentDialog.get()?.dismiss()
|
|
||||||
currentDialog = WeakReference(null)
|
|
||||||
currentDialogFragment.get()?.dismiss()
|
|
||||||
currentDialogFragment = WeakReference(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Dialog.dismissCurrentAndShow() {
|
fun Dialog.dismissCurrentAndShow() {
|
||||||
dismissCurrentDialog()
|
currentDialog.get()?.dismiss()
|
||||||
currentDialog = WeakReference(this)
|
currentDialog = WeakReference(this)
|
||||||
show()
|
show()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun DialogFragment.dismissCurrentAndShow(fragmentManager: FragmentManager, tag: String) {
|
fun DialogFragment.dismissCurrentAndShow(fragmentManager: FragmentManager, tag: String) {
|
||||||
dismissCurrentDialog()
|
currentDialog.get()?.dismiss()
|
||||||
currentDialogFragment = WeakReference(this)
|
|
||||||
show(fragmentManager, tag)
|
show(fragmentManager, tag)
|
||||||
fragmentManager.executePendingTransactions()
|
fragmentManager.executePendingTransactions()
|
||||||
|
currentDialog = WeakReference(this.dialog)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import android.content.Intent
|
|||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.graphics.PointF
|
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
@@ -46,8 +45,6 @@ import androidx.annotation.StringRes
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import org.isoron.platform.gui.toInt
|
import org.isoron.platform.gui.toInt
|
||||||
import org.isoron.uhabits.HabitsApplication
|
import org.isoron.uhabits.HabitsApplication
|
||||||
@@ -64,6 +61,7 @@ fun RelativeLayout.addBelow(
|
|||||||
height: Int = WRAP_CONTENT,
|
height: Int = WRAP_CONTENT,
|
||||||
applyCustomRules: (params: RelativeLayout.LayoutParams) -> Unit = {}
|
applyCustomRules: (params: RelativeLayout.LayoutParams) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
|
||||||
view.layoutParams = RelativeLayout.LayoutParams(width, height).apply {
|
view.layoutParams = RelativeLayout.LayoutParams(width, height).apply {
|
||||||
addRule(BELOW, subject.id)
|
addRule(BELOW, subject.id)
|
||||||
applyCustomRules(this)
|
applyCustomRules(this)
|
||||||
@@ -77,6 +75,7 @@ fun RelativeLayout.addAtBottom(
|
|||||||
width: Int = MATCH_PARENT,
|
width: Int = MATCH_PARENT,
|
||||||
height: Int = WRAP_CONTENT
|
height: Int = WRAP_CONTENT
|
||||||
) {
|
) {
|
||||||
|
|
||||||
view.layoutParams = RelativeLayout.LayoutParams(width, height).apply {
|
view.layoutParams = RelativeLayout.LayoutParams(width, height).apply {
|
||||||
addRule(ALIGN_PARENT_BOTTOM)
|
addRule(ALIGN_PARENT_BOTTOM)
|
||||||
}
|
}
|
||||||
@@ -89,6 +88,7 @@ fun RelativeLayout.addAtTop(
|
|||||||
width: Int = MATCH_PARENT,
|
width: Int = MATCH_PARENT,
|
||||||
height: Int = WRAP_CONTENT
|
height: Int = WRAP_CONTENT
|
||||||
) {
|
) {
|
||||||
|
|
||||||
view.layoutParams = RelativeLayout.LayoutParams(width, height).apply {
|
view.layoutParams = RelativeLayout.LayoutParams(width, height).apply {
|
||||||
addRule(ALIGN_PARENT_TOP)
|
addRule(ALIGN_PARENT_TOP)
|
||||||
}
|
}
|
||||||
@@ -138,11 +138,7 @@ fun Activity.startActivitySafely(intent: Intent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Activity.showSendEmailScreen(
|
fun Activity.showSendEmailScreen(@StringRes toId: Int, @StringRes subjectId: Int, content: String?) {
|
||||||
@StringRes toId: Int,
|
|
||||||
@StringRes subjectId: Int,
|
|
||||||
content: String?
|
|
||||||
) {
|
|
||||||
val to = this.getString(toId)
|
val to = this.getString(toId)
|
||||||
val subject = this.getString(subjectId)
|
val subject = this.getString(subjectId)
|
||||||
this.startActivity(
|
this.startActivity(
|
||||||
@@ -163,7 +159,7 @@ fun Activity.restartWithFade(cls: Class<*>?) {
|
|||||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
||||||
startActivity(Intent(this, cls))
|
startActivity(Intent(this, cls))
|
||||||
},
|
},
|
||||||
500
|
500,
|
||||||
) // HACK: Let the menu disappear first
|
) // HACK: Let the menu disappear first
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +168,7 @@ fun View.setupToolbar(
|
|||||||
title: String,
|
title: String,
|
||||||
color: PaletteColor,
|
color: PaletteColor,
|
||||||
theme: Theme,
|
theme: Theme,
|
||||||
displayHomeAsUpEnabled: Boolean = true
|
displayHomeAsUpEnabled: Boolean = true,
|
||||||
) {
|
) {
|
||||||
toolbar.elevation = InterfaceUtils.dpToPixels(context, 2f)
|
toolbar.elevation = InterfaceUtils.dpToPixels(context, 2f)
|
||||||
val res = StyledResources(context)
|
val res = StyledResources(context)
|
||||||
@@ -182,10 +178,10 @@ fun View.setupToolbar(
|
|||||||
} else {
|
} else {
|
||||||
theme.color(color).toInt()
|
theme.color(color).toInt()
|
||||||
}
|
}
|
||||||
|
val darkerColor = ColorUtils.mixColors(toolbarColor, Color.BLACK, 0.75f)
|
||||||
toolbar.background = ColorDrawable(toolbarColor)
|
toolbar.background = ColorDrawable(toolbarColor)
|
||||||
toolbar.applyToolbarInsets()
|
|
||||||
val activity = context as AppCompatActivity
|
val activity = context as AppCompatActivity
|
||||||
activity.window.statusBarColor = toolbarColor
|
activity.window.statusBarColor = darkerColor
|
||||||
activity.setSupportActionBar(toolbar)
|
activity.setSupportActionBar(toolbar)
|
||||||
activity.supportActionBar?.setDisplayHomeAsUpEnabled(displayHomeAsUpEnabled)
|
activity.supportActionBar?.setDisplayHomeAsUpEnabled(displayHomeAsUpEnabled)
|
||||||
}
|
}
|
||||||
@@ -239,33 +235,3 @@ fun View.requestFocusWithKeyboard() {
|
|||||||
dispatchTouchEvent(MotionEvent.obtain(time, time, MotionEvent.ACTION_UP, 0f, 0f, 0))
|
dispatchTouchEvent(MotionEvent.obtain(time, time, MotionEvent.ACTION_UP, 0f, 0f, 0))
|
||||||
}, 250)
|
}, 250)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun View.getCenter(): PointF {
|
|
||||||
val viewLocation = IntArray(2)
|
|
||||||
this.getLocationOnScreen(viewLocation)
|
|
||||||
viewLocation[0] += this.width / 2
|
|
||||||
viewLocation[1] -= this.height / 2
|
|
||||||
return PointF(viewLocation[0].toFloat(), viewLocation[1].toFloat())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun View.applyRootViewInsets() {
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
|
|
||||||
val systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
val displayCutoutInsets = insets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
|
||||||
val left = maxOf(systemBarsInsets.left, displayCutoutInsets.left)
|
|
||||||
val right = maxOf(systemBarsInsets.right, displayCutoutInsets.right)
|
|
||||||
view.setPadding(left, 0, right, 0)
|
|
||||||
view.background = ColorDrawable(Color.BLACK)
|
|
||||||
insets
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun View.applyToolbarInsets() {
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
|
|
||||||
val systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
val displayCutoutInsets = insets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
|
||||||
val top = maxOf(systemBarsInsets.top, displayCutoutInsets.top)
|
|
||||||
view.setPadding(0, top, 0, 0)
|
|
||||||
insets
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -147,12 +147,11 @@ abstract class BaseWidget(val context: Context, val id: Int, val stacked: Boolea
|
|||||||
|
|
||||||
protected val preferedBackgroundAlpha: Int
|
protected val preferedBackgroundAlpha: Int
|
||||||
get() {
|
get() {
|
||||||
return if (stacked) {
|
return if (stacked)
|
||||||
255
|
255
|
||||||
} else {
|
else
|
||||||
prefs.widgetOpacity
|
prefs.widgetOpacity
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val app = context.applicationContext as HabitsApplication
|
val app = context.applicationContext as HabitsApplication
|
||||||
|
|||||||