Compare commits
252 Commits
feature/sy
...
hotfix/2.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
8b2adbf301
|
|||
|
88cc3a2a12
|
|||
|
26526a71a9
|
|||
|
11eb3713e5
|
|||
|
1df9cc7664
|
|||
|
b76da35752
|
|||
|
abead88ceb
|
|||
|
908eb4ac99
|
|||
|
71a05d598a
|
|||
|
2131fb3a3d
|
|||
|
1470dcd560
|
|||
|
471f977209
|
|||
|
2ba5f5fb98
|
|||
|
4de67bd27a
|
|||
|
0bb82a48a5
|
|||
|
d5a5273607
|
|||
|
40a4d254f5
|
|||
|
177d01edd9
|
|||
|
ec1f0c5356
|
|||
|
961fb7618f
|
|||
|
11f726064a
|
|||
| abced92a07 | |||
|
|
eeacc5eef8 | ||
|
|
16c65f19fd | ||
| fc402fd81b | |||
| f7c6bc716c | |||
| 2535347d5a | |||
| 13af054214 | |||
| fcbb586e80 | |||
|
|
428bf42e79 | ||
| 838e13f30c | |||
| 0eae43fe55 | |||
| c0fcd4e763 | |||
| 79e2402c9d | |||
| 53c270ee12 | |||
| e4b16f6d59 | |||
| 3021e408a7 | |||
| 7649119db7 | |||
| b0a4f26e7a | |||
| 0a95b6d2a0 | |||
| e6bcbb39ff | |||
| 90de1f3723 | |||
| 68740b4043 | |||
| b66a6ff717 | |||
| 7c1a91e35a | |||
| 366e9af167 | |||
| 03b02aaa06 | |||
|
|
180cf25ad3 | ||
| 3490cd183a | |||
|
|
0c292d1eaa | ||
| 0b256cb2c0 | |||
| cc03c48648 | |||
| 53e7ef2918 | |||
| 9609bee0f7 | |||
| 5b23a3f960 | |||
| d2d45991b0 | |||
| 25aeafb759 | |||
| 9045ae5c24 | |||
| 555873354c | |||
| 2a012619a7 | |||
| d1de3a852b | |||
| f04e37e905 | |||
| 825a5f2cb9 | |||
| 0de6896691 | |||
| 7187214282 | |||
|
|
9d4161a255 | ||
|
|
d82a3c145d | ||
|
|
a6cbd44e42 | ||
|
|
45c62b4ab2 | ||
|
|
d53312d261 | ||
|
|
c453810785 | ||
|
|
999057300b | ||
|
|
2245347e28 | ||
|
|
b79f7850ed | ||
|
|
4469f86e46 | ||
|
|
bdeddb149e | ||
|
|
3b12ec4bfe | ||
| ca4618579e | |||
| 2f13aba22c | |||
| dba5912ecd | |||
| 88b8663484 | |||
| 609886fd09 | |||
| a4db997e06 | |||
|
|
090216ccb7 | ||
|
|
472092d1bf | ||
|
|
6d3791de31 | ||
|
|
24a99d5791 | ||
|
|
6c631b1cf6 | ||
|
|
5d7677b354 | ||
|
|
22820f4f24 | ||
| 1d3bd48535 | |||
|
|
694446b7e3 | ||
|
|
20ae9d247e | ||
|
|
1943fac610 | ||
|
|
a07a50e635 | ||
|
|
8ccb9bbab1 | ||
|
|
743b8d26ad | ||
|
|
c47bd4c328 | ||
|
|
d84abc3a6a | ||
|
|
4ae85f1ec0 | ||
|
|
79d40d1d79 | ||
|
|
1902b8821e | ||
|
|
075542d605 | ||
|
|
0b5894ee6d | ||
|
|
92fb9dbdb6 | ||
|
|
1860abf532 | ||
|
|
3f4780c9fb | ||
|
|
d5d6e4616e | ||
|
|
18e267053d | ||
| 7ba9a7e4e7 | |||
| 5b8a7c39e2 | |||
| d40a5a89cd | |||
| 308d558347 | |||
| 9770ce187a | |||
| 535bc03b70 | |||
| 642e45af89 | |||
| 56d2307b75 | |||
| d875af8a8e | |||
| feeb4f057d | |||
| 1ad5c6b896 | |||
| baee3b9f86 | |||
| e6167baab1 | |||
|
|
1c15e7742e | ||
|
|
072ba63789 | ||
|
|
0fa0daa058 | ||
|
|
5a5ed3d631 | ||
|
|
3dfa376f59 | ||
|
|
7bf74634bb | ||
|
|
fc645a81bc | ||
|
|
35365bbdf3 | ||
|
|
779ef5dbee | ||
|
|
e82994c76b | ||
|
|
4e1d01d8d1 | ||
|
|
5de0fc86e5 | ||
| e26b643423 | |||
| 621534d610 | |||
| a01300e9c6 | |||
| ecb8ce105a | |||
| 32ef3c14f7 | |||
| 4972257635 | |||
| c98cb50baa | |||
| c331f34fa9 | |||
| a1aea532b5 | |||
| 43489aeb4c | |||
| 990c85aedd | |||
|
|
64337b9bee | ||
|
|
8bdfaa2434 | ||
|
|
5f6060858d | ||
| b62e436054 | |||
| bf63b4dbcf | |||
| a82d940bcc | |||
| ba59dc7ca9 | |||
| 181290a0f3 | |||
|
|
d553c2f3f2 | ||
|
|
7776093217 | ||
|
|
b27f3f8540 | ||
| eb041bf6b2 | |||
|
|
e3c53bf07f | ||
|
|
9ddab6ee59 | ||
|
|
2615795402 | ||
|
|
6531445d7f | ||
|
|
4fbf8a8ca2 | ||
|
|
707b2b4380 | ||
|
|
aae85c1170 | ||
|
|
c12a6c6a4d | ||
|
|
b15c02adbf | ||
|
|
9e24128675 | ||
|
|
66c61e2e6c | ||
|
|
7bddfbe5a7 | ||
|
|
8036b10ee6 | ||
|
|
71f400f587 | ||
|
|
79e302f922 | ||
|
|
af7f60fc4d | ||
|
|
7cc4b66dfd | ||
|
|
a9fddf9963 | ||
|
|
36c1504c6a | ||
|
|
d644170141 | ||
|
|
d38f83e961 | ||
|
|
c50c5af497 | ||
|
|
fa3774a32b | ||
|
|
fd124f2a6c | ||
|
|
265b65eb8a | ||
|
|
4c269c55d2 | ||
|
|
c03305120e | ||
|
|
29615b670b | ||
|
|
6ab4a696b6 | ||
|
|
23479c7765 | ||
|
|
6d98f7aafa | ||
| 75078ed52b | |||
| 2a0afedb1d | |||
| 66a2b41250 | |||
|
|
d6a7fa3d7a | ||
|
|
07e55f1c76 | ||
| 4ee5dd910b | |||
| 87f071b5b4 | |||
| bb0b5e8adf | |||
|
|
c79d1e82a5 | ||
|
|
4aebeedec6 | ||
|
|
7de94f2caf | ||
|
|
17ed85fc1b | ||
|
|
4355fb4d68 | ||
|
|
508200abeb | ||
|
|
a29943e783 | ||
|
|
3e6a9181d6 | ||
| 1fe3a3d1ca | |||
|
|
b2951a3475 | ||
|
|
9d3c63cf62 | ||
|
|
65d237254c | ||
|
|
fe1d5c66cb | ||
|
|
113a5028af | ||
|
|
1a56260757 | ||
|
|
697fffbc99 | ||
|
|
804edfa64e | ||
| 2ab6c396d0 | |||
| a55f467339 | |||
|
|
cf682f68c9 | ||
| 0e988e746c | |||
|
|
f119cbf8e7 | ||
|
|
056f5f6fce | ||
|
|
42f6125d5e | ||
|
|
3e20fc4d1d | ||
|
|
1f763feb69 | ||
|
|
6e7ad329fe | ||
|
|
5cb241475d | ||
|
|
27e76c7243 | ||
|
|
576ad04064 | ||
|
|
5f8187ef6d | ||
|
|
f16f919e27 | ||
|
|
736bb8a75e | ||
|
|
de9ad6d4a4 | ||
|
|
1d37ce54ea | ||
|
|
f88f1cfb54 | ||
|
|
fc1478645b | ||
|
|
ffab001b09 | ||
|
|
a58a8005e1 | ||
|
|
c884ada187 | ||
|
|
e3d46ad5a0 | ||
|
|
f4a2c03216 | ||
|
|
f2b8f2f98d | ||
|
|
2c5fd87a2a | ||
|
|
39768f7f04 | ||
|
|
cc3e1ced15 | ||
|
|
2e26cc104e | ||
|
|
42fd0926ef | ||
| ec202aa9a7 | |||
|
|
1fb56c8777 | ||
|
|
a5d4a37da8 | ||
|
|
4804a48549 | ||
|
|
c892a845b4 | ||
|
|
e98064b6a5 | ||
| 57f5f6ed5b | |||
| 79f5b8b7e8 |
59
.github/workflows/main.yml
vendored
@@ -7,59 +7,24 @@ on:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
jobs:
|
||||
Build:
|
||||
runs-on: ubuntu-latest
|
||||
Test:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Check out source code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Install Java Development Kit 1.8
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
|
||||
- name: Build Project
|
||||
- name: Build project
|
||||
run: ./build.sh build
|
||||
|
||||
- name: Upload Build Artifacts
|
||||
- name: Run Android tests
|
||||
run: ./build.sh android-tests-parallel 24 25 26 28 30 31
|
||||
|
||||
- name: Upload artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: uhabits-android
|
||||
path: uhabits-android/build/outputs/
|
||||
|
||||
AndroidTest:
|
||||
needs: Build
|
||||
runs-on: macOS-10.15
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
matrix:
|
||||
api: [
|
||||
23,
|
||||
24,
|
||||
25,
|
||||
26,
|
||||
27,
|
||||
28,
|
||||
# 29, # Crashes constantly, see: https://issuetracker.google.com/issues/159732638
|
||||
# 30, # Not available yet
|
||||
# 31, # Not available yet
|
||||
]
|
||||
|
||||
steps:
|
||||
- name: Check out source code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Download Previously Built APK
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: uhabits-android
|
||||
path: uhabits-android/build/outputs/
|
||||
|
||||
- name: Install flock
|
||||
run: |
|
||||
brew install util-linux
|
||||
echo "/usr/local/opt/util-linux/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Run Android Tests
|
||||
run: ./build.sh android-tests ${{ matrix.api }}
|
||||
name: build
|
||||
path: |
|
||||
build/*log
|
||||
uhabits-android/build/outputs
|
||||
|
||||
|
||||
56
CHANGELOG.md
@@ -1,5 +1,61 @@
|
||||
# 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)
|
||||
- Allow user to track "at most" numerical habits (@KristianTashkov, #1101)
|
||||
- Allow user to add skips to measurable habits (@kalina559, #1319)
|
||||
- Bring back custom frequencies (x times in y days) (@hiqua, #1079)
|
||||
- Improve number picker (@hiqua, @iSoron, #1082, #1370)
|
||||
- Add new checkmark and number picker (@iSoron, #1370)
|
||||
- Allow user to import numerical habits from HabitBull (@hiqua, #1278)
|
||||
- Add support for Android 13 themed icons (@cheeeeer, #1497)
|
||||
|
||||
### Removed
|
||||
- Hide snooze button Android 12 notifications (@hiqua, #1226)
|
||||
- Remove preference to set LED lights (@iSoron)
|
||||
|
||||
### Changed
|
||||
- Hide failed habits along with completed ones (@hiqua, #1052)
|
||||
- Cycle through all checkmark states when toggling (@iSoron)
|
||||
- Add delay after toggling a habit (@hiqua, @kalina559, #1147)
|
||||
- Small theme improvements (@KristianTashkov, #1113)
|
||||
- Left-align habit notes (@iSoron)
|
||||
- Increase target SDK to 31 (@hiqua)
|
||||
|
||||
### Fixed
|
||||
- Fix small dialog buttons (@kalina559, #1096)
|
||||
- Fix invalid CSV files (@hiqua, #1177)
|
||||
- Fix small issues in calendar chart (@kalina559, #1314)
|
||||
- Resort habit list after edit (@hiqua, #1350)
|
||||
- Fix marker scaling in frequency display (@eduebernal, #1425)
|
||||
- Fix widgets not working correctly on API 33 (@iSoron, #1488)
|
||||
|
||||
### Refactoring & Testing
|
||||
- Replace raster icons by vector assets (@kalina559)
|
||||
- Remove JVM dependencies from uhabits-core module (@sgallese)
|
||||
- Add various missing tests (@sgallese)
|
||||
- Upgrade project dependencies (@hiqua, @sgallese)
|
||||
|
||||
## [2.0.3] - 2021-08-21
|
||||
### Fixed
|
||||
- Improve automatic checkmarks for monthly habits (@iSoron, #947)
|
||||
|
||||
@@ -90,7 +90,7 @@ contribute, even if you are not a software developer.
|
||||
is already completed, you are also very welcome to join and proofread it.
|
||||
|
||||
* **Write some code.** If you are an Android developer, you are very welcome to
|
||||
contribute with code. Please see `docs/GUIDELINES.md`.
|
||||
contribute with code. Please see the [guidelines](https://github.com/iSoron/uhabits/blob/dev/docs/GUIDELINES.md).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
plugins {
|
||||
val kotlinVersion = "1.5.0"
|
||||
id("com.android.application") version ("4.2.0") apply (false)
|
||||
val kotlinVersion = "1.6.10"
|
||||
id("com.android.application") version ("7.0.3") apply (false)
|
||||
id("org.jetbrains.kotlin.android") version kotlinVersion apply (false)
|
||||
id("org.jetbrains.kotlin.kapt") version kotlinVersion apply (false)
|
||||
id("org.jetbrains.kotlin.android.extensions") version kotlinVersion apply (false)
|
||||
id("org.jetbrains.kotlin.multiplatform") version kotlinVersion apply (false)
|
||||
id("org.jlleitschuh.gradle.ktlint") version "10.1.0"
|
||||
id("org.jlleitschuh.gradle.ktlint") version "10.2.1"
|
||||
}
|
||||
|
||||
apply {
|
||||
@@ -15,11 +15,9 @@ apply {
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
maven(url = "https://plugins.gradle.org/m2/")
|
||||
maven(url = "https://oss.sonatype.org/content/repositories/snapshots/")
|
||||
maven(url = "https://kotlin.bintray.com/ktor")
|
||||
maven(url = "https://kotlin.bintray.com/kotlin-js-wrappers")
|
||||
maven(url = "https://jitpack.io")
|
||||
}
|
||||
}
|
||||
|
||||
134
build.sh
@@ -26,6 +26,7 @@ GRADLE="./gradlew --stacktrace --quiet"
|
||||
PACKAGE_NAME=org.isoron.uhabits
|
||||
SDKMANAGER="${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager"
|
||||
VERSION=$(grep versionName uhabits-android/build.gradle.kts | sed -e 's/.*"\([^"]*\)".*/\1/g')
|
||||
BOOT_TIMEOUT=360
|
||||
|
||||
if [ -z $VERSION ]; then
|
||||
echo "Could not parse app version from: uhabits-android/build.gradle.kts"
|
||||
@@ -69,8 +70,7 @@ core_build() {
|
||||
# Android
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# shellcheck disable=SC2016
|
||||
android_test() {
|
||||
android_setup() {
|
||||
API=$1
|
||||
AVDNAME=${AVD_PREFIX}${API}
|
||||
|
||||
@@ -95,15 +95,53 @@ android_test() {
|
||||
) 10>/tmp/uhabitsTest.lock
|
||||
|
||||
log_info "Launching emulator..."
|
||||
$EMULATOR -avd $AVDNAME -port 6${API}0 1>/dev/null 2>&1 &
|
||||
$EMULATOR \
|
||||
-avd $AVDNAME \
|
||||
-port 6${API}0 \
|
||||
1>/dev/null 2>&1 &
|
||||
|
||||
log_info "Waiting for emulator to boot..."
|
||||
export ADB="$ADB -s emulator-6${API}0"
|
||||
timeout $BOOT_TIMEOUT $ADB wait-for-device shell 'while [[ -z "$(getprop sys.boot_completed)" ]]; do echo Waiting...; sleep 1; done; input keyevent 82'
|
||||
if [ $? -ne 0 ]; then
|
||||
log_error "Emulator failed to boot after $BOOT_TIMEOUT seconds."
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Saving snapshot..."
|
||||
$ADB emu avd snapshot save fresh-install
|
||||
}
|
||||
|
||||
android_boot_attempt() {
|
||||
API=$1
|
||||
AVDNAME=${AVD_PREFIX}${API}
|
||||
|
||||
log_info "Stopping Android emulator..."
|
||||
while [[ -n $(pgrep -f ${AVDNAME}) ]]; do
|
||||
pkill -9 -f ${AVDNAME}
|
||||
done
|
||||
|
||||
log_info "Launching emulator..."
|
||||
$EMULATOR \
|
||||
-avd $AVDNAME \
|
||||
-port 6${API}0 \
|
||||
-snapshot fresh-install \
|
||||
-no-snapshot-save \
|
||||
-wipe-data \
|
||||
1>/dev/null 2>&1 &
|
||||
|
||||
log_info "Waiting for emulator to boot..."
|
||||
export ADB="$ADB -s emulator-6${API}0"
|
||||
$ADB wait-for-device shell 'while [[ -z "$(getprop sys.boot_completed)" ]]; do echo Waiting...; sleep 1; done; input keyevent 82' || return 1
|
||||
$ADB root || return 1
|
||||
sleep 5
|
||||
timeout $BOOT_TIMEOUT $ADB wait-for-device shell 'while [[ -z "$(getprop sys.boot_completed)" ]]; do echo Waiting...; sleep 1; done; input keyevent 82'
|
||||
if [ $? -ne 0 ]; then
|
||||
log_error "Emulator failed to boot after $BOOT_TIMEOUT seconds."
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Disabling animations..."
|
||||
$ADB root || return 1
|
||||
sleep 5
|
||||
$ADB shell settings put global window_animation_scale 0 || return 1
|
||||
$ADB shell settings put global transition_animation_scale 0 || return 1
|
||||
$ADB shell settings put global animator_duration_scale 0 || return 1
|
||||
@@ -111,6 +149,24 @@ android_test() {
|
||||
log_info "Acquiring wake lock..."
|
||||
$ADB shell 'echo android-test > /sys/power/wake_lock' || return 1
|
||||
|
||||
}
|
||||
|
||||
android_boot() {
|
||||
for attempt in {1..5}; do
|
||||
android_boot_attempt $1 && return 0
|
||||
sleep 5
|
||||
done
|
||||
log_error "Too many failed attempts. Aborting."
|
||||
return 1
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2016
|
||||
android_test() {
|
||||
API=$1
|
||||
AVDNAME=${AVD_PREFIX}${API}
|
||||
|
||||
android_boot $API || return 1
|
||||
|
||||
if [ -n "$RELEASE" ]; then
|
||||
log_info "Installing release APK..."
|
||||
$ADB install -r ${ANDROID_OUTPUTS_DIR}/apk/release/uhabits-android-release.apk || return 1
|
||||
@@ -122,14 +178,25 @@ android_test() {
|
||||
$ADB install -r ${ANDROID_OUTPUTS_DIR}/apk/androidTest/debug/uhabits-android-debug-androidTest.apk || return 1
|
||||
|
||||
for size in medium large; do
|
||||
log_info "Running $size instrumented tests..."
|
||||
OUT_INSTRUMENT=${ANDROID_OUTPUTS_DIR}/instrument-${API}.txt
|
||||
OUT_LOGCAT=${ANDROID_OUTPUTS_DIR}/logcat-${API}.txt
|
||||
$ADB shell am instrument \
|
||||
-r -e coverage true -e size $size \
|
||||
-w ${PACKAGE_NAME}.test/androidx.test.runner.AndroidJUnitRunner \
|
||||
| tee $OUT_INSTRUMENT
|
||||
if grep "\(INSTRUMENTATION_STATUS_CODE.*-1\|FAILURES\|ABORTED\|onError\|Error type\|crashed\)" $OUT_INSTRUMENT; then
|
||||
FAILED_TESTS=""
|
||||
for i in {1..5}; do
|
||||
log_info "Running $size instrumented tests (attempt $i)..."
|
||||
$ADB shell am instrument \
|
||||
-r -e coverage true -e size "$size" $FAILED_TESTS \
|
||||
-w ${PACKAGE_NAME}.test/androidx.test.runner.AndroidJUnitRunner \
|
||||
| ts "%.s" | tee "$OUT_INSTRUMENT"
|
||||
|
||||
FAILED_TESTS=$(tools/parseInstrument.py "$OUT_INSTRUMENT")
|
||||
SUCCESS=$?
|
||||
if [ $SUCCESS -eq 0 ]; then
|
||||
log_info "$size tests passed."
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $SUCCESS -ne 0 ]; then
|
||||
log_error "Some $size instrumented tests failed."
|
||||
log_error "Saving logcat: $OUT_LOGCAT..."
|
||||
$ADB logcat -d > $OUT_LOGCAT
|
||||
@@ -138,26 +205,50 @@ android_test() {
|
||||
$ADB shell rm -r /sdcard/Android/data/${PACKAGE_NAME}/files/test-screenshots/
|
||||
return 1
|
||||
fi
|
||||
log_info "$size tests passed."
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
android_test_parallel() {
|
||||
# Launch background processes
|
||||
PIDS=""
|
||||
for API in $*; do
|
||||
(
|
||||
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. See $LOG for more details."
|
||||
log_error "API $API: Failed"
|
||||
fi
|
||||
pkill -9 -f ${AVD_PREFIX}${API}
|
||||
exit $ret_code
|
||||
)&
|
||||
PIDS+=" $!"
|
||||
done
|
||||
wait
|
||||
|
||||
# Check exit codes
|
||||
success=0
|
||||
for pid in $PIDS; do
|
||||
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
|
||||
for API in $*; do
|
||||
echo "::group::Android Tests (API $API)"
|
||||
cat build/android-test-$API.log
|
||||
echo "::endgroup::"
|
||||
done
|
||||
|
||||
return $success
|
||||
}
|
||||
|
||||
android_build() {
|
||||
@@ -229,12 +320,14 @@ CI/CD script for Loop Habit Tracker.
|
||||
|
||||
Usage:
|
||||
build.sh build [options]
|
||||
build.sh android-setup <API>
|
||||
build.sh android-tests <API> [options]
|
||||
build.sh android-tests-parallel <API> <API>... [options]
|
||||
build.sh android-accept-images [options]
|
||||
|
||||
Commands:
|
||||
build Build the app and run small tests
|
||||
android-setup Create Android virtual machine
|
||||
android-tests Run medium and large Android tests on an emulator
|
||||
android-tests-parallel Tests multiple API levels simultaneously
|
||||
android-accept-images Copy fetched images to corresponding assets folder
|
||||
@@ -270,18 +363,17 @@ main() {
|
||||
core_build
|
||||
android_build
|
||||
;;
|
||||
android-setup)
|
||||
shift; _parse_opts "$@"
|
||||
android_setup $1
|
||||
;;
|
||||
android-tests)
|
||||
shift; _parse_opts "$@"
|
||||
if [ -z $1 ]; then
|
||||
_print_usage
|
||||
exit 1
|
||||
fi
|
||||
for attempt in {1..5}; do
|
||||
log_info "Running Android tests (attempt $attempt)..."
|
||||
android_test $1 && return 0
|
||||
done
|
||||
log_error "Maximum number of attempts reached. Failing."
|
||||
return 1
|
||||
android_test $1
|
||||
;;
|
||||
android-tests-parallel)
|
||||
shift; _parse_opts "$@"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
org.gradle.parallel=false
|
||||
org.gradle.daemon=true
|
||||
org.gradle.jvmargs=-Xms2048m -Xmx2048m -XX:MaxPermSize=2048m
|
||||
org.gradle.jvmargs=-Xms2048m -Xmx2048m
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
jcenter()
|
||||
google()
|
||||
}
|
||||
resolutionStrategy.eachPlugin {
|
||||
|
||||
67
tools/parseInstrument.py
Executable file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Android Instrumentation Test Parser
|
||||
|
||||
Given a raw Android Instrumentation log (produced by "adb shell am instrument -r ...") this script
|
||||
return zero if all tests pass and non-zero if some tests fail. In case of failure, this script
|
||||
also prints arguments that, if passed to "am instrument", will cause it to re-run just the tests
|
||||
that failed. This script additionally prints warnings about the tests on the STDERR; e.g. slow tests.
|
||||
"""
|
||||
import sys
|
||||
import re
|
||||
|
||||
STATUS_START = 1
|
||||
STATUS_DISABLED = -3
|
||||
SLOW_TEST_THRESHOLD = 5.0
|
||||
|
||||
COLOR_RED = '\033[91m'
|
||||
COLOR_YELLOW = '\033[93m'
|
||||
COLOR_END = '\033[0m'
|
||||
|
||||
def error(msg):
|
||||
sys.stderr.write("%s%s%s\n" % (COLOR_RED, msg, COLOR_END))
|
||||
|
||||
def warning(msg):
|
||||
sys.stderr.write("%s%s%s\n" % (COLOR_YELLOW, msg, COLOR_END))
|
||||
|
||||
log_filename = sys.argv[1]
|
||||
current_class, current_method = None, None
|
||||
failed_tests = []
|
||||
am_args = "-e class "
|
||||
exit_code = 1
|
||||
|
||||
for line in open(log_filename).readlines():
|
||||
matches = re.findall('^([0-9.]*)', line)
|
||||
current_time = float(matches[0])
|
||||
|
||||
matches = re.findall('INSTRUMENTATION_STATUS: class=(.*)', line)
|
||||
if len(matches) > 0:
|
||||
current_class = matches[0]
|
||||
|
||||
matches = re.findall('INSTRUMENTATION_STATUS: test=(.*)', line)
|
||||
if len(matches) > 0:
|
||||
current_method = matches[0]
|
||||
|
||||
matches = re.findall('OK \([0-9]* tests?\)', line)
|
||||
if len(matches) > 0:
|
||||
exit_code = 0
|
||||
|
||||
matches = re.findall('INSTRUMENTATION_STATUS_CODE: ([-0-9]*)', line)
|
||||
if len(matches) > 0:
|
||||
status_code = int(matches[0])
|
||||
if (status_code < 0) and (status_code != STATUS_DISABLED):
|
||||
am_args += f"{current_class}#{current_method},"
|
||||
failed_tests.append(f"{current_class}#{current_method}")
|
||||
if status_code == STATUS_START:
|
||||
initial_time = current_time
|
||||
else:
|
||||
elapsed_time = current_time - initial_time
|
||||
if(elapsed_time > SLOW_TEST_THRESHOLD):
|
||||
warning("SLOW %s#%s (%.2f seconds)" % (current_class, current_method, elapsed_time))
|
||||
|
||||
if len(failed_tests) > 0:
|
||||
for test in failed_tests:
|
||||
error("FAIL %s" % test)
|
||||
print(am_args[:-1])
|
||||
|
||||
sys.exit(exit_code)
|
||||
@@ -1,377 +1,415 @@
|
||||
Name,Languages,"Translated (Words)","Target Words","Approved (Words)",Voted,"""+"" votes received","""-"" votes received","Winning (Words)",Joined
|
||||
"Alinson Xavier (iSoron)","Portuguese, Brazilian; Japanese; Spanish; Portuguese; Italian; Chinese Simplified; French; Hungarian; German; Arabic; Hindi; Slovenian; Catalan; Greek; Korean; Bulgarian; Chinese Traditional; Polish; Russian; Serbian (Cyrillic); Turkish; Ukrainian; Czech; Indonesian; Croatian; Danish; Dutch; Romanian; Swedish; Basque; Persian; Finnish; Vietnamese; Telugu; Tamil; Afrikaans; Esperanto; Hebrew",14808,17227,1282,0,1779,80,4274,"2016-03-05 18:35:27"
|
||||
"Alinson Xavier (iSoron)","Portuguese, Brazilian; Japanese; Chinese Simplified; Italian; Spanish; Portuguese; French; Hungarian; Chinese Traditional; Turkish; Russian; Polish; Arabic; German; Korean; Greek; Catalan; Bulgarian; Hindi; Slovenian; Ukrainian; Serbian (Cyrillic); Czech; Indonesian; Croatian; Danish; Dutch; Romanian; Swedish; Basque; Persian; Finnish; Vietnamese; Tamil; Telugu; Hebrew; Esperanto; Norwegian; Afrikaans; Slovak; Armenian; Serbian (Latin); Uyghur",15497,18825,1308,0,1896,84,4315,"2016-03-05 18:35:27"
|
||||
"Slobodan Simić (Слободан Симић) (slsimic)","Serbian (Latin); Serbian (Cyrillic)",2054,1831,2114,12,33,0,1991,"2021-02-03 14:26:07"
|
||||
"Oglaigh Rystard (oglaignaheireann)","Ukrainian; Portuguese; Catalan; Greek; Basque; Romanian; Italian",1103,1037,1327,1,13,6,954,"2017-03-31 09:13:19"
|
||||
dukelc,Slovak,1046,993,0,0,0,0,0,"2020-08-27 14:02:41"
|
||||
"David (Cliff122)",Swedish,1040,1019,725,6,0,0,700,"2020-01-21 13:56:55"
|
||||
"Omer I.S. (omeritzics)",Hebrew,1000,900,1097,14,1,0,946,"2020-10-11 20:10:51"
|
||||
dukelc,Slovak,919,880,0,0,0,0,0,"2020-08-27 14:02:41"
|
||||
"Intan Ayunda (Intan_Ayunda)",Indonesian,800,793,962,0,0,0,711,"2020-10-14 07:51:58"
|
||||
"Omer I.S. (omeritzics)",Hebrew,1040,927,1122,14,1,0,975,"2020-10-11 20:10:51"
|
||||
"Intan Ayunda (Intan_Ayunda)",Indonesian,818,811,985,0,0,0,729,"2020-10-14 07:51:58"
|
||||
"Mihail Stefanov (MStefanov)",Bulgarian,755,794,3,0,2,0,2,"2017-03-31 16:09:02"
|
||||
KMakoto,"Chinese Traditional",745,1146,949,0,0,0,745,"2019-10-22 04:19:52"
|
||||
"Evren (evrenkiymaz)",Turkish,688,604,0,71,5,1,0,"2020-10-04 03:39:16"
|
||||
"Evren (evrenkiymaz)",Turkish,688,604,0,71,28,22,0,"2020-10-04 03:39:16"
|
||||
andaryon,Czech,681,606,0,108,0,0,0,"2021-11-25 10:20:45"
|
||||
"Antti Kallio (antti.kallio)",Finnish,668,539,0,5,0,0,0,"2021-07-03 05:54:44"
|
||||
"David Nos (david.nos)","Catalan; Spanish",667,731,0,0,1,0,0,"2020-01-04 10:15:36"
|
||||
"Antti Kallio (antti.kallio)",Finnish,650,525,0,0,0,0,0,"2021-07-03 05:54:44"
|
||||
androide74,Italian,644,659,0,2,0,0,0,"2020-02-06 15:46:28"
|
||||
androide74,Italian,662,681,0,2,0,0,0,"2020-02-06 15:46:28"
|
||||
Osoitz,Basque,655,595,0,9,0,0,3,"2018-01-23 14:07:47"
|
||||
"Dmitriy Bogdanov (di72nn)",Russian,643,589,1197,0,36,0,515,"2017-03-31 10:00:48"
|
||||
Tomairuka,Japanese,633,1636,909,43,0,0,564,"2020-12-12 12:14:22"
|
||||
"Dmitriy Bogdanov (di72nn)",Russian,625,572,1197,0,36,0,515,"2017-03-31 10:00:48"
|
||||
reyhoon,Persian,624,759,0,1,3,1,0,"2020-10-01 18:17:23"
|
||||
Osoitz,Basque,610,545,0,9,0,0,3,"2018-01-23 14:07:47"
|
||||
"Saeed Esmaili (saaeed.es20)",Persian,568,774,0,5,4,0,0,"2020-11-26 15:41:15"
|
||||
"Saeed Esmaili (saaeed.es20)",Persian,586,795,0,5,4,0,0,"2020-11-26 15:41:15"
|
||||
fabian.bouchal,German,548,527,0,6,0,3,72,"2020-01-07 06:43:37"
|
||||
boban77,Czech,509,461,0,2,0,0,0,"2020-04-30 13:18:24"
|
||||
"Yoav Argov (YoavArgov)",Hebrew,501,461,0,0,1,8,103,"2017-04-28 07:23:01"
|
||||
"Isti (eisti)",Hungarian,528,476,0,0,0,0,0,"2020-12-03 12:02:51"
|
||||
boban77,Czech,509,461,0,2,29,0,0,"2020-04-30 13:18:24"
|
||||
"Martim Parente (martimparente)",Portuguese,505,542,0,38,0,0,0,"2020-08-26 10:22:11"
|
||||
"Yoav Argov (YoavArgov)",Hebrew,501,461,0,0,1,8,91,"2017-04-28 07:23:01"
|
||||
REMOVED_USER,Norwegian,501,498,501,0,148,0,501,"2017-07-05 19:02:25"
|
||||
"Martim Parente (Sharlimar)",Portuguese,497,534,0,38,0,0,0,"2020-08-26 10:22:11"
|
||||
"chrrris1987 (Chrrris1987)",Dutch,467,478,0,23,0,0,0,"2020-02-03 05:26:04"
|
||||
"Huy Ngo (huyngo)",Vietnamese,461,695,0,1,0,0,0,"2020-01-26 11:58:36"
|
||||
"黄克 (hk13127)","Chinese Simplified",461,765,0,1,0,0,24,"2020-01-17 23:16:03"
|
||||
"Arkadiusz Bubak (epitek)",Polish,458,416,29,24,0,3,0,"2020-11-05 05:11:58"
|
||||
"Huy Ngo (huyngo)",Vietnamese,461,695,0,1,0,0,0,"2020-01-26 11:58:36"
|
||||
"Arkadiusz Bubak (epitek)",Polish,458,416,52,24,9,4,0,"2020-11-05 05:11:58"
|
||||
marco.baturan,Esperanto,452,452,0,0,0,0,0,"2020-06-23 02:49:46"
|
||||
"Sief Tarek (sieftarek135)",Arabic,447,455,0,0,0,0,0,"2021-02-07 14:35:21"
|
||||
"Alparslan Şakçi (sakci)",Turkish,436,372,0,118,1,0,0,"2022-01-14 12:03:11"
|
||||
JY3,"Chinese Simplified",427,727,295,0,1,0,222,"2021-03-08 08:53:35"
|
||||
"Samuel Guay (SamGuay)",French,426,486,0,6,0,0,0,"2020-06-25 07:14:38"
|
||||
"Diana Karaseva (Sun_Dianka)",Russian,399,373,0,10,1,0,209,"2020-01-30 06:40:02"
|
||||
"Alexander Jansson (dalecarlian)",Swedish,396,406,507,0,0,3,399,"2017-06-21 01:37:32"
|
||||
luiandresgonzalez,Spanish,383,403,0,1,28,0,0,"2020-07-11 14:20:44"
|
||||
"Thamara Andrade (tkcandrade)","Portuguese, Brazilian",380,387,0,0,1,0,252,"2020-01-09 19:35:48"
|
||||
"Thamara Andrade (tkcandrade)","Portuguese, Brazilian",380,387,0,0,1,0,239,"2020-01-09 19:35:48"
|
||||
"Sølv Ræven (soelvraeven)",Danish,370,370,0,0,0,0,0,"2020-11-28 16:46:18"
|
||||
"Isti (eisti)",Hungarian,367,329,0,0,0,0,0,"2020-12-03 12:02:51"
|
||||
"Anh Quân (dangquanuet)",Vietnamese,362,530,0,42,2,0,0,"2017-10-29 12:27:44"
|
||||
gapszi,Hungarian,348,301,0,86,0,0,0,"2019-04-08 01:35:54"
|
||||
JY3,"Chinese Simplified",345,585,278,0,1,0,207,"2021-03-08 08:53:35"
|
||||
"Mahdi Nasiri (mahdi.nasiri)",Persian,343,465,0,39,3,1,0,"2017-07-14 09:17:25"
|
||||
Seoyul,Korean,339,825,0,0,27,0,0,"2017-06-21 08:11:39"
|
||||
"Magimai Prakasam (magimai)",Tamil,336,831,0,12,0,0,0,"2018-04-15 21:16:08"
|
||||
"Michael Malak (MichaelKMalak)",Arabic,304,271,0,0,0,0,0,"2020-05-26 19:47:58"
|
||||
"Michael Malak (MichaelKMalak)",Arabic,304,271,0,0,1,0,0,"2020-05-26 19:47:58"
|
||||
Blinkin,Dutch,297,334,0,5,0,0,0,"2021-06-14 10:30:05"
|
||||
"Elina Salminen (salminen.elina.m)",Finnish,297,227,0,0,0,0,0,"2021-01-06 01:28:57"
|
||||
ayane.m,Japanese,292,863,0,1,3,0,22,"2019-11-20 03:28:26"
|
||||
Blinkin,Dutch,284,318,0,1,0,0,0,"2021-06-14 10:30:05"
|
||||
"Marius Teufelweich (teufelweich)",German,249,256,606,4,2,0,146,"2021-03-12 04:11:38"
|
||||
ayane.m,Japanese,292,863,0,1,5,0,22,"2019-11-20 03:28:26"
|
||||
"Marius Teufelweich (teufelweich)",German,267,272,611,4,13,1,146,"2021-03-12 04:11:38"
|
||||
hypnotichemionus,"Chinese Simplified",249,430,0,0,8,0,19,"2020-03-08 01:46:25"
|
||||
cobalt59,German,237,234,0,1,24,1,132,"2017-06-05 05:18:33"
|
||||
"QWERT (lurenjia01)","Chinese Simplified",236,407,0,0,8,0,19,"2020-03-08 01:46:25"
|
||||
beriain,Basque,234,235,0,0,2,0,0,"2017-03-31 15:42:28"
|
||||
pnhpnh,Vietnamese,225,343,0,1,3,0,0,"2017-11-27 12:06:07"
|
||||
"Dika Fitrian Dwi Putra (OsamuDazai)",Indonesian,221,215,0,0,0,0,48,"2020-07-13 04:40:27"
|
||||
easyrepro,Telugu,214,297,0,0,0,0,0,"2020-06-12 12:52:10"
|
||||
easyrepro,Telugu,214,297,0,0,4,0,0,"2020-06-12 12:52:10"
|
||||
taras-ko,Ukrainian,211,183,0,1,4,0,19,"2017-10-26 16:52:22"
|
||||
sojusnik,German,207,200,1,0,30,0,66,"2017-04-03 17:11:56"
|
||||
axmed99,Ukrainian,203,177,0,40,0,0,53,"2021-04-01 03:56:20"
|
||||
"Andrij Mizyk (andmizyk)",Ukrainian,204,178,0,40,0,0,53,"2021-04-01 03:56:20"
|
||||
"Heru Yen (heruyen)",Indonesian,201,201,0,0,0,0,25,"2020-06-29 18:39:15"
|
||||
"Vijaykumar Borkar (vjkumar)",Hindi,200,364,0,11,0,0,0,"2021-08-06 16:12:15"
|
||||
Ishmaeel,Turkish,193,174,0,129,6,0,0,"2017-10-04 03:54:00"
|
||||
_translator,French,199,227,0,11,0,0,0,"2021-07-06 07:54:12"
|
||||
Ishmaeel,Turkish,193,174,0,129,17,6,0,"2017-10-04 03:54:00"
|
||||
oscfd,Spanish,192,201,0,2,4,0,0,"2021-05-21 17:58:22"
|
||||
bruhwut,Vietnamese,189,292,0,1,0,0,0,"2021-05-21 07:16:30"
|
||||
"Aputsiak Niels Janussen (aputtu)",Danish,187,200,0,0,0,0,0,"2019-08-28 05:47:42"
|
||||
_translator,French,181,206,0,11,0,0,0,"2021-07-06 07:54:12"
|
||||
fbruna17,Danish,181,179,0,1,0,0,0,"2021-01-28 15:48:47"
|
||||
Bryanx,Dutch,179,168,0,5,2,0,0,"2019-11-21 17:08:12"
|
||||
"Omry Cohen (omrycohen)",Hebrew,175,156,0,1,0,0,33,"2021-01-18 07:33:23"
|
||||
Bryanx,Dutch,174,165,0,5,0,0,0,"2019-11-21 17:08:12"
|
||||
"Pierre GALIEGUE (pierre.galiegue)",French,171,194,0,24,4,0,0,"2020-08-16 11:41:35"
|
||||
bruhwut,Vietnamese,171,268,0,1,0,0,0,"2021-05-21 07:16:30"
|
||||
plitwin,Polish,168,151,0,2,31,0,49,"2021-01-20 06:18:37"
|
||||
DionysosDV,Greek,165,153,0,0,0,0,0,"2021-02-27 19:05:25"
|
||||
"Gustavo Lima (GustavoLima)",Portuguese,158,177,0,1,4,10,0,"2020-08-26 10:35:05"
|
||||
oscfd,Spanish,155,166,0,1,4,0,0,"2021-05-21 17:58:22"
|
||||
plitwin,Polish,145,128,0,1,16,0,26,"2021-01-20 06:18:37"
|
||||
"Ravi Rami (ramiravi)",Hindi,151,248,0,0,0,0,0,"2021-10-10 09:19:40"
|
||||
"Lương Vĩnh Khang (LuongVinhKhang)",Vietnamese,144,256,0,0,46,1,0,"2017-08-10 10:05:58"
|
||||
azzamsa,Indonesian,142,136,0,48,0,1,26,"2017-06-16 18:29:45"
|
||||
"yoding (yodingc)","Chinese Traditional; Chinese Simplified",141,271,0,10,0,0,0,"2021-07-07 01:45:45"
|
||||
"Neysa Nasywa (neysanasywa)",Indonesian,140,141,0,0,0,0,60,"2020-11-18 10:32:10"
|
||||
mohmans,Arabic,139,141,0,12,1,0,0,"2020-11-23 02:48:00"
|
||||
"Eilif Adelvice (adelvice)",Spanish,139,154,0,96,1,0,0,"2021-08-05 07:20:21"
|
||||
"Mohammed Imthath (mimthath4)",Tamil,136,274,0,0,11,0,0,"2018-02-15 22:41:15"
|
||||
carllacan,Catalan,134,155,0,2,0,0,0,"2021-11-13 13:12:07"
|
||||
roptat,French,132,154,0,112,89,5,0,"2017-04-19 16:54:47"
|
||||
"Trần Thái (tranhoangthai2001)",Vietnamese,127,186,0,8,1,0,0,"2018-03-01 10:51:39"
|
||||
"OP Smosher (teenwolffan44)","Serbian (Cyrillic)",124,122,0,0,0,0,18,"2020-11-05 09:41:35"
|
||||
4001982248998,Esperanto,122,119,0,0,0,0,0,"2017-10-08 04:13:02"
|
||||
"StoP4Me (Lcqp)",Romanian,121,119,0,0,3,0,0,"2018-05-06 18:51:59"
|
||||
alalloush,Arabic,118,129,0,2,2,0,0,"2017-03-31 12:37:17"
|
||||
"Eilif Adelvice (adelvice)",Spanish,116,126,0,96,0,0,0,"2021-08-05 07:20:21"
|
||||
Sebastian05067,Spanish,114,133,0,55,28,0,0,"2017-05-14 00:48:16"
|
||||
alalloush,Arabic,118,129,0,2,14,3,0,"2017-03-31 12:37:17"
|
||||
"Tanya (MagicUnderHood)",Russian,114,98,0,19,0,0,54,"2019-04-21 10:44:03"
|
||||
REMOVED_USER,Arabic,111,106,0,22,21,2,0,"2018-01-05 07:01:45"
|
||||
mohmans,Arabic,109,103,0,2,0,0,0,"2020-11-23 02:48:00"
|
||||
Sebastian05067,Spanish,114,133,0,55,28,0,0,"2017-05-14 00:48:16"
|
||||
REMOVED_USER,Arabic,111,106,0,22,22,2,0,"2018-01-05 07:01:45"
|
||||
"Iabin Arteaga (iabin)",Spanish,108,111,0,4,21,0,0,"2017-08-26 21:08:54"
|
||||
"Ivan Krušlin (krux3r)",Croatian,108,122,503,0,0,0,108,"2017-03-31 09:15:24"
|
||||
2kaafone,Finnish,105,90,0,0,0,0,0,"2019-08-12 06:58:48"
|
||||
"Adam Jurkiewicz (hasztagg)",Polish,104,105,529,0,0,0,104,"2017-03-31 09:50:51"
|
||||
"just a name bro (justanamebr0)",Danish,98,109,0,0,1,0,0,"2019-06-19 11:57:55"
|
||||
"Nam Nguyen (namnl2706)",Vietnamese,95,137,0,0,0,0,0,"2020-08-18 23:02:33"
|
||||
"손유정 (yuwon1213)",Korean,95,57,0,0,0,0,0,"2021-03-30 05:25:33"
|
||||
"손유정 (yuwon1213)",Korean,95,57,0,0,1,0,0,"2021-03-30 05:25:33"
|
||||
ranmagen,Hebrew,91,78,0,0,0,0,0,"2021-02-16 05:44:31"
|
||||
LoneWanderer,"Chinese Traditional",90,137,0,4,0,0,0,"2020-09-29 05:24:48"
|
||||
"Vo - (voyl)","Chinese Traditional",89,126,0,0,5,0,0,"2020-09-02 23:34:42"
|
||||
ikkaz,Indonesian,89,84,0,5,0,0,4,"2019-09-02 19:58:54"
|
||||
"Vo - (voyl)","Chinese Traditional",89,126,0,0,5,0,0,"2020-09-02 23:34:42"
|
||||
"Irene K (Heaun)",Korean,88,75,0,25,0,0,0,"2020-03-16 11:31:12"
|
||||
Prosta4ok_ua,Ukrainian,87,84,0,1,0,0,17,"2020-01-23 19:43:41"
|
||||
"Kumar Anand (kumar0500)",Hindi,87,125,0,0,0,0,0,"2020-11-07 02:46:09"
|
||||
"Ohad Edri (ohadalte)",Hebrew,85,79,0,0,1,3,18,"2020-07-04 03:42:09"
|
||||
"Ohad Edri (ohadalte)",Hebrew,85,79,0,0,1,3,13,"2020-07-04 03:42:09"
|
||||
helectron,Persian,84,102,0,1,0,0,0,"2021-03-02 04:10:51"
|
||||
"Radu Cebotari (wildProgrammer)",Romanian,84,92,0,1,0,0,0,"2020-02-05 01:20:00"
|
||||
"Bruces Lee (aplusbdesign)",Korean,82,66,0,0,0,0,0,"2021-08-23 11:27:18"
|
||||
"Israa Z (sosozozo)",Arabic,79,87,0,43,14,0,3,"2017-11-27 14:10:50"
|
||||
"Sofia Neves (sofiasonev)","Portuguese, Brazilian",79,84,0,1,0,0,46,"2020-03-12 18:19:46"
|
||||
"Jacob Roller (jdr28070)",Korean,79,61,0,0,0,0,0,"2020-01-03 11:36:40"
|
||||
"Jacob Roller (jdr28070)",Korean,79,61,0,0,1,0,0,"2020-01-03 11:36:40"
|
||||
Tiralka,French,79,91,0,92,1,0,0,"2018-02-09 18:39:01"
|
||||
"Israa Z (sosozozo)",Arabic,79,87,0,43,12,0,3,"2017-11-27 14:10:50"
|
||||
"Toni Mustonen (toni.mustonen)",Finnish,78,72,0,0,0,0,0,"2017-09-02 05:34:12"
|
||||
"Fauz Aladeem (topfauz)",Arabic,76,77,0,0,0,1,0,"2020-02-21 22:46:12"
|
||||
"Toni Mustonen (toni.mustonen)",Finnish,78,72,0,0,5,0,0,"2017-09-02 05:34:12"
|
||||
"Michael (quelbs)",German,76,75,0,1,0,0,39,"2020-08-18 07:39:26"
|
||||
"Oliver Gronowski (OliverGronowski)",German,70,69,0,5,0,0,0,"2021-05-14 16:37:10"
|
||||
"Fauz Aladeem (topfauz)",Arabic,76,77,0,0,0,1,0,"2020-02-21 22:46:12"
|
||||
"Radoslaw Biernacki (radoslaw.biernacki)",Polish,70,74,0,56,1,0,1,"2020-12-15 17:55:31"
|
||||
"Oliver Gronowski (OliverGronowski)",German,70,69,0,5,2,0,0,"2021-05-14 16:37:10"
|
||||
RealDonald,Dutch,67,69,0,121,10,0,0,"2017-06-23 20:10:12"
|
||||
sirekanyan,"Armenian; Russian",66,65,0,0,0,0,0,"2020-04-18 11:32:52"
|
||||
"Константин К. (kocyak1991)",Russian,64,60,0,0,1,2,0,"2018-06-10 13:39:37"
|
||||
"Laura Sophie (laurasophie20)",German,62,67,0,4,0,0,0,"2018-01-06 14:21:24"
|
||||
"Alparslan Sakci (sakci)",Turkish,61,55,0,11,0,0,0,"2021-06-10 11:59:22"
|
||||
raden20,Indonesian,61,62,177,0,1,0,64,"2017-04-09 22:04:23"
|
||||
"Peter Williams (williamspete001)",Japanese,60,173,0,2,0,0,3,"2020-01-01 13:17:44"
|
||||
"Jan Wojtecki (j4nw)",Polish,58,46,0,0,0,0,26,"2017-11-02 05:42:14"
|
||||
"Deepak Bharathi (deepakbharathi1994)",Tamil,56,107,0,0,11,4,0,"2017-09-17 08:00:31"
|
||||
"Peter Williams (williamspete001)",Japanese,55,147,0,2,0,0,3,"2020-01-01 13:17:44"
|
||||
"Андрій Козицький (andriikozytskyi1108)",Ukrainian,52,52,0,0,1,0,0,"2018-10-22 01:45:08"
|
||||
"Nil riera (nilriera2000)",Catalan,52,61,0,1,0,0,0,"2021-06-22 16:37:44"
|
||||
"Nil riera (nilriera2000)",Catalan,52,61,0,1,2,0,0,"2021-06-22 16:37:44"
|
||||
"Neoone (Neooneqq)",Romanian,51,54,0,0,0,0,0,"2022-05-05 20:42:11"
|
||||
REMOVED_USER,Italian,51,52,0,2,0,0,0,"2017-08-21 05:15:31"
|
||||
govindap,"Japanese; Hindi",51,114,0,6,1,0,0,"2020-06-02 20:15:52"
|
||||
"Mare Geldenhuys (mare.geldenhuys)",Afrikaans,50,57,0,0,0,0,0,"2017-10-20 18:00:14"
|
||||
"Mahmoud Magdy (M7moudManson)",Arabic,49,60,0,6,8,1,0,"2021-08-21 09:01:38"
|
||||
"Behnood HRazy (behnoodhr)",Persian,49,70,0,0,0,0,0,"2017-11-25 10:57:21"
|
||||
"tat bz (Tat_i)",German,48,56,0,55,0,0,27,"2021-03-26 05:12:54"
|
||||
J3ll3nl,Dutch,48,48,0,0,17,1,3,"2017-03-31 11:56:09"
|
||||
"Andrew Firnes (Anechan)",Russian,47,47,0,3,0,0,29,"2019-09-18 09:51:59"
|
||||
andowero,Czech,47,38,0,0,0,0,0,"2020-01-20 02:29:01"
|
||||
"tat bz (Tat_i)",German,48,56,0,55,0,1,27,"2021-03-26 05:12:54"
|
||||
vach,Armenian,47,36,0,0,0,0,0,"2020-04-18 16:53:12"
|
||||
"Andrew Firnes (Anechan)",Russian,47,47,0,3,0,0,29,"2019-09-18 09:51:59"
|
||||
andowero,Czech,47,38,0,0,3,0,0,"2020-01-20 02:29:01"
|
||||
"Rahul Shishodia (rahul.shishodia.10)",Hindi,46,85,0,6,5,1,0,"2018-12-24 22:18:19"
|
||||
"Coni Ragni (coni2ragnii)",Spanish,46,46,0,0,0,0,0,"2021-02-28 20:18:37"
|
||||
Cp0204,"Chinese Simplified",45,72,0,0,0,0,0,"2019-08-20 11:04:27"
|
||||
"cc (cavaz)",Italian,44,41,0,0,0,0,0,"2017-04-01 04:21:08"
|
||||
"Boban Jagertraum (boban40)",Czech,43,38,0,2,1,1,0,"2017-03-31 09:39:16"
|
||||
"Kamil Dziadek (prso94)",Polish,43,39,0,0,2,0,0,"2020-04-06 17:12:06"
|
||||
"Me Me (gentelwom)",Arabic,42,40,0,0,0,0,0,"2020-11-08 20:44:01"
|
||||
"Ali Elsheikh (aelsheikh1987)",Arabic,42,41,0,0,0,0,0,"2021-06-16 10:17:26"
|
||||
"Balázs Keresztury (belidzs)",Hungarian,42,41,501,0,7,0,38,"2017-04-06 02:40:24"
|
||||
"Boban Jagertraum (boban40)",Czech,43,38,0,2,18,1,0,"2017-03-31 09:39:16"
|
||||
"Kamil Dziadek (prso94)",Polish,43,39,0,0,6,0,0,"2020-04-06 17:12:06"
|
||||
andreea.muscalagiu,Romanian,42,52,0,1,0,0,0,"2017-10-22 07:19:49"
|
||||
"Mateusz Duda (MateuszDuda)",Polish,42,42,0,0,0,0,0,"2021-08-17 11:27:11"
|
||||
MStefanov,Bulgarian,41,55,2,0,2,0,2,"2017-03-31 16:09:02"
|
||||
"Sofia Veijonen (Suklaa) (sofia.veijonen)",Finnish,40,33,0,0,0,0,0,"2018-03-07 09:24:22"
|
||||
"Me Me (gentelwom)",Arabic,42,40,0,0,0,0,0,"2020-11-08 20:44:01"
|
||||
"Balázs Keresztury (belidzs)",Hungarian,42,41,501,0,7,0,38,"2017-04-06 02:40:24"
|
||||
"Mateusz Duda (MateuszDuda)",Polish,42,42,0,0,6,0,0,"2021-08-17 11:27:11"
|
||||
"Ali Elsheikh (aelsheikh1987)",Arabic,42,41,0,0,0,0,0,"2021-06-16 10:17:26"
|
||||
"Ali Zali (stm19951995)",Persian,40,60,0,0,0,0,0,"2020-03-23 19:57:26"
|
||||
"Limin Lu (liminlu)","Chinese Simplified",39,79,503,0,0,0,39,"2017-03-31 09:49:35"
|
||||
"Sofia Veijonen (Suklaa) (sofia.veijonen)",Finnish,40,33,0,0,0,0,0,"2018-03-07 09:24:22"
|
||||
dusanstrgar,Slovenian,39,41,0,0,0,0,0,"2017-03-31 10:30:28"
|
||||
"Limin Lu (liminlu)","Chinese Simplified",39,79,503,0,0,0,39,"2017-03-31 09:49:35"
|
||||
Anshoe,Tamil,38,65,0,14,0,0,0,"2018-01-02 11:06:52"
|
||||
anasshm,Arabic,37,36,0,9,0,0,0,"2019-01-27 04:07:22"
|
||||
hrexen,Armenian,37,37,0,0,0,0,0,"2020-12-09 02:30:34"
|
||||
"Abdulrahman (D7M)",Arabic,36,39,0,0,0,0,0,"2020-01-29 18:55:30"
|
||||
"Maria Chushnyakova (maria.ch)",Russian,36,31,0,3,0,0,0,"2021-08-17 03:23:58"
|
||||
REMOVED_USER,Swedish,36,33,0,5,1,0,0,"2018-09-29 17:47:33"
|
||||
xphsis,Basque,36,31,0,0,0,0,0,"2022-01-02 08:16:19"
|
||||
"Maria Chushnyakova (maria.ch)",Russian,36,31,0,3,0,0,0,"2021-08-17 03:23:58"
|
||||
"長谷川知里 (chase0213)",Japanese,34,138,0,13,0,0,24,"2018-12-14 10:52:44"
|
||||
"Piotr Łuczyński (peterluczynski)",Polish,33,30,0,6,10,0,2,"2020-01-29 07:27:40"
|
||||
"Luis E. Perichon (luisperichon)",Spanish,33,40,0,104,0,0,0,"2017-09-04 13:46:06"
|
||||
"Piotr Łuczyński (peterluczynski)",Polish,33,30,0,6,5,0,2,"2020-01-29 07:27:40"
|
||||
"milad farahani (miladfarmahini90)",Persian,33,44,0,18,1,0,3,"2017-08-31 16:09:00"
|
||||
JoeLi,"Chinese Traditional",31,70,0,12,0,0,24,"2017-06-25 05:32:48"
|
||||
andriikozytskyi2625,Ukrainian,31,23,0,0,0,0,0,"2019-07-08 00:16:41"
|
||||
REMOVED_USER,Russian,31,30,0,2,4,0,3,"2018-12-03 23:55:47"
|
||||
Moastafa,Arabic,31,25,0,0,0,0,0,"2020-07-06 11:37:53"
|
||||
"hamza gamal (hamzagamal4444)",Arabic,31,28,0,0,0,0,0,"2020-08-03 15:23:34"
|
||||
REMOVED_USER,Russian,31,30,0,2,4,0,3,"2018-12-03 23:55:47"
|
||||
JoeLi,"Chinese Traditional",31,70,0,12,0,0,24,"2017-06-25 05:32:48"
|
||||
yancyn,"Chinese Simplified",30,40,0,0,0,0,1,"2020-05-18 20:06:03"
|
||||
"비니몬youtube (khj01025276475)",Korean,29,25,0,0,0,0,0,"2020-02-09 20:44:35"
|
||||
"Ruud Schouten (ruudschouten)",Dutch,29,32,0,41,3,0,0,"2017-07-22 17:49:17"
|
||||
"Aaron Dalton (Perlkonig)",French,26,25,0,141,1,0,0,"2018-01-14 12:58:19"
|
||||
"비니몬youtube (khj01025276475)",Korean,29,25,0,0,0,0,0,"2020-02-09 20:44:35"
|
||||
avelneve,Indonesian,29,28,0,0,0,0,0,"2022-04-13 13:26:10"
|
||||
"Niraj Yadav (neverforgetniraj)",Hindi,26,48,0,0,0,0,0,"2017-04-11 02:26:50"
|
||||
"Guillaume Collic (gcollic)",French,26,28,0,126,11,0,0,"2017-05-05 16:13:00"
|
||||
"Radoslaw Biernacki (radoslaw.biernacki)",Polish,26,24,0,8,0,0,1,"2020-12-15 17:55:31"
|
||||
"Aaron Dalton (Perlkonig)",French,26,25,0,141,1,0,0,"2018-01-14 12:58:19"
|
||||
"Jonny I (jonny99dj)",Italian,26,26,0,5,0,0,0,"2017-10-07 07:35:34"
|
||||
"Guillaume Collic (gcollic)",French,26,28,0,126,11,0,0,"2017-05-05 16:13:00"
|
||||
Pan_Filuta,Czech,25,21,0,5,8,0,3,"2017-04-29 12:55:14"
|
||||
"Eddie (eddieattaboy)","Chinese Traditional",25,34,0,1,0,0,0,"2020-11-04 21:48:05"
|
||||
Pan_Filuta,Czech,25,21,0,5,4,0,3,"2017-04-29 12:55:14"
|
||||
"eduard83 (barbany.eduard)",Catalan,24,25,0,2,0,0,0,"2019-06-26 14:59:47"
|
||||
"A Aa (ylayzlmimashisafyoutub)",Arabic,23,33,0,34,1,1,0,"2021-09-27 15:34:26"
|
||||
"Caner Başaran (basarancaner)",Turkish,23,21,0,0,26,1,0,"2017-04-09 06:34:59"
|
||||
"Ľuboš Čaky (lubos.caky)",Slovak,23,22,0,0,0,0,0,"2019-07-02 16:51:44"
|
||||
"Caner Başaran (basarancaner)",Turkish,23,21,0,0,21,0,0,"2017-04-09 06:34:59"
|
||||
hodanli,Turkish,22,26,0,0,1,0,0,"2017-11-03 14:33:41"
|
||||
gnu-ewm,Polish,22,23,0,6,0,0,0,"2021-02-24 03:42:01"
|
||||
"Neeraj Verma (verma.neeraj.in)",Hindi,22,37,0,0,1,0,0,"2018-07-23 07:16:41"
|
||||
gnu-ewm,Polish,22,23,0,6,2,0,0,"2021-02-24 03:42:01"
|
||||
hodanli,Turkish,22,26,0,0,1,0,0,"2017-11-03 14:33:41"
|
||||
"Alcarkse (alexis.brusle)",French,21,25,0,7,11,0,0,"2017-08-06 09:32:29"
|
||||
olbotta,Italian,20,25,0,2,0,0,0,"2021-06-06 04:22:55"
|
||||
"Shashwat (goforgold)",Hindi,20,33,0,0,0,0,0,"2020-05-17 10:34:42"
|
||||
olbotta,Italian,20,25,0,2,0,0,0,"2021-06-06 04:22:55"
|
||||
can13,Turkish,19,14,0,8,0,0,0,"2021-01-03 10:39:03"
|
||||
"사자솥 (toke1597)",Korean,19,19,0,0,0,0,0,"2020-02-04 13:36:11"
|
||||
KenKailer,Arabic,19,25,0,0,0,0,0,"2022-05-10 06:16:54"
|
||||
"İsa Eş (IsaEs)",Turkish,19,17,0,0,6,2,0,"2017-06-20 07:30:22"
|
||||
"Magdalena Urbańczyk (madziia139)",Polish,19,19,0,0,0,0,0,"2017-10-21 03:01:04"
|
||||
sheeCesu,French,19,18,0,48,4,0,0,"2017-12-21 17:01:39"
|
||||
can13,Turkish,19,14,0,8,0,0,0,"2021-01-03 10:39:03"
|
||||
"İsa Eş (IsaEs)",Turkish,19,17,0,0,6,1,0,"2017-06-20 07:30:22"
|
||||
"사자솥 (toke1597)",Korean,19,19,0,0,0,0,0,"2020-02-04 13:36:11"
|
||||
axikman11111,Uyghur,18,19,0,0,0,0,0,"2018-10-13 12:25:31"
|
||||
"Hoon Jung (hooni100)",Korean,17,10,0,0,0,0,0,"2021-01-03 02:26:54"
|
||||
"Ceara Lopez (cealopez)",Spanish,17,18,0,0,5,1,0,"2017-08-22 22:56:13"
|
||||
takoyakibento,Korean,17,13,0,3,0,0,0,"2020-08-01 08:44:15"
|
||||
Adeline31,French,17,20,0,3,0,0,0,"2019-12-06 00:00:11"
|
||||
engineeringforgood,Russian,16,15,0,0,0,0,16,"2021-01-22 03:32:35"
|
||||
"Hoon Jung (hooni100)",Korean,17,10,0,0,0,0,0,"2021-01-03 02:26:54"
|
||||
takoyakibento,Korean,17,13,0,3,0,0,0,"2020-08-01 08:44:15"
|
||||
"Ceara Lopez (cealopez)",Spanish,17,18,0,0,5,1,0,"2017-08-22 22:56:13"
|
||||
bretzel15,German,16,20,0,0,0,0,0,"2020-04-06 02:49:14"
|
||||
"Şamil Ateşoğlu (m.samilatesoglu)",Turkish,16,22,0,11,6,3,0,"2017-07-05 18:37:08"
|
||||
DebatablySane,Bulgarian,16,15,0,48,0,0,0,"2017-07-10 15:13:18"
|
||||
"Şamil Ateşoğlu (m.samilatesoglu)",Turkish,16,22,0,11,6,3,0,"2017-07-05 18:37:08"
|
||||
engineeringforgood,Russian,16,15,0,0,0,0,16,"2021-01-22 03:32:35"
|
||||
"Bhava Tharini (bhavidanush)",Tamil,15,37,0,0,0,0,0,"2019-10-09 05:43:11"
|
||||
"Maro Chr (caprisunglasses)",Greek,14,17,0,0,0,0,0,"2021-08-17 06:53:33"
|
||||
"Zeynep Esen (nezihaesen50)",Turkish,14,13,0,0,0,0,0,"2020-01-28 07:05:15"
|
||||
iamsurajbobade,Hindi,14,30,0,0,0,0,0,"2018-05-21 11:23:27"
|
||||
"Faiz Ahamed (faiznewton)",Tamil,14,31,0,0,0,0,0,"2021-05-06 23:06:46"
|
||||
"Sanji Vinsmock (mukanzhanbolat4)",Russian,14,14,0,0,0,0,0,"2020-02-18 12:38:54"
|
||||
"Zeeshan Rabbani (Zeera)",Hindi,14,25,0,0,0,0,0,"2020-09-15 11:32:01"
|
||||
"pi hobbes (uwe_silv)",Japanese,14,46,0,0,0,0,0,"2022-01-15 02:57:14"
|
||||
"Anastasia Borchuk (al2.borchuk)",Russian,14,14,0,0,0,0,0,"2020-04-14 13:22:49"
|
||||
"Fikret Bilici (fikretbilici)",Turkish,14,13,0,0,0,0,0,"2020-06-21 17:16:11"
|
||||
"EuiHo Hwang (euiho.hwang)",Korean,14,16,0,0,0,0,0,"2020-06-23 02:40:01"
|
||||
"Zeeshan Rabbani (Zeera)",Hindi,14,25,0,0,0,0,0,"2020-09-15 11:32:01"
|
||||
"Faiz Ahamed (faiznewton)",Tamil,14,31,0,0,0,0,0,"2021-05-06 23:06:46"
|
||||
"Anastasia Borchuk (al2.borchuk)",Russian,14,14,0,0,0,0,0,"2020-04-14 13:22:49"
|
||||
iamsurajbobade,Hindi,14,30,0,0,0,0,0,"2018-05-21 11:23:27"
|
||||
"Sanji Vinsmock (mukanzhanbolat4)",Russian,14,14,0,0,0,0,0,"2020-02-18 12:38:54"
|
||||
"Maro Chr (caprisunglasses)",Greek,14,17,0,0,0,0,0,"2021-08-17 06:53:33"
|
||||
"Nenad Vukotic (vukotic.nenad)","Serbian (Cyrillic)",13,13,0,1,2,6,0,"2019-01-31 14:29:15"
|
||||
"Uwe Mönks (schirinowski)",German,13,12,0,0,0,0,0,"2021-02-18 04:00:41"
|
||||
"Dave (xdave)",Hungarian,13,11,0,0,0,0,0,"2020-03-02 20:56:50"
|
||||
"Ana Kelly Vale (anakvale)","Portuguese, Brazilian",13,21,0,4,0,0,2,"2022-03-30 00:15:37"
|
||||
GiorgioHerbie,Italian,13,15,0,0,0,0,0,"2022-01-17 17:35:40"
|
||||
"Nenad Vukotic (vukotic.nenad)","Serbian (Cyrillic)",13,13,0,1,2,6,0,"2019-01-31 14:29:15"
|
||||
soura2,Arabic,12,13,0,0,0,0,0,"2020-01-13 19:23:47"
|
||||
"shreyas (techiespace)",Hindi,12,20,0,0,0,0,0,"2018-06-10 01:14:26"
|
||||
"Sonu Sharma (riteetude)",Hindi,11,23,0,0,0,0,0,"2021-05-30 19:38:00"
|
||||
Vmrc,French,11,12,0,2,0,0,0,"2020-11-02 05:35:06"
|
||||
"Jo Chuang (josephch405)","Chinese Traditional",11,24,0,0,0,0,11,"2017-06-16 20:21:06"
|
||||
"sathvic k (sathvictripleseven)",Telugu,10,17,0,0,0,0,0,"2020-09-11 08:11:32"
|
||||
"Brian Camacho (bmcamacho)",Polish,10,11,0,0,1,0,0,"2020-08-03 02:27:28"
|
||||
"Anonymous edgy nerd (yamentaad)",Arabic,10,13,0,1,0,0,0,"2018-05-06 09:23:57"
|
||||
Vmrc,French,11,12,0,2,0,0,0,"2020-11-02 05:35:06"
|
||||
"Ammar Naif (Ammar_Naif)",Arabic,11,11,0,4,0,0,0,"2022-01-15 05:16:41"
|
||||
"Sonu Sharma (riteetude)",Hindi,11,23,0,0,0,0,0,"2021-05-30 19:38:00"
|
||||
"Edwin van Rooij (edwinvrooij)",Dutch,10,13,0,17,0,0,0,"2018-11-05 03:59:10"
|
||||
"Brian Camacho (bmcamacho)",Polish,10,11,0,0,1,1,0,"2020-08-03 02:27:28"
|
||||
"Mihael Wagner (miha.wagner)",Slovenian,10,9,0,7,0,0,0,"2017-10-18 18:26:29"
|
||||
"Hrant Hakobian (hrastgh1)",Armenian,10,9,0,0,0,0,0,"2021-08-29 15:22:10"
|
||||
"sathvic k (sathvictripleseven)",Telugu,10,17,0,0,0,0,0,"2020-09-11 08:11:32"
|
||||
"Ahmed Mosaad (ahmed.mosaad2018)",Arabic,10,12,0,6,0,0,0,"2021-02-03 18:45:43"
|
||||
"Anonymous edgy nerd (yamentaad)",Arabic,10,13,0,1,0,0,0,"2018-05-06 09:23:57"
|
||||
"Zesar Cebrián (Txorrota)",Spanish,10,44,0,0,0,0,0,"2022-02-09 01:34:32"
|
||||
"Milan Siebenbürger (lennyd)",Czech,10,7,0,1,0,0,0,"2022-01-30 07:09:42"
|
||||
"Suhaili Hassan (kucingsyg96)",Indonesian,9,10,0,0,0,0,0,"2018-06-10 11:55:09"
|
||||
"Sourire Lucide (sourire_lucide)",Russian,9,10,0,0,1,0,0,"2018-03-22 01:37:55"
|
||||
"Martin Vostatek (martinvostatek)",Czech,9,8,0,32,2,0,0,"2019-01-21 13:52:36"
|
||||
"Seweryn Piotrowski (Draxxsx)",Polish,9,10,0,0,19,0,0,"2020-01-02 09:55:48"
|
||||
"Sourire Lucide (sourire_lucide)",Russian,9,10,0,0,1,0,0,"2018-03-22 01:37:55"
|
||||
"Jakob Weickmann (jweickm)",Japanese,8,21,0,0,0,0,0,"2021-10-05 11:10:25"
|
||||
Rex123,Persian,8,8,0,0,0,0,0,"2017-07-01 00:47:42"
|
||||
"Andrey ZaXeLoN (waragaa)",Russian,7,7,0,8,1,0,0,"2017-09-18 21:37:42"
|
||||
"Konstantin (KZhidovinov)",Russian,7,7,0,0,0,0,0,"2020-01-29 13:35:12"
|
||||
"Андрій Козицький (andriikozytskyi3807)",Ukrainian,7,12,0,2,0,0,0,"2020-09-26 20:31:56"
|
||||
ftfoi,Norwegian,7,6,0,0,0,0,0,"2020-04-11 20:42:35"
|
||||
"Vladimir Pavlychev (KeyJoo)",Russian,7,9,0,0,0,0,0,"2017-12-18 02:46:56"
|
||||
"Vladimir Pavlychev (vovs03)",Russian,7,9,0,0,0,0,0,"2017-12-18 02:46:56"
|
||||
"Felipe Chagas (chagretes)","Portuguese, Brazilian",7,8,0,0,3,0,5,"2022-01-10 12:20:25"
|
||||
"Андрій Козицький (andriikozytskyi3807)",Ukrainian,7,12,0,2,0,0,0,"2020-09-26 20:31:56"
|
||||
pkorove,Greek,7,7,0,0,0,0,0,"2020-03-07 11:36:12"
|
||||
erfan2927,Persian,6,6,0,0,0,0,0,"2018-04-09 02:12:44"
|
||||
"Burak Ceylan (7burakceylan)",Turkish,6,6,0,0,0,0,0,"2018-05-20 17:24:19"
|
||||
ChloeLiang,Japanese,6,22,0,0,1,0,3,"2017-08-08 05:02:59"
|
||||
"Sam (SorodonSorodon)",German,6,6,0,13,0,0,0,"2017-04-14 11:09:27"
|
||||
"닉닉 (seohu9466)",Korean,6,14,0,13,0,0,0,"2017-10-09 23:08:15"
|
||||
"Sarita Cajas (sarayanacajas)",Spanish,6,4,0,0,1,0,0,"2021-05-14 14:27:59"
|
||||
ChloeLiang,Japanese,6,22,0,0,1,0,3,"2017-08-08 05:02:59"
|
||||
"Manuel Tassi (Mannivu)",Italian,5,6,0,0,0,0,0,"2021-01-03 11:00:33"
|
||||
"Tomáš Hrabáček (Hrabyyy)",Czech,5,3,0,0,0,0,0,"2021-05-27 11:58:11"
|
||||
erfan2927,Persian,6,6,0,0,0,0,0,"2018-04-09 02:12:44"
|
||||
"Burak Ceylan (7burakceylan)",Turkish,6,6,0,0,0,0,0,"2018-05-20 17:24:19"
|
||||
andriikozytskyi2018,Ukrainian,5,5,0,0,0,0,0,"2017-09-03 05:24:43"
|
||||
"Vitor Henrique (vitorhcl)","Portuguese, Brazilian",5,8,0,1,0,0,0,"2022-03-08 20:00:59"
|
||||
"Matthias Joly (joly.matt12)",French,5,8,0,27,1,0,0,"2017-08-28 09:53:59"
|
||||
"Tomáš Hrabáček (Hrabyyy)",Czech,5,3,0,0,1,0,0,"2021-05-27 11:58:11"
|
||||
"Guerra Ivaneth (rossanaiva-04)",Spanish,5,7,0,0,0,0,0,"2019-02-03 16:48:59"
|
||||
"Дмитрий Хапенков (d.khapenkov)",Russian,5,5,0,6,4,0,2,"2018-01-06 23:00:43"
|
||||
"Matthias Joly (joly.matt12)",French,5,8,0,27,1,0,0,"2017-08-28 09:53:59"
|
||||
"Micaela Pighin (micaelapiighin)",Spanish,5,6,0,1,0,0,0,"2019-10-09 23:32:42"
|
||||
andriikozytskyi2018,Ukrainian,5,5,0,0,0,0,0,"2017-09-03 05:24:43"
|
||||
marmo,German,4,4,0,0,0,0,0,"2021-01-13 01:16:35"
|
||||
"Eli Besirov (elibesirov07)",Turkish,4,4,0,0,0,0,0,"2019-03-25 07:12:34"
|
||||
"Lopo Isaac Fernández (rocapata)",Spanish,4,3,0,0,0,0,0,"2018-09-20 11:46:22"
|
||||
bziuum,Polish,4,4,0,0,0,0,0,"2020-09-01 09:08:01"
|
||||
"Manuel Tassi (Mannivu)",Italian,5,6,0,0,0,0,0,"2021-01-03 11:00:33"
|
||||
"Neko123 (emandic11)","Serbian (Cyrillic)",4,4,0,57,0,0,0,"2021-04-21 15:33:29"
|
||||
Magidxz,Arabic,3,3,0,0,0,0,0,"2021-01-05 05:02:54"
|
||||
"mohammadali barati (mabaraty)",Persian,3,3,0,0,0,0,0,"2021-07-10 05:54:44"
|
||||
"Lopo Isaac Fernández (rocapata)",Spanish,4,3,0,0,0,0,0,"2018-09-20 11:46:22"
|
||||
"Eli Besirov (elibesirov07)",Turkish,4,4,0,0,0,0,0,"2019-03-25 07:12:34"
|
||||
marmo,German,4,4,0,0,0,0,0,"2021-01-13 01:16:35"
|
||||
bziuum,Polish,4,4,0,0,3,0,0,"2020-09-01 09:08:01"
|
||||
"Craig Foobar (craig.foobar)",German,3,3,0,25,0,0,0,"2022-02-20 16:55:47"
|
||||
Katarin,Ukrainian,3,3,0,0,0,0,0,"2022-03-17 14:44:59"
|
||||
"Sarath S (CyberShark)",Tamil,3,7,0,0,0,0,0,"2020-08-27 22:43:16"
|
||||
"Vagner Roberto (vagner.trompete)","Portuguese, Brazilian",3,3,0,0,0,0,0,"2017-12-30 17:54:26"
|
||||
"Igor Piskun (i_piskun)",Ukrainian,3,3,0,0,0,0,0,"2018-01-19 15:20:27"
|
||||
"Cláudio Bernardo (claudiobernardo.ti)","Portuguese, Brazilian",3,4,0,1,0,0,0,"2019-01-08 14:41:10"
|
||||
"Unnie Here (Carb)",Hindi,3,8,0,0,0,0,0,"2020-03-18 23:34:35"
|
||||
REMOVED_USER,"Portuguese, Brazilian",3,4,0,0,0,0,0,"2018-11-18 09:02:37"
|
||||
"Thoum Ptrgnt (thomas.petrignet)",French,3,3,0,2,0,3,0,"2017-09-23 19:25:52"
|
||||
"Oleg Kogut (kogut_oleg)",Ukrainian,3,3,0,0,0,0,0,"2018-12-28 14:31:02"
|
||||
carsten_kafke,German,3,3,0,43,0,0,3,"2017-10-27 13:27:47"
|
||||
"Vagner Roberto (vagner.trompete)","Portuguese, Brazilian",3,3,0,0,0,0,0,"2017-12-30 17:54:26"
|
||||
"Igor Piskun (i_piskun)",Ukrainian,3,3,0,0,0,0,0,"2018-01-19 15:20:27"
|
||||
"Andrea Bianchi (andreawhite1597)",Italian,3,1,0,1,0,0,0,"2018-01-21 17:45:48"
|
||||
"Cláudio Bernardo (claudiobernardo.ti)","Portuguese, Brazilian",3,4,0,1,0,0,0,"2019-01-08 14:41:10"
|
||||
"Hiohana Rilary (hiohanarilary)","Portuguese, Brazilian",3,4,0,0,0,0,0,"2019-07-31 20:42:20"
|
||||
"joabe gabriel (joabegabrielcma1)","Portuguese, Brazilian",3,4,0,0,0,0,0,"2018-08-21 09:08:59"
|
||||
Magidxz,Arabic,3,3,0,0,0,0,0,"2021-01-05 05:02:54"
|
||||
"Péter Bernát (bernatp)",Hungarian,3,2,0,0,0,0,0,"2019-11-30 15:50:33"
|
||||
"Martin Zimdahl (zimdahlmartin)",Swedish,3,2,0,0,1,0,3,"2018-09-15 04:39:22"
|
||||
"joabe gabriel (joabegabrielcma1)","Portuguese, Brazilian",3,4,0,0,0,0,0,"2018-08-21 09:08:59"
|
||||
"Gabriel Cavalcante (gabrielc.alves14)","Portuguese, Brazilian",3,4,0,0,0,0,0,"2018-08-06 22:24:54"
|
||||
"Martin Zimdahl (zimdahlmartin)",Swedish,3,2,0,0,1,0,3,"2018-09-15 04:39:22"
|
||||
atomjani,Hungarian,3,3,0,0,0,0,0,"2019-01-19 00:49:25"
|
||||
"أم محمد تقي (souadboudia19)",Arabic,2,2,0,0,0,0,0,"2020-06-13 15:24:17"
|
||||
"FAy FAy (fayfayfay52)","Chinese Traditional",2,5,0,0,0,0,0,"2017-10-06 08:53:21"
|
||||
chavs1997,Russian,2,2,0,9,0,0,0,"2018-05-18 16:58:19"
|
||||
Soroor_SI,Persian,2,2,0,0,0,0,0,"2018-06-10 06:28:27"
|
||||
"Ilyas Fekhar (il47yas)",Arabic,2,2,0,0,0,0,0,"2018-04-17 22:00:41"
|
||||
"hesamiranii (esam.matouri)",Persian,2,2,0,0,0,0,0,"2018-09-22 16:33:36"
|
||||
"fatemeh s (fargolseifoori3)",Persian,2,2,0,0,0,0,0,"2019-01-31 12:06:57"
|
||||
amei,"Portuguese, Brazilian",2,2,0,0,0,0,0,"2018-04-19 19:42:28"
|
||||
"Naveen jai krishna (njsbpolymer1)",Tamil,2,5,0,0,0,0,0,"2020-01-10 14:19:41"
|
||||
"Danial Agh (danialagh)",Persian,2,3,0,0,0,0,0,"2019-03-30 13:24:16"
|
||||
"Walid Baazia (walidbaazia2005)",Arabic,2,1,0,0,0,0,0,"2021-01-27 12:47:34"
|
||||
"Ali Zaida (alizaeda92)",Arabic,2,2,0,0,0,0,0,"2019-12-01 11:47:00"
|
||||
LNDDYL,"Chinese Traditional",2,4,0,0,0,0,2,"2018-04-22 04:00:19"
|
||||
"Ño Bí Tã (pt614553)",Arabic,2,8,0,1,0,0,0,"2021-05-22 20:41:01"
|
||||
"mohammadali barati (mabaraty)",Persian,3,3,0,0,0,0,0,"2021-07-10 05:54:44"
|
||||
"Hiohana Rilary (hiohanarilary)","Portuguese, Brazilian",3,4,0,0,0,0,0,"2019-07-31 20:42:20"
|
||||
"Tejaswini Boppana (Tejaswini)",Telugu,3,1,0,0,0,0,0,"2021-08-27 23:48:55"
|
||||
"Andrea Bianchi (andreawhite1597)",Italian,3,1,0,1,0,0,0,"2018-01-21 17:45:48"
|
||||
"Ño Bí Tã (pt614553)",Arabic,2,8,0,1,0,2,0,"2021-05-22 20:41:01"
|
||||
"Judith Ayala (Azul1612)",Spanish,2,1,0,0,0,1,0,"2021-05-18 17:07:19"
|
||||
REMOVED_USER,Ukrainian,2,2,0,0,0,0,0,"2017-06-15 12:24:44"
|
||||
"Valerij D (vala.dobler)",German,2,2,0,0,0,0,0,"2018-09-22 09:38:27"
|
||||
"Alex Stein (diefaust1993)",Russian,2,2,0,4,4,0,2,"2017-07-13 06:56:17"
|
||||
"조화정 (yunjoo337)",Korean,2,2,0,0,0,0,0,"2019-06-16 22:25:31"
|
||||
omerfarukbas,Turkish,2,3,0,19,2,0,0,"2017-08-14 16:10:35"
|
||||
"Balthazar Aubard (Balatzar)",French,2,5,0,0,1,0,0,"2017-09-23 01:42:57"
|
||||
"Ahmed Bazazo (ahmedbazazo)",Arabic,2,2,0,0,0,0,0,"2022-02-19 20:11:09"
|
||||
"Ali Zaida (alizaeda92)",Arabic,2,2,0,0,0,0,0,"2019-12-01 11:47:00"
|
||||
"FAy FAy (fayfayfay52)","Chinese Traditional",2,5,0,0,0,0,0,"2017-10-06 08:53:21"
|
||||
Soroor_SI,Persian,2,2,0,0,0,0,0,"2018-06-10 06:28:27"
|
||||
chavs1997,Russian,2,2,0,9,0,0,0,"2018-05-18 16:58:19"
|
||||
"Naveen jai krishna (njsbpolymer1)",Tamil,2,5,0,0,0,0,0,"2020-01-10 14:19:41"
|
||||
omerfarukbas,Turkish,2,3,0,19,2,0,0,"2017-08-14 16:10:35"
|
||||
"Ilyas Fekhar (il47yas)",Arabic,2,2,0,0,0,0,0,"2018-04-17 22:00:41"
|
||||
"Héctor Mañas García (hectodium)",Catalan,2,3,0,0,0,0,0,"2021-10-02 20:32:09"
|
||||
"Walid Baazia (walidbaazia2005)",Arabic,2,1,0,0,0,0,0,"2021-01-27 12:47:34"
|
||||
"fatemeh s (fargolseifoori3)",Persian,2,2,0,0,0,0,0,"2019-01-31 12:06:57"
|
||||
"hesamiranii (esam.matouri)",Persian,2,2,0,0,0,0,0,"2018-09-22 16:33:36"
|
||||
REMOVED_USER,Ukrainian,2,2,0,0,0,0,0,"2017-06-15 12:24:44"
|
||||
"Alex Stein (diefaust1993)",Russian,2,2,0,4,4,0,2,"2017-07-13 06:56:17"
|
||||
amei,"Portuguese, Brazilian",2,2,0,0,0,0,0,"2018-04-19 19:42:28"
|
||||
"أم محمد تقي (souadboudia19)",Arabic,2,2,0,0,0,0,0,"2020-06-13 15:24:17"
|
||||
LNDDYL,"Chinese Traditional",2,4,0,0,0,0,2,"2018-04-22 04:00:19"
|
||||
"조화정 (yunjoo337)",Korean,2,2,0,0,0,0,0,"2019-06-16 22:25:31"
|
||||
"Sidali Aymen (sidaliaymen950)",Arabic,2,2,0,0,0,0,0,"2022-01-31 18:50:59"
|
||||
"Danial Agh (danialagh)",Persian,2,3,0,0,0,0,0,"2019-03-30 13:24:16"
|
||||
iSoron2,"Portuguese, Brazilian",1,1,0,0,0,0,0,"2017-03-18 17:56:29"
|
||||
"Anton (tT0NG)","Chinese Traditional",1,2,0,0,0,0,1,"2017-07-06 14:18:39"
|
||||
"Luca Gori (grolcu)",Italian,1,2,0,0,0,0,0,"2020-09-26 23:26:15"
|
||||
axd,Spanish,1,1,0,15,0,0,0,"2017-09-12 05:48:51"
|
||||
iSoron2,"Portuguese, Brazilian",1,1,0,0,0,0,0,"2017-03-18 17:56:29"
|
||||
REMOVED_USER,Russian,1,2,0,6,1,0,1,"2019-12-26 05:37:01"
|
||||
"Wibi Cahyo (wbcahyoh)",Indonesian,1,3,0,0,0,0,0,"2017-12-14 06:35:58"
|
||||
jonesses,German,1,1,0,1,0,0,1,"2021-01-01 08:03:18"
|
||||
"Anton (tT0NG)","Chinese Traditional",1,2,0,0,0,0,1,"2017-07-06 14:18:39"
|
||||
"박찌 (perpact20)",Korean,1,1,0,0,0,0,0,"2018-02-10 10:11:44"
|
||||
"Alan Jeon (skyisle)",Korean,1,2,0,8,0,0,0,"2018-01-09 10:46:00"
|
||||
"Maria Fefelova (mashafefel)",Russian,1,1,0,0,0,0,0,"2019-05-18 02:03:56"
|
||||
"Anastasiia Bondarenko (nastasya.bondarenko.97)",Russian,1,1,0,0,0,0,0,"2019-06-07 17:43:08"
|
||||
"Kan Black (kanblack.va)",Vietnamese,1,2,0,0,0,1,0,"2019-01-15 03:50:10"
|
||||
"Patrick Pimenta (trickap1)","Portuguese, Brazilian",1,1,0,0,0,0,0,"2018-12-01 14:31:21"
|
||||
"Dagna Q (dagnaq)",,0,0,0,0,0,0,0,"2017-08-06 01:42:52"
|
||||
Kamalakannan,,0,0,0,0,0,0,0,"2017-05-14 11:40:23"
|
||||
"Éjbãss Übbeî (littlebittlebottle)",Norwegian,0,0,0,152,0,0,0,"2017-07-05 21:12:02"
|
||||
"Равиль Мифтахов (ravilmif47)",Russian,0,0,0,1,0,0,0,"2019-08-12 21:58:30"
|
||||
"박찌 (perpact20)",Korean,1,1,0,0,0,0,0,"2018-02-10 10:11:44"
|
||||
"Kan Black (kanblack.va)",Vietnamese,1,2,0,0,0,1,0,"2019-01-15 03:50:10"
|
||||
"Anastasiia Bondarenko (nastasya.bondarenko.97)",Russian,1,1,0,0,0,0,0,"2019-06-07 17:43:08"
|
||||
"Wibi Cahyo (wbcahyoh)",Indonesian,1,3,0,0,0,0,0,"2017-12-14 06:35:58"
|
||||
sanyoniket,,0,0,0,0,0,0,0,"2019-07-23 12:58:40"
|
||||
REMOVED_USER,,0,0,0,0,0,0,0,"2020-02-01 03:47:48"
|
||||
"vi ve (VimalV)",,0,0,0,0,0,0,0,"2021-02-08 02:35:45"
|
||||
"George Merkulov (george142.emarket)",Russian,0,0,0,11,0,0,0,"2019-06-09 19:47:02"
|
||||
"Yasin Okumus (lacivert)",Turkish,0,0,0,1,0,0,0,"2018-02-07 04:13:51"
|
||||
"Petros Bleyan (coolbleyan)",Russian,0,0,0,14,0,0,0,"2017-08-18 18:37:18"
|
||||
"LeMeD (LeMeS)",French,0,0,0,2,0,0,0,"2021-02-06 15:35:00"
|
||||
"Sri Harsha Bhogi (sriharshabhogi)",,0,0,0,0,0,0,0,"2018-09-02 05:31:53"
|
||||
Irsgram,Russian,0,0,0,1,0,0,0,"2019-09-30 16:42:20"
|
||||
"Baran Özavcı (n2141n)",Turkish,0,0,0,1,0,0,0,"2022-02-26 04:32:51"
|
||||
"Masataka Yakura (myakura)",Japanese,0,0,0,1,0,0,0,"2021-09-03 22:10:36"
|
||||
ava_rfie,Persian,0,0,0,1,0,0,0,"2019-06-09 16:19:24"
|
||||
"Mateusz Teteruk (mttet)",Polish,0,0,0,1,0,0,0,"2021-01-23 13:09:59"
|
||||
EwanB,,0,0,0,0,0,0,0,"2019-11-19 10:04:38"
|
||||
Fazy1380,,0,0,0,0,0,0,0,"2021-04-10 11:02:53"
|
||||
"Lori Amico (lavodkaclyde2323)",Italian,0,0,0,1,0,0,0,"2017-04-09 10:08:13"
|
||||
"Florian Stuhlmann (stuhlmann)",German,0,0,0,10,0,0,0,"2017-04-15 04:04:00"
|
||||
"عبد الناصر سعيد الثبيتي (asaeed)",,0,0,0,0,0,0,0,"2018-03-13 02:09:35"
|
||||
"Rivo Zängov (Eraser)",,0,0,0,0,0,0,0,"2020-10-13 04:38:26"
|
||||
Hayder21,,0,0,0,0,0,0,0,"2019-12-31 10:56:24"
|
||||
T-v-Gerwen,Dutch,0,0,0,47,0,0,0,"2018-03-02 10:26:33"
|
||||
"Eduard Boboc (edi.boboc33)",Romanian,0,0,0,4,0,0,0,"2019-12-16 09:08:39"
|
||||
"George Merkulov (george142.emarket)",Russian,0,0,0,11,0,0,0,"2019-06-09 19:47:02"
|
||||
philfr49,French,0,0,0,2,0,0,0,"2018-09-03 14:20:32"
|
||||
"عبد الناصر سعيد الثبيتي (asaeed)",,0,0,0,0,0,0,0,"2018-03-13 02:09:35"
|
||||
"Thomas Orlita (Thomas995)",Czech,0,0,0,1,0,0,0,"2017-12-24 04:08:27"
|
||||
"Edmunds Edmundam (edmundam)",,0,0,0,0,0,0,0,"2020-06-01 14:18:18"
|
||||
"Elmo (oberknecht)",,0,0,0,0,0,0,0,"2020-04-16 08:45:50"
|
||||
"Равиль Мифтахов (ravilmif47)",Russian,0,0,0,1,0,0,0,"2019-08-12 21:58:30"
|
||||
"Manny Farsangy (manifarsangi)",Persian,0,0,0,12,0,0,0,"2021-08-10 05:32:28"
|
||||
"Samuel Przeździęk (samek22)",Polish,0,0,0,1,0,0,0,"2021-08-01 00:49:01"
|
||||
"Saiprasath B (Saiprasath)",,0,0,0,0,0,0,0,"2021-07-11 11:10:41"
|
||||
shuvo786,,0,0,0,0,0,0,0,"2019-11-13 00:18:12"
|
||||
"Edmunds Edmundam (edmundam)",,0,0,0,0,0,0,0,"2020-06-01 14:18:18"
|
||||
Itch,,0,0,0,0,0,0,0,"2017-10-16 09:18:42"
|
||||
"Manny Farsangy (manifarsangi)",Persian,0,0,0,12,0,0,0,"2021-08-10 05:32:28"
|
||||
"Matus Zdansky (matuszdansky)",,0,0,0,0,0,0,0,"2019-10-20 13:52:24"
|
||||
"Thomas Orlita (Thomas995)",Czech,0,0,0,1,0,0,0,"2017-12-24 04:08:27"
|
||||
Irsgram,Russian,0,0,0,1,0,0,0,"2019-09-30 16:42:20"
|
||||
EmanAmini,,0,0,0,0,0,0,0,"2017-03-31 13:27:43"
|
||||
mushin,,0,0,0,0,0,0,0,"2020-02-02 04:08:05"
|
||||
"Elmo (oberknecht)",,0,0,0,0,0,0,0,"2020-04-16 08:45:50"
|
||||
AnggaRifandi,,0,0,0,0,0,0,0,"2017-03-31 19:28:35"
|
||||
"darkkingredian (rediancool)",,0,0,0,0,0,0,0,"2021-07-27 16:04:32"
|
||||
"Sri Harsha Bhogi (sriharshabhogi)",,0,0,0,0,0,0,0,"2018-09-02 05:31:53"
|
||||
"Nat Fomicheva (natac)",Russian,0,0,0,3,0,0,0,"2019-01-25 14:35:02"
|
||||
mdrobulis,,0,0,0,0,0,0,0,"2018-05-24 01:40:42"
|
||||
"Sarah BCNN (fsarahboucenna)",French,0,0,0,16,0,0,0,"2018-02-11 11:07:36"
|
||||
"Arjun K. (arjunkdot)",,0,0,0,0,0,0,0,"2020-09-20 11:16:18"
|
||||
REMOVED_USER,Czech,0,0,0,18,0,0,0,"2018-03-27 06:19:52"
|
||||
martyaberger,,0,0,0,0,0,0,0,"2019-01-01 18:48:08"
|
||||
BongTran,Vietnamese,0,0,0,2,0,0,0,"2018-04-24 05:16:07"
|
||||
"Arttu Ylhävuori (arttu.ylhavuori)",,0,0,0,0,0,0,0,"2019-07-24 15:03:42"
|
||||
"Никита Карамов (nikita.karamoff)",Russian,0,0,0,10,0,0,0,"2018-10-29 03:57:21"
|
||||
rooban23,,0,0,0,0,0,0,0,"2020-09-15 11:49:14"
|
||||
"Eliška Roubalová (roubaeli)",Czech,0,0,0,6,0,0,0,"2019-12-31 12:47:29"
|
||||
valney.faria,"Portuguese, Brazilian",0,0,0,1,0,0,0,"2020-02-02 14:45:02"
|
||||
"Алтынбек Наурызғали (altinbeknaurizgali)",Russian,0,0,0,1,0,0,0,"2020-08-12 13:03:49"
|
||||
REMOVED_USER,,0,0,0,0,0,0,0,"2018-10-27 15:34:36"
|
||||
REMOVED_USER,,0,0,0,0,0,0,0,"2018-08-24 00:17:43"
|
||||
REMOVED_USER,,0,0,0,0,0,0,0,"2020-02-01 03:47:48"
|
||||
"Arjun K. (arjunkdot)",,0,0,0,0,0,0,0,"2020-09-20 11:16:18"
|
||||
EwanB,,0,0,0,0,0,0,0,"2019-11-19 10:04:38"
|
||||
shuvo786,,0,0,0,0,0,0,0,"2019-11-13 00:18:12"
|
||||
"Pro AAA (pro1010)",Arabic,0,0,0,1,0,0,0,"2022-02-14 03:32:44"
|
||||
"manu (manuL96)",,0,0,0,0,0,0,0,"2022-05-06 23:34:55"
|
||||
"Rivo Zängov (Eraser)",,0,0,0,0,0,0,0,"2020-10-13 04:38:26"
|
||||
ashik8113,,0,0,0,0,0,0,0,"2022-04-13 11:58:26"
|
||||
deepbird,,0,0,0,0,0,0,0,"2022-04-11 03:21:05"
|
||||
REMOVED_USER,,0,0,0,0,0,0,0,"2018-10-27 15:34:36"
|
||||
Elham1361,,0,0,0,0,0,0,0,"2018-10-27 12:01:06"
|
||||
dongchen.yue,German,0,0,0,4,0,0,0,"2020-09-12 15:05:59"
|
||||
"Ahnaf Tajwar (atn4404)",,0,0,0,0,0,0,0,"2018-10-16 11:13:30"
|
||||
martyaberger,,0,0,0,0,0,0,0,"2019-01-01 18:48:08"
|
||||
AsadullahIlyas,,0,0,0,0,0,0,0,"2019-01-04 06:14:15"
|
||||
droidahmed,Arabic,0,0,0,7,0,0,0,"2018-01-31 02:18:49"
|
||||
philfr49,French,0,0,0,2,0,0,0,"2018-09-03 14:20:32"
|
||||
"Ahmed Nazir (ahmednazir333)",,0,0,0,0,0,0,0,"2018-05-06 12:10:27"
|
||||
"Balaji Jayaraman (jkbalaji1103)",,0,0,0,0,0,0,0,"2017-10-30 22:12:27"
|
||||
"Wellington Ribeiro (wellington.rib)",,0,0,0,0,0,0,0,"2017-11-16 07:32:25"
|
||||
"Javid IRAN (twitteriran98)",Persian,0,0,0,1,0,0,0,"2017-11-25 16:47:25"
|
||||
"박인호 (wphestiraid)",Korean,0,0,0,2,0,0,0,"2018-01-05 00:33:14"
|
||||
"akmal shafiq (mohdakmalshafiq)",,0,0,0,0,0,0,0,"2021-11-01 01:04:50"
|
||||
"Sylwuskak (sylwuskak)",Polish,0,0,0,1,0,0,0,"2022-01-25 04:19:53"
|
||||
"Yunsu Kim (yunsukim86)",Korean,0,0,0,2,0,0,0,"2022-01-14 06:33:43"
|
||||
"Pumpith Ungsupanit (pumpithu)",,0,0,0,0,0,0,0,"2019-01-19 23:47:57"
|
||||
Sandhu564.,,0,0,0,0,0,0,0,"2020-12-14 01:27:45"
|
||||
"Quentin Hibon (hiq)",,0,0,0,0,0,0,0,"2021-02-07 16:39:31"
|
||||
AhmedDz,Arabic,0,0,0,1,0,0,0,"2017-12-31 10:12:31"
|
||||
"Nat Fomicheva (natac)",Russian,0,0,0,3,0,0,0,"2019-01-25 14:35:02"
|
||||
HemanthMeda,Telugu,0,0,0,4,0,0,0,"2021-12-01 14:02:14"
|
||||
"darkkingredian (rediancool)",,0,0,0,0,0,0,0,"2021-07-27 16:04:32"
|
||||
catemlitten,Japanese,0,0,0,1,0,0,0,"2021-11-17 15:06:02"
|
||||
"Said Tahsin Dane (tasomaniac)",,0,0,0,0,0,0,0,"2021-09-25 05:31:01"
|
||||
"Matus Zdansky (matuszdansky)",,0,0,0,0,0,0,0,"2019-10-20 13:52:24"
|
||||
mdrobulis,,0,0,0,0,0,0,0,"2018-05-24 01:40:42"
|
||||
valney.faria,"Portuguese, Brazilian",0,0,0,1,0,0,0,"2020-02-02 14:45:02"
|
||||
"Petros Bleyan (coolbleyan)",Russian,0,0,0,14,0,0,0,"2017-08-18 18:37:18"
|
||||
"Карлен Шаухаев (KarlenShaukhaev)",,0,0,0,0,0,0,0,"2020-04-27 08:53:49"
|
||||
"Shuvashish Sahoo (shuvashish76)",,0,0,0,0,0,0,0,"2020-09-17 09:10:09"
|
||||
REMOVED_USER,,0,0,0,0,0,0,0,"2018-01-05 16:56:12"
|
||||
NairaDNV,Spanish,0,0,0,9,0,0,0,"2018-01-05 19:10:33"
|
||||
"Dagna Q (dagnaq)",,0,0,0,0,0,0,0,"2017-08-06 01:42:52"
|
||||
Sandhu564.,,0,0,0,0,0,0,0,"2020-12-14 01:27:45"
|
||||
AhmedDz,Arabic,0,0,0,1,0,0,0,"2017-12-31 10:12:31"
|
||||
"Quentin Hibon (hiq)",,0,0,0,0,0,0,0,"2021-02-07 16:39:31"
|
||||
"Ahmed Nazir (ahmednazir333)",,0,0,0,0,0,0,0,"2018-05-06 12:10:27"
|
||||
"박인호 (wphestiraid)",Korean,0,0,0,2,0,0,0,"2018-01-05 00:33:14"
|
||||
Raulbertassi,,0,0,0,0,0,0,0,"2018-01-07 17:23:18"
|
||||
"Карлен Шаухаев (KarlenShaukhaev)",,0,0,0,0,0,0,0,"2020-04-27 08:53:49"
|
||||
"Javid IRAN (twitteriran98)",Persian,0,0,0,1,0,0,0,"2017-11-25 16:47:25"
|
||||
"Wellington Ribeiro (wellington.rib)",,0,0,0,0,0,0,0,"2017-11-16 07:32:25"
|
||||
dimateos,,0,0,0,0,0,0,0,"2021-01-10 06:29:52"
|
||||
"Katherine Alexandra Flórez Ramírez (katherine.florez12)",Spanish,0,0,0,46,0,0,0,"2018-01-20 02:18:32"
|
||||
"Balaji Jayaraman (jkbalaji1103)",,0,0,0,0,0,0,0,"2017-10-30 22:12:27"
|
||||
"reza golestanzadeh (reza.golestanzadeh)",Persian,0,0,0,1,0,0,0,"2020-10-21 12:07:20"
|
||||
farbod66,Persian,0,0,0,1,0,0,0,"2018-01-20 11:04:23"
|
||||
"Muhammet Furkan ALMACI (furkan.almaci)",Turkish,0,0,0,1,0,0,0,"2017-10-29 13:44:56"
|
||||
dongchen.yue,German,0,0,0,4,0,0,0,"2020-09-12 15:05:59"
|
||||
"Алтынбек Наурызғали (altinbeknaurizgali)",Russian,0,0,0,1,0,0,0,"2020-08-12 13:03:49"
|
||||
rooban23,,0,0,0,0,0,0,0,"2020-09-15 11:49:14"
|
||||
NairaDNV,Spanish,0,0,0,9,0,0,0,"2018-01-05 19:10:33"
|
||||
"Katherine Alexandra Flórez Ramírez (katherine.florez12)",Spanish,0,0,0,46,0,0,0,"2018-01-20 02:18:32"
|
||||
Itch,,0,0,0,0,0,0,0,"2017-10-16 09:18:42"
|
||||
"Yasin Okumus (lacivert)",Turkish,0,0,0,1,0,0,0,"2018-02-07 04:13:51"
|
||||
"Eduard Boboc (edi.boboc33)",Romanian,0,0,0,4,0,0,0,"2019-12-16 09:08:39"
|
||||
Hayder21,,0,0,0,0,0,0,0,"2019-12-31 10:56:24"
|
||||
"Eliška Roubalová (roubaeli)",Czech,0,0,0,6,0,0,0,"2019-12-31 12:47:29"
|
||||
Fazy1380,,0,0,0,0,0,0,0,"2021-04-10 11:02:53"
|
||||
"Arttu Ylhävuori (arttu.ylhavuori)",,0,0,0,0,0,0,0,"2019-07-24 15:03:42"
|
||||
EmanAmini,,0,0,0,0,0,0,0,"2017-03-31 13:27:43"
|
||||
AnggaRifandi,,0,0,0,0,0,0,0,"2017-03-31 19:28:35"
|
||||
"Lori Amico (lavodkaclyde2323)",Italian,0,0,0,1,0,0,0,"2017-04-09 10:08:13"
|
||||
"Florian Stuhlmann (stuhlmann)",German,0,0,0,10,0,0,0,"2017-04-15 04:04:00"
|
||||
Kamalakannan,,0,0,0,0,0,0,0,"2017-05-14 11:40:23"
|
||||
farbod66,Persian,0,0,0,1,0,0,0,"2018-01-20 11:04:23"
|
||||
"vi ve (VimalV)",,0,0,0,0,0,0,0,"2021-02-08 02:35:45"
|
||||
"Éjbãss Übbeî (littlebittlebottle)",Norwegian,0,0,0,152,0,0,0,"2017-07-05 21:12:02"
|
||||
"LeMeD (LeMeS)",French,0,0,0,2,0,0,0,"2021-02-06 15:35:00"
|
||||
BongTran,Vietnamese,0,0,0,2,0,0,0,"2018-04-24 05:16:07"
|
||||
REMOVED_USER,Czech,0,0,0,18,0,0,0,"2018-03-27 06:19:52"
|
||||
mushin,,0,0,0,0,0,0,0,"2020-02-02 04:08:05"
|
||||
"Mateusz Teteruk (mttet)",Polish,0,0,0,1,0,0,0,"2021-01-23 13:09:59"
|
||||
"Sarah BCNN (fsarahboucenna)",French,0,0,0,16,0,0,0,"2018-02-11 11:07:36"
|
||||
droidahmed,Arabic,0,0,0,7,0,0,0,"2018-01-31 02:18:49"
|
||||
"Никита Карамов (nikita.karamoff)",Russian,0,0,0,10,0,0,0,"2018-10-29 03:57:21"
|
||||
|
||||
|
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
/*
|
||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||
*
|
||||
@@ -20,7 +18,7 @@
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("com.github.triplet.play") version "3.5.0"
|
||||
id("com.github.triplet.play") version "3.7.0"
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("org.jetbrains.kotlin.kapt")
|
||||
@@ -34,15 +32,15 @@ tasks.compileLint {
|
||||
|
||||
android {
|
||||
|
||||
compileSdkVersion(30)
|
||||
compileSdk = 31
|
||||
|
||||
defaultConfig {
|
||||
versionCode(20003)
|
||||
versionName("2.0.3")
|
||||
minSdkVersion(23)
|
||||
targetSdkVersion(30)
|
||||
applicationId("org.isoron.uhabits")
|
||||
testInstrumentationRunner("androidx.test.runner.AndroidJUnitRunner")
|
||||
versionCode = 20102
|
||||
versionName = "2.1.2"
|
||||
minSdk = 23
|
||||
targetSdk = 31
|
||||
applicationId = "org.isoron.uhabits"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -58,7 +56,7 @@ android {
|
||||
|
||||
buildTypes {
|
||||
getByName("release") {
|
||||
minifyEnabled(true)
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.txt")
|
||||
if (signingConfigs.findByName("release") != null) {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
@@ -70,7 +68,7 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
lint {
|
||||
isCheckReleaseBuilds = false
|
||||
isAbortOnError = false
|
||||
disable("GoogleAppIndexingWarning")
|
||||
@@ -88,10 +86,10 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
val daggerVersion = "2.38.1"
|
||||
val kotlinVersion = "1.5.21"
|
||||
val kxCoroutinesVersion = "1.5.1"
|
||||
val ktorVersion = "1.6.2"
|
||||
val daggerVersion = "2.41"
|
||||
val kotlinVersion = "1.6.21"
|
||||
val kxCoroutinesVersion = "1.6.1"
|
||||
val ktorVersion = "1.6.8"
|
||||
val espressoVersion = "3.4.0"
|
||||
|
||||
androidTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion")
|
||||
@@ -100,17 +98,17 @@ dependencies {
|
||||
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.1")
|
||||
androidTestImplementation("io.ktor:ktor-client-mock:$ktorVersion")
|
||||
androidTestImplementation("io.ktor:ktor-jackson:$ktorVersion")
|
||||
androidTestImplementation("androidx.annotation:annotation:1.2.0")
|
||||
androidTestImplementation("androidx.annotation:annotation:1.3.0")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
||||
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
|
||||
androidTestImplementation("androidx.test:rules:1.4.0")
|
||||
androidTestImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
||||
compileOnly("javax.annotation:jsr250-api:1.0")
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
|
||||
implementation("com.github.paolorotolo:appintro:4.1.0")
|
||||
implementation("com.github.AppIntro:AppIntro:6.2.0")
|
||||
implementation("com.google.code.findbugs:jsr305:3.0.2")
|
||||
implementation("com.google.dagger:dagger:$daggerVersion")
|
||||
implementation("com.google.guava:guava:30.1.1-android")
|
||||
implementation("com.google.guava:guava:31.1-android")
|
||||
implementation("io.ktor:ktor-client-android:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-core:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-jackson:$ktorVersion")
|
||||
@@ -118,11 +116,11 @@ dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kxCoroutinesVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kxCoroutinesVersion")
|
||||
implementation("androidx.appcompat:appcompat:1.3.1")
|
||||
implementation("androidx.appcompat:appcompat:1.4.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.4.0")
|
||||
implementation("com.opencsv:opencsv:5.5.1")
|
||||
implementation("com.google.android.material:material:1.5.0")
|
||||
implementation("com.opencsv:opencsv:5.6")
|
||||
implementation(project(":uhabits-core"))
|
||||
kapt("com.google.dagger:dagger-compiler:$daggerVersion")
|
||||
kaptAndroidTest("com.google.dagger:dagger-compiler:$daggerVersion")
|
||||
|
||||
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
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: 20 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 |
@@ -55,6 +55,7 @@ import org.junit.runner.RunWith
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@LargeTest
|
||||
class HabitsTest : BaseUserInterfaceTest() {
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun shouldCreateHabit() {
|
||||
@@ -180,6 +181,8 @@ class HabitsTest : BaseUserInterfaceTest() {
|
||||
longPressCheckmarks("Wake up early", count = 2)
|
||||
clickText("Wake up early")
|
||||
verifyShowsScreen(SHOW_HABIT)
|
||||
// TODO: find a better way than sleeping in tests
|
||||
Thread.sleep(2001L)
|
||||
verifyDisplaysText("10%")
|
||||
}
|
||||
|
||||
@@ -194,6 +197,8 @@ class HabitsTest : BaseUserInterfaceTest() {
|
||||
verifyDoesNotDisplayText("Track time")
|
||||
verifyDisplaysText("Wake up early")
|
||||
longPressCheckmarks("Wake up early", count = 1)
|
||||
// TODO: find a better way than sleeping in tests
|
||||
Thread.sleep(2001L)
|
||||
verifyDoesNotDisplayText("Wake up early")
|
||||
clickMenu(TOGGLE_COMPLETED)
|
||||
verifyDisplaysText("Track time")
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
package org.isoron.uhabits.acceptance.steps
|
||||
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.SystemClock.sleep
|
||||
import androidx.test.uiautomator.By
|
||||
import androidx.test.uiautomator.UiSelector
|
||||
import org.isoron.uhabits.BaseUserInterfaceTest.Companion.device
|
||||
@@ -39,7 +40,7 @@ fun exportFullBackup() {
|
||||
}
|
||||
|
||||
fun clearDownloadFolder() {
|
||||
device.executeShellCommand("rm -rf /sdcard/Download/")
|
||||
device.executeShellCommand("rm -rf /sdcard/Download")
|
||||
}
|
||||
|
||||
fun clearBackupFolder() {
|
||||
@@ -86,6 +87,7 @@ fun importBackupFromDownloadFolder() {
|
||||
device.findObject(UiSelector().textContains("Loop")).click()
|
||||
} else {
|
||||
device.click(50, 90) // Click menu button
|
||||
Thread.sleep(1000)
|
||||
device.findObject(UiSelector().textContains("Download")).click()
|
||||
device.findObject(UiSelector().textContains("Loop")).click()
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
*/
|
||||
package org.isoron.uhabits.acceptance.steps
|
||||
|
||||
import android.os.Build.VERSION
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.test.espresso.Espresso
|
||||
@@ -133,7 +134,7 @@ object CommonSteps : BaseUserInterfaceTest() {
|
||||
@Throws(Exception::class)
|
||||
fun verifyOpensWebsite(url: String?) {
|
||||
var browserPkg = "org.chromium.webview_shell"
|
||||
if (VERSION.SDK_INT <= 23) {
|
||||
if (SDK_INT <= Build.VERSION_CODES.M) {
|
||||
browserPkg = "com.android.browser"
|
||||
}
|
||||
assertTrue(device.wait(Until.hasObject(By.pkg(browserPkg)), 5000))
|
||||
@@ -178,6 +179,22 @@ object CommonSteps : BaseUserInterfaceTest() {
|
||||
EditHabitSteps.clickSave()
|
||||
}
|
||||
|
||||
fun changeFrequencyToDaily(habitName: String) {
|
||||
clickText(habitName)
|
||||
Espresso.onView(ViewMatchers.withId(R.id.action_edit_habit)).perform(ViewActions.click())
|
||||
EditHabitSteps.pickDailyFrequency()
|
||||
EditHabitSteps.clickSave()
|
||||
pressBack()
|
||||
}
|
||||
|
||||
fun changeFrequencyToMonthly(habitName: String) {
|
||||
clickText(habitName)
|
||||
Espresso.onView(ViewMatchers.withId(R.id.action_edit_habit)).perform(ViewActions.click())
|
||||
EditHabitSteps.pickMonthFrequency()
|
||||
EditHabitSteps.clickSave()
|
||||
pressBack()
|
||||
}
|
||||
|
||||
enum class Screen {
|
||||
LIST_HABITS, SHOW_HABIT, EDIT_HABIT, SELECT_HABIT_TYPE
|
||||
}
|
||||
|
||||
@@ -36,6 +36,24 @@ object EditHabitSteps {
|
||||
Espresso.onView(ViewMatchers.withText("SAVE")).perform(ViewActions.click())
|
||||
}
|
||||
|
||||
fun pickMonthFrequency() {
|
||||
Espresso.onView(ViewMatchers.withId(R.id.boolean_frequency_picker))
|
||||
.perform(ViewActions.click())
|
||||
Espresso.onView(ViewMatchers.withId(R.id.xTimesPerMonthRadioButton))
|
||||
.perform(ViewActions.click())
|
||||
Espresso.onView(ViewMatchers.withId(R.id.xTimesPerMonthTextView))
|
||||
.perform(ViewActions.replaceText("1"))
|
||||
Espresso.onView(ViewMatchers.withText("SAVE")).perform(ViewActions.click())
|
||||
}
|
||||
|
||||
fun pickDailyFrequency() {
|
||||
Espresso.onView(ViewMatchers.withId(R.id.boolean_frequency_picker))
|
||||
.perform(ViewActions.click())
|
||||
Espresso.onView(ViewMatchers.withId(R.id.everyDayRadioButton))
|
||||
.perform(ViewActions.click())
|
||||
Espresso.onView(ViewMatchers.withText("SAVE")).perform(ViewActions.click())
|
||||
}
|
||||
|
||||
fun pickColor(color: Int) {
|
||||
Espresso.onView(ViewMatchers.withId(R.id.colorButton)).perform(ViewActions.click())
|
||||
BaseUserInterfaceTest.device.findObject(By.descStartsWith(String.format("Color %d", color)))
|
||||
|
||||
@@ -120,6 +120,12 @@ object ListHabitsSteps {
|
||||
BaseUserInterfaceTest.device.waitForIdle()
|
||||
}
|
||||
|
||||
fun changeSort(sortText: String) {
|
||||
clickViewWithId(R.id.action_filter)
|
||||
Espresso.onView(ViewMatchers.withText("Sort")).perform(ViewActions.click())
|
||||
Espresso.onView(ViewMatchers.withText(sortText)).perform(ViewActions.click())
|
||||
}
|
||||
|
||||
enum class MenuItem {
|
||||
ABOUT, HELP, SETTINGS, EDIT, DELETE, ARCHIVE, TOGGLE_ARCHIVED, UNARCHIVE, TOGGLE_COMPLETED, ADD
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
package org.isoron.uhabits.acceptance.steps
|
||||
|
||||
import android.os.Build.VERSION
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import androidx.test.uiautomator.UiScrollable
|
||||
import androidx.test.uiautomator.UiSelector
|
||||
import junit.framework.Assert.assertFalse
|
||||
@@ -50,29 +50,21 @@ object WidgetSteps {
|
||||
private fun openWidgetScreen() {
|
||||
val h = BaseUserInterfaceTest.device.displayHeight
|
||||
val w = BaseUserInterfaceTest.device.displayWidth
|
||||
if (VERSION.SDK_INT <= 21) {
|
||||
BaseUserInterfaceTest.device.pressHome()
|
||||
BaseUserInterfaceTest.device.waitForIdle()
|
||||
BaseUserInterfaceTest.device.findObject(UiSelector().description("Apps")).click()
|
||||
BaseUserInterfaceTest.device.findObject(UiSelector().description("Apps")).click()
|
||||
BaseUserInterfaceTest.device.findObject(UiSelector().description("Widgets")).click()
|
||||
} else {
|
||||
val listId = "com.android.launcher3:id/widgets_list_view"
|
||||
BaseUserInterfaceTest.device.pressHome()
|
||||
BaseUserInterfaceTest.device.waitForIdle()
|
||||
BaseUserInterfaceTest.device.drag(w / 2, h / 2, w / 2, h / 2, 8)
|
||||
var button = BaseUserInterfaceTest.device.findObject(UiSelector().text("WIDGETS"))
|
||||
if (!button.waitForExists(1000)) {
|
||||
button = BaseUserInterfaceTest.device.findObject(UiSelector().text("Widgets"))
|
||||
}
|
||||
button.click()
|
||||
if (VERSION.SDK_INT >= 28) {
|
||||
UiScrollable(UiSelector().resourceId(listId))
|
||||
.scrollForward()
|
||||
}
|
||||
UiScrollable(UiSelector().resourceId(listId))
|
||||
.scrollIntoView(UiSelector().text("Checkmark"))
|
||||
val listId = "com.android.launcher3:id/widgets_list_view"
|
||||
BaseUserInterfaceTest.device.pressHome()
|
||||
BaseUserInterfaceTest.device.waitForIdle()
|
||||
BaseUserInterfaceTest.device.drag(w / 2, h / 2, w / 2, h / 2, 8)
|
||||
var button = BaseUserInterfaceTest.device.findObject(UiSelector().text("WIDGETS"))
|
||||
if (!button.waitForExists(1000)) {
|
||||
button = BaseUserInterfaceTest.device.findObject(UiSelector().text("Widgets"))
|
||||
}
|
||||
button.click()
|
||||
if (SDK_INT >= 28) {
|
||||
UiScrollable(UiSelector().resourceId(listId))
|
||||
.scrollForward()
|
||||
}
|
||||
UiScrollable(UiSelector().resourceId(listId))
|
||||
.scrollIntoView(UiSelector().text("Checkmark"))
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
|
||||
@@ -29,6 +29,10 @@ import org.junit.runner.RunWith
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@MediumTest
|
||||
class EmptyListViewTest : BaseViewTest() {
|
||||
init {
|
||||
// TODO: fix rendering differences across APIs
|
||||
similarityCutoff = 0.00035
|
||||
}
|
||||
|
||||
private val path = "habits/list/EmptyListView"
|
||||
private val view: EmptyListView = EmptyListView(targetContext)
|
||||
|
||||
@@ -36,6 +36,7 @@ class EntryButtonViewTest : BaseViewTest() {
|
||||
lateinit var view: CheckmarkButtonView
|
||||
|
||||
var toggled = false
|
||||
var edited = false
|
||||
|
||||
@Before
|
||||
override fun setUp() {
|
||||
@@ -43,7 +44,8 @@ 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))
|
||||
}
|
||||
@@ -70,20 +72,28 @@ class EntryButtonViewTest : BaseViewTest() {
|
||||
fun testClick_withShortToggleDisabled() {
|
||||
prefs.isShortToggleEnabled = false
|
||||
view.performClick()
|
||||
assertFalse(toggled)
|
||||
assertTrue(!toggled and edited)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClick_withShortToggleEnabled() {
|
||||
prefs.isShortToggleEnabled = true
|
||||
view.performClick()
|
||||
assertTrue(toggled)
|
||||
assertTrue(toggled and !edited)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLongClick() {
|
||||
fun testLongClick_withShortToggleDisabled() {
|
||||
prefs.isShortToggleEnabled = false
|
||||
view.performLongClick()
|
||||
assertTrue(toggled)
|
||||
assertTrue(toggled and !edited)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLongClick_withShortToggleEnabled() {
|
||||
prefs.isShortToggleEnabled = true
|
||||
view.performLongClick()
|
||||
assertTrue(!toggled and edited)
|
||||
}
|
||||
|
||||
private fun assertRendersCheckedExplicitly() {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.test.filters.MediumTest
|
||||
import org.hamcrest.CoreMatchers.equalTo
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.isoron.uhabits.BaseViewTest
|
||||
import org.isoron.uhabits.core.models.NumericalHabitType
|
||||
import org.isoron.uhabits.utils.PaletteUtils
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@@ -42,6 +43,7 @@ class NumberButtonViewTest : BaseViewTest() {
|
||||
super.setUp()
|
||||
view = component.getNumberButtonViewFactory().create().apply {
|
||||
units = "steps"
|
||||
targetType = NumericalHabitType.AT_LEAST
|
||||
threshold = 100.0
|
||||
color = PaletteUtils.getAndroidTestColor(8)
|
||||
onEdit = { edited = true }
|
||||
@@ -74,10 +76,10 @@ class NumberButtonViewTest : BaseViewTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRender_emptyUnits() {
|
||||
fun testRender_atMostAboveThreshold() {
|
||||
view.value = 500.0
|
||||
view.units = ""
|
||||
assertRenders(view, "$PATH/render_unitless.png")
|
||||
view.targetType = NumericalHabitType.AT_MOST
|
||||
assertRenders(view, "$PATH/render_at_most_above.png")
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -86,6 +88,13 @@ class NumberButtonViewTest : BaseViewTest() {
|
||||
assertRenders(view, "$PATH/render_below.png")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRender_atMostBetweenThresholds() {
|
||||
view.value = 110.0
|
||||
view.targetType = NumericalHabitType.AT_MOST
|
||||
assertRenders(view, "$PATH/render_at_most_between.png")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRender_zero() {
|
||||
view.value = 0.0
|
||||
@@ -93,15 +102,21 @@ class NumberButtonViewTest : BaseViewTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClick_shortToggleDisabled() {
|
||||
prefs.isShortToggleEnabled = false
|
||||
view.performClick()
|
||||
assertFalse(edited)
|
||||
fun testRender_atMostBelowThreshold() {
|
||||
view.value = 0.0
|
||||
view.targetType = NumericalHabitType.AT_MOST
|
||||
assertRenders(view, "$PATH/render_at_most_below.png")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClick_shortToggleEnabled() {
|
||||
prefs.isShortToggleEnabled = true
|
||||
fun testRender_emptyUnits() {
|
||||
view.value = 500.0
|
||||
view.units = ""
|
||||
assertRenders(view, "$PATH/render_unitless.png")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClick() {
|
||||
view.performClick()
|
||||
assertTrue(edited)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.test.filters.MediumTest
|
||||
import org.hamcrest.CoreMatchers.equalTo
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.isoron.uhabits.BaseViewTest
|
||||
import org.isoron.uhabits.core.models.NumericalHabitType
|
||||
import org.isoron.uhabits.core.models.Timestamp
|
||||
import org.isoron.uhabits.utils.PaletteUtils
|
||||
import org.junit.After
|
||||
@@ -55,6 +56,7 @@ class NumberPanelViewTest : BaseViewTest() {
|
||||
buttonCount = 4
|
||||
color = PaletteUtils.getAndroidTestColor(7)
|
||||
units = "steps"
|
||||
targetType = NumericalHabitType.AT_LEAST
|
||||
threshold = 5000.0
|
||||
}
|
||||
view.onAttachedToWindow()
|
||||
@@ -74,7 +76,7 @@ class NumberPanelViewTest : BaseViewTest() {
|
||||
@Test
|
||||
fun testEdit() {
|
||||
val timestamps = mutableListOf<Timestamp>()
|
||||
view.onEdit = { timestamps.plusAssign(it) }
|
||||
view.onEdit = { t -> timestamps.plusAssign(t) }
|
||||
view.buttons[0].performLongClick()
|
||||
view.buttons[2].performLongClick()
|
||||
view.buttons[3].performLongClick()
|
||||
@@ -85,7 +87,7 @@ class NumberPanelViewTest : BaseViewTest() {
|
||||
fun testEdit_withOffset() {
|
||||
val timestamps = mutableListOf<Timestamp>()
|
||||
view.dataOffset = 3
|
||||
view.onEdit = { timestamps += it }
|
||||
view.onEdit = { t -> timestamps += t }
|
||||
view.buttons[0].performLongClick()
|
||||
view.buttons[2].performLongClick()
|
||||
view.buttons[3].performLongClick()
|
||||
|
||||
@@ -53,8 +53,6 @@ class SubtitleCardViewTest : BaseViewTest() {
|
||||
isNumerical = false,
|
||||
question = "Did you meditate this morning?",
|
||||
reminder = Reminder(8, 30, EVERY_DAY),
|
||||
unit = "",
|
||||
targetValue = 0.0,
|
||||
theme = LightTheme(),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -61,7 +61,7 @@ class PerformanceTest : BaseAndroidTest() {
|
||||
val habit = fixtures.createEmptyHabit()
|
||||
for (i in 0..4999) {
|
||||
val timestamp: Timestamp = Timestamp(i * DAY_LENGTH)
|
||||
CreateRepetitionCommand(habitList, habit, timestamp, 1).run()
|
||||
CreateRepetitionCommand(habitList, habit, timestamp, 1, "").run()
|
||||
}
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
@@ -21,9 +21,12 @@ package org.isoron.uhabits.regression
|
||||
|
||||
import androidx.test.filters.LargeTest
|
||||
import org.isoron.uhabits.BaseUserInterfaceTest
|
||||
import org.isoron.uhabits.acceptance.steps.CommonSteps
|
||||
import org.isoron.uhabits.acceptance.steps.CommonSteps.Screen.EDIT_HABIT
|
||||
import org.isoron.uhabits.acceptance.steps.CommonSteps.Screen.LIST_HABITS
|
||||
import org.isoron.uhabits.acceptance.steps.CommonSteps.Screen.SELECT_HABIT_TYPE
|
||||
import org.isoron.uhabits.acceptance.steps.CommonSteps.changeFrequencyToDaily
|
||||
import org.isoron.uhabits.acceptance.steps.CommonSteps.changeFrequencyToMonthly
|
||||
import org.isoron.uhabits.acceptance.steps.CommonSteps.clickText
|
||||
import org.isoron.uhabits.acceptance.steps.CommonSteps.createHabit
|
||||
import org.isoron.uhabits.acceptance.steps.CommonSteps.launchApp
|
||||
@@ -37,9 +40,12 @@ import org.isoron.uhabits.acceptance.steps.EditHabitSteps.clickSave
|
||||
import org.isoron.uhabits.acceptance.steps.EditHabitSteps.typeName
|
||||
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.MenuItem.ADD
|
||||
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.MenuItem.DELETE
|
||||
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.changeSort
|
||||
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.clickMenu
|
||||
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.longPressCheckmarks
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.NO
|
||||
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.junit.Test
|
||||
|
||||
@@ -83,4 +89,37 @@ class ListHabitsRegressionTest : BaseUserInterfaceTest() {
|
||||
offsetHeaders()
|
||||
verifyDisplaysCheckmarks("Wake up early", listOf(UNKNOWN, UNKNOWN, UNKNOWN, UNKNOWN))
|
||||
}
|
||||
|
||||
/**
|
||||
* https://github.com/iSoron/uhabits/issues/1131
|
||||
*/
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun should_refresh_sort_after_habit_edit() {
|
||||
launchApp()
|
||||
verifyShowsScreen(LIST_HABITS)
|
||||
changeSort("By score")
|
||||
changeSort("By status")
|
||||
longPressCheckmarks("Meditate", count = 1)
|
||||
changeFrequencyToMonthly("Read books")
|
||||
longPressCheckmarks("Read books", count = 2)
|
||||
longPressCheckmarks("Read books", count = 1)
|
||||
verifyDisplaysCheckmarks("Meditate", listOf(YES_AUTO, YES_MANUAL, YES_AUTO, YES_MANUAL))
|
||||
CommonSteps.verifyDisplaysTextInSequence(
|
||||
"Wake up early",
|
||||
"Read books",
|
||||
"Meditate",
|
||||
"Track time"
|
||||
)
|
||||
|
||||
changeFrequencyToDaily("Meditate")
|
||||
|
||||
verifyDisplaysCheckmarks("Meditate", listOf(NO, YES_MANUAL, UNKNOWN, YES_MANUAL))
|
||||
CommonSteps.verifyDisplaysTextInSequence(
|
||||
"Wake up early",
|
||||
"Meditate",
|
||||
"Read books",
|
||||
"Track time",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,14 +24,15 @@ import android.widget.FrameLayout
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.MediumTest
|
||||
import org.hamcrest.CoreMatchers
|
||||
import org.hamcrest.CoreMatchers.`is`
|
||||
import org.hamcrest.CoreMatchers.equalTo
|
||||
import org.hamcrest.CoreMatchers.`is`
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.isoron.uhabits.BaseViewTest
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.models.Entry
|
||||
import org.isoron.uhabits.core.models.EntryList
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.Timestamp
|
||||
import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -42,10 +43,12 @@ class CheckmarkWidgetTest : BaseViewTest() {
|
||||
private lateinit var habit: Habit
|
||||
private lateinit var entries: EntryList
|
||||
private lateinit var view: FrameLayout
|
||||
private val today = getTodayWithOffset()
|
||||
private lateinit var today: Timestamp
|
||||
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
setTheme(R.style.WidgetTheme)
|
||||
today = getTodayWithOffset()
|
||||
prefs.widgetOpacity = 255
|
||||
prefs.isSkipEnabled = true
|
||||
habit = fixtures.createVeryLongHabit()
|
||||
|
||||
@@ -32,6 +32,10 @@ import org.junit.runner.RunWith
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@MediumTest
|
||||
class TargetWidgetTest : BaseViewTest() {
|
||||
init {
|
||||
// TODO: fix rendering differences across APIs
|
||||
similarityCutoff = 0.00025
|
||||
}
|
||||
private lateinit var habit: Habit
|
||||
private lateinit var view: FrameLayout
|
||||
override fun setUp() {
|
||||
|
||||
@@ -36,6 +36,7 @@ class CheckmarkWidgetViewTest : BaseViewTest() {
|
||||
@Before
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
similarityCutoff = 0.00025
|
||||
setTheme(R.style.WidgetTheme)
|
||||
val habit = fixtures.createShortHabit()
|
||||
val computedEntries = habit.computedEntries
|
||||
|
||||
@@ -17,9 +17,10 @@
|
||||
~ with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.isoron.uhabits">
|
||||
|
||||
<uses-permission android:name="android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
@@ -48,11 +49,11 @@
|
||||
android:name=".activities.habits.list.ListHabitsActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/main_activity_title"
|
||||
android:launchMode="singleTop">
|
||||
</activity>
|
||||
android:launchMode="singleTop" />
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/main_activity_title"
|
||||
android:launchMode="singleTop"
|
||||
android:targetActivity=".activities.habits.list.ListHabitsActivity">
|
||||
@@ -85,6 +86,7 @@
|
||||
|
||||
<activity
|
||||
android:name=".widgets.activities.HabitPickerDialog"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.AppCompat.Light.Dialog">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
@@ -93,6 +95,7 @@
|
||||
|
||||
<activity
|
||||
android:name=".widgets.activities.BooleanHabitPickerDialog"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.AppCompat.Light.Dialog">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
@@ -101,6 +104,7 @@
|
||||
|
||||
<activity
|
||||
android:name=".widgets.activities.NumericalHabitPickerDialog"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.AppCompat.Light.Dialog">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
@@ -115,26 +119,16 @@
|
||||
android:value=".activities.habits.list.ListHabitsActivity" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".widgets.activities.NumericalCheckmarkWidgetActivity"
|
||||
android:label="NumericalCheckmarkWidget"
|
||||
android:noHistory="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:theme="@style/Theme.AppCompat.Light.Dialog">
|
||||
<intent-filter>
|
||||
<action android:name="org.isoron.uhabits.ACTION_SHOW_NUMERICAL_VALUE_ACTIVITY" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".notifications.SnoozeDelayPickerActivity"
|
||||
android:taskAffinity=""
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleInstance"
|
||||
android:taskAffinity=""
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.CheckmarkWidgetProvider"
|
||||
android:exported="true"
|
||||
android:label="@string/checkmark">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
@@ -152,6 +146,7 @@
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.HistoryWidgetProvider"
|
||||
android:exported="true"
|
||||
android:label="@string/history">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
@@ -164,6 +159,7 @@
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.ScoreWidgetProvider"
|
||||
android:exported="true"
|
||||
android:label="@string/score">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
@@ -176,6 +172,7 @@
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.StreakWidgetProvider"
|
||||
android:exported="true"
|
||||
android:label="@string/streaks">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
@@ -188,6 +185,7 @@
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.FrequencyWidgetProvider"
|
||||
android:exported="true"
|
||||
android:label="@string/frequency">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
@@ -200,6 +198,7 @@
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.TargetWidgetProvider"
|
||||
android:exported="true"
|
||||
android:label="@string/target">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
@@ -210,13 +209,17 @@
|
||||
android:resource="@xml/widget_target_info" />
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".receivers.ReminderReceiver">
|
||||
<receiver
|
||||
android:name=".receivers.ReminderReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".receivers.WidgetReceiver">
|
||||
<receiver android:name=".receivers.WidgetReceiver"
|
||||
android:exported="true"
|
||||
android:permission="false">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<action android:name="org.isoron.uhabits.ACTION_SET_NUMERICAL_VALUE" />
|
||||
|
||||
@@ -49,23 +49,12 @@ class AndroidDataView(
|
||||
override fun onShowPress(e: MotionEvent?) = Unit
|
||||
|
||||
override fun onSingleTapUp(e: MotionEvent?): Boolean {
|
||||
val x: Float
|
||||
val y: Float
|
||||
try {
|
||||
val pointerId = e!!.getPointerId(0)
|
||||
x = e.getX(pointerId)
|
||||
y = e.getY(pointerId)
|
||||
} catch (ex: RuntimeException) {
|
||||
// Android often throws IllegalArgumentException here. Apparently,
|
||||
// the pointer id may become invalid shortly after calling
|
||||
// e.getPointerId.
|
||||
return false
|
||||
}
|
||||
view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity)
|
||||
return true
|
||||
return handleClick(e, true)
|
||||
}
|
||||
|
||||
override fun onLongPress(e: MotionEvent?) = Unit
|
||||
override fun onLongPress(e: MotionEvent?) {
|
||||
handleClick(e)
|
||||
}
|
||||
|
||||
override fun onScroll(
|
||||
e1: MotionEvent?,
|
||||
@@ -137,4 +126,22 @@ class AndroidDataView(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleClick(e: MotionEvent?, isSingleTap: Boolean = false): Boolean {
|
||||
val x: Float
|
||||
val y: Float
|
||||
try {
|
||||
val pointerId = e!!.getPointerId(0)
|
||||
x = e.getX(pointerId)
|
||||
y = e.getY(pointerId)
|
||||
} catch (ex: RuntimeException) {
|
||||
// Android often throws IllegalArgumentException here. Apparently,
|
||||
// the pointer id may become invalid shortly after calling
|
||||
// e.getPointerId.
|
||||
return false
|
||||
}
|
||||
if (isSingleTap) view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity)
|
||||
else view?.onLongClick(x / canvas.innerDensity, y / canvas.innerDensity)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
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_MANUAL
|
||||
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
|
||||
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
|
||||
import org.isoron.uhabits.utils.sres
|
||||
|
||||
class CheckmarkDialog : AppCompatDialogFragment() {
|
||||
var onToggle: (Int, String) -> Unit = { _, _ -> }
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val appComponent = (requireActivity().application as HabitsApplication).component
|
||||
val prefs = appComponent.preferences
|
||||
val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context))
|
||||
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 = getFontAwesome(requireContext())
|
||||
}
|
||||
view.notes.setText(requireArguments().getString("notes")!!)
|
||||
if (!prefs.isSkipEnabled) view.skipBtn.visibility = GONE
|
||||
if (!prefs.areQuestionMarksEnabled) view.unknownBtn.visibility = GONE
|
||||
view.booleanButtons.visibility = VISIBLE
|
||||
val dialog = Dialog(requireContext())
|
||||
dialog.setContentView(view.root)
|
||||
dialog.window?.apply {
|
||||
setBackgroundDrawableResource(android.R.color.transparent)
|
||||
}
|
||||
fun onClick(v: Int) {
|
||||
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) }
|
||||
view.notes.setOnEditorActionListener { v, actionId, event ->
|
||||
onClick(requireArguments().getInt("value"))
|
||||
true
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ package org.isoron.uhabits.activities.common.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
@@ -44,7 +43,7 @@ class FrequencyPickerDialog(
|
||||
constructor() : this(1, 1)
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val inflater = LayoutInflater.from(activity!!)
|
||||
val inflater = LayoutInflater.from(requireActivity())
|
||||
contentView = inflater.inflate(R.layout.frequency_picker_dialog, null)
|
||||
|
||||
addBeforeAfterText(
|
||||
@@ -62,15 +61,19 @@ class FrequencyPickerDialog(
|
||||
contentView.xTimesPerMonthContainer,
|
||||
)
|
||||
|
||||
addBeforeAfterText(
|
||||
this.getString(R.string.x_times_per_y_days),
|
||||
contentView.xTimesPerYDaysContainer,
|
||||
)
|
||||
|
||||
contentView.everyDayRadioButton.setOnClickListener {
|
||||
check(contentView.everyDayRadioButton)
|
||||
unfocusAll()
|
||||
}
|
||||
|
||||
contentView.everyXDaysRadioButton.setOnClickListener {
|
||||
check(contentView.everyXDaysRadioButton)
|
||||
val everyXDaysTextView = contentView.everyXDaysTextView
|
||||
focus(everyXDaysTextView)
|
||||
selectInputField(everyXDaysTextView)
|
||||
}
|
||||
|
||||
contentView.everyXDaysTextView.setOnFocusChangeListener { v, hasFocus ->
|
||||
@@ -79,7 +82,7 @@ class FrequencyPickerDialog(
|
||||
|
||||
contentView.xTimesPerWeekRadioButton.setOnClickListener {
|
||||
check(contentView.xTimesPerWeekRadioButton)
|
||||
focus(contentView.xTimesPerWeekTextView)
|
||||
selectInputField(contentView.xTimesPerWeekTextView)
|
||||
}
|
||||
|
||||
contentView.xTimesPerWeekTextView.setOnFocusChangeListener { v, hasFocus ->
|
||||
@@ -88,14 +91,27 @@ class FrequencyPickerDialog(
|
||||
|
||||
contentView.xTimesPerMonthRadioButton.setOnClickListener {
|
||||
check(contentView.xTimesPerMonthRadioButton)
|
||||
focus(contentView.xTimesPerMonthTextView)
|
||||
selectInputField(contentView.xTimesPerMonthTextView)
|
||||
}
|
||||
|
||||
contentView.xTimesPerMonthTextView.setOnFocusChangeListener { v, hasFocus ->
|
||||
if (hasFocus) check(contentView.xTimesPerMonthRadioButton)
|
||||
}
|
||||
|
||||
return AlertDialog.Builder(activity!!)
|
||||
contentView.xTimesPerYDaysRadioButton.setOnClickListener {
|
||||
check(contentView.xTimesPerYDaysRadioButton)
|
||||
selectInputField(contentView.xTimesPerYDaysXTextView)
|
||||
}
|
||||
|
||||
contentView.xTimesPerYDaysXTextView.setOnFocusChangeListener { v, hasFocus ->
|
||||
if (hasFocus) check(contentView.xTimesPerYDaysRadioButton)
|
||||
}
|
||||
|
||||
contentView.xTimesPerYDaysYTextView.setOnFocusChangeListener { v, hasFocus ->
|
||||
if (hasFocus) check(contentView.xTimesPerYDaysRadioButton)
|
||||
}
|
||||
|
||||
return AlertDialog.Builder(requireActivity())
|
||||
.setView(contentView)
|
||||
.setPositiveButton(R.string.save) { _, _ -> onSaveClicked() }
|
||||
.create()
|
||||
@@ -106,12 +122,11 @@ class FrequencyPickerDialog(
|
||||
container: LinearLayout
|
||||
) {
|
||||
val parts = str.split("%d")
|
||||
container.addView(
|
||||
TextView(activity).apply { text = parts[0].trim() }, 1,
|
||||
)
|
||||
container.addView(
|
||||
TextView(activity).apply { text = parts[1].trim() }, 3,
|
||||
)
|
||||
for (i in parts.indices) {
|
||||
container.addView(
|
||||
TextView(activity).apply { text = parts[i].trim() }, 2 * i + 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSaveClicked() {
|
||||
@@ -132,6 +147,14 @@ class FrequencyPickerDialog(
|
||||
denominator = 7
|
||||
}
|
||||
}
|
||||
contentView.xTimesPerYDaysRadioButton.isChecked -> {
|
||||
if (contentView.xTimesPerYDaysXTextView.text.isNotEmpty() && contentView.xTimesPerYDaysYTextView.text.isNotEmpty()) {
|
||||
numerator =
|
||||
Integer.parseInt(contentView.xTimesPerYDaysXTextView.text.toString())
|
||||
denominator =
|
||||
Integer.parseInt(contentView.xTimesPerYDaysYTextView.text.toString())
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
if (contentView.xTimesPerMonthTextView.text.isNotEmpty()) {
|
||||
numerator = Integer.parseInt(contentView.xTimesPerMonthTextView.text.toString())
|
||||
@@ -147,10 +170,10 @@ class FrequencyPickerDialog(
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private fun check(view: RadioButton?) {
|
||||
private fun check(view: RadioButton) {
|
||||
uncheckAll()
|
||||
view?.isChecked = true
|
||||
view?.requestFocus()
|
||||
view.isChecked = true
|
||||
view.requestFocus()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -163,7 +186,7 @@ class FrequencyPickerDialog(
|
||||
if (freqDenominator == 30 || freqDenominator == 31) {
|
||||
contentView.xTimesPerMonthRadioButton.isChecked = true
|
||||
contentView.xTimesPerMonthTextView.setText(freqNumerator.toString())
|
||||
focus(contentView.xTimesPerMonthTextView)
|
||||
selectInputField(contentView.xTimesPerMonthTextView)
|
||||
} else {
|
||||
if (freqNumerator == 1) {
|
||||
if (freqDenominator == 1) {
|
||||
@@ -171,23 +194,23 @@ class FrequencyPickerDialog(
|
||||
} else {
|
||||
contentView.everyXDaysRadioButton.isChecked = true
|
||||
contentView.everyXDaysTextView.setText(freqDenominator.toString())
|
||||
focus(contentView.everyXDaysTextView)
|
||||
selectInputField(contentView.everyXDaysTextView)
|
||||
}
|
||||
} else {
|
||||
if (freqDenominator == 7) {
|
||||
contentView.xTimesPerWeekRadioButton.isChecked = true
|
||||
contentView.xTimesPerWeekTextView.setText(freqNumerator.toString())
|
||||
focus(contentView.xTimesPerWeekTextView)
|
||||
selectInputField(contentView.xTimesPerWeekTextView)
|
||||
} else {
|
||||
Log.w("FrequencyPickerDialog", "Unknown frequency: $freqNumerator/$freqDenominator")
|
||||
contentView.everyDayRadioButton.isChecked = true
|
||||
contentView.xTimesPerYDaysRadioButton.isChecked = true
|
||||
contentView.xTimesPerYDaysXTextView.setText(freqNumerator.toString())
|
||||
contentView.xTimesPerYDaysYTextView.setText(freqDenominator.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun focus(view: EditText) {
|
||||
view.requestFocus()
|
||||
private fun selectInputField(view: EditText) {
|
||||
view.setSelection(view.text.length)
|
||||
}
|
||||
|
||||
@@ -196,11 +219,6 @@ class FrequencyPickerDialog(
|
||||
contentView.everyXDaysRadioButton.isChecked = false
|
||||
contentView.xTimesPerWeekRadioButton.isChecked = false
|
||||
contentView.xTimesPerMonthRadioButton.isChecked = false
|
||||
}
|
||||
|
||||
private fun unfocusAll() {
|
||||
contentView.everyXDaysTextView.clearFocus()
|
||||
contentView.xTimesPerWeekTextView.clearFocus()
|
||||
contentView.xTimesPerMonthTextView.clearFocus()
|
||||
contentView.xTimesPerYDaysRadioButton.isChecked = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
package org.isoron.uhabits.activities.common.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import org.isoron.platform.gui.AndroidDataView
|
||||
@@ -43,12 +44,13 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
|
||||
private lateinit var commandRunner: CommandRunner
|
||||
private lateinit var habit: Habit
|
||||
private lateinit var preferences: Preferences
|
||||
private lateinit var dataView: AndroidDataView
|
||||
lateinit var dataView: AndroidDataView
|
||||
|
||||
private var chart: HistoryChart? = null
|
||||
private var onDateClickedListener: OnDateClickedListener? = null
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
clearCurrentDialog()
|
||||
val component = (activity!!.application as HabitsApplication).component
|
||||
commandRunner = component.commandRunner
|
||||
habit = component.habitList.getById(arguments!!.getLong("habit"))!!
|
||||
@@ -62,20 +64,30 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
|
||||
firstWeekday = preferences.firstWeekday,
|
||||
paletteColor = habit.color,
|
||||
series = emptyList(),
|
||||
defaultSquare = HistoryChart.Square.OFF,
|
||||
notesIndicators = emptyList(),
|
||||
theme = themeSwitcher.currentTheme,
|
||||
today = DateUtils.getTodayWithOffset().toLocalDate(),
|
||||
onDateClickedListener = onDateClickedListener ?: OnDateClickedListener { },
|
||||
onDateClickedListener = onDateClickedListener ?: object : OnDateClickedListener {},
|
||||
padding = 10.0,
|
||||
)
|
||||
dataView = AndroidDataView(context!!, null)
|
||||
dataView.view = chart!!
|
||||
|
||||
return Dialog(context!!).apply {
|
||||
val dialog = Dialog(context!!).apply {
|
||||
val metrics = resources.displayMetrics
|
||||
val maxHeight = resources.getDimensionPixelSize(R.dimen.history_editor_max_height)
|
||||
setContentView(dataView)
|
||||
window!!.setLayout(metrics.widthPixels, min(metrics.heightPixels, maxHeight))
|
||||
}
|
||||
|
||||
currentDialog = dialog
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
currentDialog = null
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -101,10 +113,22 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
|
||||
theme = LightTheme()
|
||||
)
|
||||
chart?.series = model.series
|
||||
chart?.defaultSquare = model.defaultSquare
|
||||
chart?.notesIndicators = model.notesIndicators
|
||||
dataView.postInvalidate()
|
||||
}
|
||||
|
||||
override fun onCommandFinished(command: Command) {
|
||||
refreshData()
|
||||
}
|
||||
|
||||
companion object {
|
||||
// HistoryEditorDialog handles multiple dialogs on its own,
|
||||
// because sometimes we want it to be shown under another dialog (e.g. NumberPopup)
|
||||
var currentDialog: Dialog? = null
|
||||
fun clearCurrentDialog() {
|
||||
currentDialog?.dismiss()
|
||||
currentDialog = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,108 +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.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.text.InputFilter
|
||||
import android.view.LayoutInflater
|
||||
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.EditText
|
||||
import android.widget.NumberPicker
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
|
||||
import org.isoron.uhabits.inject.ActivityContext
|
||||
import org.isoron.uhabits.utils.InterfaceUtils
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
class NumberPickerFactory
|
||||
@Inject constructor(
|
||||
@ActivityContext private val context: Context
|
||||
) {
|
||||
fun create(
|
||||
value: Double,
|
||||
unit: String,
|
||||
callback: ListHabitsBehavior.NumberPickerCallback
|
||||
): AlertDialog {
|
||||
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val view = inflater.inflate(R.layout.number_picker_dialog, null)
|
||||
|
||||
val picker = view.findViewById<NumberPicker>(R.id.picker)
|
||||
val picker2 = view.findViewById<NumberPicker>(R.id.picker2)
|
||||
val tvUnit = view.findViewById<TextView>(R.id.tvUnit)
|
||||
|
||||
val intValue = (value * 100).roundToLong().toInt()
|
||||
|
||||
picker.minValue = 0
|
||||
picker.maxValue = Integer.MAX_VALUE / 100
|
||||
picker.value = intValue / 100
|
||||
picker.wrapSelectorWheel = false
|
||||
|
||||
picker2.minValue = 0
|
||||
picker2.maxValue = 19
|
||||
picker2.setFormatter { v -> String.format("%02d", 5 * v) }
|
||||
picker2.value = intValue % 100 / 5
|
||||
refreshInitialValue(picker2)
|
||||
|
||||
tvUnit.text = unit
|
||||
|
||||
val dialog = AlertDialog.Builder(context)
|
||||
.setView(view)
|
||||
.setTitle(R.string.change_value)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
picker.clearFocus()
|
||||
val v = picker.value + 0.05 * picker2.value
|
||||
callback.onNumberPicked(v)
|
||||
}
|
||||
.setOnDismissListener {
|
||||
callback.onNumberPickerDismissed()
|
||||
}
|
||||
.create()
|
||||
|
||||
dialog.setOnShowListener {
|
||||
picker.getChildAt(0)?.requestFocus()
|
||||
dialog.window?.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE)
|
||||
}
|
||||
|
||||
InterfaceUtils.setupEditorAction(
|
||||
picker
|
||||
) { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE)
|
||||
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick()
|
||||
false
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
private fun refreshInitialValue(picker: NumberPicker) {
|
||||
// Workaround for Android bug:
|
||||
// https://code.google.com/p/android/issues/detail?id=35482
|
||||
val f = NumberPicker::class.java.getDeclaredField("mInputText")
|
||||
f.isAccessible = true
|
||||
val inputText = f.get(picker) as EditText
|
||||
inputText.filters = arrayOfNulls<InputFilter>(0)
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,7 @@ class WeekdayPickerDialog :
|
||||
.setNegativeButton(
|
||||
android.R.string.cancel
|
||||
) { _: DialogInterface?, _: Int -> dismiss() }
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.isoron.uhabits.core.utils.DateUtils.Companion.getShortWeekdayNames
|
||||
import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendar
|
||||
import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendarWithOffset
|
||||
import org.isoron.uhabits.core.utils.DateUtils.Companion.getWeekdaySequence
|
||||
import org.isoron.uhabits.core.utils.DateUtils.Companion.getWeekdaysInMonth
|
||||
import org.isoron.uhabits.utils.ColorUtils.mixColors
|
||||
import org.isoron.uhabits.utils.StyledResources
|
||||
import org.isoron.uhabits.utils.toSimpleDataFormat
|
||||
@@ -64,6 +65,7 @@ class FrequencyChart : ScrollableChart {
|
||||
private lateinit var frequency: HashMap<Timestamp, Array<Int>>
|
||||
private var maxFreq = 0
|
||||
private var firstWeekday = Calendar.SUNDAY
|
||||
private var isNumerical: Boolean = false
|
||||
|
||||
constructor(context: Context?) : super(context) {
|
||||
init()
|
||||
@@ -80,6 +82,11 @@ class FrequencyChart : ScrollableChart {
|
||||
postInvalidate()
|
||||
}
|
||||
|
||||
fun setIsNumerical(type: Boolean) {
|
||||
isNumerical = type
|
||||
postInvalidate()
|
||||
}
|
||||
|
||||
fun setFrequency(frequency: java.util.HashMap<Timestamp, Array<Int>>) {
|
||||
this.frequency = frequency
|
||||
maxFreq = getMaxFreq(frequency)
|
||||
@@ -166,6 +173,7 @@ class FrequencyChart : ScrollableChart {
|
||||
|
||||
private fun drawColumn(canvas: Canvas, rect: RectF?, date: GregorianCalendar) {
|
||||
val values = frequency[Timestamp(date)]
|
||||
val weekDaysInMonth = getWeekdaysInMonth(Timestamp(date))
|
||||
val rowHeight = rect!!.height() / 8.0f
|
||||
prevRect!!.set(rect)
|
||||
val localeWeekdayList: Array<Int> = getWeekdaySequence(firstWeekday)
|
||||
@@ -173,7 +181,8 @@ 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) drawMarker(canvas, rect, values[i])
|
||||
if (values != null)
|
||||
drawMarker(canvas, rect, values[i], weekDaysInMonth[i])
|
||||
rect.offset(0f, rowHeight)
|
||||
}
|
||||
drawFooter(canvas, rect, date)
|
||||
@@ -221,12 +230,16 @@ class FrequencyChart : ScrollableChart {
|
||||
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid!!)
|
||||
}
|
||||
|
||||
private fun drawMarker(canvas: Canvas, rect: RectF?, value: Int?) {
|
||||
private fun drawMarker(canvas: Canvas, rect: RectF?, value: Int?, weekdayFrequency: Int) {
|
||||
// value can be negative when the entry is skipped
|
||||
val valueCopy = value?.let { max(0, it) }
|
||||
|
||||
val padding = rect!!.height() * 0.2f
|
||||
// maximal allowed mark radius
|
||||
val maxRadius = (rect.height() - 2 * padding) / 2.0f
|
||||
// the real mark radius is scaled down by a factor depending on the maximal frequency
|
||||
val scale = 1.0f / maxFreq * value!!
|
||||
val scalingFactor = if (isNumerical) maxFreq else weekdayFrequency
|
||||
val scale = 1.0f / scalingFactor * valueCopy!!
|
||||
val radius = maxRadius * scale
|
||||
val colorIndex = min((colors.size - 1), ((colors.size - 1) * scale).roundToInt())
|
||||
pGraph!!.color = colors[colorIndex]
|
||||
|
||||
@@ -59,6 +59,7 @@ import org.isoron.uhabits.core.models.Reminder
|
||||
import org.isoron.uhabits.core.models.WeekdayList
|
||||
import org.isoron.uhabits.databinding.ActivityEditHabitBinding
|
||||
import org.isoron.uhabits.utils.ColorUtils
|
||||
import org.isoron.uhabits.utils.dismissCurrentAndShow
|
||||
import org.isoron.uhabits.utils.formatTime
|
||||
import org.isoron.uhabits.utils.toFormattedString
|
||||
|
||||
@@ -69,7 +70,7 @@ fun formatFrequency(freqNum: Int, freqDen: Int, resources: Resources) = when {
|
||||
freqNum == 1 && freqDen == 7 -> resources.getString(R.string.every_week)
|
||||
freqNum == 1 && freqDen > 1 -> resources.getString(R.string.every_x_days, freqDen)
|
||||
freqDen == 7 -> resources.getString(R.string.x_times_per_week, freqNum)
|
||||
else -> "$freqNum/$freqDen"
|
||||
else -> resources.getString(R.string.x_times_per_y_days, freqNum, freqDen)
|
||||
}
|
||||
|
||||
class EditHabitActivity : AppCompatActivity() {
|
||||
@@ -88,6 +89,7 @@ class EditHabitActivity : AppCompatActivity() {
|
||||
var reminderHour = -1
|
||||
var reminderMin = -1
|
||||
var reminderDays: WeekdayList = WeekdayList.EVERY_DAY
|
||||
var targetType = NumericalHabitType.AT_LEAST
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
@@ -107,6 +109,7 @@ class EditHabitActivity : AppCompatActivity() {
|
||||
color = habit.color
|
||||
freqNum = habit.frequency.numerator
|
||||
freqDen = habit.frequency.denominator
|
||||
targetType = habit.targetType
|
||||
habit.reminder?.let {
|
||||
reminderHour = it.hour
|
||||
reminderMin = it.minute
|
||||
@@ -138,6 +141,7 @@ class EditHabitActivity : AppCompatActivity() {
|
||||
HabitType.YES_NO -> {
|
||||
binding.unitOuterBox.visibility = View.GONE
|
||||
binding.targetOuterBox.visibility = View.GONE
|
||||
binding.targetTypeOuterBox.visibility = View.GONE
|
||||
}
|
||||
HabitType.NUMERICAL -> {
|
||||
binding.nameInput.hint = getString(R.string.measurable_short_example)
|
||||
@@ -153,23 +157,41 @@ class EditHabitActivity : AppCompatActivity() {
|
||||
|
||||
val colorPickerDialogFactory = ColorPickerDialogFactory(this)
|
||||
binding.colorButton.setOnClickListener {
|
||||
val dialog = colorPickerDialogFactory.create(color, themeSwitcher.currentTheme)
|
||||
dialog.setListener { paletteColor ->
|
||||
val picker = colorPickerDialogFactory.create(color, themeSwitcher.currentTheme)
|
||||
picker.setListener { paletteColor ->
|
||||
this.color = paletteColor
|
||||
updateColors()
|
||||
}
|
||||
dialog.show(supportFragmentManager, "colorPicker")
|
||||
picker.dismissCurrentAndShow(supportFragmentManager, "colorPicker")
|
||||
}
|
||||
|
||||
populateFrequency()
|
||||
binding.booleanFrequencyPicker.setOnClickListener {
|
||||
val dialog = FrequencyPickerDialog(freqNum, freqDen)
|
||||
dialog.onFrequencyPicked = { num, den ->
|
||||
val picker = FrequencyPickerDialog(freqNum, freqDen)
|
||||
picker.onFrequencyPicked = { num, den ->
|
||||
freqNum = num
|
||||
freqDen = den
|
||||
populateFrequency()
|
||||
}
|
||||
dialog.show(supportFragmentManager, "frequencyPicker")
|
||||
picker.dismissCurrentAndShow(supportFragmentManager, "frequencyPicker")
|
||||
}
|
||||
|
||||
populateTargetType()
|
||||
binding.targetTypePicker.setOnClickListener {
|
||||
val builder = AlertDialog.Builder(this)
|
||||
val arrayAdapter = ArrayAdapter<String>(this, android.R.layout.select_dialog_item)
|
||||
arrayAdapter.add(getString(R.string.target_type_at_least))
|
||||
arrayAdapter.add(getString(R.string.target_type_at_most))
|
||||
builder.setAdapter(arrayAdapter) { dialog, which ->
|
||||
targetType = when (which) {
|
||||
0 -> NumericalHabitType.AT_LEAST
|
||||
else -> NumericalHabitType.AT_MOST
|
||||
}
|
||||
populateTargetType()
|
||||
dialog.dismiss()
|
||||
}
|
||||
val dialog = builder.create()
|
||||
dialog.dismissCurrentAndShow()
|
||||
}
|
||||
|
||||
binding.numericalFrequencyPicker.setOnClickListener {
|
||||
@@ -215,7 +237,7 @@ class EditHabitActivity : AppCompatActivity() {
|
||||
is24HourMode,
|
||||
androidColor
|
||||
)
|
||||
dialog.show(supportFragmentManager, "timePicker")
|
||||
dialog.dismissCurrentAndShow(supportFragmentManager, "timePicker")
|
||||
}
|
||||
|
||||
binding.reminderDatePicker.setOnClickListener {
|
||||
@@ -227,7 +249,7 @@ class EditHabitActivity : AppCompatActivity() {
|
||||
populateReminder()
|
||||
}
|
||||
dialog.setSelectedDays(reminderDays)
|
||||
dialog.show(supportFragmentManager, "dayPicker")
|
||||
dialog.dismissCurrentAndShow(supportFragmentManager, "dayPicker")
|
||||
}
|
||||
|
||||
binding.buttonSave.setOnClickListener {
|
||||
@@ -262,7 +284,7 @@ class EditHabitActivity : AppCompatActivity() {
|
||||
habit.frequency = Frequency(freqNum, freqDen)
|
||||
if (habitType == HabitType.NUMERICAL) {
|
||||
habit.targetValue = targetInput.text.toString().toDouble()
|
||||
habit.targetType = NumericalHabitType.AT_LEAST
|
||||
habit.targetType = targetType
|
||||
habit.unit = unitInput.text.trim().toString()
|
||||
}
|
||||
habit.type = habitType
|
||||
@@ -324,6 +346,13 @@ class EditHabitActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateTargetType() {
|
||||
binding.targetTypePicker.text = when (targetType) {
|
||||
NumericalHabitType.AT_MOST -> getString(R.string.target_type_at_most)
|
||||
else -> getString(R.string.target_type_at_least)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateColors() {
|
||||
androidColor = themeSwitcher.currentTheme.color(color).toInt()
|
||||
binding.colorButton.backgroundTintList = ColorStateList.valueOf(androidColor)
|
||||
|
||||
@@ -21,6 +21,7 @@ package org.isoron.uhabits.activities.habits.list
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
@@ -29,6 +30,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import org.isoron.uhabits.BaseExceptionHandler
|
||||
import org.isoron.uhabits.HabitsApplication
|
||||
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
|
||||
import org.isoron.uhabits.core.models.Timestamp
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.tasks.TaskRunner
|
||||
import org.isoron.uhabits.core.ui.ThemeSwitcher.Companion.THEME_DARK
|
||||
@@ -36,11 +38,16 @@ import org.isoron.uhabits.core.utils.MidnightTimer
|
||||
import org.isoron.uhabits.database.AutoBackup
|
||||
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 {
|
||||
|
||||
var pureBlack: Boolean = false
|
||||
lateinit var appComponent: HabitsApplicationComponent
|
||||
lateinit var component: HabitsActivityComponent
|
||||
lateinit var taskRunner: TaskRunner
|
||||
lateinit var adapter: HabitCardListAdapter
|
||||
lateinit var rootView: ListHabitsRootView
|
||||
@@ -53,13 +60,14 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
||||
|
||||
override fun onQuestionMarksChanged() {
|
||||
invalidateOptionsMenu()
|
||||
menu.behavior.onPreferencesChanged()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val appComponent = (applicationContext as HabitsApplication).component
|
||||
val component = DaggerHabitsActivityComponent
|
||||
appComponent = (applicationContext as HabitsApplication).component
|
||||
component = DaggerHabitsActivityComponent
|
||||
.builder()
|
||||
.activityContextModule(ActivityContextModule(this))
|
||||
.habitsApplicationComponent(appComponent)
|
||||
@@ -82,8 +90,9 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
||||
|
||||
override fun onPause() {
|
||||
midnightTimer.onPause()
|
||||
screen.onDettached()
|
||||
screen.onDetached()
|
||||
adapter.cancelRefresh()
|
||||
dismissCurrentDialog()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
@@ -92,12 +101,19 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
||||
screen.onAttached()
|
||||
rootView.postInvalidate()
|
||||
midnightTimer.onResume()
|
||||
appComponent.reminderScheduler.scheduleAll()
|
||||
taskRunner.run {
|
||||
AutoBackup(this@ListHabitsActivity).run()
|
||||
try {
|
||||
AutoBackup(this@ListHabitsActivity).run()
|
||||
appComponent.widgetUpdater.updateWidgets()
|
||||
} catch (e: Exception) {
|
||||
Log.e("ListHabitActivity", "TaskRunner failed", e)
|
||||
}
|
||||
}
|
||||
if (prefs.theme == THEME_DARK && prefs.isPureBlackEnabled != pureBlack) {
|
||||
restartWithFade(ListHabitsActivity::class.java)
|
||||
}
|
||||
parseIntents()
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
@@ -115,4 +131,26 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
||||
super.onActivityResult(request, result, data)
|
||||
screen.onResult(request, result, data)
|
||||
}
|
||||
|
||||
private fun parseIntents() {
|
||||
if (intent == null) return
|
||||
if (intent.action == ACTION_EDIT) {
|
||||
val habitId = intent.extras?.getLong("habit")
|
||||
val timestamp = intent.extras?.getLong("timestamp")
|
||||
if (habitId != null && timestamp != null) {
|
||||
val habit = appComponent.habitList.getById(habitId)!!
|
||||
component.listHabitsBehavior.onEdit(habit, Timestamp(timestamp))
|
||||
}
|
||||
}
|
||||
intent = null
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_EDIT = "org.isoron.uhabits.ACTION_EDIT"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class ListHabitsMenu @Inject constructor(
|
||||
@ActivityContext context: Context,
|
||||
private val preferences: Preferences,
|
||||
private val themeSwitcher: ThemeSwitcher,
|
||||
private val behavior: ListHabitsMenuBehavior
|
||||
val behavior: ListHabitsMenuBehavior
|
||||
) {
|
||||
val activity = (context as AppCompatActivity)
|
||||
|
||||
|
||||
@@ -22,12 +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.CheckmarkDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
|
||||
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
|
||||
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
|
||||
@@ -40,6 +43,7 @@ import org.isoron.uhabits.core.commands.EditHabitCommand
|
||||
import org.isoron.uhabits.core.commands.UnarchiveHabitsCommand
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.tasks.TaskRunner
|
||||
import org.isoron.uhabits.core.ui.ThemeSwitcher
|
||||
import org.isoron.uhabits.core.ui.callbacks.OnColorPickedCallback
|
||||
@@ -60,6 +64,8 @@ import org.isoron.uhabits.tasks.ExportDBTaskFactory
|
||||
import org.isoron.uhabits.tasks.ImportDataTask
|
||||
import org.isoron.uhabits.tasks.ImportDataTaskFactory
|
||||
import org.isoron.uhabits.utils.copyTo
|
||||
import org.isoron.uhabits.utils.currentTheme
|
||||
import org.isoron.uhabits.utils.dismissCurrentAndShow
|
||||
import org.isoron.uhabits.utils.restartWithFade
|
||||
import org.isoron.uhabits.utils.showMessage
|
||||
import org.isoron.uhabits.utils.showSendEmailScreen
|
||||
@@ -88,8 +94,9 @@ class ListHabitsScreen
|
||||
private val exportDBFactory: ExportDBTaskFactory,
|
||||
private val importTaskFactory: ImportDataTaskFactory,
|
||||
private val colorPickerFactory: ColorPickerDialogFactory,
|
||||
private val numberPickerFactory: NumberPickerFactory,
|
||||
private val behavior: Lazy<ListHabitsBehavior>
|
||||
private val behavior: Lazy<ListHabitsBehavior>,
|
||||
private val preferences: Preferences,
|
||||
private val rootView: Lazy<ListHabitsRootView>,
|
||||
) : CommandRunner.Listener,
|
||||
ListHabitsBehavior.Screen,
|
||||
ListHabitsMenuBehavior.Screen,
|
||||
@@ -101,7 +108,7 @@ class ListHabitsScreen
|
||||
commandRunner.addListener(this)
|
||||
}
|
||||
|
||||
fun onDettached() {
|
||||
fun onDetached() {
|
||||
commandRunner.removeListener(this)
|
||||
}
|
||||
|
||||
@@ -158,7 +165,7 @@ class ListHabitsScreen
|
||||
}
|
||||
|
||||
override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback, quantity: Int) {
|
||||
ConfirmDeleteDialog(activity, callback, quantity).show()
|
||||
ConfirmDeleteDialog(activity, callback, quantity).dismissCurrentAndShow()
|
||||
}
|
||||
|
||||
override fun showEditHabitsScreen(selected: List<Habit>) {
|
||||
@@ -219,15 +226,40 @@ class ListHabitsScreen
|
||||
override fun showColorPicker(defaultColor: PaletteColor, callback: OnColorPickedCallback) {
|
||||
val picker = colorPickerFactory.create(defaultColor, themeSwitcher.currentTheme!!)
|
||||
picker.setListener(callback)
|
||||
picker.show(activity.supportFragmentManager, "picker")
|
||||
picker.dismissCurrentAndShow(activity.supportFragmentManager, "picker")
|
||||
}
|
||||
|
||||
override fun showNumberPicker(
|
||||
override fun showNumberPopup(
|
||||
value: Double,
|
||||
unit: String,
|
||||
notes: String,
|
||||
callback: ListHabitsBehavior.NumberPickerCallback
|
||||
) {
|
||||
numberPickerFactory.create(value, unit, callback).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(
|
||||
selectedValue: Int,
|
||||
notes: String,
|
||||
color: PaletteColor,
|
||||
callback: ListHabitsBehavior.CheckMarkDialogCallback
|
||||
) {
|
||||
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? {
|
||||
|
||||
@@ -37,9 +37,9 @@ import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.inject.ActivityContext
|
||||
import org.isoron.uhabits.utils.dim
|
||||
import org.isoron.uhabits.utils.drawNotesIndicator
|
||||
import org.isoron.uhabits.utils.getFontAwesome
|
||||
import org.isoron.uhabits.utils.showMessage
|
||||
import org.isoron.uhabits.utils.sp
|
||||
import org.isoron.uhabits.utils.sres
|
||||
import org.isoron.uhabits.utils.toMeasureSpec
|
||||
import javax.inject.Inject
|
||||
@@ -71,33 +71,42 @@ class CheckmarkButtonView(
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var onToggle: (Int) -> Unit = {}
|
||||
var notes = ""
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var onToggle: (Int, String) -> Unit = { _, _ -> }
|
||||
|
||||
var onEdit: () -> Unit = { }
|
||||
|
||||
private var drawer = Drawer()
|
||||
|
||||
init {
|
||||
isFocusable = false
|
||||
setOnClickListener(this)
|
||||
setOnLongClickListener(this)
|
||||
}
|
||||
|
||||
fun performToggle() {
|
||||
value = if (preferences.isSkipEnabled) {
|
||||
Entry.nextToggleValueWithSkip(value)
|
||||
} else {
|
||||
Entry.nextToggleValueWithoutSkip(value)
|
||||
}
|
||||
onToggle(value)
|
||||
value = Entry.nextToggleValue(
|
||||
value = value,
|
||||
isSkipEnabled = preferences.isSkipEnabled,
|
||||
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
|
||||
)
|
||||
onToggle(value, notes)
|
||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
if (preferences.isShortToggleEnabled) performToggle()
|
||||
else showMessage(resources.getString(R.string.long_press_to_toggle))
|
||||
else onEdit()
|
||||
}
|
||||
|
||||
override fun onLongClick(v: View): Boolean {
|
||||
performToggle()
|
||||
if (preferences.isShortToggleEnabled) onEdit()
|
||||
else performToggle()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -145,6 +154,11 @@ class CheckmarkButtonView(
|
||||
}
|
||||
else -> R.string.fa_check
|
||||
}
|
||||
paint.textSize = when {
|
||||
id == R.string.fa_question -> sp(12.0f)
|
||||
value == YES_AUTO -> sp(13.0f)
|
||||
else -> sp(14.0f)
|
||||
}
|
||||
if (value == YES_AUTO) {
|
||||
paint.strokeWidth = 5f
|
||||
paint.style = Paint.Style.STROKE
|
||||
@@ -153,11 +167,6 @@ class CheckmarkButtonView(
|
||||
paint.style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
paint.textSize = when (id) {
|
||||
UNKNOWN -> dim(R.dimen.smallerTextSize)
|
||||
else -> dim(R.dimen.smallTextSize)
|
||||
}
|
||||
|
||||
val label = resources.getString(id)
|
||||
val em = paint.measureText("m")
|
||||
|
||||
@@ -170,6 +179,8 @@ class CheckmarkButtonView(
|
||||
paint.style = Paint.Style.FILL
|
||||
canvas.drawText(label, rect.centerX(), rect.centerY(), paint)
|
||||
}
|
||||
|
||||
drawNotesIndicator(canvas, color, em, notes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,19 @@ class CheckmarkPanelView(
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var onToggle: (Timestamp, Int) -> Unit = { _, _ -> }
|
||||
var notes = arrayOf<String>()
|
||||
set(values) {
|
||||
field = values
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var onToggle: (Timestamp, Int, String) -> Unit = { _, _, _ -> }
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var onEdit: (Timestamp) -> Unit = { _ -> }
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
@@ -72,8 +84,13 @@ class CheckmarkPanelView(
|
||||
index + dataOffset < values.size -> values[index + dataOffset]
|
||||
else -> UNKNOWN
|
||||
}
|
||||
button.notes = when {
|
||||
index + dataOffset < notes.size -> notes[index + dataOffset]
|
||||
else -> ""
|
||||
}
|
||||
button.color = color
|
||||
button.onToggle = { value -> onToggle(timestamp, value) }
|
||||
button.onToggle = { value, notes -> onToggle(timestamp, value, notes) }
|
||||
button.onEdit = { onEdit(timestamp) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,8 +124,9 @@ class HabitCardListAdapter @Inject constructor(
|
||||
val habit = cache.getHabitByPosition(position)
|
||||
val score = cache.getScore(habit!!.id!!)
|
||||
val checkmarks = cache.getCheckmarks(habit.id!!)
|
||||
val notes = cache.getNotes(habit.id!!)
|
||||
val selected = selected.contains(habit)
|
||||
listView!!.bindCardView(holder, habit, score, checkmarks, selected)
|
||||
listView!!.bindCardView(holder, habit, score, checkmarks, notes, selected)
|
||||
}
|
||||
|
||||
override fun onViewAttachedToWindow(holder: HabitCardViewHolder) {
|
||||
|
||||
@@ -87,6 +87,7 @@ class HabitCardListView(
|
||||
habit: Habit,
|
||||
score: Double,
|
||||
checkmarks: IntArray,
|
||||
notes: Array<String>,
|
||||
selected: Boolean
|
||||
): View {
|
||||
val cardView = holder.itemView as HabitCardView
|
||||
@@ -98,6 +99,7 @@ class HabitCardListView(
|
||||
cardView.score = score
|
||||
cardView.unit = habit.unit
|
||||
cardView.threshold = habit.targetValue / habit.frequency.denominator
|
||||
cardView.notes = notes
|
||||
|
||||
val detector = GestureDetector(context, CardViewGestureDetector(holder))
|
||||
cardView.setOnTouchListener { _, ev ->
|
||||
|
||||
@@ -21,8 +21,8 @@ package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.text.LineBreaker.BREAK_STRATEGY_BALANCED
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Build.VERSION_CODES.M
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.TextUtils
|
||||
@@ -115,12 +115,21 @@ class HabitCardView(
|
||||
numberPanel.threshold = value
|
||||
}
|
||||
|
||||
var notes
|
||||
get() = checkmarkPanel.notes
|
||||
set(values) {
|
||||
checkmarkPanel.notes = values
|
||||
numberPanel.notes = values
|
||||
}
|
||||
|
||||
var checkmarkPanel: CheckmarkPanelView
|
||||
private var numberPanel: NumberPanelView
|
||||
private var innerFrame: LinearLayout
|
||||
private var label: TextView
|
||||
private var scoreRing: RingView
|
||||
|
||||
private var currentToggleTaskId = 0
|
||||
|
||||
init {
|
||||
scoreRing = RingView(context).apply {
|
||||
val thickness = dp(3f)
|
||||
@@ -137,13 +146,19 @@ class HabitCardView(
|
||||
maxLines = 2
|
||||
ellipsize = TextUtils.TruncateAt.END
|
||||
layoutParams = LinearLayout.LayoutParams(0, WRAP_CONTENT, 1f)
|
||||
if (SDK_INT >= M) breakStrategy = BREAK_STRATEGY_BALANCED
|
||||
if (SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
breakStrategy = BREAK_STRATEGY_BALANCED
|
||||
}
|
||||
}
|
||||
|
||||
checkmarkPanel = checkmarkPanelFactory.create().apply {
|
||||
onToggle = { timestamp, value ->
|
||||
onToggle = { timestamp, value, notes ->
|
||||
triggerRipple(timestamp)
|
||||
habit?.let { behavior.onToggle(it, timestamp, value) }
|
||||
habit?.let { behavior.onToggle(it, timestamp, value, notes) }
|
||||
}
|
||||
onEdit = { timestamp ->
|
||||
triggerRipple(timestamp)
|
||||
habit?.let { behavior.onEdit(it, timestamp) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,6 +251,7 @@ class HabitCardView(
|
||||
numberPanel.apply {
|
||||
color = c
|
||||
units = h.unit
|
||||
targetType = h.targetType
|
||||
threshold = h.targetValue
|
||||
visibility = when (h.isNumerical) {
|
||||
true -> View.VISIBLE
|
||||
@@ -262,4 +278,10 @@ class HabitCardView(
|
||||
}
|
||||
innerFrame.setBackgroundResource(background)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun (() -> Unit).delay(delayInMillis: Long) {
|
||||
Handler(Looper.getMainLooper()).postDelayed(this, delayInMillis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,13 +29,16 @@ import android.view.View
|
||||
import android.view.View.OnClickListener
|
||||
import android.view.View.OnLongClickListener
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.models.Entry
|
||||
import org.isoron.uhabits.core.models.NumericalHabitType.AT_LEAST
|
||||
import org.isoron.uhabits.core.models.NumericalHabitType.AT_MOST
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.inject.ActivityContext
|
||||
import org.isoron.uhabits.utils.InterfaceUtils.getDimension
|
||||
import org.isoron.uhabits.utils.StyledResources
|
||||
import org.isoron.uhabits.utils.dim
|
||||
import org.isoron.uhabits.utils.drawNotesIndicator
|
||||
import org.isoron.uhabits.utils.getFontAwesome
|
||||
import org.isoron.uhabits.utils.showMessage
|
||||
import org.isoron.uhabits.utils.sres
|
||||
import java.text.DecimalFormat
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -88,13 +91,25 @@ class NumberButtonView(
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var units = ""
|
||||
var targetType = AT_LEAST
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var onEdit: () -> Unit = {}
|
||||
var units = ""
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
var notes = ""
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var onEdit: () -> Unit = { }
|
||||
|
||||
private var drawer: Drawer = Drawer(context)
|
||||
|
||||
init {
|
||||
@@ -103,8 +118,7 @@ class NumberButtonView(
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
if (preferences.isShortToggleEnabled) onEdit()
|
||||
else showMessage(resources.getString(R.string.long_press_to_edit))
|
||||
onEdit()
|
||||
}
|
||||
|
||||
override fun onLongClick(v: View): Boolean {
|
||||
@@ -127,11 +141,16 @@ class NumberButtonView(
|
||||
|
||||
private val em: Float
|
||||
private val rect: RectF = RectF()
|
||||
private val sr = StyledResources(context)
|
||||
|
||||
private val lowContrast: Int
|
||||
private val mediumContrast: Int
|
||||
|
||||
private val paint = TextPaint().apply {
|
||||
typeface = getFontAwesome()
|
||||
isAntiAlias = true
|
||||
textAlign = Paint.Align.CENTER
|
||||
}
|
||||
|
||||
private val pUnit: TextPaint = TextPaint().apply {
|
||||
textSize = getDimension(context, R.dimen.smallerTextSize)
|
||||
typeface = NORMAL_TYPEFACE
|
||||
@@ -148,15 +167,16 @@ class NumberButtonView(
|
||||
|
||||
init {
|
||||
em = pNumber.measureText("m")
|
||||
lowContrast = sr.getColor(R.attr.contrast40)
|
||||
mediumContrast = sr.getColor(R.attr.contrast60)
|
||||
lowContrast = sres.getColor(R.attr.contrast40)
|
||||
mediumContrast = sres.getColor(R.attr.contrast60)
|
||||
}
|
||||
|
||||
fun draw(canvas: Canvas) {
|
||||
val activeColor = when {
|
||||
value <= 0.0 -> lowContrast
|
||||
value < threshold -> mediumContrast
|
||||
else -> color
|
||||
value < 0.0 -> lowContrast
|
||||
(targetType == AT_LEAST) && (value >= threshold) -> color
|
||||
(targetType == AT_MOST) && (value <= threshold) -> color
|
||||
else -> mediumContrast
|
||||
}
|
||||
|
||||
val label: String
|
||||
@@ -164,6 +184,11 @@ class NumberButtonView(
|
||||
val textSize: Float
|
||||
|
||||
when {
|
||||
value == Entry.SKIP.toDouble() / 1000 -> {
|
||||
label = resources.getString(R.string.fa_skipped)
|
||||
textSize = dim(R.dimen.smallTextSize)
|
||||
typeface = getFontAwesome()
|
||||
}
|
||||
value >= 0 -> {
|
||||
label = value.toShortString()
|
||||
typeface = BOLD_TYPEFACE
|
||||
@@ -196,6 +221,8 @@ class NumberButtonView(
|
||||
rect.offset(0f, 1.3f * em)
|
||||
canvas.drawText(units, rect.centerX(), rect.centerY(), pUnit)
|
||||
}
|
||||
|
||||
drawNotesIndicator(canvas, color, em, notes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.content.Context
|
||||
import org.isoron.uhabits.core.models.NumericalHabitType
|
||||
import org.isoron.uhabits.core.models.Timestamp
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.utils.DateUtils
|
||||
@@ -47,6 +48,12 @@ class NumberPanelView(
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var targetType = NumericalHabitType.AT_LEAST
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var threshold = 0.0
|
||||
set(value) {
|
||||
field = value
|
||||
@@ -65,7 +72,13 @@ class NumberPanelView(
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var onEdit: (Timestamp) -> Unit = {}
|
||||
var notes = arrayOf<String>()
|
||||
set(values) {
|
||||
field = values
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var onEdit: (Timestamp) -> Unit = { _ -> }
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
@@ -83,7 +96,12 @@ class NumberPanelView(
|
||||
index + dataOffset < values.size -> values[index + dataOffset]
|
||||
else -> 0.0
|
||||
}
|
||||
button.notes = when {
|
||||
index + dataOffset < notes.size -> notes[index + dataOffset]
|
||||
else -> ""
|
||||
}
|
||||
button.color = color
|
||||
button.targetType = targetType
|
||||
button.threshold = threshold
|
||||
button.units = units
|
||||
button.onEdit = { onEdit(timestamp) }
|
||||
|
||||
@@ -23,21 +23,25 @@ import android.os.Bundle
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.isoron.platform.gui.toInt
|
||||
import org.isoron.uhabits.AndroidDirFinder
|
||||
import org.isoron.uhabits.HabitsApplication
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
||||
import org.isoron.uhabits.activities.HabitsDirFinder
|
||||
import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
|
||||
import org.isoron.uhabits.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
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
|
||||
@@ -45,6 +49,9 @@ import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitMenuPresenter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitPresenter
|
||||
import org.isoron.uhabits.core.ui.views.OnDateClickedListener
|
||||
import org.isoron.uhabits.intents.IntentFactory
|
||||
import org.isoron.uhabits.utils.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
|
||||
@@ -123,6 +130,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
dismissCurrentDialog()
|
||||
commandRunner.removeListener(this)
|
||||
super.onPause()
|
||||
}
|
||||
@@ -161,12 +169,40 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
||||
window.decorView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
}
|
||||
|
||||
override fun showNumberPicker(
|
||||
override fun showNumberPopup(
|
||||
value: Double,
|
||||
unit: String,
|
||||
callback: ListHabitsBehavior.NumberPickerCallback,
|
||||
notes: String,
|
||||
callback: ListHabitsBehavior.NumberPickerCallback
|
||||
) {
|
||||
NumberPickerFactory(this@ShowHabitActivity).create(value, unit, callback).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,
|
||||
color: PaletteColor,
|
||||
callback: ListHabitsBehavior.CheckMarkDialogCallback
|
||||
) {
|
||||
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? {
|
||||
val dialog = supportFragmentManager.findFragmentByTag("historyEditor") as HistoryEditorDialog?
|
||||
return dialog?.dataView
|
||||
}
|
||||
|
||||
override fun showEditHabitScreen(habit: Habit) {
|
||||
@@ -186,7 +222,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
||||
}
|
||||
|
||||
override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback) {
|
||||
ConfirmDeleteDialog(this@ShowHabitActivity, callback, 1).show()
|
||||
ConfirmDeleteDialog(this@ShowHabitActivity, callback, 1).dismissCurrentAndShow()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
|
||||
@@ -33,6 +33,7 @@ class FrequencyCardView(context: Context, attrs: AttributeSet) : LinearLayout(co
|
||||
fun setState(state: FrequencyCardState) {
|
||||
val androidColor = state.theme.color(state.color).toInt()
|
||||
binding.frequencyChart.setFrequency(state.frequency)
|
||||
binding.frequencyChart.setIsNumerical(state.isNumerical)
|
||||
binding.frequencyChart.setFirstWeekday(state.firstWeekday)
|
||||
binding.title.setTextColor(androidColor)
|
||||
binding.frequencyChart.setColor(androidColor)
|
||||
|
||||
@@ -43,6 +43,8 @@ class HistoryCardView(context: Context, attrs: AttributeSet) : LinearLayout(cont
|
||||
theme = state.theme,
|
||||
dateFormatter = JavaLocalDateFormatter(Locale.getDefault()),
|
||||
series = state.series,
|
||||
defaultSquare = state.defaultSquare,
|
||||
notesIndicators = state.notesIndicators,
|
||||
firstWeekday = state.firstWeekday,
|
||||
)
|
||||
binding.chart.postInvalidate()
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.isoron.platform.gui.toInt
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.activities.habits.edit.formatFrequency
|
||||
import org.isoron.uhabits.activities.habits.list.views.toShortString
|
||||
import org.isoron.uhabits.core.models.NumericalHabitType
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardState
|
||||
import org.isoron.uhabits.databinding.ShowHabitSubtitleBinding
|
||||
import org.isoron.uhabits.utils.InterfaceUtils
|
||||
@@ -65,7 +66,12 @@ class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
|
||||
binding.questionLabel.visibility = View.VISIBLE
|
||||
binding.targetIcon.visibility = View.VISIBLE
|
||||
binding.targetText.visibility = View.VISIBLE
|
||||
if (!state.isNumerical) {
|
||||
if (state.isNumerical) {
|
||||
binding.targetIcon.text = when (state.targetType) {
|
||||
NumericalHabitType.AT_LEAST -> resources.getString(R.string.fa_arrow_circle_up)
|
||||
else -> resources.getString(R.string.fa_arrow_circle_down)
|
||||
}
|
||||
} else {
|
||||
binding.targetIcon.visibility = View.GONE
|
||||
binding.targetText.visibility = View.GONE
|
||||
}
|
||||
|
||||
@@ -21,8 +21,9 @@ package org.isoron.uhabits.activities.intro
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import com.github.paolorotolo.appintro.AppIntro2
|
||||
import com.github.paolorotolo.appintro.AppIntroFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.github.appintro.AppIntro2
|
||||
import com.github.appintro.AppIntroFragment
|
||||
import org.isoron.uhabits.R
|
||||
|
||||
/**
|
||||
@@ -30,7 +31,9 @@ import org.isoron.uhabits.R
|
||||
* launched for the first time.
|
||||
*/
|
||||
class IntroActivity : AppIntro2() {
|
||||
override fun init(savedInstanceState: Bundle?) {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
showStatusBar(false)
|
||||
|
||||
addSlide(
|
||||
@@ -61,11 +64,13 @@ class IntroActivity : AppIntro2() {
|
||||
)
|
||||
}
|
||||
|
||||
override fun onNextPressed() {}
|
||||
|
||||
override fun onDonePressed() {
|
||||
override fun onDonePressed(currentFragment: Fragment?) {
|
||||
super.onDonePressed(currentFragment)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onSlideChanged() {}
|
||||
override fun onSkipPressed(currentFragment: Fragment?) {
|
||||
super.onSkipPressed(currentFragment)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,10 +23,11 @@ import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceCategory
|
||||
@@ -43,6 +44,7 @@ import org.isoron.uhabits.core.ui.NotificationTray
|
||||
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.widgets.WidgetUpdater
|
||||
import java.util.Calendar
|
||||
|
||||
@@ -84,13 +86,19 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val sr = StyledResources(context!!)
|
||||
view.setBackgroundColor(sr.getColor(R.attr.contrast0))
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
val key = preference.key ?: return false
|
||||
if (key == "reminderSound") {
|
||||
showRingtonePicker()
|
||||
return true
|
||||
} else if (key == "reminderCustomize") {
|
||||
if (VERSION.SDK_INT < Build.VERSION_CODES.O) return true
|
||||
if (SDK_INT < Build.VERSION_CODES.O) return true
|
||||
createAndroidNotificationChannel(context!!)
|
||||
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, context!!.packageName)
|
||||
@@ -112,7 +120,7 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
|
||||
}
|
||||
updateWeekdayPreference()
|
||||
|
||||
if (VERSION.SDK_INT < Build.VERSION_CODES.O)
|
||||
if (SDK_INT < Build.VERSION_CODES.O)
|
||||
findPreference("reminderCustomize").isVisible = false
|
||||
else {
|
||||
findPreference("reminderSound").isVisible = false
|
||||
|
||||
@@ -23,17 +23,17 @@ import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import org.isoron.uhabits.HabitsApplication
|
||||
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
||||
import org.isoron.uhabits.core.models.HabitMatcherBuilder
|
||||
import org.isoron.uhabits.core.models.HabitMatcher
|
||||
|
||||
class EditSettingActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val app = applicationContext as HabitsApplication
|
||||
val habits = app.component.habitList.getFiltered(
|
||||
HabitMatcherBuilder()
|
||||
.setArchivedAllowed(false)
|
||||
.setCompletedAllowed(true)
|
||||
.build()
|
||||
HabitMatcher(
|
||||
isArchivedAllowed = false,
|
||||
isCompletedAllowed = true,
|
||||
)
|
||||
)
|
||||
AndroidThemeSwitcher(this, app.component.preferences).apply()
|
||||
|
||||
|
||||
@@ -25,8 +25,6 @@ import android.app.AlarmManager.RTC_WAKEUP
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Context.ALARM_SERVICE
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Build.VERSION_CODES.M
|
||||
import android.util.Log
|
||||
import org.isoron.uhabits.core.AppScope
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
@@ -58,10 +56,7 @@ class IntentScheduler
|
||||
)
|
||||
return SchedulerResult.IGNORED
|
||||
}
|
||||
if (SDK_INT >= M)
|
||||
manager.setExactAndAllowWhileIdle(alarmType, timestamp, intent)
|
||||
else
|
||||
manager.setExact(alarmType, timestamp, intent)
|
||||
manager.setExactAndAllowWhileIdle(alarmType, timestamp, intent)
|
||||
return SchedulerResult.OK
|
||||
}
|
||||
|
||||
|
||||
@@ -20,11 +20,17 @@
|
||||
package org.isoron.uhabits.intents
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_IMMUTABLE
|
||||
import android.app.PendingIntent.FLAG_MUTABLE
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.app.PendingIntent.getActivity
|
||||
import android.app.PendingIntent.getBroadcast
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import org.isoron.uhabits.activities.habits.list.ListHabitsActivity
|
||||
import org.isoron.uhabits.activities.habits.show.ShowHabitActivity
|
||||
import org.isoron.uhabits.core.AppScope
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.Timestamp
|
||||
@@ -49,7 +55,7 @@ class PendingIntentFactory
|
||||
action = WidgetReceiver.ACTION_ADD_REPETITION
|
||||
if (timestamp != null) putExtra("timestamp", timestamp.unixTime)
|
||||
},
|
||||
FLAG_UPDATE_CURRENT
|
||||
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
fun dismissNotification(habit: Habit): PendingIntent =
|
||||
@@ -60,7 +66,7 @@ class PendingIntentFactory
|
||||
action = WidgetReceiver.ACTION_DISMISS_REMINDER
|
||||
data = Uri.parse(habit.uriString)
|
||||
},
|
||||
FLAG_UPDATE_CURRENT
|
||||
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
fun removeRepetition(habit: Habit, timestamp: Timestamp?): PendingIntent =
|
||||
@@ -72,7 +78,7 @@ class PendingIntentFactory
|
||||
data = Uri.parse(habit.uriString)
|
||||
if (timestamp != null) putExtra("timestamp", timestamp.unixTime)
|
||||
},
|
||||
FLAG_UPDATE_CURRENT
|
||||
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
fun showHabit(habit: Habit): PendingIntent =
|
||||
@@ -84,7 +90,21 @@ class PendingIntentFactory
|
||||
habit
|
||||
)
|
||||
)
|
||||
.getPendingIntent(0, FLAG_UPDATE_CURRENT)!!
|
||||
.getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)!!
|
||||
|
||||
fun showHabitTemplate(): PendingIntent {
|
||||
return getActivity(
|
||||
context,
|
||||
0,
|
||||
Intent(context, ShowHabitActivity::class.java),
|
||||
getIntentTemplateFlags()
|
||||
)
|
||||
}
|
||||
|
||||
fun showHabitFillIn(habit: Habit) =
|
||||
Intent().apply {
|
||||
data = Uri.parse(habit.uriString)
|
||||
}
|
||||
|
||||
fun showReminder(
|
||||
habit: Habit,
|
||||
@@ -100,7 +120,7 @@ class PendingIntentFactory
|
||||
putExtra("timestamp", timestamp)
|
||||
putExtra("reminderTime", reminderTime)
|
||||
},
|
||||
FLAG_UPDATE_CURRENT
|
||||
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
fun snoozeNotification(habit: Habit): PendingIntent =
|
||||
@@ -111,7 +131,7 @@ class PendingIntentFactory
|
||||
data = Uri.parse(habit.uriString)
|
||||
action = ReminderReceiver.ACTION_SNOOZE_REMINDER
|
||||
},
|
||||
FLAG_UPDATE_CURRENT
|
||||
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
fun toggleCheckmark(habit: Habit, timestamp: Long?): PendingIntent =
|
||||
@@ -123,26 +143,7 @@ class PendingIntentFactory
|
||||
action = WidgetReceiver.ACTION_TOGGLE_REPETITION
|
||||
if (timestamp != null) putExtra("timestamp", timestamp)
|
||||
},
|
||||
FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
fun setNumericalValue(
|
||||
widgetContext: Context,
|
||||
habit: Habit,
|
||||
numericalValue: Int,
|
||||
timestamp: Long?
|
||||
):
|
||||
PendingIntent =
|
||||
getBroadcast(
|
||||
widgetContext,
|
||||
2,
|
||||
Intent(widgetContext, WidgetReceiver::class.java).apply {
|
||||
data = Uri.parse(habit.uriString)
|
||||
action = WidgetReceiver.ACTION_SET_NUMERICAL_VALUE
|
||||
putExtra("numericalValue", numericalValue)
|
||||
if (timestamp != null) putExtra("timestamp", timestamp)
|
||||
},
|
||||
FLAG_UPDATE_CURRENT
|
||||
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
fun updateWidgets(): PendingIntent =
|
||||
@@ -152,6 +153,58 @@ class PendingIntentFactory
|
||||
Intent(context, WidgetReceiver::class.java).apply {
|
||||
action = WidgetReceiver.ACTION_UPDATE_WIDGETS_VALUE
|
||||
},
|
||||
FLAG_UPDATE_CURRENT
|
||||
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
fun showNumberPicker(habit: Habit, timestamp: Timestamp): PendingIntent? {
|
||||
return getActivity(
|
||||
context,
|
||||
(habit.id!! % Integer.MAX_VALUE).toInt() + 1,
|
||||
Intent(context, ListHabitsActivity::class.java).apply {
|
||||
action = ListHabitsActivity.ACTION_EDIT
|
||||
putExtra("habit", habit.id)
|
||||
putExtra("timestamp", timestamp.unixTime)
|
||||
},
|
||||
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
|
||||
fun showNumberPickerTemplate(): PendingIntent {
|
||||
return getActivity(
|
||||
context,
|
||||
1,
|
||||
Intent(context, ListHabitsActivity::class.java).apply {
|
||||
action = ListHabitsActivity.ACTION_EDIT
|
||||
},
|
||||
getIntentTemplateFlags()
|
||||
)
|
||||
}
|
||||
|
||||
fun showNumberPickerFillIn(habit: Habit, timestamp: Timestamp) = Intent().apply {
|
||||
putExtra("habit", habit.id)
|
||||
putExtra("timestamp", timestamp.unixTime)
|
||||
}
|
||||
|
||||
private fun getIntentTemplateFlags(): Int {
|
||||
var flags = 0
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
flags = flags or FLAG_MUTABLE
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
fun toggleCheckmarkTemplate(): PendingIntent =
|
||||
getBroadcast(
|
||||
context,
|
||||
2,
|
||||
Intent(context, WidgetReceiver::class.java).apply {
|
||||
action = WidgetReceiver.ACTION_TOGGLE_REPETITION
|
||||
},
|
||||
getIntentTemplateFlags()
|
||||
)
|
||||
|
||||
fun toggleCheckmarkFillIn(habit: Habit, timestamp: Timestamp) = Intent().apply {
|
||||
data = Uri.parse(habit.uriString)
|
||||
putExtra("timestamp", timestamp.unixTime)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory.decodeResource
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.util.Log
|
||||
@@ -113,7 +112,7 @@ class AndroidNotificationTray
|
||||
val enterAction = Action(
|
||||
R.drawable.ic_action_check,
|
||||
context.getString(R.string.enter),
|
||||
pendingIntents.setNumericalValue(context, habit, 0, null)
|
||||
pendingIntents.showNumberPicker(habit, timestamp)
|
||||
)
|
||||
|
||||
val wearableBg = decodeResource(context.resources, R.drawable.stripe)
|
||||
@@ -150,16 +149,15 @@ class AndroidNotificationTray
|
||||
if (!disableSound)
|
||||
builder.setSound(ringtoneManager.getURI())
|
||||
|
||||
if (preferences.shouldMakeNotificationsLed())
|
||||
builder.setLights(Color.RED, 1000, 1000)
|
||||
|
||||
val snoozeAction = Action(
|
||||
R.drawable.ic_action_snooze,
|
||||
context.getString(R.string.snooze),
|
||||
pendingIntents.snoozeNotification(habit)
|
||||
)
|
||||
wearableExtender.addAction(snoozeAction)
|
||||
builder.addAction(snoozeAction)
|
||||
if (SDK_INT < Build.VERSION_CODES.S) {
|
||||
val snoozeAction = Action(
|
||||
R.drawable.ic_action_snooze,
|
||||
context.getString(R.string.snooze),
|
||||
pendingIntents.snoozeNotification(habit)
|
||||
)
|
||||
wearableExtender.addAction(snoozeAction)
|
||||
builder.addAction(snoozeAction)
|
||||
}
|
||||
|
||||
builder.extend(wearableExtender)
|
||||
return builder.build()
|
||||
|
||||
@@ -90,8 +90,6 @@ class SharedPreferencesStorage
|
||||
preferences.isMidnightDelayEnabled = getBoolean(key, false)
|
||||
"pref_sticky_notifications" ->
|
||||
preferences.setNotificationsSticky(getBoolean(key, false))
|
||||
"pref_led_notifications" ->
|
||||
preferences.setNotificationsLed(getBoolean(key, false))
|
||||
"pref_unknown_enabled" -> {
|
||||
preferences.areQuestionMarksEnabled = getBoolean(key, false)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import android.content.BroadcastReceiver
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.util.Log
|
||||
import org.isoron.uhabits.HabitsApplication
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
@@ -76,8 +78,21 @@ class ReminderReceiver : BroadcastReceiver() {
|
||||
}
|
||||
ACTION_SNOOZE_REMINDER -> {
|
||||
if (habit == null) return
|
||||
Log.d("ReminderReceiver", String.format("onSnoozePressed habit=%d", habit.id))
|
||||
reminderController.onSnoozePressed(habit, context)
|
||||
if (SDK_INT < Build.VERSION_CODES.S) {
|
||||
Log.d(
|
||||
"ReminderReceiver",
|
||||
String.format("onSnoozePressed habit=%d", habit.id)
|
||||
)
|
||||
reminderController.onSnoozePressed(habit, context)
|
||||
} else {
|
||||
Log.w(
|
||||
"ReminderReceiver",
|
||||
String.format(
|
||||
"onSnoozePressed habit=%d, should be deactivated in recent versions.",
|
||||
habit.id
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
Intent.ACTION_BOOT_COMPLETED -> {
|
||||
Log.d("ReminderReceiver", "onBootCompleted")
|
||||
|
||||
@@ -27,7 +27,6 @@ import org.isoron.uhabits.HabitsApplication
|
||||
import org.isoron.uhabits.core.ui.widgets.WidgetBehavior
|
||||
import org.isoron.uhabits.inject.HabitsApplicationComponent
|
||||
import org.isoron.uhabits.intents.IntentParser.CheckmarkIntentData
|
||||
import org.isoron.uhabits.widgets.activities.NumericalCheckmarkWidgetActivity
|
||||
|
||||
/**
|
||||
* The Android BroadcastReceiver for Loop Habit Tracker.
|
||||
@@ -96,15 +95,6 @@ class WidgetReceiver : BroadcastReceiver() {
|
||||
data.timestamp
|
||||
)
|
||||
}
|
||||
ACTION_SET_NUMERICAL_VALUE -> {
|
||||
context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
|
||||
val numberSelectorIntent = Intent(context, NumericalCheckmarkWidgetActivity::class.java)
|
||||
numberSelectorIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
numberSelectorIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
numberSelectorIntent.action = NumericalCheckmarkWidgetActivity.ACTION_SHOW_NUMERICAL_VALUE_ACTIVITY
|
||||
parser.copyIntentData(intent, numberSelectorIntent)
|
||||
context.startActivity(numberSelectorIntent)
|
||||
}
|
||||
ACTION_UPDATE_WIDGETS_VALUE -> {
|
||||
widgetUpdater.updateWidgets()
|
||||
widgetUpdater.scheduleStartDayWidgetUpdate()
|
||||
@@ -126,7 +116,6 @@ class WidgetReceiver : BroadcastReceiver() {
|
||||
const val ACTION_DISMISS_REMINDER = "org.isoron.uhabits.ACTION_DISMISS_REMINDER"
|
||||
const val ACTION_REMOVE_REPETITION = "org.isoron.uhabits.ACTION_REMOVE_REPETITION"
|
||||
const val ACTION_TOGGLE_REPETITION = "org.isoron.uhabits.ACTION_TOGGLE_REPETITION"
|
||||
const val ACTION_SET_NUMERICAL_VALUE = "org.isoron.uhabits.ACTION_SET_NUMERICAL_VALUE"
|
||||
const val ACTION_UPDATE_WIDGETS_VALUE = "org.isoron.uhabits.ACTION_UPDATE_WIDGETS_VALUE"
|
||||
private const val TAG = "WidgetReceiver"
|
||||
var lastReceivedIntent: Intent? = null
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.isoron.uhabits.utils
|
||||
|
||||
import android.app.Dialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
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() {
|
||||
dismissCurrentDialog()
|
||||
currentDialog = WeakReference(this)
|
||||
show()
|
||||
}
|
||||
|
||||
fun DialogFragment.dismissCurrentAndShow(fragmentManager: FragmentManager, tag: String) {
|
||||
dismissCurrentDialog()
|
||||
currentDialogFragment = WeakReference(this)
|
||||
show(fragmentManager, tag)
|
||||
fragmentManager.executePendingTransactions()
|
||||
}
|
||||
@@ -22,14 +22,13 @@ import android.app.Activity
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.view.WindowManager
|
||||
|
||||
object SystemUtils {
|
||||
val isAndroidOOrLater: Boolean
|
||||
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||
|
||||
fun unlockScreen(activity: Activity) {
|
||||
if (isAndroidOOrLater) {
|
||||
if (SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val km = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||
km.requestDismissKeyguard(activity, null)
|
||||
} else {
|
||||
|
||||
@@ -20,16 +20,22 @@
|
||||
package org.isoron.uhabits.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Handler
|
||||
import android.os.SystemClock
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.view.WindowManager
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.RelativeLayout.ALIGN_PARENT_BOTTOM
|
||||
import android.widget.RelativeLayout.ALIGN_PARENT_TOP
|
||||
@@ -199,5 +205,33 @@ fun View.dim(id: Int) = InterfaceUtils.getDimension(context, id)
|
||||
fun View.sp(value: Float) = InterfaceUtils.spToPixels(context, value)
|
||||
fun View.dp(value: Float) = InterfaceUtils.dpToPixels(context, value)
|
||||
fun View.str(id: Int) = resources.getString(id)
|
||||
|
||||
fun View.drawNotesIndicator(canvas: Canvas, color: Int, size: Float, notes: String) {
|
||||
val pNotesIndicator = Paint()
|
||||
pNotesIndicator.color = color
|
||||
if (notes.isNotBlank()) {
|
||||
val cy = 0.8f * size
|
||||
canvas.drawCircle(width.toFloat() - cy, cy, 8f, pNotesIndicator)
|
||||
}
|
||||
}
|
||||
|
||||
val View.sres: StyledResources
|
||||
get() = StyledResources(context)
|
||||
|
||||
fun Dialog.dimBehind() {
|
||||
window?.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||
window?.setDimAmount(0.5f)
|
||||
}
|
||||
|
||||
fun View.requestFocusWithKeyboard() {
|
||||
// For some reason, Android does not open the soft keyboard by default when view.requestFocus
|
||||
// is called. Several online solutions suggest using InputMethodManager, but these solutions
|
||||
// are not reliable; sometimes the keyboard does not show, and sometimes it does not go away
|
||||
// after focus is lost. Here, we simulate a click on the view, which triggers the keyboard.
|
||||
// Based on: https://stackoverflow.com/a/7699556
|
||||
postDelayed({
|
||||
val time = SystemClock.uptimeMillis()
|
||||
dispatchTouchEvent(MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0f, 0f, 0))
|
||||
dispatchTouchEvent(MotionEvent.obtain(time, time, MotionEvent.ACTION_UP, 0f, 0f, 0))
|
||||
}, 250)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ open class CheckmarkWidget(
|
||||
|
||||
override fun getOnClickPendingIntent(context: Context): PendingIntent? {
|
||||
return if (habit.isNumerical) {
|
||||
pendingIntentFactory.setNumericalValue(context, habit, 10, null)
|
||||
pendingIntentFactory.showNumberPicker(habit, DateUtils.getTodayWithOffset())
|
||||
} else {
|
||||
pendingIntentFactory.toggleCheckmark(habit, null)
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ class FrequencyWidget(
|
||||
(widgetView.dataView as FrequencyChart).apply {
|
||||
setFirstWeekday(firstWeekday)
|
||||
setColor(WidgetTheme().color(habit.color).toInt())
|
||||
setIsNumerical(habit.isNumerical)
|
||||
setFrequency(habit.originalEntries.computeWeekdayFrequency(habit.isNumerical))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,10 @@ class HistoryWidget(
|
||||
theme = WidgetTheme(),
|
||||
)
|
||||
(widgetView.dataView as AndroidDataView).apply {
|
||||
(this.view as HistoryChart).series = model.series
|
||||
val historyChart = (this.view as HistoryChart)
|
||||
historyChart.series = model.series
|
||||
historyChart.defaultSquare = model.defaultSquare
|
||||
historyChart.notesIndicators = model.notesIndicators
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +74,8 @@ class HistoryWidget(
|
||||
dateFormatter = JavaLocalDateFormatter(Locale.getDefault()),
|
||||
firstWeekday = prefs.firstWeekday,
|
||||
series = listOf(),
|
||||
defaultSquare = HistoryChart.Square.OFF,
|
||||
notesIndicators = listOf(),
|
||||
)
|
||||
}
|
||||
).apply {
|
||||
|
||||
@@ -26,8 +26,8 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import android.widget.RemoteViews
|
||||
import org.isoron.platform.utils.StringUtils
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.utils.StringUtils
|
||||
|
||||
class StackWidget(
|
||||
context: Context,
|
||||
@@ -73,6 +73,10 @@ class StackWidget(
|
||||
StackWidgetType.getStackWidgetAdapterViewId(widgetType),
|
||||
StackWidgetType.getStackWidgetEmptyViewId(widgetType)
|
||||
)
|
||||
remoteViews.setPendingIntentTemplate(
|
||||
StackWidgetType.getStackWidgetAdapterViewId(widgetType),
|
||||
StackWidgetType.getPendingIntentTemplate(pendingIntentFactory, widgetType, habits)
|
||||
)
|
||||
return remoteViews
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,13 +27,16 @@ import android.util.Log
|
||||
import android.widget.RemoteViews
|
||||
import android.widget.RemoteViewsService
|
||||
import android.widget.RemoteViewsService.RemoteViewsFactory
|
||||
import org.isoron.platform.utils.StringUtils.Companion.splitLongs
|
||||
import org.isoron.uhabits.HabitsApplication
|
||||
import org.isoron.uhabits.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.StringUtils.Companion.splitLongs
|
||||
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
|
||||
import java.util.ArrayList
|
||||
|
||||
class StackWidgetService : RemoteViewsService() {
|
||||
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
|
||||
@@ -54,7 +57,6 @@ internal class StackRemoteViewsFactory(private val context: Context, intent: Int
|
||||
)
|
||||
private val habitIds: LongArray
|
||||
private val widgetType: StackWidgetType
|
||||
private var remoteViews = ArrayList<RemoteViews>()
|
||||
override fun onCreate() {}
|
||||
override fun onDestroy() {}
|
||||
override fun getCount(): Int {
|
||||
@@ -85,8 +87,26 @@ internal class StackRemoteViewsFactory(private val context: Context, intent: Int
|
||||
}
|
||||
|
||||
override fun getViewAt(position: Int): RemoteViews? {
|
||||
Log.i("StackRemoteViewsFactory", "getViewAt $position")
|
||||
return if (0 <= position && position < remoteViews.size) remoteViews[position] else null
|
||||
Log.i("StackRemoteViewsFactory", "getViewAt $position started")
|
||||
if (position < 0 || position >= habitIds.size) return null
|
||||
val app = context.applicationContext as HabitsApplication
|
||||
val prefs = app.component.preferences
|
||||
val habitList = app.component.habitList
|
||||
val options = AppWidgetManager.getInstance(context).getAppWidgetOptions(widgetId)
|
||||
if (Looper.myLooper() == null) Looper.prepare()
|
||||
val habits = habitIds.map { habitList.getById(it) ?: throw HabitNotFoundException() }
|
||||
val h = habits[position]
|
||||
val widget = constructWidget(h, prefs)
|
||||
widget.setDimensions(getDimensionsFromOptions(context, options))
|
||||
val landscapeViews = widget.landscapeRemoteViews
|
||||
val portraitViews = widget.portraitRemoteViews
|
||||
val factory = PendingIntentFactory(context, IntentFactory())
|
||||
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)
|
||||
Log.i("StackRemoteViewsFactory", "getViewAt $position ended")
|
||||
return remoteViews
|
||||
}
|
||||
|
||||
private fun constructWidget(
|
||||
@@ -131,24 +151,6 @@ internal class StackRemoteViewsFactory(private val context: Context, intent: Int
|
||||
}
|
||||
|
||||
override fun onDataSetChanged() {
|
||||
Log.i("StackRemoteViewsFactory", "onDataSetChanged started")
|
||||
val app = context.applicationContext as HabitsApplication
|
||||
val prefs = app.component.preferences
|
||||
val habitList = app.component.habitList
|
||||
val options = AppWidgetManager.getInstance(context).getAppWidgetOptions(widgetId)
|
||||
val newRemoteViews = ArrayList<RemoteViews>()
|
||||
if (Looper.myLooper() == null) Looper.prepare()
|
||||
for (id in habitIds) {
|
||||
val h = habitList.getById(id) ?: throw HabitNotFoundException()
|
||||
val widget = constructWidget(h, prefs)
|
||||
widget.setDimensions(getDimensionsFromOptions(context, options))
|
||||
val landscapeViews = widget.landscapeRemoteViews
|
||||
val portraitViews = widget.portraitRemoteViews
|
||||
newRemoteViews.add(RemoteViews(landscapeViews, portraitViews))
|
||||
Log.i("StackRemoteViewsFactory", "onDataSetChanged constructed widget $id")
|
||||
}
|
||||
remoteViews = newRemoteViews
|
||||
Log.i("StackRemoteViewsFactory", "onDataSetChanged ended")
|
||||
}
|
||||
|
||||
init {
|
||||
|
||||
@@ -18,7 +18,12 @@
|
||||
*/
|
||||
package org.isoron.uhabits.widgets
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.Timestamp
|
||||
import org.isoron.uhabits.intents.PendingIntentFactory
|
||||
import java.lang.IllegalStateException
|
||||
|
||||
enum class StackWidgetType(val value: Int) {
|
||||
@@ -73,5 +78,39 @@ enum class StackWidgetType(val value: Int) {
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
fun getPendingIntentTemplate(
|
||||
factory: PendingIntentFactory,
|
||||
widgetType: StackWidgetType,
|
||||
habits: List<Habit>
|
||||
): PendingIntent {
|
||||
val containsNumerical = habits.any { it.isNumerical }
|
||||
return when (widgetType) {
|
||||
CHECKMARK -> if (containsNumerical) {
|
||||
factory.showNumberPickerTemplate()
|
||||
} else {
|
||||
factory.toggleCheckmarkTemplate()
|
||||
}
|
||||
FREQUENCY, SCORE, HISTORY, STREAKS, TARGET -> factory.showHabitTemplate()
|
||||
}
|
||||
}
|
||||
|
||||
fun getIntentFillIn(
|
||||
factory: PendingIntentFactory,
|
||||
widgetType: StackWidgetType,
|
||||
habit: Habit,
|
||||
allHabitsInStackWidget: List<Habit>,
|
||||
timestamp: Timestamp
|
||||
): Intent {
|
||||
val containsNumerical = allHabitsInStackWidget.any { it.isNumerical }
|
||||
return when (widgetType) {
|
||||
CHECKMARK -> if (containsNumerical) {
|
||||
factory.showNumberPickerFillIn(habit, timestamp)
|
||||
} else {
|
||||
factory.toggleCheckmarkFillIn(habit, timestamp)
|
||||
}
|
||||
FREQUENCY, SCORE, HISTORY, STREAKS, TARGET -> factory.showHabitFillIn(habit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,89 +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.widgets.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.Window
|
||||
import android.widget.FrameLayout
|
||||
import org.isoron.uhabits.HabitsApplication
|
||||
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
||||
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
|
||||
import org.isoron.uhabits.core.ui.widgets.WidgetBehavior
|
||||
import org.isoron.uhabits.core.utils.DateUtils
|
||||
import org.isoron.uhabits.intents.IntentParser
|
||||
import org.isoron.uhabits.utils.SystemUtils
|
||||
import org.isoron.uhabits.widgets.WidgetUpdater
|
||||
|
||||
class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPickerCallback {
|
||||
|
||||
private lateinit var behavior: WidgetBehavior
|
||||
private lateinit var data: IntentParser.CheckmarkIntentData
|
||||
private lateinit var widgetUpdater: WidgetUpdater
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
setContentView(FrameLayout(this))
|
||||
val app = this.applicationContext as HabitsApplication
|
||||
val component = app.component
|
||||
val parser = app.component.intentParser
|
||||
data = parser.parseCheckmarkIntent(intent)
|
||||
behavior = WidgetBehavior(
|
||||
component.habitList,
|
||||
component.commandRunner,
|
||||
component.notificationTray,
|
||||
component.preferences
|
||||
)
|
||||
widgetUpdater = component.widgetUpdater
|
||||
showNumberSelector(this)
|
||||
|
||||
SystemUtils.unlockScreen(this)
|
||||
}
|
||||
|
||||
override fun onNumberPicked(newValue: Double) {
|
||||
behavior.setValue(data.habit, data.timestamp, (newValue * 1000).toInt())
|
||||
widgetUpdater.updateWidgets()
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onNumberPickerDismissed() {
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun showNumberSelector(context: Context) {
|
||||
val app = this.applicationContext as HabitsApplication
|
||||
AndroidThemeSwitcher(this, app.component.preferences).apply()
|
||||
val numberPickerFactory = NumberPickerFactory(context)
|
||||
val today = DateUtils.getTodayWithOffset()
|
||||
val entry = data.habit.computedEntries.get(today)
|
||||
numberPickerFactory.create(
|
||||
entry.value / 1000.0,
|
||||
data.habit.unit,
|
||||
this
|
||||
).show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_SHOW_NUMERICAL_VALUE_ACTIVITY = "org.isoron.uhabits.ACTION_SHOW_NUMERICAL_VALUE_ACTIVITY"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -126,7 +124,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 +139,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,4 +1,4 @@
|
||||
حلقة تعقب الحبوب تساعدك على إنشاء عادات إيجابية طويلة الأجل والحفاظ عليها في حياتك. تعطيكم الرسوم البيانية والإحصاءات التفصيلية صورة واضحة للكيفية التي تحسنت بها عاداتك مع مرور الوقت. التطبيق خالٍ تمامًا من الإعلانات ومفتوح المصدر ويحترم خصوصيتك.
|
||||
يساعدك Loop Habit Tracker على إنشاء عادات إيجابية طويلة المدى والحفاظ عليها في حياتك. تعطيكم الرسوم البيانية والإحصاءات التفصيلية صورة واضحة للكيفية التي تحسنت بها عاداتك مع مرور الوقت. التطبيق خالٍ تمامًا من الإعلانات ومفتوح المصدر ويحترم خصوصيتك.
|
||||
|
||||
<b>واجهة جميلة وبسيطة</b>
|
||||
يحتوي Loop على واجهة أنيقة وبسيطة وسهلة الاستخدام للغاية ، حتى للمستخدمين لأول مرة. تم تحسين التطبيق ليكون ذي سرعة عالية ، ويعمل التطبيق بشكل جيد حتى على الهواتف القديمة.
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
Loop Habit Tracker vám pomáhá vytvořit a udržet si dlouhodobé pozitivní zvyky. Podrobné grafy a statistiky vám ukáží jasný přehled, jak se vaše zvyky postupem času zlepšily. Aplikace je kompletně bez reklam, open source (otevřený zdrojový kód) a respektuje vaše soukromí.
|
||||
Loop Habit Tracker Vám pomáhá vytvořit a udržet si dlouhodobé pozitivní návyky. Podrobné grafy a statistiky Vám ukáží jasný přehled, jak se Vaše návyky postupem času zlepšily. Aplikace je zcela bez reklam, open-source a respektuje Vaše soukromí.
|
||||
|
||||
<b>Krásné, minimalistické a přehledné rozhraní</b>
|
||||
Loop má elegantní a minimalostické rozhraní, které je velice jednoduché k použití. I pro nové uživatele. Skvěle optimalizováno pro rychlost, aplikace funguje i na starších telefonech.
|
||||
Loop má elegantní a minimalistické rozhraní, které je jednoduché na ovládání i pro nové uživatele. Díky vysoké optimalizaci pro rychlost aplikace funguje skvěle i na starších telefonech.
|
||||
|
||||
<b>Skóre návyků</b>
|
||||
Loop má propracovaný vzorec pro výpočet síly vašich návyků. Každé opakování posílí váš zvyk a každé vynechání ho oslabí. Ale pár zmeškaných dní po dlouhém období vám kompletně nezničí váš pokrok, jako u mnoho jiných aplikací soustředěných na "nepřerušení řetězce".
|
||||
Loop má propracovaný vzorec pro výpočet síly vašich návyků. Každé opakování posílí váš zvyk a každé vynechání ho oslabí. Pár zmeškaných dní po dlouhé výzvě Vám, na rozdíl od jiných aplikací, ve kterých nesmíte vynechat ani jeden den, nezničí Váš pokrok.
|
||||
|
||||
<b>Flexibilní plánování</b>
|
||||
Kromě každodenních návyků podporuje Loop návyky se složitějšími plány, například třikrát týdně nebo každý druhý den.
|
||||
Kromě každodenních návyků podporuje Loop i návyky náročné na plánování, například třikrát týdně nebo každý druhý den.
|
||||
|
||||
<b>Připomenutí</b>
|
||||
Nastavte si oznámení, aby vám připomněly vaše návyky. Každý návyk může mít svou notifikaci ve zvoleném čase. Jednoduše potvrďte nebo zamítněte návyk přímo z notifikace.
|
||||
Nastavte si upozornění, která Vám budou Vaše návyky připomínat. Pro každý návyk může být nastavena připomínka dle Vámi zvoleného času. Jednoduše potvrďte nebo propusťte návyk přímo z upozornění.
|
||||
|
||||
<b>Widgety</b>
|
||||
Nechte si při každém odemknutí telefonu připomenout vaše návyky. Barevné widgety vám dovolují sledovat vaše návyky přimo z vaší domovské obrazovky, aniž byste museli otevřít aplikaci.
|
||||
Nechte si při každém odemknutí telefonu připomenout Vaše návyky. Barevné widgety Vám dovolí sledovat Vaše návyky přímo z Vaší domovské obrazovky, aniž byste museli otevřít aplikaci.
|
||||
|
||||
<b>Převezměte kontrolu nad svými daty</b>
|
||||
Pokud chcete dále analyzovat vaše údaje, nebo je přesunout do jiné služby, Loop umožňuje je exportovat do tabulek (CSV) nebo do databázového souboru (SQLite). Pokročilí uživatelé můžou propojit ovládání i skrze jiné aplikace, jako je Tasker.
|
||||
Pokud chcete dále analyzovat Vaše data, nebo je přesunout do jiné služby, můžete je díky Loop exportovat do tabulek (CSV) nebo do databázového souboru (SQLite). Pokročilí uživatelé mohou propojit ovládání i skrze jiné aplikace, jako je Tasker.
|
||||
|
||||
<b>Bez omezení</b>
|
||||
Sledujte si tolik návyků, kolik chcete. Loop nenastavuje žádné umělé omezení počtu návyků, které můžete sledovat. Všechny funkce jsou k dispozici všem uživatelům. Neexistují žádné nákupy v aplikaci.
|
||||
Sledujte tolik návyků, kolik chcete. Loop nenastavuje žádné umělé omezení počtu návyků, které můžete sledovat. Všechny funkce jsou k dispozici všem uživatelům. V aplikaci nic nenakupujete.
|
||||
|
||||
<b>Zcela bez reklam a open source</b>
|
||||
V této aplikaci nejsou žádné reklamy, nepříjemná oznámení nebo dotěrná oprávnění a nikdy zde nebudou. Aplikace je zcela open-source (GPLv3).
|
||||
V této aplikaci nejsou žádné reklamy, nepříjemná oznámení nebo dotěrná oprávnění, a ani nikdy nebudou. Aplikace je zcela open-source (GPLv3).
|
||||
|
||||
<b>Funguje offline a respektuje vaše soukromí</b>
|
||||
Loop nevyžaduje připojení k Internetu ani registraci online účtu. Vaše důvěrné údaje nejsou nikdy nikomu zaslány. Nemají k nim přístup ani vývojáři, nebo třetí strany.
|
||||
<b>Funguje off-line a respektuje vaše soukromí</b>
|
||||
Loop nevyžaduje připojení k internetu ani online registraci účtu. Vaše důvěrná data nikdy nikomu neodesíláme. Nemají k nim přístup ani vývojáři, nebo třetí strany.
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
Loop Habit Tracker
|
||||
عادتسنج لوپ
|
||||
|
||||
@@ -22,7 +22,7 @@ Si vous souhaitez analyser davantage vos données ou les déplacer vers un autre
|
||||
Suivez autant d'habitudes que vous le souhaitez. Loop n'impose aucune limite artificielle sur le nombre d'habitudes que vous pouvez avoir. Toutes les fonctionnalités sont disponibles pour tous les utilisateurs. Il n'y a aucun achat dans l'application.
|
||||
|
||||
<b>Entièrement sans publicité et open source</b>
|
||||
Il n'y a pas de publicités, de notifications agaçantes ou d'autorisations intrusives dans cette application. Il n'y en aura jamais. L'application est entièrement open-source (GPLv3).
|
||||
Il n'y a pas de publicités, de notifications agaçantes ou d'autorisations intrusives dans cette application. L'application est entièrement open-source (GPLv3).
|
||||
|
||||
<b>Fonctionne hors ligne et respecte votre vie privée</b>
|
||||
Loop ne nécessite pas de connexion Internet ni d'enregistrement d'un compte en ligne. Vos données confidentielles ne sont jamais envoyées à personne. Ni les développeurs ni aucun tiers n'y ont accès.
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
„Loop למעקב אחר ההרגלים” מסייע לך ביצירה ובשימור הרגלים טובים וארוכי טווח. תרשימים וסטטיסטיקה מפורטים נותנים לך תמונה ברורה כיצד ההרגלים שלך השתפרו לאורך זמן. היישום נטול פרסומות לחלוטין, קוד המקור שלו פתוח והוא מכבד את הפרטיות שלך.
|
||||
יישום ”Loop לניהול הרגלים“ מסייע לך ביצירה ובשימור הרגלים טובים וארוכי טווח. תרשימים וסטטיסטיקה מפורטים נותנים לך תמונה ברורה כיצד ההרגלים שלך השתפרו לאורך זמן. היישום נטול פרסומות לחלוטין, קוד המקור שלו פתוח והוא מכבד את הפרטיות שלך.
|
||||
|
||||
<b>ממשק יפה, חסכוני וקליל</b>
|
||||
ל־Loop ממשק אלגנטי, חסכוני וקל לשימוש אפילו בפעם הראשונה. היישום מותאם במיוחד למהירות, ועובד היטב גם בטלפונים ישנים.
|
||||
הממשק של Loop אלגנטי, חסכוני וקל לשימוש אפילו בפעם הראשונה. היישום מותאם במיוחד למהירות, ועובד היטב גם בטלפונים ישנים.
|
||||
|
||||
<b>ציון הרגל</b>
|
||||
ל־Loop יש נוסחה מתקדמת לחישוב חוזק ההרגלים שלך. כל חזרה על ההרגל מחזקת אותו, וכל יום שהוחמץ מחליש אותו. כמה ימים שהוחמצו לאחר רצף ארוך, לעומת זאת, לא יהרסו לחלוטין את ההתקדמות, בניגוד ליישומים רבים אחרים מסוג „לא לשבור את השרשרת”.
|
||||
ל־Loop יש נוסחה מתקדמת לחישוב חוזק ההרגלים שלך. כל חזרה על ההרגל מחזקת אותו, וכל יום שהוחמץ מחליש אותו. כמה ימים שהוחמצו לאחר רצף ארוך, לעומת זאת, לא יהרסו לחלוטין את ההתקדמות, בניגוד ליישומים רבים אחרים מסוג ”לא לשבור את השרשרת“.
|
||||
|
||||
<b>לוחות זמנים גמישים</b>
|
||||
בנוסף להרגלים יומיומיים, Loop תומך בהרגלים עם לוחות זמנים מורכבים יותר, כמו 3 פעמים בשבוע או „יום כן יום לא”.
|
||||
בנוסף להרגלים יומיומיים, Loop תומך בהרגלים עם לוחות זמנים מורכבים יותר, כמו 3 פעמים בשבוע או ”יום כן ויום לא“.
|
||||
|
||||
<b>תזכורות</b>
|
||||
אפשר לתזמן התראות קבועות עם תזכורות לגבי ההרגלים שלך. לכל הרגל יכולה להיות תזכורת משלו, בשעה שנבחרה ביום. אפשר לסמן או לדחות את עשיית ההרגל ישירות מההתראה בקלות.
|
||||
|
||||
@@ -1 +1 @@
|
||||
Loop Habit Tracker
|
||||
Loop לניהול הרגלים
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Aplikacja "Śledzenie nawyków Loop" pomaga tworzyć i utrzymywać długotrwałe pozytywne nawyki w twoim życiu. Szczegółowe wykresy i statystyki dają jasny obraz tego, jak Twoje nawyki uległy poprawie z upływem czasu. Aplikacja jest całkowicie wolna od reklam, otwartoźródłowa i szanuje Twoją prywatność.
|
||||
Loop Habit Tracker to aplikacja która pomaga tworzyć i podtrzymać długoterminowe pozytywne nawyki w Twoim życiu. Szczegółowe wykresy i statystyki dają jasny obraz tego, jak Twoje nawyki uległy poprawie z upływem czasu. Aplikacja jest całkowicie wolna od reklam, otwartoźródłowa i szanuje Twoją prywatność.
|
||||
|
||||
<b>Piękny, minimalistyczny i lekki interfejs</b>
|
||||
Aplikacja "Śledzenie nawyków Loop" ma elegancki i minimalistyczny interfejs, który jest bardzo łatwy w obsłudze, nawet dla początkujących użytkowników. Jest ona wysoce zoptymalizowana pod kątem szybkości działania nawet na starszych telefonach.
|
||||
|
||||
@@ -1 +1 @@
|
||||
Twórz dobre nawyki i śledź ich postępy w czasie (bez reklam)
|
||||
Twórz dobre nawyki i śledź postępy (bez reklam)
|
||||
|
||||