Compare commits
184 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1ad28820f | ||
|
|
edaed20a98 | ||
|
|
68daf6587b | ||
|
|
b7606f33d2 | ||
|
|
4350ff0cd7 | ||
|
|
7bf74634bb | ||
|
|
fc645a81bc | ||
|
|
35365bbdf3 | ||
|
|
779ef5dbee | ||
|
|
e82994c76b | ||
|
|
4e1d01d8d1 | ||
|
|
5de0fc86e5 | ||
| e26b643423 | |||
| 621534d610 | |||
| a01300e9c6 | |||
| ecb8ce105a | |||
| 32ef3c14f7 | |||
| 4972257635 | |||
| c98cb50baa | |||
| c331f34fa9 | |||
| a1aea532b5 | |||
| 43489aeb4c | |||
| 990c85aedd | |||
|
|
64337b9bee | ||
|
|
8bdfaa2434 | ||
|
|
5f6060858d | ||
| b62e436054 | |||
| bf63b4dbcf | |||
| a82d940bcc | |||
| ba59dc7ca9 | |||
| 181290a0f3 | |||
|
|
d553c2f3f2 | ||
|
|
7776093217 | ||
|
|
b27f3f8540 | ||
| eb041bf6b2 | |||
|
|
e3c53bf07f | ||
|
|
9ddab6ee59 | ||
|
|
2615795402 | ||
|
|
6531445d7f | ||
|
|
4fbf8a8ca2 | ||
|
|
707b2b4380 | ||
|
|
aae85c1170 | ||
|
|
c12a6c6a4d | ||
|
|
b15c02adbf | ||
|
|
9e24128675 | ||
|
|
66c61e2e6c | ||
|
|
7bddfbe5a7 | ||
|
|
8036b10ee6 | ||
|
|
71f400f587 | ||
|
|
79e302f922 | ||
|
|
af7f60fc4d | ||
|
|
7cc4b66dfd | ||
|
|
a9fddf9963 | ||
|
|
36c1504c6a | ||
|
|
d644170141 | ||
|
|
d38f83e961 | ||
|
|
c50c5af497 | ||
|
|
fa3774a32b | ||
|
|
fd124f2a6c | ||
|
|
265b65eb8a | ||
|
|
4c269c55d2 | ||
|
|
c03305120e | ||
|
|
29615b670b | ||
|
|
6ab4a696b6 | ||
|
|
23479c7765 | ||
|
|
6d98f7aafa | ||
| 75078ed52b | |||
| 2a0afedb1d | |||
| 66a2b41250 | |||
|
|
d6a7fa3d7a | ||
|
|
07e55f1c76 | ||
| 4ee5dd910b | |||
| 87f071b5b4 | |||
| bb0b5e8adf | |||
|
|
c79d1e82a5 | ||
|
|
4aebeedec6 | ||
|
|
7de94f2caf | ||
|
|
17ed85fc1b | ||
|
|
4355fb4d68 | ||
|
|
508200abeb | ||
|
|
a29943e783 | ||
|
|
3e6a9181d6 | ||
| 1fe3a3d1ca | |||
|
|
b2951a3475 | ||
|
|
9d3c63cf62 | ||
|
|
65d237254c | ||
|
|
fe1d5c66cb | ||
|
|
113a5028af | ||
|
|
1a56260757 | ||
|
|
697fffbc99 | ||
|
|
804edfa64e | ||
| 2ab6c396d0 | |||
| a55f467339 | |||
|
|
cf682f68c9 | ||
| 0e988e746c | |||
|
|
f119cbf8e7 | ||
|
|
056f5f6fce | ||
|
|
42f6125d5e | ||
|
|
3e20fc4d1d | ||
|
|
1f763feb69 | ||
|
|
6e7ad329fe | ||
|
|
5cb241475d | ||
|
|
27e76c7243 | ||
|
|
576ad04064 | ||
|
|
5f8187ef6d | ||
|
|
f16f919e27 | ||
|
|
736bb8a75e | ||
|
|
de9ad6d4a4 | ||
|
|
1d37ce54ea | ||
|
|
f88f1cfb54 | ||
|
|
fc1478645b | ||
|
|
ffab001b09 | ||
|
|
a58a8005e1 | ||
|
|
c884ada187 | ||
|
|
e3d46ad5a0 | ||
|
|
f4a2c03216 | ||
|
|
f2b8f2f98d | ||
|
|
2c5fd87a2a | ||
|
|
39768f7f04 | ||
|
|
cc3e1ced15 | ||
|
|
2e26cc104e | ||
|
|
42fd0926ef | ||
| ec202aa9a7 | |||
|
|
1fb56c8777 | ||
|
|
a5d4a37da8 | ||
|
|
4804a48549 | ||
|
|
c892a845b4 | ||
|
|
e98064b6a5 | ||
| 57f5f6ed5b | |||
| 79f5b8b7e8 | |||
| f15c660d33 | |||
| 1866743c47 | |||
|
|
a781a1f947 | ||
|
|
13e57b5026 | ||
|
|
f8c7abfff4 | ||
|
|
7fe3ce970c | ||
|
|
b9eb244b0b | ||
|
|
95a1786c4a | ||
|
|
caa1c9d72e | ||
|
|
a7afe0b309 | ||
|
|
9c03174eef | ||
|
|
da02926fa6 | ||
|
|
250dabfe58 | ||
|
|
0bba3b76bc | ||
|
|
c2479278ba | ||
|
|
499a403a06 | ||
|
|
af5d622339 | ||
|
|
5eeb54bc47 | ||
|
|
e09e899aad | ||
| 37f03aca37 | |||
|
|
0ab55f6f5a | ||
|
|
43921721d4 | ||
|
|
33c88cded3 | ||
|
|
aecce891ea | ||
|
|
2ea98a7756 | ||
|
|
e667872d83 | ||
|
|
3602a614c4 | ||
|
|
d8c5f4d93c | ||
|
|
c9f4df9dae | ||
|
|
feb384bca6 | ||
|
|
71e9160460 | ||
|
|
64966d3c86 | ||
|
|
4787df4074 | ||
|
|
5908692a5c | ||
|
|
693dce8719 | ||
| b232827dfd | |||
|
|
02f9f411ce | ||
| bbf9da44e1 | |||
|
|
ee896fb4f9 | ||
|
|
4d7d8b6206 | ||
|
|
55b841a8b4 | ||
| 7fac86b617 | |||
|
|
c31d42be2d | ||
| 31c09b9c0b | |||
|
|
db91dce57f | ||
| 12c76245e6 | |||
| 59a4d7552c | |||
| 9d7840bdd1 | |||
|
|
32db4e363b | ||
|
|
004bb8d71c | ||
|
|
21a1e88c47 | ||
|
|
86fb718896 | ||
|
|
a4e9b2f874 | ||
|
|
ac924470b8 |
18
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -6,11 +6,16 @@ labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
<!--
|
||||
Please use the template below for your bug report. Issues that do not follow this
|
||||
template, or that do not contain all necessary information (namely, description of
|
||||
the problem, steps to reproduce, phone, phone operating system, and app version)
|
||||
will be closed without further consideration.
|
||||
-->
|
||||
|
||||
## Pre-submission checklist
|
||||
- [ ] I am submitting a bug report, not a feature request.
|
||||
- [ ] I am running the latest version of Loop Habit Tracker.
|
||||
- [ ] I have have searched for similar issues, but did not find any matches.
|
||||
- [ ] I have searched for similar issues, but did not find any matches.
|
||||
|
||||
## Description
|
||||
A clear and concise description of what the problem is.
|
||||
@@ -21,10 +26,11 @@ A clear and concise description of what the problem is.
|
||||
3. Scroll down to ....
|
||||
4. See error
|
||||
|
||||
## Screenshots
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
## System information
|
||||
- Phone: [e.g. Google Pixel 4]
|
||||
- Phone Operating System: [e.g. Android 10]
|
||||
- App version: [e.g. 1.8.9]
|
||||
- App version: [e.g. 2.0.2]
|
||||
|
||||
## Screenshots
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
|
||||
2
.github/dependabot.yml
vendored
@@ -3,4 +3,4 @@ updates:
|
||||
- package-ecosystem: "gradle"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
|
||||
9
.github/workflows/main.yml
vendored
@@ -13,10 +13,10 @@ jobs:
|
||||
- name: Check out source code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Install Java Development Kit 1.8
|
||||
- name: Install Java Development Kit 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
java-version: 11
|
||||
|
||||
- name: Build Project
|
||||
run: ./build.sh build
|
||||
@@ -55,6 +55,11 @@ jobs:
|
||||
name: uhabits-android
|
||||
path: uhabits-android/build/outputs/
|
||||
|
||||
- name: Install flock
|
||||
run: |
|
||||
brew install util-linux
|
||||
echo "/usr/local/opt/util-linux/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Run Android Tests
|
||||
run: ./build.sh android-tests ${{ matrix.api }}
|
||||
|
||||
|
||||
10
README.md
@@ -18,7 +18,7 @@ source.
|
||||
|
||||
<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/apps/en-play-badge-border.png" height="75px"/></a>
|
||||
<a href="http://f-droid.org/app/org.isoron.uhabits"><img alt="Get it on F-Droid" src="http://i.imgur.com/baSPE7X.png" height="75px"/></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>
|
||||
|
||||
## Screenshots
|
||||
@@ -90,7 +90,7 @@ contribute, even if you are not a software developer.
|
||||
is already completed, you are also very welcome to join and proofread it.
|
||||
|
||||
* **Write some code.** If you are an Android developer, you are very welcome to
|
||||
contribute with code. Please see `docs/GUIDELINES.md`.
|
||||
contribute with code. Please see the [guidelines](https://github.com/iSoron/uhabits/blob/dev/docs/GUIDELINES.md).
|
||||
|
||||
## License
|
||||
|
||||
@@ -109,7 +109,7 @@ contribute, even if you are not a software developer.
|
||||
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/>.
|
||||
with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
[screen1]: screenshots/1.png
|
||||
[screen2]: screenshots/2.png
|
||||
@@ -123,9 +123,9 @@ contribute, even if you are not a software developer.
|
||||
[screen4th]: screenshots/4.thumb.png
|
||||
[screen5th]: screenshots/5.thumb.png
|
||||
[screen6th]: screenshots/6.thumb.png
|
||||
[poedit]: http://translate.loophabits.org
|
||||
[poedit]: https://translate.loophabits.org
|
||||
[playstore]: https://play.google.com/store/apps/details?id=org.isoron.uhabits
|
||||
[releases]: https://github.com/iSoron/uhabits/releases
|
||||
[fdroid]: http://f-droid.org/app/org.isoron.uhabits
|
||||
[fdroid]: https://f-droid.org/app/org.isoron.uhabits
|
||||
[build]: https://github.com/iSoron/uhabits/blob/dev/docs/BUILD.md
|
||||
[beta]: https://play.google.com/apps/testing/org.isoron.uhabits
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
plugins {
|
||||
val kotlinVersion = "1.4.21"
|
||||
id("com.android.application") version ("4.1.0") apply (false)
|
||||
val kotlinVersion = "1.5.0"
|
||||
id("com.android.application") version ("7.0.3") apply (false)
|
||||
id("org.jetbrains.kotlin.android") 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.jlleitschuh.gradle.ktlint") version "10.0.0"
|
||||
id("org.jlleitschuh.gradle.ktlint") version "10.2.0"
|
||||
}
|
||||
|
||||
apply {
|
||||
@@ -15,11 +15,9 @@ apply {
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
maven(url = "https://plugins.gradle.org/m2/")
|
||||
maven(url = "https://oss.sonatype.org/content/repositories/snapshots/")
|
||||
maven(url = "https://kotlin.bintray.com/ktor")
|
||||
maven(url = "https://kotlin.bintray.com/kotlin-js-wrappers")
|
||||
maven(url = "https://jitpack.io")
|
||||
}
|
||||
}
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,5 @@
|
||||
#Sat Nov 28 09:55:24 CST 2020
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-all.zip
|
||||
|
||||
2
gradlew
vendored
@@ -82,6 +82,7 @@ esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
@@ -129,6 +130,7 @@ fi
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
|
||||
25
gradlew.bat
vendored
@@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@@ -37,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
@@ -51,7 +54,7 @@ goto fail
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
@@ -61,28 +64,14 @@ echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
jcenter()
|
||||
google()
|
||||
}
|
||||
resolutionStrategy.eachPlugin {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
/*
|
||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||
*
|
||||
@@ -20,7 +18,7 @@
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("com.github.triplet.play") version "3.2.0"
|
||||
id("com.github.triplet.play") version "3.7.0"
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("org.jetbrains.kotlin.kapt")
|
||||
@@ -34,15 +32,15 @@ tasks.compileLint {
|
||||
|
||||
android {
|
||||
|
||||
compileSdkVersion(30)
|
||||
compileSdk = 31
|
||||
|
||||
defaultConfig {
|
||||
versionCode(20003)
|
||||
versionName("2.0.3")
|
||||
minSdkVersion(23)
|
||||
targetSdkVersion(30)
|
||||
applicationId("org.isoron.uhabits")
|
||||
testInstrumentationRunner("androidx.test.runner.AndroidJUnitRunner")
|
||||
versionCode = 20003
|
||||
versionName = "2.0.3"
|
||||
minSdk = 23
|
||||
targetSdk = 31
|
||||
applicationId = "org.isoron.uhabits"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -58,7 +56,7 @@ android {
|
||||
|
||||
buildTypes {
|
||||
getByName("release") {
|
||||
minifyEnabled(true)
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.txt")
|
||||
if (signingConfigs.findByName("release") != null) {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
@@ -70,7 +68,7 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
lint {
|
||||
isCheckReleaseBuilds = false
|
||||
isAbortOnError = false
|
||||
disable("GoogleAppIndexingWarning")
|
||||
@@ -88,11 +86,11 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
val daggerVersion = "2.34.1"
|
||||
val kotlinVersion = "1.4.32"
|
||||
val kxCoroutinesVersion = "1.4.2"
|
||||
val ktorVersion = "1.5.3"
|
||||
val espressoVersion = "3.3.0"
|
||||
val daggerVersion = "2.40.3"
|
||||
val kotlinVersion = "1.6.0"
|
||||
val kxCoroutinesVersion = "1.5.2"
|
||||
val ktorVersion = "1.6.6"
|
||||
val espressoVersion = "3.4.0"
|
||||
|
||||
androidTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:$espressoVersion")
|
||||
@@ -100,17 +98,17 @@ dependencies {
|
||||
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.1")
|
||||
androidTestImplementation("io.ktor:ktor-client-mock:$ktorVersion")
|
||||
androidTestImplementation("io.ktor:ktor-jackson:$ktorVersion")
|
||||
androidTestImplementation("androidx.annotation:annotation:1.2.0")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.2")
|
||||
androidTestImplementation("androidx.annotation:annotation:1.3.0")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
||||
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
|
||||
androidTestImplementation("androidx.test:rules:1.3.0")
|
||||
androidTestImplementation("androidx.test:rules:1.4.0")
|
||||
androidTestImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
||||
compileOnly("javax.annotation:jsr250-api:1.0")
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
|
||||
implementation("com.github.paolorotolo:appintro:3.4.0")
|
||||
implementation("com.github.AppIntro:AppIntro:6.1.0")
|
||||
implementation("com.google.code.findbugs:jsr305:3.0.2")
|
||||
implementation("com.google.dagger:dagger:$daggerVersion")
|
||||
implementation("com.google.guava:guava:30.1.1-android")
|
||||
implementation("com.google.guava:guava:31.0.1-android")
|
||||
implementation("io.ktor:ktor-client-android:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-core:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-jackson:$ktorVersion")
|
||||
@@ -118,11 +116,11 @@ dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kxCoroutinesVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kxCoroutinesVersion")
|
||||
implementation("androidx.appcompat:appcompat:1.2.0")
|
||||
implementation("androidx.appcompat:appcompat:1.3.1")
|
||||
implementation("androidx.legacy:legacy-preference-v14:1.0.0")
|
||||
implementation("androidx.legacy:legacy-support-v4:1.0.0")
|
||||
implementation("com.google.android.material:material:1.3.0")
|
||||
implementation("com.opencsv:opencsv:5.4")
|
||||
implementation("com.google.android.material:material:1.4.0")
|
||||
implementation("com.opencsv:opencsv:5.5.2")
|
||||
implementation(project(":uhabits-core"))
|
||||
kapt("com.google.dagger:dagger-compiler:$daggerVersion")
|
||||
kaptAndroidTest("com.google.dagger:dagger-compiler:$daggerVersion")
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 6.4 KiB |
@@ -23,10 +23,10 @@ import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
|
||||
import org.isoron.uhabits.core.models.Frequency
|
||||
import org.isoron.uhabits.core.models.Frequency.Companion.DAILY
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.Habit.Companion.AT_LEAST
|
||||
import org.isoron.uhabits.core.models.Habit.Companion.NUMBER_HABIT
|
||||
import org.isoron.uhabits.core.models.HabitList
|
||||
import org.isoron.uhabits.core.models.HabitType
|
||||
import org.isoron.uhabits.core.models.ModelFactory
|
||||
import org.isoron.uhabits.core.models.NumericalHabitType
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.core.models.Timestamp
|
||||
import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday
|
||||
@@ -102,8 +102,8 @@ class HabitFixtures(private val modelFactory: ModelFactory, private val habitLis
|
||||
val habit = modelFactory.buildHabit().apply {
|
||||
name = "Read"
|
||||
question = "How many pages did you walk today?"
|
||||
type = NUMBER_HABIT
|
||||
targetType = AT_LEAST
|
||||
type = HabitType.NUMERICAL
|
||||
targetType = NumericalHabitType.AT_LEAST
|
||||
targetValue = 200.0
|
||||
unit = "pages"
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ import org.junit.runner.RunWith
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@LargeTest
|
||||
class HabitsTest : BaseUserInterfaceTest() {
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun shouldCreateHabit() {
|
||||
@@ -180,6 +181,8 @@ class HabitsTest : BaseUserInterfaceTest() {
|
||||
longPressCheckmarks("Wake up early", count = 2)
|
||||
clickText("Wake up early")
|
||||
verifyShowsScreen(SHOW_HABIT)
|
||||
// TODO: find a better way than sleeping in tests
|
||||
Thread.sleep(2001L)
|
||||
verifyDisplaysText("10%")
|
||||
}
|
||||
|
||||
@@ -194,6 +197,8 @@ class HabitsTest : BaseUserInterfaceTest() {
|
||||
verifyDoesNotDisplayText("Track time")
|
||||
verifyDisplaysText("Wake up early")
|
||||
longPressCheckmarks("Wake up early", count = 1)
|
||||
// TODO: find a better way than sleeping in tests
|
||||
Thread.sleep(2001L)
|
||||
verifyDoesNotDisplayText("Wake up early")
|
||||
clickMenu(TOGGLE_COMPLETED)
|
||||
verifyDisplaysText("Track time")
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.test.filters.MediumTest
|
||||
import org.hamcrest.CoreMatchers.equalTo
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.isoron.uhabits.BaseViewTest
|
||||
import org.isoron.uhabits.core.models.NumericalHabitType
|
||||
import org.isoron.uhabits.utils.PaletteUtils
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@@ -42,6 +43,7 @@ class NumberButtonViewTest : BaseViewTest() {
|
||||
super.setUp()
|
||||
view = component.getNumberButtonViewFactory().create().apply {
|
||||
units = "steps"
|
||||
targetType = NumericalHabitType.AT_LEAST
|
||||
threshold = 100.0
|
||||
color = PaletteUtils.getAndroidTestColor(8)
|
||||
onEdit = { edited = true }
|
||||
@@ -74,10 +76,10 @@ class NumberButtonViewTest : BaseViewTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRender_emptyUnits() {
|
||||
fun testRender_atMostAboveThreshold() {
|
||||
view.value = 500.0
|
||||
view.units = ""
|
||||
assertRenders(view, "$PATH/render_unitless.png")
|
||||
view.targetType = NumericalHabitType.AT_MOST
|
||||
assertRenders(view, "$PATH/render_at_most_above.png")
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -86,6 +88,13 @@ class NumberButtonViewTest : BaseViewTest() {
|
||||
assertRenders(view, "$PATH/render_below.png")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRender_atMostBetweenThresholds() {
|
||||
view.value = 110.0
|
||||
view.targetType = NumericalHabitType.AT_MOST
|
||||
assertRenders(view, "$PATH/render_at_most_between.png")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRender_zero() {
|
||||
view.value = 0.0
|
||||
@@ -93,15 +102,21 @@ class NumberButtonViewTest : BaseViewTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClick_shortToggleDisabled() {
|
||||
prefs.isShortToggleEnabled = false
|
||||
view.performClick()
|
||||
assertFalse(edited)
|
||||
fun testRender_atMostBelowThreshold() {
|
||||
view.value = 0.0
|
||||
view.targetType = NumericalHabitType.AT_MOST
|
||||
assertRenders(view, "$PATH/render_at_most_below.png")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClick_shortToggleEnabled() {
|
||||
prefs.isShortToggleEnabled = true
|
||||
fun testRender_emptyUnits() {
|
||||
view.value = 500.0
|
||||
view.units = ""
|
||||
assertRenders(view, "$PATH/render_unitless.png")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClick() {
|
||||
view.performClick()
|
||||
assertTrue(edited)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.test.filters.MediumTest
|
||||
import org.hamcrest.CoreMatchers.equalTo
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.isoron.uhabits.BaseViewTest
|
||||
import org.isoron.uhabits.core.models.NumericalHabitType
|
||||
import org.isoron.uhabits.core.models.Timestamp
|
||||
import org.isoron.uhabits.utils.PaletteUtils
|
||||
import org.junit.After
|
||||
@@ -55,6 +56,7 @@ class NumberPanelViewTest : BaseViewTest() {
|
||||
buttonCount = 4
|
||||
color = PaletteUtils.getAndroidTestColor(7)
|
||||
units = "steps"
|
||||
targetType = NumericalHabitType.AT_LEAST
|
||||
threshold = 5000.0
|
||||
}
|
||||
view.onAttachedToWindow()
|
||||
|
||||
@@ -53,8 +53,6 @@ class SubtitleCardViewTest : BaseViewTest() {
|
||||
isNumerical = false,
|
||||
question = "Did you meditate this morning?",
|
||||
reminder = Reminder(8, 30, EVERY_DAY),
|
||||
unit = "",
|
||||
targetValue = 0.0,
|
||||
theme = LightTheme(),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -61,7 +61,7 @@ class PerformanceTest : BaseAndroidTest() {
|
||||
val habit = fixtures.createEmptyHabit()
|
||||
for (i in 0..4999) {
|
||||
val timestamp: Timestamp = Timestamp(i * DAY_LENGTH)
|
||||
CreateRepetitionCommand(habitList, habit, timestamp, 1).run()
|
||||
CreateRepetitionCommand(habitList, habit, timestamp, 1, "").run()
|
||||
}
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
@@ -24,8 +24,8 @@ import android.widget.FrameLayout
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.MediumTest
|
||||
import org.hamcrest.CoreMatchers
|
||||
import org.hamcrest.CoreMatchers.`is`
|
||||
import org.hamcrest.CoreMatchers.equalTo
|
||||
import org.hamcrest.CoreMatchers.`is`
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.isoron.uhabits.BaseViewTest
|
||||
import org.isoron.uhabits.R
|
||||
|
||||
@@ -17,9 +17,10 @@
|
||||
~ with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.isoron.uhabits">
|
||||
|
||||
<uses-permission android:name="android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
@@ -48,11 +49,11 @@
|
||||
android:name=".activities.habits.list.ListHabitsActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/main_activity_title"
|
||||
android:launchMode="singleTop">
|
||||
</activity>
|
||||
android:launchMode="singleTop" />
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/main_activity_title"
|
||||
android:launchMode="singleTop"
|
||||
android:targetActivity=".activities.habits.list.ListHabitsActivity">
|
||||
@@ -85,6 +86,7 @@
|
||||
|
||||
<activity
|
||||
android:name=".widgets.activities.HabitPickerDialog"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.AppCompat.Light.Dialog">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
@@ -93,6 +95,7 @@
|
||||
|
||||
<activity
|
||||
android:name=".widgets.activities.BooleanHabitPickerDialog"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.AppCompat.Light.Dialog">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
@@ -101,6 +104,7 @@
|
||||
|
||||
<activity
|
||||
android:name=".widgets.activities.NumericalHabitPickerDialog"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.AppCompat.Light.Dialog">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
@@ -117,9 +121,10 @@
|
||||
|
||||
<activity
|
||||
android:name=".widgets.activities.NumericalCheckmarkWidgetActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:label="NumericalCheckmarkWidget"
|
||||
android:noHistory="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:theme="@style/Theme.AppCompat.Light.Dialog">
|
||||
<intent-filter>
|
||||
<action android:name="org.isoron.uhabits.ACTION_SHOW_NUMERICAL_VALUE_ACTIVITY" />
|
||||
@@ -128,13 +133,14 @@
|
||||
|
||||
<activity
|
||||
android:name=".notifications.SnoozeDelayPickerActivity"
|
||||
android:taskAffinity=""
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleInstance"
|
||||
android:taskAffinity=""
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.CheckmarkWidgetProvider"
|
||||
android:exported="true"
|
||||
android:label="@string/checkmark">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
@@ -152,6 +158,7 @@
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.HistoryWidgetProvider"
|
||||
android:exported="true"
|
||||
android:label="@string/history">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
@@ -164,6 +171,7 @@
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.ScoreWidgetProvider"
|
||||
android:exported="true"
|
||||
android:label="@string/score">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
@@ -176,6 +184,7 @@
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.StreakWidgetProvider"
|
||||
android:exported="true"
|
||||
android:label="@string/streaks">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
@@ -188,6 +197,7 @@
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.FrequencyWidgetProvider"
|
||||
android:exported="true"
|
||||
android:label="@string/frequency">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
@@ -200,6 +210,7 @@
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.TargetWidgetProvider"
|
||||
android:exported="true"
|
||||
android:label="@string/target">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
@@ -210,13 +221,17 @@
|
||||
android:resource="@xml/widget_target_info" />
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".receivers.ReminderReceiver">
|
||||
<receiver
|
||||
android:name=".receivers.ReminderReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".receivers.WidgetReceiver">
|
||||
<receiver android:name=".receivers.WidgetReceiver"
|
||||
android:exported="true"
|
||||
android:permission="false">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<action android:name="org.isoron.uhabits.ACTION_SET_NUMERICAL_VALUE" />
|
||||
@@ -267,7 +282,7 @@
|
||||
<!-- Locale/Tasker -->
|
||||
<receiver
|
||||
android:name=".automation.FireSettingReceiver"
|
||||
android:exported="true">
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
|
||||
</intent-filter>
|
||||
|
||||
@@ -49,23 +49,12 @@ class AndroidDataView(
|
||||
override fun onShowPress(e: MotionEvent?) = Unit
|
||||
|
||||
override fun onSingleTapUp(e: MotionEvent?): Boolean {
|
||||
val x: Float
|
||||
val y: Float
|
||||
try {
|
||||
val pointerId = e!!.getPointerId(0)
|
||||
x = e.getX(pointerId)
|
||||
y = e.getY(pointerId)
|
||||
} catch (ex: RuntimeException) {
|
||||
// Android often throws IllegalArgumentException here. Apparently,
|
||||
// the pointer id may become invalid shortly after calling
|
||||
// e.getPointerId.
|
||||
return false
|
||||
}
|
||||
view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity)
|
||||
return true
|
||||
return handleClick(e, true)
|
||||
}
|
||||
|
||||
override fun onLongPress(e: MotionEvent?) = Unit
|
||||
override fun onLongPress(e: MotionEvent?) {
|
||||
handleClick(e)
|
||||
}
|
||||
|
||||
override fun onScroll(
|
||||
e1: MotionEvent?,
|
||||
@@ -137,4 +126,22 @@ class AndroidDataView(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleClick(e: MotionEvent?, isSingleTap: Boolean = false): Boolean {
|
||||
val x: Float
|
||||
val y: Float
|
||||
try {
|
||||
val pointerId = e!!.getPointerId(0)
|
||||
x = e.getX(pointerId)
|
||||
y = e.getY(pointerId)
|
||||
} catch (ex: RuntimeException) {
|
||||
// Android often throws IllegalArgumentException here. Apparently,
|
||||
// the pointer id may become invalid shortly after calling
|
||||
// e.getPointerId.
|
||||
return false
|
||||
}
|
||||
if (isSingleTap) view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity)
|
||||
else view?.onLongClick(x / canvas.innerDensity, y / canvas.innerDensity)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package org.isoron.uhabits.activities.common.dialogs
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE
|
||||
import android.widget.Button
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import org.isoron.platform.gui.toInt
|
||||
import org.isoron.uhabits.R
|
||||
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.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.PaletteColor
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
|
||||
import org.isoron.uhabits.core.ui.views.Theme
|
||||
import org.isoron.uhabits.databinding.CheckmarkDialogBinding
|
||||
import org.isoron.uhabits.inject.ActivityContext
|
||||
import org.isoron.uhabits.utils.InterfaceUtils
|
||||
import org.isoron.uhabits.utils.StyledResources
|
||||
import javax.inject.Inject
|
||||
|
||||
class CheckmarkDialog
|
||||
@Inject constructor(
|
||||
@ActivityContext private val context: Context,
|
||||
private val preferences: Preferences,
|
||||
) : View.OnClickListener {
|
||||
|
||||
private lateinit var binding: CheckmarkDialogBinding
|
||||
private lateinit var fontAwesome: Typeface
|
||||
private val allButtons = mutableListOf<Button>()
|
||||
private var selectedButton: Button? = null
|
||||
|
||||
fun create(
|
||||
value: Int,
|
||||
notes: String,
|
||||
dateString: String,
|
||||
paletteColor: PaletteColor,
|
||||
callback: ListHabitsBehavior.CheckMarkDialogCallback,
|
||||
theme: Theme,
|
||||
): AlertDialog {
|
||||
binding = CheckmarkDialogBinding.inflate(LayoutInflater.from(context))
|
||||
fontAwesome = InterfaceUtils.getFontAwesome(context)!!
|
||||
binding.etNotes.append(notes)
|
||||
setUpButtons(value, theme.color(paletteColor).toInt())
|
||||
|
||||
val dialog = AlertDialog.Builder(context)
|
||||
.setView(binding.root)
|
||||
.setTitle(dateString)
|
||||
.setPositiveButton(R.string.save) { _, _ ->
|
||||
val newValue = when (selectedButton?.id) {
|
||||
R.id.yesBtn -> YES_MANUAL
|
||||
R.id.noBtn -> NO
|
||||
R.id.skippedBtn -> SKIP
|
||||
else -> UNKNOWN
|
||||
}
|
||||
callback.onNotesSaved(newValue, binding.etNotes.text.toString())
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
callback.onNotesDismissed()
|
||||
}
|
||||
.setOnDismissListener {
|
||||
callback.onNotesDismissed()
|
||||
}
|
||||
.create()
|
||||
|
||||
dialog.setOnShowListener {
|
||||
binding.etNotes.requestFocus()
|
||||
dialog.window?.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE)
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
private fun setUpButtons(value: Int, color: Int) {
|
||||
val sres = StyledResources(context)
|
||||
val mediumContrastColor = sres.getColor(R.attr.contrast60)
|
||||
setButtonAttrs(binding.yesBtn, color)
|
||||
setButtonAttrs(binding.noBtn, mediumContrastColor)
|
||||
setButtonAttrs(binding.skippedBtn, color, visible = preferences.isSkipEnabled)
|
||||
setButtonAttrs(binding.questionBtn, mediumContrastColor, visible = preferences.areQuestionMarksEnabled)
|
||||
when (value) {
|
||||
UNKNOWN -> if (preferences.areQuestionMarksEnabled) {
|
||||
binding.questionBtn.performClick()
|
||||
} else {
|
||||
binding.noBtn.performClick()
|
||||
}
|
||||
SKIP -> binding.skippedBtn.performClick()
|
||||
YES_MANUAL -> binding.yesBtn.performClick()
|
||||
YES_AUTO, NO -> binding.noBtn.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setButtonAttrs(button: Button, color: Int, visible: Boolean = true) {
|
||||
button.apply {
|
||||
visibility = if (visible) View.VISIBLE else View.GONE
|
||||
typeface = fontAwesome
|
||||
setTextColor(color)
|
||||
setOnClickListener(this@CheckmarkDialog)
|
||||
}
|
||||
allButtons.add(button)
|
||||
}
|
||||
|
||||
override fun onClick(v: View?) {
|
||||
allButtons.forEach {
|
||||
if (v?.id == it.id) {
|
||||
it.isSelected = true
|
||||
selectedButton = it
|
||||
} else it.isSelected = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ package org.isoron.uhabits.activities.common.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
@@ -44,7 +43,7 @@ class FrequencyPickerDialog(
|
||||
constructor() : this(1, 1)
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val inflater = LayoutInflater.from(activity!!)
|
||||
val inflater = LayoutInflater.from(requireActivity())
|
||||
contentView = inflater.inflate(R.layout.frequency_picker_dialog, null)
|
||||
|
||||
addBeforeAfterText(
|
||||
@@ -62,15 +61,19 @@ class FrequencyPickerDialog(
|
||||
contentView.xTimesPerMonthContainer,
|
||||
)
|
||||
|
||||
addBeforeAfterText(
|
||||
this.getString(R.string.x_times_per_y_days),
|
||||
contentView.xTimesPerYDaysContainer,
|
||||
)
|
||||
|
||||
contentView.everyDayRadioButton.setOnClickListener {
|
||||
check(contentView.everyDayRadioButton)
|
||||
unfocusAll()
|
||||
}
|
||||
|
||||
contentView.everyXDaysRadioButton.setOnClickListener {
|
||||
check(contentView.everyXDaysRadioButton)
|
||||
val everyXDaysTextView = contentView.everyXDaysTextView
|
||||
focus(everyXDaysTextView)
|
||||
selectInputField(everyXDaysTextView)
|
||||
}
|
||||
|
||||
contentView.everyXDaysTextView.setOnFocusChangeListener { v, hasFocus ->
|
||||
@@ -79,7 +82,7 @@ class FrequencyPickerDialog(
|
||||
|
||||
contentView.xTimesPerWeekRadioButton.setOnClickListener {
|
||||
check(contentView.xTimesPerWeekRadioButton)
|
||||
focus(contentView.xTimesPerWeekTextView)
|
||||
selectInputField(contentView.xTimesPerWeekTextView)
|
||||
}
|
||||
|
||||
contentView.xTimesPerWeekTextView.setOnFocusChangeListener { v, hasFocus ->
|
||||
@@ -88,14 +91,27 @@ class FrequencyPickerDialog(
|
||||
|
||||
contentView.xTimesPerMonthRadioButton.setOnClickListener {
|
||||
check(contentView.xTimesPerMonthRadioButton)
|
||||
focus(contentView.xTimesPerMonthTextView)
|
||||
selectInputField(contentView.xTimesPerMonthTextView)
|
||||
}
|
||||
|
||||
contentView.xTimesPerMonthTextView.setOnFocusChangeListener { v, hasFocus ->
|
||||
if (hasFocus) check(contentView.xTimesPerMonthRadioButton)
|
||||
}
|
||||
|
||||
return AlertDialog.Builder(activity!!)
|
||||
contentView.xTimesPerYDaysRadioButton.setOnClickListener {
|
||||
check(contentView.xTimesPerYDaysRadioButton)
|
||||
selectInputField(contentView.xTimesPerYDaysXTextView)
|
||||
}
|
||||
|
||||
contentView.xTimesPerYDaysXTextView.setOnFocusChangeListener { v, hasFocus ->
|
||||
if (hasFocus) check(contentView.xTimesPerYDaysRadioButton)
|
||||
}
|
||||
|
||||
contentView.xTimesPerYDaysYTextView.setOnFocusChangeListener { v, hasFocus ->
|
||||
if (hasFocus) check(contentView.xTimesPerYDaysRadioButton)
|
||||
}
|
||||
|
||||
return AlertDialog.Builder(requireActivity())
|
||||
.setView(contentView)
|
||||
.setPositiveButton(R.string.save) { _, _ -> onSaveClicked() }
|
||||
.create()
|
||||
@@ -106,12 +122,11 @@ class FrequencyPickerDialog(
|
||||
container: LinearLayout
|
||||
) {
|
||||
val parts = str.split("%d")
|
||||
container.addView(
|
||||
TextView(activity).apply { text = parts[0].trim() }, 1,
|
||||
)
|
||||
container.addView(
|
||||
TextView(activity).apply { text = parts[1].trim() }, 3,
|
||||
)
|
||||
for (i in parts.indices) {
|
||||
container.addView(
|
||||
TextView(activity).apply { text = parts[i].trim() }, 2 * i + 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSaveClicked() {
|
||||
@@ -132,6 +147,12 @@ class FrequencyPickerDialog(
|
||||
denominator = 7
|
||||
}
|
||||
}
|
||||
contentView.xTimesPerYDaysRadioButton.isChecked -> {
|
||||
if (contentView.xTimesPerYDaysXTextView.text.isNotEmpty() && contentView.xTimesPerYDaysYTextView.text.isNotEmpty()) {
|
||||
numerator = Integer.parseInt(contentView.xTimesPerYDaysXTextView.text.toString())
|
||||
denominator = Integer.parseInt(contentView.xTimesPerYDaysYTextView.text.toString())
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
if (contentView.xTimesPerMonthTextView.text.isNotEmpty()) {
|
||||
numerator = Integer.parseInt(contentView.xTimesPerMonthTextView.text.toString())
|
||||
@@ -147,10 +168,10 @@ class FrequencyPickerDialog(
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private fun check(view: RadioButton?) {
|
||||
private fun check(view: RadioButton) {
|
||||
uncheckAll()
|
||||
view?.isChecked = true
|
||||
view?.requestFocus()
|
||||
view.isChecked = true
|
||||
view.requestFocus()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -163,7 +184,7 @@ class FrequencyPickerDialog(
|
||||
if (freqDenominator == 30 || freqDenominator == 31) {
|
||||
contentView.xTimesPerMonthRadioButton.isChecked = true
|
||||
contentView.xTimesPerMonthTextView.setText(freqNumerator.toString())
|
||||
focus(contentView.xTimesPerMonthTextView)
|
||||
selectInputField(contentView.xTimesPerMonthTextView)
|
||||
} else {
|
||||
if (freqNumerator == 1) {
|
||||
if (freqDenominator == 1) {
|
||||
@@ -171,23 +192,23 @@ class FrequencyPickerDialog(
|
||||
} else {
|
||||
contentView.everyXDaysRadioButton.isChecked = true
|
||||
contentView.everyXDaysTextView.setText(freqDenominator.toString())
|
||||
focus(contentView.everyXDaysTextView)
|
||||
selectInputField(contentView.everyXDaysTextView)
|
||||
}
|
||||
} else {
|
||||
if (freqDenominator == 7) {
|
||||
contentView.xTimesPerWeekRadioButton.isChecked = true
|
||||
contentView.xTimesPerWeekTextView.setText(freqNumerator.toString())
|
||||
focus(contentView.xTimesPerWeekTextView)
|
||||
selectInputField(contentView.xTimesPerWeekTextView)
|
||||
} else {
|
||||
Log.w("FrequencyPickerDialog", "Unknown frequency: $freqNumerator/$freqDenominator")
|
||||
contentView.everyDayRadioButton.isChecked = true
|
||||
contentView.xTimesPerYDaysRadioButton.isChecked = true
|
||||
contentView.xTimesPerYDaysXTextView.setText(freqNumerator.toString())
|
||||
contentView.xTimesPerYDaysYTextView.setText(freqDenominator.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun focus(view: EditText) {
|
||||
view.requestFocus()
|
||||
private fun selectInputField(view: EditText) {
|
||||
view.setSelection(view.text.length)
|
||||
}
|
||||
|
||||
@@ -196,11 +217,6 @@ class FrequencyPickerDialog(
|
||||
contentView.everyXDaysRadioButton.isChecked = false
|
||||
contentView.xTimesPerWeekRadioButton.isChecked = false
|
||||
contentView.xTimesPerMonthRadioButton.isChecked = false
|
||||
}
|
||||
|
||||
private fun unfocusAll() {
|
||||
contentView.everyXDaysTextView.clearFocus()
|
||||
contentView.xTimesPerWeekTextView.clearFocus()
|
||||
contentView.xTimesPerMonthTextView.clearFocus()
|
||||
contentView.xTimesPerYDaysRadioButton.isChecked = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,9 +62,11 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
|
||||
firstWeekday = preferences.firstWeekday,
|
||||
paletteColor = habit.color,
|
||||
series = emptyList(),
|
||||
defaultSquare = HistoryChart.Square.OFF,
|
||||
notesIndicators = emptyList(),
|
||||
theme = themeSwitcher.currentTheme,
|
||||
today = DateUtils.getTodayWithOffset().toLocalDate(),
|
||||
onDateClickedListener = onDateClickedListener ?: OnDateClickedListener { },
|
||||
onDateClickedListener = onDateClickedListener ?: object : OnDateClickedListener {},
|
||||
padding = 10.0,
|
||||
)
|
||||
dataView = AndroidDataView(context!!, null)
|
||||
@@ -101,6 +103,8 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
|
||||
theme = LightTheme()
|
||||
)
|
||||
chart?.series = model.series
|
||||
chart?.defaultSquare = model.defaultSquare
|
||||
chart?.notesIndicators = model.notesIndicators
|
||||
dataView.postInvalidate()
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,11 @@
|
||||
|
||||
package org.isoron.uhabits.activities.common.dialogs
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.text.InputFilter
|
||||
import android.text.Spanned
|
||||
import android.view.LayoutInflater
|
||||
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE
|
||||
import android.view.inputmethod.EditorInfo
|
||||
@@ -33,6 +35,7 @@ import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
|
||||
import org.isoron.uhabits.inject.ActivityContext
|
||||
import org.isoron.uhabits.utils.InterfaceUtils
|
||||
import java.text.DecimalFormatSymbols
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
@@ -40,18 +43,30 @@ class NumberPickerFactory
|
||||
@Inject constructor(
|
||||
@ActivityContext private val context: Context
|
||||
) {
|
||||
|
||||
fun create(
|
||||
value: Double,
|
||||
unit: String,
|
||||
notes: String,
|
||||
dateString: String,
|
||||
callback: ListHabitsBehavior.NumberPickerCallback
|
||||
): AlertDialog {
|
||||
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val view = inflater.inflate(R.layout.number_picker_dialog, null)
|
||||
|
||||
val picker = view.findViewById<NumberPicker>(R.id.picker)
|
||||
val picker2 = view.findViewById<NumberPicker>(R.id.picker2)
|
||||
val tvUnit = view.findViewById<TextView>(R.id.tvUnit)
|
||||
val etNotes = view.findViewById<EditText>(R.id.etNotes)
|
||||
|
||||
val watcherFilter: InputFilter = SeparatorWatcherInputFilter(picker2)
|
||||
val numberPickerInputText = getNumberPickerInputText(picker)
|
||||
|
||||
// watch the unfiltered input before the filters remove a possible separator from it
|
||||
numberPickerInputText.filters = arrayOf(watcherFilter).plus(numberPickerInputText.filters)
|
||||
|
||||
view.findViewById<TextView>(R.id.tvUnit).text = unit
|
||||
view.findViewById<TextView>(R.id.tvSeparator).text =
|
||||
DecimalFormatSymbols.getInstance().decimalSeparator.toString()
|
||||
|
||||
val intValue = (value * 100).roundToLong().toInt()
|
||||
|
||||
@@ -61,20 +76,22 @@ class NumberPickerFactory
|
||||
picker.wrapSelectorWheel = false
|
||||
|
||||
picker2.minValue = 0
|
||||
picker2.maxValue = 19
|
||||
picker2.setFormatter { v -> String.format("%02d", 5 * v) }
|
||||
picker2.value = intValue % 100 / 5
|
||||
refreshInitialValue(picker2)
|
||||
|
||||
tvUnit.text = unit
|
||||
picker2.maxValue = 99
|
||||
picker2.setFormatter { v -> String.format("%02d", v) }
|
||||
picker2.value = intValue % 100
|
||||
|
||||
etNotes.setText(notes)
|
||||
val dialog = AlertDialog.Builder(context)
|
||||
.setView(view)
|
||||
.setTitle(R.string.change_value)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
.setTitle(dateString)
|
||||
.setPositiveButton(R.string.save) { _, _ ->
|
||||
picker.clearFocus()
|
||||
val v = picker.value + 0.05 * picker2.value
|
||||
callback.onNumberPicked(v)
|
||||
val v = picker.value + 0.01 * picker2.value
|
||||
val note = etNotes.text.toString()
|
||||
callback.onNumberPicked(v, note)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
callback.onNumberPickerDismissed()
|
||||
}
|
||||
.setOnDismissListener {
|
||||
callback.onNumberPickerDismissed()
|
||||
@@ -89,20 +106,50 @@ class NumberPickerFactory
|
||||
InterfaceUtils.setupEditorAction(
|
||||
picker
|
||||
) { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE)
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick()
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
InterfaceUtils.setupEditorAction(
|
||||
picker2
|
||||
) { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick()
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
private fun refreshInitialValue(picker: NumberPicker) {
|
||||
// Workaround for Android bug:
|
||||
// https://code.google.com/p/android/issues/detail?id=35482
|
||||
@SuppressLint("DiscouragedPrivateApi")
|
||||
private fun getNumberPickerInputText(picker: NumberPicker): EditText {
|
||||
val f = NumberPicker::class.java.getDeclaredField("mInputText")
|
||||
f.isAccessible = true
|
||||
val inputText = f.get(picker) as EditText
|
||||
inputText.filters = arrayOfNulls<InputFilter>(0)
|
||||
return f.get(picker) as EditText
|
||||
}
|
||||
}
|
||||
|
||||
class SeparatorWatcherInputFilter(private val nextPicker: NumberPicker) : InputFilter {
|
||||
override fun filter(
|
||||
source: CharSequence?,
|
||||
start: Int,
|
||||
end: Int,
|
||||
dest: Spanned?,
|
||||
dstart: Int,
|
||||
dend: Int
|
||||
): CharSequence {
|
||||
if (source == null || source.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
for (c in source) {
|
||||
if (c == DecimalFormatSymbols.getInstance().decimalSeparator || c == '.' || c == ',') {
|
||||
nextPicker.performLongClick()
|
||||
break
|
||||
}
|
||||
}
|
||||
return source
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,10 +29,10 @@ class TaskProgressBar(
|
||||
context: Context,
|
||||
private val runner: TaskRunner
|
||||
) : ProgressBar(
|
||||
context,
|
||||
null,
|
||||
android.R.attr.progressBarStyleHorizontal
|
||||
),
|
||||
context,
|
||||
null,
|
||||
android.R.attr.progressBarStyleHorizontal
|
||||
),
|
||||
TaskRunner.Listener {
|
||||
|
||||
init {
|
||||
|
||||
@@ -52,6 +52,8 @@ import org.isoron.uhabits.core.commands.CreateHabitCommand
|
||||
import org.isoron.uhabits.core.commands.EditHabitCommand
|
||||
import org.isoron.uhabits.core.models.Frequency
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.HabitType
|
||||
import org.isoron.uhabits.core.models.NumericalHabitType
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.core.models.Reminder
|
||||
import org.isoron.uhabits.core.models.WeekdayList
|
||||
@@ -67,7 +69,7 @@ fun formatFrequency(freqNum: Int, freqDen: Int, resources: Resources) = when {
|
||||
freqNum == 1 && freqDen == 7 -> resources.getString(R.string.every_week)
|
||||
freqNum == 1 && freqDen > 1 -> resources.getString(R.string.every_x_days, freqDen)
|
||||
freqDen == 7 -> resources.getString(R.string.x_times_per_week, freqNum)
|
||||
else -> "$freqNum/$freqDen"
|
||||
else -> resources.getString(R.string.x_times_per_y_days, freqNum, freqDen)
|
||||
}
|
||||
|
||||
class EditHabitActivity : AppCompatActivity() {
|
||||
@@ -77,7 +79,7 @@ class EditHabitActivity : AppCompatActivity() {
|
||||
private lateinit var commandRunner: CommandRunner
|
||||
|
||||
var habitId = -1L
|
||||
var habitType = -1
|
||||
lateinit var habitType: HabitType
|
||||
var unit = ""
|
||||
var color = PaletteColor(11)
|
||||
var androidColor = 0
|
||||
@@ -86,6 +88,7 @@ class EditHabitActivity : AppCompatActivity() {
|
||||
var reminderHour = -1
|
||||
var reminderMin = -1
|
||||
var reminderDays: WeekdayList = WeekdayList.EVERY_DAY
|
||||
var targetType = NumericalHabitType.AT_LEAST
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
@@ -105,6 +108,7 @@ class EditHabitActivity : AppCompatActivity() {
|
||||
color = habit.color
|
||||
freqNum = habit.frequency.numerator
|
||||
freqDen = habit.frequency.denominator
|
||||
targetType = habit.targetType
|
||||
habit.reminder?.let {
|
||||
reminderHour = it.hour
|
||||
reminderMin = it.minute
|
||||
@@ -116,12 +120,12 @@ class EditHabitActivity : AppCompatActivity() {
|
||||
binding.unitInput.setText(habit.unit)
|
||||
binding.targetInput.setText(habit.targetValue.toString())
|
||||
} else {
|
||||
habitType = intent.getIntExtra("habitType", Habit.YES_NO_HABIT)
|
||||
habitType = HabitType.fromInt(intent.getIntExtra("habitType", HabitType.YES_NO.value))
|
||||
}
|
||||
|
||||
if (state != null) {
|
||||
habitId = state.getLong("habitId")
|
||||
habitType = state.getInt("habitType")
|
||||
habitType = HabitType.fromInt(state.getInt("habitType"))
|
||||
color = PaletteColor(state.getInt("paletteColor"))
|
||||
freqNum = state.getInt("freqNum")
|
||||
freqDen = state.getInt("freqDen")
|
||||
@@ -132,13 +136,17 @@ class EditHabitActivity : AppCompatActivity() {
|
||||
|
||||
updateColors()
|
||||
|
||||
if (habitType == Habit.YES_NO_HABIT) {
|
||||
binding.unitOuterBox.visibility = View.GONE
|
||||
binding.targetOuterBox.visibility = View.GONE
|
||||
} else {
|
||||
binding.nameInput.hint = getString(R.string.measurable_short_example)
|
||||
binding.questionInput.hint = getString(R.string.measurable_question_example)
|
||||
binding.frequencyOuterBox.visibility = View.GONE
|
||||
when (habitType) {
|
||||
HabitType.YES_NO -> {
|
||||
binding.unitOuterBox.visibility = View.GONE
|
||||
binding.targetOuterBox.visibility = View.GONE
|
||||
binding.targetTypeOuterBox.visibility = View.GONE
|
||||
}
|
||||
HabitType.NUMERICAL -> {
|
||||
binding.nameInput.hint = getString(R.string.measurable_short_example)
|
||||
binding.questionInput.hint = getString(R.string.measurable_question_example)
|
||||
binding.frequencyOuterBox.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
setSupportActionBar(binding.toolbar)
|
||||
@@ -167,6 +175,23 @@ class EditHabitActivity : AppCompatActivity() {
|
||||
dialog.show(supportFragmentManager, "frequencyPicker")
|
||||
}
|
||||
|
||||
populateTargetType()
|
||||
binding.targetTypePicker.setOnClickListener {
|
||||
val builder = AlertDialog.Builder(this)
|
||||
val arrayAdapter = ArrayAdapter<String>(this, android.R.layout.select_dialog_item)
|
||||
arrayAdapter.add(getString(R.string.target_type_at_least))
|
||||
arrayAdapter.add(getString(R.string.target_type_at_most))
|
||||
builder.setAdapter(arrayAdapter) { dialog, which ->
|
||||
targetType = when (which) {
|
||||
0 -> NumericalHabitType.AT_LEAST
|
||||
else -> NumericalHabitType.AT_MOST
|
||||
}
|
||||
populateTargetType()
|
||||
dialog.dismiss()
|
||||
}
|
||||
builder.show()
|
||||
}
|
||||
|
||||
binding.numericalFrequencyPicker.setOnClickListener {
|
||||
val builder = AlertDialog.Builder(this)
|
||||
val arrayAdapter = ArrayAdapter<String>(this, android.R.layout.select_dialog_item)
|
||||
@@ -255,9 +280,9 @@ class EditHabitActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
habit.frequency = Frequency(freqNum, freqDen)
|
||||
if (habitType == Habit.NUMBER_HABIT) {
|
||||
if (habitType == HabitType.NUMERICAL) {
|
||||
habit.targetValue = targetInput.text.toString().toDouble()
|
||||
habit.targetType = Habit.AT_LEAST
|
||||
habit.targetType = targetType
|
||||
habit.unit = unitInput.text.trim().toString()
|
||||
}
|
||||
habit.type = habitType
|
||||
@@ -285,7 +310,7 @@ class EditHabitActivity : AppCompatActivity() {
|
||||
nameInput.error = getFormattedValidationError(R.string.validation_cannot_be_blank)
|
||||
isValid = false
|
||||
}
|
||||
if (habitType == Habit.NUMBER_HABIT) {
|
||||
if (habitType == HabitType.NUMERICAL) {
|
||||
if (targetInput.text.isEmpty()) {
|
||||
targetInput.error = getString(R.string.validation_cannot_be_blank)
|
||||
isValid = false
|
||||
@@ -319,6 +344,13 @@ class EditHabitActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateTargetType() {
|
||||
binding.targetTypePicker.text = when (targetType) {
|
||||
NumericalHabitType.AT_MOST -> getString(R.string.target_type_at_most)
|
||||
else -> getString(R.string.target_type_at_least)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateColors() {
|
||||
androidColor = themeSwitcher.currentTheme.color(color).toInt()
|
||||
binding.colorButton.backgroundTintList = ColorStateList.valueOf(androidColor)
|
||||
@@ -338,7 +370,7 @@ class EditHabitActivity : AppCompatActivity() {
|
||||
super.onSaveInstanceState(state)
|
||||
with(state) {
|
||||
putLong("habitId", habitId)
|
||||
putInt("habitType", habitType)
|
||||
putInt("habitType", habitType.value)
|
||||
putInt("paletteColor", color.paletteIndex)
|
||||
putInt("androidColor", androidColor)
|
||||
putInt("freqNum", freqNum)
|
||||
|
||||
@@ -25,7 +25,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.HabitType
|
||||
import org.isoron.uhabits.databinding.SelectHabitTypeBinding
|
||||
import org.isoron.uhabits.intents.IntentFactory
|
||||
|
||||
@@ -40,13 +40,13 @@ class HabitTypeDialog : AppCompatDialogFragment() {
|
||||
val binding = SelectHabitTypeBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.buttonYesNo.setOnClickListener {
|
||||
val intent = IntentFactory().startEditActivity(activity!!, Habit.YES_NO_HABIT)
|
||||
val intent = IntentFactory().startEditActivity(activity!!, HabitType.YES_NO.value)
|
||||
startActivity(intent)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
binding.buttonMeasurable.setOnClickListener {
|
||||
val intent = IntentFactory().startEditActivity(activity!!, Habit.NUMBER_HABIT)
|
||||
val intent = IntentFactory().startEditActivity(activity!!, HabitType.NUMERICAL.value)
|
||||
startActivity(intent)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import org.isoron.uhabits.inject.ActivityContextModule
|
||||
import org.isoron.uhabits.inject.DaggerHabitsActivityComponent
|
||||
import org.isoron.uhabits.utils.restartWithFade
|
||||
|
||||
class ListHabitsActivity : AppCompatActivity() {
|
||||
class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
||||
|
||||
var pureBlack: Boolean = false
|
||||
lateinit var taskRunner: TaskRunner
|
||||
@@ -51,6 +51,11 @@ class ListHabitsActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var menu: ListHabitsMenu
|
||||
|
||||
override fun onQuestionMarksChanged() {
|
||||
invalidateOptionsMenu()
|
||||
menu.behavior.onPreferencesChanged()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -63,6 +68,7 @@ class ListHabitsActivity : AppCompatActivity() {
|
||||
component.themeSwitcher.apply()
|
||||
|
||||
prefs = appComponent.preferences
|
||||
prefs.addListener(this)
|
||||
pureBlack = prefs.isPureBlackEnabled
|
||||
midnightTimer = appComponent.midnightTimer
|
||||
rootView = component.listHabitsRootView
|
||||
@@ -77,7 +83,7 @@ class ListHabitsActivity : AppCompatActivity() {
|
||||
|
||||
override fun onPause() {
|
||||
midnightTimer.onPause()
|
||||
screen.onDettached()
|
||||
screen.onDetached()
|
||||
adapter.cancelRefresh()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class ListHabitsMenu @Inject constructor(
|
||||
@ActivityContext context: Context,
|
||||
private val preferences: Preferences,
|
||||
private val themeSwitcher: ThemeSwitcher,
|
||||
private val behavior: ListHabitsMenuBehavior
|
||||
val behavior: ListHabitsMenuBehavior
|
||||
) {
|
||||
val activity = (context as AppCompatActivity)
|
||||
|
||||
@@ -52,6 +52,11 @@ class ListHabitsMenu @Inject constructor(
|
||||
nightModeItem.isChecked = themeSwitcher.isNightMode
|
||||
hideArchivedItem.isChecked = !preferences.showArchived
|
||||
hideCompletedItem.isChecked = !preferences.showCompleted
|
||||
if (preferences.areQuestionMarksEnabled || preferences.isSkipEnabled) {
|
||||
hideCompletedItem.title = activity.resources.getString(R.string.hide_entered)
|
||||
} else {
|
||||
hideCompletedItem.title = activity.resources.getString(R.string.hide_completed)
|
||||
}
|
||||
updateArrows(menu)
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.content.Intent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import dagger.Lazy
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
|
||||
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
|
||||
@@ -89,6 +90,7 @@ class ListHabitsScreen
|
||||
private val importTaskFactory: ImportDataTaskFactory,
|
||||
private val colorPickerFactory: ColorPickerDialogFactory,
|
||||
private val numberPickerFactory: NumberPickerFactory,
|
||||
private val checkMarkDialog: CheckmarkDialog,
|
||||
private val behavior: Lazy<ListHabitsBehavior>
|
||||
) : CommandRunner.Listener,
|
||||
ListHabitsBehavior.Screen,
|
||||
@@ -101,7 +103,7 @@ class ListHabitsScreen
|
||||
commandRunner.addListener(this)
|
||||
}
|
||||
|
||||
fun onDettached() {
|
||||
fun onDetached() {
|
||||
commandRunner.removeListener(this)
|
||||
}
|
||||
|
||||
@@ -225,9 +227,28 @@ class ListHabitsScreen
|
||||
override fun showNumberPicker(
|
||||
value: Double,
|
||||
unit: String,
|
||||
notes: String,
|
||||
dateString: String,
|
||||
callback: ListHabitsBehavior.NumberPickerCallback
|
||||
) {
|
||||
numberPickerFactory.create(value, unit, callback).show()
|
||||
numberPickerFactory.create(value, unit, notes, dateString, callback).show()
|
||||
}
|
||||
|
||||
override fun showCheckmarkDialog(
|
||||
value: Int,
|
||||
notes: String,
|
||||
dateString: String,
|
||||
color: PaletteColor,
|
||||
callback: ListHabitsBehavior.CheckMarkDialogCallback
|
||||
) {
|
||||
checkMarkDialog.create(
|
||||
value,
|
||||
notes,
|
||||
dateString,
|
||||
color,
|
||||
callback,
|
||||
themeSwitcher.currentTheme!!,
|
||||
).show()
|
||||
}
|
||||
|
||||
private fun getExecuteString(command: Command): String? {
|
||||
|
||||
@@ -37,9 +37,9 @@ 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.preferences.Preferences
|
||||
import org.isoron.uhabits.inject.ActivityContext
|
||||
import org.isoron.uhabits.utils.dim
|
||||
import org.isoron.uhabits.utils.drawNotesIndicator
|
||||
import org.isoron.uhabits.utils.getFontAwesome
|
||||
import org.isoron.uhabits.utils.showMessage
|
||||
import org.isoron.uhabits.utils.sp
|
||||
import org.isoron.uhabits.utils.sres
|
||||
import org.isoron.uhabits.utils.toMeasureSpec
|
||||
import javax.inject.Inject
|
||||
@@ -71,7 +71,15 @@ class CheckmarkButtonView(
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var hasNotes = false
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var onToggle: (Int) -> Unit = {}
|
||||
|
||||
var onEdit: () -> Unit = {}
|
||||
private var drawer = Drawer()
|
||||
|
||||
init {
|
||||
@@ -81,11 +89,11 @@ class CheckmarkButtonView(
|
||||
}
|
||||
|
||||
fun performToggle() {
|
||||
value = if (preferences.isSkipEnabled) {
|
||||
Entry.nextToggleValueWithSkip(value)
|
||||
} else {
|
||||
Entry.nextToggleValueWithoutSkip(value)
|
||||
}
|
||||
value = Entry.nextToggleValue(
|
||||
value = value,
|
||||
isSkipEnabled = preferences.isSkipEnabled,
|
||||
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
|
||||
)
|
||||
onToggle(value)
|
||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
invalidate()
|
||||
@@ -93,11 +101,12 @@ class CheckmarkButtonView(
|
||||
|
||||
override fun onClick(v: View) {
|
||||
if (preferences.isShortToggleEnabled) performToggle()
|
||||
else showMessage(resources.getString(R.string.long_press_to_toggle))
|
||||
else onEdit()
|
||||
}
|
||||
|
||||
override fun onLongClick(v: View): Boolean {
|
||||
performToggle()
|
||||
if (preferences.isShortToggleEnabled) onEdit()
|
||||
else performToggle()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -131,7 +140,7 @@ class CheckmarkButtonView(
|
||||
paint.color = when (value) {
|
||||
YES_MANUAL, YES_AUTO, SKIP -> color
|
||||
NO -> {
|
||||
if (preferences.areQuestionMarksEnabled()) mediumContrastColor
|
||||
if (preferences.areQuestionMarksEnabled) mediumContrastColor
|
||||
else lowContrastColor
|
||||
}
|
||||
else -> lowContrastColor
|
||||
@@ -140,11 +149,16 @@ class CheckmarkButtonView(
|
||||
SKIP -> R.string.fa_skipped
|
||||
NO -> R.string.fa_times
|
||||
UNKNOWN -> {
|
||||
if (preferences.areQuestionMarksEnabled()) R.string.fa_question
|
||||
if (preferences.areQuestionMarksEnabled) R.string.fa_question
|
||||
else R.string.fa_times
|
||||
}
|
||||
else -> R.string.fa_check
|
||||
}
|
||||
paint.textSize = when {
|
||||
id == R.string.fa_question -> sp(12.0f)
|
||||
value == YES_AUTO -> sp(13.0f)
|
||||
else -> sp(14.0f)
|
||||
}
|
||||
if (value == YES_AUTO) {
|
||||
paint.strokeWidth = 5f
|
||||
paint.style = Paint.Style.STROKE
|
||||
@@ -153,11 +167,6 @@ class CheckmarkButtonView(
|
||||
paint.style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
paint.textSize = when (id) {
|
||||
UNKNOWN -> dim(R.dimen.smallerTextSize)
|
||||
else -> dim(R.dimen.smallTextSize)
|
||||
}
|
||||
|
||||
val label = resources.getString(id)
|
||||
val em = paint.measureText("m")
|
||||
|
||||
@@ -170,6 +179,8 @@ class CheckmarkButtonView(
|
||||
paint.style = Paint.Style.FILL
|
||||
canvas.drawText(label, rect.centerX(), rect.centerY(), paint)
|
||||
}
|
||||
|
||||
drawNotesIndicator(canvas, color, em, hasNotes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,12 +54,24 @@ class CheckmarkPanelView(
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var notesIndicators = BooleanArray(0)
|
||||
set(values) {
|
||||
field = values
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var onToggle: (Timestamp, Int) -> Unit = { _, _ -> }
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var onEdit: (Timestamp) -> Unit = {}
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
override fun createButton(): CheckmarkButtonView = buttonFactory.create()
|
||||
|
||||
@Synchronized
|
||||
@@ -72,8 +84,13 @@ class CheckmarkPanelView(
|
||||
index + dataOffset < values.size -> values[index + dataOffset]
|
||||
else -> UNKNOWN
|
||||
}
|
||||
button.hasNotes = when {
|
||||
index + dataOffset < notesIndicators.size -> notesIndicators[index + dataOffset]
|
||||
else -> false
|
||||
}
|
||||
button.color = color
|
||||
button.onToggle = { value -> onToggle(timestamp, value) }
|
||||
button.onEdit = { onEdit(timestamp) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,8 +124,9 @@ class HabitCardListAdapter @Inject constructor(
|
||||
val habit = cache.getHabitByPosition(position)
|
||||
val score = cache.getScore(habit!!.id!!)
|
||||
val checkmarks = cache.getCheckmarks(habit.id!!)
|
||||
val notesIndicators = cache.getNoteIndicators(habit.id!!)
|
||||
val selected = selected.contains(habit)
|
||||
listView!!.bindCardView(holder, habit, score, checkmarks, selected)
|
||||
listView!!.bindCardView(holder, habit, score, checkmarks, notesIndicators, selected)
|
||||
}
|
||||
|
||||
override fun onViewAttachedToWindow(holder: HabitCardViewHolder) {
|
||||
|
||||
@@ -87,6 +87,7 @@ class HabitCardListView(
|
||||
habit: Habit,
|
||||
score: Double,
|
||||
checkmarks: IntArray,
|
||||
notesIndicators: BooleanArray,
|
||||
selected: Boolean
|
||||
): View {
|
||||
val cardView = holder.itemView as HabitCardView
|
||||
@@ -98,6 +99,7 @@ class HabitCardListView(
|
||||
cardView.score = score
|
||||
cardView.unit = habit.unit
|
||||
cardView.threshold = habit.targetValue / habit.frequency.denominator
|
||||
cardView.notesIndicators = notesIndicators
|
||||
|
||||
val detector = GestureDetector(context, CardViewGestureDetector(holder))
|
||||
cardView.setOnTouchListener { _, ev ->
|
||||
|
||||
@@ -57,6 +57,12 @@ class HabitCardViewFactory
|
||||
fun create() = HabitCardView(context, checkmarkPanelFactory, numberPanelFactory, behavior)
|
||||
}
|
||||
|
||||
data class DelayedToggle(
|
||||
var habit: Habit,
|
||||
var timestamp: Timestamp,
|
||||
var value: Int
|
||||
)
|
||||
|
||||
class HabitCardView(
|
||||
@ActivityContext context: Context,
|
||||
checkmarkPanelFactory: CheckmarkPanelViewFactory,
|
||||
@@ -115,12 +121,22 @@ class HabitCardView(
|
||||
numberPanel.threshold = value
|
||||
}
|
||||
|
||||
var notesIndicators
|
||||
get() = checkmarkPanel.notesIndicators
|
||||
set(values) {
|
||||
checkmarkPanel.notesIndicators = values
|
||||
numberPanel.notesIndicators = values
|
||||
}
|
||||
|
||||
var checkmarkPanel: CheckmarkPanelView
|
||||
private var numberPanel: NumberPanelView
|
||||
private var innerFrame: LinearLayout
|
||||
private var label: TextView
|
||||
private var scoreRing: RingView
|
||||
|
||||
private var currentToggleTaskId = 0
|
||||
private var queuedToggles = mutableListOf<DelayedToggle>()
|
||||
|
||||
init {
|
||||
scoreRing = RingView(context).apply {
|
||||
val thickness = dp(3f)
|
||||
@@ -143,7 +159,14 @@ class HabitCardView(
|
||||
checkmarkPanel = checkmarkPanelFactory.create().apply {
|
||||
onToggle = { timestamp, value ->
|
||||
triggerRipple(timestamp)
|
||||
habit?.let { behavior.onToggle(it, timestamp, value) }
|
||||
habit?.let {
|
||||
val taskId = queueToggle(it, timestamp, value);
|
||||
{ runPendingToggles(taskId) }.delay(TOGGLE_DELAY_MILLIS)
|
||||
}
|
||||
}
|
||||
onEdit = { timestamp ->
|
||||
triggerRipple(timestamp)
|
||||
habit?.let { behavior.onEdit(it, timestamp) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +202,24 @@ class HabitCardView(
|
||||
addView(innerFrame)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun runPendingToggles(id: Int) {
|
||||
if (currentToggleTaskId != id) return
|
||||
for ((h, t, v) in queuedToggles) behavior.onToggle(h, t, v)
|
||||
queuedToggles.clear()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun queueToggle(
|
||||
it: Habit,
|
||||
timestamp: Timestamp,
|
||||
value: Int
|
||||
): Int {
|
||||
currentToggleTaskId += 1
|
||||
queuedToggles.add(DelayedToggle(it, timestamp, value))
|
||||
return currentToggleTaskId
|
||||
}
|
||||
|
||||
override fun onModelChange() {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
habit?.let { copyAttributesFrom(it) }
|
||||
@@ -236,6 +277,7 @@ class HabitCardView(
|
||||
numberPanel.apply {
|
||||
color = c
|
||||
units = h.unit
|
||||
targetType = h.targetType
|
||||
threshold = h.targetValue
|
||||
visibility = when (h.isNumerical) {
|
||||
true -> View.VISIBLE
|
||||
@@ -262,4 +304,12 @@ class HabitCardView(
|
||||
}
|
||||
innerFrame.setBackgroundResource(background)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TOGGLE_DELAY_MILLIS = 2000L
|
||||
|
||||
fun (() -> Unit).delay(delayInMillis: Long) {
|
||||
Handler(Looper.getMainLooper()).postDelayed(this, delayInMillis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,13 +29,15 @@ import android.view.View
|
||||
import android.view.View.OnClickListener
|
||||
import android.view.View.OnLongClickListener
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.models.NumericalHabitType
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.inject.ActivityContext
|
||||
import org.isoron.uhabits.utils.InterfaceUtils.getDimension
|
||||
import org.isoron.uhabits.utils.StyledResources
|
||||
import org.isoron.uhabits.utils.dim
|
||||
import org.isoron.uhabits.utils.drawNotesIndicator
|
||||
import org.isoron.uhabits.utils.getFontAwesome
|
||||
import org.isoron.uhabits.utils.showMessage
|
||||
import org.isoron.uhabits.utils.sres
|
||||
import java.lang.Double.max
|
||||
import java.text.DecimalFormat
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -88,11 +90,22 @@ class NumberButtonView(
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var targetType = NumericalHabitType.AT_LEAST
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var units = ""
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
var hasNotes = false
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var onEdit: () -> Unit = {}
|
||||
private var drawer: Drawer = Drawer(context)
|
||||
@@ -103,8 +116,7 @@ class NumberButtonView(
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
if (preferences.isShortToggleEnabled) onEdit()
|
||||
else showMessage(resources.getString(R.string.long_press_to_edit))
|
||||
onEdit()
|
||||
}
|
||||
|
||||
override fun onLongClick(v: View): Boolean {
|
||||
@@ -127,7 +139,6 @@ class NumberButtonView(
|
||||
|
||||
private val em: Float
|
||||
private val rect: RectF = RectF()
|
||||
private val sr = StyledResources(context)
|
||||
|
||||
private val lowContrast: Int
|
||||
private val mediumContrast: Int
|
||||
@@ -148,15 +159,23 @@ class NumberButtonView(
|
||||
|
||||
init {
|
||||
em = pNumber.measureText("m")
|
||||
lowContrast = sr.getColor(R.attr.contrast40)
|
||||
mediumContrast = sr.getColor(R.attr.contrast60)
|
||||
lowContrast = sres.getColor(R.attr.contrast40)
|
||||
mediumContrast = sres.getColor(R.attr.contrast60)
|
||||
}
|
||||
|
||||
fun draw(canvas: Canvas) {
|
||||
val activeColor = when {
|
||||
value <= 0.0 -> lowContrast
|
||||
value < threshold -> mediumContrast
|
||||
else -> color
|
||||
var activeColor = if (targetType == NumericalHabitType.AT_LEAST) {
|
||||
when {
|
||||
value < 0.0 && preferences.areQuestionMarksEnabled -> lowContrast
|
||||
max(0.0, value) >= threshold -> color
|
||||
else -> mediumContrast
|
||||
}
|
||||
} else {
|
||||
when {
|
||||
value < 0.0 && preferences.areQuestionMarksEnabled -> lowContrast
|
||||
value <= threshold -> color
|
||||
else -> mediumContrast
|
||||
}
|
||||
}
|
||||
|
||||
val label: String
|
||||
@@ -169,7 +188,7 @@ class NumberButtonView(
|
||||
typeface = BOLD_TYPEFACE
|
||||
textSize = dim(R.dimen.smallTextSize)
|
||||
}
|
||||
preferences.areQuestionMarksEnabled() -> {
|
||||
preferences.areQuestionMarksEnabled -> {
|
||||
label = resources.getString(R.string.fa_question)
|
||||
typeface = getFontAwesome()
|
||||
textSize = dim(R.dimen.smallerTextSize)
|
||||
@@ -196,6 +215,8 @@ class NumberButtonView(
|
||||
rect.offset(0f, 1.3f * em)
|
||||
canvas.drawText(units, rect.centerX(), rect.centerY(), pUnit)
|
||||
}
|
||||
|
||||
drawNotesIndicator(canvas, color, em, hasNotes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.content.Context
|
||||
import org.isoron.uhabits.core.models.NumericalHabitType
|
||||
import org.isoron.uhabits.core.models.Timestamp
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.utils.DateUtils
|
||||
@@ -47,6 +48,12 @@ class NumberPanelView(
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var targetType = NumericalHabitType.AT_LEAST
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var threshold = 0.0
|
||||
set(value) {
|
||||
field = value
|
||||
@@ -65,6 +72,12 @@ class NumberPanelView(
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var notesIndicators = BooleanArray(0)
|
||||
set(values) {
|
||||
field = values
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var onEdit: (Timestamp) -> Unit = {}
|
||||
set(value) {
|
||||
field = value
|
||||
@@ -83,7 +96,12 @@ class NumberPanelView(
|
||||
index + dataOffset < values.size -> values[index + dataOffset]
|
||||
else -> 0.0
|
||||
}
|
||||
button.hasNotes = when {
|
||||
index + dataOffset < notesIndicators.size -> notesIndicators[index + dataOffset]
|
||||
else -> false
|
||||
}
|
||||
button.color = color
|
||||
button.targetType = targetType
|
||||
button.threshold = threshold
|
||||
button.units = units
|
||||
button.onEdit = { onEdit(timestamp) }
|
||||
|
||||
@@ -32,12 +32,14 @@ import org.isoron.uhabits.HabitsApplication
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
||||
import org.isoron.uhabits.activities.HabitsDirFinder
|
||||
import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
|
||||
import org.isoron.uhabits.core.commands.Command
|
||||
import org.isoron.uhabits.core.commands.CommandRunner
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
|
||||
@@ -164,9 +166,29 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
||||
override fun showNumberPicker(
|
||||
value: Double,
|
||||
unit: String,
|
||||
notes: String,
|
||||
dateString: String,
|
||||
callback: ListHabitsBehavior.NumberPickerCallback,
|
||||
) {
|
||||
NumberPickerFactory(this@ShowHabitActivity).create(value, unit, callback).show()
|
||||
NumberPickerFactory(this@ShowHabitActivity).create(value, unit, notes, dateString, callback).show()
|
||||
}
|
||||
|
||||
override fun showCheckmarkDialog(
|
||||
value: Int,
|
||||
notes: String,
|
||||
dateString: String,
|
||||
preferences: Preferences,
|
||||
color: PaletteColor,
|
||||
callback: ListHabitsBehavior.CheckMarkDialogCallback
|
||||
) {
|
||||
CheckmarkDialog(this@ShowHabitActivity, preferences).create(
|
||||
value,
|
||||
notes,
|
||||
dateString,
|
||||
color,
|
||||
callback,
|
||||
themeSwitcher.currentTheme!!,
|
||||
).show()
|
||||
}
|
||||
|
||||
override fun showEditHabitScreen(habit: Habit) {
|
||||
|
||||
@@ -43,6 +43,8 @@ class HistoryCardView(context: Context, attrs: AttributeSet) : LinearLayout(cont
|
||||
theme = state.theme,
|
||||
dateFormatter = JavaLocalDateFormatter(Locale.getDefault()),
|
||||
series = state.series,
|
||||
defaultSquare = state.defaultSquare,
|
||||
notesIndicators = state.notesIndicators,
|
||||
firstWeekday = state.firstWeekday,
|
||||
)
|
||||
binding.chart.postInvalidate()
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.isoron.platform.gui.toInt
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.activities.habits.edit.formatFrequency
|
||||
import org.isoron.uhabits.activities.habits.list.views.toShortString
|
||||
import org.isoron.uhabits.core.models.NumericalHabitType
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardState
|
||||
import org.isoron.uhabits.databinding.ShowHabitSubtitleBinding
|
||||
import org.isoron.uhabits.utils.InterfaceUtils
|
||||
@@ -65,7 +66,12 @@ class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
|
||||
binding.questionLabel.visibility = View.VISIBLE
|
||||
binding.targetIcon.visibility = View.VISIBLE
|
||||
binding.targetText.visibility = View.VISIBLE
|
||||
if (!state.isNumerical) {
|
||||
if (state.isNumerical) {
|
||||
binding.targetIcon.text = when (state.targetType) {
|
||||
NumericalHabitType.AT_LEAST -> resources.getString(R.string.fa_arrow_circle_up)
|
||||
else -> resources.getString(R.string.fa_arrow_circle_down)
|
||||
}
|
||||
} else {
|
||||
binding.targetIcon.visibility = View.GONE
|
||||
binding.targetText.visibility = View.GONE
|
||||
}
|
||||
|
||||
@@ -21,8 +21,9 @@ package org.isoron.uhabits.activities.intro
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import com.github.paolorotolo.appintro.AppIntro2
|
||||
import com.github.paolorotolo.appintro.AppIntroFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.github.appintro.AppIntro2
|
||||
import com.github.appintro.AppIntroFragment
|
||||
import org.isoron.uhabits.R
|
||||
|
||||
/**
|
||||
@@ -30,7 +31,9 @@ import org.isoron.uhabits.R
|
||||
* launched for the first time.
|
||||
*/
|
||||
class IntroActivity : AppIntro2() {
|
||||
override fun init(savedInstanceState: Bundle?) {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
showStatusBar(false)
|
||||
|
||||
addSlide(
|
||||
@@ -61,11 +64,13 @@ class IntroActivity : AppIntro2() {
|
||||
)
|
||||
}
|
||||
|
||||
override fun onNextPressed() {}
|
||||
|
||||
override fun onDonePressed() {
|
||||
override fun onDonePressed(currentFragment: Fragment?) {
|
||||
super.onDonePressed(currentFragment)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onSlideChanged() {}
|
||||
override fun onSkipPressed(currentFragment: Fragment?) {
|
||||
super.onSkipPressed(currentFragment)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,17 +23,17 @@ import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import org.isoron.uhabits.HabitsApplication
|
||||
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
||||
import org.isoron.uhabits.core.models.HabitMatcherBuilder
|
||||
import org.isoron.uhabits.core.models.HabitMatcher
|
||||
|
||||
class EditSettingActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val app = applicationContext as HabitsApplication
|
||||
val habits = app.component.habitList.getFiltered(
|
||||
HabitMatcherBuilder()
|
||||
.setArchivedAllowed(false)
|
||||
.setCompletedAllowed(true)
|
||||
.build()
|
||||
HabitMatcher(
|
||||
isArchivedAllowed = false,
|
||||
isCompletedAllowed = true,
|
||||
)
|
||||
)
|
||||
AndroidThemeSwitcher(this, app.component.preferences).apply()
|
||||
|
||||
|
||||
@@ -25,8 +25,6 @@ import android.app.AlarmManager.RTC_WAKEUP
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Context.ALARM_SERVICE
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Build.VERSION_CODES.M
|
||||
import android.util.Log
|
||||
import org.isoron.uhabits.core.AppScope
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
@@ -58,10 +56,7 @@ class IntentScheduler
|
||||
)
|
||||
return SchedulerResult.IGNORED
|
||||
}
|
||||
if (SDK_INT >= M)
|
||||
manager.setExactAndAllowWhileIdle(alarmType, timestamp, intent)
|
||||
else
|
||||
manager.setExact(alarmType, timestamp, intent)
|
||||
manager.setExactAndAllowWhileIdle(alarmType, timestamp, intent)
|
||||
return SchedulerResult.OK
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
package org.isoron.uhabits.intents
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_IMMUTABLE
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.app.PendingIntent.getBroadcast
|
||||
import android.content.Context
|
||||
@@ -49,7 +50,7 @@ class PendingIntentFactory
|
||||
action = WidgetReceiver.ACTION_ADD_REPETITION
|
||||
if (timestamp != null) putExtra("timestamp", timestamp.unixTime)
|
||||
},
|
||||
FLAG_UPDATE_CURRENT
|
||||
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
fun dismissNotification(habit: Habit): PendingIntent =
|
||||
@@ -60,7 +61,7 @@ class PendingIntentFactory
|
||||
action = WidgetReceiver.ACTION_DISMISS_REMINDER
|
||||
data = Uri.parse(habit.uriString)
|
||||
},
|
||||
FLAG_UPDATE_CURRENT
|
||||
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
fun removeRepetition(habit: Habit, timestamp: Timestamp?): PendingIntent =
|
||||
@@ -72,7 +73,7 @@ class PendingIntentFactory
|
||||
data = Uri.parse(habit.uriString)
|
||||
if (timestamp != null) putExtra("timestamp", timestamp.unixTime)
|
||||
},
|
||||
FLAG_UPDATE_CURRENT
|
||||
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
fun showHabit(habit: Habit): PendingIntent =
|
||||
@@ -84,7 +85,7 @@ class PendingIntentFactory
|
||||
habit
|
||||
)
|
||||
)
|
||||
.getPendingIntent(0, FLAG_UPDATE_CURRENT)!!
|
||||
.getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)!!
|
||||
|
||||
fun showReminder(
|
||||
habit: Habit,
|
||||
@@ -100,7 +101,7 @@ class PendingIntentFactory
|
||||
putExtra("timestamp", timestamp)
|
||||
putExtra("reminderTime", reminderTime)
|
||||
},
|
||||
FLAG_UPDATE_CURRENT
|
||||
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
fun snoozeNotification(habit: Habit): PendingIntent =
|
||||
@@ -111,7 +112,7 @@ class PendingIntentFactory
|
||||
data = Uri.parse(habit.uriString)
|
||||
action = ReminderReceiver.ACTION_SNOOZE_REMINDER
|
||||
},
|
||||
FLAG_UPDATE_CURRENT
|
||||
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
fun toggleCheckmark(habit: Habit, timestamp: Long?): PendingIntent =
|
||||
@@ -123,7 +124,7 @@ class PendingIntentFactory
|
||||
action = WidgetReceiver.ACTION_TOGGLE_REPETITION
|
||||
if (timestamp != null) putExtra("timestamp", timestamp)
|
||||
},
|
||||
FLAG_UPDATE_CURRENT
|
||||
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
fun setNumericalValue(
|
||||
@@ -133,17 +134,17 @@ class PendingIntentFactory
|
||||
timestamp: Long?
|
||||
):
|
||||
PendingIntent =
|
||||
getBroadcast(
|
||||
widgetContext,
|
||||
2,
|
||||
Intent(widgetContext, WidgetReceiver::class.java).apply {
|
||||
data = Uri.parse(habit.uriString)
|
||||
action = WidgetReceiver.ACTION_SET_NUMERICAL_VALUE
|
||||
putExtra("numericalValue", numericalValue)
|
||||
if (timestamp != null) putExtra("timestamp", timestamp)
|
||||
},
|
||||
FLAG_UPDATE_CURRENT
|
||||
)
|
||||
getBroadcast(
|
||||
widgetContext,
|
||||
2,
|
||||
Intent(widgetContext, WidgetReceiver::class.java).apply {
|
||||
data = Uri.parse(habit.uriString)
|
||||
action = WidgetReceiver.ACTION_SET_NUMERICAL_VALUE
|
||||
putExtra("numericalValue", numericalValue)
|
||||
if (timestamp != null) putExtra("timestamp", timestamp)
|
||||
},
|
||||
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
fun updateWidgets(): PendingIntent =
|
||||
getBroadcast(
|
||||
@@ -152,6 +153,6 @@ class PendingIntentFactory
|
||||
Intent(context, WidgetReceiver::class.java).apply {
|
||||
action = WidgetReceiver.ACTION_UPDATE_WIDGETS_VALUE
|
||||
},
|
||||
FLAG_UPDATE_CURRENT
|
||||
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
|
||||
@@ -153,13 +153,15 @@ class AndroidNotificationTray
|
||||
if (preferences.shouldMakeNotificationsLed())
|
||||
builder.setLights(Color.RED, 1000, 1000)
|
||||
|
||||
val snoozeAction = Action(
|
||||
R.drawable.ic_action_snooze,
|
||||
context.getString(R.string.snooze),
|
||||
pendingIntents.snoozeNotification(habit)
|
||||
)
|
||||
wearableExtender.addAction(snoozeAction)
|
||||
builder.addAction(snoozeAction)
|
||||
if (SDK_INT < Build.VERSION_CODES.S) {
|
||||
val snoozeAction = Action(
|
||||
R.drawable.ic_action_snooze,
|
||||
context.getString(R.string.snooze),
|
||||
pendingIntents.snoozeNotification(habit)
|
||||
)
|
||||
wearableExtender.addAction(snoozeAction)
|
||||
builder.addAction(snoozeAction)
|
||||
}
|
||||
|
||||
builder.extend(wearableExtender)
|
||||
return builder.build()
|
||||
|
||||
@@ -92,6 +92,9 @@ class SharedPreferencesStorage
|
||||
preferences.setNotificationsSticky(getBoolean(key, false))
|
||||
"pref_led_notifications" ->
|
||||
preferences.setNotificationsLed(getBoolean(key, false))
|
||||
"pref_unknown_enabled" -> {
|
||||
preferences.areQuestionMarksEnabled = getBoolean(key, false)
|
||||
}
|
||||
}
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import android.content.BroadcastReceiver
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.util.Log
|
||||
import org.isoron.uhabits.HabitsApplication
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
@@ -76,8 +78,21 @@ class ReminderReceiver : BroadcastReceiver() {
|
||||
}
|
||||
ACTION_SNOOZE_REMINDER -> {
|
||||
if (habit == null) return
|
||||
Log.d("ReminderReceiver", String.format("onSnoozePressed habit=%d", habit.id))
|
||||
reminderController.onSnoozePressed(habit, context)
|
||||
if (SDK_INT < Build.VERSION_CODES.S) {
|
||||
Log.d(
|
||||
"ReminderReceiver",
|
||||
String.format("onSnoozePressed habit=%d", habit.id)
|
||||
)
|
||||
reminderController.onSnoozePressed(habit, context)
|
||||
} else {
|
||||
Log.w(
|
||||
"ReminderReceiver",
|
||||
String.format(
|
||||
"onSnoozePressed habit=%d, should be deactivated in recent versions.",
|
||||
habit.id
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
Intent.ACTION_BOOT_COMPLETED -> {
|
||||
Log.d("ReminderReceiver", "onBootCompleted")
|
||||
|
||||
@@ -22,7 +22,9 @@ package org.isoron.uhabits.utils
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Handler
|
||||
import android.view.LayoutInflater
|
||||
@@ -199,5 +201,15 @@ fun View.dim(id: Int) = InterfaceUtils.getDimension(context, id)
|
||||
fun View.sp(value: Float) = InterfaceUtils.spToPixels(context, value)
|
||||
fun View.dp(value: Float) = InterfaceUtils.dpToPixels(context, value)
|
||||
fun View.str(id: Int) = resources.getString(id)
|
||||
|
||||
fun View.drawNotesIndicator(canvas: Canvas, color: Int, size: Float, hasNotes: Boolean) {
|
||||
val pNotesIndicator = Paint()
|
||||
pNotesIndicator.color = color
|
||||
if (hasNotes) {
|
||||
val cy = 0.8f * size
|
||||
canvas.drawCircle(width.toFloat() - cy, cy, 8f, pNotesIndicator)
|
||||
}
|
||||
}
|
||||
|
||||
val View.sres: StyledResources
|
||||
get() = StyledResources(context)
|
||||
|
||||
@@ -56,7 +56,10 @@ class HistoryWidget(
|
||||
theme = WidgetTheme(),
|
||||
)
|
||||
(widgetView.dataView as AndroidDataView).apply {
|
||||
(this.view as HistoryChart).series = model.series
|
||||
val historyChart = (this.view as HistoryChart)
|
||||
historyChart.series = model.series
|
||||
historyChart.defaultSquare = model.defaultSquare
|
||||
historyChart.notesIndicators = model.notesIndicators
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +74,8 @@ class HistoryWidget(
|
||||
dateFormatter = JavaLocalDateFormatter(Locale.getDefault()),
|
||||
firstWeekday = prefs.firstWeekday,
|
||||
series = listOf(),
|
||||
defaultSquare = HistoryChart.Square.OFF,
|
||||
notesIndicators = listOf(),
|
||||
)
|
||||
}
|
||||
).apply {
|
||||
|
||||
@@ -26,8 +26,8 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import android.widget.RemoteViews
|
||||
import org.isoron.platform.utils.StringUtils
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.utils.StringUtils
|
||||
|
||||
class StackWidget(
|
||||
context: Context,
|
||||
|
||||
@@ -27,11 +27,11 @@ import android.util.Log
|
||||
import android.widget.RemoteViews
|
||||
import android.widget.RemoteViewsService
|
||||
import android.widget.RemoteViewsService.RemoteViewsFactory
|
||||
import org.isoron.platform.utils.StringUtils.Companion.splitLongs
|
||||
import org.isoron.uhabits.HabitsApplication
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.HabitNotFoundException
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.utils.StringUtils.Companion.splitLongs
|
||||
import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels
|
||||
import java.util.ArrayList
|
||||
|
||||
|
||||
@@ -60,8 +60,8 @@ class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPi
|
||||
SystemUtils.unlockScreen(this)
|
||||
}
|
||||
|
||||
override fun onNumberPicked(newValue: Double) {
|
||||
behavior.setValue(data.habit, data.timestamp, (newValue * 1000).toInt())
|
||||
override fun onNumberPicked(newValue: Double, notes: String) {
|
||||
behavior.setValue(data.habit, data.timestamp, (newValue * 1000).toInt(), notes)
|
||||
widgetUpdater.updateWidgets()
|
||||
finish()
|
||||
}
|
||||
@@ -79,6 +79,8 @@ class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPi
|
||||
numberPickerFactory.create(
|
||||
entry.value / 1000.0,
|
||||
data.habit.unit,
|
||||
entry.notes,
|
||||
today.toDialogDateString(),
|
||||
this
|
||||
).show()
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ class CheckmarkWidgetView : HabitWidgetView {
|
||||
SKIP -> resources.getString(R.string.fa_skipped)
|
||||
UNKNOWN -> {
|
||||
run {
|
||||
if (preferences!!.areQuestionMarksEnabled()) {
|
||||
if (preferences!!.areQuestionMarksEnabled) {
|
||||
return resources.getString(R.string.fa_question)
|
||||
} else {
|
||||
resources.getString(R.string.fa_times)
|
||||
|
||||
1
uhabits-android/src/main/play/listings/af-rZA/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Loop - Gewoonte Boekhouer
|
||||
@@ -1,22 +0,0 @@
|
||||
لب يساعدك على خلق والحفاظ على العادات الجيدة، مما يسمح لك لتحقيق أهدافكة. الرسوم البيانية والإحصاءات التفصيلية تبين لكم كيف تحسن عاداتك مع مرور الوقت. هو تماما خالية من الاعلانات ومفتوحة المصدر.
|
||||
|
||||
<b>واجهة بسيطة، جميلة وحديثة </b>
|
||||
لوب يحتوي على واجهة بسيطة وهي سهلة الاستخدام و تتابع نظام تصميم الماتريل دسيجن.
|
||||
|
||||
<b>نتيجة العادات</b>
|
||||
بالإضافة إلى عرض التقدم الحالي، لوب ديه خوارزمية متقدمة لحساب قوة عاداتك. كل التكرار يجعل هذه العادة أقوى، وفي كل يوم غاب يجعلها أضعف. مع ذلك غيب أيام قليلة بعد تقدم طويلة ، لن تدمر تماما تقدمك .
|
||||
|
||||
<b>الرسوم البيانية والإحصاءات المفصلة</b>
|
||||
نرى بوضوح كيف كنت قد تحسنت عاداتك بمرور الوقت مع الرسوم البيانية الجميله ومفصلة. انتقل إلى الوراء لنرى التاريخ الكامل لعاداتك.
|
||||
|
||||
<b>جداول مرنة</b>
|
||||
تؤيد كل من العادات اليومية والعادات مع جداول أكثر تعقيدا، مثل 3 مرات كل أسبوع، مرة واحدة كل أسبوعين، أو مرة كل يومين.
|
||||
|
||||
<b>تذكير</b>
|
||||
إنشاء تذكير لكل فرد من عاداتك، في ساعة اختيار من اليوم. تحقق بسهولة، رفض أو غفوة عادتك مباشرة من الإخطار، دون الحاجة إلى فتح التطبيق.
|
||||
|
||||
<b>خالية تماما من الإعلانات و المصدر المفتوح</b>
|
||||
لا توجد على الاطلاق الإعلانات والشعارات المزعجة أو أذونات إضافية في هذا التطبيق، و سوف يكون هناك أبدا.
|
||||
|
||||
<b>الأمثل للساعات الذكية</b>
|
||||
يمكن التحقق من رسائل التذكير، رفض أو غفوة عادتك مباشرة من ساعتك الاندرويد وير.
|
||||
@@ -1 +0,0 @@
|
||||
خلق عادات جيدة وتتبع تقدمك على مر الزمن
|
||||
@@ -1 +0,0 @@
|
||||
لوب ملاحق العادة
|
||||
1
uhabits-android/src/main/play/listings/bg-rBG/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Loop Следене на навици
|
||||
1
uhabits-android/src/main/play/listings/ca-rES/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Loop - Hàbit Rastrejador
|
||||
@@ -1,22 +0,0 @@
|
||||
Loop t'ajuda a crear i mantenir bons hàbits, permetent-te aconseguir els teus objectius a llarg termini. Els gràfics i estadístiques detallades et mostren com han millorat els teus hàbits al llarg del temps. És completament de codi obert i lliure de publicitat.
|
||||
|
||||
<b>Senzilla, bonica i moderna interfície</b>
|
||||
Loop té una interfície minimalista que és fàcil d'utilitzar i segueix les guies de "material design".
|
||||
|
||||
<b>Puntuació d'hàbit</b>
|
||||
A més de mostrar la teva ratxa actual, Loop té un algoritme avançat per a calcular la fortalesa dels teus hàbits. Cada repetició fa el teu hàbit més fort, i cada dia que fallis el farà més dèbil. Tot i això uns quants dies que fallis després d'una llarga ratxa no malmetrà completament el teu progrès.
|
||||
|
||||
<b>Gràfics i estadístiques detallades</b>
|
||||
Permet veure de forma clara com han millorat els teus hàbits al llarg del temps amb gràfics bonics i detallats. Pots anar enrera i veure també l'històric complet dels teus hàbits.
|
||||
|
||||
<b>Planificació flexible</b>
|
||||
Suporta tant hàbits diaris com hàbits amb planificacions més complexes, com per exemple 3 vegades per setmana; un cop setmana si setmana no; o un dia si i altre no.
|
||||
|
||||
<b>Recordatoris</b>
|
||||
Crea un recordatori individual per a cada hàbit, a l'hora escollida del dia. Revisa fàcilment, endarrereix o anul·la el teu hàbit directament des de la notificació, sense obrir l'app.
|
||||
|
||||
<b>Completament de codi obert i lliure de publicitat</b>
|
||||
No hi ha cap tipus de publicitat, notificacions molestes o permisos intrusius en aquesta app, i mai n'hi haurà. El codi font complet està disponible sota la llicència GPLv3.
|
||||
|
||||
<b>Optimitzat per a rellotges intel·ligents</b>
|
||||
Els recordatoris poden ser revisats, endarrerits o anul·lats directament des del teu rellotge Android Wear.
|
||||
@@ -1 +0,0 @@
|
||||
Crea bons hàbits i segueix el seu progrés al llarg del temps (sense publicitat)
|
||||
@@ -1,22 +0,0 @@
|
||||
Aplikace ti pomůže vytvořit a udržovat si prospěšné návyky a dosáhnout tak tvých dlouhodobých cílů. Detailní grafy a statistiky ti ukáží, jak se tvoje zvyky postupem času zlepšují. Vše je kompletně bez reklam a open source.
|
||||
|
||||
<b>Jednoduché, krásné a moderní prostředí</b>
|
||||
Aplikace má minimalistický design s jednoduchým použitím. Dodržuje pravidla material designu.
|
||||
|
||||
<b>Síla zvyku</b>
|
||||
Pro zobrazení aktuální úspěšné serie aplikace využívá pokročilý algoritmus, aby vypočítala sílu tvých zvyků. Každé opakování dělá tvůj zvyk silnějším a každé nedodržení ho oslabuje. Ale pár vynechaných dní po dlouhé řadě kompletně nezničí celý tvůj postup.
|
||||
|
||||
<b>Detailní grafy a statistika</b>
|
||||
Přehledně vidíš na krásných grafech, jak moc se tvoje zvyky zlepšují v průběhu času. Jeď zpět, abys viděl kompletní historii.
|
||||
|
||||
<b>Flexibilní časový plán</b>
|
||||
Podpora jak denních návyků, tak návyků s komplexnějším rozvrhem. Jako jsou 3 krát týdně, jednou za dva týdny, nebo obden.
|
||||
|
||||
<b>Upomínky</b>
|
||||
Vytvoř si individuální upomínku pro každý zvyk ve zvolený čas. Jednoduše potvrď, přeskoč nebo odlož notifikace bez nutnosti otevření aplikace.
|
||||
|
||||
<b>Kompletně bez reklam a open source</b>
|
||||
Nejsou tu naprosto žádné reklamy, otravné notifikace nebo dotěrná povolení aplikace. A nikdy nebudou. Kompletní zdrojový kód je dostupný pod GPLv3.
|
||||
|
||||
<b>Optimalizované pro chytré hodinky</b>
|
||||
Upomínky mohou být potvrzen, odloženy nebo smazány přímo z tvého zařízení s Android Wear.
|
||||
@@ -1 +0,0 @@
|
||||
Vytvoř si prospěšné návyky a sleduj jejich vývoj v průběhu času (bez reklam)
|
||||
@@ -1 +0,0 @@
|
||||
Loop - Habit Tracker
|
||||
1
uhabits-android/src/main/play/listings/da-rDK/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Loop Vane Tracker
|
||||
@@ -1,22 +0,0 @@
|
||||
Loop hilft dir gute Gewohnheiten anzunehmen und deine langfristigen Ziele zu erreichen. Detailierte Statistiken zeigen dir, wie du dich entwickelt hast. Es ist ohne Werbung und Open Source.
|
||||
|
||||
<b>Einfaches, schönes und modernes Oberfläche</b>
|
||||
Loop hat eine minimale Oberfläche und ist deshalb einfach zu nutzen. Es folgt dem material Design.
|
||||
|
||||
<b>Habit Punkte</b>
|
||||
Um dir deine Schwächen zu zeigen, hat Loop einen Algorithmus, um deine starken Angewohnheiten zu erkennen. Jede Wiederholung verstärkt diese und jedes Aussetzen schwächt sie. Aber ein paar Verfehlungen nach langem Durchhalten machen natürlich nicht gleich alles zu nichte.
|
||||
|
||||
<b>Statistiken</b>
|
||||
Schau dir an, wie sich deine Angewohnheiten im Laufe der Zeit gemacht haben. Schau auf die schönen Diagramme und gehe zurück im gesamten Verlauf.
|
||||
|
||||
<b>Flexible Zeiten</b>
|
||||
Unterstützt sowohl tägliche Vorgaben, als auch komplexere Pläne, woe etwa 3 mal pro Woche; ein mal in jeder anderen Woche; oder jeden anderen Tag.
|
||||
|
||||
<b>Erinnerungen</b>
|
||||
Erstelle individuelle Erinnerungen und wann diese dich benachrichtigen sollen. Kontrolliere deine Vorhaben ganz einfach und lehne sie bei Bedarf direkt ab, ohne in die App zu wechseln.
|
||||
|
||||
<b>Komplett werbefrei und Open Source</b>
|
||||
Es gibt absolut keine Werbung, nervende Einblendungen oder merkwürdige Berechtigungen in dieser App und das wird auch so bleiben. Der komplette Quellcode steht unter der GPLv3.
|
||||
|
||||
<b>Optimiert für Smartwatches</b>
|
||||
Erinnerungen können direkt von der Android Wear watch kontrolliert, pausiert, oder verschoben werden.
|
||||
@@ -1 +0,0 @@
|
||||
Nimm gute Gewohnheiten an und verfolge deinen Fortschritt (ohne Werbung)
|
||||
1
uhabits-android/src/main/play/listings/de-rDE/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Loop - Gewohnheiten Tracking
|
||||
1
uhabits-android/src/main/play/listings/el-rGR/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Loop - Καταγραφή Συνηθειών
|
||||
@@ -1,22 +0,0 @@
|
||||
Loop te ayuda a crear y mantener buenos hábitos, permitiéndote alcanzar tus metas a largo plazo. Detallados gráficos y estadísticas muestran como tus hábitos mejoran con el tiempo. No existe ningún anuncio y es de código abierto.
|
||||
|
||||
<b>Una interfaz simple, bella y moderna</b>
|
||||
Loop tiene una interfaz minimalista que es fácil de usar y sigue los principios del material design.
|
||||
|
||||
<b>Puntuación del hábito</b>
|
||||
Además de mostrar tu racha actual, Loop tiene un algoritmo avanzado para calcular la fuerza de tus hábitos. Cada repetición hace tu hábito más fuerte y cada día fallido lo hace más débil. Sin embargo, unos pocos días después de una larga racha no destruirán completamente todo tu progreso.
|
||||
|
||||
<b>Detallados gráficos y estadísticas</b>
|
||||
Observa claramente como tus hábitos han mejorado con el tiempo con bellos y detallados gráficos. Ve hacia atrás para ver el historial completo del hábito.
|
||||
|
||||
<b>Horarios flexibles</b>
|
||||
Soporta hábitos diarios y hábitos con repeticiones más complejas, como 3 veces por semana; una vez cada varias semanas; o cada día.
|
||||
|
||||
<b>Recordatorios</b>
|
||||
Crea recordatorios individuales para cada hábito a una hora determinada del día. Fácilmente marcables, descartables o posponibles directamente desde la notificación, sin abrir la app.
|
||||
|
||||
<b>Completamente sin anuncios y de código abierto</b>
|
||||
No existe ningún tipo de publicidad, notificaciones molestas o permisos intrusivos, y nunca los habrá. Todo el código está disponible bajo GPLv3.
|
||||
|
||||
<b>Optimizado para smartwatches</b>
|
||||
Los recordatorios se pueden marcar, posponer o descartar directamente desde tu reloj Android Wear.
|
||||
@@ -1 +0,0 @@
|
||||
Crea buenos hábitos y haz un seguimiento de su progreso a lo largo del tiempo
|
||||
@@ -1 +0,0 @@
|
||||
Loop - Analizador de Hábitos
|
||||
1
uhabits-android/src/main/play/listings/es-rES/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Loop Analizador de Hábitos
|
||||
@@ -1,22 +0,0 @@
|
||||
Loop vous aide à créer et maintenir de bonnes habitudes, permettant de réussir vos objectifs à long terme. Des graphiques détaillés et des statistiques vous montrent comment vos habitudes s’améliorent au fil du temps. C'est totalement sans pub et open source.
|
||||
|
||||
<b>Simple, beau avec une interface moderne</b>
|
||||
Loop a une interface minimaliste, facile à utiliser et qui suit les règles de material design.
|
||||
|
||||
<b>Score d'habitude</b>
|
||||
En plus de montrer votre série en cours, Loop a un algorithme pour calculer la force de vos habitudes. Chaque jours réussis augmente la force de l'habitude chaque jours ratés le rend plus faible. Cependant, quelques jours ratés après une longue série ne détruiront pas entièrement votre progrès.
|
||||
|
||||
<b>Graphiques détaillés et statistiques</b>
|
||||
Observez clairement comment vos habitudes s’améliorent au fil du temps avec de beaux graphiques détaillés. Défilez vers les jours passés pour voir l'historique complet de vos habitudes.
|
||||
|
||||
<b>Calendrier flexible</b>
|
||||
Supporte les habitudes quotidiennes et celles avec un calendrier plus complexes, comme 3 fois par semaine, une fois par semaine ou un jour sur deux.
|
||||
|
||||
<b>Rappel</b>
|
||||
Créez un rappel propre pour chaque habitude, à une heure choise de la journée. Cochez, supprimez ou réglez à plus tard votre habitude directement à partir de la notification, sans l'ouvrir l'application.
|
||||
|
||||
<b>Entièrement sans pub et open-source</b>
|
||||
Il n'y a pas de publicités, de notifications embêtantes ou de permissions intrusives avec cette application, et il n'y en aura jamais. L'ensemble du code source est disponible sous GPLv3.
|
||||
|
||||
<b>Optimisée pour les montres android</b>
|
||||
Les rappels peuvent être cochés, reportés ou supprimés directement à partir de votre montre Android
|
||||
@@ -1 +0,0 @@
|
||||
Créez des bonnes habitudes et suivez leurs avancées au fil du temps (sans pub)
|
||||
1
uhabits-android/src/main/play/listings/hi-rIN/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Loop - आदत पर नजर
|
||||
1
uhabits-android/src/main/play/listings/hr-rHR/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Loop Habit Tracker
|
||||
@@ -1,21 +0,0 @@
|
||||
Loop Vam pomaže stvoriti i održavati dobre navike kako biste ostvarili svoje dugoročne ciljeve. Detaljni grafikoni Vam pomažu kako biste bolje pratili kako napreduju Vaše navike. Loop je open source aplikacija i ne sadrži nikakav oblik reklama.
|
||||
|
||||
<b>Jednostavno, lijepo i moderno sučelje.</b>
|
||||
Loop sadrži minimalistično sučelje jednostavno za korištenje koje prati smjernice Material Design-a.
|
||||
|
||||
<b>Rezultat navike</b>
|
||||
Osim prikazivanja trenutnog niza, Loop ima napredni algoritam za računanje snage vaših navika. Svako ponavljanje jača vašu naviku, a svaki propušteni dan ju čini slabijom. Međutim, par propuštenih dana nakon dugačkog niza neće u potpunosti uništiti cjelokupni napredak.
|
||||
|
||||
<b>Detaljni grafovi i statistike</b>
|
||||
Jasno vidite kako su se vaše navike poboljšale kroz vrijeme u prekrasnim i detaljnim grafovima. Kliznite natrag kako bi prikazali kompletnu povijest vaših navika.
|
||||
|
||||
<b>Fleksibilno planiranje</b>
|
||||
Podržava i dnevne navike i navike sa složenijim planiranjem, kao npr. 3 puta svakog tjedna; jednom svaki drugi tjedan; ili svaki drugi dan.
|
||||
|
||||
<b>Podsjetnici</b>
|
||||
Stvorite individualne podsjetnike za svaku naviku, u određenom satu u danu. Lako provjeravajte, odbacite i odgodite vašu naviku direktno iz obavijesti, bez otvaranja aplikacije.
|
||||
|
||||
<b>Optimizirano za pametne satove</b>
|
||||
Podsjetnici se mogu provjeravati, odgoditi ili odbaciti direktno sa vašeg Android Wear sata.
|
||||
|
||||
<b>Potpuno bez oglasa i otvorenog izvora koda</b>
|
||||
@@ -1 +0,0 @@
|
||||
Stvorite dobre navike i pratite njihov napredak (bez reklama)
|
||||
1
uhabits-android/src/main/play/listings/hu-rHU/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Loop Szokásrögzítő
|
||||
1
uhabits-android/src/main/play/listings/hy-rAM/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Loop Habit Tracker
|
||||
@@ -1,23 +0,0 @@
|
||||
Loop membantu Anda untuk membuat dan mengelola Kebiasaan baik, memungkinkan Anda mencapai gol jangka panjang. Grafik detail dan statistik menampilkan perkembangan Kebiasaan Anda dari waktu ke waktu. Aplikasi ini bebas iklan dan open-source.
|
||||
|
||||
<b>Sederhana, minimalis dengan balutan antarmuka yang modern</b>
|
||||
Loop memiliki tampilan minimalis yang mudah digunakan dan mengikuti panduan material design.
|
||||
|
||||
<b>Skor Kebiasaan</b>
|
||||
Algoritma Loop akan menghitung kekuatan Kebiasaan Anda. Setiap kali Anda melakukan pengulangan maka skor Anda akan bertambah, sebaliknya jika Anda tidak mengerjakan Kebiasaan maka nilai akan dikurangi. Beberapa hari yang terlewat tidak akan menghapus seluruh perkembangan Kebiasaan Anda.
|
||||
|
||||
<b>Detail Grafik dan Statistik</b>
|
||||
Lihat perkembangan Kebiasaan dari waktu ke waktu dengan tampilan yang menarik dan detail. Geser ke bawah untuk melihat seluruh catatan Kebiasaan Anda.
|
||||
|
||||
<b>Pengaturan jadwal fleksibel</b>
|
||||
Mendukung kebiasaan harian dan kebiasaan dengan penjadwalan yang kompleks, seperti 3 kali dalam setiap minggu; 2 minggu sekali; hingga 2 hari sekali.
|
||||
|
||||
<b>Pengingat</b>
|
||||
Anda dapat membuat pengingat untuk setiap Kebiasaan, dengan waktu yang telah Anda tentukan setiap harinya. Mudah untuk di-cek, batalkan ataupun tunda melalui panel notifikasi tanpa perlu membuka aplikasi.
|
||||
|
||||
<b>Bebas dari iklan dan open-source</b>
|
||||
Tidak ada iklan, notifikasi menyebalkan dan juga hak akses yang tidak dibutuhkan.
|
||||
Kode aplikasi tersedia dengan lisensi GPLv3
|
||||
|
||||
<b>Mendukung Smartwatch</b>
|
||||
Anda dapat dengan mudah mengecek, menunda ataupun membatalkan pengingat melalui smartwatch Anda.
|
||||
@@ -1 +0,0 @@
|
||||
Buat Kebiasaan baik dan catat perkembangannya setiap waktu (bebas iklan)
|
||||
1
uhabits-android/src/main/play/listings/in-rID/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Loop Habit Tracker
|
||||
@@ -1,18 +0,0 @@
|
||||
Loop ti aiuta a creare e mantenere buone abitudini, permettendoti di raggiungere i tuoi obbiettivi a lungo termine. Grafici dettagliati e le statistiche ti mostrano come le tue abitudini sono migliorate durante il tempo. E' completamente senza pubblicità ed opensource.
|
||||
|
||||
<b>Interfaccia semplice e moderna</b> Loop ha un'interfaccia minimale che è semplice da usare e segue le linee guida del Material Design
|
||||
|
||||
<b>Forza dell'abitudine</b>
|
||||
In aggiunta al traguardo attuale, Loop ha un algoritmo avanzato per calcolare la forza delle tue abitudini. Ogni ripetizione la rafforza, mentre ogni giorno mancato la indebolisce. Pochi giorni mancati dopo una lunga serie però non vanificherà completamente il tuo progresso totale.
|
||||
|
||||
<b>Grafici dettagliati e statistiche</b> Visualizza in modo semplice come le tue abitudini sono migliorate nel tempo con grafici dettagliati. Scorri indietro per vedere la cronologia completa delle tue abitudini.
|
||||
|
||||
<b>Programmi flessibili</b> Supporto per abitudini sia giornaliere che con organizzazioni più complesse, come 3 volte alla settimana; una volta ogni 2 settimane; ogni due giorni...
|
||||
|
||||
<b>Promemoria</b>
|
||||
Crea un promemoria per ogni abitudine, ad una specificata ora del giorno. Completa, ritarda o ignora il promemoria direttamente dalla notifica, senza aprire l'app.
|
||||
|
||||
<b>Completamente gratuito ed opensource</b>
|
||||
Non ci sono pubblicità, notifiche invasive o permessi intrusivi e mai ce ne saranno. Il codice sorgente completo è disponibile sotto licenza GPLv3.
|
||||
|
||||
<b>Ottimizzata per gli smartwatch</b> I promemoria possono essere completati, ritardati o ignorati direttamente dal tuo orologio Android Wear.
|
||||
@@ -1 +0,0 @@
|
||||
Acquisisci nuove abitudini e traccia il tuo progresso (senza pubblicità)
|
||||
1
uhabits-android/src/main/play/listings/it-rIT/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Loop - Tracciatore di Abitudine
|
||||