Compare commits
50 Commits
feature/sy
...
2530afa735
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2530afa735 | ||
|
|
11ca993a75 | ||
|
|
7348ddeffa | ||
|
|
e4d2c93a1d | ||
|
|
07065a60ad | ||
|
|
916cd76be1 | ||
|
f70d33878c
|
|||
|
6a55d3c01a
|
|||
|
69b5ed3a6d
|
|||
|
8b2adbf301
|
|||
|
88cc3a2a12
|
|||
|
26526a71a9
|
|||
|
11eb3713e5
|
|||
|
1df9cc7664
|
|||
|
|
499eb467cb | ||
|
|
9d4df73c56 | ||
|
|
d0f32dfa0a | ||
|
|
c609eceefc | ||
|
|
eb68220b8d | ||
|
|
f1a003fabf | ||
|
b76da35752
|
|||
|
abead88ceb
|
|||
|
908eb4ac99
|
|||
|
|
856a0726f7 | ||
|
|
df97b1fd4f | ||
|
71a05d598a
|
|||
|
2131fb3a3d
|
|||
|
1470dcd560
|
|||
| dc678e59df | |||
|
|
d95084500f | ||
|
6f7215b46f
|
|||
|
|
c423d2b3ca | ||
|
|
758fc56277 | ||
|
|
c7d1e92cae | ||
|
|
d24dcbf2ca | ||
|
|
579b33cc78 | ||
|
|
1a3e6315a1 | ||
|
|
7f4d06d15d | ||
|
|
b8033a6012 | ||
|
471f977209
|
|||
|
2ba5f5fb98
|
|||
|
4de67bd27a
|
|||
|
0bb82a48a5
|
|||
|
d5a5273607
|
|||
|
404671546c
|
|||
|
a94c6e8b9f
|
|||
|
da09df2dd1
|
|||
|
40a4d254f5
|
|||
|
177d01edd9
|
|||
|
9f5da7b4fe
|
4
.github/dependabot.yml
vendored
@@ -4,3 +4,7 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
|
||||
5
.github/workflows/main.yml
vendored
@@ -9,9 +9,10 @@ on:
|
||||
jobs:
|
||||
Test:
|
||||
runs-on: self-hosted
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Check out source code
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build project
|
||||
run: ./build.sh build
|
||||
@@ -21,7 +22,7 @@ jobs:
|
||||
|
||||
- name: Upload artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: |
|
||||
|
||||
19
CHANGELOG.md
@@ -1,5 +1,24 @@
|
||||
# Changelog
|
||||
|
||||
## [2.1.2] -- 2023-05-26
|
||||
### Fixed
|
||||
- Fix bug that caused widget to enter checkmark on wrong date (@iSoron, #1541)
|
||||
- Fix widget corners on Android 12 (@iSoron)
|
||||
- Fix bug that caused notes to be lost when editing a checkmark (@iSoron, #1566)
|
||||
- Prevent soft keyboard from covering entry popup (@iSoron)
|
||||
- Accept comma (instead of dot) in certain locales (@iSoron)
|
||||
|
||||
### Changed
|
||||
- Remove update delay after entering a checkmark (@iSoron)
|
||||
|
||||
### Removed
|
||||
- Remove stack widgets (@iSoron)
|
||||
|
||||
|
||||
## [2.1.1] -- 2022-09-24
|
||||
### Fixed
|
||||
- Fix Tasker plugin (@iSoron, #1503)
|
||||
|
||||
## [2.1.0] -- 2022-09-10
|
||||
### Added
|
||||
- Allow user to add notes to specific dates (@vbh, #1103)
|
||||
|
||||
@@ -17,8 +17,8 @@ show you how your habits improved over time. It is completely ad-free and open
|
||||
source.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://play.google.com/store/apps/details?id=org.isoron.uhabits&utm_source=global_co&utm_medium=prtnr&utm_content=Mar2515&utm_campaign=PartBadge&pcampaignid=MKT-AC-global-none-all-co-pr-py-PartBadges-Oct1515-1"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/apps/en-play-badge-border.png" height="75px"/></a>
|
||||
<a href="https://f-droid.org/app/org.isoron.uhabits"><img alt="Get it on F-Droid" src="https://i.imgur.com/baSPE7X.png" height="75px"/></a>
|
||||
<a href="https://play.google.com/store/apps/details?id=org.isoron.uhabits&utm_source=global_co&utm_medium=prtnr&utm_content=Mar2515&utm_campaign=PartBadge&pcampaignid=MKT-AC-global-none-all-co-pr-py-PartBadges-Oct1515-1"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" height="80px"/></a>
|
||||
<a href="https://f-droid.org/app/org.isoron.uhabits"><img alt="Get it on F-Droid" src="https://f-droid.org/badge/get-it-on.png" height="80px"/></a>
|
||||
</p>
|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
plugins {
|
||||
val kotlinVersion = "1.7.10"
|
||||
id("com.android.application") version ("7.3.0-rc01") apply (false)
|
||||
val kotlinVersion = "1.7.21"
|
||||
id("com.android.application") version ("7.3.1") apply (false)
|
||||
id("org.jetbrains.kotlin.android") version kotlinVersion apply (false)
|
||||
id("org.jetbrains.kotlin.kapt") version kotlinVersion apply (false)
|
||||
id("org.jetbrains.kotlin.android.extensions") version kotlinVersion apply (false)
|
||||
id("org.jetbrains.kotlin.multiplatform") version kotlinVersion apply (false)
|
||||
id("org.jlleitschuh.gradle.ktlint") version "11.0.0"
|
||||
id("org.jlleitschuh.gradle.ktlint") version "11.4.2"
|
||||
}
|
||||
|
||||
apply {
|
||||
|
||||
16
build.sh
@@ -217,20 +217,28 @@ android_test_parallel() {
|
||||
(
|
||||
LOG=build/android-test-$API.log
|
||||
log_info "API $API: Running tests..."
|
||||
if android_test $API 1>$LOG 2>&1; then
|
||||
android_test $API 1>$LOG 2>&1
|
||||
ret_code=$?
|
||||
if [ $ret_code = 0 ]; then
|
||||
log_info "API $API: Passed"
|
||||
else
|
||||
log_error "API $API: Failed"
|
||||
fi
|
||||
pkill -9 -f ${AVD_PREFIX}${API}
|
||||
exit $ret_code
|
||||
)&
|
||||
PIDS+=" $!"
|
||||
done
|
||||
|
||||
# Check exit codes
|
||||
RET_CODE=0
|
||||
success=0
|
||||
for pid in $PIDS; do
|
||||
wait $pid || RET_CODE=1
|
||||
wait $pid
|
||||
ret_code=$?
|
||||
if [ $ret_code != 0 ]; then
|
||||
success=1
|
||||
fi
|
||||
echo pid=$pid ret_code=$ret_code success=$success
|
||||
done
|
||||
|
||||
# Print all logs
|
||||
@@ -240,7 +248,7 @@ android_test_parallel() {
|
||||
echo "::endgroup::"
|
||||
done
|
||||
|
||||
return $RET_CODE
|
||||
return $success
|
||||
}
|
||||
|
||||
android_build() {
|
||||
|
||||
54
docs/TEST.md
@@ -2,51 +2,25 @@
|
||||
|
||||
Loop Habit Tracker has a fairly large number of automated tests to reduce the chance of bugs being silently introduced in our code base. The tests are divided into three categories:
|
||||
|
||||
* **Small tests:** These tests run very quickly on the developer's computer, inside a JVM, and do not need an Android emulator or device. They typically test the correctness of core functions of the application, such as the computation of scores and streaks.
|
||||
* **Medium tests:** These tests require an Android emulator or device, but they are still quite fast to run, since only individual classes are tested. The app itself does not need to be launched. Examples include *view tests*, which render our custom views on the device and compare them against prerendered images.
|
||||
* **Large tests:** These are end-to-end tests, which launch the application on an Android emulator and interact with it by touching the screen, much like a regular user.
|
||||
- **Unit tests:** These tests run very quickly on the developer's computer, inside a JVM, and do not need an Android emulator or device. They typically test the correctness of core functions of the application, such as the computation of scores and streaks.
|
||||
- **Instrumented tests:** These tests require an Android emulator or device. _Medium_ instrumented tests are still quite fast to run, since only individual classes are tested. The app itself does not need to be launched. Examples include _view tests_, which render our custom views on the device and compare them against prerendered images. _Large_ instrumented tests launch the application on an Android emulator and interact with it by touching the screen, much like a regular user.
|
||||
|
||||
## Running small tests
|
||||
## Running unit tests
|
||||
|
||||
Small tests can be launched by running `./gradlew test` or by right-clicking a particular class/method in Android Studio and selecting "Run testMethod()" or "Run ClassTest". An alternative way is to use `build.sh`, the script used by our continuous integration server. By running `./build.sh build`, the script will automatically build and run all small tests.
|
||||
Unit tests can be launched by running `./gradlew test` or by right-clicking a particular class/method in Android Studio and selecting "Run testMethod()" or "Run ClassTest". An alternative way is to use `build.sh`, the script used by our continuous integration server. By running `./build.sh build`, the script will automatically build and run all small tests.
|
||||
|
||||
## Running medium tests
|
||||
## Running instrumented tests
|
||||
|
||||
To run medium tests, it is recommended to use the `build.sh` script:
|
||||
To run medium tests, it is recommended to use the `build.sh` script.
|
||||
|
||||
./build.sh build
|
||||
./build.sh medium-tests
|
||||
1. Run `./build.sh android-setup API` to create the emulator, where `API` is the desired API level.
|
||||
2. Run `./build.sh android-tests API` to run the tests on a single API.
|
||||
3. Run `./build.sh android-tests-parallel API API...` to run the tests on multiple APIs in parallel.
|
||||
|
||||
Note that instrumented tests are designed to run on a clean install, inside an emulator. They will not work on actual devices. All tests are also designed for a particular screen size, namely the Nexus 4 configuration (4.7" 768x1280 xhdpi), and a particular locale, namely English (US). Furthermore:
|
||||
|
||||
For this script to succeed, make sure that an emulator is currently running, or that a device (with developer mode activated) is connected via USB.
|
||||
- No additional apps should be installed on the device;
|
||||
- The homescreen must look exactly like it was when the emulator was originally created, with no additional icons or widgets;
|
||||
- All animations must be manually disabled.
|
||||
|
||||
**WARNING!** This script will uninstall the app prior to testing it, and therefore delete all user data!
|
||||
|
||||
|
||||
If there are failing view tests (that is, if some custom views do not render exactly like the prerendered images we have), then the script `./build.sh fetch-images` can be used to download both the actual and the expected images from the device. The images will be downloaded from the device into the folder `tmp/`. After verifying the differences, if you feel that the actual images are actually fine and should replace the prerendered ones, then run `./build.sh accept-images`.
|
||||
|
||||
## Running large tests
|
||||
|
||||
Large tests are significantly more complicated to run. In particular, they require:
|
||||
|
||||
* An Android emulator; they will **not** work on actual devices;
|
||||
* A vanilla x86 AOSP image; they will **not** work with Google API images;
|
||||
* A particular screen size, namely the Nexus 4 configuration on Android Studio (4.7 768x1280 xhdpi);
|
||||
* A particular locale, namely English (US).
|
||||
|
||||
Furthermore:
|
||||
|
||||
* No additional apps should be installed on the device;
|
||||
* The homescreen must look exactly like it was when the emulator was originally created, with no additional icons or widgets;
|
||||
* Developer mode must be activated, and all animations must be manually disabled.
|
||||
|
||||
Only the following Android versions are supported by our test suite:
|
||||
|
||||
* Android 7.0 (API 24)
|
||||
* Android 7.1.1 (API 25)
|
||||
* Android 8.0 (API 26)
|
||||
* Android 8.1 (API 27)
|
||||
* Android 9.0 (API 28)
|
||||
* Android 10.0 (API 29)
|
||||
|
||||
After creating an emulator and configuring it exactly as described above, launch it, wait for it to finish booting up, then run `./build.sh large-tests`. As mentioned before, this script will uninstall the app before testing it, and therefore will delete all the user data.
|
||||
If there are failing view tests (that is, if some custom views do not render exactly like the prerendered images we have), then both the actual and expected images will be automatically downloaded from the device to the folder `uhabits-android/build/outputs`. After verifying the differences, if you feel that the actual images are actually fine and should replace the prerendered ones, then run `./build.sh android-accept-images`.
|
||||
|
||||
@@ -35,8 +35,8 @@ android {
|
||||
compileSdk = 32
|
||||
|
||||
defaultConfig {
|
||||
versionCode = 20100
|
||||
versionName = "2.1.0"
|
||||
versionCode = 20200
|
||||
versionName = "2.2.0"
|
||||
minSdk = 28
|
||||
targetSdk = 31
|
||||
applicationId = "org.isoron.uhabits"
|
||||
@@ -80,11 +80,11 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
val daggerVersion = "2.43.2"
|
||||
val kotlinVersion = "1.7.10"
|
||||
val daggerVersion = "2.46"
|
||||
val kotlinVersion = "1.7.21"
|
||||
val kxCoroutinesVersion = "1.6.4"
|
||||
val ktorVersion = "1.6.8"
|
||||
val espressoVersion = "3.4.0"
|
||||
val espressoVersion = "3.5.1"
|
||||
|
||||
androidTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:$espressoVersion")
|
||||
@@ -92,10 +92,10 @@ 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.4.0")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
||||
androidTestImplementation("androidx.annotation:annotation:1.5.0")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
|
||||
androidTestImplementation("androidx.test:rules:1.4.0")
|
||||
androidTestImplementation("androidx.test:rules:1.5.0")
|
||||
androidTestImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
||||
compileOnly("javax.annotation:jsr250-api:1.0")
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.2")
|
||||
@@ -110,11 +110,11 @@ dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kxCoroutinesVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kxCoroutinesVersion")
|
||||
implementation("androidx.appcompat:appcompat:1.5.0")
|
||||
implementation("androidx.appcompat:appcompat:1.5.1")
|
||||
implementation("androidx.legacy:legacy-preference-v14:1.0.0")
|
||||
implementation("androidx.legacy:legacy-support-v4:1.0.0")
|
||||
implementation("com.google.android.material:material:1.6.1")
|
||||
implementation("com.opencsv:opencsv:5.6")
|
||||
implementation("com.google.android.material:material:1.8.0")
|
||||
implementation("com.opencsv:opencsv:5.7.1")
|
||||
implementation(project(":uhabits-core"))
|
||||
kapt("com.google.dagger:dagger-compiler:$daggerVersion")
|
||||
kaptAndroidTest("com.google.dagger:dagger-compiler:$daggerVersion")
|
||||
|
||||
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
@@ -39,6 +39,7 @@ open class BaseUserInterfaceTest {
|
||||
private lateinit var prefs: Preferences
|
||||
private lateinit var fixtures: HabitFixtures
|
||||
private lateinit var cache: HabitCardListCache
|
||||
|
||||
@Before
|
||||
@Throws(Exception::class)
|
||||
fun setUp() {
|
||||
|
||||
@@ -153,18 +153,22 @@ open class BaseViewTest : BaseAndroidTest() {
|
||||
var filename = filename
|
||||
var dir = getSDCardDir("test-screenshots")
|
||||
if (dir == null) dir = AndroidDirFinder(targetContext).getFilesDir("test-screenshots")
|
||||
if (dir == null) throw RuntimeException(
|
||||
"Could not find suitable dir for screenshots"
|
||||
)
|
||||
if (dir == null) {
|
||||
throw RuntimeException(
|
||||
"Could not find suitable dir for screenshots"
|
||||
)
|
||||
}
|
||||
filename = filename.replace("\\.png$".toRegex(), "$suffix.png")
|
||||
val absolutePath = String.format("%s/%s", dir.absolutePath, filename)
|
||||
val parent = File(absolutePath).parentFile
|
||||
if (!parent.exists() && !parent.mkdirs()) throw RuntimeException(
|
||||
String.format(
|
||||
"Could not create dir: %s",
|
||||
parent.absolutePath
|
||||
if (!parent.exists() && !parent.mkdirs()) {
|
||||
throw RuntimeException(
|
||||
String.format(
|
||||
"Could not create dir: %s",
|
||||
parent.absolutePath
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
val out = FileOutputStream(absolutePath)
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||
return absolutePath
|
||||
|
||||
@@ -44,7 +44,7 @@ class ScoreChartTest : BaseViewTest() {
|
||||
habit = habit,
|
||||
firstWeekday = prefs.firstWeekdayInt,
|
||||
spinnerPosition = 0,
|
||||
theme = LightTheme(),
|
||||
theme = LightTheme()
|
||||
)
|
||||
view = ScoreChart(targetContext).apply {
|
||||
setScores(state.scores)
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.junit.runner.RunWith
|
||||
@MediumTest
|
||||
class StreakChartTest : BaseViewTest() {
|
||||
private lateinit var view: StreakChart
|
||||
|
||||
@Before
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
|
||||
@@ -44,7 +44,7 @@ class EntryButtonViewTest : BaseViewTest() {
|
||||
view = component.getEntryButtonViewFactory().create().apply {
|
||||
value = Entry.NO
|
||||
color = PaletteUtils.getAndroidTestColor(5)
|
||||
onToggle = { _, _, _ -> toggled = true }
|
||||
onToggle = { _, _ -> toggled = true }
|
||||
onEdit = { edited = true }
|
||||
}
|
||||
measureView(view, dpToPixels(48), dpToPixels(48))
|
||||
|
||||
@@ -77,7 +77,7 @@ class EntryPanelViewTest : BaseViewTest() {
|
||||
@Test
|
||||
fun testToggle() {
|
||||
val timestamps = mutableListOf<Timestamp>()
|
||||
view.onToggle = { t, _, _, _ -> timestamps.add(t) }
|
||||
view.onToggle = { t, _, _ -> timestamps.add(t) }
|
||||
view.buttons[0].performLongClick()
|
||||
view.buttons[2].performLongClick()
|
||||
view.buttons[3].performLongClick()
|
||||
@@ -88,7 +88,7 @@ class EntryPanelViewTest : BaseViewTest() {
|
||||
fun testToggle_withOffset() {
|
||||
val timestamps = mutableListOf<Timestamp>()
|
||||
view.dataOffset = 3
|
||||
view.onToggle = { t, _, _, _ -> timestamps += t }
|
||||
view.onToggle = { t, _, _ -> timestamps += t }
|
||||
view.buttons[0].performLongClick()
|
||||
view.buttons[2].performLongClick()
|
||||
view.buttons[3].performLongClick()
|
||||
|
||||
@@ -48,7 +48,7 @@ class FrequencyCardViewTest : BaseViewTest() {
|
||||
FrequencyCardPresenter.buildState(
|
||||
habit = habit,
|
||||
firstWeekday = 0,
|
||||
theme = LightTheme(),
|
||||
theme = LightTheme()
|
||||
)
|
||||
)
|
||||
measureView(view, 800f, 600f)
|
||||
|
||||
@@ -49,7 +49,7 @@ class HistoryCardViewTest : BaseViewTest() {
|
||||
HistoryCardPresenter.buildState(
|
||||
habit = habit,
|
||||
firstWeekday = SUNDAY,
|
||||
theme = LightTheme(),
|
||||
theme = LightTheme()
|
||||
)
|
||||
)
|
||||
measureView(view, 800f, 600f)
|
||||
|
||||
@@ -51,7 +51,7 @@ class OverviewCardViewTest : BaseViewTest() {
|
||||
scoreYearDiff = 0.74f,
|
||||
totalCount = 44,
|
||||
color = PaletteColor(7),
|
||||
theme = LightTheme(),
|
||||
theme = LightTheme()
|
||||
)
|
||||
)
|
||||
measureView(view, 800f, 300f)
|
||||
|
||||
@@ -49,7 +49,7 @@ class ScoreCardViewTest : BaseViewTest() {
|
||||
habit = habit,
|
||||
firstWeekday = 0,
|
||||
spinnerPosition = 0,
|
||||
theme = LightTheme(),
|
||||
theme = LightTheme()
|
||||
)
|
||||
)
|
||||
measureView(view, 800f, 600f)
|
||||
|
||||
@@ -48,7 +48,7 @@ class StreakCardViewTest : BaseViewTest() {
|
||||
StreakCardState(
|
||||
bestStreaks = habit.streaks.getBest(10),
|
||||
color = habit.color,
|
||||
theme = LightTheme(),
|
||||
theme = LightTheme()
|
||||
)
|
||||
)
|
||||
measureView(view, 800f, 600f)
|
||||
|
||||
@@ -53,7 +53,7 @@ class SubtitleCardViewTest : BaseViewTest() {
|
||||
isNumerical = false,
|
||||
question = "Did you meditate this morning?",
|
||||
reminder = Reminder(8, 30, EVERY_DAY),
|
||||
theme = LightTheme(),
|
||||
theme = LightTheme()
|
||||
)
|
||||
)
|
||||
measureView(view, 800f, 200f)
|
||||
|
||||
@@ -119,7 +119,7 @@ class ListHabitsRegressionTest : BaseUserInterfaceTest() {
|
||||
"Wake up early",
|
||||
"Meditate",
|
||||
"Read books",
|
||||
"Track time",
|
||||
"Track time"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,9 +33,11 @@ import java.io.IOException
|
||||
@MediumTest
|
||||
class CheckmarkWidgetViewTest : BaseViewTest() {
|
||||
private lateinit var view: CheckmarkWidgetView
|
||||
|
||||
@Before
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
similarityCutoff = 0.00025
|
||||
setTheme(R.style.WidgetTheme)
|
||||
val habit = fixtures.createShortHabit()
|
||||
val computedEntries = habit.computedEntries
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.isoron.uhabits">
|
||||
|
||||
<uses-permission android:name="android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
@@ -270,7 +269,7 @@
|
||||
<!-- Locale/Tasker -->
|
||||
<receiver
|
||||
android:name=".automation.FireSettingReceiver"
|
||||
android:exported="false">
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
|
||||
</intent-filter>
|
||||
|
||||
@@ -60,7 +60,7 @@ class AndroidCanvas : Canvas {
|
||||
y1.toDp(),
|
||||
x2.toDp(),
|
||||
y2.toDp(),
|
||||
paint,
|
||||
paint
|
||||
)
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ class AndroidCanvas : Canvas {
|
||||
text,
|
||||
x.toDp(),
|
||||
y.toDp() + 0.6f * mHeight,
|
||||
textPaint,
|
||||
textPaint
|
||||
)
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ class AndroidCanvas : Canvas {
|
||||
y: Double,
|
||||
width: Double,
|
||||
height: Double,
|
||||
cornerRadius: Double,
|
||||
cornerRadius: Double
|
||||
) {
|
||||
paint.style = Paint.Style.FILL
|
||||
innerCanvas.drawRoundRect(
|
||||
@@ -93,7 +93,7 @@ class AndroidCanvas : Canvas {
|
||||
(y + height).toDp(),
|
||||
cornerRadius.toDp(),
|
||||
cornerRadius.toDp(),
|
||||
paint,
|
||||
paint
|
||||
)
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ class AndroidCanvas : Canvas {
|
||||
y.toDp(),
|
||||
(x + width).toDp(),
|
||||
(y + height).toDp(),
|
||||
paint,
|
||||
paint
|
||||
)
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ class AndroidCanvas : Canvas {
|
||||
centerY: Double,
|
||||
radius: Double,
|
||||
startAngle: Double,
|
||||
swipeAngle: Double,
|
||||
swipeAngle: Double
|
||||
) {
|
||||
paint.style = Paint.Style.FILL
|
||||
innerCanvas.drawArc(
|
||||
@@ -159,14 +159,14 @@ class AndroidCanvas : Canvas {
|
||||
-startAngle.toFloat(),
|
||||
-swipeAngle.toFloat(),
|
||||
true,
|
||||
paint,
|
||||
paint
|
||||
)
|
||||
}
|
||||
|
||||
override fun fillCircle(
|
||||
centerX: Double,
|
||||
centerY: Double,
|
||||
radius: Double,
|
||||
radius: Double
|
||||
) {
|
||||
paint.style = Paint.Style.FILL
|
||||
innerCanvas.drawCircle(centerX.toDp(), centerY.toDp(), radius.toDp(), paint)
|
||||
|
||||
@@ -33,7 +33,7 @@ import kotlin.math.max
|
||||
*/
|
||||
class AndroidDataView(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
attrs: AttributeSet? = null
|
||||
) : AndroidView<DataView>(context, attrs),
|
||||
GestureDetector.OnGestureListener,
|
||||
ValueAnimator.AnimatorUpdateListener {
|
||||
@@ -60,7 +60,7 @@ class AndroidDataView(
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent?,
|
||||
dx: Float,
|
||||
dy: Float,
|
||||
dy: Float
|
||||
): Boolean {
|
||||
if (abs(dx) > abs(dy)) {
|
||||
val parent = parent
|
||||
@@ -82,7 +82,7 @@ class AndroidDataView(
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent?,
|
||||
velocityX: Float,
|
||||
velocityY: Float,
|
||||
velocityY: Float
|
||||
): Boolean {
|
||||
scroller.fling(
|
||||
scroller.currX,
|
||||
@@ -140,8 +140,11 @@ class AndroidDataView(
|
||||
// e.getPointerId.
|
||||
return false
|
||||
}
|
||||
if (isSingleTap) view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity)
|
||||
else view?.onLongClick(x / canvas.innerDensity, y / canvas.innerDensity)
|
||||
if (isSingleTap) {
|
||||
view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity)
|
||||
} else {
|
||||
view?.onLongClick(x / canvas.innerDensity, y / canvas.innerDensity)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,6 @@ fun Color.toInt(): Int {
|
||||
(255 * this.alpha).roundToInt(),
|
||||
(255 * this.red).roundToInt(),
|
||||
(255 * this.green).roundToInt(),
|
||||
(255 * this.blue).roundToInt(),
|
||||
(255 * this.blue).roundToInt()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import android.util.AttributeSet
|
||||
|
||||
open class AndroidView<T : View>(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
attrs: AttributeSet? = null
|
||||
) : android.view.View(context, attrs) {
|
||||
|
||||
var view: T? = null
|
||||
|
||||
@@ -39,7 +39,7 @@ import org.isoron.uhabits.inject.ActivityScope
|
||||
class AndroidThemeSwitcher
|
||||
constructor(
|
||||
@ActivityContext val context: Context,
|
||||
preferences: Preferences,
|
||||
preferences: Preferences
|
||||
) : ThemeSwitcher(preferences) {
|
||||
|
||||
override var currentTheme: Theme = LightTheme()
|
||||
|
||||
@@ -27,7 +27,7 @@ import org.isoron.uhabits.utils.startActivitySafely
|
||||
class AboutScreen(
|
||||
private val activity: AboutActivity,
|
||||
private val intents: IntentFactory,
|
||||
private val prefs: Preferences,
|
||||
private val prefs: Preferences
|
||||
) {
|
||||
|
||||
private var developerCountdown = 5
|
||||
|
||||
@@ -32,7 +32,7 @@ import org.isoron.uhabits.utils.setupToolbar
|
||||
@SuppressLint("ViewConstructor")
|
||||
class AboutView(
|
||||
context: Context,
|
||||
private val screen: AboutScreen,
|
||||
private val screen: AboutScreen
|
||||
) : FrameLayout(context) {
|
||||
|
||||
private var binding = AboutBinding.inflate(LayoutInflater.from(context))
|
||||
@@ -43,7 +43,7 @@ class AboutView(
|
||||
toolbar = binding.toolbar,
|
||||
color = PaletteColor(11),
|
||||
title = resources.getString(R.string.about),
|
||||
theme = currentTheme(),
|
||||
theme = currentTheme()
|
||||
)
|
||||
val version = resources.getString(R.string.version_n)
|
||||
binding.tvContributors.setOnClickListener { screen.showCodeContributorsWebsite() }
|
||||
|
||||
@@ -20,109 +20,60 @@
|
||||
package org.isoron.uhabits.activities.common.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import org.isoron.uhabits.HabitsApplication
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.NO
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
|
||||
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
|
||||
import org.isoron.uhabits.utils.dimBehind
|
||||
import org.isoron.uhabits.utils.dismissCurrentAndShow
|
||||
import org.isoron.uhabits.utils.dp
|
||||
import org.isoron.uhabits.utils.sres
|
||||
|
||||
const val POPUP_WIDTH = 4 * 48f + 16f
|
||||
const val POPUP_HEIGHT = 48f * 2.5f + 8f
|
||||
|
||||
class CheckmarkPopup(
|
||||
private val context: Context,
|
||||
private val color: Int,
|
||||
private var notes: String,
|
||||
private var value: Int,
|
||||
private val prefs: Preferences,
|
||||
private val anchor: View,
|
||||
) {
|
||||
class CheckmarkDialog : AppCompatDialogFragment() {
|
||||
var onToggle: (Int, String) -> Unit = { _, _ -> }
|
||||
private lateinit var dialog: Dialog
|
||||
|
||||
private val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context)).apply {
|
||||
// Required for round corners
|
||||
container.clipToOutline = true
|
||||
}
|
||||
|
||||
init {
|
||||
view.booleanButtons.visibility = VISIBLE
|
||||
initColors()
|
||||
initTypefaces()
|
||||
hideDisabledButtons()
|
||||
populate()
|
||||
}
|
||||
|
||||
private fun initColors() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val appComponent = (requireActivity().application as HabitsApplication).component
|
||||
val prefs = appComponent.preferences
|
||||
val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context))
|
||||
arrayOf(view.yesBtn, view.skipBtn).forEach {
|
||||
it.setTextColor(color)
|
||||
it.setTextColor(requireArguments().getInt("color"))
|
||||
}
|
||||
arrayOf(view.noBtn, view.unknownBtn).forEach {
|
||||
it.setTextColor(view.root.sres.getColor(R.attr.contrast60))
|
||||
}
|
||||
}
|
||||
|
||||
private fun initTypefaces() {
|
||||
arrayOf(view.yesBtn, view.noBtn, view.skipBtn, view.unknownBtn).forEach {
|
||||
it.typeface = getFontAwesome(context)
|
||||
it.typeface = getFontAwesome(requireContext())
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideDisabledButtons() {
|
||||
view.notes.setText(requireArguments().getString("notes")!!)
|
||||
if (!prefs.isSkipEnabled) view.skipBtn.visibility = GONE
|
||||
if (!prefs.areQuestionMarksEnabled) view.unknownBtn.visibility = GONE
|
||||
}
|
||||
|
||||
private fun populate() {
|
||||
val selectedBtn = when (value) {
|
||||
YES_MANUAL -> view.yesBtn
|
||||
YES_AUTO -> view.noBtn
|
||||
NO -> view.noBtn
|
||||
UNKNOWN -> if (prefs.areQuestionMarksEnabled) view.unknownBtn else view.noBtn
|
||||
SKIP -> if (prefs.isSkipEnabled) view.skipBtn else view.noBtn
|
||||
else -> null
|
||||
}
|
||||
view.notes.setText(notes)
|
||||
}
|
||||
|
||||
fun show() {
|
||||
dialog = Dialog(context, android.R.style.Theme_NoTitleBar)
|
||||
view.booleanButtons.visibility = VISIBLE
|
||||
val dialog = Dialog(requireContext())
|
||||
dialog.setContentView(view.root)
|
||||
dialog.window?.apply {
|
||||
setLayout(
|
||||
view.root.dp(POPUP_WIDTH).toInt(),
|
||||
view.root.dp(POPUP_HEIGHT).toInt()
|
||||
)
|
||||
setBackgroundDrawableResource(android.R.color.transparent)
|
||||
}
|
||||
fun onClick(v: Int) {
|
||||
this.value = v
|
||||
save()
|
||||
val notes = view.notes.text.toString().trim()
|
||||
onToggle(v, notes)
|
||||
requireDialog().dismiss()
|
||||
}
|
||||
view.yesBtn.setOnClickListener { onClick(YES_MANUAL) }
|
||||
view.noBtn.setOnClickListener { onClick(NO) }
|
||||
view.skipBtn.setOnClickListener { onClick(SKIP) }
|
||||
view.unknownBtn.setOnClickListener { onClick(UNKNOWN) }
|
||||
dialog.setCanceledOnTouchOutside(true)
|
||||
dialog.dimBehind()
|
||||
dialog.dismissCurrentAndShow()
|
||||
}
|
||||
view.notes.setOnEditorActionListener { v, actionId, event ->
|
||||
onClick(requireArguments().getInt("value"))
|
||||
true
|
||||
}
|
||||
|
||||
fun save() {
|
||||
onToggle(value, view.notes.text.toString().trim())
|
||||
dialog.dismiss()
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
@@ -48,22 +48,22 @@ class FrequencyPickerDialog(
|
||||
|
||||
addBeforeAfterText(
|
||||
this.getString(R.string.every_x_days),
|
||||
contentView.everyXDaysContainer,
|
||||
contentView.everyXDaysContainer
|
||||
)
|
||||
|
||||
addBeforeAfterText(
|
||||
this.getString(R.string.x_times_per_week),
|
||||
contentView.xTimesPerWeekContainer,
|
||||
contentView.xTimesPerWeekContainer
|
||||
)
|
||||
|
||||
addBeforeAfterText(
|
||||
this.getString(R.string.x_times_per_month),
|
||||
contentView.xTimesPerMonthContainer,
|
||||
contentView.xTimesPerMonthContainer
|
||||
)
|
||||
|
||||
addBeforeAfterText(
|
||||
this.getString(R.string.x_times_per_y_days),
|
||||
contentView.xTimesPerYDaysContainer,
|
||||
contentView.xTimesPerYDaysContainer
|
||||
)
|
||||
|
||||
contentView.everyDayRadioButton.setOnClickListener {
|
||||
@@ -124,7 +124,8 @@ class FrequencyPickerDialog(
|
||||
val parts = str.split("%d")
|
||||
for (i in parts.indices) {
|
||||
container.addView(
|
||||
TextView(activity).apply { text = parts[i].trim() }, 2 * i + 1,
|
||||
TextView(activity).apply { text = parts[i].trim() },
|
||||
2 * i + 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
|
||||
theme = themeSwitcher.currentTheme,
|
||||
today = DateUtils.getTodayWithOffset().toLocalDate(),
|
||||
onDateClickedListener = onDateClickedListener ?: object : OnDateClickedListener {},
|
||||
padding = 10.0,
|
||||
padding = 10.0
|
||||
)
|
||||
dataView = AndroidDataView(requireContext(), null)
|
||||
dataView.view = chart!!
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package org.isoron.uhabits.activities.common.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.text.method.DigitsKeyListener
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import org.isoron.uhabits.HabitsApplication
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.models.Entry
|
||||
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
|
||||
import org.isoron.uhabits.utils.InterfaceUtils
|
||||
import org.isoron.uhabits.utils.requestFocusWithKeyboard
|
||||
import org.isoron.uhabits.utils.sres
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.text.NumberFormat
|
||||
import java.text.ParseException
|
||||
|
||||
class NumberDialog : AppCompatDialogFragment() {
|
||||
|
||||
var onToggle: (Double, String) -> Unit = { _, _ -> }
|
||||
var onDismiss: () -> Unit = {}
|
||||
|
||||
private var originalNotes: String = ""
|
||||
private var originalValue: Double = 0.0
|
||||
private lateinit var view: CheckmarkPopupBinding
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val appComponent = (requireActivity().application as HabitsApplication).component
|
||||
val prefs = appComponent.preferences
|
||||
view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context))
|
||||
arrayOf(view.yesBtn, view.skipBtn).forEach {
|
||||
it.setTextColor(requireArguments().getInt("color"))
|
||||
}
|
||||
arrayOf(view.noBtn, view.unknownBtn).forEach {
|
||||
it.setTextColor(view.root.sres.getColor(R.attr.contrast60))
|
||||
}
|
||||
arrayOf(view.yesBtn, view.noBtn, view.skipBtn, view.unknownBtn).forEach {
|
||||
it.typeface = InterfaceUtils.getFontAwesome(requireContext())
|
||||
}
|
||||
if (!prefs.isSkipEnabled) view.skipBtnNumber.visibility = View.GONE
|
||||
view.numberButtons.visibility = View.VISIBLE
|
||||
fixDecimalSeparator(view)
|
||||
originalNotes = requireArguments().getString("notes")!!
|
||||
originalValue = requireArguments().getDouble("value")
|
||||
view.notes.setText(originalNotes)
|
||||
view.value.setText(
|
||||
when {
|
||||
originalValue < 0.01 -> "0"
|
||||
else -> DecimalFormat("#.##").format(originalValue)
|
||||
}
|
||||
)
|
||||
view.value.setOnKeyListener { _, keyCode, event ->
|
||||
if (event.action == MotionEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
|
||||
save()
|
||||
return@setOnKeyListener true
|
||||
}
|
||||
return@setOnKeyListener false
|
||||
}
|
||||
view.saveBtn.setOnClickListener {
|
||||
save()
|
||||
}
|
||||
view.skipBtnNumber.setOnClickListener {
|
||||
view.value.setText((Entry.SKIP.toDouble() / 1000).toString())
|
||||
save()
|
||||
}
|
||||
view.notes.setOnEditorActionListener { v, actionId, event ->
|
||||
save()
|
||||
true
|
||||
}
|
||||
view.value.requestFocusWithKeyboard()
|
||||
val dialog = Dialog(requireContext())
|
||||
dialog.setContentView(view.root)
|
||||
dialog.window?.apply {
|
||||
setBackgroundDrawableResource(android.R.color.transparent)
|
||||
}
|
||||
dialog.setOnDismissListener { onDismiss() }
|
||||
return dialog
|
||||
}
|
||||
|
||||
private fun fixDecimalSeparator(view: CheckmarkPopupBinding) {
|
||||
// https://stackoverflow.com/a/34256139
|
||||
val separator = DecimalFormatSymbols.getInstance().decimalSeparator
|
||||
view.value.keyListener = DigitsKeyListener.getInstance("0123456789$separator")
|
||||
}
|
||||
|
||||
fun save() {
|
||||
var value = originalValue
|
||||
try {
|
||||
val numberFormat = NumberFormat.getInstance()
|
||||
val valueStr = view.value.text.toString()
|
||||
value = numberFormat.parse(valueStr)!!.toDouble()
|
||||
} catch (e: ParseException) {
|
||||
// NOP
|
||||
}
|
||||
val notes = view.notes.text.toString()
|
||||
onToggle(value, notes)
|
||||
requireDialog().dismiss()
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.view.KeyEvent.KEYCODE_ENTER
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent.ACTION_DOWN
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
import org.isoron.uhabits.core.models.Entry
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
|
||||
import org.isoron.uhabits.utils.dimBehind
|
||||
import org.isoron.uhabits.utils.dismissCurrentAndShow
|
||||
import org.isoron.uhabits.utils.dp
|
||||
import org.isoron.uhabits.utils.requestFocusWithKeyboard
|
||||
import java.text.DecimalFormat
|
||||
|
||||
class NumberPopup(
|
||||
private val context: Context,
|
||||
private var notes: String,
|
||||
private var value: Double,
|
||||
private val prefs: Preferences,
|
||||
private val anchor: View,
|
||||
) {
|
||||
var onToggle: (Double, String) -> Unit = { _, _ -> }
|
||||
var onDismiss: () -> Unit = {}
|
||||
private val originalValue = value
|
||||
private lateinit var dialog: Dialog
|
||||
|
||||
private val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context)).apply {
|
||||
// Required for round corners
|
||||
container.clipToOutline = true
|
||||
}
|
||||
|
||||
init {
|
||||
view.numberButtons.visibility = VISIBLE
|
||||
hideDisabledButtons()
|
||||
populate()
|
||||
}
|
||||
|
||||
private fun hideDisabledButtons() {
|
||||
if (!prefs.isSkipEnabled) view.skipBtnNumber.visibility = GONE
|
||||
}
|
||||
|
||||
private fun populate() {
|
||||
view.notes.setText(notes)
|
||||
view.value.setText(
|
||||
when {
|
||||
value < 0.01 -> "0"
|
||||
else -> DecimalFormat("#.##").format(value)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun show() {
|
||||
dialog = Dialog(context, android.R.style.Theme_NoTitleBar)
|
||||
dialog.setContentView(view.root)
|
||||
dialog.window?.apply {
|
||||
setLayout(
|
||||
view.root.dp(POPUP_WIDTH).toInt(),
|
||||
view.root.dp(POPUP_HEIGHT).toInt()
|
||||
)
|
||||
setBackgroundDrawableResource(android.R.color.transparent)
|
||||
}
|
||||
dialog.setOnDismissListener {
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
view.value.setOnKeyListener { _, keyCode, event ->
|
||||
if (event.action == ACTION_DOWN && keyCode == KEYCODE_ENTER) {
|
||||
save()
|
||||
return@setOnKeyListener true
|
||||
}
|
||||
return@setOnKeyListener false
|
||||
}
|
||||
view.saveBtn.setOnClickListener {
|
||||
save()
|
||||
}
|
||||
view.skipBtnNumber.setOnClickListener {
|
||||
view.value.setText((Entry.SKIP.toDouble() / 1000).toString())
|
||||
save()
|
||||
}
|
||||
view.value.requestFocusWithKeyboard()
|
||||
dialog.setCanceledOnTouchOutside(true)
|
||||
dialog.dimBehind()
|
||||
dialog.dismissCurrentAndShow()
|
||||
}
|
||||
|
||||
fun save() {
|
||||
val value = view.value.text.toString().toDoubleOrNull() ?: originalValue
|
||||
val notes = view.notes.text.toString()
|
||||
onToggle(value, notes)
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
@@ -181,8 +181,9 @@ class FrequencyChart : ScrollableChart {
|
||||
rect[0f, 0f, baseSize.toFloat()] = baseSize.toFloat()
|
||||
rect.offset(prevRect!!.left, prevRect!!.top + baseSize * j)
|
||||
val i = localeWeekdayList[j] % 7
|
||||
if (values != null)
|
||||
if (values != null) {
|
||||
drawMarker(canvas, rect, values[i], weekDaysInMonth[i])
|
||||
}
|
||||
rect.offset(0f, rowHeight)
|
||||
}
|
||||
drawFooter(canvas, rect, date)
|
||||
@@ -196,12 +197,14 @@ class FrequencyChart : ScrollableChart {
|
||||
rect.centerY() - 0.1f * em,
|
||||
pText!!
|
||||
)
|
||||
if (date[Calendar.MONTH] == 1) canvas.drawText(
|
||||
dfYear!!.format(time),
|
||||
rect.centerX(),
|
||||
rect.centerY() + 0.9f * em,
|
||||
pText!!
|
||||
)
|
||||
if (date[Calendar.MONTH] == 1) {
|
||||
canvas.drawText(
|
||||
dfYear!!.format(time),
|
||||
rect.centerX(),
|
||||
rect.centerY() + 0.9f * em,
|
||||
pText!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawGrid(canvas: Canvas, rGrid: RectF?) {
|
||||
|
||||
@@ -148,8 +148,12 @@ class RingView : View {
|
||||
pRing!!.color = inactiveColor!!
|
||||
activeCanvas.drawArc(rect!!, angle - 90, 360 - angle, true, pRing!!)
|
||||
if (thickness > 0) {
|
||||
if (isTransparencyEnabled) pRing!!.xfermode = XFERMODE_CLEAR else pRing!!.color =
|
||||
backgroundColor!!
|
||||
if (isTransparencyEnabled) {
|
||||
pRing!!.xfermode = XFERMODE_CLEAR
|
||||
} else {
|
||||
pRing!!.color =
|
||||
backgroundColor!!
|
||||
}
|
||||
rect!!.inset(thickness, thickness)
|
||||
activeCanvas.drawArc(rect!!, 0f, 360f, true, pRing!!)
|
||||
pRing!!.xfermode = null
|
||||
|
||||
@@ -40,6 +40,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.dismissCurrentDialog
|
||||
import org.isoron.uhabits.utils.restartWithFade
|
||||
|
||||
class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
||||
@@ -91,6 +92,7 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
||||
midnightTimer.onPause()
|
||||
screen.onDetached()
|
||||
adapter.cancelRefresh()
|
||||
dismissCurrentDialog()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
@@ -99,6 +101,7 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
||||
screen.onAttached()
|
||||
rootView.postInvalidate()
|
||||
midnightTimer.onResume()
|
||||
appComponent.reminderScheduler.scheduleAll()
|
||||
taskRunner.run {
|
||||
try {
|
||||
AutoBackup(this@ListHabitsActivity).run()
|
||||
|
||||
@@ -94,7 +94,7 @@ class ListHabitsRootView @Inject constructor(
|
||||
title = resources.getString(R.string.main_activity_title),
|
||||
color = PaletteColor(17),
|
||||
displayHomeAsUpEnabled = false,
|
||||
theme = currentTheme(),
|
||||
theme = currentTheme()
|
||||
)
|
||||
addView(rootView, MATCH_PARENT, MATCH_PARENT)
|
||||
listAdapter.setListView(listView)
|
||||
|
||||
@@ -22,14 +22,15 @@ package org.isoron.uhabits.activities.habits.list
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import dagger.Lazy
|
||||
import org.isoron.platform.gui.toInt
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.activities.common.dialogs.CheckmarkPopup
|
||||
import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
|
||||
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.NumberPopup
|
||||
import org.isoron.uhabits.activities.common.dialogs.NumberDialog
|
||||
import org.isoron.uhabits.activities.habits.edit.HabitTypeDialog
|
||||
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
|
||||
import org.isoron.uhabits.core.commands.ArchiveHabitsCommand
|
||||
@@ -95,7 +96,7 @@ class ListHabitsScreen
|
||||
private val colorPickerFactory: ColorPickerDialogFactory,
|
||||
private val behavior: Lazy<ListHabitsBehavior>,
|
||||
private val preferences: Preferences,
|
||||
private val rootView: Lazy<ListHabitsRootView>,
|
||||
private val rootView: Lazy<ListHabitsRootView>
|
||||
) : CommandRunner.Listener,
|
||||
ListHabitsBehavior.Screen,
|
||||
ListHabitsMenuBehavior.Screen,
|
||||
@@ -233,17 +234,14 @@ class ListHabitsScreen
|
||||
notes: String,
|
||||
callback: ListHabitsBehavior.NumberPickerCallback
|
||||
) {
|
||||
val view = rootView.get()
|
||||
NumberPopup(
|
||||
context = context,
|
||||
prefs = preferences,
|
||||
anchor = view,
|
||||
notes = notes,
|
||||
value = value,
|
||||
).apply {
|
||||
onToggle = { value, notes -> callback.onNumberPicked(value, notes) }
|
||||
show()
|
||||
val fm = (context as AppCompatActivity).supportFragmentManager
|
||||
val dialog = NumberDialog()
|
||||
dialog.arguments = Bundle().apply {
|
||||
putDouble("value", value)
|
||||
putString("notes", notes)
|
||||
}
|
||||
dialog.onToggle = { v, n -> callback.onNumberPicked(v, n) }
|
||||
dialog.dismissCurrentAndShow(fm, "numberDialog")
|
||||
}
|
||||
|
||||
override fun showCheckmarkPopup(
|
||||
@@ -252,18 +250,16 @@ class ListHabitsScreen
|
||||
color: PaletteColor,
|
||||
callback: ListHabitsBehavior.CheckMarkDialogCallback
|
||||
) {
|
||||
val view = rootView.get()
|
||||
CheckmarkPopup(
|
||||
context = context,
|
||||
prefs = preferences,
|
||||
anchor = view,
|
||||
color = view.currentTheme().color(color).toInt(),
|
||||
notes = notes,
|
||||
value = selectedValue,
|
||||
).apply {
|
||||
onToggle = { value, notes -> callback.onNotesSaved(value, notes) }
|
||||
show()
|
||||
val theme = rootView.get().currentTheme()
|
||||
val fm = (context as AppCompatActivity).supportFragmentManager
|
||||
val dialog = CheckmarkDialog()
|
||||
dialog.arguments = Bundle().apply {
|
||||
putInt("color", theme.color(color).toInt())
|
||||
putInt("value", selectedValue)
|
||||
putString("notes", notes)
|
||||
}
|
||||
dialog.onToggle = { v, n -> callback.onNotesSaved(v, n) }
|
||||
dialog.dismissCurrentAndShow(fm, "checkmarkDialog")
|
||||
}
|
||||
|
||||
private fun getExecuteString(command: Command): String? {
|
||||
@@ -325,8 +321,11 @@ class ListHabitsScreen
|
||||
private fun onExportDB() {
|
||||
taskRunner.execute(
|
||||
exportDBFactory.create { filename ->
|
||||
if (filename != null) activity.showSendFileScreen(filename)
|
||||
else activity.showMessage(activity.resources.getString(R.string.could_not_export))
|
||||
if (filename != null) {
|
||||
activity.showSendFileScreen(filename)
|
||||
} else {
|
||||
activity.showMessage(activity.resources.getString(R.string.could_not_export))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -60,8 +60,11 @@ abstract class ButtonPanelView<T : View>(
|
||||
repeat(buttonCount) { buttons.add(createButton()) }
|
||||
|
||||
removeAllViews()
|
||||
if (reverse) buttons.reversed().forEach { addView(it) }
|
||||
else buttons.forEach { addView(it) }
|
||||
if (reverse) {
|
||||
buttons.reversed().forEach { addView(it) }
|
||||
} else {
|
||||
buttons.forEach { addView(it) }
|
||||
}
|
||||
setupButtons()
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
@@ -44,8 +44,6 @@ import org.isoron.uhabits.utils.sres
|
||||
import org.isoron.uhabits.utils.toMeasureSpec
|
||||
import javax.inject.Inject
|
||||
|
||||
const val TOGGLE_DELAY_MILLIS = 2000L
|
||||
|
||||
class CheckmarkButtonViewFactory
|
||||
@Inject constructor(
|
||||
@ActivityContext val context: Context,
|
||||
@@ -79,7 +77,7 @@ class CheckmarkButtonView(
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var onToggle: (Int, String, Long) -> Unit = { _, _, _ -> }
|
||||
var onToggle: (Int, String) -> Unit = { _, _ -> }
|
||||
|
||||
var onEdit: () -> Unit = { }
|
||||
|
||||
@@ -90,25 +88,31 @@ class CheckmarkButtonView(
|
||||
setOnLongClickListener(this)
|
||||
}
|
||||
|
||||
fun performToggle(delay: Long) {
|
||||
fun performToggle() {
|
||||
value = Entry.nextToggleValue(
|
||||
value = value,
|
||||
isSkipEnabled = preferences.isSkipEnabled,
|
||||
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
|
||||
)
|
||||
onToggle(value, notes, delay)
|
||||
onToggle(value, notes)
|
||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
if (preferences.isShortToggleEnabled) performToggle(TOGGLE_DELAY_MILLIS)
|
||||
else onEdit()
|
||||
if (preferences.isShortToggleEnabled) {
|
||||
performToggle()
|
||||
} else {
|
||||
onEdit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLongClick(v: View): Boolean {
|
||||
if (preferences.isShortToggleEnabled) onEdit()
|
||||
else performToggle(TOGGLE_DELAY_MILLIS)
|
||||
if (preferences.isShortToggleEnabled) {
|
||||
onEdit()
|
||||
} else {
|
||||
performToggle()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -142,8 +146,11 @@ class CheckmarkButtonView(
|
||||
paint.color = when (value) {
|
||||
YES_MANUAL, YES_AUTO, SKIP -> color
|
||||
NO -> {
|
||||
if (preferences.areQuestionMarksEnabled) mediumContrastColor
|
||||
else lowContrastColor
|
||||
if (preferences.areQuestionMarksEnabled) {
|
||||
mediumContrastColor
|
||||
} else {
|
||||
lowContrastColor
|
||||
}
|
||||
}
|
||||
else -> lowContrastColor
|
||||
}
|
||||
@@ -151,8 +158,11 @@ class CheckmarkButtonView(
|
||||
SKIP -> R.string.fa_skipped
|
||||
NO -> R.string.fa_times
|
||||
UNKNOWN -> {
|
||||
if (preferences.areQuestionMarksEnabled) R.string.fa_question
|
||||
else R.string.fa_times
|
||||
if (preferences.areQuestionMarksEnabled) {
|
||||
R.string.fa_question
|
||||
} else {
|
||||
R.string.fa_times
|
||||
}
|
||||
}
|
||||
else -> R.string.fa_check
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ class CheckmarkPanelView(
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var onToggle: (Timestamp, Int, String, Long) -> Unit = { _, _, _, _ -> }
|
||||
var onToggle: (Timestamp, Int, String) -> Unit = { _, _, _ -> }
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
@@ -89,7 +89,7 @@ class CheckmarkPanelView(
|
||||
else -> ""
|
||||
}
|
||||
button.color = color
|
||||
button.onToggle = { value, notes, delay -> onToggle(timestamp, value, notes, delay) }
|
||||
button.onToggle = { value, notes -> onToggle(timestamp, value, notes) }
|
||||
button.onEdit = { onEdit(timestamp) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,10 +156,11 @@ class HabitCardListController @Inject constructor(
|
||||
}
|
||||
|
||||
private fun notifyListener() {
|
||||
if (activeMode is SelectionMode)
|
||||
if (activeMode is SelectionMode) {
|
||||
selectionMenu.get().onSelectionChange()
|
||||
else
|
||||
} else {
|
||||
selectionMenu.get().onSelectionFinish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,13 +57,6 @@ class HabitCardViewFactory
|
||||
fun create() = HabitCardView(context, checkmarkPanelFactory, numberPanelFactory, behavior)
|
||||
}
|
||||
|
||||
data class DelayedToggle(
|
||||
var habit: Habit,
|
||||
var timestamp: Timestamp,
|
||||
var value: Int,
|
||||
var notes: String
|
||||
)
|
||||
|
||||
class HabitCardView(
|
||||
@ActivityContext context: Context,
|
||||
checkmarkPanelFactory: CheckmarkPanelViewFactory,
|
||||
@@ -136,7 +129,6 @@ class HabitCardView(
|
||||
private var scoreRing: RingView
|
||||
|
||||
private var currentToggleTaskId = 0
|
||||
private var queuedToggles = mutableListOf<DelayedToggle>()
|
||||
|
||||
init {
|
||||
scoreRing = RingView(context).apply {
|
||||
@@ -160,12 +152,9 @@ class HabitCardView(
|
||||
}
|
||||
|
||||
checkmarkPanel = checkmarkPanelFactory.create().apply {
|
||||
onToggle = { timestamp, value, notes, delay ->
|
||||
if (delay > 0) triggerRipple(timestamp)
|
||||
habit?.let {
|
||||
val taskId = queueToggle(it, timestamp, value, notes);
|
||||
{ runPendingToggles(taskId) }.delay(delay)
|
||||
}
|
||||
onToggle = { timestamp, value, notes ->
|
||||
triggerRipple(timestamp)
|
||||
habit?.let { behavior.onToggle(it, timestamp, value, notes) }
|
||||
}
|
||||
onEdit = { timestamp ->
|
||||
triggerRipple(timestamp)
|
||||
@@ -205,25 +194,6 @@ class HabitCardView(
|
||||
addView(innerFrame)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun runPendingToggles(id: Int) {
|
||||
if (currentToggleTaskId != id) return
|
||||
for ((h, t, v, n) in queuedToggles) behavior.onToggle(h, t, v, n)
|
||||
queuedToggles.clear()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun queueToggle(
|
||||
it: Habit,
|
||||
timestamp: Timestamp,
|
||||
value: Int,
|
||||
notes: String,
|
||||
): Int {
|
||||
currentToggleTaskId += 1
|
||||
queuedToggles.add(DelayedToggle(it, timestamp, value, notes))
|
||||
return currentToggleTaskId
|
||||
}
|
||||
|
||||
override fun onModelChange() {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
habit?.let { copyAttributesFrom(it) }
|
||||
@@ -255,7 +225,6 @@ class HabitCardView(
|
||||
}
|
||||
|
||||
private fun copyAttributesFrom(h: Habit) {
|
||||
|
||||
fun getActiveColor(habit: Habit): Int {
|
||||
return when (habit.isArchived) {
|
||||
true -> sres.getColor(R.attr.contrast60)
|
||||
@@ -301,7 +270,6 @@ class HabitCardView(
|
||||
}
|
||||
|
||||
private fun updateBackground(isSelected: Boolean) {
|
||||
|
||||
val background = when (isSelected) {
|
||||
true -> R.drawable.selected_box
|
||||
false -> R.drawable.ripple
|
||||
|
||||
@@ -124,15 +124,20 @@ class HeaderView(
|
||||
rect.set(0f, 0f, width, height)
|
||||
rect.offset(canvas.width.toFloat() - dp(3.0f), 0f)
|
||||
|
||||
if (isReversed) rect.offset(-(index + 1) * width, 0f)
|
||||
else rect.offset((index - buttonCount) * width, 0f)
|
||||
if (isReversed) {
|
||||
rect.offset(-(index + 1) * width, 0f)
|
||||
} else {
|
||||
rect.offset((index - buttonCount) * width, 0f)
|
||||
}
|
||||
|
||||
if (isRTL()) rect.set(
|
||||
canvas.width - rect.right,
|
||||
rect.top,
|
||||
canvas.width - rect.left,
|
||||
rect.bottom
|
||||
)
|
||||
if (isRTL()) {
|
||||
rect.set(
|
||||
canvas.width - rect.right,
|
||||
rect.top,
|
||||
canvas.width - rect.left,
|
||||
rect.bottom
|
||||
)
|
||||
}
|
||||
|
||||
val y1 = rect.centerY() - 0.25 * em
|
||||
val y2 = rect.centerY() + 1.25 * em
|
||||
|
||||
@@ -34,10 +34,10 @@ import org.isoron.uhabits.HabitsApplication
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
||||
import org.isoron.uhabits.activities.HabitsDirFinder
|
||||
import org.isoron.uhabits.activities.common.dialogs.CheckmarkPopup
|
||||
import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.NumberPopup
|
||||
import org.isoron.uhabits.activities.common.dialogs.NumberDialog
|
||||
import org.isoron.uhabits.core.commands.Command
|
||||
import org.isoron.uhabits.core.commands.CommandRunner
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
@@ -51,6 +51,7 @@ import org.isoron.uhabits.core.ui.views.OnDateClickedListener
|
||||
import org.isoron.uhabits.intents.IntentFactory
|
||||
import org.isoron.uhabits.utils.currentTheme
|
||||
import org.isoron.uhabits.utils.dismissCurrentAndShow
|
||||
import org.isoron.uhabits.utils.dismissCurrentDialog
|
||||
import org.isoron.uhabits.utils.showMessage
|
||||
import org.isoron.uhabits.utils.showSendFileScreen
|
||||
import org.isoron.uhabits.widgets.WidgetUpdater
|
||||
@@ -87,7 +88,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
||||
habit = habit,
|
||||
habitList = habitList,
|
||||
preferences = preferences,
|
||||
screen = screen,
|
||||
screen = screen
|
||||
)
|
||||
|
||||
view = ShowHabitView(this)
|
||||
@@ -98,13 +99,13 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
||||
habitList = habitList,
|
||||
screen = screen,
|
||||
system = HabitsDirFinder(AndroidDirFinder(this)),
|
||||
taskRunner = appComponent.taskRunner,
|
||||
taskRunner = appComponent.taskRunner
|
||||
)
|
||||
|
||||
menu = ShowHabitMenu(
|
||||
activity = this,
|
||||
presenter = menuPresenter,
|
||||
preferences = preferences,
|
||||
preferences = preferences
|
||||
)
|
||||
|
||||
view.setListener(presenter)
|
||||
@@ -129,6 +130,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
dismissCurrentDialog()
|
||||
commandRunner.removeListener(this)
|
||||
super.onPause()
|
||||
}
|
||||
@@ -148,7 +150,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
||||
ShowHabitPresenter.buildState(
|
||||
habit = habit,
|
||||
preferences = preferences,
|
||||
theme = themeSwitcher.currentTheme,
|
||||
theme = themeSwitcher.currentTheme
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -170,41 +172,32 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
||||
override fun showNumberPopup(
|
||||
value: Double,
|
||||
notes: String,
|
||||
preferences: Preferences,
|
||||
callback: ListHabitsBehavior.NumberPickerCallback
|
||||
) {
|
||||
val anchor = getPopupAnchor() ?: return
|
||||
NumberPopup(
|
||||
context = this@ShowHabitActivity,
|
||||
prefs = preferences,
|
||||
notes = notes,
|
||||
anchor = anchor,
|
||||
value = value,
|
||||
).apply {
|
||||
onToggle = { v, n -> callback.onNumberPicked(v, n) }
|
||||
show()
|
||||
val dialog = NumberDialog()
|
||||
dialog.arguments = Bundle().apply {
|
||||
putDouble("value", value)
|
||||
putString("notes", notes)
|
||||
}
|
||||
dialog.onToggle = { v, n -> callback.onNumberPicked(v, n) }
|
||||
dialog.dismissCurrentAndShow(supportFragmentManager, "numberDialog")
|
||||
}
|
||||
|
||||
override fun showCheckmarkPopup(
|
||||
selectedValue: Int,
|
||||
notes: String,
|
||||
preferences: Preferences,
|
||||
color: PaletteColor,
|
||||
callback: ListHabitsBehavior.CheckMarkDialogCallback
|
||||
) {
|
||||
val anchor = getPopupAnchor() ?: return
|
||||
CheckmarkPopup(
|
||||
context = this@ShowHabitActivity,
|
||||
prefs = preferences,
|
||||
notes = notes,
|
||||
color = view.currentTheme().color(color).toInt(),
|
||||
anchor = anchor,
|
||||
value = selectedValue,
|
||||
).apply {
|
||||
onToggle = { v, n -> callback.onNotesSaved(v, n) }
|
||||
show()
|
||||
val theme = view.currentTheme()
|
||||
val dialog = CheckmarkDialog()
|
||||
dialog.arguments = Bundle().apply {
|
||||
putInt("color", theme.color(color).toInt())
|
||||
putInt("value", selectedValue)
|
||||
putString("notes", notes)
|
||||
}
|
||||
dialog.onToggle = { v, n -> callback.onNotesSaved(v, n) }
|
||||
dialog.dismissCurrentAndShow(supportFragmentManager, "checkmarkDialog")
|
||||
}
|
||||
|
||||
private fun getPopupAnchor(): View? {
|
||||
|
||||
@@ -28,7 +28,7 @@ import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitMenuPresenter
|
||||
class ShowHabitMenu(
|
||||
val activity: ShowHabitActivity,
|
||||
val presenter: ShowHabitMenuPresenter,
|
||||
val preferences: Preferences,
|
||||
val preferences: Preferences
|
||||
) {
|
||||
fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
activity.menuInflater.inflate(R.menu.show_habit, menu)
|
||||
|
||||
@@ -39,7 +39,7 @@ class ShowHabitView(context: Context) : FrameLayout(context) {
|
||||
binding.toolbar,
|
||||
title = data.title,
|
||||
color = data.color,
|
||||
theme = data.theme,
|
||||
theme = data.theme
|
||||
)
|
||||
binding.subtitleCard.setState(data.subtitle)
|
||||
binding.overviewCard.setState(data.overview)
|
||||
|
||||
@@ -63,7 +63,7 @@ class BarCardView(context: Context, attrs: AttributeSet) : LinearLayout(context,
|
||||
parent: AdapterView<*>?,
|
||||
view: View?,
|
||||
position: Int,
|
||||
id: Long,
|
||||
id: Long
|
||||
) {
|
||||
presenter.onBoolSpinnerPosition(position)
|
||||
}
|
||||
@@ -77,7 +77,7 @@ class BarCardView(context: Context, attrs: AttributeSet) : LinearLayout(context,
|
||||
parent: AdapterView<*>?,
|
||||
view: View?,
|
||||
position: Int,
|
||||
id: Long,
|
||||
id: Long
|
||||
) {
|
||||
presenter.onNumericalSpinnerPosition(position)
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class HistoryCardView(context: Context, attrs: AttributeSet) : LinearLayout(cont
|
||||
series = state.series,
|
||||
defaultSquare = state.defaultSquare,
|
||||
notesIndicators = state.notesIndicators,
|
||||
firstWeekday = state.firstWeekday,
|
||||
firstWeekday = state.firstWeekday
|
||||
)
|
||||
binding.chart.postInvalidate()
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
|
||||
binding.frequencyLabel.text = formatFrequency(
|
||||
state.frequency.numerator,
|
||||
state.frequency.denominator,
|
||||
resources,
|
||||
resources
|
||||
)
|
||||
binding.questionLabel.setTextColor(color)
|
||||
binding.questionLabel.text = state.question
|
||||
|
||||
@@ -40,7 +40,7 @@ class SettingsActivity : AppCompatActivity() {
|
||||
toolbar = binding.toolbar,
|
||||
title = resources.getString(R.string.settings),
|
||||
color = PaletteColor(11),
|
||||
theme = themeSwitcher.currentTheme,
|
||||
theme = themeSwitcher.currentTheme
|
||||
)
|
||||
setContentView(binding.root)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import android.app.backup.BackupManager
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
@@ -43,6 +44,7 @@ import org.isoron.uhabits.core.utils.DateUtils.Companion.getLongWeekdayNames
|
||||
import org.isoron.uhabits.notifications.AndroidNotificationTray.Companion.createAndroidNotificationChannel
|
||||
import org.isoron.uhabits.notifications.RingtoneManager
|
||||
import org.isoron.uhabits.utils.StyledResources
|
||||
import org.isoron.uhabits.utils.startActivitySafely
|
||||
import org.isoron.uhabits.widgets.WidgetUpdater
|
||||
import java.util.Calendar
|
||||
|
||||
@@ -92,16 +94,24 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
val key = preference.key ?: return false
|
||||
if (key == "reminderSound") {
|
||||
showRingtonePicker()
|
||||
return true
|
||||
} else if (key == "reminderCustomize") {
|
||||
createAndroidNotificationChannel(requireContext())
|
||||
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
|
||||
intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationTray.REMINDERS_CHANNEL_ID)
|
||||
startActivity(intent)
|
||||
return true
|
||||
when (key) {
|
||||
"reminderSound" -> {
|
||||
showRingtonePicker()
|
||||
return true
|
||||
}
|
||||
"reminderCustomize" -> {
|
||||
createAndroidNotificationChannel(requireContext())
|
||||
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
|
||||
intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationTray.REMINDERS_CHANNEL_ID)
|
||||
startActivity(intent)
|
||||
return true
|
||||
}
|
||||
"rateApp" -> {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.playStoreURL)))
|
||||
activity?.startActivitySafely(intent)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class EditSettingActivity : AppCompatActivity() {
|
||||
val habits = app.component.habitList.getFiltered(
|
||||
HabitMatcher(
|
||||
isArchivedAllowed = false,
|
||||
isCompletedAllowed = true,
|
||||
isCompletedAllowed = true
|
||||
)
|
||||
)
|
||||
AndroidThemeSwitcher(this, app.component.preferences).apply()
|
||||
@@ -43,7 +43,7 @@ class EditSettingActivity : AppCompatActivity() {
|
||||
context = this,
|
||||
habitList = app.component.habitList,
|
||||
onSave = controller::onSave,
|
||||
args = args,
|
||||
args = args
|
||||
)
|
||||
setContentView(view)
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class EditSettingRootView(
|
||||
title = resources.getString(R.string.app_name),
|
||||
color = PaletteColor(11),
|
||||
displayHomeAsUpEnabled = false,
|
||||
theme = currentTheme(),
|
||||
theme = currentTheme()
|
||||
)
|
||||
populateHabitSpinner()
|
||||
binding.habitSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
@@ -69,7 +69,7 @@ class EditSettingRootView(
|
||||
val habit = habitList.getByPosition(binding.habitSpinner.selectedItemPosition)
|
||||
val action = mapSpinnerPositionToAction(
|
||||
isNumerical = habit.isNumerical,
|
||||
itemPosition = binding.actionSpinner.selectedItemPosition,
|
||||
itemPosition = binding.actionSpinner.selectedItemPosition
|
||||
)
|
||||
onSave(habit, action)
|
||||
}
|
||||
|
||||
@@ -27,22 +27,34 @@ class AndroidCursor(private val cursor: android.database.Cursor) : Cursor {
|
||||
override fun moveToNext() = cursor.moveToNext()
|
||||
|
||||
override fun getInt(index: Int): Int? {
|
||||
return if (cursor.isNull(index)) null
|
||||
else cursor.getInt(index)
|
||||
return if (cursor.isNull(index)) {
|
||||
null
|
||||
} else {
|
||||
cursor.getInt(index)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLong(index: Int): Long? {
|
||||
return if (cursor.isNull(index)) null
|
||||
else cursor.getLong(index)
|
||||
return if (cursor.isNull(index)) {
|
||||
null
|
||||
} else {
|
||||
cursor.getLong(index)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDouble(index: Int): Double? {
|
||||
return if (cursor.isNull(index)) null
|
||||
else cursor.getDouble(index)
|
||||
return if (cursor.isNull(index)) {
|
||||
null
|
||||
} else {
|
||||
cursor.getDouble(index)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getString(index: Int): String? {
|
||||
return if (cursor.isNull(index)) null
|
||||
else cursor.getString(index)
|
||||
return if (cursor.isNull(index)) {
|
||||
null
|
||||
} else {
|
||||
cursor.getString(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import java.io.File
|
||||
|
||||
class AndroidDatabase(
|
||||
private val db: SQLiteDatabase,
|
||||
override val file: File?,
|
||||
override val file: File?
|
||||
) : Database {
|
||||
|
||||
override fun beginTransaction() = db.beginTransaction()
|
||||
@@ -45,7 +45,7 @@ class AndroidDatabase(
|
||||
tableName: String,
|
||||
values: Map<String, Any?>,
|
||||
where: String,
|
||||
vararg params: String,
|
||||
vararg params: String
|
||||
): Int {
|
||||
val contValues = mapToContentValues(values)
|
||||
return db.update(tableName, contValues, where, params)
|
||||
@@ -59,7 +59,7 @@ class AndroidDatabase(
|
||||
override fun delete(
|
||||
tableName: String,
|
||||
where: String,
|
||||
vararg params: String,
|
||||
vararg params: String
|
||||
) {
|
||||
db.delete(tableName, where, params)
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ class AndroidDatabaseOpener @Inject constructor() : DatabaseOpener {
|
||||
db = SQLiteDatabase.openDatabase(
|
||||
file.absolutePath,
|
||||
null,
|
||||
SQLiteDatabase.OPEN_READWRITE,
|
||||
SQLiteDatabase.OPEN_READWRITE
|
||||
),
|
||||
file = file
|
||||
)
|
||||
|
||||
@@ -25,5 +25,6 @@ import dagger.Provides
|
||||
@Module
|
||||
class ActivityContextModule(
|
||||
@get:Provides
|
||||
@get:ActivityContext val context: Context
|
||||
@get:ActivityContext
|
||||
val context: Context
|
||||
)
|
||||
|
||||
@@ -26,5 +26,6 @@ import dagger.Provides
|
||||
class AppContextModule(
|
||||
@get:Provides
|
||||
@get:AppContext
|
||||
@param:AppContext val context: Context
|
||||
@param:AppContext
|
||||
val context: Context
|
||||
)
|
||||
|
||||
@@ -53,8 +53,9 @@ class IntentParser
|
||||
var timestamp = intent.getLongExtra("timestamp", today)
|
||||
timestamp = DateUtils.getStartOfDay(timestamp)
|
||||
|
||||
if (timestamp < 0 || timestamp > today)
|
||||
if (timestamp < 0 || timestamp > today) {
|
||||
throw IllegalArgumentException("timestamp is not valid")
|
||||
}
|
||||
|
||||
return Timestamp(timestamp)
|
||||
}
|
||||
|
||||
@@ -96,7 +96,6 @@ class AndroidNotificationTray
|
||||
timestamp: Timestamp,
|
||||
disableSound: Boolean = false
|
||||
): Notification {
|
||||
|
||||
val addRepetitionAction = Action(
|
||||
R.drawable.ic_action_check,
|
||||
context.getString(R.string.yes),
|
||||
@@ -146,8 +145,9 @@ class AndroidNotificationTray
|
||||
.addAction(removeRepetitionAction)
|
||||
}
|
||||
|
||||
if (!disableSound)
|
||||
if (!disableSound) {
|
||||
builder.setSound(ringtoneManager.getURI())
|
||||
}
|
||||
|
||||
if (SDK_INT < Build.VERSION_CODES.S) {
|
||||
val snoozeAction = Action(
|
||||
|
||||
@@ -61,8 +61,9 @@ class RingtoneManager
|
||||
"pref_ringtone_uri",
|
||||
defaultRingtoneUri.toString()
|
||||
)!!
|
||||
if (prefRingtoneUri.isNotEmpty())
|
||||
if (prefRingtoneUri.isNotEmpty()) {
|
||||
ringtoneUri = Uri.parse(prefRingtoneUri)
|
||||
}
|
||||
|
||||
return ringtoneUri
|
||||
}
|
||||
|
||||
@@ -33,7 +33,8 @@ import org.isoron.uhabits.HabitsApplication
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.ui.ThemeSwitcher.Companion.THEME_LIGHT
|
||||
import org.isoron.uhabits.core.ui.views.DarkTheme
|
||||
import org.isoron.uhabits.core.ui.views.LightTheme
|
||||
import org.isoron.uhabits.receivers.ReminderController
|
||||
import org.isoron.uhabits.utils.SystemUtils
|
||||
import java.util.Calendar
|
||||
@@ -51,11 +52,8 @@ class SnoozeDelayPickerActivity : FragmentActivity(), OnItemClickListener {
|
||||
val app = applicationContext as HabitsApplication
|
||||
val appComponent = app.component
|
||||
val themeSwitcher = AndroidThemeSwitcher(this, appComponent.preferences)
|
||||
if (themeSwitcher.getSystemTheme() == THEME_LIGHT) {
|
||||
setTheme(R.style.BaseDialog)
|
||||
} else {
|
||||
setTheme(R.style.BaseDialogDark)
|
||||
}
|
||||
themeSwitcher.setTheme()
|
||||
|
||||
val data = intent.data
|
||||
if (data == null) {
|
||||
finish()
|
||||
@@ -75,6 +73,16 @@ class SnoozeDelayPickerActivity : FragmentActivity(), OnItemClickListener {
|
||||
SystemUtils.unlockScreen(this)
|
||||
}
|
||||
|
||||
private fun AndroidThemeSwitcher.setTheme() {
|
||||
if (this.isNightMode) {
|
||||
setTheme(R.style.BaseDialogDark)
|
||||
this.currentTheme = DarkTheme()
|
||||
} else {
|
||||
setTheme(R.style.BaseDialog)
|
||||
this.currentTheme = LightTheme()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showTimePicker() {
|
||||
val calendar = Calendar.getInstance()
|
||||
val dialog = TimePickerDialog.newInstance(
|
||||
@@ -95,7 +103,9 @@ class SnoozeDelayPickerActivity : FragmentActivity(), OnItemClickListener {
|
||||
if (snoozeValues[position] >= 0) {
|
||||
reminderController!!.onSnoozeDelayPicked(habit!!, snoozeValues[position])
|
||||
finish()
|
||||
} else showTimePicker()
|
||||
} else {
|
||||
showTimePicker()
|
||||
}
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
|
||||
@@ -27,7 +27,7 @@ import javax.inject.Inject
|
||||
class ExportDBTaskFactory
|
||||
@Inject constructor(
|
||||
@AppContext private val context: Context,
|
||||
private val system: AndroidDirFinder,
|
||||
private val system: AndroidDirFinder
|
||||
) {
|
||||
fun create(listener: ExportDBTask.Listener) = ExportDBTask(context, system, listener)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import javax.inject.Inject
|
||||
class ImportDataTaskFactory
|
||||
@Inject constructor(
|
||||
private val importer: GenericImporter,
|
||||
private val modelFactory: ModelFactory,
|
||||
private val modelFactory: ModelFactory
|
||||
) {
|
||||
fun create(file: File, listener: ImportDataTask.Listener) =
|
||||
ImportDataTask(importer, modelFactory, file, listener)
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.jetbrains.annotations.Contract
|
||||
|
||||
object AttributeSetUtils {
|
||||
const val ISORON_NAMESPACE = "http://isoron.org/android"
|
||||
|
||||
@JvmStatic
|
||||
fun getAttribute(
|
||||
context: Context,
|
||||
|
||||
@@ -33,6 +33,7 @@ import java.text.SimpleDateFormat
|
||||
|
||||
object DatabaseUtils {
|
||||
private var opener: HabitsDatabaseOpener? = null
|
||||
|
||||
@JvmStatic
|
||||
fun getDatabaseFile(context: Context): File {
|
||||
val databaseFilename = databaseFilename
|
||||
|
||||
@@ -6,16 +6,24 @@ import androidx.fragment.app.FragmentManager
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
var currentDialog: WeakReference<Dialog> = WeakReference(null)
|
||||
var currentDialogFragment: WeakReference<DialogFragment> = WeakReference(null)
|
||||
|
||||
fun dismissCurrentDialog() {
|
||||
currentDialog.get()?.dismiss()
|
||||
currentDialog = WeakReference(null)
|
||||
currentDialogFragment.get()?.dismiss()
|
||||
currentDialogFragment = WeakReference(null)
|
||||
}
|
||||
|
||||
fun Dialog.dismissCurrentAndShow() {
|
||||
currentDialog.get()?.dismiss()
|
||||
dismissCurrentDialog()
|
||||
currentDialog = WeakReference(this)
|
||||
show()
|
||||
}
|
||||
|
||||
fun DialogFragment.dismissCurrentAndShow(fragmentManager: FragmentManager, tag: String) {
|
||||
currentDialog.get()?.dismiss()
|
||||
dismissCurrentDialog()
|
||||
currentDialogFragment = WeakReference(this)
|
||||
show(fragmentManager, tag)
|
||||
fragmentManager.executePendingTransactions()
|
||||
currentDialog = WeakReference(this.dialog)
|
||||
}
|
||||
|
||||
@@ -61,7 +61,6 @@ fun RelativeLayout.addBelow(
|
||||
height: Int = WRAP_CONTENT,
|
||||
applyCustomRules: (params: RelativeLayout.LayoutParams) -> Unit = {}
|
||||
) {
|
||||
|
||||
view.layoutParams = RelativeLayout.LayoutParams(width, height).apply {
|
||||
addRule(BELOW, subject.id)
|
||||
applyCustomRules(this)
|
||||
@@ -75,7 +74,6 @@ fun RelativeLayout.addAtBottom(
|
||||
width: Int = MATCH_PARENT,
|
||||
height: Int = WRAP_CONTENT
|
||||
) {
|
||||
|
||||
view.layoutParams = RelativeLayout.LayoutParams(width, height).apply {
|
||||
addRule(ALIGN_PARENT_BOTTOM)
|
||||
}
|
||||
@@ -88,7 +86,6 @@ fun RelativeLayout.addAtTop(
|
||||
width: Int = MATCH_PARENT,
|
||||
height: Int = WRAP_CONTENT
|
||||
) {
|
||||
|
||||
view.layoutParams = RelativeLayout.LayoutParams(width, height).apply {
|
||||
addRule(ALIGN_PARENT_TOP)
|
||||
}
|
||||
@@ -159,7 +156,7 @@ fun Activity.restartWithFade(cls: Class<*>?) {
|
||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
||||
startActivity(Intent(this, cls))
|
||||
},
|
||||
500,
|
||||
500
|
||||
) // HACK: Let the menu disappear first
|
||||
}
|
||||
|
||||
@@ -168,7 +165,7 @@ fun View.setupToolbar(
|
||||
title: String,
|
||||
color: PaletteColor,
|
||||
theme: Theme,
|
||||
displayHomeAsUpEnabled: Boolean = true,
|
||||
displayHomeAsUpEnabled: Boolean = true
|
||||
) {
|
||||
toolbar.elevation = InterfaceUtils.dpToPixels(context, 2f)
|
||||
val res = StyledResources(context)
|
||||
|
||||
@@ -147,10 +147,11 @@ abstract class BaseWidget(val context: Context, val id: Int, val stacked: Boolea
|
||||
|
||||
protected val preferedBackgroundAlpha: Int
|
||||
get() {
|
||||
return if (stacked)
|
||||
return if (stacked) {
|
||||
255
|
||||
else
|
||||
} else {
|
||||
prefs.widgetOpacity
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
|
||||
@@ -33,7 +33,7 @@ open class CheckmarkWidget(
|
||||
context: Context,
|
||||
widgetId: Int,
|
||||
protected val habit: Habit,
|
||||
stacked: Boolean = false,
|
||||
stacked: Boolean = false
|
||||
) : BaseWidget(context, widgetId, stacked) {
|
||||
|
||||
override val defaultHeight: Int = 125
|
||||
@@ -41,7 +41,7 @@ open class CheckmarkWidget(
|
||||
|
||||
override fun getOnClickPendingIntent(context: Context): PendingIntent? {
|
||||
return if (habit.isNumerical) {
|
||||
pendingIntentFactory.showNumberPicker(habit, DateUtils.getToday())
|
||||
pendingIntentFactory.showNumberPicker(habit, DateUtils.getTodayWithOffset())
|
||||
} else {
|
||||
pendingIntentFactory.toggleCheckmark(habit, null)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,10 @@ import android.content.Context
|
||||
class CheckmarkWidgetProvider : BaseWidgetProvider() {
|
||||
override fun getWidgetFromId(context: Context, id: Int): BaseWidget {
|
||||
val habits = getHabitsFromWidgetId(id)
|
||||
return if (habits.size == 1) CheckmarkWidget(context, id, habits[0])
|
||||
else StackWidget(context, id, StackWidgetType.CHECKMARK, habits)
|
||||
return if (habits.size == 1) {
|
||||
CheckmarkWidget(context, id, habits[0])
|
||||
} else {
|
||||
StackWidget(context, id, StackWidgetType.CHECKMARK, habits)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import org.isoron.uhabits.widgets.views.EmptyWidgetView
|
||||
class EmptyWidget(
|
||||
context: Context,
|
||||
widgetId: Int,
|
||||
stacked: Boolean = false,
|
||||
stacked: Boolean = false
|
||||
) : BaseWidget(context, widgetId, stacked) {
|
||||
override val defaultHeight: Int = 200
|
||||
override val defaultWidth: Int = 200
|
||||
|
||||
@@ -33,7 +33,7 @@ class FrequencyWidget(
|
||||
widgetId: Int,
|
||||
private val habit: Habit,
|
||||
private val firstWeekday: Int,
|
||||
stacked: Boolean = false,
|
||||
stacked: Boolean = false
|
||||
) : BaseWidget(context, widgetId, stacked) {
|
||||
override val defaultHeight: Int = 200
|
||||
override val defaultWidth: Int = 200
|
||||
|
||||
@@ -24,12 +24,15 @@ import android.content.Context
|
||||
class FrequencyWidgetProvider : BaseWidgetProvider() {
|
||||
override fun getWidgetFromId(context: Context, id: Int): BaseWidget {
|
||||
val habits = getHabitsFromWidgetId(id)
|
||||
return if (habits.size == 1) FrequencyWidget(
|
||||
context,
|
||||
id,
|
||||
habits[0],
|
||||
preferences.firstWeekdayInt
|
||||
)
|
||||
else StackWidget(context, id, StackWidgetType.FREQUENCY, habits)
|
||||
return if (habits.size == 1) {
|
||||
FrequencyWidget(
|
||||
context,
|
||||
id,
|
||||
habits[0],
|
||||
preferences.firstWeekdayInt
|
||||
)
|
||||
} else {
|
||||
StackWidget(context, id, StackWidgetType.FREQUENCY, habits)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class HistoryWidget(
|
||||
context: Context,
|
||||
id: Int,
|
||||
private val habit: Habit,
|
||||
stacked: Boolean = false,
|
||||
stacked: Boolean = false
|
||||
) : BaseWidget(context, id, stacked) {
|
||||
|
||||
override val defaultHeight: Int = 250
|
||||
@@ -53,7 +53,7 @@ class HistoryWidget(
|
||||
val model = HistoryCardPresenter.buildState(
|
||||
habit = habit,
|
||||
firstWeekday = prefs.firstWeekday,
|
||||
theme = WidgetTheme(),
|
||||
theme = WidgetTheme()
|
||||
)
|
||||
(widgetView.dataView as AndroidDataView).apply {
|
||||
val historyChart = (this.view as HistoryChart)
|
||||
@@ -75,7 +75,7 @@ class HistoryWidget(
|
||||
firstWeekday = prefs.firstWeekday,
|
||||
series = listOf(),
|
||||
defaultSquare = HistoryChart.Square.OFF,
|
||||
notesIndicators = listOf(),
|
||||
notesIndicators = listOf()
|
||||
)
|
||||
}
|
||||
).apply {
|
||||
|
||||
@@ -23,11 +23,14 @@ import android.content.Context
|
||||
class HistoryWidgetProvider : BaseWidgetProvider() {
|
||||
override fun getWidgetFromId(context: Context, id: Int): BaseWidget {
|
||||
val habits = getHabitsFromWidgetId(id)
|
||||
return if (habits.size == 1) HistoryWidget(
|
||||
context,
|
||||
id,
|
||||
habits[0]
|
||||
)
|
||||
else StackWidget(context, id, StackWidgetType.HISTORY, habits)
|
||||
return if (habits.size == 1) {
|
||||
HistoryWidget(
|
||||
context,
|
||||
id,
|
||||
habits[0]
|
||||
)
|
||||
} else {
|
||||
StackWidget(context, id, StackWidgetType.HISTORY, habits)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class ScoreWidget(
|
||||
context: Context,
|
||||
id: Int,
|
||||
private val habit: Habit,
|
||||
stacked: Boolean = false,
|
||||
stacked: Boolean = false
|
||||
) : BaseWidget(context, id, stacked) {
|
||||
override val defaultHeight: Int = 300
|
||||
override val defaultWidth: Int = 300
|
||||
@@ -46,7 +46,7 @@ class ScoreWidget(
|
||||
habit = habit,
|
||||
firstWeekday = prefs.firstWeekdayInt,
|
||||
spinnerPosition = prefs.scoreCardSpinnerPosition,
|
||||
theme = WidgetTheme(),
|
||||
theme = WidgetTheme()
|
||||
)
|
||||
val widgetView = view as GraphWidgetView
|
||||
widgetView.setBackgroundAlpha(preferedBackgroundAlpha)
|
||||
|
||||
@@ -23,7 +23,10 @@ import android.content.Context
|
||||
class ScoreWidgetProvider : BaseWidgetProvider() {
|
||||
override fun getWidgetFromId(context: Context, id: Int): BaseWidget {
|
||||
val habits = getHabitsFromWidgetId(id)
|
||||
return if (habits.size == 1) ScoreWidget(context, id, habits[0])
|
||||
else StackWidget(context, id, StackWidgetType.SCORE, habits)
|
||||
return if (habits.size == 1) {
|
||||
ScoreWidget(context, id, habits[0])
|
||||
} else {
|
||||
StackWidget(context, id, StackWidgetType.SCORE, habits)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ class StackWidget(
|
||||
widgetId: Int,
|
||||
private val widgetType: StackWidgetType,
|
||||
private val habits: List<Habit>,
|
||||
stacked: Boolean = true,
|
||||
stacked: Boolean = true
|
||||
) : BaseWidget(context, widgetId, stacked) {
|
||||
override val defaultHeight: Int = 0
|
||||
override val defaultWidth: Int = 0
|
||||
|
||||
@@ -33,7 +33,7 @@ import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.HabitNotFoundException
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday
|
||||
import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset
|
||||
import org.isoron.uhabits.intents.IntentFactory
|
||||
import org.isoron.uhabits.intents.PendingIntentFactory
|
||||
import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels
|
||||
@@ -101,7 +101,7 @@ internal class StackRemoteViewsFactory(private val context: Context, intent: Int
|
||||
val landscapeViews = widget.landscapeRemoteViews
|
||||
val portraitViews = widget.portraitRemoteViews
|
||||
val factory = PendingIntentFactory(context, IntentFactory())
|
||||
val intent = StackWidgetType.getIntentFillIn(factory, widgetType, h, habits, getToday())
|
||||
val intent = StackWidgetType.getIntentFillIn(factory, widgetType, h, habits, getTodayWithOffset())
|
||||
landscapeViews.setOnClickFillInIntent(R.id.button, intent)
|
||||
portraitViews.setOnClickFillInIntent(R.id.button, intent)
|
||||
val remoteViews = RemoteViews(landscapeViews, portraitViews)
|
||||
|
||||
@@ -34,7 +34,7 @@ class StreakWidget(
|
||||
context: Context,
|
||||
id: Int,
|
||||
private val habit: Habit,
|
||||
stacked: Boolean = false,
|
||||
stacked: Boolean = false
|
||||
) : BaseWidget(context, id, stacked) {
|
||||
override val defaultHeight: Int = 200
|
||||
override val defaultWidth: Int = 200
|
||||
|
||||
@@ -23,7 +23,10 @@ import android.content.Context
|
||||
class StreakWidgetProvider : BaseWidgetProvider() {
|
||||
override fun getWidgetFromId(context: Context, id: Int): BaseWidget {
|
||||
val habits = getHabitsFromWidgetId(id)
|
||||
return if (habits.size == 1) StreakWidget(context, id, habits[0])
|
||||
else StackWidget(context, id, StackWidgetType.STREAKS, habits)
|
||||
return if (habits.size == 1) {
|
||||
StreakWidget(context, id, habits[0])
|
||||
} else {
|
||||
StackWidget(context, id, StackWidgetType.STREAKS, habits)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ class TargetWidget(
|
||||
context: Context,
|
||||
id: Int,
|
||||
private val habit: Habit,
|
||||
stacked: Boolean = false,
|
||||
stacked: Boolean = false
|
||||
) : BaseWidget(context, id, stacked) {
|
||||
override val defaultHeight: Int = 200
|
||||
override val defaultWidth: Int = 200
|
||||
@@ -53,7 +53,7 @@ class TargetWidget(
|
||||
val data = TargetCardPresenter.buildState(
|
||||
habit = habit,
|
||||
firstWeekday = prefs.firstWeekdayInt,
|
||||
theme = WidgetTheme(),
|
||||
theme = WidgetTheme()
|
||||
)
|
||||
chart.setColor(WidgetTheme().color(habit.color).toInt())
|
||||
chart.setTargets(data.targets)
|
||||
|
||||
@@ -23,7 +23,10 @@ import android.content.Context
|
||||
class TargetWidgetProvider : BaseWidgetProvider() {
|
||||
override fun getWidgetFromId(context: Context, id: Int): BaseWidget {
|
||||
val habits = getHabitsFromWidgetId(id)
|
||||
return if (habits.size == 1) TargetWidget(context, id, habits[0])
|
||||
else StackWidget(context, id, StackWidgetType.TARGET, habits)
|
||||
return if (habits.size == 1) {
|
||||
TargetWidget(context, id, habits[0])
|
||||
} else {
|
||||
StackWidget(context, id, StackWidgetType.TARGET, habits)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID
|
||||
import android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.AbsListView.CHOICE_MODE_MULTIPLE
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Button
|
||||
import android.widget.ListView
|
||||
@@ -34,7 +33,6 @@ import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
||||
import org.isoron.uhabits.core.preferences.WidgetPreferences
|
||||
import org.isoron.uhabits.widgets.WidgetUpdater
|
||||
import java.util.ArrayList
|
||||
|
||||
class BooleanHabitPickerDialog : HabitPickerDialog() {
|
||||
override fun shouldHideNumerical() = true
|
||||
@@ -88,20 +86,12 @@ open class HabitPickerDialog : Activity() {
|
||||
with(listView) {
|
||||
adapter = ArrayAdapter(
|
||||
context,
|
||||
android.R.layout.simple_list_item_multiple_choice,
|
||||
android.R.layout.simple_list_item_1,
|
||||
habitNames
|
||||
)
|
||||
choiceMode = CHOICE_MODE_MULTIPLE
|
||||
itemsCanFocus = false
|
||||
}
|
||||
saveButton.setOnClickListener {
|
||||
val selectedIds = mutableListOf<Long>()
|
||||
for (i in 0..listView.count) {
|
||||
if (listView.isItemChecked(i)) {
|
||||
selectedIds.add(habitIds[i])
|
||||
}
|
||||
setOnItemClickListener { parent, view, position, id ->
|
||||
confirm(mutableListOf(habitIds[position]))
|
||||
}
|
||||
confirm(selectedIds)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,23 +66,21 @@ class CheckmarkWidgetView : HabitWidgetView {
|
||||
val res = StyledResources(context)
|
||||
val bgColor: Int
|
||||
val fgColor: Int
|
||||
setShadowAlpha(0x4f)
|
||||
when (entryState) {
|
||||
YES_MANUAL, SKIP -> {
|
||||
bgColor = activeColor
|
||||
fgColor = res.getColor(R.attr.contrast0)
|
||||
setShadowAlpha(0x4f)
|
||||
backgroundPaint!!.color = bgColor
|
||||
frame!!.setBackgroundDrawable(background)
|
||||
}
|
||||
YES_AUTO, NO, UNKNOWN -> {
|
||||
bgColor = res.getColor(R.attr.cardBgColor)
|
||||
fgColor = res.getColor(R.attr.contrast60)
|
||||
setShadowAlpha(0x00)
|
||||
}
|
||||
else -> {
|
||||
bgColor = res.getColor(R.attr.cardBgColor)
|
||||
fgColor = res.getColor(R.attr.contrast60)
|
||||
setShadowAlpha(0x00)
|
||||
}
|
||||
}
|
||||
ring.setPercentage(percentage)
|
||||
@@ -98,21 +96,23 @@ class CheckmarkWidgetView : HabitWidgetView {
|
||||
private val text: String
|
||||
get() = if (isNumerical) {
|
||||
(max(0, entryValue) / 1000.0).toShortString()
|
||||
} else when (entryState) {
|
||||
YES_MANUAL, YES_AUTO -> resources.getString(R.string.fa_check)
|
||||
SKIP -> resources.getString(R.string.fa_skipped)
|
||||
UNKNOWN -> {
|
||||
run {
|
||||
if (preferences!!.areQuestionMarksEnabled) {
|
||||
return resources.getString(R.string.fa_question)
|
||||
} else {
|
||||
resources.getString(R.string.fa_times)
|
||||
} else {
|
||||
when (entryState) {
|
||||
YES_MANUAL, YES_AUTO -> resources.getString(R.string.fa_check)
|
||||
SKIP -> resources.getString(R.string.fa_skipped)
|
||||
UNKNOWN -> {
|
||||
run {
|
||||
if (preferences!!.areQuestionMarksEnabled) {
|
||||
return resources.getString(R.string.fa_question)
|
||||
} else {
|
||||
resources.getString(R.string.fa_times)
|
||||
}
|
||||
}
|
||||
resources.getString(R.string.fa_times)
|
||||
}
|
||||
resources.getString(R.string.fa_times)
|
||||
NO -> resources.getString(R.string.fa_times)
|
||||
else -> resources.getString(R.string.fa_times)
|
||||
}
|
||||
NO -> resources.getString(R.string.fa_times)
|
||||
else -> resources.getString(R.string.fa_times)
|
||||
}
|
||||
|
||||
override val innerLayoutId: Int
|
||||
@@ -126,7 +126,7 @@ class CheckmarkWidgetView : HabitWidgetView {
|
||||
} else {
|
||||
width = min(width, height)
|
||||
}
|
||||
val textSize = min(0.2f * width, getDimension(context, R.dimen.smallerTextSize))
|
||||
val textSize = min(0.175f * width, getDimension(context, R.dimen.smallTextSize))
|
||||
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
||||
if (isNumerical) {
|
||||
ring.setTextSize(textSize * 0.9f)
|
||||
@@ -141,7 +141,8 @@ class CheckmarkWidgetView : HabitWidgetView {
|
||||
}
|
||||
|
||||
private fun init() {
|
||||
val appComponent: HabitsApplicationComponent = (context.applicationContext as HabitsApplication).component
|
||||
val appComponent: HabitsApplicationComponent =
|
||||
(context.applicationContext as HabitsApplication).component
|
||||
preferences = appComponent.preferences
|
||||
ring = findViewById<View>(R.id.scoreRing) as RingView
|
||||
label = findViewById<View>(R.id.label) as TextView
|
||||
|
||||
@@ -69,7 +69,7 @@ abstract class HabitWidgetView : FrameLayout {
|
||||
val shadowRadius = dpToPixels(context, 2f).toInt()
|
||||
val shadowOffset = dpToPixels(context, 1f).toInt()
|
||||
val shadowColor = Color.argb(shadowAlpha, 0, 0, 0)
|
||||
val cornerRadius = dpToPixels(context, 5f)
|
||||
val cornerRadius = dpToPixels(context, 12f)
|
||||
val radii = FloatArray(8)
|
||||
Arrays.fill(radii, cornerRadius)
|
||||
val shape = RoundRectShape(radii, null, null)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
2.1.1:
|
||||
* Fix Tasker plugin
|
||||
|
||||
2.1:
|
||||
* Add notes to specific dates
|
||||
* Track at-most measurable habits
|
||||
* Add skips to measurable habits
|
||||
* Bring back custom frequencies
|
||||
* Other minor improvements and bug fixes
|
||||
* Other minor improvements and bug fixes
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
<item android:id="@android:id/mask">
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="5dp"/>
|
||||
<corners android:radius="12dp"/>
|
||||
<solid android:color="?android:colorPrimary"/>
|
||||
</shape>
|
||||
<color android:color="@color/white"/>
|
||||
|
||||
@@ -21,8 +21,10 @@
|
||||
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="128dp"
|
||||
android:minWidth="208dp"
|
||||
app:divider="@drawable/checkmark_dialog_divider"
|
||||
app:showDividers="middle"
|
||||
android:orientation="vertical"
|
||||
@@ -34,7 +36,7 @@
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:inputType="textCapSentences|textMultiLine"
|
||||
android:inputType="textCapSentences"
|
||||
android:textSize="@dimen/smallTextSize"
|
||||
android:padding="4dp"
|
||||
android:background="@color/transparent"
|
||||
|
||||