Compare commits

...

50 Commits

Author SHA1 Message Date
Chris Smowton
107c898f51 Add repository now required for old kotlin-css-jvm versions
Gradle Plugin Portal was previously making kotlin-css-jvm 1.0.0-pre.148-kotlin-1.4.30 available by virtue of mirroring JCenter. Now that Gradle have stopped doing that in preparation for JCenter to go away, we need to retrieve the old version from a different repository.

If this is bumped forwards to Kotlin 1.5.0+ it will become possible to get the package from Central instead.
2025-02-08 22:11:53 -06:00
Abdul Majeed
4a7d7ef490 Added Streaks info for numeric habits 2025-02-08 22:07:28 -06:00
Abdul Majeed
13ecc2a386 Added Streaks info for numeric habits 2025-02-08 22:07:28 -06:00
Jakub Kalinowski
2296a49999 Corrected HabitTest 2025-02-08 21:58:41 -06:00
Jakub Kalinowski
303020a8c0 Never mark AT_MOST habits as completed for today 2025-02-08 21:58:41 -06:00
Jakub Kalinowski
37219cb13f Ignoring isCompleted for AT_MOST habits 2025-02-08 21:56:17 -06:00
CM
62d9d29e91 [Fix] Markdown linter problems (#2084)
* MD024

* MD033

* MD034

* MD036

* MD040

* MD045

* MD046

---------

Co-authored-by: MacroController <anonymous@github.com>
2025-02-08 21:48:23 -06:00
451b536e71 Upgrade dependencies 2025-02-08 21:37:16 -06:00
14dbf90c23 Upgrade Kotlin, AGP 2025-02-08 21:12:11 -06:00
280a5ddceb Bump targedSdk to 35 2025-02-08 20:58:23 -06:00
22331ed364 Remove dependabot 2025-02-08 07:43:10 -06:00
d0a45eb523 Merge pull request #2028 from getgo-nobugs/dev
Fix the misalignment of "About" page in RTL language
2025-02-08 07:38:03 -06:00
getgo-nobugs
fec73af665 Update styles.xml 2024-07-28 11:16:23 +08:00
fc9cc423d0 Multiple fixes to edge-to-edge display (API 35) 2024-05-20 20:30:09 -05:00
e7165d993f Make compatible with edge-to-edge (API 35) 2024-05-20 09:33:00 -05:00
732ec1c70a Update AGP; disable lint 2024-05-19 20:36:43 -05:00
e823cd5758 Update lint-baseline 2024-05-19 20:01:32 -05:00
46fe683d71 Merge pull request #1942 from foralost/#1941_DateUtilsTest_failing
#1941 DateUtils test fix
2024-04-04 22:03:46 -05:00
7f6248123c DateUtilsTest: Simplify getCalendarUTC 2024-04-04 21:55:01 -05:00
2024277ebe Merge branch 'dev' into #1941_DateUtilsTest_failing 2024-04-04 21:52:27 -05:00
c216fb01d6 Merge pull request #1785 from hiqua/android_lint_baseline
Check android lint violations against current baseline
2024-04-04 21:45:37 -05:00
7cb32f486b lint: disable GradleDependency 2024-04-04 21:36:51 -05:00
093591fbaf Update lint baseline 2024-04-04 21:24:09 -05:00
985234cdf3 Merge branch 'dev' into android_lint_baseline 2024-04-04 21:18:22 -05:00
06090e238a Merge branch 'feature/konfetti' into dev 2024-04-02 19:51:59 -05:00
e48452f724 Move showConfetti to ListHabitsScreen; use button location; other tweaks 2024-04-02 19:34:00 -05:00
dependabot[bot]
936986e110 Bump daggerVersion from 2.50 to 2.51.1
Bumps `daggerVersion` from 2.50 to 2.51.1.

Updates `com.google.dagger:dagger` from 2.50 to 2.51.1
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.50...dagger-2.51.1)

Updates `com.google.dagger:dagger-compiler` from 2.50 to 2.51.1
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.50...dagger-2.51.1)

---
updated-dependencies:
- dependency-name: com.google.dagger:dagger
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.google.dagger:dagger-compiler
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 22:04:24 +02:00
dependabot[bot]
4b3910aea8 Bump com.google.guava:guava from 33.0.0-android to 33.1.0-android
Bumps [com.google.guava:guava](https://github.com/google/guava) from 33.0.0-android to 33.1.0-android.
- [Release notes](https://github.com/google/guava/releases)
- [Commits](https://github.com/google/guava/commits)

---
updated-dependencies:
- dependency-name: com.google.guava:guava
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-01 18:11:48 +02:00
foralost
10074ded32 UTC Calendar fix 2024-03-04 11:31:45 +01:00
Gokul K
1280e798d2 rendering only if layout and konfetti view exists - to avoid null reference errors 2024-02-03 23:14:58 -06:00
Gokul K
b09306e793 fixing another lint error 2024-02-03 23:14:58 -06:00
Gokul K
e30636a447 fixed ktlint erorrs and is building successfully now 2024-02-03 23:14:58 -06:00
Gokul K
ad8738180c fixing some comments from @hiqua 2024-02-03 23:14:58 -06:00
Gokul K
08410c59d0 introducing confetti animation using konfetti library 2024-02-03 23:14:58 -06:00
Manish Kumar Verma
ab86cee70b Fix: No notification when goal type is set to "maximum" (#1931) 2024-02-02 19:21:40 -06:00
dependabot[bot]
3a0603605b Bump androidx.annotation:annotation from 1.7.0 to 1.7.1
Bumps androidx.annotation:annotation from 1.7.0 to 1.7.1.

---
updated-dependencies:
- dependency-name: androidx.annotation:annotation
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-01 04:43:18 -06:00
dependabot[bot]
6a78b4d853 Bump daggerVersion from 2.48.1 to 2.50 (#1907)
Bumps `daggerVersion` from 2.48.1 to 2.50.

Updates `com.google.dagger:dagger` from 2.48.1 to 2.50
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.48.1...dagger-2.50)

Updates `com.google.dagger:dagger-compiler` from 2.48.1 to 2.50
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.48.1...dagger-2.50)

---
updated-dependencies:
- dependency-name: com.google.dagger:dagger
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.google.dagger:dagger-compiler
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-01 04:34:40 -06:00
dependabot[bot]
fe43b1435d Bump ch.qos.logback:logback-classic from 1.4.13 to 1.4.14 (#1905)
Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.4.13 to 1.4.14.
- [Commits](https://github.com/qos-ch/logback/compare/v_1.4.13...v_1.4.14)

---
updated-dependencies:
- dependency-name: ch.qos.logback:logback-classic
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-01 04:33:18 -06:00
dependabot[bot]
12503b8a6d Bump com.google.guava:guava from 32.1.3-android to 33.0.0-android (#1902)
Bumps [com.google.guava:guava](https://github.com/google/guava) from 32.1.3-android to 33.0.0-android.
- [Release notes](https://github.com/google/guava/releases)
- [Commits](https://github.com/google/guava/commits)

---
updated-dependencies:
- dependency-name: com.google.guava:guava
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-01 04:33:03 -06:00
dependabot[bot]
ef7f78bff0 Bump org.xerial:sqlite-jdbc from 3.42.0.0 to 3.45.1.0 (#1929)
Bumps [org.xerial:sqlite-jdbc](https://github.com/xerial/sqlite-jdbc) from 3.42.0.0 to 3.45.1.0.
- [Release notes](https://github.com/xerial/sqlite-jdbc/releases)
- [Changelog](https://github.com/xerial/sqlite-jdbc/blob/master/CHANGELOG)
- [Commits](https://github.com/xerial/sqlite-jdbc/compare/3.42.0.0...3.45.1.0)

---
updated-dependencies:
- dependency-name: org.xerial:sqlite-jdbc
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-01 04:32:43 -06:00
dependabot[bot]
53c208ded5 Bump com.google.android.material:material from 1.10.0 to 1.11.0 (#1908)
Bumps [com.google.android.material:material](https://github.com/material-components/material-components-android) from 1.10.0 to 1.11.0.
- [Release notes](https://github.com/material-components/material-components-android/releases)
- [Commits](https://github.com/material-components/material-components-android/compare/1.10.0...1.11.0)

---
updated-dependencies:
- dependency-name: com.google.android.material:material
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-01 04:32:23 -06:00
dependabot[bot]
1bdc83e92f Bump org.mockito.kotlin:mockito-kotlin from 5.1.0 to 5.2.1 (#1906)
Bumps [org.mockito.kotlin:mockito-kotlin](https://github.com/mockito/mockito-kotlin) from 5.1.0 to 5.2.1.
- [Release notes](https://github.com/mockito/mockito-kotlin/releases)
- [Commits](https://github.com/mockito/mockito-kotlin/compare/5.1.0...5.2.1)

---
updated-dependencies:
- dependency-name: org.mockito.kotlin:mockito-kotlin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-01 04:32:08 -06:00
dependabot[bot]
680c1cdc76 Bump kotlinVersion from 1.9.21 to 1.9.22 (#1904)
Bumps `kotlinVersion` from 1.9.21 to 1.9.22.

Updates `org.jetbrains.kotlin.android` from 1.9.21 to 1.9.22

Updates `org.jetbrains.kotlin.kapt` from 1.9.21 to 1.9.22

Updates `org.jetbrains.kotlin.multiplatform` from 1.9.21 to 1.9.22

Updates `org.jetbrains.kotlin:kotlin-stdlib-jdk8` from 1.9.21 to 1.9.22
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.21...v1.9.22)

---
updated-dependencies:
- dependency-name: org.jetbrains.kotlin.android
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jetbrains.kotlin.kapt
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jetbrains.kotlin.multiplatform
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jetbrains.kotlin:kotlin-stdlib-jdk8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-01 04:31:50 -06:00
dependabot[bot]
80916bac50 Bump actions/upload-artifact from 3 to 4 (#1901)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-01 04:31:37 -06:00
Leon Todd
a5e3e9b3cf Unset measurable habits (#1899) 2024-01-30 19:42:53 -06:00
248ba50a8e Reformat source code 2024-01-30 19:38:32 -06:00
Andreas Gebhardt
45a82b3c2d Changed Checkmark Widget Color on implicit check
As proposed in #615, the background is now solid color on implicit check but has a stroked checkmark instead of a full one.
2024-01-30 19:25:57 -06:00
770d1293dc Merge branch 'master' into dev 2024-01-30 19:15:48 -06:00
d10538e720 Merge branch 'release/2.2.0' 2024-01-30 19:15:12 -06:00
Quentin Hibon
25cff3d9b0 Check android lint violations against current baseline 2023-08-07 23:04:46 +02:00
42 changed files with 8575 additions and 145 deletions

View File

@@ -1,11 +0,0 @@
version: 2
updates:
- package-ecosystem: "gradle"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 10
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"

View File

@@ -22,7 +22,7 @@ jobs:
- name: Upload artifacts - name: Upload artifacts
if: always() if: always()
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: build name: build
path: | path: |

View File

@@ -1,6 +1,6 @@
# Copyright Notices # Copyright Notices
### ActiveAndroid ## ActiveAndroid
<https://github.com/pardom/ActiveAndroid> <https://github.com/pardom/ActiveAndroid>
@@ -18,7 +18,7 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
### Android Open Source Project ## Android Open Source Project
<https://source.android.com/> <https://source.android.com/>
@@ -36,7 +36,7 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
### FontAwesome ## FontAwesome
<http://fontawesome.io> <http://fontawesome.io>
@@ -59,7 +59,7 @@ under the SIL OFL 1.1.
requirement for fonts to remain under this license does not apply requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives. to any document created using the fonts or their derivatives.
### Material Design Icons ## Material Design Icons
<https://github.com/google/material-design-icons> <https://github.com/google/material-design-icons>
@@ -67,7 +67,7 @@ Material design icons are the official icon set from Google that are designed
under the material design guidelines. Available under the Creative Common under the material design guidelines. Available under the Creative Common
Attribution 4.0 International License (CC-BY 4.0). Attribution 4.0 International License (CC-BY 4.0).
### Android Flow Layout ## Android Flow Layout
<https://github.com/ApmeM/android-flowlayout> <https://github.com/ApmeM/android-flowlayout>
@@ -87,7 +87,7 @@ Extended linear layout that wrap its content when there is no place in the curre
License for the specific language governing permissions and limitations License for the specific language governing permissions and limitations
under the License. under the License.
### Dagger 2 ## Dagger 2
<https://github.com/google/dagger> <https://github.com/google/dagger>
@@ -108,7 +108,7 @@ A fast dependency injector for Android and Java.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
### AutoFactory ## AutoFactory
<https://github.com/google/auto/tree/master/factory> <https://github.com/google/auto/tree/master/factory>
@@ -128,7 +128,7 @@ A source code generator for JSR-330-compatible factories.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
### Retrolambda ## Retrolambda
<https://github.com/orfjackal/retrolambda> <https://github.com/orfjackal/retrolambda>
@@ -138,7 +138,7 @@ Backport of Java 8's lambda expressions to Java 7, 6 and 5
This software is released under the Apache License 2.0. This software is released under the Apache License 2.0.
The license text is at http://www.apache.org/licenses/LICENSE-2.0 The license text is at http://www.apache.org/licenses/LICENSE-2.0
### PebbleKit SDK ## PebbleKit SDK
<https://github.com/pebble/pebble-android-sdk/> <https://github.com/pebble/pebble-android-sdk/>
@@ -147,7 +147,7 @@ Android PebbleKit SDK to talk to the Pebble via Bluetooth
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2014 - 2015 Pebble Technology Copyright (c) 2014 - 2015 Pebble Technology
### AppIntro ## AppIntro
<https://github.com/PaoloRotolo/AppIntro> <https://github.com/PaoloRotolo/AppIntro>
@@ -168,7 +168,7 @@ Make a cool intro for your Android app.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
### ButterKnife ## ButterKnife
<https://github.com/JakeWharton/butterknife> <https://github.com/JakeWharton/butterknife>
@@ -188,7 +188,7 @@ Bind Android views and callbacks to fields and methods
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
### opencsv ## opencsv
<http://opencsv.sourceforge.net/> <http://opencsv.sourceforge.net/>

View File

@@ -1,13 +1,13 @@
<h1 align="center">Loop Habit Tracker</h1> <h1 align="center">Loop Habit Tracker</h1>
<p align="center"> <p align="center">
<a href="https://github.com/iSoron/uhabits/actions?query=workflow%3A%22Build+%26+Test%22"> <a href="https://github.com/iSoron/uhabits/actions?query=workflow%3A%22Build+%26+Test%22">
<img src="https://github.com/iSoron/uhabits/workflows/Build%20&%20Test/badge.svg" /> <img alt="Build & Test" src="https://github.com/iSoron/uhabits/workflows/Build%20&%20Test/badge.svg" />
</a> </a>
<a href="https://github.com/iSoron/uhabits/releases/latest"> <a href="https://github.com/iSoron/uhabits/releases/latest">
<img src="https://img.shields.io/github/v/release/iSoron/uhabits" /> <img alt="release" src="https://img.shields.io/github/v/release/iSoron/uhabits" />
</a> </a>
<a href="https://github.com/iSoron/uhabits/discussions"> <a href="https://github.com/iSoron/uhabits/discussions">
<img src="https://img.shields.io/badge/GitHub-Discussions-%23fc4ebc" /> <img alt="GitHub" src="https://img.shields.io/badge/GitHub-Discussions-%23fc4ebc" />
</a> </a>
</p> </p>
@@ -32,32 +32,23 @@ source.
## Features ## Features
* <b>Beautiful, minimalistic and lightweight interface.</b> * **Beautiful, minimalistic and lightweight interface.** Loop has an elegant and minimalistic interface that is very easy to use, even for first-time users. Highly optimized for speed, the app works well even on older phones.
Loop has an elegant and minimalistic interface that is very easy to use, even for first-time users. Highly optimized for speed, the app works well even on older phones.
* <b>Habit score.</b> * **Habit score.** Loop has an advanced formula for calculating the strength of your habits. Every repetition makes your habit stronger and every missed day makes it weaker. A few missed days after a long streak, however, will not completely destroy your progress, unlike many other don't-break-the-chain apps.
Loop has an advanced formula for calculating the strength of your habits. Every repetition makes your habit stronger and every missed day makes it weaker. A few missed days after a long streak, however, will not completely destroy your progress, unlike many other don't-break-the-chain apps.
* <b>Flexible schedules.</b> * **Flexible schedules.** In addition to daily habits, Loop supports habits with more complex schedules, such as 3 times per week or every other day.
In addition to daily habits, Loop supports habits with more complex schedules, such as 3 times per week or every other day.
* <b>Reminders.</b> * **Reminders.** Schedule notifications to remind you of your habits. Each habit can have its own reminder, at a chosen time of the day. Easily check or dismiss your habit directly from the notification.
Schedule notifications to remind you of your habits. Each habit can have its own reminder, at a chosen time of the day. Easily check or dismiss your habit directly from the notification.
* <b>Widgets.</b> * **Widgets.** Be reminded of your habits whenever you unlock your phone. Colorful widgets allow you to track your habits directly from your home screen, without even opening the app.
Be reminded of your habits whenever you unlock your phone. Colorful widgets allow you to track your habits directly from your home screen, without even opening the app.
* <b>Take control of your data.</b> * **Take control of your data.** If you want to further analyze your data, or move it to another service, Loop allows you to export it to spreadsheets (CSV) or to a database file (SQLite). For power users, checkmarks can be added through other apps, such as Tasker.
If you want to further analyze your data, or move it to another service, Loop allows you to export it to spreadsheets (CSV) or to a database file (SQLite). For power users, checkmarks can be added through other apps, such as Tasker.
* <b>No limitations.</b> * **No limitations.** Track as many habits as you wish. Loop imposes no artificial limits on how many habits you can have. All features are available to all users. There are no in-app purchases.
Track as many habits as you wish. Loop imposes no artificial limits on how many habits you can have. All features are available to all users. There are no in-app purchases.
* <b>Completely ad-free and open source.</b> * **Completely ad-free and open source.** There are no advertisements, annoying notifications or intrusive permissions in this app, and there will never be. The app is completely open-source (GPLv3).
There are no advertisements, annoying notifications or intrusive permissions in this app, and there will never be. The app is completely open-source (GPLv3).
* <b>Works offline and respects your privacy.</b> * **Works offline and respects your privacy.** Loop doesn't require an Internet connection or online account registration. Your confidential data is never sent to anyone. Neither the developers nor any third-parties have access to it.
Loop doesn't require an Internet connection or online account registration. Your confidential data is never sent to anyone. Neither the developers nor any third-parties have access to it.
## Installing ## Installing
@@ -94,7 +85,7 @@ contribute, even if you are not a software developer.
## License ## License
<img align="right" src="https://www.gnu.org/graphics/gplv3-88x31.png"> <img align="right" alt="GPL v3" src="https://www.gnu.org/graphics/gplv3-88x31.png">
Copyright (C) 2016-2021 Álinson Santos Xavier <isoron@gmail.com> Copyright (C) 2016-2021 Álinson Santos Xavier <isoron@gmail.com>

View File

@@ -1,6 +1,6 @@
plugins { plugins {
val kotlinVersion = "1.9.21" val kotlinVersion = "2.1.10"
id("com.android.application") version "8.1.4" apply (false) id("com.android.application") version "8.8.0" apply (false)
id("org.jetbrains.kotlin.android") version kotlinVersion apply (false) id("org.jetbrains.kotlin.android") version kotlinVersion apply (false)
id("org.jetbrains.kotlin.kapt") version kotlinVersion apply (false) id("org.jetbrains.kotlin.kapt") version kotlinVersion apply (false)
id("org.jetbrains.kotlin.multiplatform") version kotlinVersion apply (false) id("org.jetbrains.kotlin.multiplatform") version kotlinVersion apply (false)
@@ -18,5 +18,6 @@ allprojects {
maven(url = "https://plugins.gradle.org/m2/") maven(url = "https://plugins.gradle.org/m2/")
maven(url = "https://oss.sonatype.org/content/repositories/snapshots/") maven(url = "https://oss.sonatype.org/content/repositories/snapshots/")
maven(url = "https://jitpack.io") maven(url = "https://jitpack.io")
maven(url = "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/kotlin-js-wrappers/") // Repository for kotlin-css-jvm old versions, now that the Gradle Plugin Portal no longer brings these in by mirroring JCenter
} }
} }

View File

@@ -64,6 +64,7 @@ fail() {
core_build() { core_build() {
log_info "Building uhabits-core..." log_info "Building uhabits-core..."
$GRADLE ktlintCheck || fail $GRADLE ktlintCheck || fail
$GRADLE lintDebug || fail
$GRADLE :uhabits-core:build || fail $GRADLE :uhabits-core:build || fail
} }

View File

@@ -9,17 +9,17 @@ This pages describes how to download and build the app from the source code. If
## Build using Android Studio ## Build using Android Studio
**Step 1: Install git** ### Step 1: Install git
The package `git` is required for downloading the source code of the app and submitting changes GitHub. Please see [the git book](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) for further instructions. If you are planning to submit pull requests in the future, it is recommended to [generate and configure your SSH keys](https://help.github.com/en/github/authenticating-to-github/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent). The package `git` is required for downloading the source code of the app and submitting changes GitHub. Please see [the git book](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) for further instructions. If you are planning to submit pull requests in the future, it is recommended to [generate and configure your SSH keys](https://help.github.com/en/github/authenticating-to-github/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent).
**Step 2: Download and install Android Studio** ### Step 2: Download and install Android Studio
Although Android Studio can be downloaded [from their official website](https://developer.android.com/studio/), a much better option is to install it through [JetBrains Toolbox](https://www.jetbrains.com/toolbox-app/). This tool, developed by the same developers of Android Studio, allows you to easily upgrade and downgrade the IDE, or switch between stable, beta and canary versions. After downloading and installing JetBrains Toolbox, simply click the install button near Android Studio to install the newest stable version of IDE. Beta and canary versions have not been tested and may not work correctly. Although Android Studio can be downloaded [from their official website](https://developer.android.com/studio/), a much better option is to install it through [JetBrains Toolbox](https://www.jetbrains.com/toolbox-app/). This tool, developed by the same developers of Android Studio, allows you to easily upgrade and downgrade the IDE, or switch between stable, beta and canary versions. After downloading and installing JetBrains Toolbox, simply click the install button near Android Studio to install the newest stable version of IDE. Beta and canary versions have not been tested and may not work correctly.
After installation, launch Android Studio. If this is the first time you launch it, you will need to go through a wizard to setup the IDE. The default options should work fine. The wizard will download all additional components necessary for development, including the emulator, so it may take a while. After installation, launch Android Studio. If this is the first time you launch it, you will need to go through a wizard to setup the IDE. The default options should work fine. The wizard will download all additional components necessary for development, including the emulator, so it may take a while.
**Step 3: Download the source code** ### Step 3: Download the source code
To create a complete copy of the source code repository, open the terminal (Linux/macOS) or Git Bash (Windows), navigate to the desired folder, then run: To create a complete copy of the source code repository, open the terminal (Linux/macOS) or Git Bash (Windows), navigate to the desired folder, then run:
```bash ```bash
@@ -27,7 +27,7 @@ git clone https://github.com/iSoron/uhabits.git
``` ```
The repository will be downloaded to the directory `uhabits`. The repository will be downloaded to the directory `uhabits`.
**Step 4: Open and run the project on Android Studio** ### Step 4: Open and run the project on Android Studio
1. Launch Android Studio and select "Open an existing Android Studio project". 1. Launch Android Studio and select "Open an existing Android Studio project".
2. When the IDE asks you for the project location, select `uhabits` and click "Ok". 2. When the IDE asks you for the project location, select `uhabits` and click "Ok".
@@ -41,7 +41,7 @@ The repository will be downloaded to the directory `uhabits`.
The following instructions were tested on **Ubuntu Linux 18.04 LTS** and may need to be modified for other operating systems. The following instructions were tested on **Ubuntu Linux 18.04 LTS** and may need to be modified for other operating systems.
**Step 1: Install basic packages** ### Step 1: Install basic packages
To build the application, some basic packages are required. The package `git` is required to download the source code, while `openjdk-8-jdk-headless` is required for compiling Java and Kotlin files. To build the application, some basic packages are required. The package `git` is required to download the source code, while `openjdk-8-jdk-headless` is required for compiling Java and Kotlin files.
@@ -53,14 +53,14 @@ sudo apt-get install -y git openjdk-8-jdk-headless
**IMPORTANT:** Newer JDK versions have not been tested and may not work correctly. **IMPORTANT:** Newer JDK versions have not been tested and may not work correctly.
**Step 2: Install Android SDK tools** ### Step 2: Install Android SDK tools
The Android SDK tools contains many necessary tools for developing and debugging Android applications. It can be obtained as part of Android Studio, but, for simple command line usage, it can also be downloaded individually. The Android SDK tools contains many necessary tools for developing and debugging Android applications. It can be obtained as part of Android Studio, but, for simple command line usage, it can also be downloaded individually.
1. Download the file `sdk-tools-linux-4333796.zip` (or a newer version) from https://developer.android.com/studio/#downloads, and extract it somewhere. In this guide, we assume that it was extracted to `/opt/android-sdk/tools`; that is, the script `/opt/android-sdk/tools/bin/sdkmanager` should exist. 1. Download the file `sdk-tools-linux-4333796.zip` (or a newer version) from <https://developer.android.com/studio/#downloads>, and extract it somewhere. In this guide, we assume that it was extracted to `/opt/android-sdk/tools`; that is, the script `/opt/android-sdk/tools/bin/sdkmanager` should exist.
2. Append the following lines to `~/.profile`, so that other tools can locate your Android SDK installation. It is necessary to restart your terminal for these changes to take effect. 2. Append the following lines to `~/.profile`, so that other tools can locate your Android SDK installation. It is necessary to restart your terminal for these changes to take effect.
``` ```bash
export PATH="$PATH:/opt/android-sdk/tools/bin" export PATH="$PATH:/opt/android-sdk/tools/bin"
export PATH="$PATH:/opt/android-sdk/platform-tools" export PATH="$PATH:/opt/android-sdk/platform-tools"
export ANDROID_HOME="/opt/android-sdk" export ANDROID_HOME="/opt/android-sdk"
@@ -71,7 +71,7 @@ export ANDROID_HOME="/opt/android-sdk"
yes | sdkmanager --licenses yes | sdkmanager --licenses
``` ```
**Step 3: Download the source code** ### Step 3: Download the source code
To create a complete copy of the source code repository, navigate to your home directory and run: To create a complete copy of the source code repository, navigate to your home directory and run:
```bash ```bash
@@ -79,13 +79,11 @@ git clone https://github.com/iSoron/uhabits.git
``` ```
The repository will be downloaded to the directory `uhabits`. The repository will be downloaded to the directory `uhabits`.
**Step 4: Compile the source code** ### Step 4: Compile the source code
1. Navigate to the directory `uhabits` 1. Navigate to the directory `uhabits`
2. Run `./gradlew assembleDebug --stacktrace` 2. Run `./gradlew assembleDebug --stacktrace`
If the compilation is successful, a debug APK will be generated somewhere inside the folder `uhabits-android/build/`. Currently, the full path is the following, but it may change in the future: If the compilation is successful, a debug APK will be generated somewhere inside the folder `uhabits-android/build/`. Currently, the full path is `./uhabits-android/build/outputs/apk/debug/uhabits-android-debug.apk`, but it may change in the future.
./uhabits-android/build/outputs/apk/debug/uhabits-android-debug.apk
The APK can be installed using the tool `adb`, which should have been automatically installed at `/opt/android-sdk/platform-tools/adb` during compilation of the project. The APK can be installed using the tool `adb`, which should have been automatically installed at `/opt/android-sdk/platform-tools/adb` during compilation of the project.

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@@ -18,8 +18,7 @@
*/ */
plugins { plugins {
id("com.github.triplet.play") version "3.8.6" id("com.android.application") version "8.8.0"
id("com.android.application") version "8.1.4"
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.kapt") id("org.jetbrains.kotlin.kapt")
id("org.jlleitschuh.gradle.ktlint") id("org.jlleitschuh.gradle.ktlint")
@@ -43,13 +42,14 @@ kotlin {
android { android {
namespace = "org.isoron.uhabits" namespace = "org.isoron.uhabits"
compileSdk = 34 compileSdk = 35
// compileSdkPreview = "VanillaIceCream"
defaultConfig { defaultConfig {
versionCode = 20200 versionCode = 20200
versionName = "2.2.0" versionName = "2.2.0"
minSdk = 28 minSdk = 28
targetSdk = 34 targetSdk = 35
applicationId = "org.isoron.uhabits" applicationId = "org.isoron.uhabits"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -91,14 +91,18 @@ android {
buildFeatures { buildFeatures {
viewBinding = true viewBinding = true
} }
lint {
abortOnError = false
}
} }
dependencies { dependencies {
val daggerVersion = "2.48.1" val daggerVersion = "2.51.1"
val kotlinVersion = "1.9.21" val kotlinVersion = "2.1.10"
val kxCoroutinesVersion = "1.7.3" val kxCoroutinesVersion = "1.10.1"
val ktorVersion = "1.6.8" val ktorVersion = "1.6.8"
val espressoVersion = "3.5.1" val espressoVersion = "3.6.1"
androidTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion") androidTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion")
androidTestImplementation("androidx.test.espresso:espresso-core:$espressoVersion") androidTestImplementation("androidx.test.espresso:espresso-core:$espressoVersion")
@@ -106,17 +110,17 @@ dependencies {
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.3") androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.3")
androidTestImplementation("io.ktor:ktor-client-mock:$ktorVersion") androidTestImplementation("io.ktor:ktor-client-mock:$ktorVersion")
androidTestImplementation("io.ktor:ktor-jackson:$ktorVersion") androidTestImplementation("io.ktor:ktor-jackson:$ktorVersion")
androidTestImplementation("androidx.annotation:annotation:1.7.0") androidTestImplementation("androidx.annotation:annotation:1.7.1")
androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0") androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
androidTestImplementation("androidx.test:rules:1.5.0") androidTestImplementation("androidx.test:rules:1.6.1")
androidTestImplementation("org.mockito.kotlin:mockito-kotlin:5.1.0") androidTestImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0")
compileOnly("javax.annotation:jsr250-api:1.0") compileOnly("javax.annotation:jsr250-api:1.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
implementation("com.github.AppIntro:AppIntro:6.3.1") implementation("com.github.AppIntro:AppIntro:6.3.1")
implementation("com.google.code.findbugs:jsr305:3.0.2") implementation("com.google.code.findbugs:jsr305:3.0.2")
implementation("com.google.dagger:dagger:$daggerVersion") implementation("com.google.dagger:dagger:$daggerVersion")
implementation("com.google.guava:guava:32.1.3-android") implementation("com.google.guava:guava:33.1.0-android")
implementation("io.ktor:ktor-client-android:$ktorVersion") implementation("io.ktor:ktor-client-android:$ktorVersion")
implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-jackson:$ktorVersion") implementation("io.ktor:ktor-client-jackson:$ktorVersion")
@@ -124,24 +128,20 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kxCoroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kxCoroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kxCoroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kxCoroutinesVersion")
implementation("androidx.appcompat:appcompat:1.6.1") implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.legacy:legacy-preference-v14:1.0.0") implementation("androidx.legacy:legacy-preference-v14:1.0.0")
implementation("androidx.legacy:legacy-support-v4:1.0.0") implementation("androidx.legacy:legacy-support-v4:1.0.0")
implementation("com.google.android.material:material:1.10.0") implementation("com.google.android.material:material:1.12.0")
implementation("com.opencsv:opencsv:5.9") implementation("com.opencsv:opencsv:5.9")
implementation("nl.dionsegijn:konfetti-xml:2.0.2")
implementation(project(":uhabits-core")) implementation(project(":uhabits-core"))
kapt("com.google.dagger:dagger-compiler:$daggerVersion") kapt("com.google.dagger:dagger-compiler:$daggerVersion")
kaptAndroidTest("com.google.dagger:dagger-compiler:$daggerVersion") kaptAndroidTest("com.google.dagger:dagger-compiler:$daggerVersion")
testImplementation("com.google.dagger:dagger:$daggerVersion") testImplementation("com.google.dagger:dagger:$daggerVersion")
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.1.0") testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0")
} }
kapt { kapt {
correctErrorTypes = true correctErrorTypes = true
} }
play {
serviceAccountCredentials.set(file("../.secret/gcp-key.json"))
track.set("alpha")
}

File diff suppressed because it is too large Load Diff

View File

@@ -132,7 +132,7 @@ object CommonSteps : BaseUserInterfaceTest() {
} }
@Throws(Exception::class) @Throws(Exception::class)
fun verifyOpensWebsite(url: String?) { fun verifyOpensWebsite(url: String) {
var browserPkg = "org.chromium.webview_shell" var browserPkg = "org.chromium.webview_shell"
if (SDK_INT <= Build.VERSION_CODES.M) { if (SDK_INT <= Build.VERSION_CODES.M) {
browserPkg = "com.android.browser" browserPkg = "com.android.browser"

View File

@@ -26,6 +26,7 @@ import org.isoron.uhabits.BuildConfig
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.databinding.AboutBinding import org.isoron.uhabits.databinding.AboutBinding
import org.isoron.uhabits.utils.applyRootViewInsets
import org.isoron.uhabits.utils.currentTheme import org.isoron.uhabits.utils.currentTheme
import org.isoron.uhabits.utils.setupToolbar import org.isoron.uhabits.utils.setupToolbar
@@ -54,5 +55,6 @@ class AboutView(
binding.tvTranslate.setOnClickListener { screen.showTranslationWebsite() } binding.tvTranslate.setOnClickListener { screen.showTranslationWebsite() }
binding.tvVersion.setOnClickListener { screen.onPressDeveloperCountdown() } binding.tvVersion.setOnClickListener { screen.onPressDeveloperCountdown() }
binding.tvVersion.text = String.format(version, BuildConfig.VERSION_NAME) binding.tvVersion.text = String.format(version, BuildConfig.VERSION_NAME)
applyRootViewInsets()
} }
} }

View File

@@ -33,17 +33,19 @@ import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
import org.isoron.uhabits.databinding.CheckmarkPopupBinding import org.isoron.uhabits.databinding.CheckmarkPopupBinding
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
import org.isoron.uhabits.utils.getCenter
import org.isoron.uhabits.utils.sres import org.isoron.uhabits.utils.sres
class CheckmarkDialog : AppCompatDialogFragment() { class CheckmarkDialog : AppCompatDialogFragment() {
var onToggle: (Int, String) -> Unit = { _, _ -> } var onToggle: (Int, String, Float, Float) -> Unit = { _, _, _, _ -> }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val appComponent = (requireActivity().application as HabitsApplication).component val appComponent = (requireActivity().application as HabitsApplication).component
val prefs = appComponent.preferences val prefs = appComponent.preferences
val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context)) val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context))
val color = requireArguments().getInt("color")
arrayOf(view.yesBtn, view.skipBtn).forEach { arrayOf(view.yesBtn, view.skipBtn).forEach {
it.setTextColor(requireArguments().getInt("color")) it.setTextColor(color)
} }
arrayOf(view.noBtn, view.unknownBtn).forEach { arrayOf(view.noBtn, view.unknownBtn).forEach {
it.setTextColor(view.root.sres.getColor(R.attr.contrast60)) it.setTextColor(view.root.sres.getColor(R.attr.contrast60))
@@ -62,7 +64,8 @@ class CheckmarkDialog : AppCompatDialogFragment() {
} }
fun onClick(v: Int) { fun onClick(v: Int) {
val notes = view.notes.text.toString().trim() val notes = view.notes.text.toString().trim()
onToggle(v, notes) val location = view.yesBtn.getCenter()
onToggle(v, notes, location.x, location.y)
requireDialog().dismiss() requireDialog().dismiss()
} }
view.yesBtn.setOnClickListener { onClick(YES_MANUAL) } view.yesBtn.setOnClickListener { onClick(YES_MANUAL) }

View File

@@ -19,6 +19,7 @@
package org.isoron.uhabits.activities.common.dialogs package org.isoron.uhabits.activities.common.dialogs
import android.content.Context import android.content.Context
import com.android.colorpicker.ColorPickerDialog.SIZE_SMALL
import org.isoron.platform.gui.toInt import org.isoron.platform.gui.toInt
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.PaletteColor
@@ -39,7 +40,7 @@ class ColorPickerDialogFactory @Inject constructor(@param:ActivityContext privat
res.getPalette(), res.getPalette(),
androidColor, androidColor,
4, 4,
com.android.colorpicker.ColorPickerDialog.SIZE_SMALL SIZE_SMALL
) )
return dialog return dialog
} }

View File

@@ -15,6 +15,7 @@ import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.databinding.CheckmarkPopupBinding import org.isoron.uhabits.databinding.CheckmarkPopupBinding
import org.isoron.uhabits.utils.InterfaceUtils import org.isoron.uhabits.utils.InterfaceUtils
import org.isoron.uhabits.utils.getCenter
import org.isoron.uhabits.utils.requestFocusWithKeyboard import org.isoron.uhabits.utils.requestFocusWithKeyboard
import org.isoron.uhabits.utils.sres import org.isoron.uhabits.utils.sres
import java.text.DecimalFormat import java.text.DecimalFormat
@@ -24,7 +25,7 @@ import java.text.ParseException
class NumberDialog : AppCompatDialogFragment() { class NumberDialog : AppCompatDialogFragment() {
var onToggle: (Double, String) -> Unit = { _, _ -> } var onToggle: (Double, String, Float, Float) -> Unit = { _, _, _, _ -> }
var onDismiss: () -> Unit = {} var onDismiss: () -> Unit = {}
private var originalNotes: String = "" private var originalNotes: String = ""
@@ -104,12 +105,17 @@ class NumberDialog : AppCompatDialogFragment() {
try { try {
val numberFormat = NumberFormat.getInstance() val numberFormat = NumberFormat.getInstance()
val valueStr = view.value.text.toString() val valueStr = view.value.text.toString()
value = numberFormat.parse(valueStr)!!.toDouble() value = if (valueStr.isNotEmpty()) {
numberFormat.parse(valueStr)!!.toDouble()
} else {
Entry.UNKNOWN.toDouble() / 1000
}
} catch (e: ParseException) { } catch (e: ParseException) {
// NOP // NOP
} }
val notes = view.notes.text.toString() val notes = view.notes.text.toString()
onToggle(value, notes) val location = view.saveBtn.getCenter()
onToggle(value, notes, location.x, location.y)
requireDialog().dismiss() requireDialog().dismiss()
} }
} }

View File

@@ -58,6 +58,7 @@ class RingView : View {
private var em = 0f private var em = 0f
private var text: String? private var text: String?
private var textSize: Float private var textSize: Float
private var isStrokedTextEnabled: Boolean = false
private var enableFontAwesome = false private var enableFontAwesome = false
private var internalDrawingCache: Bitmap? = null private var internalDrawingCache: Bitmap? = null
private var cacheCanvas: Canvas? = null private var cacheCanvas: Canvas? = null
@@ -131,6 +132,10 @@ class RingView : View {
invalidate() invalidate()
} }
fun setIsStrokedTextEnabled(isStroked: Boolean) {
this.isStrokedTextEnabled = isStroked
}
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
super.onDraw(canvas) super.onDraw(canvas)
val activeCanvas: Canvas? val activeCanvas: Canvas?
@@ -159,6 +164,12 @@ class RingView : View {
pRing!!.xfermode = null pRing!!.xfermode = null
pRing!!.color = color pRing!!.color = color
pRing!!.textSize = textSize pRing!!.textSize = textSize
if (isStrokedTextEnabled) {
pRing!!.style = Paint.Style.STROKE
pRing!!.strokeWidth = textSize / 15f
}
if (enableFontAwesome) pRing!!.typeface = getFontAwesome(context) if (enableFontAwesome) pRing!!.typeface = getFontAwesome(context)
activeCanvas.drawText( activeCanvas.drawText(
text!!, text!!,

View File

@@ -22,7 +22,6 @@ package org.isoron.uhabits.activities.habits.edit
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.text.Html import android.text.Html
import android.text.Spanned import android.text.Spanned
@@ -53,7 +52,8 @@ import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Reminder import org.isoron.uhabits.core.models.Reminder
import org.isoron.uhabits.core.models.WeekdayList import org.isoron.uhabits.core.models.WeekdayList
import org.isoron.uhabits.databinding.ActivityEditHabitBinding import org.isoron.uhabits.databinding.ActivityEditHabitBinding
import org.isoron.uhabits.utils.ColorUtils import org.isoron.uhabits.utils.applyRootViewInsets
import org.isoron.uhabits.utils.applyToolbarInsets
import org.isoron.uhabits.utils.dismissCurrentAndShow import org.isoron.uhabits.utils.dismissCurrentAndShow
import org.isoron.uhabits.utils.formatTime import org.isoron.uhabits.utils.formatTime
import org.isoron.uhabits.utils.toFormattedString import org.isoron.uhabits.utils.toFormattedString
@@ -94,6 +94,8 @@ class EditHabitActivity : AppCompatActivity() {
themeSwitcher.apply() themeSwitcher.apply()
binding = ActivityEditHabitBinding.inflate(layoutInflater) binding = ActivityEditHabitBinding.inflate(layoutInflater)
binding.root.applyRootViewInsets()
binding.toolbar.applyToolbarInsets()
setContentView(binding.root) setContentView(binding.root)
if (intent.hasExtra("habitId")) { if (intent.hasExtra("habitId")) {
@@ -352,8 +354,7 @@ class EditHabitActivity : AppCompatActivity() {
androidColor = themeSwitcher.currentTheme.color(color).toInt() androidColor = themeSwitcher.currentTheme.color(color).toInt()
binding.colorButton.backgroundTintList = ColorStateList.valueOf(androidColor) binding.colorButton.backgroundTintList = ColorStateList.valueOf(androidColor)
if (!themeSwitcher.isNightMode) { if (!themeSwitcher.isNightMode) {
val darkerAndroidColor = ColorUtils.mixColors(Color.BLACK, androidColor, 0.15f) window.statusBarColor = androidColor
window.statusBarColor = darkerAndroidColor
binding.toolbar.setBackgroundColor(androidColor) binding.toolbar.setBackgroundColor(androidColor)
} }
} }

View File

@@ -45,6 +45,7 @@ import org.isoron.uhabits.inject.ActivityContextModule
import org.isoron.uhabits.inject.DaggerHabitsActivityComponent import org.isoron.uhabits.inject.DaggerHabitsActivityComponent
import org.isoron.uhabits.inject.HabitsActivityComponent import org.isoron.uhabits.inject.HabitsActivityComponent
import org.isoron.uhabits.inject.HabitsApplicationComponent import org.isoron.uhabits.inject.HabitsApplicationComponent
import org.isoron.uhabits.utils.applyRootViewInsets
import org.isoron.uhabits.utils.dismissCurrentDialog import org.isoron.uhabits.utils.dismissCurrentDialog
import org.isoron.uhabits.utils.restartWithFade import org.isoron.uhabits.utils.restartWithFade
@@ -100,6 +101,7 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
menu = component.listHabitsMenu menu = component.listHabitsMenu
Thread.setDefaultUncaughtExceptionHandler(BaseExceptionHandler(this)) Thread.setDefaultUncaughtExceptionHandler(BaseExceptionHandler(this))
component.listHabitsBehavior.onStartup() component.listHabitsBehavior.onStartup()
rootView.applyRootViewInsets()
setContentView(rootView) setContentView(rootView)
} }

View File

@@ -23,6 +23,7 @@ import android.content.Context
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.RelativeLayout import android.widget.RelativeLayout
import nl.dionsegijn.konfetti.xml.KonfettiView
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.views.ScrollableChart import org.isoron.uhabits.activities.common.views.ScrollableChart
import org.isoron.uhabits.activities.common.views.TaskProgressBar import org.isoron.uhabits.activities.common.views.TaskProgressBar
@@ -69,6 +70,9 @@ class ListHabitsRootView @Inject constructor(
val listView: HabitCardListView = habitCardListViewFactory.create() val listView: HabitCardListView = habitCardListViewFactory.create()
val llEmpty = EmptyListView(context) val llEmpty = EmptyListView(context)
val tbar = buildToolbar() val tbar = buildToolbar()
val konfettiView = KonfettiView(context).apply {
translationZ = 10f
}
val progressBar = TaskProgressBar(context, runner) val progressBar = TaskProgressBar(context, runner)
val hintView: HintView val hintView: HintView
val header = HeaderView(context, preferences, midnightTimer) val header = HeaderView(context, preferences, midnightTimer)
@@ -80,6 +84,7 @@ class ListHabitsRootView @Inject constructor(
val rootView = RelativeLayout(context).apply { val rootView = RelativeLayout(context).apply {
background = sres.getDrawable(R.attr.windowBackgroundColor) background = sres.getDrawable(R.attr.windowBackgroundColor)
addAtTop(konfettiView)
addAtTop(tbar) addAtTop(tbar)
addBelow(header, tbar) addBelow(header, tbar)
addBelow(listView, header, height = MATCH_PARENT) addBelow(listView, header, height = MATCH_PARENT)

View File

@@ -25,6 +25,9 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import dagger.Lazy import dagger.Lazy
import nl.dionsegijn.konfetti.core.Party
import nl.dionsegijn.konfetti.core.Position
import nl.dionsegijn.konfetti.core.emitter.Emitter
import org.isoron.platform.gui.toInt import org.isoron.platform.gui.toInt
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog
@@ -63,6 +66,7 @@ import org.isoron.uhabits.intents.IntentFactory
import org.isoron.uhabits.tasks.ExportDBTaskFactory import org.isoron.uhabits.tasks.ExportDBTaskFactory
import org.isoron.uhabits.tasks.ImportDataTask import org.isoron.uhabits.tasks.ImportDataTask
import org.isoron.uhabits.tasks.ImportDataTaskFactory import org.isoron.uhabits.tasks.ImportDataTaskFactory
import org.isoron.uhabits.utils.ColorUtils
import org.isoron.uhabits.utils.copyTo import org.isoron.uhabits.utils.copyTo
import org.isoron.uhabits.utils.currentTheme import org.isoron.uhabits.utils.currentTheme
import org.isoron.uhabits.utils.dismissCurrentAndShow import org.isoron.uhabits.utils.dismissCurrentAndShow
@@ -72,6 +76,7 @@ import org.isoron.uhabits.utils.showSendEmailScreen
import org.isoron.uhabits.utils.showSendFileScreen import org.isoron.uhabits.utils.showSendFileScreen
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
const val RESULT_IMPORT_DATA = 101 const val RESULT_IMPORT_DATA = 101
@@ -218,6 +223,28 @@ class ListHabitsScreen
activity.showSendFileScreen(filename) activity.showSendFileScreen(filename)
} }
override fun showConfetti(color: PaletteColor, x: Float, y: Float) {
val baseColor = themeSwitcher.currentTheme!!.color(color).toInt()
rootView.get().konfettiView.start(
Party(
speed = 0f,
maxSpeed = 16f,
damping = 0.9f,
spread = 360,
angle = 0,
colors = listOf(
ColorUtils.changeHue(baseColor, 180f),
ColorUtils.changeHue(baseColor, 20f),
ColorUtils.changeHue(baseColor, -20f),
baseColor
),
position = Position.Absolute(x, y),
emitter = Emitter(duration = 25, TimeUnit.MILLISECONDS).max(25),
timeToLive = 0
)
)
}
override fun showSettingsScreen() { override fun showSettingsScreen() {
val intent = intentFactory.startSettingsActivity(activity) val intent = intentFactory.startSettingsActivity(activity)
activity.startActivityForResult(intent, REQUEST_SETTINGS) activity.startActivityForResult(intent, REQUEST_SETTINGS)
@@ -240,7 +267,7 @@ class ListHabitsScreen
putDouble("value", value) putDouble("value", value)
putString("notes", notes) putString("notes", notes)
} }
dialog.onToggle = { v, n -> callback.onNumberPicked(v, n) } dialog.onToggle = { v, n, x, y -> callback.onNumberPicked(v, n, x, y) }
dialog.dismissCurrentAndShow(fm, "numberDialog") dialog.dismissCurrentAndShow(fm, "numberDialog")
} }
@@ -258,7 +285,7 @@ class ListHabitsScreen
putInt("value", selectedValue) putInt("value", selectedValue)
putString("notes", notes) putString("notes", notes)
} }
dialog.onToggle = { v, n -> callback.onNotesSaved(v, n) } dialog.onToggle = { v, n, x, y -> callback.onNotesSaved(v, n, x, y) }
dialog.dismissCurrentAndShow(fm, "checkmarkDialog") dialog.dismissCurrentAndShow(fm, "checkmarkDialog")
} }

View File

@@ -20,6 +20,7 @@
package org.isoron.uhabits.activities.habits.list.views package org.isoron.uhabits.activities.habits.list.views
import android.content.Context import android.content.Context
import android.graphics.PointF
import android.graphics.text.LineBreaker.BREAK_STRATEGY_BALANCED import android.graphics.text.LineBreaker.BREAK_STRATEGY_BALANCED
import android.os.Build import android.os.Build
import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION.SDK_INT
@@ -154,7 +155,17 @@ class HabitCardView(
checkmarkPanel = checkmarkPanelFactory.create().apply { checkmarkPanel = checkmarkPanelFactory.create().apply {
onToggle = { timestamp, value, notes -> onToggle = { timestamp, value, notes ->
triggerRipple(timestamp) triggerRipple(timestamp)
habit?.let { behavior.onToggle(it, timestamp, value, notes) } val location = getAbsoluteButtonLocation(timestamp)
habit?.let {
behavior.onToggle(
it,
timestamp,
value,
notes,
location.x,
location.y
)
}
} }
onEdit = { timestamp -> onEdit = { timestamp ->
triggerRipple(timestamp) triggerRipple(timestamp)
@@ -206,12 +217,27 @@ class HabitCardView(
} }
fun triggerRipple(timestamp: Timestamp) { fun triggerRipple(timestamp: Timestamp) {
val location = getRelativeButtonLocation(timestamp)
triggerRipple(location.x, location.y)
}
private fun getRelativeButtonLocation(timestamp: Timestamp): PointF {
val today = DateUtils.getTodayWithOffset() val today = DateUtils.getTodayWithOffset()
val offset = timestamp.daysUntil(today) - dataOffset val offset = timestamp.daysUntil(today) - dataOffset
val button = checkmarkPanel.buttons[offset] val button = checkmarkPanel.buttons[offset]
val y = button.height / 2.0f val y = button.height / 2.0f
val x = checkmarkPanel.x + button.x + (button.width / 2).toFloat() val x = checkmarkPanel.x + button.x + (button.width / 2).toFloat()
triggerRipple(x, y) return PointF(x, y)
}
private fun getAbsoluteButtonLocation(timestamp: Timestamp): PointF {
val containerLocation = IntArray(2)
this.getLocationOnScreen(containerLocation)
val relButtonLocation = getRelativeButtonLocation(timestamp)
return PointF(
containerLocation[0].toFloat() + relButtonLocation.x,
containerLocation[1].toFloat() - relButtonLocation.y
)
} }
override fun onAttachedToWindow() { override fun onAttachedToWindow() {

View File

@@ -141,7 +141,7 @@ class HeaderView(
val y1 = rect.centerY() - 0.25 * em val y1 = rect.centerY() - 0.25 * em
val y2 = rect.centerY() + 1.25 * em val y2 = rect.centerY() + 1.25 * em
val lines = DateUtils.formatHeaderDate(day).toUpperCase().split("\n") val lines = DateUtils.formatHeaderDate(day).uppercase().split("\n")
canvas.drawText(lines[0], rect.centerX(), y1.toFloat(), paint) canvas.drawText(lines[0], rect.centerX(), y1.toFloat(), paint)
canvas.drawText(lines[1], rect.centerX(), y2.toFloat(), paint) canvas.drawText(lines[1], rect.centerX(), y2.toFloat(), paint)
day.add(GregorianCalendar.DAY_OF_MONTH, -1) day.add(GregorianCalendar.DAY_OF_MONTH, -1)

View File

@@ -49,6 +49,7 @@ import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitMenuPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitPresenter import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitPresenter
import org.isoron.uhabits.core.ui.views.OnDateClickedListener import org.isoron.uhabits.core.ui.views.OnDateClickedListener
import org.isoron.uhabits.intents.IntentFactory import org.isoron.uhabits.intents.IntentFactory
import org.isoron.uhabits.utils.applyRootViewInsets
import org.isoron.uhabits.utils.currentTheme import org.isoron.uhabits.utils.currentTheme
import org.isoron.uhabits.utils.dismissCurrentAndShow import org.isoron.uhabits.utils.dismissCurrentAndShow
import org.isoron.uhabits.utils.dismissCurrentDialog import org.isoron.uhabits.utils.dismissCurrentDialog
@@ -109,6 +110,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
) )
view.setListener(presenter) view.setListener(presenter)
view.applyRootViewInsets()
setContentView(view) setContentView(view)
} }
@@ -179,7 +181,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
putDouble("value", value) putDouble("value", value)
putString("notes", notes) putString("notes", notes)
} }
dialog.onToggle = { v, n -> callback.onNumberPicked(v, n) } dialog.onToggle = { v, n, x, y -> callback.onNumberPicked(v, n, x, y) }
dialog.dismissCurrentAndShow(supportFragmentManager, "numberDialog") dialog.dismissCurrentAndShow(supportFragmentManager, "numberDialog")
} }
@@ -196,7 +198,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
putInt("value", selectedValue) putInt("value", selectedValue)
putString("notes", notes) putString("notes", notes)
} }
dialog.onToggle = { v, n -> callback.onNotesSaved(v, n) } dialog.onToggle = { v, n, x, y -> callback.onNotesSaved(v, n, x, y) }
dialog.dismissCurrentAndShow(supportFragmentManager, "checkmarkDialog") dialog.dismissCurrentAndShow(supportFragmentManager, "checkmarkDialog")
} }

View File

@@ -25,12 +25,14 @@ import android.widget.FrameLayout
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitPresenter import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitState import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitState
import org.isoron.uhabits.databinding.ShowHabitBinding import org.isoron.uhabits.databinding.ShowHabitBinding
import org.isoron.uhabits.utils.applyToolbarInsets
import org.isoron.uhabits.utils.setupToolbar import org.isoron.uhabits.utils.setupToolbar
class ShowHabitView(context: Context) : FrameLayout(context) { class ShowHabitView(context: Context) : FrameLayout(context) {
private val binding = ShowHabitBinding.inflate(LayoutInflater.from(context)) private val binding = ShowHabitBinding.inflate(LayoutInflater.from(context))
init { init {
binding.toolbar.applyToolbarInsets()
addView(binding.root) addView(binding.root)
} }
@@ -52,7 +54,6 @@ class ShowHabitView(context: Context) : FrameLayout(context) {
binding.barCard.setState(data.bar) binding.barCard.setState(data.bar)
if (data.isNumerical) { if (data.isNumerical) {
binding.overviewCard.visibility = GONE binding.overviewCard.visibility = GONE
binding.streakCard.visibility = GONE
} else { } else {
binding.targetCard.visibility = GONE binding.targetCard.visibility = GONE
} }

View File

@@ -26,6 +26,7 @@ import org.isoron.uhabits.R
import org.isoron.uhabits.activities.AndroidThemeSwitcher import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.databinding.SettingsActivityBinding import org.isoron.uhabits.databinding.SettingsActivityBinding
import org.isoron.uhabits.utils.applyRootViewInsets
import org.isoron.uhabits.utils.setupToolbar import org.isoron.uhabits.utils.setupToolbar
class SettingsActivity : AppCompatActivity() { class SettingsActivity : AppCompatActivity() {
@@ -42,6 +43,7 @@ class SettingsActivity : AppCompatActivity() {
color = PaletteColor(11), color = PaletteColor(11),
theme = themeSwitcher.currentTheme theme = themeSwitcher.currentTheme
) )
binding.root.applyRootViewInsets()
setContentView(binding.root) setContentView(binding.root)
} }
} }

View File

@@ -24,6 +24,7 @@ import androidx.appcompat.app.AppCompatActivity
import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.activities.AndroidThemeSwitcher import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.core.models.HabitMatcher import org.isoron.uhabits.core.models.HabitMatcher
import org.isoron.uhabits.utils.applyRootViewInsets
class EditSettingActivity : AppCompatActivity() { class EditSettingActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -45,6 +46,7 @@ class EditSettingActivity : AppCompatActivity() {
onSave = controller::onSave, onSave = controller::onSave,
args = args args = args
) )
view.applyRootViewInsets()
setContentView(view) setContentView(view)
} }
} }

View File

@@ -36,6 +36,13 @@ object ColorUtils {
return a or r or g or b return a or r or g or b
} }
fun changeHue(color: Int, delta: Float): Int {
val hsv = FloatArray(3)
Color.colorToHSV(color, hsv)
hsv[0] = (hsv[0] + delta).mod(360f)
return Color.HSVToColor(hsv)
}
@JvmStatic @JvmStatic
fun setAlpha(color: Int, newAlpha: Float): Int { fun setAlpha(color: Int, newAlpha: Float): Int {
val intAlpha = (newAlpha * 255).toInt() val intAlpha = (newAlpha * 255).toInt()

View File

@@ -26,6 +26,7 @@ import android.content.Intent
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.graphics.Paint import android.graphics.Paint
import android.graphics.PointF
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Handler import android.os.Handler
import android.os.SystemClock import android.os.SystemClock
@@ -45,6 +46,8 @@ import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.isoron.platform.gui.toInt import org.isoron.platform.gui.toInt
import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.HabitsApplication
@@ -135,7 +138,11 @@ fun Activity.startActivitySafely(intent: Intent) {
} }
} }
fun Activity.showSendEmailScreen(@StringRes toId: Int, @StringRes subjectId: Int, content: String?) { fun Activity.showSendEmailScreen(
@StringRes toId: Int,
@StringRes subjectId: Int,
content: String?
) {
val to = this.getString(toId) val to = this.getString(toId)
val subject = this.getString(subjectId) val subject = this.getString(subjectId)
this.startActivity( this.startActivity(
@@ -175,10 +182,10 @@ fun View.setupToolbar(
} else { } else {
theme.color(color).toInt() theme.color(color).toInt()
} }
val darkerColor = ColorUtils.mixColors(toolbarColor, Color.BLACK, 0.75f)
toolbar.background = ColorDrawable(toolbarColor) toolbar.background = ColorDrawable(toolbarColor)
toolbar.applyToolbarInsets()
val activity = context as AppCompatActivity val activity = context as AppCompatActivity
activity.window.statusBarColor = darkerColor activity.window.statusBarColor = toolbarColor
activity.setSupportActionBar(toolbar) activity.setSupportActionBar(toolbar)
activity.supportActionBar?.setDisplayHomeAsUpEnabled(displayHomeAsUpEnabled) activity.supportActionBar?.setDisplayHomeAsUpEnabled(displayHomeAsUpEnabled)
} }
@@ -232,3 +239,33 @@ fun View.requestFocusWithKeyboard() {
dispatchTouchEvent(MotionEvent.obtain(time, time, MotionEvent.ACTION_UP, 0f, 0f, 0)) dispatchTouchEvent(MotionEvent.obtain(time, time, MotionEvent.ACTION_UP, 0f, 0f, 0))
}, 250) }, 250)
} }
fun View.getCenter(): PointF {
val viewLocation = IntArray(2)
this.getLocationOnScreen(viewLocation)
viewLocation[0] += this.width / 2
viewLocation[1] -= this.height / 2
return PointF(viewLocation[0].toFloat(), viewLocation[1].toFloat())
}
fun View.applyRootViewInsets() {
ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
val systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
val displayCutoutInsets = insets.getInsets(WindowInsetsCompat.Type.displayCutout())
val left = maxOf(systemBarsInsets.left, displayCutoutInsets.left)
val right = maxOf(systemBarsInsets.right, displayCutoutInsets.right)
view.setPadding(left, 0, right, 0)
view.background = ColorDrawable(Color.BLACK)
insets
}
}
fun View.applyToolbarInsets() {
ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
val systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
val displayCutoutInsets = insets.getInsets(WindowInsetsCompat.Type.displayCutout())
val top = maxOf(systemBarsInsets.top, displayCutoutInsets.top)
view.setPadding(0, top, 0, 0)
insets
}
}

View File

@@ -68,13 +68,13 @@ class CheckmarkWidgetView : HabitWidgetView {
val fgColor: Int val fgColor: Int
setShadowAlpha(0x4f) setShadowAlpha(0x4f)
when (entryState) { when (entryState) {
YES_MANUAL, SKIP -> { YES_MANUAL, SKIP, YES_AUTO -> {
bgColor = activeColor bgColor = activeColor
fgColor = res.getColor(R.attr.contrast0) fgColor = res.getColor(R.attr.contrast0)
backgroundPaint!!.color = bgColor backgroundPaint!!.color = bgColor
frame!!.setBackgroundDrawable(background) frame!!.setBackgroundDrawable(background)
} }
YES_AUTO, NO, UNKNOWN -> { NO, UNKNOWN -> {
bgColor = res.getColor(R.attr.cardBgColor) bgColor = res.getColor(R.attr.cardBgColor)
fgColor = res.getColor(R.attr.contrast60) fgColor = res.getColor(R.attr.contrast60)
} }
@@ -87,12 +87,23 @@ class CheckmarkWidgetView : HabitWidgetView {
ring.setColor(fgColor) ring.setColor(fgColor)
ring.setBackgroundColor(bgColor) ring.setBackgroundColor(bgColor)
ring.setText(text) ring.setText(text)
ring.setIsStrokedTextEnabled(strokedTextEnabled)
label.text = name label.text = name
label.setTextColor(fgColor) label.setTextColor(fgColor)
requestLayout() requestLayout()
postInvalidate() postInvalidate()
} }
private val strokedTextEnabled: Boolean
get() = if (isNumerical) {
false
} else {
when (entryState) {
YES_AUTO -> true
else -> false
}
}
private val text: String private val text: String
get() = if (isNumerical) { get() = if (isNumerical) {
(max(0, entryValue) / 1000.0).toShortString() (max(0, entryValue) / 1000.0).toShortString()

View File

@@ -18,7 +18,8 @@
~ with this program. If not, see <http://www.gnu.org/licenses/>. ~ with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -61,8 +62,10 @@
<ScrollView <ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="0dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"> android:layout_weight="1"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:background="?attr/contrast0">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -265,4 +268,4 @@
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </LinearLayout>

View File

@@ -58,7 +58,10 @@
<LinearLayout <LinearLayout
android:id="@+id/formPanel" android:id="@+id/formPanel"
style="@style/dialogFormPanel"> style="@style/dialogFormPanel"
android:layout_height="0dp"
android:layout_weight="1"
android:background="?attr/contrast0">
<FrameLayout style="@style/FormOuterBox"> <FrameLayout style="@style/FormOuterBox">
<LinearLayout style="@style/FormInnerBox"> <LinearLayout style="@style/FormInnerBox">

View File

@@ -226,6 +226,7 @@
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>
<item name="android:paddingTop">6dp</item> <item name="android:paddingTop">6dp</item>
<item name="android:paddingBottom">6dp</item> <item name="android:paddingBottom">6dp</item>
<item name="android:textAlignment">viewStart</item>
</style> </style>
<style name="About.Item.Language"> <style name="About.Item.Language">
@@ -233,6 +234,7 @@
<item name="android:textSize">@dimen/smallTextSize</item> <item name="android:textSize">@dimen/smallTextSize</item>
<item name="android:gravity">center</item> <item name="android:gravity">center</item>
<item name="android:background">?attr/contrast20</item> <item name="android:background">?attr/contrast20</item>
<item name="android:textAlignment">viewStart</item>
</style> </style>
<style name="About.Item.Clickable"> <style name="About.Item.Clickable">
@@ -243,7 +245,7 @@
<style name="Toolbar"> <style name="Toolbar">
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">match_parent</item>
<item name="android:layout_height">?actionBarSize</item> <item name="android:layout_height">wrap_content</item>
<item name="android:background">?colorPrimary</item> <item name="android:background">?colorPrimary</item>
<item name="android:theme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item> <item name="android:theme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item>
</style> </style>

View File

@@ -44,10 +44,10 @@ kotlin {
val jvmMain by getting { val jvmMain by getting {
dependencies { dependencies {
implementation(kotlin("stdlib-jdk8")) implementation(kotlin("stdlib-jdk8"))
compileOnly("com.google.dagger:dagger:2.48.1") compileOnly("com.google.dagger:dagger:2.51.1")
implementation("com.google.guava:guava:32.1.3-android") implementation("com.google.guava:guava:33.1.0-android")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.10.1")
implementation("androidx.annotation:annotation:1.7.0") implementation("androidx.annotation:annotation:1.7.1")
implementation("com.google.code.findbugs:jsr305:3.0.2") implementation("com.google.code.findbugs:jsr305:3.0.2")
implementation("com.opencsv:opencsv:5.9") implementation("com.opencsv:opencsv:5.9")
implementation("commons-codec:commons-codec:1.16.0") implementation("commons-codec:commons-codec:1.16.0")
@@ -59,10 +59,10 @@ kotlin {
dependencies { dependencies {
implementation(kotlin("test")) implementation(kotlin("test"))
implementation(kotlin("test-junit")) implementation(kotlin("test-junit"))
implementation("org.xerial:sqlite-jdbc:3.42.0.0") implementation("org.xerial:sqlite-jdbc:3.45.1.0")
implementation("org.hamcrest:hamcrest:2.2") implementation("org.hamcrest:hamcrest:2.2")
implementation("org.apache.commons:commons-io:1.3.2") implementation("org.apache.commons:commons-io:1.3.2")
implementation("org.mockito.kotlin:mockito-kotlin:5.1.0") implementation("org.mockito.kotlin:mockito-kotlin:5.4.0")
implementation("org.junit.jupiter:junit-jupiter:5.10.1") implementation("org.junit.jupiter:junit-jupiter:5.10.1")
} }
} }

View File

@@ -61,7 +61,7 @@ data class Habit(
return if (isNumerical) { return if (isNumerical) {
when (targetType) { when (targetType) {
NumericalHabitType.AT_LEAST -> value / 1000.0 >= targetValue NumericalHabitType.AT_LEAST -> value / 1000.0 >= targetValue
NumericalHabitType.AT_MOST -> value / 1000.0 <= targetValue NumericalHabitType.AT_MOST -> false
} }
} else { } else {
value != Entry.NO && value != Entry.UNKNOWN value != Entry.NO && value != Entry.UNKNOWN
@@ -100,7 +100,10 @@ data class Habit(
streaks.recompute( streaks.recompute(
computedEntries, computedEntries,
from, from,
to to,
isNumerical,
targetValue,
targetType
) )
} }

View File

@@ -37,12 +37,25 @@ class StreakList {
fun recompute( fun recompute(
computedEntries: EntryList, computedEntries: EntryList,
from: Timestamp, from: Timestamp,
to: Timestamp to: Timestamp,
isNumerical: Boolean,
targetValue: Double,
targetType: NumericalHabitType
) { ) {
list.clear() list.clear()
val timestamps = computedEntries val timestamps = computedEntries
.getByInterval(from, to) .getByInterval(from, to)
.filter { it.value > 0 } .filter {
val value = it.value
if (isNumerical) {
when (targetType) {
NumericalHabitType.AT_LEAST -> value / 1000.0 >= targetValue
NumericalHabitType.AT_MOST -> value != Entry.UNKNOWN && value / 1000.0 <= targetValue
}
} else {
value > 0
}
}
.map { it.timestamp } .map { it.timestamp }
.toTypedArray() .toTypedArray()

View File

@@ -24,6 +24,7 @@ import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.commands.CreateRepetitionCommand import org.isoron.uhabits.core.commands.CreateRepetitionCommand
import org.isoron.uhabits.core.commands.DeleteHabitsCommand import org.isoron.uhabits.core.commands.DeleteHabitsCommand
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.tasks.Task import org.isoron.uhabits.core.tasks.Task
@@ -120,7 +121,7 @@ class NotificationTray @Inject constructor(
override fun onPostExecute() { override fun onPostExecute() {
systemTray.log("Showing notification for habit=" + habit.id) systemTray.log("Showing notification for habit=" + habit.id)
if (isCompleted) { if (isCompleted && habit.targetType != NumericalHabitType.AT_MOST) {
systemTray.log( systemTray.log(
String.format( String.format(
Locale.US, Locale.US,

View File

@@ -20,9 +20,12 @@ package org.isoron.uhabits.core.ui.screens.habits.list
import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.commands.CreateRepetitionCommand import org.isoron.uhabits.core.commands.CreateRepetitionCommand
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.HabitType import org.isoron.uhabits.core.models.HabitType
import org.isoron.uhabits.core.models.NumericalHabitType.AT_LEAST
import org.isoron.uhabits.core.models.NumericalHabitType.AT_MOST
import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
@@ -52,8 +55,16 @@ open class ListHabitsBehavior @Inject constructor(
val entry = habit.computedEntries.get(timestamp!!) val entry = habit.computedEntries.get(timestamp!!)
if (habit.type == HabitType.NUMERICAL) { if (habit.type == HabitType.NUMERICAL) {
val oldValue = entry.value.toDouble() / 1000 val oldValue = entry.value.toDouble() / 1000
screen.showNumberPopup(oldValue, entry.notes) { newValue: Double, newNotes: String -> screen.showNumberPopup(oldValue, entry.notes) { newValue: Double, newNotes: String, x: Float, y: Float ->
val value = (newValue * 1000).roundToInt() val value = (newValue * 1000).roundToInt()
if (newValue != oldValue) {
if (
(habit.targetType == AT_LEAST && newValue >= habit.targetValue) ||
(habit.targetType == AT_MOST && newValue <= habit.targetValue)
) {
screen.showConfetti(habit.color, x, y)
}
}
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value, newNotes)) commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value, newNotes))
} }
} else { } else {
@@ -61,7 +72,8 @@ open class ListHabitsBehavior @Inject constructor(
entry.value, entry.value,
entry.notes, entry.notes,
habit.color habit.color
) { newValue, newNotes -> ) { newValue: Int, newNotes: String, x: Float, y: Float ->
if (newValue != entry.value && newValue == YES_MANUAL) screen.showConfetti(habit.color, x, y)
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, newValue, newNotes)) commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, newValue, newNotes))
} }
} }
@@ -117,10 +129,11 @@ open class ListHabitsBehavior @Inject constructor(
if (prefs.isFirstRun) onFirstRun() if (prefs.isFirstRun) onFirstRun()
} }
fun onToggle(habit: Habit, timestamp: Timestamp, value: Int, notes: String) { fun onToggle(habit: Habit, timestamp: Timestamp, value: Int, notes: String, x: Float, y: Float) {
commandRunner.run( commandRunner.run(
CreateRepetitionCommand(habitList, habit, timestamp, value, notes) CreateRepetitionCommand(habitList, habit, timestamp, value, notes)
) )
if (value == YES_MANUAL) screen.showConfetti(habit.color, x, y)
} }
enum class Message { enum class Message {
@@ -144,12 +157,22 @@ open class ListHabitsBehavior @Inject constructor(
} }
fun interface NumberPickerCallback { fun interface NumberPickerCallback {
fun onNumberPicked(newValue: Double, notes: String) fun onNumberPicked(
newValue: Double,
notes: String,
x: Float,
y: Float
)
fun onNumberPickerDismissed() {} fun onNumberPickerDismissed() {}
} }
fun interface CheckMarkDialogCallback { fun interface CheckMarkDialogCallback {
fun onNotesSaved(value: Int, notes: String) fun onNotesSaved(
value: Int,
notes: String,
x: Float,
y: Float
)
fun onNotesDismissed() {} fun onNotesDismissed() {}
} }
@@ -170,5 +193,6 @@ open class ListHabitsBehavior @Inject constructor(
) )
fun showSendBugReportToDeveloperScreen(log: String) fun showSendBugReportToDeveloperScreen(log: String)
fun showSendFileScreen(filename: String) fun showSendFileScreen(filename: String)
fun showConfetti(color: PaletteColor, x: Float, y: Float)
} }
} }

View File

@@ -98,7 +98,7 @@ class HistoryCardPresenter(
entry.value, entry.value,
entry.notes, entry.notes,
habit.color habit.color
) { newValue, newNotes -> ) { newValue, newNotes, _: Float, _: Float ->
commandRunner.run( commandRunner.run(
CreateRepetitionCommand( CreateRepetitionCommand(
habitList, habitList,
@@ -135,7 +135,7 @@ class HistoryCardPresenter(
screen.showNumberPopup( screen.showNumberPopup(
value = oldValue / 1000.0, value = oldValue / 1000.0,
notes = entry.notes notes = entry.notes
) { newValue: Double, newNotes: String -> ) { newValue: Double, newNotes: String, _: Float, _: Float ->
val thousands = (newValue * 1000).roundToInt() val thousands = (newValue * 1000).roundToInt()
commandRunner.run( commandRunner.run(
CreateRepetitionCommand( CreateRepetitionCommand(

View File

@@ -110,10 +110,10 @@ class HabitTest : BaseUnitTest() {
assertFalse(h.isCompletedToday()) assertFalse(h.isCompletedToday())
h.originalEntries.add(Entry(getToday(), 100000)) h.originalEntries.add(Entry(getToday(), 100000))
h.recompute() h.recompute()
assertTrue(h.isCompletedToday()) assertFalse(h.isCompletedToday())
h.originalEntries.add(Entry(getToday(), 50000)) h.originalEntries.add(Entry(getToday(), 50000))
h.recompute() h.recompute()
assertTrue(h.isCompletedToday()) assertFalse(h.isCompletedToday())
} }
@Test @Test

View File

@@ -84,7 +84,7 @@ class ListHabitsBehaviorTest : BaseUnitTest() {
eq(""), eq(""),
picker.capture() picker.capture()
) )
picker.lastValue.onNumberPicked(100.0, "") picker.lastValue.onNumberPicked(100.0, "", 0f, 0f)
val today = getTodayWithOffset() val today = getTodayWithOffset()
assertThat(habit2.computedEntries.get(today).value, equalTo(100000)) assertThat(habit2.computedEntries.get(today).value, equalTo(100000))
} }
@@ -168,7 +168,9 @@ class ListHabitsBehaviorTest : BaseUnitTest() {
habit = habit1, habit = habit1,
timestamp = getToday(), timestamp = getToday(),
value = Entry.NO, value = Entry.NO,
notes = "" notes = "",
x = 0f,
y = 0f
) )
assertFalse(habit1.isCompletedToday()) assertFalse(habit1.isCompletedToday())
} }

View File

@@ -120,10 +120,15 @@ class DateUtilsTest : BaseUnitTest() {
@Test @Test
fun getWeekdaysInMonth() { fun getWeekdaysInMonth() {
val february = GregorianCalendar(2018, Calendar.FEBRUARY, 1) fun getCalendarUTC(year: Int, month: Int, dayOfMonth: Int) =
val leapFebruary = GregorianCalendar(2020, Calendar.FEBRUARY, 1) GregorianCalendar(year, month, dayOfMonth).apply {
val month = GregorianCalendar(2020, Calendar.APRIL, 1) timeZone = TimeZone.getTimeZone("UTC")
val longMonth = GregorianCalendar(2020, Calendar.AUGUST, 1) }
val february = getCalendarUTC(2018, Calendar.FEBRUARY, 1)
val leapFebruary = getCalendarUTC(2020, Calendar.FEBRUARY, 1)
val month = getCalendarUTC(2020, Calendar.APRIL, 1)
val longMonth = getCalendarUTC(2020, Calendar.AUGUST, 1)
assertThat( assertThat(
arrayOf(4, 4, 4, 4, 4, 4, 4), arrayOf(4, 4, 4, 4, 4, 4, 4),

View File

@@ -38,9 +38,9 @@ application {
dependencies { dependencies {
val ktorVersion = "1.6.8" val ktorVersion = "1.6.8"
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.21") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.22")
implementation("io.ktor:ktor-server-netty:$ktorVersion") implementation("io.ktor:ktor-server-netty:$ktorVersion")
implementation("ch.qos.logback:logback-classic:1.4.13") implementation("ch.qos.logback:logback-classic:1.4.14")
implementation("io.ktor:ktor-server-core:$ktorVersion") implementation("io.ktor:ktor-server-core:$ktorVersion")
implementation("io.ktor:ktor-html-builder:$ktorVersion") implementation("io.ktor:ktor-html-builder:$ktorVersion")
implementation("io.ktor:ktor-jackson:$ktorVersion") implementation("io.ktor:ktor-jackson:$ktorVersion")
@@ -49,7 +49,7 @@ dependencies {
implementation("io.prometheus:simpleclient_httpserver:0.16.0") implementation("io.prometheus:simpleclient_httpserver:0.16.0")
implementation("io.prometheus:simpleclient_hotspot:0.16.0") implementation("io.prometheus:simpleclient_hotspot:0.16.0")
testImplementation("io.ktor:ktor-server-tests:$ktorVersion") testImplementation("io.ktor:ktor-server-tests:$ktorVersion")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.1.0") testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
testImplementation(kotlin("test")) testImplementation(kotlin("test"))
testImplementation(kotlin("test-junit")) testImplementation(kotlin("test-junit"))
} }