mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 09:08:52 -06:00
Compare commits
43 Commits
v2.2.0
...
62d9d29e91
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62d9d29e91 | ||
| 451b536e71 | |||
| 14dbf90c23 | |||
| 280a5ddceb | |||
| 22331ed364 | |||
| d0a45eb523 | |||
|
|
fec73af665 | ||
|
fc9cc423d0
|
|||
|
e7165d993f
|
|||
|
732ec1c70a
|
|||
|
e823cd5758
|
|||
| 46fe683d71 | |||
|
7f6248123c
|
|||
|
2024277ebe
|
|||
| c216fb01d6 | |||
|
7cb32f486b
|
|||
|
093591fbaf
|
|||
|
985234cdf3
|
|||
|
06090e238a
|
|||
|
e48452f724
|
|||
|
|
936986e110 | ||
|
|
4b3910aea8 | ||
|
|
10074ded32 | ||
|
|
1280e798d2
|
||
|
|
b09306e793
|
||
|
|
e30636a447
|
||
|
|
ad8738180c
|
||
|
|
08410c59d0
|
||
|
|
ab86cee70b | ||
|
|
3a0603605b
|
||
|
|
6a78b4d853 | ||
|
|
fe43b1435d | ||
|
|
12503b8a6d | ||
|
|
ef7f78bff0 | ||
|
|
53c208ded5 | ||
|
|
1bdc83e92f | ||
|
|
680c1cdc76 | ||
|
|
80916bac50 | ||
|
|
a5e3e9b3cf | ||
|
248ba50a8e
|
|||
|
|
45a82b3c2d
|
||
|
770d1293dc
|
|||
|
|
25cff3d9b0 |
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
@@ -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"
|
||||
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
|
||||
- name: Upload artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: |
|
||||
|
||||
24
NOTICE.md
24
NOTICE.md
@@ -1,6 +1,6 @@
|
||||
# Copyright Notices
|
||||
|
||||
### ActiveAndroid
|
||||
## ActiveAndroid
|
||||
|
||||
<https://github.com/pardom/ActiveAndroid>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
### Android Open Source Project
|
||||
## Android Open Source Project
|
||||
|
||||
<https://source.android.com/>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
### FontAwesome
|
||||
## FontAwesome
|
||||
|
||||
<http://fontawesome.io>
|
||||
|
||||
@@ -59,7 +59,7 @@ under the SIL OFL 1.1.
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
### Material Design Icons
|
||||
## 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
|
||||
Attribution 4.0 International License (CC-BY 4.0).
|
||||
|
||||
### Android Flow Layout
|
||||
## Android Flow Layout
|
||||
|
||||
<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
|
||||
under the License.
|
||||
|
||||
### Dagger 2
|
||||
## Dagger 2
|
||||
|
||||
<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
|
||||
limitations under the License.
|
||||
|
||||
### AutoFactory
|
||||
## AutoFactory
|
||||
|
||||
<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
|
||||
limitations under the License.
|
||||
|
||||
### Retrolambda
|
||||
## 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.
|
||||
The license text is at http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
### PebbleKit SDK
|
||||
## PebbleKit 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)
|
||||
Copyright (c) 2014 - 2015 Pebble Technology
|
||||
|
||||
### AppIntro
|
||||
## 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
|
||||
limitations under the License.
|
||||
|
||||
### ButterKnife
|
||||
## 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
|
||||
limitations under the License.
|
||||
|
||||
### opencsv
|
||||
## opencsv
|
||||
|
||||
<http://opencsv.sourceforge.net/>
|
||||
|
||||
|
||||
35
README.md
35
README.md
@@ -1,13 +1,13 @@
|
||||
<h1 align="center">Loop Habit Tracker</h1>
|
||||
<p align="center">
|
||||
<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 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 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>
|
||||
</p>
|
||||
|
||||
@@ -32,32 +32,23 @@ source.
|
||||
|
||||
## Features
|
||||
|
||||
* <b>Beautiful, minimalistic and lightweight interface.</b>
|
||||
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.
|
||||
* **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.
|
||||
|
||||
* <b>Habit score.</b>
|
||||
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.
|
||||
* **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.
|
||||
|
||||
* <b>Flexible schedules.</b>
|
||||
In addition to daily habits, Loop supports habits with more complex schedules, such as 3 times per week or every other day.
|
||||
* **Flexible schedules.** 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>
|
||||
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.
|
||||
* **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.
|
||||
|
||||
* <b>Widgets.</b>
|
||||
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.
|
||||
* **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.
|
||||
|
||||
* <b>Take control of your data.</b>
|
||||
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.
|
||||
* **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.
|
||||
|
||||
* <b>No limitations.</b>
|
||||
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.
|
||||
* **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.
|
||||
|
||||
* <b>Completely ad-free and open source.</b>
|
||||
There are no advertisements, annoying notifications or intrusive permissions in this app, and there will never be. The app is completely open-source (GPLv3).
|
||||
* **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).
|
||||
|
||||
* <b>Works offline and respects your privacy.</b>
|
||||
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.
|
||||
* **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.
|
||||
|
||||
## Installing
|
||||
|
||||
@@ -94,7 +85,7 @@ contribute, even if you are not a software developer.
|
||||
|
||||
## 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>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
val kotlinVersion = "1.9.21"
|
||||
id("com.android.application") version "8.1.4" apply (false)
|
||||
val kotlinVersion = "2.1.10"
|
||||
id("com.android.application") version "8.8.0" apply (false)
|
||||
id("org.jetbrains.kotlin.android") version kotlinVersion apply (false)
|
||||
id("org.jetbrains.kotlin.kapt") version kotlinVersion apply (false)
|
||||
id("org.jetbrains.kotlin.multiplatform") version kotlinVersion apply (false)
|
||||
|
||||
1
build.sh
1
build.sh
@@ -64,6 +64,7 @@ fail() {
|
||||
core_build() {
|
||||
log_info "Building uhabits-core..."
|
||||
$GRADLE ktlintCheck || fail
|
||||
$GRADLE lintDebug || fail
|
||||
$GRADLE :uhabits-core:build || fail
|
||||
}
|
||||
|
||||
|
||||
@@ -9,17 +9,17 @@ This pages describes how to download and build the app from the source code. If
|
||||
|
||||
## 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).
|
||||
|
||||
**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.
|
||||
|
||||
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:
|
||||
```bash
|
||||
@@ -27,7 +27,7 @@ git clone https://github.com/iSoron/uhabits.git
|
||||
```
|
||||
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".
|
||||
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.
|
||||
|
||||
**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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
**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.
|
||||
|
||||
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.
|
||||
```
|
||||
```bash
|
||||
export PATH="$PATH:/opt/android-sdk/tools/bin"
|
||||
export PATH="$PATH:/opt/android-sdk/platform-tools"
|
||||
export ANDROID_HOME="/opt/android-sdk"
|
||||
@@ -71,7 +71,7 @@ export ANDROID_HOME="/opt/android-sdk"
|
||||
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:
|
||||
```bash
|
||||
@@ -79,13 +79,11 @@ git clone https://github.com/iSoron/uhabits.git
|
||||
```
|
||||
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`
|
||||
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.
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
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
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("com.github.triplet.play") version "3.8.6"
|
||||
id("com.android.application") version "8.1.4"
|
||||
id("com.android.application") version "8.8.0"
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("org.jetbrains.kotlin.kapt")
|
||||
id("org.jlleitschuh.gradle.ktlint")
|
||||
@@ -43,13 +42,14 @@ kotlin {
|
||||
android {
|
||||
|
||||
namespace = "org.isoron.uhabits"
|
||||
compileSdk = 34
|
||||
compileSdk = 35
|
||||
// compileSdkPreview = "VanillaIceCream"
|
||||
|
||||
defaultConfig {
|
||||
versionCode = 20200
|
||||
versionName = "2.2.0"
|
||||
minSdk = 28
|
||||
targetSdk = 34
|
||||
targetSdk = 35
|
||||
applicationId = "org.isoron.uhabits"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -91,14 +91,18 @@ android {
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError = false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
val daggerVersion = "2.48.1"
|
||||
val kotlinVersion = "1.9.21"
|
||||
val kxCoroutinesVersion = "1.7.3"
|
||||
val daggerVersion = "2.51.1"
|
||||
val kotlinVersion = "2.1.10"
|
||||
val kxCoroutinesVersion = "1.10.1"
|
||||
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-core:$espressoVersion")
|
||||
@@ -106,17 +110,17 @@ dependencies {
|
||||
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.3")
|
||||
androidTestImplementation("io.ktor:ktor-client-mock:$ktorVersion")
|
||||
androidTestImplementation("io.ktor:ktor-jackson:$ktorVersion")
|
||||
androidTestImplementation("androidx.annotation:annotation:1.7.0")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
|
||||
androidTestImplementation("androidx.test:rules:1.5.0")
|
||||
androidTestImplementation("org.mockito.kotlin:mockito-kotlin:5.1.0")
|
||||
androidTestImplementation("androidx.annotation:annotation:1.7.1")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.2.1")
|
||||
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
|
||||
androidTestImplementation("androidx.test:rules:1.6.1")
|
||||
androidTestImplementation("org.mockito.kotlin:mockito-kotlin:5.4.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.google.code.findbugs:jsr305:3.0.2")
|
||||
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-core:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-jackson:$ktorVersion")
|
||||
@@ -124,24 +128,20 @@ 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.6.1")
|
||||
implementation("androidx.appcompat:appcompat:1.7.0")
|
||||
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.10.0")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
implementation("com.opencsv:opencsv:5.9")
|
||||
implementation("nl.dionsegijn:konfetti-xml:2.0.2")
|
||||
implementation(project(":uhabits-core"))
|
||||
kapt("com.google.dagger:dagger-compiler:$daggerVersion")
|
||||
kaptAndroidTest("com.google.dagger:dagger-compiler:$daggerVersion")
|
||||
testImplementation("com.google.dagger:dagger:$daggerVersion")
|
||||
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 {
|
||||
correctErrorTypes = true
|
||||
}
|
||||
|
||||
play {
|
||||
serviceAccountCredentials.set(file("../.secret/gcp-key.json"))
|
||||
track.set("alpha")
|
||||
}
|
||||
|
||||
8248
uhabits-android/lint-baseline.xml
Normal file
8248
uhabits-android/lint-baseline.xml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -132,7 +132,7 @@ object CommonSteps : BaseUserInterfaceTest() {
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun verifyOpensWebsite(url: String?) {
|
||||
fun verifyOpensWebsite(url: String) {
|
||||
var browserPkg = "org.chromium.webview_shell"
|
||||
if (SDK_INT <= Build.VERSION_CODES.M) {
|
||||
browserPkg = "com.android.browser"
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.isoron.uhabits.BuildConfig
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.databinding.AboutBinding
|
||||
import org.isoron.uhabits.utils.applyRootViewInsets
|
||||
import org.isoron.uhabits.utils.currentTheme
|
||||
import org.isoron.uhabits.utils.setupToolbar
|
||||
|
||||
@@ -54,5 +55,6 @@ class AboutView(
|
||||
binding.tvTranslate.setOnClickListener { screen.showTranslationWebsite() }
|
||||
binding.tvVersion.setOnClickListener { screen.onPressDeveloperCountdown() }
|
||||
binding.tvVersion.text = String.format(version, BuildConfig.VERSION_NAME)
|
||||
applyRootViewInsets()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.databinding.CheckmarkPopupBinding
|
||||
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
|
||||
import org.isoron.uhabits.utils.getCenter
|
||||
import org.isoron.uhabits.utils.sres
|
||||
|
||||
class CheckmarkDialog : AppCompatDialogFragment() {
|
||||
var onToggle: (Int, String) -> Unit = { _, _ -> }
|
||||
var onToggle: (Int, String, Float, Float) -> Unit = { _, _, _, _ -> }
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val appComponent = (requireActivity().application as HabitsApplication).component
|
||||
val prefs = appComponent.preferences
|
||||
val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context))
|
||||
val color = requireArguments().getInt("color")
|
||||
arrayOf(view.yesBtn, view.skipBtn).forEach {
|
||||
it.setTextColor(requireArguments().getInt("color"))
|
||||
it.setTextColor(color)
|
||||
}
|
||||
arrayOf(view.noBtn, view.unknownBtn).forEach {
|
||||
it.setTextColor(view.root.sres.getColor(R.attr.contrast60))
|
||||
@@ -62,7 +64,8 @@ class CheckmarkDialog : AppCompatDialogFragment() {
|
||||
}
|
||||
fun onClick(v: Int) {
|
||||
val notes = view.notes.text.toString().trim()
|
||||
onToggle(v, notes)
|
||||
val location = view.yesBtn.getCenter()
|
||||
onToggle(v, notes, location.x, location.y)
|
||||
requireDialog().dismiss()
|
||||
}
|
||||
view.yesBtn.setOnClickListener { onClick(YES_MANUAL) }
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
package org.isoron.uhabits.activities.common.dialogs
|
||||
|
||||
import android.content.Context
|
||||
import com.android.colorpicker.ColorPickerDialog.SIZE_SMALL
|
||||
import org.isoron.platform.gui.toInt
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
@@ -39,7 +40,7 @@ class ColorPickerDialogFactory @Inject constructor(@param:ActivityContext privat
|
||||
res.getPalette(),
|
||||
androidColor,
|
||||
4,
|
||||
com.android.colorpicker.ColorPickerDialog.SIZE_SMALL
|
||||
SIZE_SMALL
|
||||
)
|
||||
return dialog
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.models.Entry
|
||||
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
|
||||
import org.isoron.uhabits.utils.InterfaceUtils
|
||||
import org.isoron.uhabits.utils.getCenter
|
||||
import org.isoron.uhabits.utils.requestFocusWithKeyboard
|
||||
import org.isoron.uhabits.utils.sres
|
||||
import java.text.DecimalFormat
|
||||
@@ -24,7 +25,7 @@ import java.text.ParseException
|
||||
|
||||
class NumberDialog : AppCompatDialogFragment() {
|
||||
|
||||
var onToggle: (Double, String) -> Unit = { _, _ -> }
|
||||
var onToggle: (Double, String, Float, Float) -> Unit = { _, _, _, _ -> }
|
||||
var onDismiss: () -> Unit = {}
|
||||
|
||||
private var originalNotes: String = ""
|
||||
@@ -104,12 +105,17 @@ class NumberDialog : AppCompatDialogFragment() {
|
||||
try {
|
||||
val numberFormat = NumberFormat.getInstance()
|
||||
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) {
|
||||
// NOP
|
||||
}
|
||||
val notes = view.notes.text.toString()
|
||||
onToggle(value, notes)
|
||||
val location = view.saveBtn.getCenter()
|
||||
onToggle(value, notes, location.x, location.y)
|
||||
requireDialog().dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ class RingView : View {
|
||||
private var em = 0f
|
||||
private var text: String?
|
||||
private var textSize: Float
|
||||
private var isStrokedTextEnabled: Boolean = false
|
||||
private var enableFontAwesome = false
|
||||
private var internalDrawingCache: Bitmap? = null
|
||||
private var cacheCanvas: Canvas? = null
|
||||
@@ -131,6 +132,10 @@ class RingView : View {
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun setIsStrokedTextEnabled(isStroked: Boolean) {
|
||||
this.isStrokedTextEnabled = isStroked
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
val activeCanvas: Canvas?
|
||||
@@ -159,6 +164,12 @@ class RingView : View {
|
||||
pRing!!.xfermode = null
|
||||
pRing!!.color = color
|
||||
pRing!!.textSize = textSize
|
||||
|
||||
if (isStrokedTextEnabled) {
|
||||
pRing!!.style = Paint.Style.STROKE
|
||||
pRing!!.strokeWidth = textSize / 15f
|
||||
}
|
||||
|
||||
if (enableFontAwesome) pRing!!.typeface = getFontAwesome(context)
|
||||
activeCanvas.drawText(
|
||||
text!!,
|
||||
|
||||
@@ -22,7 +22,6 @@ package org.isoron.uhabits.activities.habits.edit
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
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.WeekdayList
|
||||
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.formatTime
|
||||
import org.isoron.uhabits.utils.toFormattedString
|
||||
@@ -94,6 +94,8 @@ class EditHabitActivity : AppCompatActivity() {
|
||||
themeSwitcher.apply()
|
||||
|
||||
binding = ActivityEditHabitBinding.inflate(layoutInflater)
|
||||
binding.root.applyRootViewInsets()
|
||||
binding.toolbar.applyToolbarInsets()
|
||||
setContentView(binding.root)
|
||||
|
||||
if (intent.hasExtra("habitId")) {
|
||||
@@ -352,8 +354,7 @@ class EditHabitActivity : AppCompatActivity() {
|
||||
androidColor = themeSwitcher.currentTheme.color(color).toInt()
|
||||
binding.colorButton.backgroundTintList = ColorStateList.valueOf(androidColor)
|
||||
if (!themeSwitcher.isNightMode) {
|
||||
val darkerAndroidColor = ColorUtils.mixColors(Color.BLACK, androidColor, 0.15f)
|
||||
window.statusBarColor = darkerAndroidColor
|
||||
window.statusBarColor = androidColor
|
||||
binding.toolbar.setBackgroundColor(androidColor)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import org.isoron.uhabits.inject.ActivityContextModule
|
||||
import org.isoron.uhabits.inject.DaggerHabitsActivityComponent
|
||||
import org.isoron.uhabits.inject.HabitsActivityComponent
|
||||
import org.isoron.uhabits.inject.HabitsApplicationComponent
|
||||
import org.isoron.uhabits.utils.applyRootViewInsets
|
||||
import org.isoron.uhabits.utils.dismissCurrentDialog
|
||||
import org.isoron.uhabits.utils.restartWithFade
|
||||
|
||||
@@ -100,6 +101,7 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
||||
menu = component.listHabitsMenu
|
||||
Thread.setDefaultUncaughtExceptionHandler(BaseExceptionHandler(this))
|
||||
component.listHabitsBehavior.onStartup()
|
||||
rootView.applyRootViewInsets()
|
||||
setContentView(rootView)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.content.Context
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.RelativeLayout
|
||||
import nl.dionsegijn.konfetti.xml.KonfettiView
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.activities.common.views.ScrollableChart
|
||||
import org.isoron.uhabits.activities.common.views.TaskProgressBar
|
||||
@@ -69,6 +70,9 @@ class ListHabitsRootView @Inject constructor(
|
||||
val listView: HabitCardListView = habitCardListViewFactory.create()
|
||||
val llEmpty = EmptyListView(context)
|
||||
val tbar = buildToolbar()
|
||||
val konfettiView = KonfettiView(context).apply {
|
||||
translationZ = 10f
|
||||
}
|
||||
val progressBar = TaskProgressBar(context, runner)
|
||||
val hintView: HintView
|
||||
val header = HeaderView(context, preferences, midnightTimer)
|
||||
@@ -80,6 +84,7 @@ class ListHabitsRootView @Inject constructor(
|
||||
|
||||
val rootView = RelativeLayout(context).apply {
|
||||
background = sres.getDrawable(R.attr.windowBackgroundColor)
|
||||
addAtTop(konfettiView)
|
||||
addAtTop(tbar)
|
||||
addBelow(header, tbar)
|
||||
addBelow(listView, header, height = MATCH_PARENT)
|
||||
|
||||
@@ -25,6 +25,9 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
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.uhabits.R
|
||||
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.ImportDataTask
|
||||
import org.isoron.uhabits.tasks.ImportDataTaskFactory
|
||||
import org.isoron.uhabits.utils.ColorUtils
|
||||
import org.isoron.uhabits.utils.copyTo
|
||||
import org.isoron.uhabits.utils.currentTheme
|
||||
import org.isoron.uhabits.utils.dismissCurrentAndShow
|
||||
@@ -72,6 +76,7 @@ import org.isoron.uhabits.utils.showSendEmailScreen
|
||||
import org.isoron.uhabits.utils.showSendFileScreen
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
const val RESULT_IMPORT_DATA = 101
|
||||
@@ -218,6 +223,28 @@ class ListHabitsScreen
|
||||
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() {
|
||||
val intent = intentFactory.startSettingsActivity(activity)
|
||||
activity.startActivityForResult(intent, REQUEST_SETTINGS)
|
||||
@@ -240,7 +267,7 @@ class ListHabitsScreen
|
||||
putDouble("value", value)
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -258,7 +285,7 @@ class ListHabitsScreen
|
||||
putInt("value", selectedValue)
|
||||
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")
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.PointF
|
||||
import android.graphics.text.LineBreaker.BREAK_STRATEGY_BALANCED
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
@@ -154,7 +155,17 @@ class HabitCardView(
|
||||
checkmarkPanel = checkmarkPanelFactory.create().apply {
|
||||
onToggle = { timestamp, value, notes ->
|
||||
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 ->
|
||||
triggerRipple(timestamp)
|
||||
@@ -206,12 +217,27 @@ class HabitCardView(
|
||||
}
|
||||
|
||||
fun triggerRipple(timestamp: Timestamp) {
|
||||
val location = getRelativeButtonLocation(timestamp)
|
||||
triggerRipple(location.x, location.y)
|
||||
}
|
||||
|
||||
private fun getRelativeButtonLocation(timestamp: Timestamp): PointF {
|
||||
val today = DateUtils.getTodayWithOffset()
|
||||
val offset = timestamp.daysUntil(today) - dataOffset
|
||||
val button = checkmarkPanel.buttons[offset]
|
||||
val y = button.height / 2.0f
|
||||
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() {
|
||||
|
||||
@@ -141,7 +141,7 @@ class HeaderView(
|
||||
|
||||
val y1 = rect.centerY() - 0.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[1], rect.centerX(), y2.toFloat(), paint)
|
||||
day.add(GregorianCalendar.DAY_OF_MONTH, -1)
|
||||
|
||||
@@ -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.views.OnDateClickedListener
|
||||
import org.isoron.uhabits.intents.IntentFactory
|
||||
import org.isoron.uhabits.utils.applyRootViewInsets
|
||||
import org.isoron.uhabits.utils.currentTheme
|
||||
import org.isoron.uhabits.utils.dismissCurrentAndShow
|
||||
import org.isoron.uhabits.utils.dismissCurrentDialog
|
||||
@@ -109,6 +110,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
||||
)
|
||||
|
||||
view.setListener(presenter)
|
||||
view.applyRootViewInsets()
|
||||
setContentView(view)
|
||||
}
|
||||
|
||||
@@ -179,7 +181,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
||||
putDouble("value", value)
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -196,7 +198,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
||||
putInt("value", selectedValue)
|
||||
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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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.ShowHabitState
|
||||
import org.isoron.uhabits.databinding.ShowHabitBinding
|
||||
import org.isoron.uhabits.utils.applyToolbarInsets
|
||||
import org.isoron.uhabits.utils.setupToolbar
|
||||
|
||||
class ShowHabitView(context: Context) : FrameLayout(context) {
|
||||
private val binding = ShowHabitBinding.inflate(LayoutInflater.from(context))
|
||||
|
||||
init {
|
||||
binding.toolbar.applyToolbarInsets()
|
||||
addView(binding.root)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.databinding.SettingsActivityBinding
|
||||
import org.isoron.uhabits.utils.applyRootViewInsets
|
||||
import org.isoron.uhabits.utils.setupToolbar
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
@@ -42,6 +43,7 @@ class SettingsActivity : AppCompatActivity() {
|
||||
color = PaletteColor(11),
|
||||
theme = themeSwitcher.currentTheme
|
||||
)
|
||||
binding.root.applyRootViewInsets()
|
||||
setContentView(binding.root)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import org.isoron.uhabits.HabitsApplication
|
||||
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
||||
import org.isoron.uhabits.core.models.HabitMatcher
|
||||
import org.isoron.uhabits.utils.applyRootViewInsets
|
||||
|
||||
class EditSettingActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -45,6 +46,7 @@ class EditSettingActivity : AppCompatActivity() {
|
||||
onSave = controller::onSave,
|
||||
args = args
|
||||
)
|
||||
view.applyRootViewInsets()
|
||||
setContentView(view)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,13 @@ object ColorUtils {
|
||||
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
|
||||
fun setAlpha(color: Int, newAlpha: Float): Int {
|
||||
val intAlpha = (newAlpha * 255).toInt()
|
||||
|
||||
@@ -26,6 +26,7 @@ import android.content.Intent
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PointF
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Handler
|
||||
import android.os.SystemClock
|
||||
@@ -45,6 +46,8 @@ import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.isoron.platform.gui.toInt
|
||||
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 subject = this.getString(subjectId)
|
||||
this.startActivity(
|
||||
@@ -175,10 +182,10 @@ fun View.setupToolbar(
|
||||
} else {
|
||||
theme.color(color).toInt()
|
||||
}
|
||||
val darkerColor = ColorUtils.mixColors(toolbarColor, Color.BLACK, 0.75f)
|
||||
toolbar.background = ColorDrawable(toolbarColor)
|
||||
toolbar.applyToolbarInsets()
|
||||
val activity = context as AppCompatActivity
|
||||
activity.window.statusBarColor = darkerColor
|
||||
activity.window.statusBarColor = toolbarColor
|
||||
activity.setSupportActionBar(toolbar)
|
||||
activity.supportActionBar?.setDisplayHomeAsUpEnabled(displayHomeAsUpEnabled)
|
||||
}
|
||||
@@ -232,3 +239,33 @@ fun View.requestFocusWithKeyboard() {
|
||||
dispatchTouchEvent(MotionEvent.obtain(time, time, MotionEvent.ACTION_UP, 0f, 0f, 0))
|
||||
}, 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,13 +68,13 @@ class CheckmarkWidgetView : HabitWidgetView {
|
||||
val fgColor: Int
|
||||
setShadowAlpha(0x4f)
|
||||
when (entryState) {
|
||||
YES_MANUAL, SKIP -> {
|
||||
YES_MANUAL, SKIP, YES_AUTO -> {
|
||||
bgColor = activeColor
|
||||
fgColor = res.getColor(R.attr.contrast0)
|
||||
backgroundPaint!!.color = bgColor
|
||||
frame!!.setBackgroundDrawable(background)
|
||||
}
|
||||
YES_AUTO, NO, UNKNOWN -> {
|
||||
NO, UNKNOWN -> {
|
||||
bgColor = res.getColor(R.attr.cardBgColor)
|
||||
fgColor = res.getColor(R.attr.contrast60)
|
||||
}
|
||||
@@ -87,12 +87,23 @@ class CheckmarkWidgetView : HabitWidgetView {
|
||||
ring.setColor(fgColor)
|
||||
ring.setBackgroundColor(bgColor)
|
||||
ring.setText(text)
|
||||
ring.setIsStrokedTextEnabled(strokedTextEnabled)
|
||||
label.text = name
|
||||
label.setTextColor(fgColor)
|
||||
requestLayout()
|
||||
postInvalidate()
|
||||
}
|
||||
|
||||
private val strokedTextEnabled: Boolean
|
||||
get() = if (isNumerical) {
|
||||
false
|
||||
} else {
|
||||
when (entryState) {
|
||||
YES_AUTO -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private val text: String
|
||||
get() = if (isNumerical) {
|
||||
(max(0, entryValue) / 1000.0).toShortString()
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
~ 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:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
@@ -61,8 +62,10 @@
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
android:background="?attr/contrast0">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -265,4 +268,4 @@
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -58,7 +58,10 @@
|
||||
|
||||
<LinearLayout
|
||||
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">
|
||||
<LinearLayout style="@style/FormInnerBox">
|
||||
|
||||
@@ -226,6 +226,7 @@
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:paddingTop">6dp</item>
|
||||
<item name="android:paddingBottom">6dp</item>
|
||||
<item name="android:textAlignment">viewStart</item>
|
||||
</style>
|
||||
|
||||
<style name="About.Item.Language">
|
||||
@@ -233,6 +234,7 @@
|
||||
<item name="android:textSize">@dimen/smallTextSize</item>
|
||||
<item name="android:gravity">center</item>
|
||||
<item name="android:background">?attr/contrast20</item>
|
||||
<item name="android:textAlignment">viewStart</item>
|
||||
</style>
|
||||
|
||||
<style name="About.Item.Clickable">
|
||||
@@ -243,7 +245,7 @@
|
||||
|
||||
<style name="Toolbar">
|
||||
<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:theme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item>
|
||||
</style>
|
||||
|
||||
@@ -44,10 +44,10 @@ kotlin {
|
||||
val jvmMain by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib-jdk8"))
|
||||
compileOnly("com.google.dagger:dagger:2.48.1")
|
||||
implementation("com.google.guava:guava:32.1.3-android")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3")
|
||||
implementation("androidx.annotation:annotation:1.7.0")
|
||||
compileOnly("com.google.dagger:dagger:2.51.1")
|
||||
implementation("com.google.guava:guava:33.1.0-android")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.10.1")
|
||||
implementation("androidx.annotation:annotation:1.7.1")
|
||||
implementation("com.google.code.findbugs:jsr305:3.0.2")
|
||||
implementation("com.opencsv:opencsv:5.9")
|
||||
implementation("commons-codec:commons-codec:1.16.0")
|
||||
@@ -59,10 +59,10 @@ kotlin {
|
||||
dependencies {
|
||||
implementation(kotlin("test"))
|
||||
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.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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ data class Habit(
|
||||
return if (isNumerical) {
|
||||
when (targetType) {
|
||||
NumericalHabitType.AT_LEAST -> value / 1000.0 >= targetValue
|
||||
NumericalHabitType.AT_MOST -> value / 1000.0 <= targetValue
|
||||
NumericalHabitType.AT_MOST -> value != Entry.UNKNOWN && value / 1000.0 <= targetValue
|
||||
}
|
||||
} else {
|
||||
value != Entry.NO && value != Entry.UNKNOWN
|
||||
|
||||
@@ -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.CreateRepetitionCommand
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.HabitList
|
||||
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.Timestamp
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
@@ -52,8 +55,16 @@ open class ListHabitsBehavior @Inject constructor(
|
||||
val entry = habit.computedEntries.get(timestamp!!)
|
||||
if (habit.type == HabitType.NUMERICAL) {
|
||||
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()
|
||||
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))
|
||||
}
|
||||
} else {
|
||||
@@ -61,7 +72,8 @@ open class ListHabitsBehavior @Inject constructor(
|
||||
entry.value,
|
||||
entry.notes,
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -117,10 +129,11 @@ open class ListHabitsBehavior @Inject constructor(
|
||||
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(
|
||||
CreateRepetitionCommand(habitList, habit, timestamp, value, notes)
|
||||
)
|
||||
if (value == YES_MANUAL) screen.showConfetti(habit.color, x, y)
|
||||
}
|
||||
|
||||
enum class Message {
|
||||
@@ -144,12 +157,22 @@ open class ListHabitsBehavior @Inject constructor(
|
||||
}
|
||||
|
||||
fun interface NumberPickerCallback {
|
||||
fun onNumberPicked(newValue: Double, notes: String)
|
||||
fun onNumberPicked(
|
||||
newValue: Double,
|
||||
notes: String,
|
||||
x: Float,
|
||||
y: Float
|
||||
)
|
||||
fun onNumberPickerDismissed() {}
|
||||
}
|
||||
|
||||
fun interface CheckMarkDialogCallback {
|
||||
fun onNotesSaved(value: Int, notes: String)
|
||||
fun onNotesSaved(
|
||||
value: Int,
|
||||
notes: String,
|
||||
x: Float,
|
||||
y: Float
|
||||
)
|
||||
fun onNotesDismissed() {}
|
||||
}
|
||||
|
||||
@@ -170,5 +193,6 @@ open class ListHabitsBehavior @Inject constructor(
|
||||
)
|
||||
fun showSendBugReportToDeveloperScreen(log: String)
|
||||
fun showSendFileScreen(filename: String)
|
||||
fun showConfetti(color: PaletteColor, x: Float, y: Float)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ class HistoryCardPresenter(
|
||||
entry.value,
|
||||
entry.notes,
|
||||
habit.color
|
||||
) { newValue, newNotes ->
|
||||
) { newValue, newNotes, _: Float, _: Float ->
|
||||
commandRunner.run(
|
||||
CreateRepetitionCommand(
|
||||
habitList,
|
||||
@@ -135,7 +135,7 @@ class HistoryCardPresenter(
|
||||
screen.showNumberPopup(
|
||||
value = oldValue / 1000.0,
|
||||
notes = entry.notes
|
||||
) { newValue: Double, newNotes: String ->
|
||||
) { newValue: Double, newNotes: String, _: Float, _: Float ->
|
||||
val thousands = (newValue * 1000).roundToInt()
|
||||
commandRunner.run(
|
||||
CreateRepetitionCommand(
|
||||
|
||||
@@ -84,7 +84,7 @@ class ListHabitsBehaviorTest : BaseUnitTest() {
|
||||
eq(""),
|
||||
picker.capture()
|
||||
)
|
||||
picker.lastValue.onNumberPicked(100.0, "")
|
||||
picker.lastValue.onNumberPicked(100.0, "", 0f, 0f)
|
||||
val today = getTodayWithOffset()
|
||||
assertThat(habit2.computedEntries.get(today).value, equalTo(100000))
|
||||
}
|
||||
@@ -168,7 +168,9 @@ class ListHabitsBehaviorTest : BaseUnitTest() {
|
||||
habit = habit1,
|
||||
timestamp = getToday(),
|
||||
value = Entry.NO,
|
||||
notes = ""
|
||||
notes = "",
|
||||
x = 0f,
|
||||
y = 0f
|
||||
)
|
||||
assertFalse(habit1.isCompletedToday())
|
||||
}
|
||||
|
||||
@@ -120,10 +120,15 @@ class DateUtilsTest : BaseUnitTest() {
|
||||
|
||||
@Test
|
||||
fun getWeekdaysInMonth() {
|
||||
val february = GregorianCalendar(2018, Calendar.FEBRUARY, 1)
|
||||
val leapFebruary = GregorianCalendar(2020, Calendar.FEBRUARY, 1)
|
||||
val month = GregorianCalendar(2020, Calendar.APRIL, 1)
|
||||
val longMonth = GregorianCalendar(2020, Calendar.AUGUST, 1)
|
||||
fun getCalendarUTC(year: Int, month: Int, dayOfMonth: Int) =
|
||||
GregorianCalendar(year, month, dayOfMonth).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
|
||||
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(
|
||||
arrayOf(4, 4, 4, 4, 4, 4, 4),
|
||||
|
||||
@@ -38,9 +38,9 @@ application {
|
||||
|
||||
dependencies {
|
||||
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("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-html-builder:$ktorVersion")
|
||||
implementation("io.ktor:ktor-jackson:$ktorVersion")
|
||||
@@ -49,7 +49,7 @@ dependencies {
|
||||
implementation("io.prometheus:simpleclient_httpserver:0.16.0")
|
||||
implementation("io.prometheus:simpleclient_hotspot:0.16.0")
|
||||
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-junit"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user