Merge branch 'dev' into feature/sync2

feature/sync2
Alinson S. Xavier 3 years ago
commit b2cb54e32b
Signed by: isoron
GPG Key ID: 0DA8E4B9E1109DCA

@ -7,59 +7,24 @@ on:
paths-ignore:
- '**.md'
jobs:
Build:
runs-on: ubuntu-latest
Test:
runs-on: self-hosted
steps:
- name: Check out source code
uses: actions/checkout@v1
- name: Install Java Development Kit 11
uses: actions/setup-java@v1
with:
java-version: 11
- name: Build Project
- name: Build project
run: ./build.sh build
- name: Upload Build Artifacts
uses: actions/upload-artifact@v2
with:
name: uhabits-android
path: uhabits-android/build/outputs/
AndroidTest:
needs: Build
runs-on: macOS-10.15
timeout-minutes: 60
strategy:
matrix:
api: [
23,
24,
25,
26,
27,
28,
# 29, # Crashes constantly, see: https://issuetracker.google.com/issues/159732638
# 30, # Not available yet
# 31, # Not available yet
]
steps:
- name: Check out source code
uses: actions/checkout@v1
- name: Run Android tests
run: ./build.sh android-tests-parallel 28 29 30 31 32 33
- name: Download Previously Built APK
uses: actions/download-artifact@v2
- name: Upload artifacts
if: always()
uses: actions/upload-artifact@v2
with:
name: uhabits-android
path: uhabits-android/build/outputs/
- name: Install flock
run: |
brew install util-linux
echo "/usr/local/opt/util-linux/bin" >> $GITHUB_PATH
- name: Run Android Tests
run: ./build.sh android-tests ${{ matrix.api }}
name: build
path: |
build/*log
uhabits-android/build/outputs

5
.gitignore vendored

@ -12,13 +12,8 @@
.idea
.secret
build
build/
captures
local.properties
node_modules
*xcuserdata*
*.sketch
/design
/releases
/screenshots
crowdin.yml

@ -1,16 +0,0 @@
#!/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

Binary file not shown.

@ -1,5 +1,42 @@
# 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.5.0"
id("com.android.application") version ("7.0.3") apply (false)
val kotlinVersion = "1.7.10"
id("com.android.application") version ("7.3.0-rc01") apply (false)
id("org.jetbrains.kotlin.android") version kotlinVersion apply (false)
id("org.jetbrains.kotlin.kapt") version kotlinVersion apply (false)
id("org.jetbrains.kotlin.android.extensions") version kotlinVersion apply (false)
id("org.jetbrains.kotlin.multiplatform") version kotlinVersion apply (false)
id("org.jlleitschuh.gradle.ktlint") version "10.2.0"
id("org.jlleitschuh.gradle.ktlint") version "11.0.0"
}
apply {

@ -26,6 +26,7 @@ GRADLE="./gradlew --stacktrace --quiet"
PACKAGE_NAME=org.isoron.uhabits
SDKMANAGER="${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager"
VERSION=$(grep versionName uhabits-android/build.gradle.kts | sed -e 's/.*"\([^"]*\)".*/\1/g')
BOOT_TIMEOUT=360
if [ -z $VERSION ]; then
echo "Could not parse app version from: uhabits-android/build.gradle.kts"
@ -69,8 +70,7 @@ core_build() {
# Android
# -----------------------------------------------------------------------------
# shellcheck disable=SC2016
android_test() {
android_setup() {
API=$1
AVDNAME=${AVD_PREFIX}${API}
@ -85,25 +85,63 @@ android_test() {
$AVDMANAGER delete avd --name $AVDNAME
log_info "Creating new Android virtual device (API $API)..."
(echo "y" | $SDKMANAGER --install "system-images;android-$API;default;x86_64") || return 1
(echo "y" | $SDKMANAGER --install "system-images;android-$API;google_apis;x86_64") || return 1
$AVDMANAGER create avd \
--name $AVDNAME \
--package "system-images;android-$API;default;x86_64" \
--package "system-images;android-$API;google_apis;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"
$ADB wait-for-device shell 'while [[ -z "$(getprop sys.boot_completed)" ]]; do echo Waiting...; sleep 1; done; input keyevent 82' || return 1
$ADB root || return 1
sleep 5
timeout $BOOT_TIMEOUT $ADB wait-for-device shell 'while [[ -z "$(getprop sys.boot_completed)" ]]; do echo Waiting...; sleep 1; done; input keyevent 82'
if [ $? -ne 0 ]; then
log_error "Emulator failed to boot after $BOOT_TIMEOUT seconds."
return 1
fi
log_info "Disabling animations..."
$ADB root || return 1
sleep 5
$ADB shell settings put global window_animation_scale 0 || return 1
$ADB shell settings put global transition_animation_scale 0 || return 1
$ADB shell settings put global animator_duration_scale 0 || return 1
@ -111,6 +149,24 @@ android_test() {
log_info "Acquiring wake lock..."
$ADB shell 'echo android-test > /sys/power/wake_lock' || return 1
}
android_boot() {
for attempt in {1..5}; do
android_boot_attempt $1 && return 0
sleep 5
done
log_error "Too many failed attempts. Aborting."
return 1
}
# shellcheck disable=SC2016
android_test() {
API=$1
AVDNAME=${AVD_PREFIX}${API}
android_boot $API || return 1
if [ -n "$RELEASE" ]; then
log_info "Installing release APK..."
$ADB install -r ${ANDROID_OUTPUTS_DIR}/apk/release/uhabits-android-release.apk || return 1
@ -122,14 +178,25 @@ android_test() {
$ADB install -r ${ANDROID_OUTPUTS_DIR}/apk/androidTest/debug/uhabits-android-debug-androidTest.apk || return 1
for size in medium large; do
log_info "Running $size instrumented tests..."
OUT_INSTRUMENT=${ANDROID_OUTPUTS_DIR}/instrument-${API}.txt
OUT_LOGCAT=${ANDROID_OUTPUTS_DIR}/logcat-${API}.txt
$ADB shell am instrument \
-r -e coverage true -e size $size \
-w ${PACKAGE_NAME}.test/androidx.test.runner.AndroidJUnitRunner \
| tee $OUT_INSTRUMENT
if grep "\(INSTRUMENTATION_STATUS_CODE.*-1\|FAILURES\|ABORTED\|onError\|Error type\|crashed\)" $OUT_INSTRUMENT; then
FAILED_TESTS=""
for i in {1..5}; do
log_info "Running $size instrumented tests (attempt $i)..."
$ADB shell am instrument \
-r -e coverage true -e size "$size" $FAILED_TESTS \
-w ${PACKAGE_NAME}.test/androidx.test.runner.AndroidJUnitRunner \
| ts "%.s" | tee "$OUT_INSTRUMENT"
FAILED_TESTS=$(tools/parseInstrument.py "$OUT_INSTRUMENT")
SUCCESS=$?
if [ $SUCCESS -eq 0 ]; then
log_info "$size tests passed."
break
fi
done
if [ $SUCCESS -ne 0 ]; then
log_error "Some $size instrumented tests failed."
log_error "Saving logcat: $OUT_LOGCAT..."
$ADB logcat -d > $OUT_LOGCAT
@ -138,13 +205,14 @@ 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
@ -152,12 +220,27 @@ android_test_parallel() {
if android_test $API 1>$LOG 2>&1; then
log_info "API $API: Passed"
else
log_error "API $API: Failed. See $LOG for more details."
log_error "API $API: Failed"
fi
pkill -9 -f ${AVD_PREFIX}${API}
)&
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
wait
return $RET_CODE
}
android_build() {
@ -229,12 +312,14 @@ CI/CD script for Loop Habit Tracker.
Usage:
build.sh build [options]
build.sh android-setup <API>
build.sh android-tests <API> [options]
build.sh android-tests-parallel <API> <API>... [options]
build.sh android-accept-images [options]
Commands:
build Build the app and run small tests
android-setup Create Android virtual machine
android-tests Run medium and large Android tests on an emulator
android-tests-parallel Tests multiple API levels simultaneously
android-accept-images Copy fetched images to corresponding assets folder
@ -270,18 +355,17 @@ main() {
core_build
android_build
;;
android-setup)
shift; _parse_opts "$@"
android_setup $1
;;
android-tests)
shift; _parse_opts "$@"
if [ -z $1 ]; then
_print_usage
exit 1
fi
for attempt in {1..5}; do
log_info "Running Android tests (attempt $attempt)..."
android_test $1 && return 0
done
log_error "Maximum number of attempts reached. Failing."
return 1
android_test $1
;;
android-tests-parallel)
shift; _parse_opts "$@"

@ -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 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 feel 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 -XX:MaxPermSize=2048m
org.gradle.jvmargs=-Xms2048m -Xmx2048m
android.useAndroidX=true
android.enableJetifier=true

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

@ -1,2 +0,0 @@
out/
.sass-cache

@ -1,27 +0,0 @@
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

@ -1,27 +0,0 @@
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 +0,0 @@
<meta http-equiv="Refresh" content="0; url='https://github.com/iSoron/uhabits/discussions/689'" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

@ -1,106 +0,0 @@
!!! 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&trade; 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&ndash;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")

@ -1,104 +0,0 @@
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

@ -1,32 +0,0 @@
!!! 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}

@ -1,14 +0,0 @@
## 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.

@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""
Android Instrumentation Test Parser
Given a raw Android Instrumentation log (produced by "adb shell am instrument -r ...") this script
return zero if all tests pass and non-zero if some tests fail. In case of failure, this script
also prints arguments that, if passed to "am instrument", will cause it to re-run just the tests
that failed. This script additionally prints warnings about the tests on the STDERR; e.g. slow tests.
"""
import sys
import re
STATUS_START = 1
STATUS_DISABLED = -3
SLOW_TEST_THRESHOLD = 5.0
COLOR_RED = '\033[91m'
COLOR_YELLOW = '\033[93m'
COLOR_END = '\033[0m'
def error(msg):
sys.stderr.write("%s%s%s\n" % (COLOR_RED, msg, COLOR_END))
def warning(msg):
sys.stderr.write("%s%s%s\n" % (COLOR_YELLOW, msg, COLOR_END))
log_filename = sys.argv[1]
current_class, current_method = None, None
failed_tests = []
am_args = "-e class "
exit_code = 1
for line in open(log_filename).readlines():
matches = re.findall('^([0-9.]*)', line)
current_time = float(matches[0])
matches = re.findall('INSTRUMENTATION_STATUS: class=(.*)', line)
if len(matches) > 0:
current_class = matches[0]
matches = re.findall('INSTRUMENTATION_STATUS: test=(.*)', line)
if len(matches) > 0:
current_method = matches[0]
matches = re.findall('OK \([0-9]* tests?\)', line)
if len(matches) > 0:
exit_code = 0
matches = re.findall('INSTRUMENTATION_STATUS_CODE: ([-0-9]*)', line)
if len(matches) > 0:
status_code = int(matches[0])
if (status_code < 0) and (status_code != STATUS_DISABLED):
am_args += f"{current_class}#{current_method},"
failed_tests.append(f"{current_class}#{current_method}")
if status_code == STATUS_START:
initial_time = current_time
else:
elapsed_time = current_time - initial_time
if(elapsed_time > SLOW_TEST_THRESHOLD):
warning("SLOW %s#%s (%.2f seconds)" % (current_class, current_method, elapsed_time))
if len(failed_tests) > 0:
for test in failed_tests:
error("FAIL %s" % test)
print(am_args[:-1])
sys.exit(exit_code)

@ -1,377 +1,415 @@
Name,Languages,"Translated (Words)","Target Words","Approved (Words)",Voted,"""+"" votes received","""-"" votes received","Winning (Words)",Joined
"Alinson Xavier (iSoron)","Portuguese, Brazilian; Japanese; Spanish; Portuguese; Italian; Chinese Simplified; French; Hungarian; German; Arabic; Hindi; Slovenian; Catalan; Greek; Korean; Bulgarian; Chinese Traditional; Polish; Russian; Serbian (Cyrillic); Turkish; Ukrainian; Czech; Indonesian; Croatian; Danish; Dutch; Romanian; Swedish; Basque; Persian; Finnish; Vietnamese; Telugu; Tamil; Afrikaans; Esperanto; Hebrew",14808,17227,1282,0,1779,80,4274,"2016-03-05 18:35:27"
"Alinson Xavier (iSoron)","Portuguese, Brazilian; Japanese; Chinese Simplified; Italian; Spanish; Portuguese; French; Hungarian; Chinese Traditional; Turkish; Russian; Polish; Arabic; German; Korean; Greek; Catalan; Bulgarian; Hindi; Slovenian; Ukrainian; Serbian (Cyrillic); Czech; Indonesian; Croatian; Danish; Dutch; Romanian; Swedish; Basque; Persian; Finnish; Vietnamese; Tamil; Telugu; Hebrew; Esperanto; Norwegian; Afrikaans; Slovak; Armenian; Serbian (Latin); Uyghur",15497,18825,1308,0,1896,84,4315,"2016-03-05 18:35:27"
"Slobodan Simić (Слободан Симић) (slsimic)","Serbian (Latin); Serbian (Cyrillic)",2054,1831,2114,12,33,0,1991,"2021-02-03 14:26:07"
"Oglaigh Rystard (oglaignaheireann)","Ukrainian; Portuguese; Catalan; Greek; Basque; Romanian; Italian",1103,1037,1327,1,13,6,954,"2017-03-31 09:13:19"
dukelc,Slovak,1046,993,0,0,0,0,0,"2020-08-27 14:02:41"
"David (Cliff122)",Swedish,1040,1019,725,6,0,0,700,"2020-01-21 13:56:55"
"Omer I.S. (omeritzics)",Hebrew,1000,900,1097,14,1,0,946,"2020-10-11 20:10:51"
dukelc,Slovak,919,880,0,0,0,0,0,"2020-08-27 14:02:41"
"Intan Ayunda (Intan_Ayunda)",Indonesian,800,793,962,0,0,0,711,"2020-10-14 07:51:58"
"Omer I.S. (omeritzics)",Hebrew,1040,927,1122,14,1,0,975,"2020-10-11 20:10:51"
"Intan Ayunda (Intan_Ayunda)",Indonesian,818,811,985,0,0,0,729,"2020-10-14 07:51:58"
"Mihail Stefanov (MStefanov)",Bulgarian,755,794,3,0,2,0,2,"2017-03-31 16:09:02"
KMakoto,"Chinese Traditional",745,1146,949,0,0,0,745,"2019-10-22 04:19:52"
"Evren (evrenkiymaz)",Turkish,688,604,0,71,5,1,0,"2020-10-04 03:39:16"
"Evren (evrenkiymaz)",Turkish,688,604,0,71,28,22,0,"2020-10-04 03:39:16"
andaryon,Czech,681,606,0,108,0,0,0,"2021-11-25 10:20:45"
"Antti Kallio (antti.kallio)",Finnish,668,539,0,5,0,0,0,"2021-07-03 05:54:44"
"David Nos (david.nos)","Catalan; Spanish",667,731,0,0,1,0,0,"2020-01-04 10:15:36"
"Antti Kallio (antti.kallio)",Finnish,650,525,0,0,0,0,0,"2021-07-03 05:54:44"
androide74,Italian,644,659,0,2,0,0,0,"2020-02-06 15:46:28"
androide74,Italian,662,681,0,2,0,0,0,"2020-02-06 15:46:28"
Osoitz,Basque,655,595,0,9,0,0,3,"2018-01-23 14:07:47"
"Dmitriy Bogdanov (di72nn)",Russian,643,589,1197,0,36,0,515,"2017-03-31 10:00:48"
Tomairuka,Japanese,633,1636,909,43,0,0,564,"2020-12-12 12:14:22"
"Dmitriy Bogdanov (di72nn)",Russian,625,572,1197,0,36,0,515,"2017-03-31 10:00:48"
reyhoon,Persian,624,759,0,1,3,1,0,"2020-10-01 18:17:23"
Osoitz,Basque,610,545,0,9,0,0,3,"2018-01-23 14:07:47"
"Saeed Esmaili (saaeed.es20)",Persian,568,774,0,5,4,0,0,"2020-11-26 15:41:15"
"Saeed Esmaili (saaeed.es20)",Persian,586,795,0,5,4,0,0,"2020-11-26 15:41:15"
fabian.bouchal,German,548,527,0,6,0,3,72,"2020-01-07 06:43:37"
boban77,Czech,509,461,0,2,0,0,0,"2020-04-30 13:18:24"
"Yoav Argov (YoavArgov)",Hebrew,501,461,0,0,1,8,103,"2017-04-28 07:23:01"
"Isti (eisti)",Hungarian,528,476,0,0,0,0,0,"2020-12-03 12:02:51"
boban77,Czech,509,461,0,2,29,0,0,"2020-04-30 13:18:24"
"Martim Parente (martimparente)",Portuguese,505,542,0,38,0,0,0,"2020-08-26 10:22:11"
"Yoav Argov (YoavArgov)",Hebrew,501,461,0,0,1,8,91,"2017-04-28 07:23:01"
REMOVED_USER,Norwegian,501,498,501,0,148,0,501,"2017-07-05 19:02:25"
"Martim Parente (Sharlimar)",Portuguese,497,534,0,38,0,0,0,"2020-08-26 10:22:11"
"chrrris1987 (Chrrris1987)",Dutch,467,478,0,23,0,0,0,"2020-02-03 05:26:04"
"Huy Ngo (huyngo)",Vietnamese,461,695,0,1,0,0,0,"2020-01-26 11:58:36"
"黄克 (hk13127)","Chinese Simplified",461,765,0,1,0,0,24,"2020-01-17 23:16:03"
"Arkadiusz Bubak (epitek)",Polish,458,416,29,24,0,3,0,"2020-11-05 05:11:58"
"Huy Ngo (huyngo)",Vietnamese,461,695,0,1,0,0,0,"2020-01-26 11:58:36"
"Arkadiusz Bubak (epitek)",Polish,458,416,52,24,9,4,0,"2020-11-05 05:11:58"
marco.baturan,Esperanto,452,452,0,0,0,0,0,"2020-06-23 02:49:46"
"Sief Tarek (sieftarek135)",Arabic,447,455,0,0,0,0,0,"2021-02-07 14:35:21"
"Alparslan Şakçi (sakci)",Turkish,436,372,0,118,1,0,0,"2022-01-14 12:03:11"
JY3,"Chinese Simplified",427,727,295,0,1,0,222,"2021-03-08 08:53:35"
"Samuel Guay (SamGuay)",French,426,486,0,6,0,0,0,"2020-06-25 07:14:38"
"Diana Karaseva (Sun_Dianka)",Russian,399,373,0,10,1,0,209,"2020-01-30 06:40:02"
"Alexander Jansson (dalecarlian)",Swedish,396,406,507,0,0,3,399,"2017-06-21 01:37:32"
luiandresgonzalez,Spanish,383,403,0,1,28,0,0,"2020-07-11 14:20:44"
"Thamara Andrade (tkcandrade)","Portuguese, Brazilian",380,387,0,0,1,0,252,"2020-01-09 19:35:48"
"Thamara Andrade (tkcandrade)","Portuguese, Brazilian",380,387,0,0,1,0,239,"2020-01-09 19:35:48"
"Sølv Ræven (soelvraeven)",Danish,370,370,0,0,0,0,0,"2020-11-28 16:46:18"
"Isti (eisti)",Hungarian,367,329,0,0,0,0,0,"2020-12-03 12:02:51"
"Anh Quân (dangquanuet)",Vietnamese,362,530,0,42,2,0,0,"2017-10-29 12:27:44"
gapszi,Hungarian,348,301,0,86,0,0,0,"2019-04-08 01:35:54"
JY3,"Chinese Simplified",345,585,278,0,1,0,207,"2021-03-08 08:53:35"
"Mahdi Nasiri (mahdi.nasiri)",Persian,343,465,0,39,3,1,0,"2017-07-14 09:17:25"
Seoyul,Korean,339,825,0,0,27,0,0,"2017-06-21 08:11:39"
"Magimai Prakasam (magimai)",Tamil,336,831,0,12,0,0,0,"2018-04-15 21:16:08"
"Michael Malak (MichaelKMalak)",Arabic,304,271,0,0,0,0,0,"2020-05-26 19:47:58"
"Michael Malak (MichaelKMalak)",Arabic,304,271,0,0,1,0,0,"2020-05-26 19:47:58"
Blinkin,Dutch,297,334,0,5,0,0,0,"2021-06-14 10:30:05"
"Elina Salminen (salminen.elina.m)",Finnish,297,227,0,0,0,0,0,"2021-01-06 01:28:57"
ayane.m,Japanese,292,863,0,1,3,0,22,"2019-11-20 03:28:26"
Blinkin,Dutch,284,318,0,1,0,0,0,"2021-06-14 10:30:05"
"Marius Teufelweich (teufelweich)",German,249,256,606,4,2,0,146,"2021-03-12 04:11:38"
ayane.m,Japanese,292,863,0,1,5,0,22,"2019-11-20 03:28:26"
"Marius Teufelweich (teufelweich)",German,267,272,611,4,13,1,146,"2021-03-12 04:11:38"
hypnotichemionus,"Chinese Simplified",249,430,0,0,8,0,19,"2020-03-08 01:46:25"
cobalt59,German,237,234,0,1,24,1,132,"2017-06-05 05:18:33"
"QWERT (lurenjia01)","Chinese Simplified",236,407,0,0,8,0,19,"2020-03-08 01:46:25"
beriain,Basque,234,235,0,0,2,0,0,"2017-03-31 15:42:28"
pnhpnh,Vietnamese,225,343,0,1,3,0,0,"2017-11-27 12:06:07"
"Dika Fitrian Dwi Putra (OsamuDazai)",Indonesian,221,215,0,0,0,0,48,"2020-07-13 04:40:27"
easyrepro,Telugu,214,297,0,0,0,0,0,"2020-06-12 12:52:10"
easyrepro,Telugu,214,297,0,0,4,0,0,"2020-06-12 12:52:10"
taras-ko,Ukrainian,211,183,0,1,4,0,19,"2017-10-26 16:52:22"
sojusnik,German,207,200,1,0,30,0,66,"2017-04-03 17:11:56"
axmed99,Ukrainian,203,177,0,40,0,0,53,"2021-04-01 03:56:20"
"Andrij Mizyk (andmizyk)",Ukrainian,204,178,0,40,0,0,53,"2021-04-01 03:56:20"
"Heru Yen (heruyen)",Indonesian,201,201,0,0,0,0,25,"2020-06-29 18:39:15"
"Vijaykumar Borkar (vjkumar)",Hindi,200,364,0,11,0,0,0,"2021-08-06 16:12:15"
Ishmaeel,Turkish,193,174,0,129,6,0,0,"2017-10-04 03:54:00"
_translator,French,199,227,0,11,0,0,0,"2021-07-06 07:54:12"
Ishmaeel,Turkish,193,174,0,129,17,6,0,"2017-10-04 03:54:00"
oscfd,Spanish,192,201,0,2,4,0,0,"2021-05-21 17:58:22"
bruhwut,Vietnamese,189,292,0,1,0,0,0,"2021-05-21 07:16:30"
"Aputsiak Niels Janussen (aputtu)",Danish,187,200,0,0,0,0,0,"2019-08-28 05:47:42"
_translator,French,181,206,0,11,0,0,0,"2021-07-06 07:54:12"
fbruna17,Danish,181,179,0,1,0,0,0,"2021-01-28 15:48:47"
Bryanx,Dutch,179,168,0,5,2,0,0,"2019-11-21 17:08:12"
"Omry Cohen (omrycohen)",Hebrew,175,156,0,1,0,0,33,"2021-01-18 07:33:23"
Bryanx,Dutch,174,165,0,5,0,0,0,"2019-11-21 17:08:12"
"Pierre GALIEGUE (pierre.galiegue)",French,171,194,0,24,4,0,0,"2020-08-16 11:41:35"
bruhwut,Vietnamese,171,268,0,1,0,0,0,"2021-05-21 07:16:30"
plitwin,Polish,168,151,0,2,31,0,49,"2021-01-20 06:18:37"
DionysosDV,Greek,165,153,0,0,0,0,0,"2021-02-27 19:05:25"
"Gustavo Lima (GustavoLima)",Portuguese,158,177,0,1,4,10,0,"2020-08-26 10:35:05"
oscfd,Spanish,155,166,0,1,4,0,0,"2021-05-21 17:58:22"
plitwin,Polish,145,128,0,1,16,0,26,"2021-01-20 06:18:37"
"Ravi Rami (ramiravi)",Hindi,151,248,0,0,0,0,0,"2021-10-10 09:19:40"
"Lương Vĩnh Khang (LuongVinhKhang)",Vietnamese,144,256,0,0,46,1,0,"2017-08-10 10:05:58"
azzamsa,Indonesian,142,136,0,48,0,1,26,"2017-06-16 18:29:45"
"yoding (yodingc)","Chinese Traditional; Chinese Simplified",141,271,0,10,0,0,0,"2021-07-07 01:45:45"
"Neysa Nasywa (neysanasywa)",Indonesian,140,141,0,0,0,0,60,"2020-11-18 10:32:10"
mohmans,Arabic,139,141,0,12,1,0,0,"2020-11-23 02:48:00"
"Eilif Adelvice (adelvice)",Spanish,139,154,0,96,1,0,0,"2021-08-05 07:20:21"
"Mohammed Imthath (mimthath4)",Tamil,136,274,0,0,11,0,0,"2018-02-15 22:41:15"
carllacan,Catalan,134,155,0,2,0,0,0,"2021-11-13 13:12:07"
roptat,French,132,154,0,112,89,5,0,"2017-04-19 16:54:47"
"Trần Thái (tranhoangthai2001)",Vietnamese,127,186,0,8,1,0,0,"2018-03-01 10:51:39"
"OP Smosher (teenwolffan44)","Serbian (Cyrillic)",124,122,0,0,0,0,18,"2020-11-05 09:41:35"
4001982248998,Esperanto,122,119,0,0,0,0,0,"2017-10-08 04:13:02"
"StoP4Me (Lcqp)",Romanian,121,119,0,0,3,0,0,"2018-05-06 18:51:59"
alalloush,Arabic,118,129,0,2,2,0,0,"2017-03-31 12:37:17"
"Eilif Adelvice (adelvice)",Spanish,116,126,0,96,0,0,0,"2021-08-05 07:20:21"
Sebastian05067,Spanish,114,133,0,55,28,0,0,"2017-05-14 00:48:16"
alalloush,Arabic,118,129,0,2,14,3,0,"2017-03-31 12:37:17"
"Tanya (MagicUnderHood)",Russian,114,98,0,19,0,0,54,"2019-04-21 10:44:03"
REMOVED_USER,Arabic,111,106,0,22,21,2,0,"2018-01-05 07:01:45"
mohmans,Arabic,109,103,0,2,0,0,0,"2020-11-23 02:48:00"
Sebastian05067,Spanish,114,133,0,55,28,0,0,"2017-05-14 00:48:16"
REMOVED_USER,Arabic,111,106,0,22,22,2,0,"2018-01-05 07:01:45"
"Iabin Arteaga (iabin)",Spanish,108,111,0,4,21,0,0,"2017-08-26 21:08:54"
"Ivan Krušlin (krux3r)",Croatian,108,122,503,0,0,0,108,"2017-03-31 09:15:24"
2kaafone,Finnish,105,90,0,0,0,0,0,"2019-08-12 06:58:48"
"Adam Jurkiewicz (hasztagg)",Polish,104,105,529,0,0,0,104,"2017-03-31 09:50:51"
"just a name bro (justanamebr0)",Danish,98,109,0,0,1,0,0,"2019-06-19 11:57:55"
"Nam Nguyen (namnl2706)",Vietnamese,95,137,0,0,0,0,0,"2020-08-18 23:02:33"
"손유정 (yuwon1213)",Korean,95,57,0,0,0,0,0,"2021-03-30 05:25:33"
"손유정 (yuwon1213)",Korean,95,57,0,0,1,0,0,"2021-03-30 05:25:33"
ranmagen,Hebrew,91,78,0,0,0,0,0,"2021-02-16 05:44:31"
LoneWanderer,"Chinese Traditional",90,137,0,4,0,0,0,"2020-09-29 05:24:48"
"Vo - (voyl)","Chinese Traditional",89,126,0,0,5,0,0,"2020-09-02 23:34:42"
ikkaz,Indonesian,89,84,0,5,0,0,4,"2019-09-02 19:58:54"
"Vo - (voyl)","Chinese Traditional",89,126,0,0,5,0,0,"2020-09-02 23:34:42"
"Irene K (Heaun)",Korean,88,75,0,25,0,0,0,"2020-03-16 11:31:12"
Prosta4ok_ua,Ukrainian,87,84,0,1,0,0,17,"2020-01-23 19:43:41"
"Kumar Anand (kumar0500)",Hindi,87,125,0,0,0,0,0,"2020-11-07 02:46:09"
"Ohad Edri (ohadalte)",Hebrew,85,79,0,0,1,3,18,"2020-07-04 03:42:09"
"Ohad Edri (ohadalte)",Hebrew,85,79,0,0,1,3,13,"2020-07-04 03:42:09"
helectron,Persian,84,102,0,1,0,0,0,"2021-03-02 04:10:51"
"Radu Cebotari (wildProgrammer)",Romanian,84,92,0,1,0,0,0,"2020-02-05 01:20:00"
"Bruces Lee (aplusbdesign)",Korean,82,66,0,0,0,0,0,"2021-08-23 11:27:18"
"Israa Z (sosozozo)",Arabic,79,87,0,43,14,0,3,"2017-11-27 14:10:50"
"Sofia Neves (sofiasonev)","Portuguese, Brazilian",79,84,0,1,0,0,46,"2020-03-12 18:19:46"
"Jacob Roller (jdr28070)",Korean,79,61,0,0,0,0,0,"2020-01-03 11:36:40"
"Jacob Roller (jdr28070)",Korean,79,61,0,0,1,0,0,"2020-01-03 11:36:40"
Tiralka,French,79,91,0,92,1,0,0,"2018-02-09 18:39:01"
"Israa Z (sosozozo)",Arabic,79,87,0,43,12,0,3,"2017-11-27 14:10:50"
"Toni Mustonen (toni.mustonen)",Finnish,78,72,0,0,0,0,0,"2017-09-02 05:34:12"
"Fauz Aladeem (topfauz)",Arabic,76,77,0,0,0,1,0,"2020-02-21 22:46:12"
"Toni Mustonen (toni.mustonen)",Finnish,78,72,0,0,5,0,0,"2017-09-02 05:34:12"
"Michael (quelbs)",German,76,75,0,1,0,0,39,"2020-08-18 07:39:26"
"Oliver Gronowski (OliverGronowski)",German,70,69,0,5,0,0,0,"2021-05-14 16:37:10"
"Fauz Aladeem (topfauz)",Arabic,76,77,0,0,0,1,0,"2020-02-21 22:46:12"
"Radoslaw Biernacki (radoslaw.biernacki)",Polish,70,74,0,56,1,0,1,"2020-12-15 17:55:31"
"Oliver Gronowski (OliverGronowski)",German,70,69,0,5,2,0,0,"2021-05-14 16:37:10"
RealDonald,Dutch,67,69,0,121,10,0,0,"2017-06-23 20:10:12"
sirekanyan,"Armenian; Russian",66,65,0,0,0,0,0,"2020-04-18 11:32:52"
"Константин К. (kocyak1991)",Russian,64,60,0,0,1,2,0,"2018-06-10 13:39:37"
"Laura Sophie (laurasophie20)",German,62,67,0,4,0,0,0,"2018-01-06 14:21:24"
"Alparslan Sakci (sakci)",Turkish,61,55,0,11,0,0,0,"2021-06-10 11:59:22"
raden20,Indonesian,61,62,177,0,1,0,64,"2017-04-09 22:04:23"
"Peter Williams (williamspete001)",Japanese,60,173,0,2,0,0,3,"2020-01-01 13:17:44"
"Jan Wojtecki (j4nw)",Polish,58,46,0,0,0,0,26,"2017-11-02 05:42:14"
"Deepak Bharathi (deepakbharathi1994)",Tamil,56,107,0,0,11,4,0,"2017-09-17 08:00:31"
"Peter Williams (williamspete001)",Japanese,55,147,0,2,0,0,3,"2020-01-01 13:17:44"
"Андрій Козицький (andriikozytskyi1108)",Ukrainian,52,52,0,0,1,0,0,"2018-10-22 01:45:08"
"Nil riera (nilriera2000)",Catalan,52,61,0,1,0,0,0,"2021-06-22 16:37:44"
"Nil riera (nilriera2000)",Catalan,52,61,0,1,2,0,0,"2021-06-22 16:37:44"
"Neoone (Neooneqq)",Romanian,51,54,0,0,0,0,0,"2022-05-05 20:42:11"
REMOVED_USER,Italian,51,52,0,2,0,0,0,"2017-08-21 05:15:31"
govindap,"Japanese; Hindi",51,114,0,6,1,0,0,"2020-06-02 20:15:52"
"Mare Geldenhuys (mare.geldenhuys)",Afrikaans,50,57,0,0,0,0,0,"2017-10-20 18:00:14"
"Mahmoud Magdy (M7moudManson)",Arabic,49,60,0,6,8,1,0,"2021-08-21 09:01:38"
"Behnood HRazy (behnoodhr)",Persian,49,70,0,0,0,0,0,"2017-11-25 10:57:21"
"tat bz (Tat_i)",German,48,56,0,55,0,0,27,"2021-03-26 05:12:54"
J3ll3nl,Dutch,48,48,0,0,17,1,3,"2017-03-31 11:56:09"
"Andrew Firnes (Anechan)",Russian,47,47,0,3,0,0,29,"2019-09-18 09:51:59"
andowero,Czech,47,38,0,0,0,0,0,"2020-01-20 02:29:01"
"tat bz (Tat_i)",German,48,56,0,55,0,1,27,"2021-03-26 05:12:54"
vach,Armenian,47,36,0,0,0,0,0,"2020-04-18 16:53:12"
"Andrew Firnes (Anechan)",Russian,47,47,0,3,0,0,29,"2019-09-18 09:51:59"
andowero,Czech,47,38,0,0,3,0,0,"2020-01-20 02:29:01"
"Rahul Shishodia (rahul.shishodia.10)",Hindi,46,85,0,6,5,1,0,"2018-12-24 22:18:19"
"Coni Ragni (coni2ragnii)",Spanish,46,46,0,0,0,0,0,"2021-02-28 20:18:37"
Cp0204,"Chinese Simplified",45,72,0,0,0,0,0,"2019-08-20 11:04:27"
"cc (cavaz)",Italian,44,41,0,0,0,0,0,"2017-04-01 04:21:08"
"Boban Jagertraum (boban40)",Czech,43,38,0,2,1,1,0,"2017-03-31 09:39:16"
"Kamil Dziadek (prso94)",Polish,43,39,0,0,2,0,0,"2020-04-06 17:12:06"
"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"
"Me Me (gentelwom)",Arabic,42,40,0,0,0,0,0,"2020-11-08 20:44:01"
"Ali Elsheikh (aelsheikh1987)",Arabic,42,41,0,0,0,0,0,"2021-06-16 10:17:26"
"Balázs Keresztury (belidzs)",Hungarian,42,41,501,0,7,0,38,"2017-04-06 02:40:24"
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"
"Mateusz Duda (MateuszDuda)",Polish,42,42,0,0,6,0,0,"2021-08-17 11:27:11"
"Ali Elsheikh (aelsheikh1987)",Arabic,42,41,0,0,0,0,0,"2021-06-16 10:17:26"
"Ali Zali (stm19951995)",Persian,40,60,0,0,0,0,0,"2020-03-23 19:57:26"
"Limin Lu (liminlu)","Chinese Simplified",39,79,503,0,0,0,39,"2017-03-31 09:49:35"
"Sofia Veijonen (Suklaa) (sofia.veijonen)",Finnish,40,33,0,0,0,0,0,"2018-03-07 09:24:22"
dusanstrgar,Slovenian,39,41,0,0,0,0,0,"2017-03-31 10:30:28"
"Limin Lu (liminlu)","Chinese Simplified",39,79,503,0,0,0,39,"2017-03-31 09:49:35"
Anshoe,Tamil,38,65,0,14,0,0,0,"2018-01-02 11:06:52"
anasshm,Arabic,37,36,0,9,0,0,0,"2019-01-27 04:07:22"
hrexen,Armenian,37,37,0,0,0,0,0,"2020-12-09 02:30:34"
"Abdulrahman (D7M)",Arabic,36,39,0,0,0,0,0,"2020-01-29 18:55:30"
"Maria Chushnyakova (maria.ch)",Russian,36,31,0,3,0,0,0,"2021-08-17 03:23:58"
REMOVED_USER,Swedish,36,33,0,5,1,0,0,"2018-09-29 17:47:33"
xphsis,Basque,36,31,0,0,0,0,0,"2022-01-02 08:16:19"
"Maria Chushnyakova (maria.ch)",Russian,36,31,0,3,0,0,0,"2021-08-17 03:23:58"
"長谷川知里 (chase0213)",Japanese,34,138,0,13,0,0,24,"2018-12-14 10:52:44"
"Piotr Łuczyński (peterluczynski)",Polish,33,30,0,6,10,0,2,"2020-01-29 07:27:40"
"Luis E. Perichon (luisperichon)",Spanish,33,40,0,104,0,0,0,"2017-09-04 13:46:06"
"Piotr Łuczyński (peterluczynski)",Polish,33,30,0,6,5,0,2,"2020-01-29 07:27:40"
"milad farahani (miladfarmahini90)",Persian,33,44,0,18,1,0,3,"2017-08-31 16:09:00"
JoeLi,"Chinese Traditional",31,70,0,12,0,0,24,"2017-06-25 05:32:48"
andriikozytskyi2625,Ukrainian,31,23,0,0,0,0,0,"2019-07-08 00:16:41"
REMOVED_USER,Russian,31,30,0,2,4,0,3,"2018-12-03 23:55:47"
Moastafa,Arabic,31,25,0,0,0,0,0,"2020-07-06 11:37:53"
"hamza gamal (hamzagamal4444)",Arabic,31,28,0,0,0,0,0,"2020-08-03 15:23:34"
REMOVED_USER,Russian,31,30,0,2,4,0,3,"2018-12-03 23:55:47"
JoeLi,"Chinese Traditional",31,70,0,12,0,0,24,"2017-06-25 05:32:48"
yancyn,"Chinese Simplified",30,40,0,0,0,0,1,"2020-05-18 20:06:03"
"비니몬youtube (khj01025276475)",Korean,29,25,0,0,0,0,0,"2020-02-09 20:44:35"
"Ruud Schouten (ruudschouten)",Dutch,29,32,0,41,3,0,0,"2017-07-22 17:49:17"
"Aaron Dalton (Perlkonig)",French,26,25,0,141,1,0,0,"2018-01-14 12:58:19"
"비니몬youtube (khj01025276475)",Korean,29,25,0,0,0,0,0,"2020-02-09 20:44:35"
avelneve,Indonesian,29,28,0,0,0,0,0,"2022-04-13 13:26:10"
"Niraj Yadav (neverforgetniraj)",Hindi,26,48,0,0,0,0,0,"2017-04-11 02:26:50"
"Guillaume Collic (gcollic)",French,26,28,0,126,11,0,0,"2017-05-05 16:13:00"
"Radoslaw Biernacki (radoslaw.biernacki)",Polish,26,24,0,8,0,0,1,"2020-12-15 17:55:31"
"Aaron Dalton (Perlkonig)",French,26,25,0,141,1,0,0,"2018-01-14 12:58:19"
"Jonny I (jonny99dj)",Italian,26,26,0,5,0,0,0,"2017-10-07 07:35:34"
"Guillaume Collic (gcollic)",French,26,28,0,126,11,0,0,"2017-05-05 16:13:00"
Pan_Filuta,Czech,25,21,0,5,8,0,3,"2017-04-29 12:55:14"
"Eddie (eddieattaboy)","Chinese Traditional",25,34,0,1,0,0,0,"2020-11-04 21:48:05"
Pan_Filuta,Czech,25,21,0,5,4,0,3,"2017-04-29 12:55:14"
"eduard83 (barbany.eduard)",Catalan,24,25,0,2,0,0,0,"2019-06-26 14:59:47"
"A Aa (ylayzlmimashisafyoutub)",Arabic,23,33,0,34,1,1,0,"2021-09-27 15:34:26"
"Caner Başaran (basarancaner)",Turkish,23,21,0,0,26,1,0,"2017-04-09 06:34:59"
"Ľuboš Čaky (lubos.caky)",Slovak,23,22,0,0,0,0,0,"2019-07-02 16:51:44"
"Caner Başaran (basarancaner)",Turkish,23,21,0,0,21,0,0,"2017-04-09 06:34:59"
hodanli,Turkish,22,26,0,0,1,0,0,"2017-11-03 14:33:41"
gnu-ewm,Polish,22,23,0,6,0,0,0,"2021-02-24 03:42:01"
"Neeraj Verma (verma.neeraj.in)",Hindi,22,37,0,0,1,0,0,"2018-07-23 07:16:41"
gnu-ewm,Polish,22,23,0,6,2,0,0,"2021-02-24 03:42:01"
hodanli,Turkish,22,26,0,0,1,0,0,"2017-11-03 14:33:41"
"Alcarkse (alexis.brusle)",French,21,25,0,7,11,0,0,"2017-08-06 09:32:29"
olbotta,Italian,20,25,0,2,0,0,0,"2021-06-06 04:22:55"
"Shashwat (goforgold)",Hindi,20,33,0,0,0,0,0,"2020-05-17 10:34:42"
"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"
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"
"İ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"
KenKailer,Arabic,19,25,0,0,0,0,0,"2022-05-10 06:16:54"
"İsa Eş (IsaEs)",Turkish,19,17,0,0,6,2,0,"2017-06-20 07:30:22"
"Magdalena Urbańczyk (madziia139)",Polish,19,19,0,0,0,0,0,"2017-10-21 03:01:04"
sheeCesu,French,19,18,0,48,4,0,0,"2017-12-21 17:01:39"
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"
"Ceara Lopez (cealopez)",Spanish,17,18,0,0,5,1,0,"2017-08-22 22:56:13"
takoyakibento,Korean,17,13,0,3,0,0,0,"2020-08-01 08:44:15"
Adeline31,French,17,20,0,3,0,0,0,"2019-12-06 00:00:11"
engineeringforgood,Russian,16,15,0,0,0,0,16,"2021-01-22 03:32:35"
"Ceara Lopez (cealopez)",Spanish,17,18,0,0,5,1,0,"2017-08-22 22:56:13"
bretzel15,German,16,20,0,0,0,0,0,"2020-04-06 02:49:14"
"Şamil Ateşoğlu (m.samilatesoglu)",Turkish,16,22,0,11,6,3,0,"2017-07-05 18:37:08"
DebatablySane,Bulgarian,16,15,0,48,0,0,0,"2017-07-10 15:13:18"
"Şamil Ateşoğlu (m.samilatesoglu)",Turkish,16,22,0,11,6,3,0,"2017-07-05 18:37:08"
engineeringforgood,Russian,16,15,0,0,0,0,16,"2021-01-22 03:32:35"
"Bhava Tharini (bhavidanush)",Tamil,15,37,0,0,0,0,0,"2019-10-09 05:43:11"
"Maro Chr (caprisunglasses)",Greek,14,17,0,0,0,0,0,"2021-08-17 06:53:33"
"Zeynep Esen (nezihaesen50)",Turkish,14,13,0,0,0,0,0,"2020-01-28 07:05:15"
"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"
"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"
"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"
"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"
"Uwe Mönks (schirinowski)",German,13,12,0,0,0,0,0,"2021-02-18 04:00:41"
"Dave (xdave)",Hungarian,13,11,0,0,0,0,0,"2020-03-02 20:56:50"
"Ana Kelly Vale (anakvale)","Portuguese, Brazilian",13,21,0,4,0,0,2,"2022-03-30 00:15:37"
GiorgioHerbie,Italian,13,15,0,0,0,0,0,"2022-01-17 17:35:40"
"Nenad Vukotic (vukotic.nenad)","Serbian (Cyrillic)",13,13,0,1,2,6,0,"2019-01-31 14:29:15"
soura2,Arabic,12,13,0,0,0,0,0,"2020-01-13 19:23:47"
"shreyas (techiespace)",Hindi,12,20,0,0,0,0,0,"2018-06-10 01:14:26"
"Sonu Sharma (riteetude)",Hindi,11,23,0,0,0,0,0,"2021-05-30 19:38:00"
Vmrc,French,11,12,0,2,0,0,0,"2020-11-02 05:35:06"
"Jo Chuang (josephch405)","Chinese Traditional",11,24,0,0,0,0,11,"2017-06-16 20:21:06"
"sathvic k (sathvictripleseven)",Telugu,10,17,0,0,0,0,0,"2020-09-11 08:11:32"
"Brian Camacho (bmcamacho)",Polish,10,11,0,0,1,0,0,"2020-08-03 02:27:28"
"Anonymous edgy nerd (yamentaad)",Arabic,10,13,0,1,0,0,0,"2018-05-06 09:23:57"
Vmrc,French,11,12,0,2,0,0,0,"2020-11-02 05:35:06"
"Ammar Naif (Ammar_Naif)",Arabic,11,11,0,4,0,0,0,"2022-01-15 05:16:41"
"Sonu Sharma (riteetude)",Hindi,11,23,0,0,0,0,0,"2021-05-30 19:38:00"
"Edwin van Rooij (edwinvrooij)",Dutch,10,13,0,17,0,0,0,"2018-11-05 03:59:10"
"Brian Camacho (bmcamacho)",Polish,10,11,0,0,1,1,0,"2020-08-03 02:27:28"
"Mihael Wagner (miha.wagner)",Slovenian,10,9,0,7,0,0,0,"2017-10-18 18:26:29"
"Hrant Hakobian (hrastgh1)",Armenian,10,9,0,0,0,0,0,"2021-08-29 15:22:10"
"sathvic k (sathvictripleseven)",Telugu,10,17,0,0,0,0,0,"2020-09-11 08:11:32"
"Ahmed Mosaad (ahmed.mosaad2018)",Arabic,10,12,0,6,0,0,0,"2021-02-03 18:45:43"
"Anonymous edgy nerd (yamentaad)",Arabic,10,13,0,1,0,0,0,"2018-05-06 09:23:57"
"Zesar Cebrián (Txorrota)",Spanish,10,44,0,0,0,0,0,"2022-02-09 01:34:32"
"Milan Siebenbürger (lennyd)",Czech,10,7,0,1,0,0,0,"2022-01-30 07:09:42"
"Suhaili Hassan (kucingsyg96)",Indonesian,9,10,0,0,0,0,0,"2018-06-10 11:55:09"
"Sourire Lucide (sourire_lucide)",Russian,9,10,0,0,1,0,0,"2018-03-22 01:37:55"
"Martin Vostatek (martinvostatek)",Czech,9,8,0,32,2,0,0,"2019-01-21 13:52:36"
"Seweryn Piotrowski (Draxxsx)",Polish,9,10,0,0,19,0,0,"2020-01-02 09:55:48"
"Sourire Lucide (sourire_lucide)",Russian,9,10,0,0,1,0,0,"2018-03-22 01:37:55"
"Jakob Weickmann (jweickm)",Japanese,8,21,0,0,0,0,0,"2021-10-05 11:10:25"
Rex123,Persian,8,8,0,0,0,0,0,"2017-07-01 00:47:42"
"Andrey ZaXeLoN (waragaa)",Russian,7,7,0,8,1,0,0,"2017-09-18 21:37:42"
"Konstantin (KZhidovinov)",Russian,7,7,0,0,0,0,0,"2020-01-29 13:35:12"
"Андрій Козицький (andriikozytskyi3807)",Ukrainian,7,12,0,2,0,0,0,"2020-09-26 20:31:56"
ftfoi,Norwegian,7,6,0,0,0,0,0,"2020-04-11 20:42:35"
"Vladimir Pavlychev (KeyJoo)",Russian,7,9,0,0,0,0,0,"2017-12-18 02:46:56"
"Vladimir Pavlychev (vovs03)",Russian,7,9,0,0,0,0,0,"2017-12-18 02:46:56"
"Felipe Chagas (chagretes)","Portuguese, Brazilian",7,8,0,0,3,0,5,"2022-01-10 12:20:25"
"Андрій Козицький (andriikozytskyi3807)",Ukrainian,7,12,0,2,0,0,0,"2020-09-26 20:31:56"
pkorove,Greek,7,7,0,0,0,0,0,"2020-03-07 11:36:12"
erfan2927,Persian,6,6,0,0,0,0,0,"2018-04-09 02:12:44"
"Burak Ceylan (7burakceylan)",Turkish,6,6,0,0,0,0,0,"2018-05-20 17:24:19"
ChloeLiang,Japanese,6,22,0,0,1,0,3,"2017-08-08 05:02:59"
"Sam (SorodonSorodon)",German,6,6,0,13,0,0,0,"2017-04-14 11:09:27"
"닉닉 (seohu9466)",Korean,6,14,0,13,0,0,0,"2017-10-09 23:08:15"
"Sarita Cajas (sarayanacajas)",Spanish,6,4,0,0,1,0,0,"2021-05-14 14:27:59"
ChloeLiang,Japanese,6,22,0,0,1,0,3,"2017-08-08 05:02:59"
"Manuel Tassi (Mannivu)",Italian,5,6,0,0,0,0,0,"2021-01-03 11:00:33"
"Tomáš Hrabáček (Hrabyyy)",Czech,5,3,0,0,0,0,0,"2021-05-27 11:58:11"
erfan2927,Persian,6,6,0,0,0,0,0,"2018-04-09 02:12:44"
"Burak Ceylan (7burakceylan)",Turkish,6,6,0,0,0,0,0,"2018-05-20 17:24:19"
andriikozytskyi2018,Ukrainian,5,5,0,0,0,0,0,"2017-09-03 05:24:43"
"Vitor Henrique (vitorhcl)","Portuguese, Brazilian",5,8,0,1,0,0,0,"2022-03-08 20:00:59"
"Matthias Joly (joly.matt12)",French,5,8,0,27,1,0,0,"2017-08-28 09:53:59"
"Tomáš Hrabáček (Hrabyyy)",Czech,5,3,0,0,1,0,0,"2021-05-27 11:58:11"
"Guerra Ivaneth (rossanaiva-04)",Spanish,5,7,0,0,0,0,0,"2019-02-03 16:48:59"
"Дмитрий Хапенков (d.khapenkov)",Russian,5,5,0,6,4,0,2,"2018-01-06 23:00:43"
"Matthias Joly (joly.matt12)",French,5,8,0,27,1,0,0,"2017-08-28 09:53:59"
"Micaela Pighin (micaelapiighin)",Spanish,5,6,0,1,0,0,0,"2019-10-09 23:32:42"
andriikozytskyi2018,Ukrainian,5,5,0,0,0,0,0,"2017-09-03 05:24:43"
marmo,German,4,4,0,0,0,0,0,"2021-01-13 01:16:35"
"Eli Besirov (elibesirov07)",Turkish,4,4,0,0,0,0,0,"2019-03-25 07:12:34"
"Lopo Isaac Fernández (rocapata)",Spanish,4,3,0,0,0,0,0,"2018-09-20 11:46:22"
bziuum,Polish,4,4,0,0,0,0,0,"2020-09-01 09:08:01"
"Manuel Tassi (Mannivu)",Italian,5,6,0,0,0,0,0,"2021-01-03 11:00:33"
"Neko123 (emandic11)","Serbian (Cyrillic)",4,4,0,57,0,0,0,"2021-04-21 15:33:29"
Magidxz,Arabic,3,3,0,0,0,0,0,"2021-01-05 05:02:54"
"mohammadali barati (mabaraty)",Persian,3,3,0,0,0,0,0,"2021-07-10 05:54:44"
"Lopo Isaac Fernández (rocapata)",Spanish,4,3,0,0,0,0,0,"2018-09-20 11:46:22"
"Eli Besirov (elibesirov07)",Turkish,4,4,0,0,0,0,0,"2019-03-25 07:12:34"
marmo,German,4,4,0,0,0,0,0,"2021-01-13 01:16:35"
bziuum,Polish,4,4,0,0,3,0,0,"2020-09-01 09:08:01"
"Craig Foobar (craig.foobar)",German,3,3,0,25,0,0,0,"2022-02-20 16:55:47"
Katarin,Ukrainian,3,3,0,0,0,0,0,"2022-03-17 14:44:59"
"Sarath S (CyberShark)",Tamil,3,7,0,0,0,0,0,"2020-08-27 22:43:16"
"Vagner Roberto (vagner.trompete)","Portuguese, Brazilian",3,3,0,0,0,0,0,"2017-12-30 17:54:26"
"Igor Piskun (i_piskun)",Ukrainian,3,3,0,0,0,0,0,"2018-01-19 15:20:27"
"Cláudio Bernardo (claudiobernardo.ti)","Portuguese, Brazilian",3,4,0,1,0,0,0,"2019-01-08 14:41:10"
"Unnie Here (Carb)",Hindi,3,8,0,0,0,0,0,"2020-03-18 23:34:35"
REMOVED_USER,"Portuguese, Brazilian",3,4,0,0,0,0,0,"2018-11-18 09:02:37"
"Thoum Ptrgnt (thomas.petrignet)",French,3,3,0,2,0,3,0,"2017-09-23 19:25:52"
"Oleg Kogut (kogut_oleg)",Ukrainian,3,3,0,0,0,0,0,"2018-12-28 14:31:02"
carsten_kafke,German,3,3,0,43,0,0,3,"2017-10-27 13:27:47"
"Vagner Roberto (vagner.trompete)","Portuguese, Brazilian",3,3,0,0,0,0,0,"2017-12-30 17:54:26"
"Igor Piskun (i_piskun)",Ukrainian,3,3,0,0,0,0,0,"2018-01-19 15:20:27"
"Andrea Bianchi (andreawhite1597)",Italian,3,1,0,1,0,0,0,"2018-01-21 17:45:48"
"Cláudio Bernardo (claudiobernardo.ti)","Portuguese, Brazilian",3,4,0,1,0,0,0,"2019-01-08 14:41:10"
"Hiohana Rilary (hiohanarilary)","Portuguese, Brazilian",3,4,0,0,0,0,0,"2019-07-31 20:42:20"
"joabe gabriel (joabegabrielcma1)","Portuguese, Brazilian",3,4,0,0,0,0,0,"2018-08-21 09:08:59"
Magidxz,Arabic,3,3,0,0,0,0,0,"2021-01-05 05:02:54"
"Péter Bernát (bernatp)",Hungarian,3,2,0,0,0,0,0,"2019-11-30 15:50:33"
"Martin Zimdahl (zimdahlmartin)",Swedish,3,2,0,0,1,0,3,"2018-09-15 04:39:22"
"joabe gabriel (joabegabrielcma1)","Portuguese, Brazilian",3,4,0,0,0,0,0,"2018-08-21 09:08:59"
"Gabriel Cavalcante (gabrielc.alves14)","Portuguese, Brazilian",3,4,0,0,0,0,0,"2018-08-06 22:24:54"
"Martin Zimdahl (zimdahlmartin)",Swedish,3,2,0,0,1,0,3,"2018-09-15 04:39:22"
atomjani,Hungarian,3,3,0,0,0,0,0,"2019-01-19 00:49:25"
"أم محمد تقي (souadboudia19)",Arabic,2,2,0,0,0,0,0,"2020-06-13 15:24:17"
"mohammadali barati (mabaraty)",Persian,3,3,0,0,0,0,0,"2021-07-10 05:54:44"
"Hiohana Rilary (hiohanarilary)","Portuguese, Brazilian",3,4,0,0,0,0,0,"2019-07-31 20:42:20"
"Tejaswini Boppana (Tejaswini)",Telugu,3,1,0,0,0,0,0,"2021-08-27 23:48:55"
"Andrea Bianchi (andreawhite1597)",Italian,3,1,0,1,0,0,0,"2018-01-21 17:45:48"
"Ño Bí Tã (pt614553)",Arabic,2,8,0,1,0,2,0,"2021-05-22 20:41:01"
"Judith Ayala (Azul1612)",Spanish,2,1,0,0,0,1,0,"2021-05-18 17:07:19"
"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"
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"
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"
"Danial Agh (danialagh)",Persian,2,3,0,0,0,0,0,"2019-03-30 13:24:16"
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"
"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"
"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"
"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"
amei,"Portuguese, Brazilian",2,2,0,0,0,0,0,"2018-04-19 19:42:28"
"أم محمد تقي (souadboudia19)",Arabic,2,2,0,0,0,0,0,"2020-06-13 15:24:17"
LNDDYL,"Chinese Traditional",2,4,0,0,0,0,2,"2018-04-22 04:00:19"
"조화정 (yunjoo337)",Korean,2,2,0,0,0,0,0,"2019-06-16 22:25:31"
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"
"Sidali Aymen (sidaliaymen950)",Arabic,2,2,0,0,0,0,0,"2022-01-31 18:50:59"
"Danial Agh (danialagh)",Persian,2,3,0,0,0,0,0,"2019-03-30 13:24:16"
iSoron2,"Portuguese, Brazilian",1,1,0,0,0,0,0,"2017-03-18 17:56:29"
"Anton (tT0NG)","Chinese Traditional",1,2,0,0,0,0,1,"2017-07-06 14:18:39"
"Luca Gori (grolcu)",Italian,1,2,0,0,0,0,0,"2020-09-26 23:26:15"
axd,Spanish,1,1,0,15,0,0,0,"2017-09-12 05:48:51"
iSoron2,"Portuguese, Brazilian",1,1,0,0,0,0,0,"2017-03-18 17:56:29"
REMOVED_USER,Russian,1,2,0,6,1,0,1,"2019-12-26 05:37:01"
"Wibi Cahyo (wbcahyoh)",Indonesian,1,3,0,0,0,0,0,"2017-12-14 06:35:58"
jonesses,German,1,1,0,1,0,0,1,"2021-01-01 08:03:18"
"Anton (tT0NG)","Chinese Traditional",1,2,0,0,0,0,1,"2017-07-06 14:18:39"
"박찌 (perpact20)",Korean,1,1,0,0,0,0,0,"2018-02-10 10:11:44"
"Alan Jeon (skyisle)",Korean,1,2,0,8,0,0,0,"2018-01-09 10:46:00"
"Maria Fefelova (mashafefel)",Russian,1,1,0,0,0,0,0,"2019-05-18 02:03:56"
"Anastasiia Bondarenko (nastasya.bondarenko.97)",Russian,1,1,0,0,0,0,0,"2019-06-07 17:43:08"
"Kan Black (kanblack.va)",Vietnamese,1,2,0,0,0,1,0,"2019-01-15 03:50:10"
"Patrick Pimenta (trickap1)","Portuguese, Brazilian",1,1,0,0,0,0,0,"2018-12-01 14:31:21"
"Dagna Q (dagnaq)",,0,0,0,0,0,0,0,"2017-08-06 01:42:52"
Kamalakannan,,0,0,0,0,0,0,0,"2017-05-14 11:40:23"
"Éjbãss Übbeî (littlebittlebottle)",Norwegian,0,0,0,152,0,0,0,"2017-07-05 21:12:02"
"Равиль Мифтахов (ravilmif47)",Russian,0,0,0,1,0,0,0,"2019-08-12 21:58:30"
"박찌 (perpact20)",Korean,1,1,0,0,0,0,0,"2018-02-10 10:11:44"
"Kan Black (kanblack.va)",Vietnamese,1,2,0,0,0,1,0,"2019-01-15 03:50:10"
"Anastasiia Bondarenko (nastasya.bondarenko.97)",Russian,1,1,0,0,0,0,0,"2019-06-07 17:43:08"
"Wibi Cahyo (wbcahyoh)",Indonesian,1,3,0,0,0,0,0,"2017-12-14 06:35:58"
sanyoniket,,0,0,0,0,0,0,0,"2019-07-23 12:58:40"
REMOVED_USER,,0,0,0,0,0,0,0,"2020-02-01 03:47:48"
"vi ve (VimalV)",,0,0,0,0,0,0,0,"2021-02-08 02:35:45"
"George Merkulov (george142.emarket)",Russian,0,0,0,11,0,0,0,"2019-06-09 19:47:02"
"Yasin Okumus (lacivert)",Turkish,0,0,0,1,0,0,0,"2018-02-07 04:13:51"
"Petros Bleyan (coolbleyan)",Russian,0,0,0,14,0,0,0,"2017-08-18 18:37:18"
"LeMeD (LeMeS)",French,0,0,0,2,0,0,0,"2021-02-06 15:35:00"
"Sri Harsha Bhogi (sriharshabhogi)",,0,0,0,0,0,0,0,"2018-09-02 05:31:53"
Irsgram,Russian,0,0,0,1,0,0,0,"2019-09-30 16:42:20"
"Baran Özavcı (n2141n)",Turkish,0,0,0,1,0,0,0,"2022-02-26 04:32:51"
"Masataka Yakura (myakura)",Japanese,0,0,0,1,0,0,0,"2021-09-03 22:10:36"
ava_rfie,Persian,0,0,0,1,0,0,0,"2019-06-09 16:19:24"
"Mateusz Teteruk (mttet)",Polish,0,0,0,1,0,0,0,"2021-01-23 13:09:59"
EwanB,,0,0,0,0,0,0,0,"2019-11-19 10:04:38"
Fazy1380,,0,0,0,0,0,0,0,"2021-04-10 11:02:53"
"Lori Amico (lavodkaclyde2323)",Italian,0,0,0,1,0,0,0,"2017-04-09 10:08:13"
"Florian Stuhlmann (stuhlmann)",German,0,0,0,10,0,0,0,"2017-04-15 04:04:00"
"عبد الناصر سعيد الثبيتي (asaeed)",,0,0,0,0,0,0,0,"2018-03-13 02:09:35"
"Rivo Zängov (Eraser)",,0,0,0,0,0,0,0,"2020-10-13 04:38:26"
Hayder21,,0,0,0,0,0,0,0,"2019-12-31 10:56:24"
T-v-Gerwen,Dutch,0,0,0,47,0,0,0,"2018-03-02 10:26:33"
"Eduard Boboc (edi.boboc33)",Romanian,0,0,0,4,0,0,0,"2019-12-16 09:08:39"
"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"
"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"
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"
"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"
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"
"Равиль Мифтахов (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"
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"
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"
" (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"
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"
martyaberger,,0,0,0,0,0,0,0,"2019-01-01 18:48:08"
AsadullahIlyas,,0,0,0,0,0,0,0,"2019-01-04 06:14:15"
droidahmed,Arabic,0,0,0,7,0,0,0,"2018-01-31 02:18:49"
philfr49,French,0,0,0,2,0,0,0,"2018-09-03 14:20:32"
"Ahmed Nazir (ahmednazir333)",,0,0,0,0,0,0,0,"2018-05-06 12:10:27"
"Balaji Jayaraman (jkbalaji1103)",,0,0,0,0,0,0,0,"2017-10-30 22:12:27"
"Wellington Ribeiro (wellington.rib)",,0,0,0,0,0,0,0,"2017-11-16 07:32:25"
"Javid IRAN (twitteriran98)",Persian,0,0,0,1,0,0,0,"2017-11-25 16:47:25"
"박인호 (wphestiraid)",Korean,0,0,0,2,0,0,0,"2018-01-05 00:33:14"
"akmal shafiq (mohdakmalshafiq)",,0,0,0,0,0,0,0,"2021-11-01 01:04:50"
"Sylwuskak (sylwuskak)",Polish,0,0,0,1,0,0,0,"2022-01-25 04:19:53"
"Yunsu Kim (yunsukim86)",Korean,0,0,0,2,0,0,0,"2022-01-14 06:33:43"
"Pumpith Ungsupanit (pumpithu)",,0,0,0,0,0,0,0,"2019-01-19 23:47:57"
Sandhu564.,,0,0,0,0,0,0,0,"2020-12-14 01:27:45"
"Quentin Hibon (hiq)",,0,0,0,0,0,0,0,"2021-02-07 16:39:31"
AhmedDz,Arabic,0,0,0,1,0,0,0,"2017-12-31 10:12:31"
"Nat Fomicheva (natac)",Russian,0,0,0,3,0,0,0,"2019-01-25 14:35:02"
HemanthMeda,Telugu,0,0,0,4,0,0,0,"2021-12-01 14:02:14"
"darkkingredian (rediancool)",,0,0,0,0,0,0,0,"2021-07-27 16:04:32"
catemlitten,Japanese,0,0,0,1,0,0,0,"2021-11-17 15:06:02"
"Said Tahsin Dane (tasomaniac)",,0,0,0,0,0,0,0,"2021-09-25 05:31:01"
"Matus Zdansky (matuszdansky)",,0,0,0,0,0,0,0,"2019-10-20 13:52:24"
mdrobulis,,0,0,0,0,0,0,0,"2018-05-24 01:40:42"
valney.faria,"Portuguese, Brazilian",0,0,0,1,0,0,0,"2020-02-02 14:45:02"
"Petros Bleyan (coolbleyan)",Russian,0,0,0,14,0,0,0,"2017-08-18 18:37:18"
"Карлен Шаухаев (KarlenShaukhaev)",,0,0,0,0,0,0,0,"2020-04-27 08:53:49"
"Shuvashish Sahoo (shuvashish76)",,0,0,0,0,0,0,0,"2020-09-17 09:10:09"
REMOVED_USER,,0,0,0,0,0,0,0,"2018-01-05 16:56:12"
NairaDNV,Spanish,0,0,0,9,0,0,0,"2018-01-05 19:10:33"
"Dagna Q (dagnaq)",,0,0,0,0,0,0,0,"2017-08-06 01:42:52"
Sandhu564.,,0,0,0,0,0,0,0,"2020-12-14 01:27:45"
AhmedDz,Arabic,0,0,0,1,0,0,0,"2017-12-31 10:12:31"
"Quentin Hibon (hiq)",,0,0,0,0,0,0,0,"2021-02-07 16:39:31"
"Ahmed Nazir (ahmednazir333)",,0,0,0,0,0,0,0,"2018-05-06 12:10:27"
"박인호 (wphestiraid)",Korean,0,0,0,2,0,0,0,"2018-01-05 00:33:14"
Raulbertassi,,0,0,0,0,0,0,0,"2018-01-07 17:23:18"
"Карлен Шаухаев (KarlenShaukhaev)",,0,0,0,0,0,0,0,"2020-04-27 08:53:49"
"Javid IRAN (twitteriran98)",Persian,0,0,0,1,0,0,0,"2017-11-25 16:47:25"
"Wellington Ribeiro (wellington.rib)",,0,0,0,0,0,0,0,"2017-11-16 07:32:25"
dimateos,,0,0,0,0,0,0,0,"2021-01-10 06:29:52"
"Katherine Alexandra Flórez Ramírez (katherine.florez12)",Spanish,0,0,0,46,0,0,0,"2018-01-20 02:18:32"
"Balaji Jayaraman (jkbalaji1103)",,0,0,0,0,0,0,0,"2017-10-30 22:12:27"
"reza golestanzadeh (reza.golestanzadeh)",Persian,0,0,0,1,0,0,0,"2020-10-21 12:07:20"
farbod66,Persian,0,0,0,1,0,0,0,"2018-01-20 11:04:23"
"Muhammet Furkan ALMACI (furkan.almaci)",Turkish,0,0,0,1,0,0,0,"2017-10-29 13:44:56"
dongchen.yue,German,0,0,0,4,0,0,0,"2020-09-12 15:05:59"
"Алтынбек Наурызғали (altinbeknaurizgali)",Russian,0,0,0,1,0,0,0,"2020-08-12 13:03:49"
rooban23,,0,0,0,0,0,0,0,"2020-09-15 11:49:14"
NairaDNV,Spanish,0,0,0,9,0,0,0,"2018-01-05 19:10:33"
"Katherine Alexandra Flórez Ramírez (katherine.florez12)",Spanish,0,0,0,46,0,0,0,"2018-01-20 02:18:32"
Itch,,0,0,0,0,0,0,0,"2017-10-16 09:18:42"
"Yasin Okumus (lacivert)",Turkish,0,0,0,1,0,0,0,"2018-02-07 04:13:51"
"Eduard Boboc (edi.boboc33)",Romanian,0,0,0,4,0,0,0,"2019-12-16 09:08:39"
Hayder21,,0,0,0,0,0,0,0,"2019-12-31 10:56:24"
"Eliška Roubalová (roubaeli)",Czech,0,0,0,6,0,0,0,"2019-12-31 12:47:29"
Fazy1380,,0,0,0,0,0,0,0,"2021-04-10 11:02:53"
"Arttu Ylhävuori (arttu.ylhavuori)",,0,0,0,0,0,0,0,"2019-07-24 15:03:42"
EmanAmini,,0,0,0,0,0,0,0,"2017-03-31 13:27:43"
AnggaRifandi,,0,0,0,0,0,0,0,"2017-03-31 19:28:35"
"Lori Amico (lavodkaclyde2323)",Italian,0,0,0,1,0,0,0,"2017-04-09 10:08:13"
"Florian Stuhlmann (stuhlmann)",German,0,0,0,10,0,0,0,"2017-04-15 04:04:00"
Kamalakannan,,0,0,0,0,0,0,0,"2017-05-14 11:40:23"
farbod66,Persian,0,0,0,1,0,0,0,"2018-01-20 11:04:23"
"vi ve (VimalV)",,0,0,0,0,0,0,0,"2021-02-08 02:35:45"
"Éjbãss Übbeî (littlebittlebottle)",Norwegian,0,0,0,152,0,0,0,"2017-07-05 21:12:02"
"LeMeD (LeMeS)",French,0,0,0,2,0,0,0,"2021-02-06 15:35:00"
BongTran,Vietnamese,0,0,0,2,0,0,0,"2018-04-24 05:16:07"
REMOVED_USER,Czech,0,0,0,18,0,0,0,"2018-03-27 06:19:52"
mushin,,0,0,0,0,0,0,0,"2020-02-02 04:08:05"
"Mateusz Teteruk (mttet)",Polish,0,0,0,1,0,0,0,"2021-01-23 13:09:59"
"Sarah BCNN (fsarahboucenna)",French,0,0,0,16,0,0,0,"2018-02-11 11:07:36"
droidahmed,Arabic,0,0,0,7,0,0,0,"2018-01-31 02:18:49"
"Никита Карамов (nikita.karamoff)",Russian,0,0,0,10,0,0,0,"2018-10-29 03:57:21"

1 Name Languages Translated (Words) Target Words Approved (Words) Voted "+" votes received "-" votes received Winning (Words) Joined
2 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 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 14808 15497 17227 18825 1282 1308 0 1779 1896 80 84 4274 4315 2016-03-05 18:35:27
3 Slobodan Simić (Слободан Симић) (slsimic) Serbian (Latin); Serbian (Cyrillic) 2054 1831 2114 12 33 0 1991 2021-02-03 14:26:07
4 Oglaigh Rystard (oglaignaheireann) Ukrainian; Portuguese; Catalan; Greek; Basque; Romanian; Italian 1103 1037 1327 1 13 6 954 2017-03-31 09:13:19
5 dukelc Slovak 1046 993 0 0 0 0 0 2020-08-27 14:02:41
6 David (Cliff122) Swedish 1040 1019 725 6 0 0 700 2020-01-21 13:56:55
7 Omer I.S. (omeritzics) Hebrew 1000 1040 900 927 1097 1122 14 1 0 946 975 2020-10-11 20:10:51
8 dukelc Intan Ayunda (Intan_Ayunda) Slovak Indonesian 919 818 880 811 0 985 0 0 0 0 729 2020-08-27 14:02:41 2020-10-14 07:51:58
9 Intan Ayunda (Intan_Ayunda) Mihail Stefanov (MStefanov) Indonesian Bulgarian 800 755 793 794 962 3 0 0 2 0 711 2 2020-10-14 07:51:58 2017-03-31 16:09:02
10 KMakoto Chinese Traditional 745 1146 949 0 0 0 745 2019-10-22 04:19:52
11 Evren (evrenkiymaz) Turkish 688 604 0 71 5 28 1 22 0 2020-10-04 03:39:16
12 andaryon Czech 681 606 0 108 0 0 0 2021-11-25 10:20:45
13 Antti Kallio (antti.kallio) Finnish 668 539 0 5 0 0 0 2021-07-03 05:54:44
14 David Nos (david.nos) Catalan; Spanish 667 731 0 0 1 0 0 2020-01-04 10:15:36
15 Antti Kallio (antti.kallio) androide74 Finnish Italian 650 662 525 681 0 0 2 0 0 0 2021-07-03 05:54:44 2020-02-06 15:46:28
16 androide74 Osoitz Italian Basque 644 655 659 595 0 2 9 0 0 0 3 2020-02-06 15:46:28 2018-01-23 14:07:47
17 Dmitriy Bogdanov (di72nn) Russian 643 589 1197 0 36 0 515 2017-03-31 10:00:48
18 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
19 reyhoon Persian 624 759 0 1 3 1 0 2020-10-01 18:17:23
20 Osoitz Saeed Esmaili (saaeed.es20) Basque Persian 610 586 545 795 0 9 5 0 4 0 3 0 2018-01-23 14:07:47 2020-11-26 15:41:15
Saeed Esmaili (saaeed.es20) Persian 568 774 0 5 4 0 0 2020-11-26 15:41:15
21 fabian.bouchal German 548 527 0 6 0 3 72 2020-01-07 06:43:37
22 boban77 Isti (eisti) Czech Hungarian 509 528 461 476 0 2 0 0 0 0 2020-04-30 13:18:24 2020-12-03 12:02:51
23 Yoav Argov (YoavArgov) boban77 Hebrew Czech 501 509 461 0 0 2 1 29 8 0 103 0 2017-04-28 07:23:01 2020-04-30 13:18:24
24 Martim Parente (martimparente) Portuguese 505 542 0 38 0 0 0 2020-08-26 10:22:11
25 Yoav Argov (YoavArgov) Hebrew 501 461 0 0 1 8 91 2017-04-28 07:23:01
26 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
27 chrrris1987 (Chrrris1987) Dutch 467 478 0 23 0 0 0 2020-02-03 05:26:04
Huy Ngo (huyngo) Vietnamese 461 695 0 1 0 0 0 2020-01-26 11:58:36
28 黄克 (hk13127) Chinese Simplified 461 765 0 1 0 0 24 2020-01-17 23:16:03
29 Arkadiusz Bubak (epitek) Huy Ngo (huyngo) Polish Vietnamese 458 461 416 695 29 0 24 1 0 3 0 0 2020-11-05 05:11:58 2020-01-26 11:58:36
30 Arkadiusz Bubak (epitek) Polish 458 416 52 24 9 4 0 2020-11-05 05:11:58
31 marco.baturan Esperanto 452 452 0 0 0 0 0 2020-06-23 02:49:46
32 Sief Tarek (sieftarek135) Arabic 447 455 0 0 0 0 0 2021-02-07 14:35:21
33 Alparslan Şakçi (sakci) Turkish 436 372 0 118 1 0 0 2022-01-14 12:03:11
34 JY3 Chinese Simplified 427 727 295 0 1 0 222 2021-03-08 08:53:35
35 Samuel Guay (SamGuay) French 426 486 0 6 0 0 0 2020-06-25 07:14:38
36 Diana Karaseva (Sun_Dianka) Russian 399 373 0 10 1 0 209 2020-01-30 06:40:02
37 Alexander Jansson (dalecarlian) Swedish 396 406 507 0 0 3 399 2017-06-21 01:37:32
38 luiandresgonzalez Spanish 383 403 0 1 28 0 0 2020-07-11 14:20:44
39 Thamara Andrade (tkcandrade) Portuguese, Brazilian 380 387 0 0 1 0 252 239 2020-01-09 19:35:48
40 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
41 Anh Quân (dangquanuet) Vietnamese 362 530 0 42 2 0 0 2017-10-29 12:27:44
42 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
43 Mahdi Nasiri (mahdi.nasiri) Persian 343 465 0 39 3 1 0 2017-07-14 09:17:25
44 Seoyul Korean 339 825 0 0 27 0 0 2017-06-21 08:11:39
45 Magimai Prakasam (magimai) Tamil 336 831 0 12 0 0 0 2018-04-15 21:16:08
46 Michael Malak (MichaelKMalak) Arabic 304 271 0 0 0 1 0 0 2020-05-26 19:47:58
47 Blinkin Dutch 297 334 0 5 0 0 0 2021-06-14 10:30:05
48 Elina Salminen (salminen.elina.m) Finnish 297 227 0 0 0 0 0 2021-01-06 01:28:57
49 ayane.m Japanese 292 863 0 1 3 5 0 22 2019-11-20 03:28:26
50 Blinkin Marius Teufelweich (teufelweich) Dutch German 284 267 318 272 0 611 1 4 0 13 0 1 0 146 2021-06-14 10:30:05 2021-03-12 04:11:38
51 Marius Teufelweich (teufelweich) hypnotichemionus German Chinese Simplified 249 256 430 606 0 4 0 2 8 0 146 19 2021-03-12 04:11:38 2020-03-08 01:46:25
52 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
53 beriain Basque 234 235 0 0 2 0 0 2017-03-31 15:42:28
54 pnhpnh Vietnamese 225 343 0 1 3 0 0 2017-11-27 12:06:07
55 Dika Fitrian Dwi Putra (OsamuDazai) Indonesian 221 215 0 0 0 0 48 2020-07-13 04:40:27
56 easyrepro Telugu 214 297 0 0 0 4 0 0 2020-06-12 12:52:10
57 taras-ko Ukrainian 211 183 0 1 4 0 19 2017-10-26 16:52:22
58 sojusnik German 207 200 1 0 30 0 66 2017-04-03 17:11:56
59 axmed99 Andrij Mizyk (andmizyk) Ukrainian 203 204 177 178 0 40 0 0 53 2021-04-01 03:56:20
60 Heru Yen (heruyen) Indonesian 201 201 0 0 0 0 25 2020-06-29 18:39:15
61 Vijaykumar Borkar (vjkumar) Hindi 200 364 0 11 0 0 0 2021-08-06 16:12:15
62 Ishmaeel _translator Turkish French 193 199 174 227 0 129 11 6 0 0 0 2017-10-04 03:54:00 2021-07-06 07:54:12
63 Ishmaeel Turkish 193 174 0 129 17 6 0 2017-10-04 03:54:00
64 oscfd Spanish 192 201 0 2 4 0 0 2021-05-21 17:58:22
65 bruhwut Vietnamese 189 292 0 1 0 0 0 2021-05-21 07:16:30
66 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
67 fbruna17 Danish 181 179 0 1 0 0 0 2021-01-28 15:48:47
68 Bryanx Dutch 179 168 0 5 2 0 0 2019-11-21 17:08:12
69 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
70 Pierre GALIEGUE (pierre.galiegue) French 171 194 0 24 4 0 0 2020-08-16 11:41:35
71 bruhwut plitwin Vietnamese Polish 171 168 268 151 0 1 2 0 31 0 0 49 2021-05-21 07:16:30 2021-01-20 06:18:37
72 DionysosDV Greek 165 153 0 0 0 0 0 2021-02-27 19:05:25
73 Gustavo Lima (GustavoLima) Portuguese 158 177 0 1 4 10 0 2020-08-26 10:35:05
74 oscfd Ravi Rami (ramiravi) Spanish Hindi 155 151 166 248 0 1 0 4 0 0 0 2021-05-21 17:58:22 2021-10-10 09:19:40
plitwin Polish 145 128 0 1 16 0 26 2021-01-20 06:18:37
75 Lương Vĩnh Khang (LuongVinhKhang) Vietnamese 144 256 0 0 46 1 0 2017-08-10 10:05:58
76 azzamsa Indonesian 142 136 0 48 0 1 26 2017-06-16 18:29:45
77 yoding (yodingc) Chinese Traditional; Chinese Simplified 141 271 0 10 0 0 0 2021-07-07 01:45:45
78 Neysa Nasywa (neysanasywa) Indonesian 140 141 0 0 0 0 60 2020-11-18 10:32:10
79 mohmans Arabic 139 141 0 12 1 0 0 2020-11-23 02:48:00
80 Eilif Adelvice (adelvice) Spanish 139 154 0 96 1 0 0 2021-08-05 07:20:21
81 Mohammed Imthath (mimthath4) Tamil 136 274 0 0 11 0 0 2018-02-15 22:41:15
82 carllacan Catalan 134 155 0 2 0 0 0 2021-11-13 13:12:07
83 roptat French 132 154 0 112 89 5 0 2017-04-19 16:54:47
84 Trần Thái (tranhoangthai2001) Vietnamese 127 186 0 8 1 0 0 2018-03-01 10:51:39
85 OP Smosher (teenwolffan44) Serbian (Cyrillic) 124 122 0 0 0 0 18 2020-11-05 09:41:35
86 4001982248998 Esperanto 122 119 0 0 0 0 0 2017-10-08 04:13:02
87 StoP4Me (Lcqp) Romanian 121 119 0 0 3 0 0 2018-05-06 18:51:59
88 alalloush Arabic 118 129 0 2 2 14 0 3 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
89 Tanya (MagicUnderHood) Russian 114 98 0 19 0 0 54 2019-04-21 10:44:03
90 REMOVED_USER Sebastian05067 Arabic Spanish 111 114 106 133 0 22 55 21 28 2 0 0 2018-01-05 07:01:45 2017-05-14 00:48:16
91 mohmans REMOVED_USER Arabic 109 111 103 106 0 2 22 0 22 0 2 0 2020-11-23 02:48:00 2018-01-05 07:01:45
92 Iabin Arteaga (iabin) Spanish 108 111 0 4 21 0 0 2017-08-26 21:08:54
93 Ivan Krušlin (krux3r) Croatian 108 122 503 0 0 0 108 2017-03-31 09:15:24
94 2kaafone Finnish 105 90 0 0 0 0 0 2019-08-12 06:58:48
95 Adam Jurkiewicz (hasztagg) Polish 104 105 529 0 0 0 104 2017-03-31 09:50:51
96 just a name bro (justanamebr0) Danish 98 109 0 0 1 0 0 2019-06-19 11:57:55
97 Nam Nguyen (namnl2706) Vietnamese 95 137 0 0 0 0 0 2020-08-18 23:02:33
98 손유정 (yuwon1213) Korean 95 57 0 0 0 1 0 0 2021-03-30 05:25:33
99 ranmagen Hebrew 91 78 0 0 0 0 0 2021-02-16 05:44:31
100 LoneWanderer Chinese Traditional 90 137 0 4 0 0 0 2020-09-29 05:24:48
Vo - (voyl) Chinese Traditional 89 126 0 0 5 0 0 2020-09-02 23:34:42
101 ikkaz Indonesian 89 84 0 5 0 0 4 2019-09-02 19:58:54
102 Vo - (voyl) Chinese Traditional 89 126 0 0 5 0 0 2020-09-02 23:34:42
103 Irene K (Heaun) Korean 88 75 0 25 0 0 0 2020-03-16 11:31:12
104 Prosta4ok_ua Ukrainian 87 84 0 1 0 0 17 2020-01-23 19:43:41
105 Kumar Anand (kumar0500) Hindi 87 125 0 0 0 0 0 2020-11-07 02:46:09
106 Ohad Edri (ohadalte) Hebrew 85 79 0 0 1 3 18 13 2020-07-04 03:42:09
107 helectron Persian 84 102 0 1 0 0 0 2021-03-02 04:10:51
108 Radu Cebotari (wildProgrammer) Romanian 84 92 0 1 0 0 0 2020-02-05 01:20:00
109 Bruces Lee (aplusbdesign) Korean 82 66 0 0 0 0 0 2021-08-23 11:27:18
110 Israa Z (sosozozo) Arabic 79 87 0 43 14 0 3 2017-11-27 14:10:50
111 Sofia Neves (sofiasonev) Portuguese, Brazilian 79 84 0 1 0 0 46 2020-03-12 18:19:46
112 Jacob Roller (jdr28070) Korean 79 61 0 0 0 1 0 0 2020-01-03 11:36:40
113 Tiralka French 79 91 0 92 1 0 0 2018-02-09 18:39:01
114 Israa Z (sosozozo) Toni Mustonen (toni.mustonen) Arabic Finnish 79 78 87 72 0 43 0 12 5 0 3 0 2017-11-27 14:10:50 2017-09-02 05:34:12
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
115 Michael (quelbs) German 76 75 0 1 0 0 39 2020-08-18 07:39:26
116 Oliver Gronowski (OliverGronowski) Fauz Aladeem (topfauz) German Arabic 70 76 69 77 0 5 0 0 0 1 0 2021-05-14 16:37:10 2020-02-21 22:46:12
117 Radoslaw Biernacki (radoslaw.biernacki) Polish 70 74 0 56 1 0 1 2020-12-15 17:55:31
118 Oliver Gronowski (OliverGronowski) German 70 69 0 5 2 0 0 2021-05-14 16:37:10
119 RealDonald Dutch 67 69 0 121 10 0 0 2017-06-23 20:10:12
120 sirekanyan Armenian; Russian 66 65 0 0 0 0 0 2020-04-18 11:32:52
121 Константин К. (kocyak1991) Russian 64 60 0 0 1 2 0 2018-06-10 13:39:37
122 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
123 raden20 Indonesian 61 62 177 0 1 0 64 2017-04-09 22:04:23
124 Peter Williams (williamspete001) Japanese 60 173 0 2 0 0 3 2020-01-01 13:17:44
125 Jan Wojtecki (j4nw) Polish 58 46 0 0 0 0 26 2017-11-02 05:42:14
126 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
127 Андрій Козицький (andriikozytskyi1108) Ukrainian 52 52 0 0 1 0 0 2018-10-22 01:45:08
128 Nil riera (nilriera2000) Catalan 52 61 0 1 0 2 0 0 2021-06-22 16:37:44
129 Neoone (Neooneqq) Romanian 51 54 0 0 0 0 0 2022-05-05 20:42:11
130 REMOVED_USER Italian 51 52 0 2 0 0 0 2017-08-21 05:15:31
131 govindap Japanese; Hindi 51 114 0 6 1 0 0 2020-06-02 20:15:52
132 Mare Geldenhuys (mare.geldenhuys) Afrikaans 50 57 0 0 0 0 0 2017-10-20 18:00:14
133 Mahmoud Magdy (M7moudManson) Arabic 49 60 0 6 8 1 0 2021-08-21 09:01:38
134 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
135 J3ll3nl Dutch 48 48 0 0 17 1 3 2017-03-31 11:56:09
136 Andrew Firnes (Anechan) tat bz (Tat_i) Russian German 47 48 47 56 0 3 55 0 0 1 29 27 2019-09-18 09:51:59 2021-03-26 05:12:54
andowero Czech 47 38 0 0 0 0 0 2020-01-20 02:29:01
137 vach Armenian 47 36 0 0 0 0 0 2020-04-18 16:53:12
138 Andrew Firnes (Anechan) Russian 47 47 0 3 0 0 29 2019-09-18 09:51:59
139 andowero Czech 47 38 0 0 3 0 0 2020-01-20 02:29:01
140 Rahul Shishodia (rahul.shishodia.10) Hindi 46 85 0 6 5 1 0 2018-12-24 22:18:19
141 Coni Ragni (coni2ragnii) Spanish 46 46 0 0 0 0 0 2021-02-28 20:18:37
142 Cp0204 Chinese Simplified 45 72 0 0 0 0 0 2019-08-20 11:04:27
143 cc (cavaz) Italian 44 41 0 0 0 0 0 2017-04-01 04:21:08
144 Boban Jagertraum (boban40) Czech 43 38 0 2 1 18 1 0 2017-03-31 09:39:16
145 Kamil Dziadek (prso94) Polish 43 39 0 0 2 6 0 0 2020-04-06 17:12:06
146 andreea.muscalagiu Romanian 42 52 0 1 0 0 0 2017-10-22 07:19:49
147 Me Me (gentelwom) Arabic 42 40 0 0 0 0 0 2020-11-08 20:44:01
Ali Elsheikh (aelsheikh1987) Arabic 42 41 0 0 0 0 0 2021-06-16 10:17:26
148 Balázs Keresztury (belidzs) Hungarian 42 41 501 0 7 0 38 2017-04-06 02:40:24
149 andreea.muscalagiu Mateusz Duda (MateuszDuda) Romanian Polish 42 52 42 0 1 0 0 6 0 0 2017-10-22 07:19:49 2021-08-17 11:27:11
150 Mateusz Duda (MateuszDuda) Ali Elsheikh (aelsheikh1987) Polish Arabic 42 42 41 0 0 0 0 0 2021-08-17 11:27:11 2021-06-16 10:17:26
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
151 Ali Zali (stm19951995) Persian 40 60 0 0 0 0 0 2020-03-23 19:57:26
152 Limin Lu (liminlu) Sofia Veijonen (Suklaa) (sofia.veijonen) Chinese Simplified Finnish 39 40 79 33 503 0 0 0 0 39 0 2017-03-31 09:49:35 2018-03-07 09:24:22
153 dusanstrgar Slovenian 39 41 0 0 0 0 0 2017-03-31 10:30:28
154 Limin Lu (liminlu) Chinese Simplified 39 79 503 0 0 0 39 2017-03-31 09:49:35
155 Anshoe Tamil 38 65 0 14 0 0 0 2018-01-02 11:06:52
156 anasshm Arabic 37 36 0 9 0 0 0 2019-01-27 04:07:22
157 hrexen Armenian 37 37 0 0 0 0 0 2020-12-09 02:30:34
158 Abdulrahman (D7M) Arabic 36 39 0 0 0 0 0 2020-01-29 18:55:30
Maria Chushnyakova (maria.ch) Russian 36 31 0 3 0 0 0 2021-08-17 03:23:58
159 REMOVED_USER Swedish 36 33 0 5 1 0 0 2018-09-29 17:47:33
160 xphsis Basque 36 31 0 0 0 0 0 2022-01-02 08:16:19
161 Maria Chushnyakova (maria.ch) Russian 36 31 0 3 0 0 0 2021-08-17 03:23:58
162 長谷川知里 (chase0213) Japanese 34 138 0 13 0 0 24 2018-12-14 10:52:44
163 Piotr Łuczyński (peterluczynski) Polish 33 30 0 6 10 0 2 2020-01-29 07:27:40
164 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
165 milad farahani (miladfarmahini90) Persian 33 44 0 18 1 0 3 2017-08-31 16:09:00
166 JoeLi Chinese Traditional 31 70 0 12 0 0 24 2017-06-25 05:32:48
167 andriikozytskyi2625 Ukrainian 31 23 0 0 0 0 0 2019-07-08 00:16:41
168 REMOVED_USER Russian 31 30 0 2 4 0 3 2018-12-03 23:55:47
169 Moastafa Arabic 31 25 0 0 0 0 0 2020-07-06 11:37:53
170 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
171 yancyn Chinese Simplified 30 40 0 0 0 0 1 2020-05-18 20:06:03
비니몬youtube (khj01025276475) Korean 29 25 0 0 0 0 0 2020-02-09 20:44:35
172 Ruud Schouten (ruudschouten) Dutch 29 32 0 41 3 0 0 2017-07-22 17:49:17
173 Aaron Dalton (Perlkonig) 비니몬youtube (khj01025276475) French Korean 26 29 25 0 141 0 1 0 0 0 2018-01-14 12:58:19 2020-02-09 20:44:35
174 avelneve Indonesian 29 28 0 0 0 0 0 2022-04-13 13:26:10
175 Niraj Yadav (neverforgetniraj) Hindi 26 48 0 0 0 0 0 2017-04-11 02:26:50
176 Guillaume Collic (gcollic) Aaron Dalton (Perlkonig) French 26 28 25 0 126 141 11 1 0 0 2017-05-05 16:13:00 2018-01-14 12:58:19
Radoslaw Biernacki (radoslaw.biernacki) Polish 26 24 0 8 0 0 1 2020-12-15 17:55:31
177 Jonny I (jonny99dj) Italian 26 26 0 5 0 0 0 2017-10-07 07:35:34
178 Guillaume Collic (gcollic) French 26 28 0 126 11 0 0 2017-05-05 16:13:00
179 Pan_Filuta Czech 25 21 0 5 8 0 3 2017-04-29 12:55:14
180 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
181 eduard83 (barbany.eduard) Catalan 24 25 0 2 0 0 0 2019-06-26 14:59:47
182 A Aa (ylayzlmimashisafyoutub) Arabic 23 33 0 34 1 1 0 2021-09-27 15:34:26
183 Caner Başaran (basarancaner) Turkish 23 21 0 0 26 1 0 2017-04-09 06:34:59
184 Ľuboš Čaky (lubos.caky) Slovak 23 22 0 0 0 0 0 2019-07-02 16:51:44
Caner Başaran (basarancaner) Turkish 23 21 0 0 21 0 0 2017-04-09 06:34:59
hodanli Turkish 22 26 0 0 1 0 0 2017-11-03 14:33:41
gnu-ewm Polish 22 23 0 6 0 0 0 2021-02-24 03:42:01
185 Neeraj Verma (verma.neeraj.in) Hindi 22 37 0 0 1 0 0 2018-07-23 07:16:41
186 gnu-ewm Polish 22 23 0 6 2 0 0 2021-02-24 03:42:01
187 hodanli Turkish 22 26 0 0 1 0 0 2017-11-03 14:33:41
188 Alcarkse (alexis.brusle) French 21 25 0 7 11 0 0 2017-08-06 09:32:29
olbotta Italian 20 25 0 2 0 0 0 2021-06-06 04:22:55
189 Shashwat (goforgold) Hindi 20 33 0 0 0 0 0 2020-05-17 10:34:42
190 Magdalena Urbańczyk (madziia139) olbotta Polish Italian 19 20 19 25 0 0 2 0 0 0 2017-10-21 03:01:04 2021-06-06 04:22:55
sheeCesu French 19 18 0 48 4 0 0 2017-12-21 17:01:39
191 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
192 사자솥 (toke1597) Korean 19 19 0 0 0 0 0 2020-02-04 13:36:11
193 KenKailer Arabic 19 25 0 0 0 0 0 2022-05-10 06:16:54
194 İsa Eş (IsaEs) Turkish 19 17 0 0 6 2 0 2017-06-20 07:30:22
195 Magdalena Urbańczyk (madziia139) Polish 19 19 0 0 0 0 0 2017-10-21 03:01:04
196 sheeCesu French 19 18 0 48 4 0 0 2017-12-21 17:01:39
197 axikman11111 Uyghur 18 19 0 0 0 0 0 2018-10-13 12:25:31
198 Adeline31 French 17 20 0 3 0 0 0 2019-12-06 00:00:11
199 Hoon Jung (hooni100) Korean 17 10 0 0 0 0 0 2021-01-03 02:26:54
Ceara Lopez (cealopez) Spanish 17 18 0 0 5 1 0 2017-08-22 22:56:13
200 takoyakibento Korean 17 13 0 3 0 0 0 2020-08-01 08:44:15
201 Adeline31 Ceara Lopez (cealopez) French Spanish 17 20 18 0 3 0 0 5 0 1 0 2019-12-06 00:00:11 2017-08-22 22:56:13
engineeringforgood Russian 16 15 0 0 0 0 16 2021-01-22 03:32:35
202 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
203 DebatablySane Bulgarian 16 15 0 48 0 0 0 2017-07-10 15:13:18
204 Şamil Ateşoğlu (m.samilatesoglu) Turkish 16 22 0 11 6 3 0 2017-07-05 18:37:08
205 engineeringforgood Russian 16 15 0 0 0 0 16 2021-01-22 03:32:35
206 Bhava Tharini (bhavidanush) Tamil 15 37 0 0 0 0 0 2019-10-09 05:43:11
207 Maro Chr (caprisunglasses) Greek 14 17 0 0 0 0 0 2021-08-17 06:53:33
208 Zeynep Esen (nezihaesen50) Turkish 14 13 0 0 0 0 0 2020-01-28 07:05:15
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
209 iamsurajbobade Hindi 14 30 0 0 0 0 0 2018-05-21 11:23:27
210 Faiz Ahamed (faiznewton) Tamil 14 31 0 0 0 0 0 2021-05-06 23:06:46
211 Sanji Vinsmock (mukanzhanbolat4) Russian 14 14 0 0 0 0 0 2020-02-18 12:38:54
212 Maro Chr (caprisunglasses) Zeeshan Rabbani (Zeera) Greek Hindi 14 17 25 0 0 0 0 0 2021-08-17 06:53:33 2020-09-15 11:32:01
213 Nenad Vukotic (vukotic.nenad) pi hobbes (uwe_silv) Serbian (Cyrillic) Japanese 13 14 13 46 0 1 0 2 0 6 0 0 2019-01-31 14:29:15 2022-01-15 02:57:14
214 Anastasia Borchuk (al2.borchuk) Russian 14 14 0 0 0 0 0 2020-04-14 13:22:49
215 Fikret Bilici (fikretbilici) Turkish 14 13 0 0 0 0 0 2020-06-21 17:16:11
216 EuiHo Hwang (euiho.hwang) Korean 14 16 0 0 0 0 0 2020-06-23 02:40:01
217 Uwe Mönks (schirinowski) German 13 12 0 0 0 0 0 2021-02-18 04:00:41
218 Dave (xdave) Hungarian 13 11 0 0 0 0 0 2020-03-02 20:56:50
219 Ana Kelly Vale (anakvale) Portuguese, Brazilian 13 21 0 4 0 0 2 2022-03-30 00:15:37
220 GiorgioHerbie Italian 13 15 0 0 0 0 0 2022-01-17 17:35:40
221 Nenad Vukotic (vukotic.nenad) Serbian (Cyrillic) 13 13 0 1 2 6 0 2019-01-31 14:29:15
222 soura2 Arabic 12 13 0 0 0 0 0 2020-01-13 19:23:47
223 shreyas (techiespace) Hindi 12 20 0 0 0 0 0 2018-06-10 01:14:26
Sonu Sharma (riteetude) Hindi 11 23 0 0 0 0 0 2021-05-30 19:38:00
Vmrc French 11 12 0 2 0 0 0 2020-11-02 05:35:06
224 Jo Chuang (josephch405) Chinese Traditional 11 24 0 0 0 0 11 2017-06-16 20:21:06
225 sathvic k (sathvictripleseven) Vmrc Telugu French 10 11 17 12 0 0 2 0 0 0 2020-09-11 08:11:32 2020-11-02 05:35:06
226 Brian Camacho (bmcamacho) Ammar Naif (Ammar_Naif) Polish Arabic 10 11 11 0 0 4 1 0 0 0 2020-08-03 02:27:28 2022-01-15 05:16:41
227 Anonymous edgy nerd (yamentaad) Sonu Sharma (riteetude) Arabic Hindi 10 11 13 23 0 1 0 0 0 0 2018-05-06 09:23:57 2021-05-30 19:38:00
228 Edwin van Rooij (edwinvrooij) Dutch 10 13 0 17 0 0 0 2018-11-05 03:59:10
229 Brian Camacho (bmcamacho) Polish 10 11 0 0 1 1 0 2020-08-03 02:27:28
230 Mihael Wagner (miha.wagner) Slovenian 10 9 0 7 0 0 0 2017-10-18 18:26:29
231 Hrant Hakobian (hrastgh1) Armenian 10 9 0 0 0 0 0 2021-08-29 15:22:10
232 sathvic k (sathvictripleseven) Telugu 10 17 0 0 0 0 0 2020-09-11 08:11:32
233 Ahmed Mosaad (ahmed.mosaad2018) Arabic 10 12 0 6 0 0 0 2021-02-03 18:45:43
234 Anonymous edgy nerd (yamentaad) Arabic 10 13 0 1 0 0 0 2018-05-06 09:23:57
235 Zesar Cebrián (Txorrota) Spanish 10 44 0 0 0 0 0 2022-02-09 01:34:32
236 Milan Siebenbürger (lennyd) Czech 10 7 0 1 0 0 0 2022-01-30 07:09:42
237 Suhaili Hassan (kucingsyg96) Indonesian 9 10 0 0 0 0 0 2018-06-10 11:55:09
238 Sourire Lucide (sourire_lucide) Russian 9 10 0 0 1 0 0 2018-03-22 01:37:55
239 Martin Vostatek (martinvostatek) Czech 9 8 0 32 2 0 0 2019-01-21 13:52:36
240 Seweryn Piotrowski (Draxxsx) Polish 9 10 0 0 19 0 0 2020-01-02 09:55:48
241 Sourire Lucide (sourire_lucide) Jakob Weickmann (jweickm) Russian Japanese 9 8 10 21 0 0 1 0 0 0 2018-03-22 01:37:55 2021-10-05 11:10:25
242 Rex123 Persian 8 8 0 0 0 0 0 2017-07-01 00:47:42
243 Andrey ZaXeLoN (waragaa) Russian 7 7 0 8 1 0 0 2017-09-18 21:37:42
244 Konstantin (KZhidovinov) Russian 7 7 0 0 0 0 0 2020-01-29 13:35:12
Андрій Козицький (andriikozytskyi3807) Ukrainian 7 12 0 2 0 0 0 2020-09-26 20:31:56
245 ftfoi Norwegian 7 6 0 0 0 0 0 2020-04-11 20:42:35
246 Vladimir Pavlychev (KeyJoo) Vladimir Pavlychev (vovs03) Russian 7 9 0 0 0 0 0 2017-12-18 02:46:56
247 Felipe Chagas (chagretes) Portuguese, Brazilian 7 8 0 0 3 0 5 2022-01-10 12:20:25
248 Андрій Козицький (andriikozytskyi3807) Ukrainian 7 12 0 2 0 0 0 2020-09-26 20:31:56
249 pkorove Greek 7 7 0 0 0 0 0 2020-03-07 11:36:12
250 erfan2927 ChloeLiang Persian Japanese 6 6 22 0 0 0 1 0 0 3 2018-04-09 02:12:44 2017-08-08 05:02:59
Burak Ceylan (7burakceylan) Turkish 6 6 0 0 0 0 0 2018-05-20 17:24:19
251 Sam (SorodonSorodon) German 6 6 0 13 0 0 0 2017-04-14 11:09:27
252 닉닉 (seohu9466) Korean 6 14 0 13 0 0 0 2017-10-09 23:08:15
253 Sarita Cajas (sarayanacajas) Spanish 6 4 0 0 1 0 0 2021-05-14 14:27:59
254 ChloeLiang erfan2927 Japanese Persian 6 22 6 0 0 1 0 0 3 0 2017-08-08 05:02:59 2018-04-09 02:12:44
255 Manuel Tassi (Mannivu) Burak Ceylan (7burakceylan) Italian Turkish 5 6 6 0 0 0 0 0 2021-01-03 11:00:33 2018-05-20 17:24:19
256 Tomáš Hrabáček (Hrabyyy) andriikozytskyi2018 Czech Ukrainian 5 3 5 0 0 0 0 0 2021-05-27 11:58:11 2017-09-03 05:24:43
257 Vitor Henrique (vitorhcl) Portuguese, Brazilian 5 8 0 1 0 0 0 2022-03-08 20:00:59
258 Matthias Joly (joly.matt12) French 5 8 0 27 1 0 0 2017-08-28 09:53:59
259 Tomáš Hrabáček (Hrabyyy) Czech 5 3 0 0 1 0 0 2021-05-27 11:58:11
260 Guerra Ivaneth (rossanaiva-04) Spanish 5 7 0 0 0 0 0 2019-02-03 16:48:59
261 Дмитрий Хапенков (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
262 Micaela Pighin (micaelapiighin) Spanish 5 6 0 1 0 0 0 2019-10-09 23:32:42
263 andriikozytskyi2018 Manuel Tassi (Mannivu) Ukrainian Italian 5 5 6 0 0 0 0 0 2017-09-03 05:24:43 2021-01-03 11:00:33
marmo German 4 4 0 0 0 0 0 2021-01-13 01:16:35
Eli Besirov (elibesirov07) Turkish 4 4 0 0 0 0 0 2019-03-25 07:12:34
Lopo Isaac Fernández (rocapata) Spanish 4 3 0 0 0 0 0 2018-09-20 11:46:22
bziuum Polish 4 4 0 0 0 0 0 2020-09-01 09:08:01
264 Neko123 (emandic11) Serbian (Cyrillic) 4 4 0 57 0 0 0 2021-04-21 15:33:29
265 Magidxz Lopo Isaac Fernández (rocapata) Arabic Spanish 3 4 3 0 0 0 0 0 2021-01-05 05:02:54 2018-09-20 11:46:22
266 mohammadali barati (mabaraty) Eli Besirov (elibesirov07) Persian Turkish 3 4 3 4 0 0 0 0 0 2021-07-10 05:54:44 2019-03-25 07:12:34
267 marmo German 4 4 0 0 0 0 0 2021-01-13 01:16:35
268 bziuum Polish 4 4 0 0 3 0 0 2020-09-01 09:08:01
269 Craig Foobar (craig.foobar) German 3 3 0 25 0 0 0 2022-02-20 16:55:47
270 Katarin Ukrainian 3 3 0 0 0 0 0 2022-03-17 14:44:59
271 Sarath S (CyberShark) Tamil 3 7 0 0 0 0 0 2020-08-27 22:43:16
272 Vagner Roberto (vagner.trompete) Portuguese, Brazilian 3 3 0 0 0 0 0 2017-12-30 17:54:26
273 Igor Piskun (i_piskun) Ukrainian 3 3 0 0 0 0 0 2018-01-19 15:20:27
274 Cláudio Bernardo (claudiobernardo.ti) Portuguese, Brazilian 3 4 0 1 0 0 0 2019-01-08 14:41:10
275 Unnie Here (Carb) Hindi 3 8 0 0 0 0 0 2020-03-18 23:34:35
276 REMOVED_USER Portuguese, Brazilian 3 4 0 0 0 0 0 2018-11-18 09:02:37
277 Thoum Ptrgnt (thomas.petrignet) French 3 3 0 2 0 3 0 2017-09-23 19:25:52
278 Oleg Kogut (kogut_oleg) Ukrainian 3 3 0 0 0 0 0 2018-12-28 14:31:02
279 carsten_kafke German 3 3 0 43 0 0 3 2017-10-27 13:27:47
280 Vagner Roberto (vagner.trompete) Magidxz Portuguese, Brazilian Arabic 3 3 0 0 0 0 0 2017-12-30 17:54:26 2021-01-05 05:02:54
Igor Piskun (i_piskun) Ukrainian 3 3 0 0 0 0 0 2018-01-19 15:20:27
Andrea Bianchi (andreawhite1597) Italian 3 1 0 1 0 0 0 2018-01-21 17:45:48
Cláudio Bernardo (claudiobernardo.ti) Portuguese, Brazilian 3 4 0 1 0 0 0 2019-01-08 14:41:10
Hiohana Rilary (hiohanarilary) Portuguese, Brazilian 3 4 0 0 0 0 0 2019-07-31 20:42:20
joabe gabriel (joabegabrielcma1) Portuguese, Brazilian 3 4 0 0 0 0 0 2018-08-21 09:08:59
281 Péter Bernát (bernatp) Hungarian 3 2 0 0 0 0 0 2019-11-30 15:50:33
282 Martin Zimdahl (zimdahlmartin) joabe gabriel (joabegabrielcma1) Swedish Portuguese, Brazilian 3 2 4 0 0 1 0 0 3 0 2018-09-15 04:39:22 2018-08-21 09:08:59
283 Gabriel Cavalcante (gabrielc.alves14) Portuguese, Brazilian 3 4 0 0 0 0 0 2018-08-06 22:24:54
284 Martin Zimdahl (zimdahlmartin) Swedish 3 2 0 0 1 0 3 2018-09-15 04:39:22
285 atomjani Hungarian 3 3 0 0 0 0 0 2019-01-19 00:49:25
286 أم محمد تقي (souadboudia19) mohammadali barati (mabaraty) Arabic Persian 2 3 2 3 0 0 0 0 0 2020-06-13 15:24:17 2021-07-10 05:54:44
287 Hiohana Rilary (hiohanarilary) Portuguese, Brazilian 3 4 0 0 0 0 0 2019-07-31 20:42:20
288 Tejaswini Boppana (Tejaswini) Telugu 3 1 0 0 0 0 0 2021-08-27 23:48:55
289 Andrea Bianchi (andreawhite1597) Italian 3 1 0 1 0 0 0 2018-01-21 17:45:48
290 Ño Bí Tã (pt614553) Arabic 2 8 0 1 0 2 0 2021-05-22 20:41:01
291 Judith Ayala (Azul1612) Spanish 2 1 0 0 0 1 0 2021-05-18 17:07:19
292 Valerij D (vala.dobler) German 2 2 0 0 0 0 0 2018-09-22 09:38:27
293 Balthazar Aubard (Balatzar) French 2 5 0 0 1 0 0 2017-09-23 01:42:57
294 Ahmed Bazazo (ahmedbazazo) Arabic 2 2 0 0 0 0 0 2022-02-19 20:11:09
295 Ali Zaida (alizaeda92) Arabic 2 2 0 0 0 0 0 2019-12-01 11:47:00
296 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
297 Soroor_SI Persian 2 2 0 0 0 0 0 2018-06-10 06:28:27
298 Ilyas Fekhar (il47yas) chavs1997 Arabic Russian 2 2 0 0 9 0 0 0 2018-04-17 22:00:41 2018-05-18 16:58:19
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
299 Naveen jai krishna (njsbpolymer1) Tamil 2 5 0 0 0 0 0 2020-01-10 14:19:41
300 Danial Agh (danialagh) omerfarukbas Persian Turkish 2 3 0 0 19 0 2 0 0 2019-03-30 13:24:16 2017-08-14 16:10:35
301 Ilyas Fekhar (il47yas) Arabic 2 2 0 0 0 0 0 2018-04-17 22:00:41
302 Héctor Mañas García (hectodium) Catalan 2 3 0 0 0 0 0 2021-10-02 20:32:09
303 Walid Baazia (walidbaazia2005) Arabic 2 1 0 0 0 0 0 2021-01-27 12:47:34
304 Ali Zaida (alizaeda92) fatemeh s (fargolseifoori3) Arabic Persian 2 2 0 0 0 0 0 2019-12-01 11:47:00 2019-01-31 12:06:57
305 LNDDYL hesamiranii (esam.matouri) Chinese Traditional Persian 2 4 2 0 0 0 0 2 0 2018-04-22 04:00:19 2018-09-22 16:33:36
Ñ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
306 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
307 Alex Stein (diefaust1993) Russian 2 2 0 4 4 0 2 2017-07-13 06:56:17
308 amei Portuguese, Brazilian 2 2 0 0 0 0 0 2018-04-19 19:42:28
309 أم محمد تقي (souadboudia19) Arabic 2 2 0 0 0 0 0 2020-06-13 15:24:17
310 LNDDYL Chinese Traditional 2 4 0 0 0 0 2 2018-04-22 04:00:19
311 조화정 (yunjoo337) Korean 2 2 0 0 0 0 0 2019-06-16 22:25:31
312 omerfarukbas Sidali Aymen (sidaliaymen950) Turkish Arabic 2 3 2 0 19 0 2 0 0 0 2017-08-14 16:10:35 2022-01-31 18:50:59
313 Balthazar Aubard (Balatzar) Danial Agh (danialagh) French Persian 2 5 3 0 0 1 0 0 0 2017-09-23 01:42:57 2019-03-30 13:24:16
314 iSoron2 Portuguese, Brazilian 1 1 0 0 0 0 0 2017-03-18 17:56:29
315 Anton (tT0NG) Chinese Traditional 1 2 0 0 0 0 1 2017-07-06 14:18:39
316 Luca Gori (grolcu) Italian 1 2 0 0 0 0 0 2020-09-26 23:26:15
317 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
318 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
319 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
320 Alan Jeon (skyisle) Korean 1 2 0 8 0 0 0 2018-01-09 10:46:00
321 Maria Fefelova (mashafefel) Russian 1 1 0 0 0 0 0 2019-05-18 02:03:56
Anastasiia Bondarenko (nastasya.bondarenko.97) Russian 1 1 0 0 0 0 0 2019-06-07 17:43:08
Kan Black (kanblack.va) Vietnamese 1 2 0 0 0 1 0 2019-01-15 03:50:10
322 Patrick Pimenta (trickap1) Portuguese, Brazilian 1 1 0 0 0 0 0 2018-12-01 14:31:21
323 Dagna Q (dagnaq) 박찌 (perpact20) Korean 0 1 0 1 0 0 0 0 0 2017-08-06 01:42:52 2018-02-10 10:11:44
324 Kamalakannan Kan Black (kanblack.va) Vietnamese 0 1 0 2 0 0 0 0 1 0 2017-05-14 11:40:23 2019-01-15 03:50:10
325 Éjbãss Übbeî (littlebittlebottle) Anastasiia Bondarenko (nastasya.bondarenko.97) Norwegian Russian 0 1 0 1 0 152 0 0 0 0 2017-07-05 21:12:02 2019-06-07 17:43:08
326 Равиль Мифтахов (ravilmif47) Wibi Cahyo (wbcahyoh) Russian Indonesian 0 1 0 3 0 1 0 0 0 0 2019-08-12 21:58:30 2017-12-14 06:35:58
327 sanyoniket 0 0 0 0 0 0 0 2019-07-23 12:58:40
328 REMOVED_USER Sri Harsha Bhogi (sriharshabhogi) 0 0 0 0 0 0 0 2020-02-01 03:47:48 2018-09-02 05:31:53
329 vi ve (VimalV) Irsgram Russian 0 0 0 0 1 0 0 0 2021-02-08 02:35:45 2019-09-30 16:42:20
330 George Merkulov (george142.emarket) Baran Özavcı (n2141n) Russian Turkish 0 0 0 11 1 0 0 0 2019-06-09 19:47:02 2022-02-26 04:32:51
331 Yasin Okumus (lacivert) Masataka Yakura (myakura) Turkish Japanese 0 0 0 1 0 0 0 2018-02-07 04:13:51 2021-09-03 22:10:36
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
332 ava_rfie Persian 0 0 0 1 0 0 0 2019-06-09 16:19:24
Mateusz Teteruk (mttet) Polish 0 0 0 1 0 0 0 2021-01-23 13:09:59
EwanB 0 0 0 0 0 0 0 2019-11-19 10:04:38
Fazy1380 0 0 0 0 0 0 0 2021-04-10 11:02:53
Lori Amico (lavodkaclyde2323) Italian 0 0 0 1 0 0 0 2017-04-09 10:08:13
Florian Stuhlmann (stuhlmann) German 0 0 0 10 0 0 0 2017-04-15 04:04:00
عبد الناصر سعيد الثبيتي (asaeed) 0 0 0 0 0 0 0 2018-03-13 02:09:35
Rivo Zängov (Eraser) 0 0 0 0 0 0 0 2020-10-13 04:38:26
Hayder21 0 0 0 0 0 0 0 2019-12-31 10:56:24
333 T-v-Gerwen Dutch 0 0 0 47 0 0 0 2018-03-02 10:26:33
334 Eduard Boboc (edi.boboc33) George Merkulov (george142.emarket) Romanian Russian 0 0 0 4 11 0 0 0 2019-12-16 09:08:39 2019-06-09 19:47:02
335 Samuel Przeździęk (samek22) philfr49 Polish French 0 0 0 1 2 0 0 0 2021-08-01 00:49:01 2018-09-03 14:20:32
336 Saiprasath B (Saiprasath) عبد الناصر سعيد الثبيتي (asaeed) 0 0 0 0 0 0 0 2021-07-11 11:10:41 2018-03-13 02:09:35
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
337 Thomas Orlita (Thomas995) Czech 0 0 0 1 0 0 0 2017-12-24 04:08:27
338 Irsgram Edmunds Edmundam (edmundam) Russian 0 0 0 1 0 0 0 0 2019-09-30 16:42:20 2020-06-01 14:18:18
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
339 Elmo (oberknecht) 0 0 0 0 0 0 0 2020-04-16 08:45:50
340 AnggaRifandi Равиль Мифтахов (ravilmif47) Russian 0 0 0 0 1 0 0 0 2017-03-31 19:28:35 2019-08-12 21:58:30
341 darkkingredian (rediancool) Manny Farsangy (manifarsangi) Persian 0 0 0 0 12 0 0 0 2021-07-27 16:04:32 2021-08-10 05:32:28
342 Sri Harsha Bhogi (sriharshabhogi) Samuel Przeździęk (samek22) Polish 0 0 0 0 1 0 0 0 2018-09-02 05:31:53 2021-08-01 00:49:01
343 Nat Fomicheva (natac) Saiprasath B (Saiprasath) Russian 0 0 0 3 0 0 0 0 2019-01-25 14:35:02 2021-07-11 11:10:41
344 mdrobulis REMOVED_USER 0 0 0 0 0 0 0 2018-05-24 01:40:42 2018-08-24 00:17:43
345 Sarah BCNN (fsarahboucenna) REMOVED_USER French 0 0 0 16 0 0 0 0 2018-02-11 11:07:36 2020-02-01 03:47:48
346 Arjun K. (arjunkdot) 0 0 0 0 0 0 0 2020-09-20 11:16:18
347 REMOVED_USER EwanB Czech 0 0 0 18 0 0 0 0 2018-03-27 06:19:52 2019-11-19 10:04:38
348 martyaberger shuvo786 0 0 0 0 0 0 0 2019-01-01 18:48:08 2019-11-13 00:18:12
349 BongTran Pro AAA (pro1010) Vietnamese Arabic 0 0 0 2 1 0 0 0 2018-04-24 05:16:07 2022-02-14 03:32:44
350 Arttu Ylhävuori (arttu.ylhavuori) manu (manuL96) 0 0 0 0 0 0 0 2019-07-24 15:03:42 2022-05-06 23:34:55
351 Никита Карамов (nikita.karamoff) Rivo Zängov (Eraser) Russian 0 0 0 10 0 0 0 0 2018-10-29 03:57:21 2020-10-13 04:38:26
352 rooban23 ashik8113 0 0 0 0 0 0 0 2020-09-15 11:49:14 2022-04-13 11:58:26
353 Eliška Roubalová (roubaeli) deepbird Czech 0 0 0 6 0 0 0 0 2019-12-31 12:47:29 2022-04-11 03:21:05
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
354 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
355 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
356 Ahnaf Tajwar (atn4404) 0 0 0 0 0 0 0 2018-10-16 11:13:30
357 martyaberger 0 0 0 0 0 0 0 2019-01-01 18:48:08
358 AsadullahIlyas 0 0 0 0 0 0 0 2019-01-04 06:14:15
359 droidahmed akmal shafiq (mohdakmalshafiq) Arabic 0 0 0 7 0 0 0 0 2018-01-31 02:18:49 2021-11-01 01:04:50
360 philfr49 Sylwuskak (sylwuskak) French Polish 0 0 0 2 1 0 0 0 2018-09-03 14:20:32 2022-01-25 04:19:53
361 Ahmed Nazir (ahmednazir333) Yunsu Kim (yunsukim86) Korean 0 0 0 0 2 0 0 0 2018-05-06 12:10:27 2022-01-14 06:33:43
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
362 Pumpith Ungsupanit (pumpithu) 0 0 0 0 0 0 0 2019-01-19 23:47:57
363 Sandhu564. Nat Fomicheva (natac) Russian 0 0 0 0 3 0 0 0 2020-12-14 01:27:45 2019-01-25 14:35:02
364 Quentin Hibon (hiq) HemanthMeda Telugu 0 0 0 0 4 0 0 0 2021-02-07 16:39:31 2021-12-01 14:02:14
365 AhmedDz darkkingredian (rediancool) Arabic 0 0 0 1 0 0 0 0 2017-12-31 10:12:31 2021-07-27 16:04:32
366 catemlitten Japanese 0 0 0 1 0 0 0 2021-11-17 15:06:02
367 Said Tahsin Dane (tasomaniac) 0 0 0 0 0 0 0 2021-09-25 05:31:01
368 Matus Zdansky (matuszdansky) 0 0 0 0 0 0 0 2019-10-20 13:52:24
369 mdrobulis 0 0 0 0 0 0 0 2018-05-24 01:40:42
370 valney.faria Portuguese, Brazilian 0 0 0 1 0 0 0 2020-02-02 14:45:02
371 Petros Bleyan (coolbleyan) Russian 0 0 0 14 0 0 0 2017-08-18 18:37:18
372 Карлен Шаухаев (KarlenShaukhaev) 0 0 0 0 0 0 0 2020-04-27 08:53:49
373 Shuvashish Sahoo (shuvashish76) 0 0 0 0 0 0 0 2020-09-17 09:10:09
374 REMOVED_USER 0 0 0 0 0 0 0 2018-01-05 16:56:12
375 NairaDNV Dagna Q (dagnaq) Spanish 0 0 0 9 0 0 0 0 2018-01-05 19:10:33 2017-08-06 01:42:52
376 Sandhu564. 0 0 0 0 0 0 0 2020-12-14 01:27:45
377 AhmedDz Arabic 0 0 0 1 0 0 0 2017-12-31 10:12:31
378 Quentin Hibon (hiq) 0 0 0 0 0 0 0 2021-02-07 16:39:31
379 Ahmed Nazir (ahmednazir333) 0 0 0 0 0 0 0 2018-05-06 12:10:27
380 박인호 (wphestiraid) Korean 0 0 0 2 0 0 0 2018-01-05 00:33:14
381 Raulbertassi 0 0 0 0 0 0 0 2018-01-07 17:23:18
382 Карлен Шаухаев (KarlenShaukhaev) Javid IRAN (twitteriran98) Persian 0 0 0 0 1 0 0 0 2020-04-27 08:53:49 2017-11-25 16:47:25
383 Wellington Ribeiro (wellington.rib) 0 0 0 0 0 0 0 2017-11-16 07:32:25
384 dimateos 0 0 0 0 0 0 0 2021-01-10 06:29:52
385 Katherine Alexandra Flórez Ramírez (katherine.florez12) Balaji Jayaraman (jkbalaji1103) Spanish 0 0 0 46 0 0 0 0 2018-01-20 02:18:32 2017-10-30 22:12:27
386 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
387 Muhammet Furkan ALMACI (furkan.almaci) Turkish 0 0 0 1 0 0 0 2017-10-29 13:44:56
388 dongchen.yue German 0 0 0 4 0 0 0 2020-09-12 15:05:59
389 Алтынбек Наурызғали (altinbeknaurizgali) Russian 0 0 0 1 0 0 0 2020-08-12 13:03:49
390 rooban23 0 0 0 0 0 0 0 2020-09-15 11:49:14
391 NairaDNV Spanish 0 0 0 9 0 0 0 2018-01-05 19:10:33
392 Katherine Alexandra Flórez Ramírez (katherine.florez12) Spanish 0 0 0 46 0 0 0 2018-01-20 02:18:32
393 Itch 0 0 0 0 0 0 0 2017-10-16 09:18:42
394 Yasin Okumus (lacivert) Turkish 0 0 0 1 0 0 0 2018-02-07 04:13:51
395 Eduard Boboc (edi.boboc33) Romanian 0 0 0 4 0 0 0 2019-12-16 09:08:39
396 Hayder21 0 0 0 0 0 0 0 2019-12-31 10:56:24
397 Eliška Roubalová (roubaeli) Czech 0 0 0 6 0 0 0 2019-12-31 12:47:29
398 Fazy1380 0 0 0 0 0 0 0 2021-04-10 11:02:53
399 Arttu Ylhävuori (arttu.ylhavuori) 0 0 0 0 0 0 0 2019-07-24 15:03:42
400 EmanAmini 0 0 0 0 0 0 0 2017-03-31 13:27:43
401 AnggaRifandi 0 0 0 0 0 0 0 2017-03-31 19:28:35
402 Lori Amico (lavodkaclyde2323) Italian 0 0 0 1 0 0 0 2017-04-09 10:08:13
403 Florian Stuhlmann (stuhlmann) German 0 0 0 10 0 0 0 2017-04-15 04:04:00
404 Kamalakannan 0 0 0 0 0 0 0 2017-05-14 11:40:23
405 farbod66 Persian 0 0 0 1 0 0 0 2018-01-20 11:04:23
406 vi ve (VimalV) 0 0 0 0 0 0 0 2021-02-08 02:35:45
407 Éjbãss Übbeî (littlebittlebottle) Norwegian 0 0 0 152 0 0 0 2017-07-05 21:12:02
408 LeMeD (LeMeS) French 0 0 0 2 0 0 0 2021-02-06 15:35:00
409 BongTran Vietnamese 0 0 0 2 0 0 0 2018-04-24 05:16:07
410 REMOVED_USER Czech 0 0 0 18 0 0 0 2018-03-27 06:19:52
411 mushin 0 0 0 0 0 0 0 2020-02-02 04:08:05
412 Mateusz Teteruk (mttet) Polish 0 0 0 1 0 0 0 2021-01-23 13:09:59
413 Sarah BCNN (fsarahboucenna) French 0 0 0 16 0 0 0 2018-02-11 11:07:36
414 droidahmed Arabic 0 0 0 7 0 0 0 2018-01-31 02:18:49
415 Никита Карамов (nikita.karamoff) Russian 0 0 0 10 0 0 0 2018-10-29 03:57:21

@ -32,12 +32,12 @@ tasks.compileLint {
android {
compileSdk = 31
compileSdk = 32
defaultConfig {
versionCode = 20003
versionName = "2.0.3"
minSdk = 23
versionCode = 20100
versionName = "2.1.0"
minSdk = 28
targetSdk = 31
applicationId = "org.isoron.uhabits"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@ -68,12 +68,6 @@ android {
}
}
lint {
isCheckReleaseBuilds = false
isAbortOnError = false
disable("GoogleAppIndexingWarning")
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
targetCompatibility(JavaVersion.VERSION_1_8)
@ -86,29 +80,29 @@ android {
}
dependencies {
val daggerVersion = "2.40.3"
val kotlinVersion = "1.6.0"
val kxCoroutinesVersion = "1.5.2"
val ktorVersion = "1.6.6"
val daggerVersion = "2.43.2"
val kotlinVersion = "1.7.10"
val kxCoroutinesVersion = "1.6.4"
val ktorVersion = "1.6.8"
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.1")
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.3")
androidTestImplementation("io.ktor:ktor-client-mock:$ktorVersion")
androidTestImplementation("io.ktor:ktor-jackson:$ktorVersion")
androidTestImplementation("androidx.annotation:annotation:1.3.0")
androidTestImplementation("androidx.annotation:annotation:1.4.0")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
androidTestImplementation("androidx.test:rules:1.4.0")
androidTestImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
compileOnly("javax.annotation:jsr250-api:1.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
implementation("com.github.AppIntro:AppIntro:6.1.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.2")
implementation("com.github.AppIntro:AppIntro:6.2.0")
implementation("com.google.code.findbugs:jsr305:3.0.2")
implementation("com.google.dagger:dagger:$daggerVersion")
implementation("com.google.guava:guava:31.0.1-android")
implementation("com.google.guava:guava:31.1-android")
implementation("io.ktor:ktor-client-android:$ktorVersion")
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-jackson:$ktorVersion")
@ -116,11 +110,11 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kxCoroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kxCoroutinesVersion")
implementation("androidx.appcompat:appcompat:1.3.1")
implementation("androidx.appcompat:appcompat:1.5.0")
implementation("androidx.legacy:legacy-preference-v14:1.0.0")
implementation("androidx.legacy:legacy-support-v4:1.0.0")
implementation("com.google.android.material:material:1.4.0")
implementation("com.opencsv:opencsv:5.5.2")
implementation("com.google.android.material:material:1.6.1")
implementation("com.opencsv:opencsv:5.6")
implementation(project(":uhabits-core"))
kapt("com.google.dagger:dagger-compiler:$daggerVersion")
kaptAndroidTest("com.google.dagger:dagger-compiler:$daggerVersion")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 22 KiB

@ -20,6 +20,7 @@
package org.isoron.uhabits.acceptance.steps
import android.os.Build.VERSION.SDK_INT
import android.os.SystemClock.sleep
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiSelector
import org.isoron.uhabits.BaseUserInterfaceTest.Companion.device
@ -39,7 +40,7 @@ fun exportFullBackup() {
}
fun clearDownloadFolder() {
device.executeShellCommand("rm -rf /sdcard/Download/")
device.executeShellCommand("rm -rf /sdcard/Download")
}
fun clearBackupFolder() {
@ -86,6 +87,7 @@ fun importBackupFromDownloadFolder() {
device.findObject(UiSelector().textContains("Loop")).click()
} else {
device.click(50, 90) // Click menu button
Thread.sleep(1000)
device.findObject(UiSelector().textContains("Download")).click()
device.findObject(UiSelector().textContains("Loop")).click()
}

@ -18,7 +18,9 @@
*/
package org.isoron.uhabits.acceptance.steps
import android.os.Build.VERSION
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES
import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso
@ -72,7 +74,7 @@ object CommonSteps : BaseUserInterfaceTest() {
}
fun offsetHeaders() {
device.swipe(750, 160, 600, 160, 20)
device.swipe(500, 160, 350, 160, 20)
}
fun scrollToText(text: String?) {
@ -133,7 +135,7 @@ object CommonSteps : BaseUserInterfaceTest() {
@Throws(Exception::class)
fun verifyOpensWebsite(url: String?) {
var browserPkg = "org.chromium.webview_shell"
if (VERSION.SDK_INT <= 23) {
if (SDK_INT <= Build.VERSION_CODES.M) {
browserPkg = "com.android.browser"
}
assertTrue(device.wait(Until.hasObject(By.pkg(browserPkg)), 5000))
@ -178,6 +180,22 @@ object CommonSteps : BaseUserInterfaceTest() {
EditHabitSteps.clickSave()
}
fun changeFrequencyToDaily(habitName: String) {
clickText(habitName)
Espresso.onView(ViewMatchers.withId(R.id.action_edit_habit)).perform(ViewActions.click())
EditHabitSteps.pickDailyFrequency()
EditHabitSteps.clickSave()
pressBack()
}
fun changeFrequencyToMonthly(habitName: String) {
clickText(habitName)
Espresso.onView(ViewMatchers.withId(R.id.action_edit_habit)).perform(ViewActions.click())
EditHabitSteps.pickMonthFrequency()
EditHabitSteps.clickSave()
pressBack()
}
enum class Screen {
LIST_HABITS, SHOW_HABIT, EDIT_HABIT, SELECT_HABIT_TYPE
}

@ -36,6 +36,24 @@ object EditHabitSteps {
Espresso.onView(ViewMatchers.withText("SAVE")).perform(ViewActions.click())
}
fun pickMonthFrequency() {
Espresso.onView(ViewMatchers.withId(R.id.boolean_frequency_picker))
.perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.xTimesPerMonthRadioButton))
.perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.xTimesPerMonthTextView))
.perform(ViewActions.replaceText("1"))
Espresso.onView(ViewMatchers.withText("SAVE")).perform(ViewActions.click())
}
fun pickDailyFrequency() {
Espresso.onView(ViewMatchers.withId(R.id.boolean_frequency_picker))
.perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.everyDayRadioButton))
.perform(ViewActions.click())
Espresso.onView(ViewMatchers.withText("SAVE")).perform(ViewActions.click())
}
fun pickColor(color: Int) {
Espresso.onView(ViewMatchers.withId(R.id.colorButton)).perform(ViewActions.click())
BaseUserInterfaceTest.device.findObject(By.descStartsWith(String.format("Color %d", color)))

@ -53,6 +53,7 @@ object ListHabitsSteps {
clickViewWithId(R.id.action_filter)
CommonSteps.clickText(R.string.hide_completed)
}
else -> throw RuntimeException()
}
device.waitForIdle()
}
@ -120,6 +121,12 @@ object ListHabitsSteps {
BaseUserInterfaceTest.device.waitForIdle()
}
fun changeSort(sortText: String) {
clickViewWithId(R.id.action_filter)
Espresso.onView(ViewMatchers.withText("Sort")).perform(ViewActions.click())
Espresso.onView(ViewMatchers.withText(sortText)).perform(ViewActions.click())
}
enum class MenuItem {
ABOUT, HELP, SETTINGS, EDIT, DELETE, ARCHIVE, TOGGLE_ARCHIVED, UNARCHIVE, TOGGLE_COMPLETED, ADD
}

@ -18,7 +18,7 @@
*/
package org.isoron.uhabits.acceptance.steps
import android.os.Build.VERSION
import android.os.Build.VERSION.SDK_INT
import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import junit.framework.Assert.assertFalse
@ -50,29 +50,21 @@ object WidgetSteps {
private fun openWidgetScreen() {
val h = BaseUserInterfaceTest.device.displayHeight
val w = BaseUserInterfaceTest.device.displayWidth
if (VERSION.SDK_INT <= 21) {
BaseUserInterfaceTest.device.pressHome()
BaseUserInterfaceTest.device.waitForIdle()
BaseUserInterfaceTest.device.findObject(UiSelector().description("Apps")).click()
BaseUserInterfaceTest.device.findObject(UiSelector().description("Apps")).click()
BaseUserInterfaceTest.device.findObject(UiSelector().description("Widgets")).click()
} else {
val listId = "com.android.launcher3:id/widgets_list_view"
BaseUserInterfaceTest.device.pressHome()
BaseUserInterfaceTest.device.waitForIdle()
BaseUserInterfaceTest.device.drag(w / 2, h / 2, w / 2, h / 2, 8)
var button = BaseUserInterfaceTest.device.findObject(UiSelector().text("WIDGETS"))
if (!button.waitForExists(1000)) {
button = BaseUserInterfaceTest.device.findObject(UiSelector().text("Widgets"))
}
button.click()
if (VERSION.SDK_INT >= 28) {
UiScrollable(UiSelector().resourceId(listId))
.scrollForward()
}
val listId = "com.android.launcher3:id/widgets_list_view"
BaseUserInterfaceTest.device.pressHome()
BaseUserInterfaceTest.device.waitForIdle()
BaseUserInterfaceTest.device.drag(w / 2, h / 2, w / 2, h / 2, 8)
var button = BaseUserInterfaceTest.device.findObject(UiSelector().text("WIDGETS"))
if (!button.waitForExists(1000)) {
button = BaseUserInterfaceTest.device.findObject(UiSelector().text("Widgets"))
}
button.click()
if (SDK_INT >= 28) {
UiScrollable(UiSelector().resourceId(listId))
.scrollIntoView(UiSelector().text("Checkmark"))
.scrollForward()
}
UiScrollable(UiSelector().resourceId(listId))
.scrollIntoView(UiSelector().text("Checkmark"))
}
@Throws(Exception::class)

@ -29,6 +29,10 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
class EmptyListViewTest : BaseViewTest() {
init {
// TODO: fix rendering differences across APIs
similarityCutoff = 0.00035
}
private val path = "habits/list/EmptyListView"
private val view: EmptyListView = EmptyListView(targetContext)

@ -36,6 +36,7 @@ class EntryButtonViewTest : BaseViewTest() {
lateinit var view: CheckmarkButtonView
var toggled = false
var edited = false
@Before
override fun setUp() {
@ -43,7 +44,8 @@ class EntryButtonViewTest : BaseViewTest() {
view = component.getEntryButtonViewFactory().create().apply {
value = Entry.NO
color = PaletteUtils.getAndroidTestColor(5)
onToggle = { toggled = true }
onToggle = { _, _, _ -> toggled = true }
onEdit = { edited = true }
}
measureView(view, dpToPixels(48), dpToPixels(48))
}
@ -70,20 +72,28 @@ class EntryButtonViewTest : BaseViewTest() {
fun testClick_withShortToggleDisabled() {
prefs.isShortToggleEnabled = false
view.performClick()
assertFalse(toggled)
assertTrue(!toggled and edited)
}
@Test
fun testClick_withShortToggleEnabled() {
prefs.isShortToggleEnabled = true
view.performClick()
assertTrue(toggled)
assertTrue(toggled and !edited)
}
@Test
fun testLongClick() {
fun testLongClick_withShortToggleDisabled() {
prefs.isShortToggleEnabled = false
view.performLongClick()
assertTrue(toggled and !edited)
}
@Test
fun testLongClick_withShortToggleEnabled() {
prefs.isShortToggleEnabled = true
view.performLongClick()
assertTrue(toggled)
assertTrue(!toggled and edited)
}
private fun assertRendersCheckedExplicitly() {

@ -77,7 +77,7 @@ class EntryPanelViewTest : BaseViewTest() {
@Test
fun testToggle() {
val timestamps = mutableListOf<Timestamp>()
view.onToggle = { t, _ -> timestamps.add(t) }
view.onToggle = { t, _, _, _ -> timestamps.add(t) }
view.buttons[0].performLongClick()
view.buttons[2].performLongClick()
view.buttons[3].performLongClick()
@ -88,7 +88,7 @@ class EntryPanelViewTest : BaseViewTest() {
fun testToggle_withOffset() {
val timestamps = mutableListOf<Timestamp>()
view.dataOffset = 3
view.onToggle = { t, _ -> timestamps += t }
view.onToggle = { t, _, _, _ -> timestamps += t }
view.buttons[0].performLongClick()
view.buttons[2].performLongClick()
view.buttons[3].performLongClick()

@ -76,7 +76,7 @@ class NumberPanelViewTest : BaseViewTest() {
@Test
fun testEdit() {
val timestamps = mutableListOf<Timestamp>()
view.onEdit = { timestamps.plusAssign(it) }
view.onEdit = { t -> timestamps.plusAssign(t) }
view.buttons[0].performLongClick()
view.buttons[2].performLongClick()
view.buttons[3].performLongClick()
@ -87,7 +87,7 @@ class NumberPanelViewTest : BaseViewTest() {
fun testEdit_withOffset() {
val timestamps = mutableListOf<Timestamp>()
view.dataOffset = 3
view.onEdit = { timestamps += it }
view.onEdit = { t -> timestamps += t }
view.buttons[0].performLongClick()
view.buttons[2].performLongClick()
view.buttons[3].performLongClick()

@ -21,9 +21,12 @@ package org.isoron.uhabits.regression
import androidx.test.filters.LargeTest
import org.isoron.uhabits.BaseUserInterfaceTest
import org.isoron.uhabits.acceptance.steps.CommonSteps
import org.isoron.uhabits.acceptance.steps.CommonSteps.Screen.EDIT_HABIT
import org.isoron.uhabits.acceptance.steps.CommonSteps.Screen.LIST_HABITS
import org.isoron.uhabits.acceptance.steps.CommonSteps.Screen.SELECT_HABIT_TYPE
import org.isoron.uhabits.acceptance.steps.CommonSteps.changeFrequencyToDaily
import org.isoron.uhabits.acceptance.steps.CommonSteps.changeFrequencyToMonthly
import org.isoron.uhabits.acceptance.steps.CommonSteps.clickText
import org.isoron.uhabits.acceptance.steps.CommonSteps.createHabit
import org.isoron.uhabits.acceptance.steps.CommonSteps.launchApp
@ -37,9 +40,12 @@ import org.isoron.uhabits.acceptance.steps.EditHabitSteps.clickSave
import org.isoron.uhabits.acceptance.steps.EditHabitSteps.typeName
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.MenuItem.ADD
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.MenuItem.DELETE
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.changeSort
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.clickMenu
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.longPressCheckmarks
import org.isoron.uhabits.core.models.Entry.Companion.NO
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
import org.junit.Test
@ -83,4 +89,37 @@ class ListHabitsRegressionTest : BaseUserInterfaceTest() {
offsetHeaders()
verifyDisplaysCheckmarks("Wake up early", listOf(UNKNOWN, UNKNOWN, UNKNOWN, UNKNOWN))
}
/**
* https://github.com/iSoron/uhabits/issues/1131
*/
@Test
@Throws(Exception::class)
fun should_refresh_sort_after_habit_edit() {
launchApp()
verifyShowsScreen(LIST_HABITS)
changeSort("By score")
changeSort("By status")
longPressCheckmarks("Meditate", count = 1)
changeFrequencyToMonthly("Read books")
longPressCheckmarks("Read books", count = 2)
longPressCheckmarks("Read books", count = 1)
verifyDisplaysCheckmarks("Meditate", listOf(YES_AUTO, YES_MANUAL, YES_AUTO, YES_MANUAL))
CommonSteps.verifyDisplaysTextInSequence(
"Wake up early",
"Read books",
"Meditate",
"Track time"
)
changeFrequencyToDaily("Meditate")
verifyDisplaysCheckmarks("Meditate", listOf(NO, YES_MANUAL, UNKNOWN, YES_MANUAL))
CommonSteps.verifyDisplaysTextInSequence(
"Wake up early",
"Meditate",
"Read books",
"Track time",
)
}
}

@ -32,6 +32,7 @@ import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.EntryList
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset
import org.junit.Test
import org.junit.runner.RunWith
@ -42,10 +43,12 @@ class CheckmarkWidgetTest : BaseViewTest() {
private lateinit var habit: Habit
private lateinit var entries: EntryList
private lateinit var view: FrameLayout
private val today = getTodayWithOffset()
private lateinit var today: Timestamp
override fun setUp() {
super.setUp()
setTheme(R.style.WidgetTheme)
today = getTodayWithOffset()
prefs.widgetOpacity = 255
prefs.isSkipEnabled = true
habit = fixtures.createVeryLongHabit()

@ -32,6 +32,10 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
class TargetWidgetTest : BaseViewTest() {
init {
// TODO: fix rendering differences across APIs
similarityCutoff = 0.00025
}
private lateinit var habit: Habit
private lateinit var view: FrameLayout
override fun setUp() {

@ -127,18 +127,6 @@
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,7 +22,6 @@ 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;
@ -43,17 +42,13 @@ 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 (isJellybeanOrLater() && view != null && text != null) {
if (view != null && text != null) {
view.announceForAccessibility(text);
}
}

@ -383,10 +383,6 @@ 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;
}
}

@ -1,116 +0,0 @@
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
}
}
}

@ -0,0 +1,128 @@
/*
* 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(context!!)
val pc = c.toPaletteColor(requireContext())
callback.onColorPicked(pc)
}
}

@ -149,8 +149,10 @@ 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,6 +19,7 @@
package org.isoron.uhabits.activities.common.dialogs
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import androidx.appcompat.app.AppCompatDialogFragment
import org.isoron.platform.gui.AndroidDataView
@ -43,18 +44,19 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
private lateinit var commandRunner: CommandRunner
private lateinit var habit: Habit
private lateinit var preferences: Preferences
private lateinit var dataView: AndroidDataView
lateinit var dataView: AndroidDataView
private var chart: HistoryChart? = null
private var onDateClickedListener: OnDateClickedListener? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val component = (activity!!.application as HabitsApplication).component
clearCurrentDialog()
val component = (requireActivity().application as HabitsApplication).component
commandRunner = component.commandRunner
habit = component.habitList.getById(arguments!!.getLong("habit"))!!
habit = component.habitList.getById(requireArguments().getLong("habit"))!!
preferences = component.preferences
val themeSwitcher = AndroidThemeSwitcher(activity!!, preferences)
val themeSwitcher = AndroidThemeSwitcher(requireActivity(), preferences)
themeSwitcher.apply()
chart = HistoryChart(
@ -69,15 +71,23 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
onDateClickedListener = onDateClickedListener ?: object : OnDateClickedListener {},
padding = 10.0,
)
dataView = AndroidDataView(context!!, null)
dataView = AndroidDataView(requireContext(), null)
dataView.view = chart!!
return Dialog(context!!).apply {
val dialog = Dialog(requireContext()).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() {
@ -111,4 +121,14 @@ 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
}
}
}

@ -1,155 +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.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
}
}

@ -0,0 +1,116 @@
/*
* 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(
activity!!
requireActivity()
)
builder
.setTitle(R.string.select_weekdays)
@ -73,6 +73,7 @@ class WeekdayPickerDialog :
.setNegativeButton(
android.R.string.cancel
) { _: DialogInterface?, _: Int -> dismiss() }
return builder.create()
}

@ -29,6 +29,7 @@ import org.isoron.uhabits.core.utils.DateUtils.Companion.getShortWeekdayNames
import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendar
import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendarWithOffset
import org.isoron.uhabits.core.utils.DateUtils.Companion.getWeekdaySequence
import org.isoron.uhabits.core.utils.DateUtils.Companion.getWeekdaysInMonth
import org.isoron.uhabits.utils.ColorUtils.mixColors
import org.isoron.uhabits.utils.StyledResources
import org.isoron.uhabits.utils.toSimpleDataFormat
@ -64,6 +65,7 @@ class FrequencyChart : ScrollableChart {
private lateinit var frequency: HashMap<Timestamp, Array<Int>>
private var maxFreq = 0
private var firstWeekday = Calendar.SUNDAY
private var isNumerical: Boolean = false
constructor(context: Context?) : super(context) {
init()
@ -80,6 +82,11 @@ class FrequencyChart : ScrollableChart {
postInvalidate()
}
fun setIsNumerical(type: Boolean) {
isNumerical = type
postInvalidate()
}
fun setFrequency(frequency: java.util.HashMap<Timestamp, Array<Int>>) {
this.frequency = frequency
maxFreq = getMaxFreq(frequency)
@ -166,6 +173,7 @@ class FrequencyChart : ScrollableChart {
private fun drawColumn(canvas: Canvas, rect: RectF?, date: GregorianCalendar) {
val values = frequency[Timestamp(date)]
val weekDaysInMonth = getWeekdaysInMonth(Timestamp(date))
val rowHeight = rect!!.height() / 8.0f
prevRect!!.set(rect)
val localeWeekdayList: Array<Int> = getWeekdaySequence(firstWeekday)
@ -173,7 +181,8 @@ class FrequencyChart : ScrollableChart {
rect[0f, 0f, baseSize.toFloat()] = baseSize.toFloat()
rect.offset(prevRect!!.left, prevRect!!.top + baseSize * j)
val i = localeWeekdayList[j] % 7
if (values != null) drawMarker(canvas, rect, values[i])
if (values != null)
drawMarker(canvas, rect, values[i], weekDaysInMonth[i])
rect.offset(0f, rowHeight)
}
drawFooter(canvas, rect, date)
@ -221,12 +230,16 @@ class FrequencyChart : ScrollableChart {
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid!!)
}
private fun drawMarker(canvas: Canvas, rect: RectF?, value: Int?) {
private fun drawMarker(canvas: Canvas, rect: RectF?, value: Int?, weekdayFrequency: Int) {
// value can be negative when the entry is skipped
val valueCopy = value?.let { max(0, it) }
val padding = rect!!.height() * 0.2f
// maximal allowed mark radius
val maxRadius = (rect.height() - 2 * padding) / 2.0f
// the real mark radius is scaled down by a factor depending on the maximal frequency
val scale = 1.0f / maxFreq * value!!
val scalingFactor = if (isNumerical) maxFreq else weekdayFrequency
val scale = 1.0f / scalingFactor * valueCopy!!
val radius = maxRadius * scale
val colorIndex = min((colors.size - 1), ((colors.size - 1) * scale).roundToInt())
pGraph!!.color = colors[colorIndex]

@ -59,6 +59,7 @@ import org.isoron.uhabits.core.models.Reminder
import org.isoron.uhabits.core.models.WeekdayList
import org.isoron.uhabits.databinding.ActivityEditHabitBinding
import org.isoron.uhabits.utils.ColorUtils
import org.isoron.uhabits.utils.dismissCurrentAndShow
import org.isoron.uhabits.utils.formatTime
import org.isoron.uhabits.utils.toFormattedString
@ -156,23 +157,23 @@ class EditHabitActivity : AppCompatActivity() {
val colorPickerDialogFactory = ColorPickerDialogFactory(this)
binding.colorButton.setOnClickListener {
val dialog = colorPickerDialogFactory.create(color, themeSwitcher.currentTheme)
dialog.setListener { paletteColor ->
val picker = colorPickerDialogFactory.create(color, themeSwitcher.currentTheme)
picker.setListener { paletteColor ->
this.color = paletteColor
updateColors()
}
dialog.show(supportFragmentManager, "colorPicker")
picker.dismissCurrentAndShow(supportFragmentManager, "colorPicker")
}
populateFrequency()
binding.booleanFrequencyPicker.setOnClickListener {
val dialog = FrequencyPickerDialog(freqNum, freqDen)
dialog.onFrequencyPicked = { num, den ->
val picker = FrequencyPickerDialog(freqNum, freqDen)
picker.onFrequencyPicked = { num, den ->
freqNum = num
freqDen = den
populateFrequency()
}
dialog.show(supportFragmentManager, "frequencyPicker")
picker.dismissCurrentAndShow(supportFragmentManager, "frequencyPicker")
}
populateTargetType()
@ -189,7 +190,8 @@ class EditHabitActivity : AppCompatActivity() {
populateTargetType()
dialog.dismiss()
}
builder.show()
val dialog = builder.create()
dialog.dismissCurrentAndShow()
}
binding.numericalFrequencyPicker.setOnClickListener {
@ -235,7 +237,7 @@ class EditHabitActivity : AppCompatActivity() {
is24HourMode,
androidColor
)
dialog.show(supportFragmentManager, "timePicker")
dialog.dismissCurrentAndShow(supportFragmentManager, "timePicker")
}
binding.reminderDatePicker.setOnClickListener {
@ -247,7 +249,7 @@ class EditHabitActivity : AppCompatActivity() {
populateReminder()
}
dialog.setSelectedDays(reminderDays)
dialog.show(supportFragmentManager, "dayPicker")
dialog.dismissCurrentAndShow(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(activity!!, HabitType.YES_NO.value)
val intent = IntentFactory().startEditActivity(requireActivity(), HabitType.YES_NO.value)
startActivity(intent)
dismiss()
}
binding.buttonMeasurable.setOnClickListener {
val intent = IntentFactory().startEditActivity(activity!!, HabitType.NUMERICAL.value)
val intent = IntentFactory().startEditActivity(requireActivity(), HabitType.NUMERICAL.value)
startActivity(intent)
dismiss()
}

@ -21,6 +21,7 @@ package org.isoron.uhabits.activities.habits.list
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
@ -29,6 +30,7 @@ import kotlinx.coroutines.Dispatchers
import org.isoron.uhabits.BaseExceptionHandler
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.tasks.TaskRunner
import org.isoron.uhabits.core.ui.ThemeSwitcher.Companion.THEME_DARK
@ -36,11 +38,15 @@ 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
@ -59,8 +65,8 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val appComponent = (applicationContext as HabitsApplication).component
val component = DaggerHabitsActivityComponent
appComponent = (applicationContext as HabitsApplication).component
component = DaggerHabitsActivityComponent
.builder()
.activityContextModule(ActivityContextModule(this))
.habitsApplicationComponent(appComponent)
@ -94,11 +100,17 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
rootView.postInvalidate()
midnightTimer.onResume()
taskRunner.run {
AutoBackup(this@ListHabitsActivity).run()
try {
AutoBackup(this@ListHabitsActivity).run()
appComponent.widgetUpdater.updateWidgets()
} catch (e: Exception) {
Log.e("ListHabitActivity", "TaskRunner failed", e)
}
}
if (prefs.theme == THEME_DARK && prefs.isPureBlackEnabled != pureBlack) {
restartWithFade(ListHabitsActivity::class.java)
}
parseIntents()
super.onResume()
}
@ -116,4 +128,26 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
super.onActivityResult(request, result, data)
screen.onResult(request, result, data)
}
private fun parseIntents() {
if (intent == null) return
if (intent.action == ACTION_EDIT) {
val habitId = intent.extras?.getLong("habit")
val timestamp = intent.extras?.getLong("timestamp")
if (habitId != null && timestamp != null) {
val habit = appComponent.habitList.getById(habitId)!!
component.listHabitsBehavior.onEdit(habit, Timestamp(timestamp))
}
}
intent = null
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
setIntent(intent)
}
companion object {
const val ACTION_EDIT = "org.isoron.uhabits.ACTION_EDIT"
}
}

@ -24,11 +24,12 @@ 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.CheckmarkDialog
import org.isoron.uhabits.activities.common.dialogs.CheckmarkPopup
import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
import org.isoron.uhabits.activities.common.dialogs.NumberPopup
import org.isoron.uhabits.activities.habits.edit.HabitTypeDialog
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
import org.isoron.uhabits.core.commands.ArchiveHabitsCommand
@ -41,6 +42,7 @@ import org.isoron.uhabits.core.commands.EditHabitCommand
import org.isoron.uhabits.core.commands.UnarchiveHabitsCommand
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.tasks.TaskRunner
import org.isoron.uhabits.core.ui.ThemeSwitcher
import org.isoron.uhabits.core.ui.callbacks.OnColorPickedCallback
@ -61,6 +63,8 @@ import org.isoron.uhabits.tasks.ExportDBTaskFactory
import org.isoron.uhabits.tasks.ImportDataTask
import org.isoron.uhabits.tasks.ImportDataTaskFactory
import org.isoron.uhabits.utils.copyTo
import org.isoron.uhabits.utils.currentTheme
import org.isoron.uhabits.utils.dismissCurrentAndShow
import org.isoron.uhabits.utils.restartWithFade
import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.showSendEmailScreen
@ -89,9 +93,9 @@ class ListHabitsScreen
private val exportDBFactory: ExportDBTaskFactory,
private val importTaskFactory: ImportDataTaskFactory,
private val colorPickerFactory: ColorPickerDialogFactory,
private val numberPickerFactory: NumberPickerFactory,
private val checkMarkDialog: CheckmarkDialog,
private val behavior: Lazy<ListHabitsBehavior>
private val behavior: Lazy<ListHabitsBehavior>,
private val preferences: Preferences,
private val rootView: Lazy<ListHabitsRootView>,
) : CommandRunner.Listener,
ListHabitsBehavior.Screen,
ListHabitsMenuBehavior.Screen,
@ -160,7 +164,7 @@ class ListHabitsScreen
}
override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback, quantity: Int) {
ConfirmDeleteDialog(activity, callback, quantity).show()
ConfirmDeleteDialog(activity, callback, quantity).dismissCurrentAndShow()
}
override fun showEditHabitsScreen(selected: List<Habit>) {
@ -221,34 +225,45 @@ class ListHabitsScreen
override fun showColorPicker(defaultColor: PaletteColor, callback: OnColorPickedCallback) {
val picker = colorPickerFactory.create(defaultColor, themeSwitcher.currentTheme!!)
picker.setListener(callback)
picker.show(activity.supportFragmentManager, "picker")
picker.dismissCurrentAndShow(activity.supportFragmentManager, "picker")
}
override fun showNumberPicker(
override fun showNumberPopup(
value: Double,
unit: String,
notes: String,
dateString: String,
callback: ListHabitsBehavior.NumberPickerCallback
) {
numberPickerFactory.create(value, unit, notes, dateString, callback).show()
val view = rootView.get()
NumberPopup(
context = context,
prefs = preferences,
anchor = view,
notes = notes,
value = value,
).apply {
onToggle = { value, notes -> callback.onNumberPicked(value, notes) }
show()
}
}
override fun showCheckmarkDialog(
value: Int,
override fun showCheckmarkPopup(
selectedValue: Int,
notes: String,
dateString: String,
color: PaletteColor,
callback: ListHabitsBehavior.CheckMarkDialogCallback
) {
checkMarkDialog.create(
value,
notes,
dateString,
color,
callback,
themeSwitcher.currentTheme!!,
).show()
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()
}
}
private fun getExecuteString(command: Command): String? {

@ -44,6 +44,8 @@ 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,
@ -71,42 +73,42 @@ class CheckmarkButtonView(
invalidate()
}
var hasNotes = false
var notes = ""
set(value) {
field = value
invalidate()
}
var onToggle: (Int) -> Unit = {}
var onToggle: (Int, String, Long) -> Unit = { _, _, _ -> }
var onEdit: () -> Unit = { }
var onEdit: () -> Unit = {}
private var drawer = Drawer()
init {
isFocusable = false
setOnClickListener(this)
setOnLongClickListener(this)
}
fun performToggle() {
fun performToggle(delay: Long) {
value = Entry.nextToggleValue(
value = value,
isSkipEnabled = preferences.isSkipEnabled,
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
)
onToggle(value)
onToggle(value, notes, delay)
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
invalidate()
}
override fun onClick(v: View) {
if (preferences.isShortToggleEnabled) performToggle()
if (preferences.isShortToggleEnabled) performToggle(TOGGLE_DELAY_MILLIS)
else onEdit()
}
override fun onLongClick(v: View): Boolean {
if (preferences.isShortToggleEnabled) onEdit()
else performToggle()
else performToggle(TOGGLE_DELAY_MILLIS)
return true
}
@ -180,7 +182,7 @@ class CheckmarkButtonView(
canvas.drawText(label, rect.centerX(), rect.centerY(), paint)
}
drawNotesIndicator(canvas, color, em, hasNotes)
drawNotesIndicator(canvas, color, em, notes)
}
}
}

@ -54,19 +54,19 @@ class CheckmarkPanelView(
setupButtons()
}
var notesIndicators = BooleanArray(0)
var notes = arrayOf<String>()
set(values) {
field = values
setupButtons()
}
var onToggle: (Timestamp, Int) -> Unit = { _, _ -> }
var onToggle: (Timestamp, Int, String, Long) -> 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.hasNotes = when {
index + dataOffset < notesIndicators.size -> notesIndicators[index + dataOffset]
else -> false
button.notes = when {
index + dataOffset < notes.size -> notes[index + dataOffset]
else -> ""
}
button.color = color
button.onToggle = { value -> onToggle(timestamp, value) }
button.onToggle = { value, notes, delay -> onToggle(timestamp, value, notes, delay) }
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 notesIndicators = cache.getNoteIndicators(habit.id!!)
val notes = cache.getNotes(habit.id!!)
val selected = selected.contains(habit)
listView!!.bindCardView(holder, habit, score, checkmarks, notesIndicators, selected)
listView!!.bindCardView(holder, habit, score, checkmarks, notes, selected)
}
override fun onViewAttachedToWindow(holder: HabitCardViewHolder) {

@ -87,7 +87,7 @@ class HabitCardListView(
habit: Habit,
score: Double,
checkmarks: IntArray,
notesIndicators: BooleanArray,
notes: Array<String>,
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.notesIndicators = notesIndicators
cardView.notes = notes
val detector = GestureDetector(context, CardViewGestureDetector(holder))
cardView.setOnTouchListener { _, ev ->

@ -21,8 +21,8 @@ package org.isoron.uhabits.activities.habits.list.views
import android.content.Context
import android.graphics.text.LineBreaker.BREAK_STRATEGY_BALANCED
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.M
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
@ -60,7 +60,8 @@ class HabitCardViewFactory
data class DelayedToggle(
var habit: Habit,
var timestamp: Timestamp,
var value: Int
var value: Int,
var notes: String
)
class HabitCardView(
@ -121,11 +122,11 @@ class HabitCardView(
numberPanel.threshold = value
}
var notesIndicators
get() = checkmarkPanel.notesIndicators
var notes
get() = checkmarkPanel.notes
set(values) {
checkmarkPanel.notesIndicators = values
numberPanel.notesIndicators = values
checkmarkPanel.notes = values
numberPanel.notes = values
}
var checkmarkPanel: CheckmarkPanelView
@ -153,15 +154,17 @@ class HabitCardView(
maxLines = 2
ellipsize = TextUtils.TruncateAt.END
layoutParams = LinearLayout.LayoutParams(0, WRAP_CONTENT, 1f)
if (SDK_INT >= M) breakStrategy = BREAK_STRATEGY_BALANCED
if (SDK_INT >= Build.VERSION_CODES.Q) {
breakStrategy = BREAK_STRATEGY_BALANCED
}
}
checkmarkPanel = checkmarkPanelFactory.create().apply {
onToggle = { timestamp, value ->
triggerRipple(timestamp)
onToggle = { timestamp, value, notes, delay ->
if (delay > 0) triggerRipple(timestamp)
habit?.let {
val taskId = queueToggle(it, timestamp, value);
{ runPendingToggles(taskId) }.delay(TOGGLE_DELAY_MILLIS)
val taskId = queueToggle(it, timestamp, value, notes);
{ runPendingToggles(taskId) }.delay(delay)
}
}
onEdit = { timestamp ->
@ -205,7 +208,7 @@ class HabitCardView(
@Synchronized
private fun runPendingToggles(id: Int) {
if (currentToggleTaskId != id) return
for ((h, t, v) in queuedToggles) behavior.onToggle(h, t, v)
for ((h, t, v, n) in queuedToggles) behavior.onToggle(h, t, v, n)
queuedToggles.clear()
}
@ -213,10 +216,11 @@ class HabitCardView(
private fun queueToggle(
it: Habit,
timestamp: Timestamp,
value: Int
value: Int,
notes: String,
): Int {
currentToggleTaskId += 1
queuedToggles.add(DelayedToggle(it, timestamp, value))
queuedToggles.add(DelayedToggle(it, timestamp, value, notes))
return currentToggleTaskId
}
@ -306,8 +310,6 @@ class HabitCardView(
}
companion object {
const val TOGGLE_DELAY_MILLIS = 2000L
fun (() -> Unit).delay(delayInMillis: Long) {
Handler(Looper.getMainLooper()).postDelayed(this, delayInMillis)
}

@ -29,7 +29,9 @@ 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.NumericalHabitType
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.NumericalHabitType.AT_LEAST
import org.isoron.uhabits.core.models.NumericalHabitType.AT_MOST
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.InterfaceUtils.getDimension
@ -37,7 +39,6 @@ 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
@ -90,7 +91,7 @@ class NumberButtonView(
invalidate()
}
var targetType = NumericalHabitType.AT_LEAST
var targetType = AT_LEAST
set(value) {
field = value
invalidate()
@ -101,13 +102,14 @@ class NumberButtonView(
field = value
invalidate()
}
var hasNotes = false
var notes = ""
set(value) {
field = value
invalidate()
}
var onEdit: () -> Unit = {}
var onEdit: () -> Unit = { }
private var drawer: Drawer = Drawer(context)
init {
@ -143,6 +145,12 @@ 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
@ -164,18 +172,11 @@ class NumberButtonView(
}
fun draw(canvas: Canvas) {
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 activeColor = when {
value < 0.0 -> lowContrast
(targetType == AT_LEAST) && (value >= threshold) -> color
(targetType == AT_MOST) && (value <= threshold) -> color
else -> mediumContrast
}
val label: String
@ -183,6 +184,11 @@ class NumberButtonView(
val textSize: Float
when {
value == Entry.SKIP.toDouble() / 1000 -> {
label = resources.getString(R.string.fa_skipped)
textSize = dim(R.dimen.smallTextSize)
typeface = getFontAwesome()
}
value >= 0 -> {
label = value.toShortString()
typeface = BOLD_TYPEFACE
@ -216,7 +222,7 @@ class NumberButtonView(
canvas.drawText(units, rect.centerX(), rect.centerY(), pUnit)
}
drawNotesIndicator(canvas, color, em, hasNotes)
drawNotesIndicator(canvas, color, em, notes)
}
}
}

@ -72,13 +72,13 @@ class NumberPanelView(
setupButtons()
}
var notesIndicators = BooleanArray(0)
var notes = arrayOf<String>()
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.hasNotes = when {
index + dataOffset < notesIndicators.size -> notesIndicators[index + dataOffset]
else -> false
button.notes = when {
index + dataOffset < notes.size -> notes[index + dataOffset]
else -> ""
}
button.color = color
button.targetType = targetType

@ -23,19 +23,21 @@ import android.os.Bundle
import android.view.HapticFeedbackConstants
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.isoron.platform.gui.toInt
import org.isoron.uhabits.AndroidDirFinder
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.activities.HabitsDirFinder
import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog
import org.isoron.uhabits.activities.common.dialogs.CheckmarkPopup
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
import org.isoron.uhabits.activities.common.dialogs.NumberPopup
import org.isoron.uhabits.core.commands.Command
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.models.Habit
@ -47,6 +49,8 @@ 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
@ -163,32 +167,49 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
window.decorView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
}
override fun showNumberPicker(
override fun showNumberPopup(
value: Double,
unit: String,
notes: String,
dateString: String,
callback: ListHabitsBehavior.NumberPickerCallback,
preferences: Preferences,
callback: ListHabitsBehavior.NumberPickerCallback
) {
NumberPickerFactory(this@ShowHabitActivity).create(value, unit, notes, dateString, callback).show()
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()
}
}
override fun showCheckmarkDialog(
value: Int,
override fun showCheckmarkPopup(
selectedValue: Int,
notes: String,
dateString: String,
preferences: Preferences,
color: PaletteColor,
callback: ListHabitsBehavior.CheckMarkDialogCallback
) {
CheckmarkDialog(this@ShowHabitActivity, preferences).create(
value,
notes,
dateString,
color,
callback,
themeSwitcher.currentTheme!!,
).show()
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
}
override fun showEditHabitScreen(habit: Habit) {
@ -200,6 +221,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
ShowHabitMenuPresenter.Message.COULD_NOT_EXPORT -> {
showMessage(resources.getString(R.string.could_not_export))
}
else -> {}
}
}
@ -208,7 +230,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
}
override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback) {
ConfirmDeleteDialog(this@ShowHabitActivity, callback, 1).show()
ConfirmDeleteDialog(this@ShowHabitActivity, callback, 1).dismissCurrentAndShow()
}
override fun close() {

@ -33,6 +33,7 @@ class FrequencyCardView(context: Context, attrs: AttributeSet) : LinearLayout(co
fun setState(state: FrequencyCardState) {
val androidColor = state.theme.color(state.color).toInt()
binding.frequencyChart.setFrequency(state.frequency)
binding.frequencyChart.setIsNumerical(state.isNumerical)
binding.frequencyChart.setFirstWeekday(state.firstWeekday)
binding.title.setTextColor(androidColor)
binding.frequencyChart.setColor(androidColor)

@ -22,11 +22,10 @@ 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
@ -43,6 +42,7 @@ import org.isoron.uhabits.core.ui.NotificationTray
import org.isoron.uhabits.core.utils.DateUtils.Companion.getLongWeekdayNames
import org.isoron.uhabits.notifications.AndroidNotificationTray.Companion.createAndroidNotificationChannel
import org.isoron.uhabits.notifications.RingtoneManager
import org.isoron.uhabits.utils.StyledResources
import org.isoron.uhabits.widgets.WidgetUpdater
import java.util.Calendar
@ -63,7 +63,7 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
addPreferencesFromResource(R.xml.preferences)
val appContext = context!!.applicationContext
val appContext = requireContext().applicationContext
if (appContext is HabitsApplication) {
prefs = appContext.component.preferences
widgetUpdater = appContext.component.widgetUpdater
@ -84,16 +84,21 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
super.onPause()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val sr = StyledResources(context!!)
view.setBackgroundColor(sr.getColor(R.attr.contrast0))
super.onViewCreated(view, savedInstanceState)
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
val key = preference.key ?: return false
if (key == "reminderSound") {
showRingtonePicker()
return true
} else if (key == "reminderCustomize") {
if (VERSION.SDK_INT < Build.VERSION_CODES.O) return true
createAndroidNotificationChannel(context!!)
createAndroidNotificationChannel(requireContext())
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
intent.putExtra(Settings.EXTRA_APP_PACKAGE, context!!.packageName)
intent.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationTray.REMINDERS_CHANNEL_ID)
startActivity(intent)
return true
@ -103,7 +108,7 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
override fun onResume() {
super.onResume()
ringtoneManager = RingtoneManager(activity!!)
ringtoneManager = RingtoneManager(requireActivity())
sharedPrefs = preferenceManager.sharedPreferences
sharedPrefs!!.registerOnSharedPreferenceChangeListener(this)
if (!prefs.isDeveloper) {
@ -112,11 +117,7 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
}
updateWeekdayPreference()
if (VERSION.SDK_INT < Build.VERSION_CODES.O)
findPreference("reminderCustomize").isVisible = false
else {
findPreference("reminderSound").isVisible = false
}
findPreference("reminderSound").isVisible = false
}
private fun updateWeekdayPreference() {
@ -146,8 +147,8 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
val pref = findPreference(key)
pref.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
activity!!.setResult(result)
activity!!.finish()
requireActivity().setResult(result)
requireActivity().finish()
true
}
}

@ -21,11 +21,16 @@ 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
@ -87,6 +92,20 @@ 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?,
@ -127,32 +146,65 @@ class PendingIntentFactory
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
)
fun setNumericalValue(
widgetContext: Context,
habit: Habit,
numericalValue: Int,
timestamp: Long?
):
PendingIntent =
fun updateWidgets(): 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)
context,
0,
Intent(context, WidgetReceiver::class.java).apply {
action = WidgetReceiver.ACTION_UPDATE_WIDGETS_VALUE
},
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
)
fun updateWidgets(): PendingIntent =
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,
0,
2,
Intent(context, WidgetReceiver::class.java).apply {
action = WidgetReceiver.ACTION_UPDATE_WIDGETS_VALUE
action = WidgetReceiver.ACTION_TOGGLE_REPETITION
},
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
getIntentTemplateFlags()
)
fun toggleCheckmarkFillIn(habit: Habit, timestamp: Timestamp) = Intent().apply {
data = Uri.parse(habit.uriString)
putExtra("timestamp", timestamp.unixTime)
}
}

@ -25,7 +25,6 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.graphics.BitmapFactory.decodeResource
import android.graphics.Color
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.util.Log
@ -113,7 +112,7 @@ class AndroidNotificationTray
val enterAction = Action(
R.drawable.ic_action_check,
context.getString(R.string.enter),
pendingIntents.setNumericalValue(context, habit, 0, null)
pendingIntents.showNumberPicker(habit, timestamp)
)
val wearableBg = decodeResource(context.resources, R.drawable.stripe)
@ -150,9 +149,6 @@ 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,
@ -172,14 +168,12 @@ class AndroidNotificationTray
fun createAndroidNotificationChannel(context: Context) {
val notificationManager = context.getSystemService(Activity.NOTIFICATION_SERVICE)
as NotificationManager
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)
}
val channel = NotificationChannel(
REMINDERS_CHANNEL_ID,
context.resources.getString(R.string.reminder),
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager.createNotificationChannel(channel)
}
}
}

@ -90,8 +90,6 @@ class SharedPreferencesStorage
preferences.isMidnightDelayEnabled = getBoolean(key, false)
"pref_sticky_notifications" ->
preferences.setNotificationsSticky(getBoolean(key, false))
"pref_led_notifications" ->
preferences.setNotificationsLed(getBoolean(key, false))
"pref_unknown_enabled" -> {
preferences.areQuestionMarksEnabled = getBoolean(key, false)
}

@ -27,7 +27,6 @@ import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.core.ui.widgets.WidgetBehavior
import org.isoron.uhabits.inject.HabitsApplicationComponent
import org.isoron.uhabits.intents.IntentParser.CheckmarkIntentData
import org.isoron.uhabits.widgets.activities.NumericalCheckmarkWidgetActivity
/**
* The Android BroadcastReceiver for Loop Habit Tracker.
@ -96,15 +95,6 @@ class WidgetReceiver : BroadcastReceiver() {
data.timestamp
)
}
ACTION_SET_NUMERICAL_VALUE -> {
context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
val numberSelectorIntent = Intent(context, NumericalCheckmarkWidgetActivity::class.java)
numberSelectorIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
numberSelectorIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
numberSelectorIntent.action = NumericalCheckmarkWidgetActivity.ACTION_SHOW_NUMERICAL_VALUE_ACTIVITY
parser.copyIntentData(intent, numberSelectorIntent)
context.startActivity(numberSelectorIntent)
}
ACTION_UPDATE_WIDGETS_VALUE -> {
widgetUpdater.updateWidgets()
widgetUpdater.scheduleStartDayWidgetUpdate()
@ -126,7 +116,6 @@ class WidgetReceiver : BroadcastReceiver() {
const val ACTION_DISMISS_REMINDER = "org.isoron.uhabits.ACTION_DISMISS_REMINDER"
const val ACTION_REMOVE_REPETITION = "org.isoron.uhabits.ACTION_REMOVE_REPETITION"
const val ACTION_TOGGLE_REPETITION = "org.isoron.uhabits.ACTION_TOGGLE_REPETITION"
const val ACTION_SET_NUMERICAL_VALUE = "org.isoron.uhabits.ACTION_SET_NUMERICAL_VALUE"
const val ACTION_UPDATE_WIDGETS_VALUE = "org.isoron.uhabits.ACTION_UPDATE_WIDGETS_VALUE"
private const val TAG = "WidgetReceiver"
var lastReceivedIntent: Intent? = null

@ -0,0 +1,21 @@
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,19 +21,11 @@ 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) {
if (isAndroidOOrLater) {
val km = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
km.requestDismissKeyguard(activity, null)
} else {
activity.window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD)
}
val km = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
km.requestDismissKeyguard(activity, null)
}
}

@ -20,6 +20,7 @@
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
@ -27,11 +28,14 @@ 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
@ -202,10 +206,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, hasNotes: Boolean) {
fun View.drawNotesIndicator(canvas: Canvas, color: Int, size: Float, notes: String) {
val pNotesIndicator = Paint()
pNotesIndicator.color = color
if (hasNotes) {
if (notes.isNotBlank()) {
val cy = 0.8f * size
canvas.drawCircle(width.toFloat() - cy, cy, 8f, pNotesIndicator)
}
@ -213,3 +217,21 @@ fun View.drawNotesIndicator(canvas: Canvas, color: Int, size: Float, hasNotes: B
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,9 +21,7 @@ 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
@ -43,13 +41,12 @@ open class CheckmarkWidget(
override fun getOnClickPendingIntent(context: Context): PendingIntent? {
return if (habit.isNumerical) {
pendingIntentFactory.setNumericalValue(context, habit, 10, null)
pendingIntentFactory.showNumberPicker(habit, DateUtils.getToday())
} else {
pendingIntentFactory.toggleCheckmark(habit, null)
}
}
@RequiresApi(Build.VERSION_CODES.O)
override fun refreshData(widgetView: View) {
(widgetView as CheckmarkWidgetView).apply {
val today = DateUtils.getTodayWithOffset()

@ -49,6 +49,7 @@ class FrequencyWidget(
(widgetView.dataView as FrequencyChart).apply {
setFirstWeekday(firstWeekday)
setColor(WidgetTheme().color(habit.color).toInt())
setIsNumerical(habit.isNumerical)
setFrequency(habit.originalEntries.computeWeekdayFrequency(habit.isNumerical))
}
}

@ -73,6 +73,10 @@ class StackWidget(
StackWidgetType.getStackWidgetAdapterViewId(widgetType),
StackWidgetType.getStackWidgetEmptyViewId(widgetType)
)
remoteViews.setPendingIntentTemplate(
StackWidgetType.getStackWidgetAdapterViewId(widgetType),
StackWidgetType.getPendingIntentTemplate(pendingIntentFactory, widgetType, habits)
)
return remoteViews
}
}

@ -29,11 +29,14 @@ 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 {
@ -54,7 +57,6 @@ internal class StackRemoteViewsFactory(private val context: Context, intent: Int
)
private val habitIds: LongArray
private val widgetType: StackWidgetType
private var remoteViews = ArrayList<RemoteViews>()
override fun onCreate() {}
override fun onDestroy() {}
override fun getCount(): Int {
@ -85,8 +87,26 @@ internal class StackRemoteViewsFactory(private val context: Context, intent: Int
}
override fun getViewAt(position: Int): RemoteViews? {
Log.i("StackRemoteViewsFactory", "getViewAt $position")
return if (0 <= position && position < remoteViews.size) remoteViews[position] else null
Log.i("StackRemoteViewsFactory", "getViewAt $position started")
if (position < 0 || position >= habitIds.size) return null
val app = context.applicationContext as HabitsApplication
val prefs = app.component.preferences
val habitList = app.component.habitList
val options = AppWidgetManager.getInstance(context).getAppWidgetOptions(widgetId)
if (Looper.myLooper() == null) Looper.prepare()
val habits = habitIds.map { habitList.getById(it) ?: throw HabitNotFoundException() }
val h = habits[position]
val widget = constructWidget(h, prefs)
widget.setDimensions(getDimensionsFromOptions(context, options))
val landscapeViews = widget.landscapeRemoteViews
val portraitViews = widget.portraitRemoteViews
val factory = PendingIntentFactory(context, IntentFactory())
val intent = StackWidgetType.getIntentFillIn(factory, widgetType, h, habits, 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
}
private fun constructWidget(
@ -131,24 +151,6 @@ internal class StackRemoteViewsFactory(private val context: Context, intent: Int
}
override fun onDataSetChanged() {
Log.i("StackRemoteViewsFactory", "onDataSetChanged started")
val app = context.applicationContext as HabitsApplication
val prefs = app.component.preferences
val habitList = app.component.habitList
val options = AppWidgetManager.getInstance(context).getAppWidgetOptions(widgetId)
val newRemoteViews = ArrayList<RemoteViews>()
if (Looper.myLooper() == null) Looper.prepare()
for (id in habitIds) {
val h = habitList.getById(id) ?: throw HabitNotFoundException()
val widget = constructWidget(h, prefs)
widget.setDimensions(getDimensionsFromOptions(context, options))
val landscapeViews = widget.landscapeRemoteViews
val portraitViews = widget.portraitRemoteViews
newRemoteViews.add(RemoteViews(landscapeViews, portraitViews))
Log.i("StackRemoteViewsFactory", "onDataSetChanged constructed widget $id")
}
remoteViews = newRemoteViews
Log.i("StackRemoteViewsFactory", "onDataSetChanged ended")
}
init {

@ -18,7 +18,12 @@
*/
package org.isoron.uhabits.widgets
import android.app.PendingIntent
import android.content.Intent
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.intents.PendingIntentFactory
import java.lang.IllegalStateException
enum class StackWidgetType(val value: Int) {
@ -73,5 +78,39 @@ enum class StackWidgetType(val value: Int) {
else -> throw IllegalStateException()
}
}
fun getPendingIntentTemplate(
factory: PendingIntentFactory,
widgetType: StackWidgetType,
habits: List<Habit>
): PendingIntent {
val containsNumerical = habits.any { it.isNumerical }
return when (widgetType) {
CHECKMARK -> if (containsNumerical) {
factory.showNumberPickerTemplate()
} else {
factory.toggleCheckmarkTemplate()
}
FREQUENCY, SCORE, HISTORY, STREAKS, TARGET -> factory.showHabitTemplate()
}
}
fun getIntentFillIn(
factory: PendingIntentFactory,
widgetType: StackWidgetType,
habit: Habit,
allHabitsInStackWidget: List<Habit>,
timestamp: Timestamp
): Intent {
val containsNumerical = allHabitsInStackWidget.any { it.isNumerical }
return when (widgetType) {
CHECKMARK -> if (containsNumerical) {
factory.showNumberPickerFillIn(habit, timestamp)
} else {
factory.toggleCheckmarkFillIn(habit, timestamp)
}
FREQUENCY, SCORE, HISTORY, STREAKS, TARGET -> factory.showHabitFillIn(habit)
}
}
}
}

@ -1,91 +0,0 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.widgets.activities
import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.view.Window
import android.widget.FrameLayout
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.core.ui.widgets.WidgetBehavior
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.intents.IntentParser
import org.isoron.uhabits.utils.SystemUtils
import org.isoron.uhabits.widgets.WidgetUpdater
class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPickerCallback {
private lateinit var behavior: WidgetBehavior
private lateinit var data: IntentParser.CheckmarkIntentData
private lateinit var widgetUpdater: WidgetUpdater
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
setContentView(FrameLayout(this))
val app = this.applicationContext as HabitsApplication
val component = app.component
val parser = app.component.intentParser
data = parser.parseCheckmarkIntent(intent)
behavior = WidgetBehavior(
component.habitList,
component.commandRunner,
component.notificationTray,
component.preferences
)
widgetUpdater = component.widgetUpdater
showNumberSelector(this)
SystemUtils.unlockScreen(this)
}
override fun onNumberPicked(newValue: Double, 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 على واجهة أنيقة وبسيطة وسهلة الاستخدام للغاية ، حتى للمستخدمين لأول مرة. تم تحسين التطبيق ليكون ذي سرعة عالية ، ويعمل التطبيق بشكل جيد حتى على الهواتف القديمة.

@ -1,29 +1,29 @@
Loop Habit Tracker vám pomáhá vytvořit a udržet si dlouhodobé pozitivní zvyky. Podrobné grafy a statistiky vám ukáží jasný přehled, jak se vaše zvyky postupem času zlepšily. Aplikace je kompletně bez reklam, open source (otevřený zdrojový kód) a respektuje vaše soukromí.
Loop Habit Tracker Vám pomáhá vytvořit a udržet si dlouhodobé pozitivní návyky. Podrobné grafy a statistiky Vám ukáží jasný přehled, jak se Vaše návyky postupem času zlepšily. Aplikace je zcela bez reklam, open-source a respektuje Vaše soukromí.
<b>Krásné, minimalistické a přehledné rozhraní</b>
Loop má elegantní a minimalostické rozhraní, které je velice jednoduché k použití. I pro nové uživatele. Skvěle optimalizováno pro rychlost, aplikace funguje i na starších telefonech.
Loop má elegantní a minimalistické rozhraní, které je jednoduché na ovládání i pro nové uživatele. Díky vysoké optimalizaci pro rychlost aplikace funguje skvěle i na starších telefonech.
<b>Skóre návyků</b>
Loop má propracovaný vzorec pro výpočet síly vašich návyků. Každé opakování posílí váš zvyk a každé vynechání ho oslabí. Ale pár zmeškaných dní po dlouhém období vám kompletně nezničí váš pokrok, jako u mnoho jiných aplikací soustředěných na "nepřerušení řetězce".
Loop má propracovaný vzorec pro výpočet síly vašich návyků. Každé opakování posílí váš zvyk a každé vynechání ho oslabí. Pár zmeškaných dní po dlouhé výzvě Vám, na rozdíl od jiných aplikací, ve kterých nesmíte vynechat ani jeden den, nezničí Váš pokrok.
<b>Flexibilní plánování</b>
Kromě každodenních návyků podporuje Loop návyky se složitějšími plány, například třikrát týdně nebo každý druhý den.
Kromě každodenních návyků podporuje Loop i návyky náročné na plánování, například třikrát týdně nebo každý druhý den.
<b>Připomenutí</b>
Nastavte si oznámení, aby vám připomněly vaše návyky. Každý návyk může mít svou notifikaci ve zvoleném čase. Jednoduše potvrďte nebo zamítněte návyk přímo z notifikace.
Nastavte si upozornění, která Vám budou Vaše návyky připomínat. Pro každý návyk může být nastavena připomínka dle Vámi zvoleného času. Jednoduše potvrďte nebo propusťte návyk přímo z upozornění.
<b>Widgety</b>
Nechte si při každém odemknutí telefonu připomenout vaše návyky. Barevné widgety vám dovolují sledovat vaše návyky přimo z vaší domovské obrazovky, aniž byste museli otevřít aplikaci.
Nechte si při každém odemknutí telefonu připomenout Vaše návyky. Barevné widgety Vám dovolí sledovat Vaše návyky přímo z Vaší domovské obrazovky, aniž byste museli otevřít aplikaci.
<b>Převezměte kontrolu nad svými daty</b>
Pokud chcete dále analyzovat vaše údaje, nebo je přesunout do jiné služby, Loop umožňuje je exportovat do tabulek (CSV) nebo do databázového souboru (SQLite). Pokročilí uživatelé můžou propojit ovládání i skrze jiné aplikace, jako je Tasker.
Pokud chcete dále analyzovat Vaše data, nebo je přesunout do jiné služby, můžete je díky Loop exportovat do tabulek (CSV) nebo do databázového souboru (SQLite). Pokročilí uživatelé mohou propojit ovládání i skrze jiné aplikace, jako je Tasker.
<b>Bez omezení</b>
Sledujte si tolik návyků, kolik chcete. Loop nenastavuje žádné umělé omezení počtu návyků, které můžete sledovat. Všechny funkce jsou k dispozici všem uživatelům. Neexistují žádné nákupy v aplikaci.
Sledujte tolik návyků, kolik chcete. Loop nenastavuje žádné umělé omezení počtu návyků, které můžete sledovat. Všechny funkce jsou k dispozici všem uživatelům. V aplikaci nic nenakupujete.
<b>Zcela bez reklam a open source</b>
V této aplikaci nejsou žádné reklamy, nepříjemná oznámení nebo dotěrná oprávnění a nikdy zde nebudou. Aplikace je zcela open-source (GPLv3).
V této aplikaci nejsou žádné reklamy, nepříjemná oznámení nebo dotěrná oprávnění, a ani nikdy nebudou. Aplikace je zcela open-source (GPLv3).
<b>Funguje offline a respektuje vaše soukromí</b>
Loop nevyžaduje připojení k Internetu ani registraci online účtu. Vaše důvěrné údaje nejsou nikdy nikomu zaslány. Nemají k nim přístup ani vývojáři, nebo třetí strany.
<b>Funguje off-line a respektuje vaše soukromí</b>
Loop nevyžaduje připojení k internetu ani online registraci účtu. Vaše důvěrná data nikdy nikomu neodesíláme. Nemají k nim přístup ani vývojáři, nebo třetí strany.

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save