Compare commits
5 Commits
feature/sy
...
sdk_30_31
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1ad28820f | ||
|
|
edaed20a98 | ||
|
|
68daf6587b | ||
|
|
b7606f33d2 | ||
|
|
4350ff0cd7 |
59
.github/workflows/main.yml
vendored
@@ -7,24 +7,59 @@ on:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
jobs:
|
||||
Test:
|
||||
runs-on: self-hosted
|
||||
Build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out source code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Build project
|
||||
- name: Install Java Development Kit 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
|
||||
- name: Build Project
|
||||
run: ./build.sh build
|
||||
|
||||
- name: Run Android tests
|
||||
run: ./build.sh android-tests-parallel 28 29 30 31 32 33
|
||||
|
||||
- name: Upload artifacts
|
||||
if: always()
|
||||
- name: Upload Build Artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: build
|
||||
path: |
|
||||
build/*log
|
||||
uhabits-android/build/outputs
|
||||
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 }}
|
||||
|
||||
|
||||
5
.gitignore
vendored
@@ -12,8 +12,13 @@
|
||||
.idea
|
||||
.secret
|
||||
build
|
||||
build/
|
||||
captures
|
||||
local.properties
|
||||
node_modules
|
||||
*xcuserdata*
|
||||
*.sketch
|
||||
/design
|
||||
/releases
|
||||
/screenshots
|
||||
crowdin.yml
|
||||
|
||||
16
.secret/decrypt.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
cd "$(dirname "$0")"
|
||||
if [ -z "$GPG_PASSWORD" ]; then
|
||||
echo Env variable GPG_PASSWORD must be defined
|
||||
exit 1
|
||||
fi
|
||||
gpg \
|
||||
--quiet \
|
||||
--batch \
|
||||
--yes \
|
||||
--decrypt \
|
||||
--passphrase="$GPG_PASSWORD" \
|
||||
--output secret.tar.gz \
|
||||
secret
|
||||
tar -xzf secret.tar.gz
|
||||
rm secret.tar.gz
|
||||
BIN
.secret/secret
Normal file
37
CHANGELOG.md
@@ -1,42 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [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)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
plugins {
|
||||
val kotlinVersion = "1.7.10"
|
||||
id("com.android.application") version ("7.3.0-rc01") apply (false)
|
||||
val kotlinVersion = "1.5.0"
|
||||
id("com.android.application") version ("7.0.3") apply (false)
|
||||
id("org.jetbrains.kotlin.android") version kotlinVersion apply (false)
|
||||
id("org.jetbrains.kotlin.kapt") version kotlinVersion apply (false)
|
||||
id("org.jetbrains.kotlin.android.extensions") version kotlinVersion apply (false)
|
||||
id("org.jetbrains.kotlin.multiplatform") version kotlinVersion apply (false)
|
||||
id("org.jlleitschuh.gradle.ktlint") version "11.0.0"
|
||||
id("org.jlleitschuh.gradle.ktlint") version "10.2.0"
|
||||
}
|
||||
|
||||
apply {
|
||||
|
||||
130
build.sh
@@ -26,7 +26,6 @@ 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"
|
||||
@@ -70,7 +69,8 @@ core_build() {
|
||||
# Android
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
android_setup() {
|
||||
# shellcheck disable=SC2016
|
||||
android_test() {
|
||||
API=$1
|
||||
AVDNAME=${AVD_PREFIX}${API}
|
||||
|
||||
@@ -85,63 +85,25 @@ android_setup() {
|
||||
$AVDMANAGER delete avd --name $AVDNAME
|
||||
|
||||
log_info "Creating new Android virtual device (API $API)..."
|
||||
(echo "y" | $SDKMANAGER --install "system-images;android-$API;google_apis;x86_64") || return 1
|
||||
(echo "y" | $SDKMANAGER --install "system-images;android-$API;default;x86_64") || return 1
|
||||
$AVDMANAGER create avd \
|
||||
--name $AVDNAME \
|
||||
--package "system-images;android-$API;google_apis;x86_64" \
|
||||
--package "system-images;android-$API;default;x86_64" \
|
||||
--device "Nexus 4" || return 1
|
||||
|
||||
flock -u 10
|
||||
) 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"
|
||||
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 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
|
||||
|
||||
log_info "Disabling animations..."
|
||||
$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
|
||||
@@ -149,24 +111,6 @@ android_boot_attempt() {
|
||||
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
|
||||
@@ -178,25 +122,14 @@ 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
|
||||
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
|
||||
$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
|
||||
log_error "Some $size instrumented tests failed."
|
||||
log_error "Saving logcat: $OUT_LOGCAT..."
|
||||
$ADB logcat -d > $OUT_LOGCAT
|
||||
@@ -205,14 +138,13 @@ 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
|
||||
@@ -220,27 +152,12 @@ android_test_parallel() {
|
||||
if android_test $API 1>$LOG 2>&1; then
|
||||
log_info "API $API: Passed"
|
||||
else
|
||||
log_error "API $API: Failed"
|
||||
log_error "API $API: Failed. See $LOG for more details."
|
||||
fi
|
||||
pkill -9 -f ${AVD_PREFIX}${API}
|
||||
)&
|
||||
PIDS+=" $!"
|
||||
done
|
||||
|
||||
# Check exit codes
|
||||
RET_CODE=0
|
||||
for pid in $PIDS; do
|
||||
wait $pid || RET_CODE=1
|
||||
done
|
||||
|
||||
# Print all logs
|
||||
for API in $*; do
|
||||
echo "::group::Android Tests (API $API)"
|
||||
cat build/android-test-$API.log
|
||||
echo "::endgroup::"
|
||||
done
|
||||
|
||||
return $RET_CODE
|
||||
wait
|
||||
}
|
||||
|
||||
android_build() {
|
||||
@@ -312,14 +229,12 @@ 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
|
||||
@@ -355,17 +270,18 @@ 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
|
||||
android_test $1
|
||||
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-tests-parallel)
|
||||
shift; _parse_opts "$@"
|
||||
|
||||
@@ -33,7 +33,7 @@ The repository will be downloaded to the directory `uhabits`.
|
||||
2. When the IDE asks you for the project location, select `uhabits` and click "Ok".
|
||||
3. Android Studio will spend some time indexing the project. When this is complete, click the toolbar icon "Sync Project with Gradle File", located near the right corner of the top toolbar.
|
||||
4. The operation will likely fail several times due to missing Android SDK components. Each time it fails, click the link "Install missing platforms", "Install build tools", etc, and try again.
|
||||
5. To test the application, create a virtual Android device using the menu "Tools" and "AVD Manager". The default options should work fine, but feel free to customize the device.
|
||||
5. To test the application, create a virtual Android device using the menu "Tools" and "AVD Manager". The default options should work fine, but free to customize the device.
|
||||
6. Click the menu "Run" and "uhabits-android". The application should launch.
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
org.gradle.parallel=false
|
||||
org.gradle.daemon=true
|
||||
org.gradle.jvmargs=-Xms2048m -Xmx2048m
|
||||
org.gradle.jvmargs=-Xms2048m -Xmx2048m -XX:MaxPermSize=2048m
|
||||
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.5-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
2
landing/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
out/
|
||||
.sass-cache
|
||||
27
landing/Makefile
Normal file
@@ -0,0 +1,27 @@
|
||||
haml := src/*.haml
|
||||
sass := src/*.sass
|
||||
|
||||
html := $(patsubst src/%, out/%, $(patsubst %.haml,%.html,$(wildcard $(haml))))
|
||||
css := $(patsubst src/%, out/%, $(patsubst %.sass,%.css,$(wildcard $(sass))))
|
||||
src := $(wildcard src/**)
|
||||
|
||||
compile: $(html) $(css)
|
||||
@rsync -rupE assets/ out/
|
||||
|
||||
out/%.css: src/%.sass $(src)
|
||||
@echo ' sass $<'
|
||||
@mkdir -p `dirname $@`
|
||||
@sass $< $@
|
||||
|
||||
out/%.html: src/%.haml $(src)
|
||||
@echo ' haml $<'
|
||||
@mkdir -p `dirname $@`
|
||||
@haml -E UTF-8 $< $@
|
||||
|
||||
push:
|
||||
rsync -avP out/ axavier.org:/www/loophabits.org/
|
||||
|
||||
clean:
|
||||
@rm -rfv out
|
||||
@rm -rfv tmp
|
||||
|
||||
27
landing/README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
Loop Habit Tracker Landing Page
|
||||
===============================
|
||||
|
||||
This folder contains the source code that generates the project landing page, currently hosted at https://loophabits.org/
|
||||
|
||||
Pull requests with ideas for improving it are very welcome.
|
||||
|
||||
Build instructions
|
||||
------------------
|
||||
|
||||
1. Install `haml`:
|
||||
```bash
|
||||
sudo apt install ruby-haml
|
||||
```
|
||||
2. Install `pandoc-ruby`:
|
||||
```bash
|
||||
gem install pandoc-ruby
|
||||
```
|
||||
3. Run `Makefile`
|
||||
```bash
|
||||
make
|
||||
```
|
||||
4. View the results (using, for example, [npm serve](https://www.npmjs.com/package/serve))
|
||||
```bash
|
||||
npm serve out/
|
||||
```
|
||||
|
||||
1
landing/assets/faq.html
Normal file
@@ -0,0 +1 @@
|
||||
<meta http-equiv="Refresh" content="0; url='https://github.com/iSoron/uhabits/discussions/689'" />
|
||||
BIN
landing/assets/images/f-droid.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
landing/assets/images/google-play.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
landing/assets/images/hero-background-filter.jpg
Normal file
|
After Width: | Height: | Size: 458 KiB |
7
landing/assets/lib/css/bootstrap.min.css
vendored
Normal file
1
landing/assets/lib/css/bootstrap.min.css.map
Normal file
7
landing/assets/lib/js/bootstrap.bundle.min.js
vendored
Normal file
1
landing/assets/lib/js/bootstrap.bundle.min.js.map
Normal file
2
landing/assets/lib/js/jquery.min.js
vendored
Normal file
BIN
landing/assets/screenshots/uhabits1.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
landing/assets/screenshots/uhabits1_th.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
landing/assets/screenshots/uhabits2.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
landing/assets/screenshots/uhabits2_th.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
landing/assets/screenshots/uhabits3.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
landing/assets/screenshots/uhabits3_th.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
landing/assets/screenshots/uhabits4.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
landing/assets/screenshots/uhabits4_th.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
landing/assets/screenshots/uhabits5.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
landing/assets/screenshots/uhabits5_th.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
landing/assets/screenshots/uhabits6.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
landing/assets/screenshots/uhabits6_th.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
106
landing/src/index.haml
Normal file
@@ -0,0 +1,106 @@
|
||||
!!! 5
|
||||
%html
|
||||
%head
|
||||
%meta(charset="UTF-8")
|
||||
%link(href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet" type="text/css")
|
||||
%meta(name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no")
|
||||
%title Loop Habit Tracker
|
||||
%link(rel="stylesheet" type="text/css" href="lib/css/bootstrap.min.css")
|
||||
%link(rel="stylesheet" type="text/css" href="index.css")
|
||||
|
||||
%body
|
||||
.navbar.navbar-expand-md.navbar-light.bg-light
|
||||
%a.navbar-brand(href="/")
|
||||
%b Loop
|
||||
Habit Tracker
|
||||
%button.navbar-toggler(type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation")
|
||||
%span.navbar-toggler-icon
|
||||
#navbar.collapse.navbar-collapse
|
||||
%ul.navbar-nav.mr-auto.mt-2.mt-lg-0
|
||||
%li.nav-item
|
||||
%a.nav-link(href="faq.html") FAQ
|
||||
%li.nav-item
|
||||
%a.nav-link(href="privacy.html") Privacy
|
||||
%li.nav-item
|
||||
%a.nav-link(href="https://source.loophabits.org") Source Code
|
||||
%li.nav-item
|
||||
%a.nav-link(href="https://translate.loophabits.org") Translate
|
||||
|
||||
.jumbotron.jumbotron-fluid
|
||||
.site-wrapper
|
||||
.container
|
||||
.row.vertical-align
|
||||
.col-md
|
||||
%h1.display-4
|
||||
Get your life on track
|
||||
%p.lead
|
||||
With daily reminders, beautiful charts and insightful statistics,
|
||||
Loop Habit Tracker™ helps you create and maintain great habits. Completely free and open-source.
|
||||
|
||||
.store-badges
|
||||
%a(href="https://play.google.com/store/apps/details?id=org.isoron.uhabits")
|
||||
%img(src="images/google-play.png")
|
||||
%a(href="https://f-droid.org/en/packages/org.isoron.uhabits/")
|
||||
%img(src="images/f-droid.png")
|
||||
.col-md
|
||||
.s2
|
||||
%img.screenshot(src="screenshots/uhabits1.png")
|
||||
.s1
|
||||
%img.screenshot(src="screenshots/uhabits4.png")
|
||||
|
||||
.section.screenshots
|
||||
%span
|
||||
%a(href="screenshots/uhabits1.png")
|
||||
%img(src="screenshots/uhabits1_th.png")
|
||||
%a(href="screenshots/uhabits2.png")
|
||||
%img(src="screenshots/uhabits2_th.png")
|
||||
%a(href="screenshots/uhabits3.png")
|
||||
%img(src="screenshots/uhabits3_th.png")
|
||||
%span
|
||||
%a(href="screenshots/uhabits4.png")
|
||||
%img(src="screenshots/uhabits4_th.png")
|
||||
%a(href="screenshots/uhabits5.png")
|
||||
%img(src="screenshots/uhabits5_th.png")
|
||||
|
||||
.section
|
||||
.feature-header
|
||||
%h1
|
||||
Features
|
||||
.container
|
||||
.row
|
||||
.col-md
|
||||
%ul
|
||||
%li
|
||||
%h3 Habit score
|
||||
Loop has an advanced formula for calculating the strength of your habits. Every repetition makes your habit stronger and every missed day makes it weaker. A few missed days after a long streak, however, will not completely destroy your progress, unlike many other don't-break-the-chain apps.
|
||||
|
||||
%li
|
||||
%h3 Flexible schedules
|
||||
In addition to daily habits, Loop supports habits with more complex schedules, such as 3 times per week or every other day.
|
||||
%li
|
||||
%h3 Reminders
|
||||
Schedule notifications to remind you of your habits. Each habit can have its own reminder, at a chosen time of the day. Easily check or dismiss your habit directly from the notification.
|
||||
%li
|
||||
%h3 Widgets
|
||||
Be reminded of your habits whenever you unlock your phone. Colorful widgets allow you to track your habits directly from your home screen, without even opening the app.
|
||||
.col-md
|
||||
%ul
|
||||
%li
|
||||
%h3 Take control of your data
|
||||
If you want to further analyze your data, or move it to another service, Loop allows you to export it to spreadsheets (CSV) or to a database file (SQLite). For power users, check marks can be added through task automation apps such as Tasker.
|
||||
%li
|
||||
%h3 No limitations
|
||||
Track as many habits as you wish. Loop imposes no artificial limits on how many habits you can have. All features are available to all users, and there are no in-app purchases.
|
||||
%li
|
||||
%h3 Completely ad-free and open source
|
||||
There are no advertisements, annoying notifications or intrusive permissions in this app, and there will never be. The app is completely open-source (GPLv3).
|
||||
%li
|
||||
%h3 Works offline and respects your privacy
|
||||
Loop doesn't require an Internet connection or online account registration. Your confidential data is never sent to anyone. Neither the developers nor any third-parties have access to it.
|
||||
|
||||
.section.footer
|
||||
Copyright © 2016–2020, Alinson Santos Xavier. All Rights Reserved.
|
||||
|
||||
|
||||
%script(type="text/javascript" src="lib/js/jquery.min.js")
|
||||
%script(type="text/javascript" src="lib/js/bootstrap.bundle.min.js")
|
||||
104
landing/src/index.sass
Normal file
@@ -0,0 +1,104 @@
|
||||
html, body
|
||||
max-width: 100%
|
||||
overflow-x: hidden
|
||||
|
||||
body
|
||||
font-family: 'Open Sans', sans-serif
|
||||
padding-bottom: 0
|
||||
|
||||
a, a:hover
|
||||
text-decoration: none
|
||||
|
||||
.navbar
|
||||
box-shadow: rgba(0,0,0,0.4) 0px 0px 20px
|
||||
background-color: white !important
|
||||
|
||||
.nav-link
|
||||
margin: 0px 18px
|
||||
|
||||
.section
|
||||
background-color: transparent
|
||||
padding: 18px 0px
|
||||
|
||||
.container
|
||||
ul
|
||||
list-style-type: none
|
||||
h3
|
||||
font-size: 16px
|
||||
font-weight: bold
|
||||
margin: 18px 0px 0px 0px
|
||||
|
||||
.screenshots
|
||||
text-align: center
|
||||
background-color: #222
|
||||
img
|
||||
margin: 0.5%
|
||||
border-radius: 10px
|
||||
border: 3px solid #fff2
|
||||
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.5)
|
||||
max-width: 17%
|
||||
|
||||
.footer
|
||||
color: #888
|
||||
background-color: #222
|
||||
text-align: center
|
||||
font-size: 12px
|
||||
|
||||
.jumbotron
|
||||
background: linear-gradient(rgba(0,30,200,0.8),rgba(90,30,150,0.5)), url("images/hero-background-filter.jpg")
|
||||
box-shadow: rgba(0,0,0,0.5) 0px 0px 20px
|
||||
margin: 0
|
||||
h1
|
||||
max-width: 25rem
|
||||
font-weight: bold
|
||||
color: white
|
||||
p
|
||||
max-width: 40rem
|
||||
color: white
|
||||
.screenshot
|
||||
box-shadow: rgba(0, 0, 0, 0.5) 5px 5px 20px
|
||||
padding: 0px 0px 0px 0px
|
||||
border-radius: 10px
|
||||
border: 2px solid rgba(255, 255, 255, 0.2)
|
||||
background-color: transparent
|
||||
max-width: 300px
|
||||
.store-badges
|
||||
margin: 2rem 1rem
|
||||
img
|
||||
opacity: 0.8
|
||||
height: 75px
|
||||
img:hover
|
||||
opacity: 1.0
|
||||
.s1
|
||||
padding-bottom: 50px
|
||||
padding-left: 50px
|
||||
.s2
|
||||
position: absolute
|
||||
top: 50px
|
||||
left: 175px
|
||||
|
||||
.feature-header
|
||||
text-align: center
|
||||
font-weight: bold
|
||||
padding: 18px
|
||||
|
||||
.align-right
|
||||
text-align: right
|
||||
|
||||
.vertical-align
|
||||
display: flex
|
||||
align-items: center
|
||||
|
||||
.content
|
||||
max-width: 800px
|
||||
margin: 18px auto
|
||||
padding: 0px 18px
|
||||
//padding-left: 120px
|
||||
h2, h3, h4
|
||||
margin: 27px 0px 9px 0px
|
||||
h2, h3
|
||||
//margin-left: -120px
|
||||
h4
|
||||
//margin-left: -60px
|
||||
font-size: 16px
|
||||
font-weight: bold
|
||||
32
landing/src/privacy.haml
Normal file
@@ -0,0 +1,32 @@
|
||||
!!! 5
|
||||
%html
|
||||
%head
|
||||
%meta(charset="UTF-8")
|
||||
%link(href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet" type="text/css")
|
||||
%meta(name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no")
|
||||
%title Privacy | Loop Habit Tracker
|
||||
%link(rel="stylesheet" type="text/css" href="lib/css/bootstrap.min.css")
|
||||
%link(rel="stylesheet" type="text/css" href="index.css")
|
||||
|
||||
%body
|
||||
.navbar.navbar-expand-md.navbar-light.bg-light
|
||||
%a.navbar-brand(href="/")
|
||||
%b Loop
|
||||
Habit Tracker
|
||||
%button.navbar-toggler(type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation")
|
||||
%span.navbar-toggler-icon
|
||||
#navbar.collapse.navbar-collapse
|
||||
%ul.navbar-nav.mr-auto.mt-2.mt-lg-0
|
||||
%li.nav-item
|
||||
%a.nav-link(href="faq.html") FAQ
|
||||
%li.nav-item
|
||||
%a.nav-link(href="privacy.html") Privacy
|
||||
%li.nav-item
|
||||
%a.nav-link(href="https://source.loophabits.org") Source Code
|
||||
%li.nav-item
|
||||
%a.nav-link(href="https://translate.loophabits.org") Translate
|
||||
|
||||
%body
|
||||
.content
|
||||
:markdown
|
||||
#{File.open("src/privacy.md").read}
|
||||
14
landing/src/privacy.md
Normal file
@@ -0,0 +1,14 @@
|
||||
## Privacy Policy
|
||||
|
||||
- All data provided to Loop Habit Tracker is only stored locally in your
|
||||
device. Loop Habit Tracker does not upload your data anywhere. The
|
||||
developers of Loop Habit Tracker do not have access to your data.
|
||||
|
||||
- Your data is not shared with any 3rd parties. Loop Habit Tracker does not
|
||||
include any advertisement libraries or any 3rd party tracking (analytics)
|
||||
code, such as Google Analytics or Facebook SDK.
|
||||
|
||||
- If you have activated "backup & reset" in your phone settings (Settings /
|
||||
Backup & Reset / Back up my data), you should be aware that Android itself
|
||||
will periodically save a copy of your phone's data in Google's servers. The
|
||||
developers of Loop Habit Tracker do not have access to this data.
|
||||
@@ -1,67 +0,0 @@
|
||||
#!/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,415 +1,377 @@
|
||||
Name,Languages,"Translated (Words)","Target Words","Approved (Words)",Voted,"""+"" votes received","""-"" votes received","Winning (Words)",Joined
|
||||
"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"
|
||||
"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"
|
||||
"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,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"
|
||||
"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"
|
||||
KMakoto,"Chinese Traditional",745,1146,949,0,0,0,745,"2019-10-22 04:19:52"
|
||||
"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"
|
||||
"Evren (evrenkiymaz)",Turkish,688,604,0,71,5,1,0,"2020-10-04 03:39:16"
|
||||
"David Nos (david.nos)","Catalan; Spanish",667,731,0,0,1,0,0,"2020-01-04 10:15:36"
|
||||
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"
|
||||
"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"
|
||||
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"
|
||||
"Saeed Esmaili (saaeed.es20)",Persian,586,795,0,5,4,0,0,"2020-11-26 15:41:15"
|
||||
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"
|
||||
fabian.bouchal,German,548,527,0,6,0,3,72,"2020-01-07 06:43:37"
|
||||
"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"
|
||||
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"
|
||||
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"
|
||||
"黄克 (hk13127)","Chinese Simplified",461,765,0,1,0,0,24,"2020-01-17 23:16:03"
|
||||
"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"
|
||||
"黄克 (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"
|
||||
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,239,"2020-01-09 19:35:48"
|
||||
"Thamara Andrade (tkcandrade)","Portuguese, Brazilian",380,387,0,0,1,0,252,"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,1,0,0,"2020-05-26 19:47:58"
|
||||
Blinkin,Dutch,297,334,0,5,0,0,0,"2021-06-14 10:30:05"
|
||||
"Michael Malak (MichaelKMalak)",Arabic,304,271,0,0,0,0,0,"2020-05-26 19:47:58"
|
||||
"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,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"
|
||||
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"
|
||||
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,4,0,0,"2020-06-12 12:52:10"
|
||||
easyrepro,Telugu,214,297,0,0,0,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"
|
||||
"Andrij Mizyk (andmizyk)",Ukrainian,204,178,0,40,0,0,53,"2021-04-01 03:56:20"
|
||||
axmed99,Ukrainian,203,177,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"
|
||||
_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"
|
||||
Ishmaeel,Turkish,193,174,0,129,6,0,0,"2017-10-04 03:54:00"
|
||||
"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"
|
||||
plitwin,Polish,168,151,0,2,31,0,49,"2021-01-20 06:18:37"
|
||||
bruhwut,Vietnamese,171,268,0,1,0,0,0,"2021-05-21 07:16:30"
|
||||
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"
|
||||
"Ravi Rami (ramiravi)",Hindi,151,248,0,0,0,0,0,"2021-10-10 09:19:40"
|
||||
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"
|
||||
"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,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"
|
||||
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"
|
||||
REMOVED_USER,Arabic,111,106,0,22,22,2,0,"2018-01-05 07:01:45"
|
||||
"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"
|
||||
"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,1,0,0,"2021-03-30 05:25:33"
|
||||
"손유정 (yuwon1213)",Korean,95,57,0,0,0,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"
|
||||
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"
|
||||
ikkaz,Indonesian,89,84,0,5,0,0,4,"2019-09-02 19:58:54"
|
||||
"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,13,"2020-07-04 03:42:09"
|
||||
"Ohad Edri (ohadalte)",Hebrew,85,79,0,0,1,3,18,"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,1,0,0,"2020-01-03 11:36:40"
|
||||
"Jacob Roller (jdr28070)",Korean,79,61,0,0,0,0,0,"2020-01-03 11:36:40"
|
||||
Tiralka,French,79,91,0,92,1,0,0,"2018-02-09 18:39:01"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
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,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"
|
||||
"Nil riera (nilriera2000)",Catalan,52,61,0,1,0,0,0,"2021-06-22 16:37:44"
|
||||
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"
|
||||
"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"
|
||||
andowero,Czech,47,38,0,0,0,0,0,"2020-01-20 02:29:01"
|
||||
vach,Armenian,47,36,0,0,0,0,0,"2020-04-18 16:53:12"
|
||||
"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,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"
|
||||
"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"
|
||||
"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"
|
||||
"Balázs Keresztury (belidzs)",Hungarian,42,41,501,0,7,0,38,"2017-04-06 02:40:24"
|
||||
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"
|
||||
dusanstrgar,Slovenian,39,41,0,0,0,0,0,"2017-03-31 10:30:28"
|
||||
"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"
|
||||
dusanstrgar,Slovenian,39,41,0,0,0,0,0,"2017-03-31 10:30:28"
|
||||
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"
|
||||
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"
|
||||
REMOVED_USER,Swedish,36,33,0,5,1,0,0,"2018-09-29 17:47:33"
|
||||
"長谷川知里 (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"
|
||||
"Ruud Schouten (ruudschouten)",Dutch,29,32,0,41,3,0,0,"2017-07-22 17:49:17"
|
||||
"비니몬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"
|
||||
"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"
|
||||
"Jonny I (jonny99dj)",Italian,26,26,0,5,0,0,0,"2017-10-07 07:35:34"
|
||||
"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"
|
||||
Pan_Filuta,Czech,25,21,0,5,8,0,3,"2017-04-29 12:55:14"
|
||||
"Radoslaw Biernacki (radoslaw.biernacki)",Polish,26,24,0,8,0,0,1,"2020-12-15 17:55:31"
|
||||
"Jonny I (jonny99dj)",Italian,26,26,0,5,0,0,0,"2017-10-07 07:35:34"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
"Alcarkse (alexis.brusle)",French,21,25,0,7,11,0,0,"2017-08-06 09:32:29"
|
||||
"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"
|
||||
"Shashwat (goforgold)",Hindi,20,33,0,0,0,0,0,"2020-05-17 10:34:42"
|
||||
"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"
|
||||
Adeline31,French,17,20,0,3,0,0,0,"2019-12-06 00:00:11"
|
||||
"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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
"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"
|
||||
"Jo Chuang (josephch405)","Chinese Traditional",11,24,0,0,0,0,11,"2017-06-16 20:21:06"
|
||||
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"
|
||||
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"
|
||||
"Ahmed Mosaad (ahmed.mosaad2018)",Arabic,10,12,0,6,0,0,0,"2021-02-03 18:45:43"
|
||||
"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"
|
||||
"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"
|
||||
"Edwin van Rooij (edwinvrooij)",Dutch,10,13,0,17,0,0,0,"2018-11-05 03:59:10"
|
||||
"Mihael Wagner (miha.wagner)",Slovenian,10,9,0,7,0,0,0,"2017-10-18 18:26:29"
|
||||
"Ahmed Mosaad (ahmed.mosaad2018)",Arabic,10,12,0,6,0,0,0,"2021-02-03 18:45:43"
|
||||
"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"
|
||||
"Jakob Weickmann (jweickm)",Japanese,8,21,0,0,0,0,0,"2021-10-05 11:10:25"
|
||||
"Sourire Lucide (sourire_lucide)",Russian,9,10,0,0,1,0,0,"2018-03-22 01:37:55"
|
||||
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"
|
||||
ftfoi,Norwegian,7,6,0,0,0,0,0,"2020-04-11 20:42:35"
|
||||
"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"
|
||||
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"
|
||||
pkorove,Greek,7,7,0,0,0,0,0,"2020-03-07 11:36:12"
|
||||
ChloeLiang,Japanese,6,22,0,0,1,0,3,"2017-08-08 05:02:59"
|
||||
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"
|
||||
"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"
|
||||
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"
|
||||
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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
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"
|
||||
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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
"Ñ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"
|
||||
"Valerij D (vala.dobler)",German,2,2,0,0,0,0,0,"2018-09-22 09:38:27"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
"Gabriel Cavalcante (gabrielc.alves14)","Portuguese, Brazilian",3,4,0,0,0,0,0,"2018-08-06 22:24:54"
|
||||
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"
|
||||
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"
|
||||
"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"
|
||||
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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
"Patrick Pimenta (trickap1)","Portuguese, Brazilian",1,1,0,0,0,0,0,"2018-12-01 14:31:21"
|
||||
"박찌 (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"
|
||||
"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"
|
||||
T-v-Gerwen,Dutch,0,0,0,47,0,0,0,"2018-03-02 10:26:33"
|
||||
"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"
|
||||
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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
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"
|
||||
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"
|
||||
"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"
|
||||
"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"
|
||||
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"
|
||||
"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"
|
||||
"عبد الناصر سعيد الثبيتي (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"
|
||||
"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"
|
||||
"Mateusz Teteruk (mttet)",Polish,0,0,0,1,0,0,0,"2021-01-23 13:09:59"
|
||||
"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"
|
||||
droidahmed,Arabic,0,0,0,7,0,0,0,"2018-01-31 02:18:49"
|
||||
"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"
|
||||
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"
|
||||
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"
|
||||
"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"
|
||||
"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"
|
||||
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"
|
||||
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"
|
||||
"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"
|
||||
|
||||
|
@@ -32,12 +32,12 @@ tasks.compileLint {
|
||||
|
||||
android {
|
||||
|
||||
compileSdk = 32
|
||||
compileSdk = 31
|
||||
|
||||
defaultConfig {
|
||||
versionCode = 20100
|
||||
versionName = "2.1.0"
|
||||
minSdk = 28
|
||||
versionCode = 20003
|
||||
versionName = "2.0.3"
|
||||
minSdk = 23
|
||||
targetSdk = 31
|
||||
applicationId = "org.isoron.uhabits"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
@@ -68,6 +68,12 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
isCheckReleaseBuilds = false
|
||||
isAbortOnError = false
|
||||
disable("GoogleAppIndexingWarning")
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
targetCompatibility(JavaVersion.VERSION_1_8)
|
||||
@@ -80,29 +86,29 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
val daggerVersion = "2.43.2"
|
||||
val kotlinVersion = "1.7.10"
|
||||
val kxCoroutinesVersion = "1.6.4"
|
||||
val ktorVersion = "1.6.8"
|
||||
val daggerVersion = "2.40.3"
|
||||
val kotlinVersion = "1.6.0"
|
||||
val kxCoroutinesVersion = "1.5.2"
|
||||
val ktorVersion = "1.6.6"
|
||||
val espressoVersion = "3.4.0"
|
||||
|
||||
androidTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:$espressoVersion")
|
||||
androidTestImplementation("com.google.dagger:dagger:$daggerVersion")
|
||||
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.3")
|
||||
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.4.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.2.2")
|
||||
implementation("com.github.AppIntro:AppIntro:6.2.0")
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
|
||||
implementation("com.github.AppIntro:AppIntro:6.1.0")
|
||||
implementation("com.google.code.findbugs:jsr305:3.0.2")
|
||||
implementation("com.google.dagger:dagger:$daggerVersion")
|
||||
implementation("com.google.guava:guava:31.1-android")
|
||||
implementation("com.google.guava:guava:31.0.1-android")
|
||||
implementation("io.ktor:ktor-client-android:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-core:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-jackson:$ktorVersion")
|
||||
@@ -110,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.5.0")
|
||||
implementation("androidx.appcompat:appcompat:1.3.1")
|
||||
implementation("androidx.legacy:legacy-preference-v14:1.0.0")
|
||||
implementation("androidx.legacy:legacy-support-v4:1.0.0")
|
||||
implementation("com.google.android.material:material:1.6.1")
|
||||
implementation("com.opencsv:opencsv:5.6")
|
||||
implementation("com.google.android.material:material:1.4.0")
|
||||
implementation("com.opencsv:opencsv:5.5.2")
|
||||
implementation(project(":uhabits-core"))
|
||||
kapt("com.google.dagger:dagger-compiler:$daggerVersion")
|
||||
kaptAndroidTest("com.google.dagger:dagger-compiler:$daggerVersion")
|
||||
|
||||
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 9.0 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: 22 KiB After Width: | Height: | Size: 20 KiB |
@@ -20,7 +20,6 @@
|
||||
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
|
||||
@@ -40,7 +39,7 @@ fun exportFullBackup() {
|
||||
}
|
||||
|
||||
fun clearDownloadFolder() {
|
||||
device.executeShellCommand("rm -rf /sdcard/Download")
|
||||
device.executeShellCommand("rm -rf /sdcard/Download/")
|
||||
}
|
||||
|
||||
fun clearBackupFolder() {
|
||||
@@ -87,7 +86,6 @@ 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,9 +18,7 @@
|
||||
*/
|
||||
package org.isoron.uhabits.acceptance.steps
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Build.VERSION_CODES
|
||||
import android.os.Build.VERSION
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.test.espresso.Espresso
|
||||
@@ -74,7 +72,7 @@ object CommonSteps : BaseUserInterfaceTest() {
|
||||
}
|
||||
|
||||
fun offsetHeaders() {
|
||||
device.swipe(500, 160, 350, 160, 20)
|
||||
device.swipe(750, 160, 600, 160, 20)
|
||||
}
|
||||
|
||||
fun scrollToText(text: String?) {
|
||||
@@ -135,7 +133,7 @@ object CommonSteps : BaseUserInterfaceTest() {
|
||||
@Throws(Exception::class)
|
||||
fun verifyOpensWebsite(url: String?) {
|
||||
var browserPkg = "org.chromium.webview_shell"
|
||||
if (SDK_INT <= Build.VERSION_CODES.M) {
|
||||
if (VERSION.SDK_INT <= 23) {
|
||||
browserPkg = "com.android.browser"
|
||||
}
|
||||
assertTrue(device.wait(Until.hasObject(By.pkg(browserPkg)), 5000))
|
||||
@@ -180,22 +178,6 @@ 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,24 +36,6 @@ 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)))
|
||||
|
||||
@@ -53,7 +53,6 @@ object ListHabitsSteps {
|
||||
clickViewWithId(R.id.action_filter)
|
||||
CommonSteps.clickText(R.string.hide_completed)
|
||||
}
|
||||
else -> throw RuntimeException()
|
||||
}
|
||||
device.waitForIdle()
|
||||
}
|
||||
@@ -121,12 +120,6 @@ 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.SDK_INT
|
||||
import android.os.Build.VERSION
|
||||
import androidx.test.uiautomator.UiScrollable
|
||||
import androidx.test.uiautomator.UiSelector
|
||||
import junit.framework.Assert.assertFalse
|
||||
@@ -50,21 +50,29 @@ object WidgetSteps {
|
||||
private fun openWidgetScreen() {
|
||||
val h = BaseUserInterfaceTest.device.displayHeight
|
||||
val w = BaseUserInterfaceTest.device.displayWidth
|
||||
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) {
|
||||
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))
|
||||
.scrollForward()
|
||||
.scrollIntoView(UiSelector().text("Checkmark"))
|
||||
}
|
||||
UiScrollable(UiSelector().resourceId(listId))
|
||||
.scrollIntoView(UiSelector().text("Checkmark"))
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
|
||||
@@ -29,10 +29,6 @@ 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,7 +36,6 @@ class EntryButtonViewTest : BaseViewTest() {
|
||||
lateinit var view: CheckmarkButtonView
|
||||
|
||||
var toggled = false
|
||||
var edited = false
|
||||
|
||||
@Before
|
||||
override fun setUp() {
|
||||
@@ -44,8 +43,7 @@ class EntryButtonViewTest : BaseViewTest() {
|
||||
view = component.getEntryButtonViewFactory().create().apply {
|
||||
value = Entry.NO
|
||||
color = PaletteUtils.getAndroidTestColor(5)
|
||||
onToggle = { _, _, _ -> toggled = true }
|
||||
onEdit = { edited = true }
|
||||
onToggle = { toggled = true }
|
||||
}
|
||||
measureView(view, dpToPixels(48), dpToPixels(48))
|
||||
}
|
||||
@@ -72,28 +70,20 @@ class EntryButtonViewTest : BaseViewTest() {
|
||||
fun testClick_withShortToggleDisabled() {
|
||||
prefs.isShortToggleEnabled = false
|
||||
view.performClick()
|
||||
assertTrue(!toggled and edited)
|
||||
assertFalse(toggled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClick_withShortToggleEnabled() {
|
||||
prefs.isShortToggleEnabled = true
|
||||
view.performClick()
|
||||
assertTrue(toggled and !edited)
|
||||
assertTrue(toggled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLongClick_withShortToggleDisabled() {
|
||||
prefs.isShortToggleEnabled = false
|
||||
fun testLongClick() {
|
||||
view.performLongClick()
|
||||
assertTrue(toggled and !edited)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLongClick_withShortToggleEnabled() {
|
||||
prefs.isShortToggleEnabled = true
|
||||
view.performLongClick()
|
||||
assertTrue(!toggled and edited)
|
||||
assertTrue(toggled)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -76,7 +76,7 @@ class NumberPanelViewTest : BaseViewTest() {
|
||||
@Test
|
||||
fun testEdit() {
|
||||
val timestamps = mutableListOf<Timestamp>()
|
||||
view.onEdit = { t -> timestamps.plusAssign(t) }
|
||||
view.onEdit = { timestamps.plusAssign(it) }
|
||||
view.buttons[0].performLongClick()
|
||||
view.buttons[2].performLongClick()
|
||||
view.buttons[3].performLongClick()
|
||||
@@ -87,7 +87,7 @@ class NumberPanelViewTest : BaseViewTest() {
|
||||
fun testEdit_withOffset() {
|
||||
val timestamps = mutableListOf<Timestamp>()
|
||||
view.dataOffset = 3
|
||||
view.onEdit = { t -> timestamps += t }
|
||||
view.onEdit = { timestamps += it }
|
||||
view.buttons[0].performLongClick()
|
||||
view.buttons[2].performLongClick()
|
||||
view.buttons[3].performLongClick()
|
||||
|
||||
@@ -21,12 +21,9 @@ 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
|
||||
@@ -40,12 +37,9 @@ 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
|
||||
|
||||
@@ -89,37 +83,4 @@ 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",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ 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
|
||||
@@ -43,12 +42,10 @@ class CheckmarkWidgetTest : BaseViewTest() {
|
||||
private lateinit var habit: Habit
|
||||
private lateinit var entries: EntryList
|
||||
private lateinit var view: FrameLayout
|
||||
private lateinit var today: Timestamp
|
||||
|
||||
private val today = getTodayWithOffset()
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
setTheme(R.style.WidgetTheme)
|
||||
today = getTodayWithOffset()
|
||||
prefs.widgetOpacity = 255
|
||||
prefs.isSkipEnabled = true
|
||||
habit = fixtures.createVeryLongHabit()
|
||||
|
||||
@@ -32,10 +32,6 @@ 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() {
|
||||
|
||||
@@ -79,14 +79,6 @@
|
||||
android:value=".activities.habits.list.ListHabitsActivity" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activities.sync.SyncActivity"
|
||||
android:label="@string/device_sync">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activities.sync.SyncActivity" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activities.intro.IntroActivity"
|
||||
android:label=""
|
||||
@@ -127,6 +119,18 @@
|
||||
android:value=".activities.habits.list.ListHabitsActivity" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".widgets.activities.NumericalCheckmarkWidgetActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:label="NumericalCheckmarkWidget"
|
||||
android:noHistory="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:excludeFromRecents="true"
|
||||
|
||||
@@ -22,6 +22,7 @@ import android.animation.Keyframe;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.PropertyValuesHolder;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Build;
|
||||
import android.text.format.Time;
|
||||
import android.view.View;
|
||||
|
||||
@@ -42,13 +43,17 @@ public class Utils {
|
||||
|
||||
static final String SHARED_PREFS_NAME = "com.android.calendar_preferences";
|
||||
|
||||
public static boolean isJellybeanOrLater() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to speak the specified text, for accessibility. Only available on JB or later.
|
||||
* @param text Text to announce.
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
public static void tryAccessibilityAnnounce(View view, CharSequence text) {
|
||||
if (view != null && text != null) {
|
||||
if (isJellybeanOrLater() && view != null && text != null) {
|
||||
view.announceForAccessibility(text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,6 +383,10 @@ public abstract class DayPickerView extends ListView implements OnScrollListener
|
||||
if (child instanceof MonthView) {
|
||||
final CalendarDay focus = ((MonthView) child).getAccessibilityFocus();
|
||||
if (focus != null) {
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
// Clear focus to avoid ListView bug in Jelly Bean MR1.
|
||||
((MonthView) child).clearAccessibilityFocus();
|
||||
}
|
||||
return focus;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package org.isoron.uhabits.activities.common.dialogs
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE
|
||||
import android.widget.Button
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import org.isoron.platform.gui.toInt
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.NO
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
|
||||
import org.isoron.uhabits.core.ui.views.Theme
|
||||
import org.isoron.uhabits.databinding.CheckmarkDialogBinding
|
||||
import org.isoron.uhabits.inject.ActivityContext
|
||||
import org.isoron.uhabits.utils.InterfaceUtils
|
||||
import org.isoron.uhabits.utils.StyledResources
|
||||
import javax.inject.Inject
|
||||
|
||||
class CheckmarkDialog
|
||||
@Inject constructor(
|
||||
@ActivityContext private val context: Context,
|
||||
private val preferences: Preferences,
|
||||
) : View.OnClickListener {
|
||||
|
||||
private lateinit var binding: CheckmarkDialogBinding
|
||||
private lateinit var fontAwesome: Typeface
|
||||
private val allButtons = mutableListOf<Button>()
|
||||
private var selectedButton: Button? = null
|
||||
|
||||
fun create(
|
||||
value: Int,
|
||||
notes: String,
|
||||
dateString: String,
|
||||
paletteColor: PaletteColor,
|
||||
callback: ListHabitsBehavior.CheckMarkDialogCallback,
|
||||
theme: Theme,
|
||||
): AlertDialog {
|
||||
binding = CheckmarkDialogBinding.inflate(LayoutInflater.from(context))
|
||||
fontAwesome = InterfaceUtils.getFontAwesome(context)!!
|
||||
binding.etNotes.append(notes)
|
||||
setUpButtons(value, theme.color(paletteColor).toInt())
|
||||
|
||||
val dialog = AlertDialog.Builder(context)
|
||||
.setView(binding.root)
|
||||
.setTitle(dateString)
|
||||
.setPositiveButton(R.string.save) { _, _ ->
|
||||
val newValue = when (selectedButton?.id) {
|
||||
R.id.yesBtn -> YES_MANUAL
|
||||
R.id.noBtn -> NO
|
||||
R.id.skippedBtn -> SKIP
|
||||
else -> UNKNOWN
|
||||
}
|
||||
callback.onNotesSaved(newValue, binding.etNotes.text.toString())
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
callback.onNotesDismissed()
|
||||
}
|
||||
.setOnDismissListener {
|
||||
callback.onNotesDismissed()
|
||||
}
|
||||
.create()
|
||||
|
||||
dialog.setOnShowListener {
|
||||
binding.etNotes.requestFocus()
|
||||
dialog.window?.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE)
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
private fun setUpButtons(value: Int, color: Int) {
|
||||
val sres = StyledResources(context)
|
||||
val mediumContrastColor = sres.getColor(R.attr.contrast60)
|
||||
setButtonAttrs(binding.yesBtn, color)
|
||||
setButtonAttrs(binding.noBtn, mediumContrastColor)
|
||||
setButtonAttrs(binding.skippedBtn, color, visible = preferences.isSkipEnabled)
|
||||
setButtonAttrs(binding.questionBtn, mediumContrastColor, visible = preferences.areQuestionMarksEnabled)
|
||||
when (value) {
|
||||
UNKNOWN -> if (preferences.areQuestionMarksEnabled) {
|
||||
binding.questionBtn.performClick()
|
||||
} else {
|
||||
binding.noBtn.performClick()
|
||||
}
|
||||
SKIP -> binding.skippedBtn.performClick()
|
||||
YES_MANUAL -> binding.yesBtn.performClick()
|
||||
YES_AUTO, NO -> binding.noBtn.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setButtonAttrs(button: Button, color: Int, visible: Boolean = true) {
|
||||
button.apply {
|
||||
visibility = if (visible) View.VISIBLE else View.GONE
|
||||
typeface = fontAwesome
|
||||
setTextColor(color)
|
||||
setOnClickListener(this@CheckmarkDialog)
|
||||
}
|
||||
allButtons.add(button)
|
||||
}
|
||||
|
||||
override fun onClick(v: View?) {
|
||||
allButtons.forEach {
|
||||
if (v?.id == it.id) {
|
||||
it.isSelected = true
|
||||
selectedButton = it
|
||||
} else it.isSelected = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.NO
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
|
||||
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
|
||||
import org.isoron.uhabits.utils.dimBehind
|
||||
import org.isoron.uhabits.utils.dismissCurrentAndShow
|
||||
import org.isoron.uhabits.utils.dp
|
||||
import org.isoron.uhabits.utils.sres
|
||||
|
||||
const val POPUP_WIDTH = 4 * 48f + 16f
|
||||
const val POPUP_HEIGHT = 48f * 2.5f + 8f
|
||||
|
||||
class CheckmarkPopup(
|
||||
private val context: Context,
|
||||
private val color: Int,
|
||||
private var notes: String,
|
||||
private var value: Int,
|
||||
private val prefs: Preferences,
|
||||
private val anchor: View,
|
||||
) {
|
||||
var onToggle: (Int, String) -> Unit = { _, _ -> }
|
||||
private lateinit var dialog: Dialog
|
||||
|
||||
private val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context)).apply {
|
||||
// Required for round corners
|
||||
container.clipToOutline = true
|
||||
}
|
||||
|
||||
init {
|
||||
view.booleanButtons.visibility = VISIBLE
|
||||
initColors()
|
||||
initTypefaces()
|
||||
hideDisabledButtons()
|
||||
populate()
|
||||
}
|
||||
|
||||
private fun initColors() {
|
||||
arrayOf(view.yesBtn, view.skipBtn).forEach {
|
||||
it.setTextColor(color)
|
||||
}
|
||||
arrayOf(view.noBtn, view.unknownBtn).forEach {
|
||||
it.setTextColor(view.root.sres.getColor(R.attr.contrast60))
|
||||
}
|
||||
}
|
||||
|
||||
private fun initTypefaces() {
|
||||
arrayOf(view.yesBtn, view.noBtn, view.skipBtn, view.unknownBtn).forEach {
|
||||
it.typeface = getFontAwesome(context)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideDisabledButtons() {
|
||||
if (!prefs.isSkipEnabled) view.skipBtn.visibility = GONE
|
||||
if (!prefs.areQuestionMarksEnabled) view.unknownBtn.visibility = GONE
|
||||
}
|
||||
|
||||
private fun populate() {
|
||||
val selectedBtn = when (value) {
|
||||
YES_MANUAL -> view.yesBtn
|
||||
YES_AUTO -> view.noBtn
|
||||
NO -> view.noBtn
|
||||
UNKNOWN -> if (prefs.areQuestionMarksEnabled) view.unknownBtn else view.noBtn
|
||||
SKIP -> if (prefs.isSkipEnabled) view.skipBtn else view.noBtn
|
||||
else -> null
|
||||
}
|
||||
view.notes.setText(notes)
|
||||
}
|
||||
|
||||
fun show() {
|
||||
dialog = Dialog(context, android.R.style.Theme_NoTitleBar)
|
||||
dialog.setContentView(view.root)
|
||||
dialog.window?.apply {
|
||||
setLayout(
|
||||
view.root.dp(POPUP_WIDTH).toInt(),
|
||||
view.root.dp(POPUP_HEIGHT).toInt()
|
||||
)
|
||||
setBackgroundDrawableResource(android.R.color.transparent)
|
||||
}
|
||||
fun onClick(v: Int) {
|
||||
this.value = v
|
||||
save()
|
||||
}
|
||||
view.yesBtn.setOnClickListener { onClick(YES_MANUAL) }
|
||||
view.noBtn.setOnClickListener { onClick(NO) }
|
||||
view.skipBtn.setOnClickListener { onClick(SKIP) }
|
||||
view.unknownBtn.setOnClickListener { onClick(UNKNOWN) }
|
||||
dialog.setCanceledOnTouchOutside(true)
|
||||
dialog.dimBehind()
|
||||
dialog.dismissCurrentAndShow()
|
||||
}
|
||||
|
||||
fun save() {
|
||||
onToggle(value, view.notes.text.toString().trim())
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ import org.isoron.uhabits.utils.toPaletteColor
|
||||
class ColorPickerDialog : ColorPickerDialog() {
|
||||
fun setListener(callback: OnColorPickedCallback) {
|
||||
super.setOnColorSelectedListener { c: Int ->
|
||||
val pc = c.toPaletteColor(requireContext())
|
||||
val pc = c.toPaletteColor(context!!)
|
||||
callback.onColorPicked(pc)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,10 +149,8 @@ class FrequencyPickerDialog(
|
||||
}
|
||||
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())
|
||||
numerator = Integer.parseInt(contentView.xTimesPerYDaysXTextView.text.toString())
|
||||
denominator = Integer.parseInt(contentView.xTimesPerYDaysYTextView.text.toString())
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
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
|
||||
@@ -44,19 +43,18 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
|
||||
private lateinit var commandRunner: CommandRunner
|
||||
private lateinit var habit: Habit
|
||||
private lateinit var preferences: Preferences
|
||||
lateinit var dataView: AndroidDataView
|
||||
private lateinit var dataView: AndroidDataView
|
||||
|
||||
private var chart: HistoryChart? = null
|
||||
private var onDateClickedListener: OnDateClickedListener? = null
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
clearCurrentDialog()
|
||||
val component = (requireActivity().application as HabitsApplication).component
|
||||
val component = (activity!!.application as HabitsApplication).component
|
||||
commandRunner = component.commandRunner
|
||||
habit = component.habitList.getById(requireArguments().getLong("habit"))!!
|
||||
habit = component.habitList.getById(arguments!!.getLong("habit"))!!
|
||||
preferences = component.preferences
|
||||
|
||||
val themeSwitcher = AndroidThemeSwitcher(requireActivity(), preferences)
|
||||
val themeSwitcher = AndroidThemeSwitcher(activity!!, preferences)
|
||||
themeSwitcher.apply()
|
||||
|
||||
chart = HistoryChart(
|
||||
@@ -71,23 +69,15 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
|
||||
onDateClickedListener = onDateClickedListener ?: object : OnDateClickedListener {},
|
||||
padding = 10.0,
|
||||
)
|
||||
dataView = AndroidDataView(requireContext(), null)
|
||||
dataView = AndroidDataView(context!!, null)
|
||||
dataView.view = chart!!
|
||||
|
||||
val dialog = Dialog(requireContext()).apply {
|
||||
return 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() {
|
||||
@@ -121,14 +111,4 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
|
||||
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,155 @@
|
||||
/*
|
||||
* 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.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.text.InputFilter
|
||||
import android.text.Spanned
|
||||
import android.view.LayoutInflater
|
||||
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE
|
||||
import android.view.inputmethod.EditorInfo
|
||||
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 java.text.DecimalFormatSymbols
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
class NumberPickerFactory
|
||||
@Inject constructor(
|
||||
@ActivityContext private val context: Context
|
||||
) {
|
||||
|
||||
fun create(
|
||||
value: Double,
|
||||
unit: String,
|
||||
notes: String,
|
||||
dateString: String,
|
||||
callback: ListHabitsBehavior.NumberPickerCallback
|
||||
): AlertDialog {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val view = inflater.inflate(R.layout.number_picker_dialog, null)
|
||||
|
||||
val picker = view.findViewById<NumberPicker>(R.id.picker)
|
||||
val picker2 = view.findViewById<NumberPicker>(R.id.picker2)
|
||||
val etNotes = view.findViewById<EditText>(R.id.etNotes)
|
||||
|
||||
val watcherFilter: InputFilter = SeparatorWatcherInputFilter(picker2)
|
||||
val numberPickerInputText = getNumberPickerInputText(picker)
|
||||
|
||||
// watch the unfiltered input before the filters remove a possible separator from it
|
||||
numberPickerInputText.filters = arrayOf(watcherFilter).plus(numberPickerInputText.filters)
|
||||
|
||||
view.findViewById<TextView>(R.id.tvUnit).text = unit
|
||||
view.findViewById<TextView>(R.id.tvSeparator).text =
|
||||
DecimalFormatSymbols.getInstance().decimalSeparator.toString()
|
||||
|
||||
val intValue = (value * 100).roundToLong().toInt()
|
||||
|
||||
picker.minValue = 0
|
||||
picker.maxValue = Integer.MAX_VALUE / 100
|
||||
picker.value = intValue / 100
|
||||
picker.wrapSelectorWheel = false
|
||||
|
||||
picker2.minValue = 0
|
||||
picker2.maxValue = 99
|
||||
picker2.setFormatter { v -> String.format("%02d", v) }
|
||||
picker2.value = intValue % 100
|
||||
|
||||
etNotes.setText(notes)
|
||||
val dialog = AlertDialog.Builder(context)
|
||||
.setView(view)
|
||||
.setTitle(dateString)
|
||||
.setPositiveButton(R.string.save) { _, _ ->
|
||||
picker.clearFocus()
|
||||
val v = picker.value + 0.01 * picker2.value
|
||||
val note = etNotes.text.toString()
|
||||
callback.onNumberPicked(v, note)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
callback.onNumberPickerDismissed()
|
||||
}
|
||||
.setOnDismissListener {
|
||||
callback.onNumberPickerDismissed()
|
||||
}
|
||||
.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
|
||||
}
|
||||
|
||||
InterfaceUtils.setupEditorAction(
|
||||
picker2
|
||||
) { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick()
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
@SuppressLint("DiscouragedPrivateApi")
|
||||
private fun getNumberPickerInputText(picker: NumberPicker): EditText {
|
||||
val f = NumberPicker::class.java.getDeclaredField("mInputText")
|
||||
f.isAccessible = true
|
||||
return f.get(picker) as EditText
|
||||
}
|
||||
}
|
||||
|
||||
class SeparatorWatcherInputFilter(private val nextPicker: NumberPicker) : InputFilter {
|
||||
override fun filter(
|
||||
source: CharSequence?,
|
||||
start: Int,
|
||||
end: Int,
|
||||
dest: Spanned?,
|
||||
dstart: Int,
|
||||
dend: Int
|
||||
): CharSequence {
|
||||
if (source == null || source.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
for (c in source) {
|
||||
if (c == DecimalFormatSymbols.getInstance().decimalSeparator || c == '.' || c == ',') {
|
||||
nextPicker.performLongClick()
|
||||
break
|
||||
}
|
||||
}
|
||||
return source
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.view.KeyEvent.KEYCODE_ENTER
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent.ACTION_DOWN
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
import org.isoron.uhabits.core.models.Entry
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
|
||||
import org.isoron.uhabits.utils.dimBehind
|
||||
import org.isoron.uhabits.utils.dismissCurrentAndShow
|
||||
import org.isoron.uhabits.utils.dp
|
||||
import org.isoron.uhabits.utils.requestFocusWithKeyboard
|
||||
import java.text.DecimalFormat
|
||||
|
||||
class NumberPopup(
|
||||
private val context: Context,
|
||||
private var notes: String,
|
||||
private var value: Double,
|
||||
private val prefs: Preferences,
|
||||
private val anchor: View,
|
||||
) {
|
||||
var onToggle: (Double, String) -> Unit = { _, _ -> }
|
||||
var onDismiss: () -> Unit = {}
|
||||
private val originalValue = value
|
||||
private lateinit var dialog: Dialog
|
||||
|
||||
private val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context)).apply {
|
||||
// Required for round corners
|
||||
container.clipToOutline = true
|
||||
}
|
||||
|
||||
init {
|
||||
view.numberButtons.visibility = VISIBLE
|
||||
hideDisabledButtons()
|
||||
populate()
|
||||
}
|
||||
|
||||
private fun hideDisabledButtons() {
|
||||
if (!prefs.isSkipEnabled) view.skipBtnNumber.visibility = GONE
|
||||
}
|
||||
|
||||
private fun populate() {
|
||||
view.notes.setText(notes)
|
||||
view.value.setText(
|
||||
when {
|
||||
value < 0.01 -> "0"
|
||||
else -> DecimalFormat("#.##").format(value)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun show() {
|
||||
dialog = Dialog(context, android.R.style.Theme_NoTitleBar)
|
||||
dialog.setContentView(view.root)
|
||||
dialog.window?.apply {
|
||||
setLayout(
|
||||
view.root.dp(POPUP_WIDTH).toInt(),
|
||||
view.root.dp(POPUP_HEIGHT).toInt()
|
||||
)
|
||||
setBackgroundDrawableResource(android.R.color.transparent)
|
||||
}
|
||||
dialog.setOnDismissListener {
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
view.value.setOnKeyListener { _, keyCode, event ->
|
||||
if (event.action == ACTION_DOWN && keyCode == KEYCODE_ENTER) {
|
||||
save()
|
||||
return@setOnKeyListener true
|
||||
}
|
||||
return@setOnKeyListener false
|
||||
}
|
||||
view.saveBtn.setOnClickListener {
|
||||
save()
|
||||
}
|
||||
view.skipBtnNumber.setOnClickListener {
|
||||
view.value.setText((Entry.SKIP.toDouble() / 1000).toString())
|
||||
save()
|
||||
}
|
||||
view.value.requestFocusWithKeyboard()
|
||||
dialog.setCanceledOnTouchOutside(true)
|
||||
dialog.dimBehind()
|
||||
dialog.dismissCurrentAndShow()
|
||||
}
|
||||
|
||||
fun save() {
|
||||
val value = view.value.text.toString().toDoubleOrNull() ?: originalValue
|
||||
val notes = view.notes.text.toString()
|
||||
onToggle(value, notes)
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ class WeekdayPickerDialog :
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val builder = AlertDialog.Builder(
|
||||
requireActivity()
|
||||
activity!!
|
||||
)
|
||||
builder
|
||||
.setTitle(R.string.select_weekdays)
|
||||
@@ -73,7 +73,6 @@ class WeekdayPickerDialog :
|
||||
.setNegativeButton(
|
||||
android.R.string.cancel
|
||||
) { _: DialogInterface?, _: Int -> dismiss() }
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ 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
|
||||
@@ -65,7 +64,6 @@ 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()
|
||||
@@ -82,11 +80,6 @@ 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)
|
||||
@@ -173,7 +166,6 @@ 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)
|
||||
@@ -181,8 +173,7 @@ 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], weekDaysInMonth[i])
|
||||
if (values != null) drawMarker(canvas, rect, values[i])
|
||||
rect.offset(0f, rowHeight)
|
||||
}
|
||||
drawFooter(canvas, rect, date)
|
||||
@@ -230,16 +221,12 @@ class FrequencyChart : ScrollableChart {
|
||||
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid!!)
|
||||
}
|
||||
|
||||
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) }
|
||||
|
||||
private fun drawMarker(canvas: Canvas, rect: RectF?, value: Int?) {
|
||||
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 scalingFactor = if (isNumerical) maxFreq else weekdayFrequency
|
||||
val scale = 1.0f / scalingFactor * valueCopy!!
|
||||
val scale = 1.0f / maxFreq * value!!
|
||||
val radius = maxRadius * scale
|
||||
val colorIndex = min((colors.size - 1), ((colors.size - 1) * scale).roundToInt())
|
||||
pGraph!!.color = colors[colorIndex]
|
||||
|
||||
@@ -59,7 +59,6 @@ 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
|
||||
|
||||
@@ -157,23 +156,23 @@ class EditHabitActivity : AppCompatActivity() {
|
||||
|
||||
val colorPickerDialogFactory = ColorPickerDialogFactory(this)
|
||||
binding.colorButton.setOnClickListener {
|
||||
val picker = colorPickerDialogFactory.create(color, themeSwitcher.currentTheme)
|
||||
picker.setListener { paletteColor ->
|
||||
val dialog = colorPickerDialogFactory.create(color, themeSwitcher.currentTheme)
|
||||
dialog.setListener { paletteColor ->
|
||||
this.color = paletteColor
|
||||
updateColors()
|
||||
}
|
||||
picker.dismissCurrentAndShow(supportFragmentManager, "colorPicker")
|
||||
dialog.show(supportFragmentManager, "colorPicker")
|
||||
}
|
||||
|
||||
populateFrequency()
|
||||
binding.booleanFrequencyPicker.setOnClickListener {
|
||||
val picker = FrequencyPickerDialog(freqNum, freqDen)
|
||||
picker.onFrequencyPicked = { num, den ->
|
||||
val dialog = FrequencyPickerDialog(freqNum, freqDen)
|
||||
dialog.onFrequencyPicked = { num, den ->
|
||||
freqNum = num
|
||||
freqDen = den
|
||||
populateFrequency()
|
||||
}
|
||||
picker.dismissCurrentAndShow(supportFragmentManager, "frequencyPicker")
|
||||
dialog.show(supportFragmentManager, "frequencyPicker")
|
||||
}
|
||||
|
||||
populateTargetType()
|
||||
@@ -190,8 +189,7 @@ class EditHabitActivity : AppCompatActivity() {
|
||||
populateTargetType()
|
||||
dialog.dismiss()
|
||||
}
|
||||
val dialog = builder.create()
|
||||
dialog.dismissCurrentAndShow()
|
||||
builder.show()
|
||||
}
|
||||
|
||||
binding.numericalFrequencyPicker.setOnClickListener {
|
||||
@@ -237,7 +235,7 @@ class EditHabitActivity : AppCompatActivity() {
|
||||
is24HourMode,
|
||||
androidColor
|
||||
)
|
||||
dialog.dismissCurrentAndShow(supportFragmentManager, "timePicker")
|
||||
dialog.show(supportFragmentManager, "timePicker")
|
||||
}
|
||||
|
||||
binding.reminderDatePicker.setOnClickListener {
|
||||
@@ -249,7 +247,7 @@ class EditHabitActivity : AppCompatActivity() {
|
||||
populateReminder()
|
||||
}
|
||||
dialog.setSelectedDays(reminderDays)
|
||||
dialog.dismissCurrentAndShow(supportFragmentManager, "dayPicker")
|
||||
dialog.show(supportFragmentManager, "dayPicker")
|
||||
}
|
||||
|
||||
binding.buttonSave.setOnClickListener {
|
||||
|
||||
@@ -40,13 +40,13 @@ class HabitTypeDialog : AppCompatDialogFragment() {
|
||||
val binding = SelectHabitTypeBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.buttonYesNo.setOnClickListener {
|
||||
val intent = IntentFactory().startEditActivity(requireActivity(), HabitType.YES_NO.value)
|
||||
val intent = IntentFactory().startEditActivity(activity!!, HabitType.YES_NO.value)
|
||||
startActivity(intent)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
binding.buttonMeasurable.setOnClickListener {
|
||||
val intent = IntentFactory().startEditActivity(requireActivity(), HabitType.NUMERICAL.value)
|
||||
val intent = IntentFactory().startEditActivity(activity!!, HabitType.NUMERICAL.value)
|
||||
startActivity(intent)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ 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
|
||||
@@ -30,7 +29,6 @@ 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
|
||||
@@ -38,15 +36,11 @@ 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.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
|
||||
@@ -65,8 +59,8 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
appComponent = (applicationContext as HabitsApplication).component
|
||||
component = DaggerHabitsActivityComponent
|
||||
val appComponent = (applicationContext as HabitsApplication).component
|
||||
val component = DaggerHabitsActivityComponent
|
||||
.builder()
|
||||
.activityContextModule(ActivityContextModule(this))
|
||||
.habitsApplicationComponent(appComponent)
|
||||
@@ -100,17 +94,11 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
||||
rootView.postInvalidate()
|
||||
midnightTimer.onResume()
|
||||
taskRunner.run {
|
||||
try {
|
||||
AutoBackup(this@ListHabitsActivity).run()
|
||||
appComponent.widgetUpdater.updateWidgets()
|
||||
} catch (e: Exception) {
|
||||
Log.e("ListHabitActivity", "TaskRunner failed", e)
|
||||
}
|
||||
AutoBackup(this@ListHabitsActivity).run()
|
||||
}
|
||||
if (prefs.theme == THEME_DARK && prefs.isPureBlackEnabled != pureBlack) {
|
||||
restartWithFade(ListHabitsActivity::class.java)
|
||||
}
|
||||
parseIntents()
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
@@ -128,26 +116,4 @@ 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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,12 +24,11 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import dagger.Lazy
|
||||
import org.isoron.platform.gui.toInt
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.activities.common.dialogs.CheckmarkPopup
|
||||
import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
|
||||
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.NumberPopup
|
||||
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
|
||||
import org.isoron.uhabits.activities.habits.edit.HabitTypeDialog
|
||||
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
|
||||
import org.isoron.uhabits.core.commands.ArchiveHabitsCommand
|
||||
@@ -42,7 +41,6 @@ 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
|
||||
@@ -63,8 +61,6 @@ 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
|
||||
@@ -93,9 +89,9 @@ class ListHabitsScreen
|
||||
private val exportDBFactory: ExportDBTaskFactory,
|
||||
private val importTaskFactory: ImportDataTaskFactory,
|
||||
private val colorPickerFactory: ColorPickerDialogFactory,
|
||||
private val behavior: Lazy<ListHabitsBehavior>,
|
||||
private val preferences: Preferences,
|
||||
private val rootView: Lazy<ListHabitsRootView>,
|
||||
private val numberPickerFactory: NumberPickerFactory,
|
||||
private val checkMarkDialog: CheckmarkDialog,
|
||||
private val behavior: Lazy<ListHabitsBehavior>
|
||||
) : CommandRunner.Listener,
|
||||
ListHabitsBehavior.Screen,
|
||||
ListHabitsMenuBehavior.Screen,
|
||||
@@ -164,7 +160,7 @@ class ListHabitsScreen
|
||||
}
|
||||
|
||||
override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback, quantity: Int) {
|
||||
ConfirmDeleteDialog(activity, callback, quantity).dismissCurrentAndShow()
|
||||
ConfirmDeleteDialog(activity, callback, quantity).show()
|
||||
}
|
||||
|
||||
override fun showEditHabitsScreen(selected: List<Habit>) {
|
||||
@@ -225,45 +221,34 @@ class ListHabitsScreen
|
||||
override fun showColorPicker(defaultColor: PaletteColor, callback: OnColorPickedCallback) {
|
||||
val picker = colorPickerFactory.create(defaultColor, themeSwitcher.currentTheme!!)
|
||||
picker.setListener(callback)
|
||||
picker.dismissCurrentAndShow(activity.supportFragmentManager, "picker")
|
||||
picker.show(activity.supportFragmentManager, "picker")
|
||||
}
|
||||
|
||||
override fun showNumberPopup(
|
||||
override fun showNumberPicker(
|
||||
value: Double,
|
||||
unit: String,
|
||||
notes: String,
|
||||
dateString: String,
|
||||
callback: ListHabitsBehavior.NumberPickerCallback
|
||||
) {
|
||||
val view = rootView.get()
|
||||
NumberPopup(
|
||||
context = context,
|
||||
prefs = preferences,
|
||||
anchor = view,
|
||||
notes = notes,
|
||||
value = value,
|
||||
).apply {
|
||||
onToggle = { value, notes -> callback.onNumberPicked(value, notes) }
|
||||
show()
|
||||
}
|
||||
numberPickerFactory.create(value, unit, notes, dateString, callback).show()
|
||||
}
|
||||
|
||||
override fun showCheckmarkPopup(
|
||||
selectedValue: Int,
|
||||
override fun showCheckmarkDialog(
|
||||
value: Int,
|
||||
notes: String,
|
||||
dateString: String,
|
||||
color: PaletteColor,
|
||||
callback: ListHabitsBehavior.CheckMarkDialogCallback
|
||||
) {
|
||||
val view = rootView.get()
|
||||
CheckmarkPopup(
|
||||
context = context,
|
||||
prefs = preferences,
|
||||
anchor = view,
|
||||
color = view.currentTheme().color(color).toInt(),
|
||||
notes = notes,
|
||||
value = selectedValue,
|
||||
).apply {
|
||||
onToggle = { value, notes -> callback.onNotesSaved(value, notes) }
|
||||
show()
|
||||
}
|
||||
checkMarkDialog.create(
|
||||
value,
|
||||
notes,
|
||||
dateString,
|
||||
color,
|
||||
callback,
|
||||
themeSwitcher.currentTheme!!,
|
||||
).show()
|
||||
}
|
||||
|
||||
private fun getExecuteString(command: Command): String? {
|
||||
|
||||
@@ -44,8 +44,6 @@ import org.isoron.uhabits.utils.sres
|
||||
import org.isoron.uhabits.utils.toMeasureSpec
|
||||
import javax.inject.Inject
|
||||
|
||||
const val TOGGLE_DELAY_MILLIS = 2000L
|
||||
|
||||
class CheckmarkButtonViewFactory
|
||||
@Inject constructor(
|
||||
@ActivityContext val context: Context,
|
||||
@@ -73,42 +71,42 @@ class CheckmarkButtonView(
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var notes = ""
|
||||
var hasNotes = false
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var onToggle: (Int, String, Long) -> Unit = { _, _, _ -> }
|
||||
|
||||
var onEdit: () -> Unit = { }
|
||||
var onToggle: (Int) -> Unit = {}
|
||||
|
||||
var onEdit: () -> Unit = {}
|
||||
private var drawer = Drawer()
|
||||
|
||||
init {
|
||||
isFocusable = false
|
||||
setOnClickListener(this)
|
||||
setOnLongClickListener(this)
|
||||
}
|
||||
|
||||
fun performToggle(delay: Long) {
|
||||
fun performToggle() {
|
||||
value = Entry.nextToggleValue(
|
||||
value = value,
|
||||
isSkipEnabled = preferences.isSkipEnabled,
|
||||
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
|
||||
)
|
||||
onToggle(value, notes, delay)
|
||||
onToggle(value)
|
||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
if (preferences.isShortToggleEnabled) performToggle(TOGGLE_DELAY_MILLIS)
|
||||
if (preferences.isShortToggleEnabled) performToggle()
|
||||
else onEdit()
|
||||
}
|
||||
|
||||
override fun onLongClick(v: View): Boolean {
|
||||
if (preferences.isShortToggleEnabled) onEdit()
|
||||
else performToggle(TOGGLE_DELAY_MILLIS)
|
||||
else performToggle()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -182,7 +180,7 @@ class CheckmarkButtonView(
|
||||
canvas.drawText(label, rect.centerX(), rect.centerY(), paint)
|
||||
}
|
||||
|
||||
drawNotesIndicator(canvas, color, em, notes)
|
||||
drawNotesIndicator(canvas, color, em, hasNotes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,19 +54,19 @@ class CheckmarkPanelView(
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var notes = arrayOf<String>()
|
||||
var notesIndicators = BooleanArray(0)
|
||||
set(values) {
|
||||
field = values
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var onToggle: (Timestamp, Int, String, Long) -> Unit = { _, _, _, _ -> }
|
||||
var onToggle: (Timestamp, Int) -> Unit = { _, _ -> }
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var onEdit: (Timestamp) -> Unit = { _ -> }
|
||||
var onEdit: (Timestamp) -> Unit = {}
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
@@ -84,12 +84,12 @@ class CheckmarkPanelView(
|
||||
index + dataOffset < values.size -> values[index + dataOffset]
|
||||
else -> UNKNOWN
|
||||
}
|
||||
button.notes = when {
|
||||
index + dataOffset < notes.size -> notes[index + dataOffset]
|
||||
else -> ""
|
||||
button.hasNotes = when {
|
||||
index + dataOffset < notesIndicators.size -> notesIndicators[index + dataOffset]
|
||||
else -> false
|
||||
}
|
||||
button.color = color
|
||||
button.onToggle = { value, notes, delay -> onToggle(timestamp, value, notes, delay) }
|
||||
button.onToggle = { value -> onToggle(timestamp, value) }
|
||||
button.onEdit = { onEdit(timestamp) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,9 +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 notesIndicators = cache.getNoteIndicators(habit.id!!)
|
||||
val selected = selected.contains(habit)
|
||||
listView!!.bindCardView(holder, habit, score, checkmarks, notes, selected)
|
||||
listView!!.bindCardView(holder, habit, score, checkmarks, notesIndicators, selected)
|
||||
}
|
||||
|
||||
override fun onViewAttachedToWindow(holder: HabitCardViewHolder) {
|
||||
|
||||
@@ -87,7 +87,7 @@ class HabitCardListView(
|
||||
habit: Habit,
|
||||
score: Double,
|
||||
checkmarks: IntArray,
|
||||
notes: Array<String>,
|
||||
notesIndicators: BooleanArray,
|
||||
selected: Boolean
|
||||
): View {
|
||||
val cardView = holder.itemView as HabitCardView
|
||||
@@ -99,7 +99,7 @@ class HabitCardListView(
|
||||
cardView.score = score
|
||||
cardView.unit = habit.unit
|
||||
cardView.threshold = habit.targetValue / habit.frequency.denominator
|
||||
cardView.notes = notes
|
||||
cardView.notesIndicators = notesIndicators
|
||||
|
||||
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
|
||||
@@ -60,8 +60,7 @@ class HabitCardViewFactory
|
||||
data class DelayedToggle(
|
||||
var habit: Habit,
|
||||
var timestamp: Timestamp,
|
||||
var value: Int,
|
||||
var notes: String
|
||||
var value: Int
|
||||
)
|
||||
|
||||
class HabitCardView(
|
||||
@@ -122,11 +121,11 @@ class HabitCardView(
|
||||
numberPanel.threshold = value
|
||||
}
|
||||
|
||||
var notes
|
||||
get() = checkmarkPanel.notes
|
||||
var notesIndicators
|
||||
get() = checkmarkPanel.notesIndicators
|
||||
set(values) {
|
||||
checkmarkPanel.notes = values
|
||||
numberPanel.notes = values
|
||||
checkmarkPanel.notesIndicators = values
|
||||
numberPanel.notesIndicators = values
|
||||
}
|
||||
|
||||
var checkmarkPanel: CheckmarkPanelView
|
||||
@@ -154,17 +153,15 @@ class HabitCardView(
|
||||
maxLines = 2
|
||||
ellipsize = TextUtils.TruncateAt.END
|
||||
layoutParams = LinearLayout.LayoutParams(0, WRAP_CONTENT, 1f)
|
||||
if (SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
breakStrategy = BREAK_STRATEGY_BALANCED
|
||||
}
|
||||
if (SDK_INT >= M) breakStrategy = BREAK_STRATEGY_BALANCED
|
||||
}
|
||||
|
||||
checkmarkPanel = checkmarkPanelFactory.create().apply {
|
||||
onToggle = { timestamp, value, notes, delay ->
|
||||
if (delay > 0) triggerRipple(timestamp)
|
||||
onToggle = { timestamp, value ->
|
||||
triggerRipple(timestamp)
|
||||
habit?.let {
|
||||
val taskId = queueToggle(it, timestamp, value, notes);
|
||||
{ runPendingToggles(taskId) }.delay(delay)
|
||||
val taskId = queueToggle(it, timestamp, value);
|
||||
{ runPendingToggles(taskId) }.delay(TOGGLE_DELAY_MILLIS)
|
||||
}
|
||||
}
|
||||
onEdit = { timestamp ->
|
||||
@@ -208,7 +205,7 @@ class HabitCardView(
|
||||
@Synchronized
|
||||
private fun runPendingToggles(id: Int) {
|
||||
if (currentToggleTaskId != id) return
|
||||
for ((h, t, v, n) in queuedToggles) behavior.onToggle(h, t, v, n)
|
||||
for ((h, t, v) in queuedToggles) behavior.onToggle(h, t, v)
|
||||
queuedToggles.clear()
|
||||
}
|
||||
|
||||
@@ -216,11 +213,10 @@ class HabitCardView(
|
||||
private fun queueToggle(
|
||||
it: Habit,
|
||||
timestamp: Timestamp,
|
||||
value: Int,
|
||||
notes: String,
|
||||
value: Int
|
||||
): Int {
|
||||
currentToggleTaskId += 1
|
||||
queuedToggles.add(DelayedToggle(it, timestamp, value, notes))
|
||||
queuedToggles.add(DelayedToggle(it, timestamp, value))
|
||||
return currentToggleTaskId
|
||||
}
|
||||
|
||||
@@ -310,6 +306,8 @@ class HabitCardView(
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TOGGLE_DELAY_MILLIS = 2000L
|
||||
|
||||
fun (() -> Unit).delay(delayInMillis: Long) {
|
||||
Handler(Looper.getMainLooper()).postDelayed(this, delayInMillis)
|
||||
}
|
||||
|
||||
@@ -29,9 +29,7 @@ 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.models.NumericalHabitType
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.inject.ActivityContext
|
||||
import org.isoron.uhabits.utils.InterfaceUtils.getDimension
|
||||
@@ -39,6 +37,7 @@ import org.isoron.uhabits.utils.dim
|
||||
import org.isoron.uhabits.utils.drawNotesIndicator
|
||||
import org.isoron.uhabits.utils.getFontAwesome
|
||||
import org.isoron.uhabits.utils.sres
|
||||
import java.lang.Double.max
|
||||
import java.text.DecimalFormat
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -91,7 +90,7 @@ class NumberButtonView(
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var targetType = AT_LEAST
|
||||
var targetType = NumericalHabitType.AT_LEAST
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
@@ -102,14 +101,13 @@ class NumberButtonView(
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
var notes = ""
|
||||
var hasNotes = false
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var onEdit: () -> Unit = { }
|
||||
|
||||
var onEdit: () -> Unit = {}
|
||||
private var drawer: Drawer = Drawer(context)
|
||||
|
||||
init {
|
||||
@@ -145,12 +143,6 @@ class NumberButtonView(
|
||||
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
|
||||
@@ -172,11 +164,18 @@ class NumberButtonView(
|
||||
}
|
||||
|
||||
fun draw(canvas: Canvas) {
|
||||
val activeColor = when {
|
||||
value < 0.0 -> lowContrast
|
||||
(targetType == AT_LEAST) && (value >= threshold) -> color
|
||||
(targetType == AT_MOST) && (value <= threshold) -> color
|
||||
else -> mediumContrast
|
||||
var activeColor = if (targetType == NumericalHabitType.AT_LEAST) {
|
||||
when {
|
||||
value < 0.0 && preferences.areQuestionMarksEnabled -> lowContrast
|
||||
max(0.0, value) >= threshold -> color
|
||||
else -> mediumContrast
|
||||
}
|
||||
} else {
|
||||
when {
|
||||
value < 0.0 && preferences.areQuestionMarksEnabled -> lowContrast
|
||||
value <= threshold -> color
|
||||
else -> mediumContrast
|
||||
}
|
||||
}
|
||||
|
||||
val label: String
|
||||
@@ -184,11 +183,6 @@ 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
|
||||
@@ -222,7 +216,7 @@ class NumberButtonView(
|
||||
canvas.drawText(units, rect.centerX(), rect.centerY(), pUnit)
|
||||
}
|
||||
|
||||
drawNotesIndicator(canvas, color, em, notes)
|
||||
drawNotesIndicator(canvas, color, em, hasNotes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,13 +72,13 @@ class NumberPanelView(
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var notes = arrayOf<String>()
|
||||
var notesIndicators = BooleanArray(0)
|
||||
set(values) {
|
||||
field = values
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var onEdit: (Timestamp) -> Unit = { _ -> }
|
||||
var onEdit: (Timestamp) -> Unit = {}
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
@@ -96,9 +96,9 @@ class NumberPanelView(
|
||||
index + dataOffset < values.size -> values[index + dataOffset]
|
||||
else -> 0.0
|
||||
}
|
||||
button.notes = when {
|
||||
index + dataOffset < notes.size -> notes[index + dataOffset]
|
||||
else -> ""
|
||||
button.hasNotes = when {
|
||||
index + dataOffset < notesIndicators.size -> notesIndicators[index + dataOffset]
|
||||
else -> false
|
||||
}
|
||||
button.color = color
|
||||
button.targetType = targetType
|
||||
|
||||
@@ -23,21 +23,19 @@ 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.CheckmarkPopup
|
||||
import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog
|
||||
import org.isoron.uhabits.activities.common.dialogs.NumberPopup
|
||||
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
|
||||
import org.isoron.uhabits.core.commands.Command
|
||||
import org.isoron.uhabits.core.commands.CommandRunner
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
@@ -49,8 +47,6 @@ 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.showMessage
|
||||
import org.isoron.uhabits.utils.showSendFileScreen
|
||||
import org.isoron.uhabits.widgets.WidgetUpdater
|
||||
@@ -167,49 +163,32 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
||||
window.decorView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
}
|
||||
|
||||
override fun showNumberPopup(
|
||||
override fun showNumberPicker(
|
||||
value: Double,
|
||||
unit: String,
|
||||
notes: String,
|
||||
preferences: Preferences,
|
||||
callback: ListHabitsBehavior.NumberPickerCallback
|
||||
dateString: String,
|
||||
callback: ListHabitsBehavior.NumberPickerCallback,
|
||||
) {
|
||||
val anchor = getPopupAnchor() ?: return
|
||||
NumberPopup(
|
||||
context = this@ShowHabitActivity,
|
||||
prefs = preferences,
|
||||
notes = notes,
|
||||
anchor = anchor,
|
||||
value = value,
|
||||
).apply {
|
||||
onToggle = { v, n -> callback.onNumberPicked(v, n) }
|
||||
show()
|
||||
}
|
||||
NumberPickerFactory(this@ShowHabitActivity).create(value, unit, notes, dateString, callback).show()
|
||||
}
|
||||
|
||||
override fun showCheckmarkPopup(
|
||||
selectedValue: Int,
|
||||
override fun showCheckmarkDialog(
|
||||
value: Int,
|
||||
notes: String,
|
||||
dateString: String,
|
||||
preferences: Preferences,
|
||||
color: PaletteColor,
|
||||
callback: ListHabitsBehavior.CheckMarkDialogCallback
|
||||
) {
|
||||
val anchor = getPopupAnchor() ?: return
|
||||
CheckmarkPopup(
|
||||
context = this@ShowHabitActivity,
|
||||
prefs = preferences,
|
||||
notes = notes,
|
||||
color = view.currentTheme().color(color).toInt(),
|
||||
anchor = anchor,
|
||||
value = selectedValue,
|
||||
).apply {
|
||||
onToggle = { v, n -> callback.onNotesSaved(v, n) }
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPopupAnchor(): View? {
|
||||
val dialog = supportFragmentManager.findFragmentByTag("historyEditor") as HistoryEditorDialog?
|
||||
return dialog?.dataView
|
||||
CheckmarkDialog(this@ShowHabitActivity, preferences).create(
|
||||
value,
|
||||
notes,
|
||||
dateString,
|
||||
color,
|
||||
callback,
|
||||
themeSwitcher.currentTheme!!,
|
||||
).show()
|
||||
}
|
||||
|
||||
override fun showEditHabitScreen(habit: Habit) {
|
||||
@@ -221,7 +200,6 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
||||
ShowHabitMenuPresenter.Message.COULD_NOT_EXPORT -> {
|
||||
showMessage(resources.getString(R.string.could_not_export))
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +208,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
||||
}
|
||||
|
||||
override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback) {
|
||||
ConfirmDeleteDialog(this@ShowHabitActivity, callback, 1).dismissCurrentAndShow()
|
||||
ConfirmDeleteDialog(this@ShowHabitActivity, callback, 1).show()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
|
||||
@@ -33,7 +33,6 @@ 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)
|
||||
|
||||
@@ -22,10 +22,11 @@ import android.app.backup.BackupManager
|
||||
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.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
|
||||
@@ -42,7 +43,6 @@ 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
|
||||
|
||||
@@ -63,7 +63,7 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
addPreferencesFromResource(R.xml.preferences)
|
||||
val appContext = requireContext().applicationContext
|
||||
val appContext = context!!.applicationContext
|
||||
if (appContext is HabitsApplication) {
|
||||
prefs = appContext.component.preferences
|
||||
widgetUpdater = appContext.component.widgetUpdater
|
||||
@@ -84,21 +84,16 @@ 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") {
|
||||
createAndroidNotificationChannel(requireContext())
|
||||
if (VERSION.SDK_INT < Build.VERSION_CODES.O) return true
|
||||
createAndroidNotificationChannel(context!!)
|
||||
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, context!!.packageName)
|
||||
intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationTray.REMINDERS_CHANNEL_ID)
|
||||
startActivity(intent)
|
||||
return true
|
||||
@@ -108,7 +103,7 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
ringtoneManager = RingtoneManager(requireActivity())
|
||||
ringtoneManager = RingtoneManager(activity!!)
|
||||
sharedPrefs = preferenceManager.sharedPreferences
|
||||
sharedPrefs!!.registerOnSharedPreferenceChangeListener(this)
|
||||
if (!prefs.isDeveloper) {
|
||||
@@ -117,7 +112,11 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
|
||||
}
|
||||
updateWeekdayPreference()
|
||||
|
||||
findPreference("reminderSound").isVisible = false
|
||||
if (VERSION.SDK_INT < Build.VERSION_CODES.O)
|
||||
findPreference("reminderCustomize").isVisible = false
|
||||
else {
|
||||
findPreference("reminderSound").isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateWeekdayPreference() {
|
||||
@@ -147,8 +146,8 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
|
||||
val pref = findPreference(key)
|
||||
pref.onPreferenceClickListener =
|
||||
Preference.OnPreferenceClickListener {
|
||||
requireActivity().setResult(result)
|
||||
requireActivity().finish()
|
||||
activity!!.setResult(result)
|
||||
activity!!.finish()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,100 +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.sync
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import org.isoron.uhabits.HabitsApplication
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.databinding.SyncActivityBinding
|
||||
import org.isoron.uhabits.utils.setupToolbar
|
||||
|
||||
class SyncActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: SyncActivityBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val component = (application as HabitsApplication).component
|
||||
val themeSwitcher = AndroidThemeSwitcher(this, component.preferences)
|
||||
themeSwitcher.apply()
|
||||
|
||||
binding = SyncActivityBinding.inflate(LayoutInflater.from(this))
|
||||
binding.root.setupToolbar(
|
||||
toolbar = binding.toolbar,
|
||||
title = resources.getString(R.string.device_sync),
|
||||
color = PaletteColor(11),
|
||||
theme = themeSwitcher.currentTheme,
|
||||
)
|
||||
binding.generateButton.setOnClickListener { onGenerateCode() }
|
||||
binding.enterButton.setOnClickListener {
|
||||
val et = EditText(this)
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.sync_code)
|
||||
.setView(et)
|
||||
.setPositiveButton(R.string.save) { _, _ ->
|
||||
onEnterCode(et.text.toString())
|
||||
}
|
||||
.show()
|
||||
}
|
||||
binding.disableButton.setOnClickListener {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.disable_sync)
|
||||
.setMessage(R.string.disable_sync_description)
|
||||
.setPositiveButton(R.string.disable) { _, _ ->
|
||||
onDisableSync()
|
||||
}
|
||||
.setNegativeButton(R.string.keep_enabled) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
setContentView(binding.root)
|
||||
}
|
||||
|
||||
private fun onGenerateCode() {
|
||||
showCodeScreen()
|
||||
}
|
||||
|
||||
private fun onEnterCode(code: String) {
|
||||
showCodeScreen()
|
||||
}
|
||||
|
||||
private fun onDisableSync() {
|
||||
showIntroScreen()
|
||||
}
|
||||
|
||||
private fun showCodeScreen() {
|
||||
binding.introGroup.visibility = View.GONE
|
||||
binding.codeGroup.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun showIntroScreen() {
|
||||
binding.introGroup.visibility = View.VISIBLE
|
||||
binding.codeGroup.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
@@ -21,16 +21,11 @@ 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
|
||||
@@ -92,20 +87,6 @@ class PendingIntentFactory
|
||||
)
|
||||
.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,
|
||||
reminderTime: Long?,
|
||||
@@ -146,6 +127,25 @@ class PendingIntentFactory
|
||||
FLAG_IMMUTABLE or 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_IMMUTABLE or FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
fun updateWidgets(): PendingIntent =
|
||||
getBroadcast(
|
||||
context,
|
||||
@@ -155,56 +155,4 @@ class PendingIntentFactory
|
||||
},
|
||||
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,6 +25,7 @@ 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
|
||||
@@ -112,7 +113,7 @@ class AndroidNotificationTray
|
||||
val enterAction = Action(
|
||||
R.drawable.ic_action_check,
|
||||
context.getString(R.string.enter),
|
||||
pendingIntents.showNumberPicker(habit, timestamp)
|
||||
pendingIntents.setNumericalValue(context, habit, 0, null)
|
||||
)
|
||||
|
||||
val wearableBg = decodeResource(context.resources, R.drawable.stripe)
|
||||
@@ -149,6 +150,9 @@ class AndroidNotificationTray
|
||||
if (!disableSound)
|
||||
builder.setSound(ringtoneManager.getURI())
|
||||
|
||||
if (preferences.shouldMakeNotificationsLed())
|
||||
builder.setLights(Color.RED, 1000, 1000)
|
||||
|
||||
if (SDK_INT < Build.VERSION_CODES.S) {
|
||||
val snoozeAction = Action(
|
||||
R.drawable.ic_action_snooze,
|
||||
@@ -168,12 +172,14 @@ class AndroidNotificationTray
|
||||
fun createAndroidNotificationChannel(context: Context) {
|
||||
val notificationManager = context.getSystemService(Activity.NOTIFICATION_SERVICE)
|
||||
as NotificationManager
|
||||
val channel = NotificationChannel(
|
||||
REMINDERS_CHANNEL_ID,
|
||||
context.resources.getString(R.string.reminder),
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
)
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
if (SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
REMINDERS_CHANNEL_ID,
|
||||
context.resources.getString(R.string.reminder),
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
)
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,8 @@ 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)
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ 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.
|
||||
@@ -95,6 +96,15 @@ 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()
|
||||
@@ -116,6 +126,7 @@ 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
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
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)
|
||||
|
||||
fun Dialog.dismissCurrentAndShow() {
|
||||
currentDialog.get()?.dismiss()
|
||||
currentDialog = WeakReference(this)
|
||||
show()
|
||||
}
|
||||
|
||||
fun DialogFragment.dismissCurrentAndShow(fragmentManager: FragmentManager, tag: String) {
|
||||
currentDialog.get()?.dismiss()
|
||||
show(fragmentManager, tag)
|
||||
fragmentManager.executePendingTransactions()
|
||||
currentDialog = WeakReference(this.dialog)
|
||||
}
|
||||
@@ -21,11 +21,19 @@ package org.isoron.uhabits.utils
|
||||
import android.app.Activity
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.view.WindowManager
|
||||
|
||||
object SystemUtils {
|
||||
val isAndroidOOrLater: Boolean
|
||||
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||
|
||||
fun unlockScreen(activity: Activity) {
|
||||
val km = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||
km.requestDismissKeyguard(activity, null)
|
||||
if (isAndroidOOrLater) {
|
||||
val km = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||
km.requestDismissKeyguard(activity, null)
|
||||
} else {
|
||||
activity.window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
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
|
||||
@@ -28,14 +27,11 @@ 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
|
||||
@@ -206,10 +202,10 @@ 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) {
|
||||
fun View.drawNotesIndicator(canvas: Canvas, color: Int, size: Float, hasNotes: Boolean) {
|
||||
val pNotesIndicator = Paint()
|
||||
pNotesIndicator.color = color
|
||||
if (notes.isNotBlank()) {
|
||||
if (hasNotes) {
|
||||
val cy = 0.8f * size
|
||||
canvas.drawCircle(width.toFloat() - cy, cy, 8f, pNotesIndicator)
|
||||
}
|
||||
@@ -217,21 +213,3 @@ fun View.drawNotesIndicator(canvas: Canvas, color: Int, size: Float, notes: Stri
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ package org.isoron.uhabits.widgets
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.isoron.platform.gui.toInt
|
||||
import org.isoron.uhabits.core.models.Entry
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
@@ -41,12 +43,13 @@ open class CheckmarkWidget(
|
||||
|
||||
override fun getOnClickPendingIntent(context: Context): PendingIntent? {
|
||||
return if (habit.isNumerical) {
|
||||
pendingIntentFactory.showNumberPicker(habit, DateUtils.getToday())
|
||||
pendingIntentFactory.setNumericalValue(context, habit, 10, null)
|
||||
} else {
|
||||
pendingIntentFactory.toggleCheckmark(habit, null)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
override fun refreshData(widgetView: View) {
|
||||
(widgetView as CheckmarkWidgetView).apply {
|
||||
val today = DateUtils.getTodayWithOffset()
|
||||
|
||||
@@ -49,7 +49,6 @@ class FrequencyWidget(
|
||||
(widgetView.dataView as FrequencyChart).apply {
|
||||
setFirstWeekday(firstWeekday)
|
||||
setColor(WidgetTheme().color(habit.color).toInt())
|
||||
setIsNumerical(habit.isNumerical)
|
||||
setFrequency(habit.originalEntries.computeWeekdayFrequency(habit.isNumerical))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,10 +73,6 @@ class StackWidget(
|
||||
StackWidgetType.getStackWidgetAdapterViewId(widgetType),
|
||||
StackWidgetType.getStackWidgetEmptyViewId(widgetType)
|
||||
)
|
||||
remoteViews.setPendingIntentTemplate(
|
||||
StackWidgetType.getStackWidgetAdapterViewId(widgetType),
|
||||
StackWidgetType.getPendingIntentTemplate(pendingIntentFactory, widgetType, habits)
|
||||
)
|
||||
return remoteViews
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,14 +29,11 @@ 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.DateUtils.Companion.getToday
|
||||
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 {
|
||||
@@ -57,6 +54,7 @@ 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 {
|
||||
@@ -87,26 +85,8 @@ internal class StackRemoteViewsFactory(private val context: Context, intent: Int
|
||||
}
|
||||
|
||||
override fun getViewAt(position: Int): RemoteViews? {
|
||||
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, getToday())
|
||||
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
|
||||
Log.i("StackRemoteViewsFactory", "getViewAt $position")
|
||||
return if (0 <= position && position < remoteViews.size) remoteViews[position] else null
|
||||
}
|
||||
|
||||
private fun constructWidget(
|
||||
@@ -151,6 +131,24 @@ 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,12 +18,7 @@
|
||||
*/
|
||||
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) {
|
||||
@@ -78,39 +73,5 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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, notes: String) {
|
||||
behavior.setValue(data.habit, data.timestamp, (newValue * 1000).toInt(), notes)
|
||||
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,
|
||||
entry.notes,
|
||||
today.toDialogDateString(),
|
||||
this
|
||||
).show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_SHOW_NUMERICAL_VALUE_ACTIVITY = "org.isoron.uhabits.ACTION_SHOW_NUMERICAL_VALUE_ACTIVITY"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
يساعدك Loop Habit Tracker على إنشاء عادات إيجابية طويلة المدى والحفاظ عليها في حياتك. تعطيكم الرسوم البيانية والإحصاءات التفصيلية صورة واضحة للكيفية التي تحسنت بها عاداتك مع مرور الوقت. التطبيق خالٍ تمامًا من الإعلانات ومفتوح المصدر ويحترم خصوصيتك.
|
||||
حلقة تعقب الحبوب تساعدك على إنشاء عادات إيجابية طويلة الأجل والحفاظ عليها في حياتك. تعطيكم الرسوم البيانية والإحصاءات التفصيلية صورة واضحة للكيفية التي تحسنت بها عاداتك مع مرور الوقت. التطبيق خالٍ تمامًا من الإعلانات ومفتوح المصدر ويحترم خصوصيتك.
|
||||
|
||||
<b>واجهة جميلة وبسيطة</b>
|
||||
يحتوي Loop على واجهة أنيقة وبسيطة وسهلة الاستخدام للغاية ، حتى للمستخدمين لأول مرة. تم تحسين التطبيق ليكون ذي سرعة عالية ، ويعمل التطبيق بشكل جيد حتى على الهواتف القديمة.
|
||||
|
||||