Compare commits

..

2 Commits

Author SHA1 Message Date
e8a4a9740e Merge branch 'dev' into feature/sync 2021-08-22 05:13:37 -05:00
7f1a1add8c Revert "Temporarily remove device sync"
This reverts commit da018fc64d.
2021-05-09 18:22:15 -05:00
340 changed files with 3006 additions and 4639 deletions

View File

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

View File

@@ -1,40 +1,5 @@
# Changelog # Changelog
## [2.1.0] -- Unreleased
### 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)
### 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)
### 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 ## [2.0.3] - 2021-08-21
### Fixed ### Fixed
- Improve automatic checkmarks for monthly habits (@iSoron, #947) - Improve automatic checkmarks for monthly habits (@iSoron, #947)

View File

@@ -90,7 +90,7 @@ contribute, even if you are not a software developer.
is already completed, you are also very welcome to join and proofread it. is already completed, you are also very welcome to join and proofread it.
* **Write some code.** If you are an Android developer, you are very welcome to * **Write some code.** If you are an Android developer, you are very welcome to
contribute with code. Please see the [guidelines](https://github.com/iSoron/uhabits/blob/dev/docs/GUIDELINES.md). contribute with code. Please see `docs/GUIDELINES.md`.
## License ## License

View File

@@ -1,11 +1,11 @@
plugins { plugins {
val kotlinVersion = "1.6.10" val kotlinVersion = "1.5.0"
id("com.android.application") version ("7.0.3") apply (false) id("com.android.application") version ("4.2.0") apply (false)
id("org.jetbrains.kotlin.android") version kotlinVersion apply (false) id("org.jetbrains.kotlin.android") version kotlinVersion apply (false)
id("org.jetbrains.kotlin.kapt") 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.android.extensions") version kotlinVersion apply (false)
id("org.jetbrains.kotlin.multiplatform") version kotlinVersion apply (false) id("org.jetbrains.kotlin.multiplatform") version kotlinVersion apply (false)
id("org.jlleitschuh.gradle.ktlint") version "10.2.1" id("org.jlleitschuh.gradle.ktlint") version "10.1.0"
} }
apply { apply {
@@ -15,9 +15,11 @@ apply {
allprojects { allprojects {
repositories { repositories {
google() google()
jcenter()
mavenCentral() mavenCentral()
maven(url = "https://plugins.gradle.org/m2/") maven(url = "https://plugins.gradle.org/m2/")
maven(url = "https://oss.sonatype.org/content/repositories/snapshots/") maven(url = "https://oss.sonatype.org/content/repositories/snapshots/")
maven(url = "https://jitpack.io") maven(url = "https://kotlin.bintray.com/ktor")
maven(url = "https://kotlin.bintray.com/kotlin-js-wrappers")
} }
} }

126
build.sh
View File

@@ -26,7 +26,6 @@ GRADLE="./gradlew --stacktrace --quiet"
PACKAGE_NAME=org.isoron.uhabits PACKAGE_NAME=org.isoron.uhabits
SDKMANAGER="${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" SDKMANAGER="${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager"
VERSION=$(grep versionName uhabits-android/build.gradle.kts | sed -e 's/.*"\([^"]*\)".*/\1/g') VERSION=$(grep versionName uhabits-android/build.gradle.kts | sed -e 's/.*"\([^"]*\)".*/\1/g')
BOOT_TIMEOUT=360
if [ -z $VERSION ]; then if [ -z $VERSION ]; then
echo "Could not parse app version from: uhabits-android/build.gradle.kts" echo "Could not parse app version from: uhabits-android/build.gradle.kts"
@@ -70,7 +69,8 @@ core_build() {
# Android # Android
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
android_setup() { # shellcheck disable=SC2016
android_test() {
API=$1 API=$1
AVDNAME=${AVD_PREFIX}${API} AVDNAME=${AVD_PREFIX}${API}
@@ -95,53 +95,15 @@ android_setup() {
) 10>/tmp/uhabitsTest.lock ) 10>/tmp/uhabitsTest.lock
log_info "Launching emulator..." log_info "Launching emulator..."
$EMULATOR \ $EMULATOR -avd $AVDNAME -port 6${API}0 1>/dev/null 2>&1 &
-avd $AVDNAME \
-port 6${API}0 \
1>/dev/null 2>&1 &
log_info "Waiting for emulator to boot..." log_info "Waiting for emulator to boot..."
export ADB="$ADB -s emulator-6${API}0" 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' $ADB wait-for-device shell 'while [[ -z "$(getprop sys.boot_completed)" ]]; do echo Waiting...; sleep 1; done; input keyevent 82' || return 1
if [ $? -ne 0 ]; then
log_error "Emulator failed to boot after $BOOT_TIMEOUT seconds."
return 1
fi
log_info "Saving snapshot..."
$ADB emu avd snapshot save fresh-install
}
android_boot_attempt() {
API=$1
AVDNAME=${AVD_PREFIX}${API}
log_info "Stopping Android emulator..."
while [[ -n $(pgrep -f ${AVDNAME}) ]]; do
pkill -9 -f ${AVDNAME}
done
log_info "Launching emulator..."
$EMULATOR \
-avd $AVDNAME \
-port 6${API}0 \
-snapshot fresh-install \
-no-snapshot-save \
-wipe-data \
1>/dev/null 2>&1 &
log_info "Waiting for emulator to boot..."
export ADB="$ADB -s emulator-6${API}0"
sleep 5
timeout $BOOT_TIMEOUT $ADB wait-for-device shell 'while [[ -z "$(getprop sys.boot_completed)" ]]; do echo Waiting...; sleep 1; done; input keyevent 82'
if [ $? -ne 0 ]; then
log_error "Emulator failed to boot after $BOOT_TIMEOUT seconds."
return 1
fi
log_info "Disabling animations..."
$ADB root || return 1 $ADB root || return 1
sleep 5 sleep 5
log_info "Disabling animations..."
$ADB shell settings put global window_animation_scale 0 || return 1 $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 transition_animation_scale 0 || return 1
$ADB shell settings put global animator_duration_scale 0 || return 1 $ADB shell settings put global animator_duration_scale 0 || return 1
@@ -149,24 +111,6 @@ android_boot_attempt() {
log_info "Acquiring wake lock..." log_info "Acquiring wake lock..."
$ADB shell 'echo android-test > /sys/power/wake_lock' || return 1 $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 if [ -n "$RELEASE" ]; then
log_info "Installing release APK..." log_info "Installing release APK..."
$ADB install -r ${ANDROID_OUTPUTS_DIR}/apk/release/uhabits-android-release.apk || return 1 $ADB install -r ${ANDROID_OUTPUTS_DIR}/apk/release/uhabits-android-release.apk || return 1
@@ -178,25 +122,14 @@ android_test() {
$ADB install -r ${ANDROID_OUTPUTS_DIR}/apk/androidTest/debug/uhabits-android-debug-androidTest.apk || return 1 $ADB install -r ${ANDROID_OUTPUTS_DIR}/apk/androidTest/debug/uhabits-android-debug-androidTest.apk || return 1
for size in medium large; do for size in medium large; do
log_info "Running $size instrumented tests..."
OUT_INSTRUMENT=${ANDROID_OUTPUTS_DIR}/instrument-${API}.txt OUT_INSTRUMENT=${ANDROID_OUTPUTS_DIR}/instrument-${API}.txt
OUT_LOGCAT=${ANDROID_OUTPUTS_DIR}/logcat-${API}.txt OUT_LOGCAT=${ANDROID_OUTPUTS_DIR}/logcat-${API}.txt
FAILED_TESTS="" $ADB shell am instrument \
for i in {1..5}; do -r -e coverage true -e size $size \
log_info "Running $size instrumented tests (attempt $i)..." -w ${PACKAGE_NAME}.test/androidx.test.runner.AndroidJUnitRunner \
$ADB shell am instrument \ | tee $OUT_INSTRUMENT
-r -e coverage true -e size "$size" $FAILED_TESTS \ if grep "\(INSTRUMENTATION_STATUS_CODE.*-1\|FAILURES\|ABORTED\|onError\|Error type\|crashed\)" $OUT_INSTRUMENT; then
-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 "Some $size instrumented tests failed."
log_error "Saving logcat: $OUT_LOGCAT..." log_error "Saving logcat: $OUT_LOGCAT..."
$ADB logcat -d > $OUT_LOGCAT $ADB logcat -d > $OUT_LOGCAT
@@ -205,14 +138,13 @@ android_test() {
$ADB shell rm -r /sdcard/Android/data/${PACKAGE_NAME}/files/test-screenshots/ $ADB shell rm -r /sdcard/Android/data/${PACKAGE_NAME}/files/test-screenshots/
return 1 return 1
fi fi
log_info "$size tests passed."
done done
return 0 return 0
} }
android_test_parallel() { android_test_parallel() {
# Launch background processes
PIDS=""
for API in $*; do for API in $*; do
( (
LOG=build/android-test-$API.log LOG=build/android-test-$API.log
@@ -220,27 +152,12 @@ android_test_parallel() {
if android_test $API 1>$LOG 2>&1; then if android_test $API 1>$LOG 2>&1; then
log_info "API $API: Passed" log_info "API $API: Passed"
else else
log_error "API $API: Failed" log_error "API $API: Failed. See $LOG for more details."
fi fi
pkill -9 -f ${AVD_PREFIX}${API} pkill -9 -f ${AVD_PREFIX}${API}
)& )&
PIDS+=" $!"
done done
wait
# Check exit codes
RET_CODE=0
for pid in $PIDS; do
wait $pid || RET_CODE=1
done
# Print all logs
for API in $*; do
echo "::group::Android Tests (API $API)"
cat build/android-test-$API.log
echo "::endgroup::"
done
return $RET_CODE
} }
android_build() { android_build() {
@@ -312,14 +229,12 @@ CI/CD script for Loop Habit Tracker.
Usage: Usage:
build.sh build [options] build.sh build [options]
build.sh android-setup <API>
build.sh android-tests <API> [options] build.sh android-tests <API> [options]
build.sh android-tests-parallel <API> <API>... [options] build.sh android-tests-parallel <API> <API>... [options]
build.sh android-accept-images [options] build.sh android-accept-images [options]
Commands: Commands:
build Build the app and run small tests 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 Run medium and large Android tests on an emulator
android-tests-parallel Tests multiple API levels simultaneously android-tests-parallel Tests multiple API levels simultaneously
android-accept-images Copy fetched images to corresponding assets folder android-accept-images Copy fetched images to corresponding assets folder
@@ -355,17 +270,18 @@ main() {
core_build core_build
android_build android_build
;; ;;
android-setup)
shift; _parse_opts "$@"
android_setup $1
;;
android-tests) android-tests)
shift; _parse_opts "$@" shift; _parse_opts "$@"
if [ -z $1 ]; then if [ -z $1 ]; then
_print_usage _print_usage
exit 1 exit 1
fi fi
android_test $1 for attempt in {1..5}; do
log_info "Running Android tests (attempt $attempt)..."
android_test $1 && return 0
done
log_error "Maximum number of attempts reached. Failing."
return 1
;; ;;
android-tests-parallel) android-tests-parallel)
shift; _parse_opts "$@" shift; _parse_opts "$@"

View File

@@ -1,5 +1,5 @@
org.gradle.parallel=false org.gradle.parallel=false
org.gradle.daemon=true org.gradle.daemon=true
org.gradle.jvmargs=-Xms2048m -Xmx2048m org.gradle.jvmargs=-Xms2048m -Xmx2048m -XX:MaxPermSize=2048m
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true

View File

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

View File

@@ -1,6 +1,7 @@
pluginManagement { pluginManagement {
repositories { repositories {
gradlePluginPortal() gradlePluginPortal()
jcenter()
google() google()
} }
resolutionStrategy.eachPlugin { resolutionStrategy.eachPlugin {

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
/* /*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org> * Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
* *
@@ -18,7 +20,7 @@
*/ */
plugins { plugins {
id("com.github.triplet.play") version "3.7.0" id("com.github.triplet.play") version "3.5.0"
id("com.android.application") id("com.android.application")
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.kapt") id("org.jetbrains.kotlin.kapt")
@@ -32,15 +34,15 @@ tasks.compileLint {
android { android {
compileSdk = 31 compileSdkVersion(30)
defaultConfig { defaultConfig {
versionCode = 20100 versionCode(20003)
versionName = "2.1.0" versionName("2.0.3")
minSdk = 23 minSdkVersion(23)
targetSdk = 31 targetSdkVersion(30)
applicationId = "org.isoron.uhabits" applicationId("org.isoron.uhabits")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner("androidx.test.runner.AndroidJUnitRunner")
} }
signingConfigs { signingConfigs {
@@ -56,7 +58,7 @@ android {
buildTypes { buildTypes {
getByName("release") { getByName("release") {
isMinifyEnabled = true minifyEnabled(true)
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.txt") proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.txt")
if (signingConfigs.findByName("release") != null) { if (signingConfigs.findByName("release") != null) {
signingConfig = signingConfigs.getByName("release") signingConfig = signingConfigs.getByName("release")
@@ -68,7 +70,7 @@ android {
} }
} }
lint { lintOptions {
isCheckReleaseBuilds = false isCheckReleaseBuilds = false
isAbortOnError = false isAbortOnError = false
disable("GoogleAppIndexingWarning") disable("GoogleAppIndexingWarning")
@@ -86,10 +88,10 @@ android {
} }
dependencies { dependencies {
val daggerVersion = "2.41" val daggerVersion = "2.38.1"
val kotlinVersion = "1.6.21" val kotlinVersion = "1.5.21"
val kxCoroutinesVersion = "1.6.1" val kxCoroutinesVersion = "1.5.1"
val ktorVersion = "1.6.8" val ktorVersion = "1.6.2"
val espressoVersion = "3.4.0" val espressoVersion = "3.4.0"
androidTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion") androidTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion")
@@ -98,17 +100,17 @@ dependencies {
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.1") androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.1")
androidTestImplementation("io.ktor:ktor-client-mock:$ktorVersion") androidTestImplementation("io.ktor:ktor-client-mock:$ktorVersion")
androidTestImplementation("io.ktor:ktor-jackson:$ktorVersion") androidTestImplementation("io.ktor:ktor-jackson:$ktorVersion")
androidTestImplementation("androidx.annotation:annotation:1.3.0") androidTestImplementation("androidx.annotation:annotation:1.2.0")
androidTestImplementation("androidx.test.ext:junit:1.1.3") androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0") androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
androidTestImplementation("androidx.test:rules:1.4.0") androidTestImplementation("androidx.test:rules:1.4.0")
androidTestImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0") androidTestImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
compileOnly("javax.annotation:jsr250-api:1.0") compileOnly("javax.annotation:jsr250-api:1.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
implementation("com.github.AppIntro:AppIntro:6.2.0") implementation("com.github.paolorotolo:appintro:4.1.0")
implementation("com.google.code.findbugs:jsr305:3.0.2") implementation("com.google.code.findbugs:jsr305:3.0.2")
implementation("com.google.dagger:dagger:$daggerVersion") implementation("com.google.dagger:dagger:$daggerVersion")
implementation("com.google.guava:guava:31.1-android") implementation("com.google.guava:guava:30.1.1-android")
implementation("io.ktor:ktor-client-android:$ktorVersion") implementation("io.ktor:ktor-client-android:$ktorVersion")
implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-jackson:$ktorVersion") implementation("io.ktor:ktor-client-jackson:$ktorVersion")
@@ -116,11 +118,12 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kxCoroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kxCoroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kxCoroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kxCoroutinesVersion")
implementation("androidx.appcompat:appcompat:1.4.1") implementation("androidx.appcompat:appcompat:1.3.1")
implementation("androidx.legacy:legacy-preference-v14:1.0.0") implementation("androidx.legacy:legacy-preference-v14:1.0.0")
implementation("androidx.legacy:legacy-support-v4:1.0.0") implementation("androidx.legacy:legacy-support-v4:1.0.0")
implementation("com.google.android.material:material:1.5.0") implementation("com.google.android.material:material:1.4.0")
implementation("com.opencsv:opencsv:5.6") implementation("com.google.zxing:core:3.4.1")
implementation("com.opencsv:opencsv:5.5.1")
implementation(project(":uhabits-core")) implementation(project(":uhabits-core"))
kapt("com.google.dagger:dagger-compiler:$daggerVersion") kapt("com.google.dagger:dagger-compiler:$daggerVersion")
kaptAndroidTest("com.google.dagger:dagger-compiler:$daggerVersion") kaptAndroidTest("com.google.dagger:dagger-compiler:$daggerVersion")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 7.4 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: 22 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -55,7 +55,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class HabitsTest : BaseUserInterfaceTest() { class HabitsTest : BaseUserInterfaceTest() {
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun shouldCreateHabit() { fun shouldCreateHabit() {
@@ -181,8 +180,6 @@ class HabitsTest : BaseUserInterfaceTest() {
longPressCheckmarks("Wake up early", count = 2) longPressCheckmarks("Wake up early", count = 2)
clickText("Wake up early") clickText("Wake up early")
verifyShowsScreen(SHOW_HABIT) verifyShowsScreen(SHOW_HABIT)
// TODO: find a better way than sleeping in tests
Thread.sleep(2001L)
verifyDisplaysText("10%") verifyDisplaysText("10%")
} }
@@ -197,8 +194,6 @@ class HabitsTest : BaseUserInterfaceTest() {
verifyDoesNotDisplayText("Track time") verifyDoesNotDisplayText("Track time")
verifyDisplaysText("Wake up early") verifyDisplaysText("Wake up early")
longPressCheckmarks("Wake up early", count = 1) longPressCheckmarks("Wake up early", count = 1)
// TODO: find a better way than sleeping in tests
Thread.sleep(2001L)
verifyDoesNotDisplayText("Wake up early") verifyDoesNotDisplayText("Wake up early")
clickMenu(TOGGLE_COMPLETED) clickMenu(TOGGLE_COMPLETED)
verifyDisplaysText("Track time") verifyDisplaysText("Track time")

View File

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

View File

@@ -18,8 +18,7 @@
*/ */
package org.isoron.uhabits.acceptance.steps package org.isoron.uhabits.acceptance.steps
import android.os.Build import android.os.Build.VERSION
import android.os.Build.VERSION.SDK_INT
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso
@@ -134,7 +133,7 @@ object CommonSteps : BaseUserInterfaceTest() {
@Throws(Exception::class) @Throws(Exception::class)
fun verifyOpensWebsite(url: String?) { fun verifyOpensWebsite(url: String?) {
var browserPkg = "org.chromium.webview_shell" var browserPkg = "org.chromium.webview_shell"
if (SDK_INT <= Build.VERSION_CODES.M) { if (VERSION.SDK_INT <= 23) {
browserPkg = "com.android.browser" browserPkg = "com.android.browser"
} }
assertTrue(device.wait(Until.hasObject(By.pkg(browserPkg)), 5000)) assertTrue(device.wait(Until.hasObject(By.pkg(browserPkg)), 5000))
@@ -179,22 +178,6 @@ object CommonSteps : BaseUserInterfaceTest() {
EditHabitSteps.clickSave() 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 { enum class Screen {
LIST_HABITS, SHOW_HABIT, EDIT_HABIT, SELECT_HABIT_TYPE LIST_HABITS, SHOW_HABIT, EDIT_HABIT, SELECT_HABIT_TYPE
} }

View File

@@ -36,24 +36,6 @@ object EditHabitSteps {
Espresso.onView(ViewMatchers.withText("SAVE")).perform(ViewActions.click()) 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) { fun pickColor(color: Int) {
Espresso.onView(ViewMatchers.withId(R.id.colorButton)).perform(ViewActions.click()) Espresso.onView(ViewMatchers.withId(R.id.colorButton)).perform(ViewActions.click())
BaseUserInterfaceTest.device.findObject(By.descStartsWith(String.format("Color %d", color))) BaseUserInterfaceTest.device.findObject(By.descStartsWith(String.format("Color %d", color)))

View File

@@ -120,12 +120,6 @@ object ListHabitsSteps {
BaseUserInterfaceTest.device.waitForIdle() 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 { enum class MenuItem {
ABOUT, HELP, SETTINGS, EDIT, DELETE, ARCHIVE, TOGGLE_ARCHIVED, UNARCHIVE, TOGGLE_COMPLETED, ADD ABOUT, HELP, SETTINGS, EDIT, DELETE, ARCHIVE, TOGGLE_ARCHIVED, UNARCHIVE, TOGGLE_COMPLETED, ADD
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,6 @@ import androidx.test.filters.MediumTest
import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.MatcherAssert.assertThat
import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.utils.PaletteUtils import org.isoron.uhabits.utils.PaletteUtils
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@@ -43,7 +42,6 @@ class NumberButtonViewTest : BaseViewTest() {
super.setUp() super.setUp()
view = component.getNumberButtonViewFactory().create().apply { view = component.getNumberButtonViewFactory().create().apply {
units = "steps" units = "steps"
targetType = NumericalHabitType.AT_LEAST
threshold = 100.0 threshold = 100.0
color = PaletteUtils.getAndroidTestColor(8) color = PaletteUtils.getAndroidTestColor(8)
onEdit = { edited = true } onEdit = { edited = true }
@@ -76,10 +74,10 @@ class NumberButtonViewTest : BaseViewTest() {
} }
@Test @Test
fun testRender_atMostAboveThreshold() { fun testRender_emptyUnits() {
view.value = 500.0 view.value = 500.0
view.targetType = NumericalHabitType.AT_MOST view.units = ""
assertRenders(view, "$PATH/render_at_most_above.png") assertRenders(view, "$PATH/render_unitless.png")
} }
@Test @Test
@@ -88,13 +86,6 @@ class NumberButtonViewTest : BaseViewTest() {
assertRenders(view, "$PATH/render_below.png") assertRenders(view, "$PATH/render_below.png")
} }
@Test
fun testRender_atMostBetweenThresholds() {
view.value = 110.0
view.targetType = NumericalHabitType.AT_MOST
assertRenders(view, "$PATH/render_at_most_between.png")
}
@Test @Test
fun testRender_zero() { fun testRender_zero() {
view.value = 0.0 view.value = 0.0
@@ -102,21 +93,15 @@ class NumberButtonViewTest : BaseViewTest() {
} }
@Test @Test
fun testRender_atMostBelowThreshold() { fun testClick_shortToggleDisabled() {
view.value = 0.0 prefs.isShortToggleEnabled = false
view.targetType = NumericalHabitType.AT_MOST view.performClick()
assertRenders(view, "$PATH/render_at_most_below.png") assertFalse(edited)
} }
@Test @Test
fun testRender_emptyUnits() { fun testClick_shortToggleEnabled() {
view.value = 500.0 prefs.isShortToggleEnabled = true
view.units = ""
assertRenders(view, "$PATH/render_unitless.png")
}
@Test
fun testClick() {
view.performClick() view.performClick()
assertTrue(edited) assertTrue(edited)
} }

View File

@@ -24,7 +24,6 @@ import androidx.test.filters.MediumTest
import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.MatcherAssert.assertThat
import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.utils.PaletteUtils import org.isoron.uhabits.utils.PaletteUtils
import org.junit.After import org.junit.After
@@ -56,7 +55,6 @@ class NumberPanelViewTest : BaseViewTest() {
buttonCount = 4 buttonCount = 4
color = PaletteUtils.getAndroidTestColor(7) color = PaletteUtils.getAndroidTestColor(7)
units = "steps" units = "steps"
targetType = NumericalHabitType.AT_LEAST
threshold = 5000.0 threshold = 5000.0
} }
view.onAttachedToWindow() view.onAttachedToWindow()
@@ -76,7 +74,7 @@ class NumberPanelViewTest : BaseViewTest() {
@Test @Test
fun testEdit() { fun testEdit() {
val timestamps = mutableListOf<Timestamp>() val timestamps = mutableListOf<Timestamp>()
view.onEdit = { t -> timestamps.plusAssign(t) } view.onEdit = { timestamps.plusAssign(it) }
view.buttons[0].performLongClick() view.buttons[0].performLongClick()
view.buttons[2].performLongClick() view.buttons[2].performLongClick()
view.buttons[3].performLongClick() view.buttons[3].performLongClick()
@@ -87,7 +85,7 @@ class NumberPanelViewTest : BaseViewTest() {
fun testEdit_withOffset() { fun testEdit_withOffset() {
val timestamps = mutableListOf<Timestamp>() val timestamps = mutableListOf<Timestamp>()
view.dataOffset = 3 view.dataOffset = 3
view.onEdit = { t -> timestamps += t } view.onEdit = { timestamps += it }
view.buttons[0].performLongClick() view.buttons[0].performLongClick()
view.buttons[2].performLongClick() view.buttons[2].performLongClick()
view.buttons[3].performLongClick() view.buttons[3].performLongClick()

View File

@@ -53,6 +53,8 @@ class SubtitleCardViewTest : BaseViewTest() {
isNumerical = false, isNumerical = false,
question = "Did you meditate this morning?", question = "Did you meditate this morning?",
reminder = Reminder(8, 30, EVERY_DAY), reminder = Reminder(8, 30, EVERY_DAY),
unit = "",
targetValue = 0.0,
theme = LightTheme(), theme = LightTheme(),
) )
) )

View File

@@ -61,7 +61,7 @@ class PerformanceTest : BaseAndroidTest() {
val habit = fixtures.createEmptyHabit() val habit = fixtures.createEmptyHabit()
for (i in 0..4999) { for (i in 0..4999) {
val timestamp: Timestamp = Timestamp(i * DAY_LENGTH) val timestamp: Timestamp = Timestamp(i * DAY_LENGTH)
CreateRepetitionCommand(habitList, habit, timestamp, 1, "").run() CreateRepetitionCommand(habitList, habit, timestamp, 1).run()
} }
db.setTransactionSuccessful() db.setTransactionSuccessful()
db.endTransaction() db.endTransaction()

View File

@@ -21,12 +21,9 @@ package org.isoron.uhabits.regression
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import org.isoron.uhabits.BaseUserInterfaceTest 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.EDIT_HABIT
import org.isoron.uhabits.acceptance.steps.CommonSteps.Screen.LIST_HABITS 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.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.clickText
import org.isoron.uhabits.acceptance.steps.CommonSteps.createHabit import org.isoron.uhabits.acceptance.steps.CommonSteps.createHabit
import org.isoron.uhabits.acceptance.steps.CommonSteps.launchApp import org.isoron.uhabits.acceptance.steps.CommonSteps.launchApp
@@ -40,12 +37,9 @@ import org.isoron.uhabits.acceptance.steps.EditHabitSteps.clickSave
import org.isoron.uhabits.acceptance.steps.EditHabitSteps.typeName import org.isoron.uhabits.acceptance.steps.EditHabitSteps.typeName
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.MenuItem.ADD 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.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.clickMenu
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.longPressCheckmarks 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.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.Entry.Companion.YES_MANUAL
import org.junit.Test import org.junit.Test
@@ -89,37 +83,4 @@ class ListHabitsRegressionTest : BaseUserInterfaceTest() {
offsetHeaders() offsetHeaders()
verifyDisplaysCheckmarks("Wake up early", listOf(UNKNOWN, UNKNOWN, UNKNOWN, UNKNOWN)) 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",
)
}
} }

View File

@@ -0,0 +1,154 @@
/*
* 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.sync
import androidx.test.filters.MediumTest
import com.fasterxml.jackson.databind.ObjectMapper
import io.ktor.client.HttpClient
import io.ktor.client.engine.mock.MockEngine
import io.ktor.client.engine.mock.MockRequestHandleScope
import io.ktor.client.engine.mock.respond
import io.ktor.client.engine.mock.respondError
import io.ktor.client.engine.mock.respondOk
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.request.HttpRequestData
import io.ktor.client.request.HttpResponseData
import io.ktor.http.HttpStatusCode
import io.ktor.http.fullPath
import io.ktor.http.headersOf
import kotlinx.coroutines.runBlocking
import org.isoron.uhabits.BaseAndroidTest
import org.isoron.uhabits.core.sync.AbstractSyncServer
import org.isoron.uhabits.core.sync.GetDataVersionResponse
import org.isoron.uhabits.core.sync.KeyNotFoundException
import org.isoron.uhabits.core.sync.RegisterReponse
import org.isoron.uhabits.core.sync.ServiceUnavailable
import org.isoron.uhabits.core.sync.SyncData
import org.junit.Test
@MediumTest
class RemoteSyncServerTest : BaseAndroidTest() {
private val mapper = ObjectMapper()
val data = SyncData(1, "Hello world")
@Test
fun when_register_succeeds_should_return_key() = runBlocking {
val server = server("/register") {
respondWithJson(RegisterReponse("ABCDEF"))
}
assertEquals("ABCDEF", server.register())
}
@Test(expected = ServiceUnavailable::class)
fun when_register_fails_should_raise_correct_exception() = runBlocking {
val server = server("/register") {
respondError(HttpStatusCode.ServiceUnavailable)
}
server.register()
return@runBlocking
}
@Test
fun when_get_data_version_succeeds_should_return_version() = runBlocking {
server("/db/ABC/version") {
respondWithJson(GetDataVersionResponse(5))
}.apply {
assertEquals(5, getDataVersion("ABC"))
}
return@runBlocking
}
@Test(expected = ServiceUnavailable::class)
fun when_get_data_version_with_server_error_should_raise_exception() = runBlocking {
server("/db/ABC/version") {
respondError(HttpStatusCode.InternalServerError)
}.apply {
getDataVersion("ABC")
}
return@runBlocking
}
@Test(expected = KeyNotFoundException::class)
fun when_get_data_version_with_invalid_key_should_raise_exception() = runBlocking {
server("/db/ABC/version") {
respondError(HttpStatusCode.NotFound)
}.apply {
getDataVersion("ABC")
}
return@runBlocking
}
@Test
fun when_get_data_succeeds_should_return_data() = runBlocking {
server("/db/ABC") {
respondWithJson(data)
}.apply {
assertEquals(data, getData("ABC"))
}
return@runBlocking
}
@Test(expected = KeyNotFoundException::class)
fun when_get_data_with_invalid_key_should_raise_exception() = runBlocking {
server("/db/ABC") {
respondError(HttpStatusCode.NotFound)
}.apply {
getData("ABC")
}
return@runBlocking
}
@Test
fun when_put_succeeds_should_not_raise_exceptions() = runBlocking {
server("/db/ABC") {
respondOk()
}.apply {
put("ABC", data)
}
return@runBlocking
}
private fun server(
expectedPath: String,
action: MockRequestHandleScope.(HttpRequestData) -> HttpResponseData
): AbstractSyncServer {
return RemoteSyncServer(
httpClient = HttpClient(MockEngine) {
install(JsonFeature)
engine {
addHandler { request ->
when (request.url.fullPath) {
expectedPath -> action(request)
else -> error("unexpected call: ${request.url.fullPath}")
}
}
}
},
preferences = prefs
)
}
private fun MockRequestHandleScope.respondWithJson(content: Any) =
respond(
mapper.writeValueAsBytes(content),
headers = headersOf("Content-Type" to listOf("application/json"))
)
}

View File

@@ -24,15 +24,14 @@ import android.widget.FrameLayout
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import org.hamcrest.CoreMatchers import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.CoreMatchers.`is` import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.MatcherAssert.assertThat
import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.EntryList import org.isoron.uhabits.core.models.EntryList
import org.isoron.uhabits.core.models.Habit 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.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@@ -43,12 +42,10 @@ class CheckmarkWidgetTest : BaseViewTest() {
private lateinit var habit: Habit private lateinit var habit: Habit
private lateinit var entries: EntryList private lateinit var entries: EntryList
private lateinit var view: FrameLayout private lateinit var view: FrameLayout
private lateinit var today: Timestamp private val today = getTodayWithOffset()
override fun setUp() { override fun setUp() {
super.setUp() super.setUp()
setTheme(R.style.WidgetTheme) setTheme(R.style.WidgetTheme)
today = getTodayWithOffset()
prefs.widgetOpacity = 255 prefs.widgetOpacity = 255
prefs.isSkipEnabled = true prefs.isSkipEnabled = true
habit = fixtures.createVeryLongHabit() habit = fixtures.createVeryLongHabit()

View File

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

View File

@@ -17,12 +17,13 @@
~ with this program. If not, see <http://www.gnu.org/licenses/>. ~ with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.isoron.uhabits"> package="org.isoron.uhabits">
<uses-permission android:name="android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<application <application
android:name=".HabitsApplication" android:name=".HabitsApplication"
@@ -41,6 +42,14 @@
android:value=".activities.habits.list.ListHabitsActivity" /> android:value=".activities.habits.list.ListHabitsActivity" />
</activity> </activity>
<activity
android:name=".activities.sync.SyncActivity"
android:exported="true">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.settings.SettingsActivity" />
</activity>
<meta-data <meta-data
android:name="com.google.android.backup.api_key" android:name="com.google.android.backup.api_key"
android:value="AEdPqrEAAAAI6aeWncbnMNo8E5GWeZ44dlc5cQ7tCROwFhOtiw" /> android:value="AEdPqrEAAAAI6aeWncbnMNo8E5GWeZ44dlc5cQ7tCROwFhOtiw" />
@@ -49,11 +58,21 @@
android:name=".activities.habits.list.ListHabitsActivity" android:name=".activities.habits.list.ListHabitsActivity"
android:exported="true" android:exported="true"
android:label="@string/main_activity_title" android:label="@string/main_activity_title"
android:launchMode="singleTop" /> android:launchMode="singleTop">
<tools:validation testUrl="https://loophabits.org/sync/123" />
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="loophabits.org"
android:pathPrefix="/sync" />
</intent-filter>
</activity>
<activity-alias <activity-alias
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true"
android:label="@string/main_activity_title" android:label="@string/main_activity_title"
android:launchMode="singleTop" android:launchMode="singleTop"
android:targetActivity=".activities.habits.list.ListHabitsActivity"> android:targetActivity=".activities.habits.list.ListHabitsActivity">
@@ -86,7 +105,6 @@
<activity <activity
android:name=".widgets.activities.HabitPickerDialog" android:name=".widgets.activities.HabitPickerDialog"
android:exported="true"
android:theme="@style/Theme.AppCompat.Light.Dialog"> android:theme="@style/Theme.AppCompat.Light.Dialog">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" /> <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
@@ -95,7 +113,6 @@
<activity <activity
android:name=".widgets.activities.BooleanHabitPickerDialog" android:name=".widgets.activities.BooleanHabitPickerDialog"
android:exported="true"
android:theme="@style/Theme.AppCompat.Light.Dialog"> android:theme="@style/Theme.AppCompat.Light.Dialog">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" /> <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
@@ -104,7 +121,6 @@
<activity <activity
android:name=".widgets.activities.NumericalHabitPickerDialog" android:name=".widgets.activities.NumericalHabitPickerDialog"
android:exported="true"
android:theme="@style/Theme.AppCompat.Light.Dialog"> android:theme="@style/Theme.AppCompat.Light.Dialog">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" /> <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
@@ -119,16 +135,26 @@
android:value=".activities.habits.list.ListHabitsActivity" /> android:value=".activities.habits.list.ListHabitsActivity" />
</activity> </activity>
<activity
android:name=".widgets.activities.NumericalCheckmarkWidgetActivity"
android:label="NumericalCheckmarkWidget"
android:noHistory="true"
android:excludeFromRecents="true"
android:theme="@style/Theme.AppCompat.Light.Dialog">
<intent-filter>
<action android:name="org.isoron.uhabits.ACTION_SHOW_NUMERICAL_VALUE_ACTIVITY" />
</intent-filter>
</activity>
<activity <activity
android:name=".notifications.SnoozeDelayPickerActivity" android:name=".notifications.SnoozeDelayPickerActivity"
android:taskAffinity=""
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:launchMode="singleInstance" android:launchMode="singleInstance"
android:taskAffinity=""
android:theme="@android:style/Theme.Translucent.NoTitleBar" /> android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<receiver <receiver
android:name=".widgets.CheckmarkWidgetProvider" android:name=".widgets.CheckmarkWidgetProvider"
android:exported="true"
android:label="@string/checkmark"> android:label="@string/checkmark">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -146,7 +172,6 @@
<receiver <receiver
android:name=".widgets.HistoryWidgetProvider" android:name=".widgets.HistoryWidgetProvider"
android:exported="true"
android:label="@string/history"> android:label="@string/history">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -159,7 +184,6 @@
<receiver <receiver
android:name=".widgets.ScoreWidgetProvider" android:name=".widgets.ScoreWidgetProvider"
android:exported="true"
android:label="@string/score"> android:label="@string/score">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -172,7 +196,6 @@
<receiver <receiver
android:name=".widgets.StreakWidgetProvider" android:name=".widgets.StreakWidgetProvider"
android:exported="true"
android:label="@string/streaks"> android:label="@string/streaks">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -185,7 +208,6 @@
<receiver <receiver
android:name=".widgets.FrequencyWidgetProvider" android:name=".widgets.FrequencyWidgetProvider"
android:exported="true"
android:label="@string/frequency"> android:label="@string/frequency">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -198,7 +220,6 @@
<receiver <receiver
android:name=".widgets.TargetWidgetProvider" android:name=".widgets.TargetWidgetProvider"
android:exported="true"
android:label="@string/target"> android:label="@string/target">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -209,17 +230,13 @@
android:resource="@xml/widget_target_info" /> android:resource="@xml/widget_target_info" />
</receiver> </receiver>
<receiver <receiver android:name=".receivers.ReminderReceiver">
android:name=".receivers.ReminderReceiver"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".receivers.WidgetReceiver" <receiver android:name=".receivers.WidgetReceiver">
android:exported="true"
android:permission="false">
<intent-filter> <intent-filter>
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<action android:name="org.isoron.uhabits.ACTION_SET_NUMERICAL_VALUE" /> <action android:name="org.isoron.uhabits.ACTION_SET_NUMERICAL_VALUE" />
@@ -270,7 +287,7 @@
<!-- Locale/Tasker --> <!-- Locale/Tasker -->
<receiver <receiver
android:name=".automation.FireSettingReceiver" android:name=".automation.FireSettingReceiver"
android:exported="false"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" /> <action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
</intent-filter> </intent-filter>

View File

@@ -49,12 +49,23 @@ class AndroidDataView(
override fun onShowPress(e: MotionEvent?) = Unit override fun onShowPress(e: MotionEvent?) = Unit
override fun onSingleTapUp(e: MotionEvent?): Boolean { override fun onSingleTapUp(e: MotionEvent?): Boolean {
return handleClick(e, true) val x: Float
val y: Float
try {
val pointerId = e!!.getPointerId(0)
x = e.getX(pointerId)
y = e.getY(pointerId)
} catch (ex: RuntimeException) {
// Android often throws IllegalArgumentException here. Apparently,
// the pointer id may become invalid shortly after calling
// e.getPointerId.
return false
}
view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity)
return true
} }
override fun onLongPress(e: MotionEvent?) { override fun onLongPress(e: MotionEvent?) = Unit
handleClick(e)
}
override fun onScroll( override fun onScroll(
e1: MotionEvent?, e1: MotionEvent?,
@@ -126,22 +137,4 @@ class AndroidDataView(
} }
} }
} }
private fun handleClick(e: MotionEvent?, isSingleTap: Boolean = false): Boolean {
val x: Float
val y: Float
try {
val pointerId = e!!.getPointerId(0)
x = e.getX(pointerId)
y = e.getY(pointerId)
} catch (ex: RuntimeException) {
// Android often throws IllegalArgumentException here. Apparently,
// the pointer id may become invalid shortly after calling
// e.getPointerId.
return false
}
if (isSingleTap) view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity)
else view?.onLongClick(x / canvas.innerDensity, y / canvas.innerDensity)
return true
}
} }

View File

@@ -1,129 +0,0 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.dialogs
import android.app.Dialog
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.MultipleDialogsHandler.Companion.dismissCurrent
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.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.dismissCurrent()
dialog.show()
}
fun save() {
onToggle(value, view.notes.text.toString().trim())
dialog.dismiss()
}
}

View File

@@ -18,10 +18,7 @@
*/ */
package org.isoron.uhabits.activities.common.dialogs package org.isoron.uhabits.activities.common.dialogs
import android.app.Dialog
import android.os.Bundle
import com.android.colorpicker.ColorPickerDialog import com.android.colorpicker.ColorPickerDialog
import org.isoron.uhabits.activities.common.dialogs.MultipleDialogsHandler.Companion.dismissCurrent
import org.isoron.uhabits.core.ui.callbacks.OnColorPickedCallback import org.isoron.uhabits.core.ui.callbacks.OnColorPickedCallback
import org.isoron.uhabits.utils.toPaletteColor import org.isoron.uhabits.utils.toPaletteColor
@@ -31,14 +28,8 @@ import org.isoron.uhabits.utils.toPaletteColor
class ColorPickerDialog : ColorPickerDialog() { class ColorPickerDialog : ColorPickerDialog() {
fun setListener(callback: OnColorPickedCallback) { fun setListener(callback: OnColorPickedCallback) {
super.setOnColorSelectedListener { c: Int -> super.setOnColorSelectedListener { c: Int ->
val pc = c.toPaletteColor(requireContext()) val pc = c.toPaletteColor(context!!)
callback.onColorPicked(pc) callback.onColorPicked(pc)
} }
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.dismissCurrent()
return dialog
}
} }

View File

@@ -22,7 +22,6 @@ import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.MultipleDialogsHandler.Companion.dismissCurrent
import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback
import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.inject.ActivityContext
@@ -46,7 +45,5 @@ class ConfirmDeleteDialog(
BUTTON_NEGATIVE, BUTTON_NEGATIVE,
res.getString(R.string.no) res.getString(R.string.no)
) { dialog: DialogInterface?, which: Int -> } ) { dialog: DialogInterface?, which: Int -> }
this.dismissCurrent()
} }
} }

View File

@@ -0,0 +1,45 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.dialogs
import android.content.Context
import android.content.DialogInterface
import androidx.appcompat.app.AlertDialog
import org.isoron.uhabits.R
import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback
import org.isoron.uhabits.inject.ActivityContext
class ConfirmSyncKeyDialog(
@ActivityContext context: Context,
callback: OnConfirmedCallback
) : AlertDialog(context) {
init {
setTitle(R.string.device_sync)
val res = context.resources
setMessage(res.getString(R.string.sync_confirm))
setButton(
BUTTON_POSITIVE,
res.getString(R.string.yes)
) { dialog: DialogInterface?, which: Int -> callback.onConfirmed() }
setButton(
BUTTON_NEGATIVE,
res.getString(R.string.no)
) { dialog: DialogInterface?, which: Int -> }
}
}

View File

@@ -21,6 +21,7 @@ package org.isoron.uhabits.activities.common.dialogs
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.EditText import android.widget.EditText
@@ -31,7 +32,6 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment import androidx.appcompat.app.AppCompatDialogFragment
import kotlinx.android.synthetic.main.frequency_picker_dialog.view.* import kotlinx.android.synthetic.main.frequency_picker_dialog.view.*
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.MultipleDialogsHandler.Companion.dismissCurrent
class FrequencyPickerDialog( class FrequencyPickerDialog(
var freqNumerator: Int, var freqNumerator: Int,
@@ -44,7 +44,7 @@ class FrequencyPickerDialog(
constructor() : this(1, 1) constructor() : this(1, 1)
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val inflater = LayoutInflater.from(requireActivity()) val inflater = LayoutInflater.from(activity!!)
contentView = inflater.inflate(R.layout.frequency_picker_dialog, null) contentView = inflater.inflate(R.layout.frequency_picker_dialog, null)
addBeforeAfterText( addBeforeAfterText(
@@ -62,19 +62,15 @@ class FrequencyPickerDialog(
contentView.xTimesPerMonthContainer, contentView.xTimesPerMonthContainer,
) )
addBeforeAfterText(
this.getString(R.string.x_times_per_y_days),
contentView.xTimesPerYDaysContainer,
)
contentView.everyDayRadioButton.setOnClickListener { contentView.everyDayRadioButton.setOnClickListener {
check(contentView.everyDayRadioButton) check(contentView.everyDayRadioButton)
unfocusAll()
} }
contentView.everyXDaysRadioButton.setOnClickListener { contentView.everyXDaysRadioButton.setOnClickListener {
check(contentView.everyXDaysRadioButton) check(contentView.everyXDaysRadioButton)
val everyXDaysTextView = contentView.everyXDaysTextView val everyXDaysTextView = contentView.everyXDaysTextView
selectInputField(everyXDaysTextView) focus(everyXDaysTextView)
} }
contentView.everyXDaysTextView.setOnFocusChangeListener { v, hasFocus -> contentView.everyXDaysTextView.setOnFocusChangeListener { v, hasFocus ->
@@ -83,7 +79,7 @@ class FrequencyPickerDialog(
contentView.xTimesPerWeekRadioButton.setOnClickListener { contentView.xTimesPerWeekRadioButton.setOnClickListener {
check(contentView.xTimesPerWeekRadioButton) check(contentView.xTimesPerWeekRadioButton)
selectInputField(contentView.xTimesPerWeekTextView) focus(contentView.xTimesPerWeekTextView)
} }
contentView.xTimesPerWeekTextView.setOnFocusChangeListener { v, hasFocus -> contentView.xTimesPerWeekTextView.setOnFocusChangeListener { v, hasFocus ->
@@ -92,32 +88,17 @@ class FrequencyPickerDialog(
contentView.xTimesPerMonthRadioButton.setOnClickListener { contentView.xTimesPerMonthRadioButton.setOnClickListener {
check(contentView.xTimesPerMonthRadioButton) check(contentView.xTimesPerMonthRadioButton)
selectInputField(contentView.xTimesPerMonthTextView) focus(contentView.xTimesPerMonthTextView)
} }
contentView.xTimesPerMonthTextView.setOnFocusChangeListener { v, hasFocus -> contentView.xTimesPerMonthTextView.setOnFocusChangeListener { v, hasFocus ->
if (hasFocus) check(contentView.xTimesPerMonthRadioButton) if (hasFocus) check(contentView.xTimesPerMonthRadioButton)
} }
contentView.xTimesPerYDaysRadioButton.setOnClickListener { return AlertDialog.Builder(activity!!)
check(contentView.xTimesPerYDaysRadioButton)
selectInputField(contentView.xTimesPerYDaysXTextView)
}
contentView.xTimesPerYDaysXTextView.setOnFocusChangeListener { v, hasFocus ->
if (hasFocus) check(contentView.xTimesPerYDaysRadioButton)
}
contentView.xTimesPerYDaysYTextView.setOnFocusChangeListener { v, hasFocus ->
if (hasFocus) check(contentView.xTimesPerYDaysRadioButton)
}
val dialog = AlertDialog.Builder(requireActivity())
.setView(contentView) .setView(contentView)
.setPositiveButton(R.string.save) { _, _ -> onSaveClicked() } .setPositiveButton(R.string.save) { _, _ -> onSaveClicked() }
.create() .create()
dialog.dismissCurrent()
return dialog
} }
private fun addBeforeAfterText( private fun addBeforeAfterText(
@@ -125,11 +106,12 @@ class FrequencyPickerDialog(
container: LinearLayout container: LinearLayout
) { ) {
val parts = str.split("%d") val parts = str.split("%d")
for (i in parts.indices) { container.addView(
container.addView( TextView(activity).apply { text = parts[0].trim() }, 1,
TextView(activity).apply { text = parts[i].trim() }, 2 * i + 1, )
) container.addView(
} TextView(activity).apply { text = parts[1].trim() }, 3,
)
} }
private fun onSaveClicked() { private fun onSaveClicked() {
@@ -150,12 +132,6 @@ class FrequencyPickerDialog(
denominator = 7 denominator = 7
} }
} }
contentView.xTimesPerYDaysRadioButton.isChecked -> {
if (contentView.xTimesPerYDaysXTextView.text.isNotEmpty() && contentView.xTimesPerYDaysYTextView.text.isNotEmpty()) {
numerator = Integer.parseInt(contentView.xTimesPerYDaysXTextView.text.toString())
denominator = Integer.parseInt(contentView.xTimesPerYDaysYTextView.text.toString())
}
}
else -> { else -> {
if (contentView.xTimesPerMonthTextView.text.isNotEmpty()) { if (contentView.xTimesPerMonthTextView.text.isNotEmpty()) {
numerator = Integer.parseInt(contentView.xTimesPerMonthTextView.text.toString()) numerator = Integer.parseInt(contentView.xTimesPerMonthTextView.text.toString())
@@ -171,10 +147,10 @@ class FrequencyPickerDialog(
dismiss() dismiss()
} }
private fun check(view: RadioButton) { private fun check(view: RadioButton?) {
uncheckAll() uncheckAll()
view.isChecked = true view?.isChecked = true
view.requestFocus() view?.requestFocus()
} }
override fun onResume() { override fun onResume() {
@@ -187,7 +163,7 @@ class FrequencyPickerDialog(
if (freqDenominator == 30 || freqDenominator == 31) { if (freqDenominator == 30 || freqDenominator == 31) {
contentView.xTimesPerMonthRadioButton.isChecked = true contentView.xTimesPerMonthRadioButton.isChecked = true
contentView.xTimesPerMonthTextView.setText(freqNumerator.toString()) contentView.xTimesPerMonthTextView.setText(freqNumerator.toString())
selectInputField(contentView.xTimesPerMonthTextView) focus(contentView.xTimesPerMonthTextView)
} else { } else {
if (freqNumerator == 1) { if (freqNumerator == 1) {
if (freqDenominator == 1) { if (freqDenominator == 1) {
@@ -195,23 +171,23 @@ class FrequencyPickerDialog(
} else { } else {
contentView.everyXDaysRadioButton.isChecked = true contentView.everyXDaysRadioButton.isChecked = true
contentView.everyXDaysTextView.setText(freqDenominator.toString()) contentView.everyXDaysTextView.setText(freqDenominator.toString())
selectInputField(contentView.everyXDaysTextView) focus(contentView.everyXDaysTextView)
} }
} else { } else {
if (freqDenominator == 7) { if (freqDenominator == 7) {
contentView.xTimesPerWeekRadioButton.isChecked = true contentView.xTimesPerWeekRadioButton.isChecked = true
contentView.xTimesPerWeekTextView.setText(freqNumerator.toString()) contentView.xTimesPerWeekTextView.setText(freqNumerator.toString())
selectInputField(contentView.xTimesPerWeekTextView) focus(contentView.xTimesPerWeekTextView)
} else { } else {
contentView.xTimesPerYDaysRadioButton.isChecked = true Log.w("FrequencyPickerDialog", "Unknown frequency: $freqNumerator/$freqDenominator")
contentView.xTimesPerYDaysXTextView.setText(freqNumerator.toString()) contentView.everyDayRadioButton.isChecked = true
contentView.xTimesPerYDaysYTextView.setText(freqDenominator.toString())
} }
} }
} }
} }
private fun selectInputField(view: EditText) { private fun focus(view: EditText) {
view.requestFocus()
view.setSelection(view.text.length) view.setSelection(view.text.length)
} }
@@ -220,6 +196,11 @@ class FrequencyPickerDialog(
contentView.everyXDaysRadioButton.isChecked = false contentView.everyXDaysRadioButton.isChecked = false
contentView.xTimesPerWeekRadioButton.isChecked = false contentView.xTimesPerWeekRadioButton.isChecked = false
contentView.xTimesPerMonthRadioButton.isChecked = false contentView.xTimesPerMonthRadioButton.isChecked = false
contentView.xTimesPerYDaysRadioButton.isChecked = false }
private fun unfocusAll() {
contentView.everyXDaysTextView.clearFocus()
contentView.xTimesPerWeekTextView.clearFocus()
contentView.xTimesPerMonthTextView.clearFocus()
} }
} }

View File

@@ -19,7 +19,6 @@
package org.isoron.uhabits.activities.common.dialogs package org.isoron.uhabits.activities.common.dialogs
import android.app.Dialog import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatDialogFragment import androidx.appcompat.app.AppCompatDialogFragment
import org.isoron.platform.gui.AndroidDataView import org.isoron.platform.gui.AndroidDataView
@@ -44,19 +43,18 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
private lateinit var commandRunner: CommandRunner private lateinit var commandRunner: CommandRunner
private lateinit var habit: Habit private lateinit var habit: Habit
private lateinit var preferences: Preferences private lateinit var preferences: Preferences
lateinit var dataView: AndroidDataView private lateinit var dataView: AndroidDataView
private var chart: HistoryChart? = null private var chart: HistoryChart? = null
private var onDateClickedListener: OnDateClickedListener? = null private var onDateClickedListener: OnDateClickedListener? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
clearCurrentDialog() val component = (activity!!.application as HabitsApplication).component
val component = (requireActivity().application as HabitsApplication).component
commandRunner = component.commandRunner commandRunner = component.commandRunner
habit = component.habitList.getById(requireArguments().getLong("habit"))!! habit = component.habitList.getById(arguments!!.getLong("habit"))!!
preferences = component.preferences preferences = component.preferences
val themeSwitcher = AndroidThemeSwitcher(requireActivity(), preferences) val themeSwitcher = AndroidThemeSwitcher(activity!!, preferences)
themeSwitcher.apply() themeSwitcher.apply()
chart = HistoryChart( chart = HistoryChart(
@@ -64,30 +62,20 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
firstWeekday = preferences.firstWeekday, firstWeekday = preferences.firstWeekday,
paletteColor = habit.color, paletteColor = habit.color,
series = emptyList(), series = emptyList(),
defaultSquare = HistoryChart.Square.OFF,
notesIndicators = emptyList(),
theme = themeSwitcher.currentTheme, theme = themeSwitcher.currentTheme,
today = DateUtils.getTodayWithOffset().toLocalDate(), today = DateUtils.getTodayWithOffset().toLocalDate(),
onDateClickedListener = onDateClickedListener ?: object : OnDateClickedListener {}, onDateClickedListener = onDateClickedListener ?: OnDateClickedListener { },
padding = 10.0, padding = 10.0,
) )
dataView = AndroidDataView(requireContext(), null) dataView = AndroidDataView(context!!, null)
dataView.view = chart!! dataView.view = chart!!
val dialog = Dialog(requireContext()).apply { return Dialog(context!!).apply {
val metrics = resources.displayMetrics val metrics = resources.displayMetrics
val maxHeight = resources.getDimensionPixelSize(R.dimen.history_editor_max_height) val maxHeight = resources.getDimensionPixelSize(R.dimen.history_editor_max_height)
setContentView(dataView) setContentView(dataView)
window!!.setLayout(metrics.widthPixels, min(metrics.heightPixels, maxHeight)) 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() { override fun onResume() {
@@ -113,22 +101,10 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
theme = LightTheme() theme = LightTheme()
) )
chart?.series = model.series chart?.series = model.series
chart?.defaultSquare = model.defaultSquare
chart?.notesIndicators = model.notesIndicators
dataView.postInvalidate() dataView.postInvalidate()
} }
override fun onCommandFinished(command: Command) { override fun onCommandFinished(command: Command) {
refreshData() 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
}
}
} }

View File

@@ -1,15 +0,0 @@
package org.isoron.uhabits.activities.common.dialogs
import android.app.Dialog
import java.lang.ref.WeakReference
class MultipleDialogsHandler {
companion object {
var currentDialog: WeakReference<Dialog> = WeakReference(null)
fun Dialog.dismissCurrent() {
currentDialog.get()?.dismiss()
currentDialog = WeakReference(this)
}
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.dialogs
import android.content.Context
import android.content.DialogInterface
import android.text.InputFilter
import android.view.LayoutInflater
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.NumberPicker
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import org.isoron.uhabits.R
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.InterfaceUtils
import javax.inject.Inject
import kotlin.math.roundToLong
class NumberPickerFactory
@Inject constructor(
@ActivityContext private val context: Context
) {
fun create(
value: Double,
unit: String,
callback: ListHabitsBehavior.NumberPickerCallback
): AlertDialog {
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.number_picker_dialog, null)
val picker = view.findViewById<NumberPicker>(R.id.picker)
val picker2 = view.findViewById<NumberPicker>(R.id.picker2)
val tvUnit = view.findViewById<TextView>(R.id.tvUnit)
val intValue = (value * 100).roundToLong().toInt()
picker.minValue = 0
picker.maxValue = Integer.MAX_VALUE / 100
picker.value = intValue / 100
picker.wrapSelectorWheel = false
picker2.minValue = 0
picker2.maxValue = 19
picker2.setFormatter { v -> String.format("%02d", 5 * v) }
picker2.value = intValue % 100 / 5
refreshInitialValue(picker2)
tvUnit.text = unit
val dialog = AlertDialog.Builder(context)
.setView(view)
.setTitle(R.string.change_value)
.setPositiveButton(android.R.string.ok) { _, _ ->
picker.clearFocus()
val v = picker.value + 0.05 * picker2.value
callback.onNumberPicked(v)
}
.setOnDismissListener {
callback.onNumberPickerDismissed()
}
.create()
dialog.setOnShowListener {
picker.getChildAt(0)?.requestFocus()
dialog.window?.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE)
}
InterfaceUtils.setupEditorAction(
picker
) { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE)
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick()
false
}
return dialog
}
private fun refreshInitialValue(picker: NumberPicker) {
// Workaround for Android bug:
// https://code.google.com/p/android/issues/detail?id=35482
val f = NumberPicker::class.java.getDeclaredField("mInputText")
f.isAccessible = true
val inputText = f.get(picker) as EditText
inputText.filters = arrayOfNulls<InputFilter>(0)
}
}

View File

@@ -1,117 +0,0 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.dialogs
import android.app.Dialog
import android.content.Context
import android.view.KeyEvent.KEYCODE_ENTER
import android.view.LayoutInflater
import android.view.MotionEvent.ACTION_DOWN
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import org.isoron.uhabits.activities.common.dialogs.MultipleDialogsHandler.Companion.dismissCurrent
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.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.dismissCurrent()
dialog.show()
}
fun save() {
val value = view.value.text.toString().toDoubleOrNull() ?: originalValue
val notes = view.notes.text.toString()
onToggle(value, notes)
dialog.dismiss()
}
}

View File

@@ -25,7 +25,6 @@ import android.os.Bundle
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment import androidx.appcompat.app.AppCompatDialogFragment
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.MultipleDialogsHandler.Companion.dismissCurrent
import org.isoron.uhabits.core.models.WeekdayList import org.isoron.uhabits.core.models.WeekdayList
import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.core.utils.DateUtils
import java.util.Calendar import java.util.Calendar
@@ -61,7 +60,7 @@ class WeekdayPickerDialog :
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder( val builder = AlertDialog.Builder(
requireActivity() activity!!
) )
builder builder
.setTitle(R.string.select_weekdays) .setTitle(R.string.select_weekdays)
@@ -74,10 +73,7 @@ class WeekdayPickerDialog :
.setNegativeButton( .setNegativeButton(
android.R.string.cancel android.R.string.cancel
) { _: DialogInterface?, _: Int -> dismiss() } ) { _: DialogInterface?, _: Int -> dismiss() }
return builder.create()
val dialog = builder.create()
dialog.dismissCurrent()
return dialog
} }
fun setListener(listener: OnWeekdaysPickedListener?) { fun setListener(listener: OnWeekdaysPickedListener?) {

View File

@@ -29,7 +29,6 @@ import org.isoron.uhabits.core.utils.DateUtils.Companion.getShortWeekdayNames
import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendar import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendar
import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendarWithOffset 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.getWeekdaySequence
import org.isoron.uhabits.core.utils.DateUtils.Companion.getWeekdaysInMonth
import org.isoron.uhabits.utils.ColorUtils.mixColors import org.isoron.uhabits.utils.ColorUtils.mixColors
import org.isoron.uhabits.utils.StyledResources import org.isoron.uhabits.utils.StyledResources
import org.isoron.uhabits.utils.toSimpleDataFormat import org.isoron.uhabits.utils.toSimpleDataFormat
@@ -63,6 +62,7 @@ class FrequencyChart : ScrollableChart {
private var primaryColor = 0 private var primaryColor = 0
private var isBackgroundTransparent = false private var isBackgroundTransparent = false
private lateinit var frequency: HashMap<Timestamp, Array<Int>> private lateinit var frequency: HashMap<Timestamp, Array<Int>>
private var maxFreq = 0
private var firstWeekday = Calendar.SUNDAY private var firstWeekday = Calendar.SUNDAY
constructor(context: Context?) : super(context) { constructor(context: Context?) : super(context) {
@@ -82,6 +82,7 @@ class FrequencyChart : ScrollableChart {
fun setFrequency(frequency: java.util.HashMap<Timestamp, Array<Int>>) { fun setFrequency(frequency: java.util.HashMap<Timestamp, Array<Int>>) {
this.frequency = frequency this.frequency = frequency
maxFreq = getMaxFreq(frequency)
postInvalidate() postInvalidate()
} }
@@ -90,6 +91,15 @@ class FrequencyChart : ScrollableChart {
postInvalidate() postInvalidate()
} }
private fun getMaxFreq(frequency: HashMap<Timestamp, Array<Int>>): Int {
var maxValue = 1
for (values in frequency.values) for (value in values) maxValue = max(
value,
maxValue
)
return maxValue
}
fun setIsBackgroundTransparent(isBackgroundTransparent: Boolean) { fun setIsBackgroundTransparent(isBackgroundTransparent: Boolean) {
this.isBackgroundTransparent = isBackgroundTransparent this.isBackgroundTransparent = isBackgroundTransparent
initColors() initColors()
@@ -156,7 +166,6 @@ class FrequencyChart : ScrollableChart {
private fun drawColumn(canvas: Canvas, rect: RectF?, date: GregorianCalendar) { private fun drawColumn(canvas: Canvas, rect: RectF?, date: GregorianCalendar) {
val values = frequency[Timestamp(date)] val values = frequency[Timestamp(date)]
val weekDaysInMonth = getWeekdaysInMonth(Timestamp(date))
val rowHeight = rect!!.height() / 8.0f val rowHeight = rect!!.height() / 8.0f
prevRect!!.set(rect) prevRect!!.set(rect)
val localeWeekdayList: Array<Int> = getWeekdaySequence(firstWeekday) val localeWeekdayList: Array<Int> = getWeekdaySequence(firstWeekday)
@@ -164,8 +173,7 @@ class FrequencyChart : ScrollableChart {
rect[0f, 0f, baseSize.toFloat()] = baseSize.toFloat() rect[0f, 0f, baseSize.toFloat()] = baseSize.toFloat()
rect.offset(prevRect!!.left, prevRect!!.top + baseSize * j) rect.offset(prevRect!!.left, prevRect!!.top + baseSize * j)
val i = localeWeekdayList[j] % 7 val i = localeWeekdayList[j] % 7
if (values != null) if (values != null) drawMarker(canvas, rect, values[i])
drawMarker(canvas, rect, values[i], weekDaysInMonth[i])
rect.offset(0f, rowHeight) rect.offset(0f, rowHeight)
} }
drawFooter(canvas, rect, date) drawFooter(canvas, rect, date)
@@ -213,16 +221,12 @@ class FrequencyChart : ScrollableChart {
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid!!) canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid!!)
} }
private fun drawMarker(canvas: Canvas, rect: RectF?, value: Int?, frequency: Int) { private fun drawMarker(canvas: Canvas, rect: RectF?, value: Int?) {
// value can be negative when the entry is skipped
val valueCopy = value?.let { max(0, it) }
val padding = rect!!.height() * 0.2f val padding = rect!!.height() * 0.2f
// maximal allowed mark radius // maximal allowed mark radius
val maxRadius = (rect.height() - 2 * padding) / 2.0f val maxRadius = (rect.height() - 2 * padding) / 2.0f
// the real mark radius is scaled down by a factor depending on the maximal frequency // the real mark radius is scaled down by a factor depending on the maximal frequency
val scale = 1.0f / maxFreq * value!!
val scale = 1.0f / frequency * valueCopy!!
val radius = maxRadius * scale val radius = maxRadius * scale
val colorIndex = min((colors.size - 1), ((colors.size - 1) * scale).roundToInt()) val colorIndex = min((colors.size - 1), ((colors.size - 1) * scale).roundToInt())
pGraph!!.color = colors[colorIndex] pGraph!!.color = colors[colorIndex]
@@ -285,5 +289,6 @@ class FrequencyChart : ScrollableChart {
frequency[Timestamp(date)] = values frequency[Timestamp(date)] = values
date.add(Calendar.MONTH, -1) date.add(Calendar.MONTH, -1)
} }
maxFreq = getMaxFreq(frequency)
} }
} }

View File

@@ -46,7 +46,6 @@ import org.isoron.uhabits.R
import org.isoron.uhabits.activities.AndroidThemeSwitcher import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
import org.isoron.uhabits.activities.common.dialogs.FrequencyPickerDialog import org.isoron.uhabits.activities.common.dialogs.FrequencyPickerDialog
import org.isoron.uhabits.activities.common.dialogs.MultipleDialogsHandler.Companion.dismissCurrent
import org.isoron.uhabits.activities.common.dialogs.WeekdayPickerDialog import org.isoron.uhabits.activities.common.dialogs.WeekdayPickerDialog
import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.commands.CreateHabitCommand import org.isoron.uhabits.core.commands.CreateHabitCommand
@@ -70,7 +69,7 @@ fun formatFrequency(freqNum: Int, freqDen: Int, resources: Resources) = when {
freqNum == 1 && freqDen == 7 -> resources.getString(R.string.every_week) freqNum == 1 && freqDen == 7 -> resources.getString(R.string.every_week)
freqNum == 1 && freqDen > 1 -> resources.getString(R.string.every_x_days, freqDen) freqNum == 1 && freqDen > 1 -> resources.getString(R.string.every_x_days, freqDen)
freqDen == 7 -> resources.getString(R.string.x_times_per_week, freqNum) freqDen == 7 -> resources.getString(R.string.x_times_per_week, freqNum)
else -> resources.getString(R.string.x_times_per_y_days, freqNum, freqDen) else -> "$freqNum/$freqDen"
} }
class EditHabitActivity : AppCompatActivity() { class EditHabitActivity : AppCompatActivity() {
@@ -89,7 +88,6 @@ class EditHabitActivity : AppCompatActivity() {
var reminderHour = -1 var reminderHour = -1
var reminderMin = -1 var reminderMin = -1
var reminderDays: WeekdayList = WeekdayList.EVERY_DAY var reminderDays: WeekdayList = WeekdayList.EVERY_DAY
var targetType = NumericalHabitType.AT_LEAST
override fun onCreate(state: Bundle?) { override fun onCreate(state: Bundle?) {
super.onCreate(state) super.onCreate(state)
@@ -109,7 +107,6 @@ class EditHabitActivity : AppCompatActivity() {
color = habit.color color = habit.color
freqNum = habit.frequency.numerator freqNum = habit.frequency.numerator
freqDen = habit.frequency.denominator freqDen = habit.frequency.denominator
targetType = habit.targetType
habit.reminder?.let { habit.reminder?.let {
reminderHour = it.hour reminderHour = it.hour
reminderMin = it.minute reminderMin = it.minute
@@ -141,7 +138,6 @@ class EditHabitActivity : AppCompatActivity() {
HabitType.YES_NO -> { HabitType.YES_NO -> {
binding.unitOuterBox.visibility = View.GONE binding.unitOuterBox.visibility = View.GONE
binding.targetOuterBox.visibility = View.GONE binding.targetOuterBox.visibility = View.GONE
binding.targetTypeOuterBox.visibility = View.GONE
} }
HabitType.NUMERICAL -> { HabitType.NUMERICAL -> {
binding.nameInput.hint = getString(R.string.measurable_short_example) binding.nameInput.hint = getString(R.string.measurable_short_example)
@@ -176,25 +172,6 @@ class EditHabitActivity : AppCompatActivity() {
dialog.show(supportFragmentManager, "frequencyPicker") dialog.show(supportFragmentManager, "frequencyPicker")
} }
populateTargetType()
binding.targetTypePicker.setOnClickListener {
val builder = AlertDialog.Builder(this)
val arrayAdapter = ArrayAdapter<String>(this, android.R.layout.select_dialog_item)
arrayAdapter.add(getString(R.string.target_type_at_least))
arrayAdapter.add(getString(R.string.target_type_at_most))
builder.setAdapter(arrayAdapter) { dialog, which ->
targetType = when (which) {
0 -> NumericalHabitType.AT_LEAST
else -> NumericalHabitType.AT_MOST
}
populateTargetType()
dialog.dismiss()
}
val dialog = builder.create()
dialog.dismissCurrent()
dialog.show()
}
binding.numericalFrequencyPicker.setOnClickListener { binding.numericalFrequencyPicker.setOnClickListener {
val builder = AlertDialog.Builder(this) val builder = AlertDialog.Builder(this)
val arrayAdapter = ArrayAdapter<String>(this, android.R.layout.select_dialog_item) val arrayAdapter = ArrayAdapter<String>(this, android.R.layout.select_dialog_item)
@@ -285,7 +262,7 @@ class EditHabitActivity : AppCompatActivity() {
habit.frequency = Frequency(freqNum, freqDen) habit.frequency = Frequency(freqNum, freqDen)
if (habitType == HabitType.NUMERICAL) { if (habitType == HabitType.NUMERICAL) {
habit.targetValue = targetInput.text.toString().toDouble() habit.targetValue = targetInput.text.toString().toDouble()
habit.targetType = targetType habit.targetType = NumericalHabitType.AT_LEAST
habit.unit = unitInput.text.trim().toString() habit.unit = unitInput.text.trim().toString()
} }
habit.type = habitType habit.type = habitType
@@ -347,13 +324,6 @@ class EditHabitActivity : AppCompatActivity() {
} }
} }
private fun populateTargetType() {
binding.targetTypePicker.text = when (targetType) {
NumericalHabitType.AT_MOST -> getString(R.string.target_type_at_most)
else -> getString(R.string.target_type_at_least)
}
}
private fun updateColors() { private fun updateColors() {
androidColor = themeSwitcher.currentTheme.color(color).toInt() androidColor = themeSwitcher.currentTheme.color(color).toInt()
binding.colorButton.backgroundTintList = ColorStateList.valueOf(androidColor) binding.colorButton.backgroundTintList = ColorStateList.valueOf(androidColor)

View File

@@ -26,46 +26,43 @@ import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.isoron.uhabits.BaseExceptionHandler import org.isoron.uhabits.BaseExceptionHandler
import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter 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.preferences.Preferences
import org.isoron.uhabits.core.sync.SyncManager
import org.isoron.uhabits.core.tasks.TaskRunner import org.isoron.uhabits.core.tasks.TaskRunner
import org.isoron.uhabits.core.ui.ThemeSwitcher.Companion.THEME_DARK import org.isoron.uhabits.core.ui.ThemeSwitcher.Companion.THEME_DARK
import org.isoron.uhabits.core.utils.MidnightTimer import org.isoron.uhabits.core.utils.MidnightTimer
import org.isoron.uhabits.database.AutoBackup import org.isoron.uhabits.database.AutoBackup
import org.isoron.uhabits.inject.ActivityContextModule import org.isoron.uhabits.inject.ActivityContextModule
import org.isoron.uhabits.inject.DaggerHabitsActivityComponent import org.isoron.uhabits.inject.DaggerHabitsActivityComponent
import org.isoron.uhabits.inject.HabitsActivityComponent
import org.isoron.uhabits.inject.HabitsApplicationComponent
import org.isoron.uhabits.utils.restartWithFade import org.isoron.uhabits.utils.restartWithFade
class ListHabitsActivity : AppCompatActivity(), Preferences.Listener { class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
var pureBlack: Boolean = false var pureBlack: Boolean = false
lateinit var appComponent: HabitsApplicationComponent
lateinit var component: HabitsActivityComponent
lateinit var taskRunner: TaskRunner lateinit var taskRunner: TaskRunner
lateinit var adapter: HabitCardListAdapter lateinit var adapter: HabitCardListAdapter
lateinit var rootView: ListHabitsRootView lateinit var rootView: ListHabitsRootView
lateinit var screen: ListHabitsScreen lateinit var screen: ListHabitsScreen
lateinit var prefs: Preferences lateinit var prefs: Preferences
lateinit var midnightTimer: MidnightTimer lateinit var midnightTimer: MidnightTimer
lateinit var syncManager: SyncManager
private val scope = CoroutineScope(Dispatchers.Main) private val scope = CoroutineScope(Dispatchers.Main)
private lateinit var menu: ListHabitsMenu private lateinit var menu: ListHabitsMenu
override fun onQuestionMarksChanged() { override fun onQuestionMarksChanged() {
invalidateOptionsMenu() invalidateOptionsMenu()
menu.behavior.onPreferencesChanged()
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
appComponent = (applicationContext as HabitsApplication).component val appComponent = (applicationContext as HabitsApplication).component
component = DaggerHabitsActivityComponent val component = DaggerHabitsActivityComponent
.builder() .builder()
.activityContextModule(ActivityContextModule(this)) .activityContextModule(ActivityContextModule(this))
.habitsApplicationComponent(appComponent) .habitsApplicationComponent(appComponent)
@@ -74,6 +71,7 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
prefs = appComponent.preferences prefs = appComponent.preferences
prefs.addListener(this) prefs.addListener(this)
syncManager = appComponent.syncManager
pureBlack = prefs.isPureBlackEnabled pureBlack = prefs.isPureBlackEnabled
midnightTimer = appComponent.midnightTimer midnightTimer = appComponent.midnightTimer
rootView = component.listHabitsRootView rootView = component.listHabitsRootView
@@ -84,13 +82,15 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
Thread.setDefaultUncaughtExceptionHandler(BaseExceptionHandler(this)) Thread.setDefaultUncaughtExceptionHandler(BaseExceptionHandler(this))
component.listHabitsBehavior.onStartup() component.listHabitsBehavior.onStartup()
setContentView(rootView) setContentView(rootView)
parseIntents()
} }
override fun onPause() { override fun onPause() {
midnightTimer.onPause() midnightTimer.onPause()
screen.onDetached() screen.onDettached()
adapter.cancelRefresh() adapter.cancelRefresh()
scope.launch {
syncManager.onPause()
}
super.onPause() super.onPause()
} }
@@ -99,6 +99,9 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
screen.onAttached() screen.onAttached()
rootView.postInvalidate() rootView.postInvalidate()
midnightTimer.onResume() midnightTimer.onResume()
scope.launch {
syncManager.onResume()
}
taskRunner.run { taskRunner.run {
AutoBackup(this@ListHabitsActivity).run() AutoBackup(this@ListHabitsActivity).run()
} }
@@ -122,19 +125,4 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
super.onActivityResult(request, result, data) super.onActivityResult(request, result, data)
screen.onResult(request, result, data) screen.onResult(request, result, data)
} }
private fun parseIntents() {
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))
}
}
}
companion object {
const val ACTION_EDIT = "org.isoron.uhabits.ACTION_EDIT"
}
} }

View File

@@ -39,7 +39,7 @@ class ListHabitsMenu @Inject constructor(
@ActivityContext context: Context, @ActivityContext context: Context,
private val preferences: Preferences, private val preferences: Preferences,
private val themeSwitcher: ThemeSwitcher, private val themeSwitcher: ThemeSwitcher,
val behavior: ListHabitsMenuBehavior private val behavior: ListHabitsMenuBehavior
) { ) {
val activity = (context as AppCompatActivity) val activity = (context as AppCompatActivity)

View File

@@ -22,14 +22,14 @@ package org.isoron.uhabits.activities.habits.list
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Log
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import dagger.Lazy import dagger.Lazy
import org.isoron.platform.gui.toInt
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.CheckmarkPopup
import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
import org.isoron.uhabits.activities.common.dialogs.NumberPopup import org.isoron.uhabits.activities.common.dialogs.ConfirmSyncKeyDialog
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
import org.isoron.uhabits.activities.habits.edit.HabitTypeDialog import org.isoron.uhabits.activities.habits.edit.HabitTypeDialog
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
import org.isoron.uhabits.core.commands.ArchiveHabitsCommand import org.isoron.uhabits.core.commands.ArchiveHabitsCommand
@@ -42,7 +42,6 @@ import org.isoron.uhabits.core.commands.EditHabitCommand
import org.isoron.uhabits.core.commands.UnarchiveHabitsCommand import org.isoron.uhabits.core.commands.UnarchiveHabitsCommand
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor 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.tasks.TaskRunner
import org.isoron.uhabits.core.ui.ThemeSwitcher import org.isoron.uhabits.core.ui.ThemeSwitcher
import org.isoron.uhabits.core.ui.callbacks.OnColorPickedCallback import org.isoron.uhabits.core.ui.callbacks.OnColorPickedCallback
@@ -54,6 +53,8 @@ import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.FILE_NOT_RECOGNIZED import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.FILE_NOT_RECOGNIZED
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.IMPORT_FAILED import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.IMPORT_FAILED
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.IMPORT_SUCCESSFUL import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.IMPORT_SUCCESSFUL
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.SYNC_ENABLED
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.SYNC_KEY_ALREADY_INSTALLED
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsMenuBehavior import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsMenuBehavior
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsSelectionMenuBehavior import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsSelectionMenuBehavior
import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.inject.ActivityContext
@@ -63,7 +64,6 @@ import org.isoron.uhabits.tasks.ExportDBTaskFactory
import org.isoron.uhabits.tasks.ImportDataTask import org.isoron.uhabits.tasks.ImportDataTask
import org.isoron.uhabits.tasks.ImportDataTaskFactory import org.isoron.uhabits.tasks.ImportDataTaskFactory
import org.isoron.uhabits.utils.copyTo import org.isoron.uhabits.utils.copyTo
import org.isoron.uhabits.utils.currentTheme
import org.isoron.uhabits.utils.restartWithFade import org.isoron.uhabits.utils.restartWithFade
import org.isoron.uhabits.utils.showMessage import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.showSendEmailScreen import org.isoron.uhabits.utils.showSendEmailScreen
@@ -92,9 +92,8 @@ class ListHabitsScreen
private val exportDBFactory: ExportDBTaskFactory, private val exportDBFactory: ExportDBTaskFactory,
private val importTaskFactory: ImportDataTaskFactory, private val importTaskFactory: ImportDataTaskFactory,
private val colorPickerFactory: ColorPickerDialogFactory, private val colorPickerFactory: ColorPickerDialogFactory,
private val behavior: Lazy<ListHabitsBehavior>, private val numberPickerFactory: NumberPickerFactory,
private val preferences: Preferences, private val behavior: Lazy<ListHabitsBehavior>
private val rootView: Lazy<ListHabitsRootView>,
) : CommandRunner.Listener, ) : CommandRunner.Listener,
ListHabitsBehavior.Screen, ListHabitsBehavior.Screen,
ListHabitsMenuBehavior.Screen, ListHabitsMenuBehavior.Screen,
@@ -104,9 +103,17 @@ class ListHabitsScreen
fun onAttached() { fun onAttached() {
commandRunner.addListener(this) commandRunner.addListener(this)
if (activity.intent.action == "android.intent.action.VIEW") {
val uri = activity.intent.data!!.toString()
val parts = uri.replace(Regex("^.*sync/"), "").split("#")
val syncKey = parts[0]
val encKey = parts[1]
Log.i("ListHabitsScreen", "sync: $syncKey enc: $encKey")
behavior.get().onSyncKeyOffer(syncKey, encKey)
}
} }
fun onDetached() { fun onDettached() {
commandRunner.removeListener(this) commandRunner.removeListener(this)
} }
@@ -201,6 +208,8 @@ class ListHabitsScreen
DATABASE_REPAIRED -> R.string.database_repaired DATABASE_REPAIRED -> R.string.database_repaired
COULD_NOT_GENERATE_BUG_REPORT -> R.string.bug_report_failed COULD_NOT_GENERATE_BUG_REPORT -> R.string.bug_report_failed
FILE_NOT_RECOGNIZED -> R.string.file_not_recognized FILE_NOT_RECOGNIZED -> R.string.file_not_recognized
SYNC_ENABLED -> R.string.sync_enabled
SYNC_KEY_ALREADY_INSTALLED -> R.string.sync_key_already_installed
} }
) )
) )
@@ -227,42 +236,16 @@ class ListHabitsScreen
picker.show(activity.supportFragmentManager, "picker") picker.show(activity.supportFragmentManager, "picker")
} }
override fun showNumberPopup( override fun showNumberPicker(
value: Double, value: Double,
notes: String, unit: String,
callback: ListHabitsBehavior.NumberPickerCallback callback: ListHabitsBehavior.NumberPickerCallback
) { ) {
val view = rootView.get() numberPickerFactory.create(value, unit, callback).show()
NumberPopup(
context = context,
prefs = preferences,
anchor = view,
notes = notes,
value = value,
).apply {
onToggle = { value, notes -> callback.onNumberPicked(value, notes) }
show()
}
} }
override fun showCheckmarkPopup( override fun showConfirmInstallSyncKey(callback: OnConfirmedCallback) {
selectedValue: Int, ConfirmSyncKeyDialog(activity, callback).show()
notes: String,
color: PaletteColor,
callback: ListHabitsBehavior.CheckMarkDialogCallback
) {
val view = rootView.get()
CheckmarkPopup(
context = context,
prefs = preferences,
anchor = view,
color = view.currentTheme().color(color).toInt(),
notes = notes,
value = selectedValue,
).apply {
onToggle = { value, notes -> callback.onNotesSaved(value, notes) }
show()
}
} }
private fun getExecuteString(command: Command): String? { private fun getExecuteString(command: Command): String? {

View File

@@ -37,15 +37,13 @@ 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.Entry.Companion.YES_MANUAL
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.drawNotesIndicator import org.isoron.uhabits.utils.dim
import org.isoron.uhabits.utils.getFontAwesome import org.isoron.uhabits.utils.getFontAwesome
import org.isoron.uhabits.utils.sp import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.sres import org.isoron.uhabits.utils.sres
import org.isoron.uhabits.utils.toMeasureSpec import org.isoron.uhabits.utils.toMeasureSpec
import javax.inject.Inject import javax.inject.Inject
const val TOGGLE_DELAY_MILLIS = 2000L
class CheckmarkButtonViewFactory class CheckmarkButtonViewFactory
@Inject constructor( @Inject constructor(
@ActivityContext val context: Context, @ActivityContext val context: Context,
@@ -73,42 +71,33 @@ class CheckmarkButtonView(
invalidate() invalidate()
} }
var notes = "" var onToggle: (Int) -> Unit = {}
set(value) {
field = value
invalidate()
}
var onToggle: (Int, String, Long) -> Unit = { _, _, _ -> }
var onEdit: () -> Unit = { }
private var drawer = Drawer() private var drawer = Drawer()
init { init {
isFocusable = false
setOnClickListener(this) setOnClickListener(this)
setOnLongClickListener(this) setOnLongClickListener(this)
} }
fun performToggle(delay: Long) { fun performToggle() {
value = Entry.nextToggleValue( value = if (preferences.isSkipEnabled) {
value = value, Entry.nextToggleValueWithSkip(value)
isSkipEnabled = preferences.isSkipEnabled, } else {
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled Entry.nextToggleValueWithoutSkip(value)
) }
onToggle(value, notes, delay) onToggle(value)
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
invalidate() invalidate()
} }
override fun onClick(v: View) { override fun onClick(v: View) {
if (preferences.isShortToggleEnabled) performToggle(TOGGLE_DELAY_MILLIS) if (preferences.isShortToggleEnabled) performToggle()
else onEdit() else showMessage(resources.getString(R.string.long_press_to_toggle))
} }
override fun onLongClick(v: View): Boolean { override fun onLongClick(v: View): Boolean {
if (preferences.isShortToggleEnabled) onEdit() performToggle()
else performToggle(TOGGLE_DELAY_MILLIS)
return true return true
} }
@@ -156,11 +145,6 @@ class CheckmarkButtonView(
} }
else -> R.string.fa_check else -> R.string.fa_check
} }
paint.textSize = when {
id == R.string.fa_question -> sp(12.0f)
value == YES_AUTO -> sp(13.0f)
else -> sp(14.0f)
}
if (value == YES_AUTO) { if (value == YES_AUTO) {
paint.strokeWidth = 5f paint.strokeWidth = 5f
paint.style = Paint.Style.STROKE paint.style = Paint.Style.STROKE
@@ -169,6 +153,11 @@ class CheckmarkButtonView(
paint.style = Paint.Style.FILL paint.style = Paint.Style.FILL
} }
paint.textSize = when (id) {
UNKNOWN -> dim(R.dimen.smallerTextSize)
else -> dim(R.dimen.smallTextSize)
}
val label = resources.getString(id) val label = resources.getString(id)
val em = paint.measureText("m") val em = paint.measureText("m")
@@ -181,8 +170,6 @@ class CheckmarkButtonView(
paint.style = Paint.Style.FILL paint.style = Paint.Style.FILL
canvas.drawText(label, rect.centerX(), rect.centerY(), paint) canvas.drawText(label, rect.centerX(), rect.centerY(), paint)
} }
drawNotesIndicator(canvas, color, em, notes)
} }
} }
} }

View File

@@ -54,19 +54,7 @@ class CheckmarkPanelView(
setupButtons() setupButtons()
} }
var notes = arrayOf<String>() var onToggle: (Timestamp, Int) -> Unit = { _, _ -> }
set(values) {
field = values
setupButtons()
}
var onToggle: (Timestamp, Int, String, Long) -> Unit = { _, _, _, _ -> }
set(value) {
field = value
setupButtons()
}
var onEdit: (Timestamp) -> Unit = { _ -> }
set(value) { set(value) {
field = value field = value
setupButtons() setupButtons()
@@ -84,13 +72,8 @@ class CheckmarkPanelView(
index + dataOffset < values.size -> values[index + dataOffset] index + dataOffset < values.size -> values[index + dataOffset]
else -> UNKNOWN else -> UNKNOWN
} }
button.notes = when {
index + dataOffset < notes.size -> notes[index + dataOffset]
else -> ""
}
button.color = color button.color = color
button.onToggle = { value, notes, delay -> onToggle(timestamp, value, notes, delay) } button.onToggle = { value -> onToggle(timestamp, value) }
button.onEdit = { onEdit(timestamp) }
} }
} }
} }

View File

@@ -124,9 +124,8 @@ class HabitCardListAdapter @Inject constructor(
val habit = cache.getHabitByPosition(position) val habit = cache.getHabitByPosition(position)
val score = cache.getScore(habit!!.id!!) val score = cache.getScore(habit!!.id!!)
val checkmarks = cache.getCheckmarks(habit.id!!) val checkmarks = cache.getCheckmarks(habit.id!!)
val notes = cache.getNotes(habit.id!!)
val selected = selected.contains(habit) val selected = selected.contains(habit)
listView!!.bindCardView(holder, habit, score, checkmarks, notes, selected) listView!!.bindCardView(holder, habit, score, checkmarks, selected)
} }
override fun onViewAttachedToWindow(holder: HabitCardViewHolder) { override fun onViewAttachedToWindow(holder: HabitCardViewHolder) {

View File

@@ -87,7 +87,6 @@ class HabitCardListView(
habit: Habit, habit: Habit,
score: Double, score: Double,
checkmarks: IntArray, checkmarks: IntArray,
notes: Array<String>,
selected: Boolean selected: Boolean
): View { ): View {
val cardView = holder.itemView as HabitCardView val cardView = holder.itemView as HabitCardView
@@ -99,7 +98,6 @@ class HabitCardListView(
cardView.score = score cardView.score = score
cardView.unit = habit.unit cardView.unit = habit.unit
cardView.threshold = habit.targetValue / habit.frequency.denominator cardView.threshold = habit.targetValue / habit.frequency.denominator
cardView.notes = notes
val detector = GestureDetector(context, CardViewGestureDetector(holder)) val detector = GestureDetector(context, CardViewGestureDetector(holder))
cardView.setOnTouchListener { _, ev -> cardView.setOnTouchListener { _, ev ->

View File

@@ -21,8 +21,8 @@ package org.isoron.uhabits.activities.habits.list.views
import android.content.Context import android.content.Context
import android.graphics.text.LineBreaker.BREAK_STRATEGY_BALANCED import android.graphics.text.LineBreaker.BREAK_STRATEGY_BALANCED
import android.os.Build
import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.M
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.text.TextUtils import android.text.TextUtils
@@ -57,13 +57,6 @@ class HabitCardViewFactory
fun create() = HabitCardView(context, checkmarkPanelFactory, numberPanelFactory, behavior) fun create() = HabitCardView(context, checkmarkPanelFactory, numberPanelFactory, behavior)
} }
data class DelayedToggle(
var habit: Habit,
var timestamp: Timestamp,
var value: Int,
var notes: String
)
class HabitCardView( class HabitCardView(
@ActivityContext context: Context, @ActivityContext context: Context,
checkmarkPanelFactory: CheckmarkPanelViewFactory, checkmarkPanelFactory: CheckmarkPanelViewFactory,
@@ -122,22 +115,12 @@ class HabitCardView(
numberPanel.threshold = value numberPanel.threshold = value
} }
var notes
get() = checkmarkPanel.notes
set(values) {
checkmarkPanel.notes = values
numberPanel.notes = values
}
var checkmarkPanel: CheckmarkPanelView var checkmarkPanel: CheckmarkPanelView
private var numberPanel: NumberPanelView private var numberPanel: NumberPanelView
private var innerFrame: LinearLayout private var innerFrame: LinearLayout
private var label: TextView private var label: TextView
private var scoreRing: RingView private var scoreRing: RingView
private var currentToggleTaskId = 0
private var queuedToggles = mutableListOf<DelayedToggle>()
init { init {
scoreRing = RingView(context).apply { scoreRing = RingView(context).apply {
val thickness = dp(3f) val thickness = dp(3f)
@@ -154,22 +137,13 @@ class HabitCardView(
maxLines = 2 maxLines = 2
ellipsize = TextUtils.TruncateAt.END ellipsize = TextUtils.TruncateAt.END
layoutParams = LinearLayout.LayoutParams(0, WRAP_CONTENT, 1f) layoutParams = LinearLayout.LayoutParams(0, WRAP_CONTENT, 1f)
if (SDK_INT >= Build.VERSION_CODES.Q) { if (SDK_INT >= M) breakStrategy = BREAK_STRATEGY_BALANCED
breakStrategy = BREAK_STRATEGY_BALANCED
}
} }
checkmarkPanel = checkmarkPanelFactory.create().apply { checkmarkPanel = checkmarkPanelFactory.create().apply {
onToggle = { timestamp, value, notes, delay -> onToggle = { timestamp, value ->
if (delay > 0) triggerRipple(timestamp)
habit?.let {
val taskId = queueToggle(it, timestamp, value, notes);
{ runPendingToggles(taskId) }.delay(delay)
}
}
onEdit = { timestamp ->
triggerRipple(timestamp) triggerRipple(timestamp)
habit?.let { behavior.onEdit(it, timestamp) } habit?.let { behavior.onToggle(it, timestamp, value) }
} }
} }
@@ -205,25 +179,6 @@ class HabitCardView(
addView(innerFrame) addView(innerFrame)
} }
@Synchronized
private fun runPendingToggles(id: Int) {
if (currentToggleTaskId != id) return
for ((h, t, v, n) in queuedToggles) behavior.onToggle(h, t, v, n)
queuedToggles.clear()
}
@Synchronized
private fun queueToggle(
it: Habit,
timestamp: Timestamp,
value: Int,
notes: String,
): Int {
currentToggleTaskId += 1
queuedToggles.add(DelayedToggle(it, timestamp, value, notes))
return currentToggleTaskId
}
override fun onModelChange() { override fun onModelChange() {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
habit?.let { copyAttributesFrom(it) } habit?.let { copyAttributesFrom(it) }
@@ -281,7 +236,6 @@ class HabitCardView(
numberPanel.apply { numberPanel.apply {
color = c color = c
units = h.unit units = h.unit
targetType = h.targetType
threshold = h.targetValue threshold = h.targetValue
visibility = when (h.isNumerical) { visibility = when (h.isNumerical) {
true -> View.VISIBLE true -> View.VISIBLE
@@ -308,10 +262,4 @@ class HabitCardView(
} }
innerFrame.setBackgroundResource(background) innerFrame.setBackgroundResource(background)
} }
companion object {
fun (() -> Unit).delay(delayInMillis: Long) {
Handler(Looper.getMainLooper()).postDelayed(this, delayInMillis)
}
}
} }

View File

@@ -29,16 +29,13 @@ import android.view.View
import android.view.View.OnClickListener import android.view.View.OnClickListener
import android.view.View.OnLongClickListener import android.view.View.OnLongClickListener
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.NumericalHabitType.AT_LEAST
import org.isoron.uhabits.core.models.NumericalHabitType.AT_MOST
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.InterfaceUtils.getDimension import org.isoron.uhabits.utils.InterfaceUtils.getDimension
import org.isoron.uhabits.utils.StyledResources
import org.isoron.uhabits.utils.dim import org.isoron.uhabits.utils.dim
import org.isoron.uhabits.utils.drawNotesIndicator
import org.isoron.uhabits.utils.getFontAwesome import org.isoron.uhabits.utils.getFontAwesome
import org.isoron.uhabits.utils.sres import org.isoron.uhabits.utils.showMessage
import java.text.DecimalFormat import java.text.DecimalFormat
import javax.inject.Inject import javax.inject.Inject
@@ -91,25 +88,13 @@ class NumberButtonView(
invalidate() invalidate()
} }
var targetType = AT_LEAST
set(value) {
field = value
invalidate()
}
var units = "" var units = ""
set(value) { set(value) {
field = value field = value
invalidate() invalidate()
} }
var notes = ""
set(value) {
field = value
invalidate()
}
var onEdit: () -> Unit = { }
var onEdit: () -> Unit = {}
private var drawer: Drawer = Drawer(context) private var drawer: Drawer = Drawer(context)
init { init {
@@ -118,7 +103,8 @@ class NumberButtonView(
} }
override fun onClick(v: View) { override fun onClick(v: View) {
onEdit() if (preferences.isShortToggleEnabled) onEdit()
else showMessage(resources.getString(R.string.long_press_to_edit))
} }
override fun onLongClick(v: View): Boolean { override fun onLongClick(v: View): Boolean {
@@ -141,16 +127,11 @@ class NumberButtonView(
private val em: Float private val em: Float
private val rect: RectF = RectF() private val rect: RectF = RectF()
private val sr = StyledResources(context)
private val lowContrast: Int private val lowContrast: Int
private val mediumContrast: Int private val mediumContrast: Int
private val paint = TextPaint().apply {
typeface = getFontAwesome()
isAntiAlias = true
textAlign = Paint.Align.CENTER
}
private val pUnit: TextPaint = TextPaint().apply { private val pUnit: TextPaint = TextPaint().apply {
textSize = getDimension(context, R.dimen.smallerTextSize) textSize = getDimension(context, R.dimen.smallerTextSize)
typeface = NORMAL_TYPEFACE typeface = NORMAL_TYPEFACE
@@ -167,16 +148,15 @@ class NumberButtonView(
init { init {
em = pNumber.measureText("m") em = pNumber.measureText("m")
lowContrast = sres.getColor(R.attr.contrast40) lowContrast = sr.getColor(R.attr.contrast40)
mediumContrast = sres.getColor(R.attr.contrast60) mediumContrast = sr.getColor(R.attr.contrast60)
} }
fun draw(canvas: Canvas) { fun draw(canvas: Canvas) {
val activeColor = when { val activeColor = when {
value < 0.0 -> lowContrast value <= 0.0 -> lowContrast
(targetType == AT_LEAST) && (value >= threshold) -> color value < threshold -> mediumContrast
(targetType == AT_MOST) && (value <= threshold) -> color else -> color
else -> mediumContrast
} }
val label: String val label: String
@@ -184,11 +164,6 @@ class NumberButtonView(
val textSize: Float val textSize: Float
when { when {
value == Entry.SKIP.toDouble() / 1000 -> {
label = resources.getString(R.string.fa_skipped)
textSize = dim(R.dimen.smallTextSize)
typeface = getFontAwesome()
}
value >= 0 -> { value >= 0 -> {
label = value.toShortString() label = value.toShortString()
typeface = BOLD_TYPEFACE typeface = BOLD_TYPEFACE
@@ -221,8 +196,6 @@ class NumberButtonView(
rect.offset(0f, 1.3f * em) rect.offset(0f, 1.3f * em)
canvas.drawText(units, rect.centerX(), rect.centerY(), pUnit) canvas.drawText(units, rect.centerX(), rect.centerY(), pUnit)
} }
drawNotesIndicator(canvas, color, em, notes)
} }
} }
} }

View File

@@ -20,7 +20,6 @@
package org.isoron.uhabits.activities.habits.list.views package org.isoron.uhabits.activities.habits.list.views
import android.content.Context import android.content.Context
import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.core.utils.DateUtils
@@ -48,12 +47,6 @@ class NumberPanelView(
setupButtons() setupButtons()
} }
var targetType = NumericalHabitType.AT_LEAST
set(value) {
field = value
setupButtons()
}
var threshold = 0.0 var threshold = 0.0
set(value) { set(value) {
field = value field = value
@@ -72,13 +65,7 @@ class NumberPanelView(
setupButtons() setupButtons()
} }
var notes = arrayOf<String>() var onEdit: (Timestamp) -> Unit = {}
set(values) {
field = values
setupButtons()
}
var onEdit: (Timestamp) -> Unit = { _ -> }
set(value) { set(value) {
field = value field = value
setupButtons() setupButtons()
@@ -96,12 +83,7 @@ class NumberPanelView(
index + dataOffset < values.size -> values[index + dataOffset] index + dataOffset < values.size -> values[index + dataOffset]
else -> 0.0 else -> 0.0
} }
button.notes = when {
index + dataOffset < notes.size -> notes[index + dataOffset]
else -> ""
}
button.color = color button.color = color
button.targetType = targetType
button.threshold = threshold button.threshold = threshold
button.units = units button.units = units
button.onEdit = { onEdit(timestamp) } button.onEdit = { onEdit(timestamp) }

View File

@@ -23,25 +23,21 @@ import android.os.Bundle
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.isoron.platform.gui.toInt
import org.isoron.uhabits.AndroidDirFinder import org.isoron.uhabits.AndroidDirFinder
import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.AndroidThemeSwitcher import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.activities.HabitsDirFinder import org.isoron.uhabits.activities.HabitsDirFinder
import org.isoron.uhabits.activities.common.dialogs.CheckmarkPopup
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog
import org.isoron.uhabits.activities.common.dialogs.NumberPopup import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
import org.isoron.uhabits.core.commands.Command import org.isoron.uhabits.core.commands.Command
import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.models.Habit 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.preferences.Preferences
import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
@@ -49,7 +45,6 @@ import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitMenuPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitPresenter import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitPresenter
import org.isoron.uhabits.core.ui.views.OnDateClickedListener import org.isoron.uhabits.core.ui.views.OnDateClickedListener
import org.isoron.uhabits.intents.IntentFactory import org.isoron.uhabits.intents.IntentFactory
import org.isoron.uhabits.utils.currentTheme
import org.isoron.uhabits.utils.showMessage import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.showSendFileScreen import org.isoron.uhabits.utils.showSendFileScreen
import org.isoron.uhabits.widgets.WidgetUpdater import org.isoron.uhabits.widgets.WidgetUpdater
@@ -166,49 +161,12 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
window.decorView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY) window.decorView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
} }
override fun showNumberPopup( override fun showNumberPicker(
value: Double, value: Double,
notes: String, unit: String,
preferences: Preferences, callback: ListHabitsBehavior.NumberPickerCallback,
callback: ListHabitsBehavior.NumberPickerCallback
) { ) {
val anchor = getPopupAnchor() ?: return NumberPickerFactory(this@ShowHabitActivity).create(value, unit, callback).show()
NumberPopup(
context = this@ShowHabitActivity,
prefs = preferences,
notes = notes,
anchor = anchor,
value = value,
).apply {
onToggle = { v, n -> callback.onNumberPicked(v, n) }
show()
}
}
override fun showCheckmarkPopup(
selectedValue: Int,
notes: String,
preferences: Preferences,
color: PaletteColor,
callback: ListHabitsBehavior.CheckMarkDialogCallback
) {
val anchor = getPopupAnchor() ?: return
CheckmarkPopup(
context = this@ShowHabitActivity,
prefs = preferences,
notes = notes,
color = view.currentTheme().color(color).toInt(),
anchor = anchor,
value = selectedValue,
).apply {
onToggle = { v, n -> callback.onNotesSaved(v, n) }
show()
}
}
private fun getPopupAnchor(): View? {
val dialog = supportFragmentManager.findFragmentByTag("historyEditor") as HistoryEditorDialog?
return dialog?.dataView
} }
override fun showEditHabitScreen(habit: Habit) { override fun showEditHabitScreen(habit: Habit) {

View File

@@ -43,8 +43,6 @@ class HistoryCardView(context: Context, attrs: AttributeSet) : LinearLayout(cont
theme = state.theme, theme = state.theme,
dateFormatter = JavaLocalDateFormatter(Locale.getDefault()), dateFormatter = JavaLocalDateFormatter(Locale.getDefault()),
series = state.series, series = state.series,
defaultSquare = state.defaultSquare,
notesIndicators = state.notesIndicators,
firstWeekday = state.firstWeekday, firstWeekday = state.firstWeekday,
) )
binding.chart.postInvalidate() binding.chart.postInvalidate()

View File

@@ -28,7 +28,6 @@ import org.isoron.platform.gui.toInt
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.habits.edit.formatFrequency import org.isoron.uhabits.activities.habits.edit.formatFrequency
import org.isoron.uhabits.activities.habits.list.views.toShortString import org.isoron.uhabits.activities.habits.list.views.toShortString
import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardState import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardState
import org.isoron.uhabits.databinding.ShowHabitSubtitleBinding import org.isoron.uhabits.databinding.ShowHabitSubtitleBinding
import org.isoron.uhabits.utils.InterfaceUtils import org.isoron.uhabits.utils.InterfaceUtils
@@ -66,12 +65,7 @@ class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
binding.questionLabel.visibility = View.VISIBLE binding.questionLabel.visibility = View.VISIBLE
binding.targetIcon.visibility = View.VISIBLE binding.targetIcon.visibility = View.VISIBLE
binding.targetText.visibility = View.VISIBLE binding.targetText.visibility = View.VISIBLE
if (state.isNumerical) { if (!state.isNumerical) {
binding.targetIcon.text = when (state.targetType) {
NumericalHabitType.AT_LEAST -> resources.getString(R.string.fa_arrow_circle_up)
else -> resources.getString(R.string.fa_arrow_circle_down)
}
} else {
binding.targetIcon.visibility = View.GONE binding.targetIcon.visibility = View.GONE
binding.targetText.visibility = View.GONE binding.targetText.visibility = View.GONE
} }

View File

@@ -21,9 +21,8 @@ package org.isoron.uhabits.activities.intro
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment import com.github.paolorotolo.appintro.AppIntro2
import com.github.appintro.AppIntro2 import com.github.paolorotolo.appintro.AppIntroFragment
import com.github.appintro.AppIntroFragment
import org.isoron.uhabits.R import org.isoron.uhabits.R
/** /**
@@ -31,9 +30,7 @@ import org.isoron.uhabits.R
* launched for the first time. * launched for the first time.
*/ */
class IntroActivity : AppIntro2() { class IntroActivity : AppIntro2() {
override fun init(savedInstanceState: Bundle?) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
showStatusBar(false) showStatusBar(false)
addSlide( addSlide(
@@ -64,13 +61,11 @@ class IntroActivity : AppIntro2() {
) )
} }
override fun onDonePressed(currentFragment: Fragment?) { override fun onNextPressed() {}
super.onDonePressed(currentFragment)
override fun onDonePressed() {
finish() finish()
} }
override fun onSkipPressed(currentFragment: Fragment?) { override fun onSlideChanged() {}
super.onSkipPressed(currentFragment)
finish()
}
} }

View File

@@ -19,15 +19,16 @@
package org.isoron.uhabits.activities.settings package org.isoron.uhabits.activities.settings
import android.app.backup.BackupManager import android.app.backup.BackupManager
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Build import android.os.Build
import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import android.view.View import androidx.preference.CheckBoxPreference
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceCategory import androidx.preference.PreferenceCategory
@@ -42,9 +43,9 @@ import org.isoron.uhabits.activities.habits.list.RESULT_REPAIR_DB
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.NotificationTray import org.isoron.uhabits.core.ui.NotificationTray
import org.isoron.uhabits.core.utils.DateUtils.Companion.getLongWeekdayNames import org.isoron.uhabits.core.utils.DateUtils.Companion.getLongWeekdayNames
import org.isoron.uhabits.intents.IntentFactory
import org.isoron.uhabits.notifications.AndroidNotificationTray.Companion.createAndroidNotificationChannel import org.isoron.uhabits.notifications.AndroidNotificationTray.Companion.createAndroidNotificationChannel
import org.isoron.uhabits.notifications.RingtoneManager import org.isoron.uhabits.notifications.RingtoneManager
import org.isoron.uhabits.utils.StyledResources
import org.isoron.uhabits.widgets.WidgetUpdater import org.isoron.uhabits.widgets.WidgetUpdater
import java.util.Calendar import java.util.Calendar
@@ -86,25 +87,26 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
super.onPause() 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 { override fun onPreferenceTreeClick(preference: Preference): Boolean {
val key = preference.key ?: return false val key = preference.key ?: return false
if (key == "reminderSound") { if (key == "reminderSound") {
showRingtonePicker() showRingtonePicker()
return true return true
} else if (key == "reminderCustomize") { } else if (key == "reminderCustomize") {
if (SDK_INT < Build.VERSION_CODES.O) return true if (VERSION.SDK_INT < Build.VERSION_CODES.O) return true
createAndroidNotificationChannel(context!!) createAndroidNotificationChannel(context!!)
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
intent.putExtra(Settings.EXTRA_APP_PACKAGE, context!!.packageName) intent.putExtra(Settings.EXTRA_APP_PACKAGE, context!!.packageName)
intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationTray.REMINDERS_CHANNEL_ID) intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationTray.REMINDERS_CHANNEL_ID)
startActivity(intent) startActivity(intent)
return true return true
} else if (key == "pref_sync_enabled_dummy") {
if (prefs.isSyncEnabled) {
prefs.disableSync()
} else {
val context: Context? = activity
context!!.startActivity(IntentFactory().startSyncActivity(context))
}
} }
return super.onPreferenceTreeClick(preference) return super.onPreferenceTreeClick(preference)
} }
@@ -119,14 +121,21 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
devCategory.isVisible = false devCategory.isVisible = false
} }
updateWeekdayPreference() updateWeekdayPreference()
updateSyncPreferences()
if (SDK_INT < Build.VERSION_CODES.O) if (VERSION.SDK_INT < Build.VERSION_CODES.O)
findPreference("reminderCustomize").isVisible = false findPreference("reminderCustomize").isVisible = false
else { else {
findPreference("reminderSound").isVisible = false findPreference("reminderSound").isVisible = false
} }
} }
private fun updateSyncPreferences() {
findPreference("pref_sync_display").isVisible = prefs.isSyncEnabled
(findPreference("pref_sync_enabled_dummy") as CheckBoxPreference).isChecked =
prefs.isSyncEnabled
}
private fun updateWeekdayPreference() { private fun updateWeekdayPreference() {
val weekdayPref = findPreference("pref_first_weekday") as ListPreference val weekdayPref = findPreference("pref_first_weekday") as ListPreference
val currentFirstWeekday = prefs.firstWeekday.daysSinceSunday + 1 val currentFirstWeekday = prefs.firstWeekday.daysSinceSunday + 1
@@ -148,6 +157,7 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
} }
BackupManager.dataChanged("org.isoron.uhabits") BackupManager.dataChanged("org.isoron.uhabits")
updateWeekdayPreference() updateWeekdayPreference()
updateSyncPreferences()
} }
private fun setResultOnPreferenceClick(key: String, result: Int) { private fun setResultOnPreferenceClick(key: String, result: Int) {

View File

@@ -0,0 +1,132 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.sync
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.os.Bundle
import android.text.Html
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.google.zxing.BarcodeFormat
import com.google.zxing.qrcode.QRCodeWriter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.invoke
import kotlinx.coroutines.launch
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.ui.screens.sync.SyncBehavior
import org.isoron.uhabits.databinding.ActivitySyncBinding
import org.isoron.uhabits.sync.RemoteSyncServer
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
import org.isoron.uhabits.utils.setupToolbar
import org.isoron.uhabits.utils.showMessage
class SyncActivity : AppCompatActivity(), SyncBehavior.Screen {
private lateinit var binding: ActivitySyncBinding
private lateinit var behavior: SyncBehavior
private val scope = CoroutineScope(Dispatchers.Main)
override fun onCreate(savedInstance: Bundle?) {
super.onCreate(savedInstance)
val component = (application as HabitsApplication).component
val preferences = component.preferences
val server = RemoteSyncServer(preferences = preferences)
val themeSwitcher = AndroidThemeSwitcher(this, component.preferences)
themeSwitcher.apply()
behavior = SyncBehavior(this, preferences, server, component.logging)
binding = ActivitySyncBinding.inflate(layoutInflater)
binding.errorIcon.typeface = getFontAwesome(this)
binding.root.setupToolbar(
toolbar = binding.toolbar,
color = PaletteColor(11),
title = resources.getString(R.string.device_sync),
theme = themeSwitcher.currentTheme,
)
binding.syncLink.setOnClickListener { copyToClipboard() }
binding.instructions.text = Html.fromHtml(resources.getString(R.string.sync_instructions))
setContentView(binding.root)
}
override fun onResume() {
super.onResume()
scope.launch {
behavior.onResume()
}
}
private fun copyToClipboard() {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("Loop Sync Link", binding.syncLink.text))
showMessage(resources.getString(R.string.copied_to_the_clipboard))
}
suspend fun generateQR(msg: String): Bitmap = Dispatchers.IO {
val writer = QRCodeWriter()
val matrix = writer.encode(msg, BarcodeFormat.QR_CODE, 1024, 1024)
val height = matrix.height
val width = matrix.width
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
val bgColor = Color.WHITE
val fgColor = Color.BLACK
for (x in 0 until width) {
for (y in 0 until height) {
val color = if (matrix.get(x, y)) fgColor else bgColor
bitmap.setPixel(x, y, color)
}
}
return@IO bitmap
}
suspend fun showQR(msg: String) {
binding.progress.visibility = View.GONE
binding.qrCode.visibility = View.VISIBLE
binding.qrCode.setImageBitmap(generateQR(msg))
}
override suspend fun showLoadingScreen() {
binding.qrCode.visibility = View.GONE
binding.progress.visibility = View.VISIBLE
binding.errorPanel.visibility = View.GONE
}
override suspend fun showErrorScreen() {
binding.qrCode.visibility = View.GONE
binding.progress.visibility = View.GONE
binding.errorPanel.visibility = View.VISIBLE
}
override suspend fun showLink(link: String) {
binding.qrCode.visibility = View.GONE
binding.progress.visibility = View.VISIBLE
binding.errorPanel.visibility = View.GONE
binding.syncLink.text = link
showQR(link)
}
}

View File

@@ -23,17 +23,17 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.activities.AndroidThemeSwitcher import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.core.models.HabitMatcher import org.isoron.uhabits.core.models.HabitMatcherBuilder
class EditSettingActivity : AppCompatActivity() { class EditSettingActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val app = applicationContext as HabitsApplication val app = applicationContext as HabitsApplication
val habits = app.component.habitList.getFiltered( val habits = app.component.habitList.getFiltered(
HabitMatcher( HabitMatcherBuilder()
isArchivedAllowed = false, .setArchivedAllowed(false)
isCompletedAllowed = true, .setCompletedAllowed(true)
) .build()
) )
AndroidThemeSwitcher(this, app.component.preferences).apply() AndroidThemeSwitcher(this, app.component.preferences).apply()

View File

@@ -29,6 +29,7 @@ import org.isoron.uhabits.core.models.ModelFactory
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.preferences.WidgetPreferences import org.isoron.uhabits.core.preferences.WidgetPreferences
import org.isoron.uhabits.core.reminders.ReminderScheduler import org.isoron.uhabits.core.reminders.ReminderScheduler
import org.isoron.uhabits.core.sync.SyncManager
import org.isoron.uhabits.core.tasks.TaskRunner import org.isoron.uhabits.core.tasks.TaskRunner
import org.isoron.uhabits.core.ui.NotificationTray import org.isoron.uhabits.core.ui.NotificationTray
import org.isoron.uhabits.core.ui.screens.habits.list.HabitCardListCache import org.isoron.uhabits.core.ui.screens.habits.list.HabitCardListCache
@@ -63,4 +64,5 @@ interface HabitsApplicationComponent {
val taskRunner: TaskRunner val taskRunner: TaskRunner
val widgetPreferences: WidgetPreferences val widgetPreferences: WidgetPreferences
val widgetUpdater: WidgetUpdater val widgetUpdater: WidgetUpdater
val syncManager: SyncManager
} }

View File

@@ -19,6 +19,7 @@
package org.isoron.uhabits.inject package org.isoron.uhabits.inject
import android.content.Context
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import org.isoron.uhabits.core.AppScope import org.isoron.uhabits.core.AppScope
@@ -33,6 +34,8 @@ import org.isoron.uhabits.core.models.sqlite.SQLiteHabitList
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.preferences.WidgetPreferences import org.isoron.uhabits.core.preferences.WidgetPreferences
import org.isoron.uhabits.core.reminders.ReminderScheduler import org.isoron.uhabits.core.reminders.ReminderScheduler
import org.isoron.uhabits.core.sync.AbstractSyncServer
import org.isoron.uhabits.core.sync.NetworkManager
import org.isoron.uhabits.core.tasks.TaskRunner import org.isoron.uhabits.core.tasks.TaskRunner
import org.isoron.uhabits.core.ui.NotificationTray import org.isoron.uhabits.core.ui.NotificationTray
import org.isoron.uhabits.database.AndroidDatabase import org.isoron.uhabits.database.AndroidDatabase
@@ -41,6 +44,8 @@ import org.isoron.uhabits.intents.IntentScheduler
import org.isoron.uhabits.io.AndroidLogging import org.isoron.uhabits.io.AndroidLogging
import org.isoron.uhabits.notifications.AndroidNotificationTray import org.isoron.uhabits.notifications.AndroidNotificationTray
import org.isoron.uhabits.preferences.SharedPreferencesStorage import org.isoron.uhabits.preferences.SharedPreferencesStorage
import org.isoron.uhabits.sync.AndroidNetworkManager
import org.isoron.uhabits.sync.RemoteSyncServer
import org.isoron.uhabits.utils.DatabaseUtils import org.isoron.uhabits.utils.DatabaseUtils
import java.io.File import java.io.File
@@ -109,6 +114,18 @@ class HabitsModule(dbFile: File) {
return AndroidLogging() return AndroidLogging()
} }
@Provides
@AppScope
fun getNetworkManager(@AppContext context: Context): NetworkManager {
return AndroidNetworkManager(context)
}
@Provides
@AppScope
fun getSyncServer(preferences: Preferences): AbstractSyncServer {
return RemoteSyncServer(preferences)
}
@Provides @Provides
@AppScope @AppScope
fun getDatabase(): Database { fun getDatabase(): Database {

View File

@@ -28,6 +28,7 @@ import org.isoron.uhabits.activities.habits.edit.EditHabitActivity
import org.isoron.uhabits.activities.habits.show.ShowHabitActivity import org.isoron.uhabits.activities.habits.show.ShowHabitActivity
import org.isoron.uhabits.activities.intro.IntroActivity import org.isoron.uhabits.activities.intro.IntroActivity
import org.isoron.uhabits.activities.settings.SettingsActivity import org.isoron.uhabits.activities.settings.SettingsActivity
import org.isoron.uhabits.activities.sync.SyncActivity
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import javax.inject.Inject import javax.inject.Inject
@@ -100,4 +101,8 @@ class IntentFactory
intent.putExtra("habitType", habitType) intent.putExtra("habitType", habitType)
return intent return intent
} }
fun startSyncActivity(context: Context): Intent {
return Intent(context, SyncActivity::class.java)
}
} }

View File

@@ -25,6 +25,8 @@ import android.app.AlarmManager.RTC_WAKEUP
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Context.ALARM_SERVICE import android.content.Context.ALARM_SERVICE
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.M
import android.util.Log import android.util.Log
import org.isoron.uhabits.core.AppScope import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
@@ -56,7 +58,10 @@ class IntentScheduler
) )
return SchedulerResult.IGNORED return SchedulerResult.IGNORED
} }
manager.setExactAndAllowWhileIdle(alarmType, timestamp, intent) if (SDK_INT >= M)
manager.setExactAndAllowWhileIdle(alarmType, timestamp, intent)
else
manager.setExact(alarmType, timestamp, intent)
return SchedulerResult.OK return SchedulerResult.OK
} }

View File

@@ -20,14 +20,11 @@
package org.isoron.uhabits.intents package org.isoron.uhabits.intents
import android.app.PendingIntent import android.app.PendingIntent
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.PendingIntent.getActivity
import android.app.PendingIntent.getBroadcast import android.app.PendingIntent.getBroadcast
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import org.isoron.uhabits.activities.habits.list.ListHabitsActivity
import org.isoron.uhabits.core.AppScope import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.Timestamp
@@ -52,7 +49,7 @@ class PendingIntentFactory
action = WidgetReceiver.ACTION_ADD_REPETITION action = WidgetReceiver.ACTION_ADD_REPETITION
if (timestamp != null) putExtra("timestamp", timestamp.unixTime) if (timestamp != null) putExtra("timestamp", timestamp.unixTime)
}, },
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT FLAG_UPDATE_CURRENT
) )
fun dismissNotification(habit: Habit): PendingIntent = fun dismissNotification(habit: Habit): PendingIntent =
@@ -63,7 +60,7 @@ class PendingIntentFactory
action = WidgetReceiver.ACTION_DISMISS_REMINDER action = WidgetReceiver.ACTION_DISMISS_REMINDER
data = Uri.parse(habit.uriString) data = Uri.parse(habit.uriString)
}, },
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT FLAG_UPDATE_CURRENT
) )
fun removeRepetition(habit: Habit, timestamp: Timestamp?): PendingIntent = fun removeRepetition(habit: Habit, timestamp: Timestamp?): PendingIntent =
@@ -75,7 +72,7 @@ class PendingIntentFactory
data = Uri.parse(habit.uriString) data = Uri.parse(habit.uriString)
if (timestamp != null) putExtra("timestamp", timestamp.unixTime) if (timestamp != null) putExtra("timestamp", timestamp.unixTime)
}, },
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT FLAG_UPDATE_CURRENT
) )
fun showHabit(habit: Habit): PendingIntent = fun showHabit(habit: Habit): PendingIntent =
@@ -87,7 +84,7 @@ class PendingIntentFactory
habit habit
) )
) )
.getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)!! .getPendingIntent(0, FLAG_UPDATE_CURRENT)!!
fun showReminder( fun showReminder(
habit: Habit, habit: Habit,
@@ -103,7 +100,7 @@ class PendingIntentFactory
putExtra("timestamp", timestamp) putExtra("timestamp", timestamp)
putExtra("reminderTime", reminderTime) putExtra("reminderTime", reminderTime)
}, },
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT FLAG_UPDATE_CURRENT
) )
fun snoozeNotification(habit: Habit): PendingIntent = fun snoozeNotification(habit: Habit): PendingIntent =
@@ -114,7 +111,7 @@ class PendingIntentFactory
data = Uri.parse(habit.uriString) data = Uri.parse(habit.uriString)
action = ReminderReceiver.ACTION_SNOOZE_REMINDER action = ReminderReceiver.ACTION_SNOOZE_REMINDER
}, },
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT FLAG_UPDATE_CURRENT
) )
fun toggleCheckmark(habit: Habit, timestamp: Long?): PendingIntent = fun toggleCheckmark(habit: Habit, timestamp: Long?): PendingIntent =
@@ -126,7 +123,26 @@ class PendingIntentFactory
action = WidgetReceiver.ACTION_TOGGLE_REPETITION action = WidgetReceiver.ACTION_TOGGLE_REPETITION
if (timestamp != null) putExtra("timestamp", timestamp) if (timestamp != null) putExtra("timestamp", timestamp)
}, },
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT FLAG_UPDATE_CURRENT
)
fun setNumericalValue(
widgetContext: Context,
habit: Habit,
numericalValue: Int,
timestamp: Long?
):
PendingIntent =
getBroadcast(
widgetContext,
2,
Intent(widgetContext, WidgetReceiver::class.java).apply {
data = Uri.parse(habit.uriString)
action = WidgetReceiver.ACTION_SET_NUMERICAL_VALUE
putExtra("numericalValue", numericalValue)
if (timestamp != null) putExtra("timestamp", timestamp)
},
FLAG_UPDATE_CURRENT
) )
fun updateWidgets(): PendingIntent = fun updateWidgets(): PendingIntent =
@@ -136,19 +152,6 @@ class PendingIntentFactory
Intent(context, WidgetReceiver::class.java).apply { Intent(context, WidgetReceiver::class.java).apply {
action = WidgetReceiver.ACTION_UPDATE_WIDGETS_VALUE action = WidgetReceiver.ACTION_UPDATE_WIDGETS_VALUE
}, },
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT FLAG_UPDATE_CURRENT
) )
fun showNumberPicker(habit: Habit, timestamp: Timestamp): PendingIntent? {
return getActivity(
context,
0,
Intent(context, ListHabitsActivity::class.java).apply {
action = ListHabitsActivity.ACTION_EDIT
putExtra("habit", habit.id)
putExtra("timestamp", timestamp.unixTime)
},
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
)
}
} }

View File

@@ -25,6 +25,7 @@ import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.graphics.BitmapFactory.decodeResource import android.graphics.BitmapFactory.decodeResource
import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION.SDK_INT
import android.util.Log import android.util.Log
@@ -112,7 +113,7 @@ class AndroidNotificationTray
val enterAction = Action( val enterAction = Action(
R.drawable.ic_action_check, R.drawable.ic_action_check,
context.getString(R.string.enter), context.getString(R.string.enter),
pendingIntents.showNumberPicker(habit, timestamp) pendingIntents.setNumericalValue(context, habit, 0, null)
) )
val wearableBg = decodeResource(context.resources, R.drawable.stripe) val wearableBg = decodeResource(context.resources, R.drawable.stripe)
@@ -149,15 +150,16 @@ class AndroidNotificationTray
if (!disableSound) if (!disableSound)
builder.setSound(ringtoneManager.getURI()) builder.setSound(ringtoneManager.getURI())
if (SDK_INT < Build.VERSION_CODES.S) { if (preferences.shouldMakeNotificationsLed())
val snoozeAction = Action( builder.setLights(Color.RED, 1000, 1000)
R.drawable.ic_action_snooze,
context.getString(R.string.snooze), val snoozeAction = Action(
pendingIntents.snoozeNotification(habit) R.drawable.ic_action_snooze,
) context.getString(R.string.snooze),
wearableExtender.addAction(snoozeAction) pendingIntents.snoozeNotification(habit)
builder.addAction(snoozeAction) )
} wearableExtender.addAction(snoozeAction)
builder.addAction(snoozeAction)
builder.extend(wearableExtender) builder.extend(wearableExtender)
return builder.build() return builder.build()

View File

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

View File

@@ -22,8 +22,6 @@ import android.content.BroadcastReceiver
import android.content.ContentUris import android.content.ContentUris
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.util.Log import android.util.Log
import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
@@ -78,21 +76,8 @@ class ReminderReceiver : BroadcastReceiver() {
} }
ACTION_SNOOZE_REMINDER -> { ACTION_SNOOZE_REMINDER -> {
if (habit == null) return if (habit == null) return
if (SDK_INT < Build.VERSION_CODES.S) { Log.d("ReminderReceiver", String.format("onSnoozePressed habit=%d", habit.id))
Log.d( reminderController.onSnoozePressed(habit, context)
"ReminderReceiver",
String.format("onSnoozePressed habit=%d", habit.id)
)
reminderController.onSnoozePressed(habit, context)
} else {
Log.w(
"ReminderReceiver",
String.format(
"onSnoozePressed habit=%d, should be deactivated in recent versions.",
habit.id
)
)
}
} }
Intent.ACTION_BOOT_COMPLETED -> { Intent.ACTION_BOOT_COMPLETED -> {
Log.d("ReminderReceiver", "onBootCompleted") Log.d("ReminderReceiver", "onBootCompleted")

View File

@@ -27,6 +27,7 @@ import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.core.ui.widgets.WidgetBehavior import org.isoron.uhabits.core.ui.widgets.WidgetBehavior
import org.isoron.uhabits.inject.HabitsApplicationComponent import org.isoron.uhabits.inject.HabitsApplicationComponent
import org.isoron.uhabits.intents.IntentParser.CheckmarkIntentData import org.isoron.uhabits.intents.IntentParser.CheckmarkIntentData
import org.isoron.uhabits.widgets.activities.NumericalCheckmarkWidgetActivity
/** /**
* The Android BroadcastReceiver for Loop Habit Tracker. * The Android BroadcastReceiver for Loop Habit Tracker.
@@ -95,6 +96,15 @@ class WidgetReceiver : BroadcastReceiver() {
data.timestamp 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 -> { ACTION_UPDATE_WIDGETS_VALUE -> {
widgetUpdater.updateWidgets() widgetUpdater.updateWidgets()
widgetUpdater.scheduleStartDayWidgetUpdate() widgetUpdater.scheduleStartDayWidgetUpdate()
@@ -116,6 +126,7 @@ class WidgetReceiver : BroadcastReceiver() {
const val ACTION_DISMISS_REMINDER = "org.isoron.uhabits.ACTION_DISMISS_REMINDER" 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_REMOVE_REPETITION = "org.isoron.uhabits.ACTION_REMOVE_REPETITION"
const val ACTION_TOGGLE_REPETITION = "org.isoron.uhabits.ACTION_TOGGLE_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" const val ACTION_UPDATE_WIDGETS_VALUE = "org.isoron.uhabits.ACTION_UPDATE_WIDGETS_VALUE"
private const val TAG = "WidgetReceiver" private const val TAG = "WidgetReceiver"
var lastReceivedIntent: Intent? = null var lastReceivedIntent: Intent? = null

View File

@@ -0,0 +1,59 @@
/*
* 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.sync
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkRequest
import org.isoron.uhabits.core.sync.NetworkManager
class AndroidNetworkManager(
val context: Context,
) : NetworkManager, ConnectivityManager.NetworkCallback() {
val listeners = mutableListOf<NetworkManager.Listener>()
var connected = false
init {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
cm.registerNetworkCallback(NetworkRequest.Builder().build(), this)
}
override fun addListener(listener: NetworkManager.Listener) {
if (connected) listener.onNetworkAvailable()
else listener.onNetworkLost()
listeners.add(listener)
}
override fun remoteListener(listener: NetworkManager.Listener) {
listeners.remove(listener)
}
override fun onAvailable(network: Network) {
connected = true
for (l in listeners) l.onNetworkAvailable()
}
override fun onLost(network: Network) {
connected = false
for (l in listeners) l.onNetworkLost()
}
}

View File

@@ -0,0 +1,105 @@
/*
* 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.sync
import android.util.Log
import io.ktor.client.HttpClient
import io.ktor.client.engine.android.Android
import io.ktor.client.features.ClientRequestException
import io.ktor.client.features.ServerResponseException
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.post
import io.ktor.client.request.put
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.invoke
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.sync.AbstractSyncServer
import org.isoron.uhabits.core.sync.EditConflictException
import org.isoron.uhabits.core.sync.GetDataVersionResponse
import org.isoron.uhabits.core.sync.KeyNotFoundException
import org.isoron.uhabits.core.sync.RegisterReponse
import org.isoron.uhabits.core.sync.ServiceUnavailable
import org.isoron.uhabits.core.sync.SyncData
class RemoteSyncServer(
private val preferences: Preferences,
private val httpClient: HttpClient = HttpClient(Android) {
install(JsonFeature)
}
) : AbstractSyncServer {
override suspend fun register(): String = Dispatchers.IO {
try {
val url = "${preferences.syncBaseURL}/register"
Log.i("RemoteSyncServer", "POST $url")
val response: RegisterReponse = httpClient.post(url)
return@IO response.key
} catch (e: ServerResponseException) {
throw ServiceUnavailable()
}
}
override suspend fun put(key: String, newData: SyncData) = Dispatchers.IO {
try {
val url = "${preferences.syncBaseURL}/db/$key"
Log.i("RemoteSyncServer", "PUT $url")
val response: String = httpClient.put(url) {
header("Content-Type", "application/json")
body = newData
}
} catch (e: ServerResponseException) {
throw ServiceUnavailable()
} catch (e: ClientRequestException) {
Log.w("RemoteSyncServer", "ClientRequestException", e)
if (e.message!!.contains("409")) throw EditConflictException()
if (e.message!!.contains("404")) throw KeyNotFoundException()
throw e
}
}
override suspend fun getData(key: String): SyncData = Dispatchers.IO {
try {
val url = "${preferences.syncBaseURL}/db/$key"
Log.i("RemoteSyncServer", "GET $url")
return@IO httpClient.get<SyncData>(url)
} catch (e: ServerResponseException) {
throw ServiceUnavailable()
} catch (e: ClientRequestException) {
Log.w("RemoteSyncServer", "ClientRequestException", e)
throw KeyNotFoundException()
}
}
override suspend fun getDataVersion(key: String): Long = Dispatchers.IO {
try {
val url = "${preferences.syncBaseURL}/db/$key/version"
Log.i("RemoteSyncServer", "GET $url")
val response: GetDataVersionResponse = httpClient.get(url)
return@IO response.version
} catch (e: ServerResponseException) {
throw ServiceUnavailable()
} catch (e: ClientRequestException) {
Log.w("RemoteSyncServer", "ClientRequestException", e)
throw KeyNotFoundException()
}
}
}

View File

@@ -22,13 +22,14 @@ import android.app.Activity
import android.app.KeyguardManager import android.app.KeyguardManager
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.view.WindowManager import android.view.WindowManager
object SystemUtils { object SystemUtils {
val isAndroidOOrLater: Boolean
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
fun unlockScreen(activity: Activity) { fun unlockScreen(activity: Activity) {
if (SDK_INT >= Build.VERSION_CODES.O) { if (isAndroidOOrLater) {
val km = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager val km = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
km.requestDismissKeyguard(activity, null) km.requestDismissKeyguard(activity, null)
} else { } else {

View File

@@ -20,22 +20,16 @@
package org.isoron.uhabits.utils package org.isoron.uhabits.utils
import android.app.Activity import android.app.Activity
import android.app.Dialog
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.graphics.Paint
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Handler import android.os.Handler
import android.os.SystemClock
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.WindowManager
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.RelativeLayout.ALIGN_PARENT_BOTTOM import android.widget.RelativeLayout.ALIGN_PARENT_BOTTOM
import android.widget.RelativeLayout.ALIGN_PARENT_TOP import android.widget.RelativeLayout.ALIGN_PARENT_TOP
@@ -205,33 +199,5 @@ fun View.dim(id: Int) = InterfaceUtils.getDimension(context, id)
fun View.sp(value: Float) = InterfaceUtils.spToPixels(context, value) fun View.sp(value: Float) = InterfaceUtils.spToPixels(context, value)
fun View.dp(value: Float) = InterfaceUtils.dpToPixels(context, value) fun View.dp(value: Float) = InterfaceUtils.dpToPixels(context, value)
fun View.str(id: Int) = resources.getString(id) fun View.str(id: Int) = resources.getString(id)
fun View.drawNotesIndicator(canvas: Canvas, color: Int, size: Float, notes: String) {
val pNotesIndicator = Paint()
pNotesIndicator.color = color
if (notes.isNotBlank()) {
val cy = 0.8f * size
canvas.drawCircle(width.toFloat() - cy, cy, 8f, pNotesIndicator)
}
}
val View.sres: StyledResources val View.sres: StyledResources
get() = StyledResources(context) 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)
}

View File

@@ -43,7 +43,7 @@ open class CheckmarkWidget(
override fun getOnClickPendingIntent(context: Context): PendingIntent? { override fun getOnClickPendingIntent(context: Context): PendingIntent? {
return if (habit.isNumerical) { return if (habit.isNumerical) {
pendingIntentFactory.showNumberPicker(habit, DateUtils.getToday()) pendingIntentFactory.setNumericalValue(context, habit, 10, null)
} else { } else {
pendingIntentFactory.toggleCheckmark(habit, null) pendingIntentFactory.toggleCheckmark(habit, null)
} }

View File

@@ -56,10 +56,7 @@ class HistoryWidget(
theme = WidgetTheme(), theme = WidgetTheme(),
) )
(widgetView.dataView as AndroidDataView).apply { (widgetView.dataView as AndroidDataView).apply {
val historyChart = (this.view as HistoryChart) (this.view as HistoryChart).series = model.series
historyChart.series = model.series
historyChart.defaultSquare = model.defaultSquare
historyChart.notesIndicators = model.notesIndicators
} }
} }
@@ -74,8 +71,6 @@ class HistoryWidget(
dateFormatter = JavaLocalDateFormatter(Locale.getDefault()), dateFormatter = JavaLocalDateFormatter(Locale.getDefault()),
firstWeekday = prefs.firstWeekday, firstWeekday = prefs.firstWeekday,
series = listOf(), series = listOf(),
defaultSquare = HistoryChart.Square.OFF,
notesIndicators = listOf(),
) )
} }
).apply { ).apply {

View File

@@ -26,8 +26,8 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.view.View import android.view.View
import android.widget.RemoteViews import android.widget.RemoteViews
import org.isoron.platform.utils.StringUtils
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.utils.StringUtils
class StackWidget( class StackWidget(
context: Context, context: Context,

View File

@@ -27,11 +27,11 @@ import android.util.Log
import android.widget.RemoteViews import android.widget.RemoteViews
import android.widget.RemoteViewsService import android.widget.RemoteViewsService
import android.widget.RemoteViewsService.RemoteViewsFactory import android.widget.RemoteViewsService.RemoteViewsFactory
import org.isoron.platform.utils.StringUtils.Companion.splitLongs
import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitNotFoundException import org.isoron.uhabits.core.models.HabitNotFoundException
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.utils.StringUtils.Companion.splitLongs
import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels
import java.util.ArrayList import java.util.ArrayList

View File

@@ -0,0 +1,89 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.widgets.activities
import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.view.Window
import android.widget.FrameLayout
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.core.ui.widgets.WidgetBehavior
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.intents.IntentParser
import org.isoron.uhabits.utils.SystemUtils
import org.isoron.uhabits.widgets.WidgetUpdater
class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPickerCallback {
private lateinit var behavior: WidgetBehavior
private lateinit var data: IntentParser.CheckmarkIntentData
private lateinit var widgetUpdater: WidgetUpdater
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
setContentView(FrameLayout(this))
val app = this.applicationContext as HabitsApplication
val component = app.component
val parser = app.component.intentParser
data = parser.parseCheckmarkIntent(intent)
behavior = WidgetBehavior(
component.habitList,
component.commandRunner,
component.notificationTray,
component.preferences
)
widgetUpdater = component.widgetUpdater
showNumberSelector(this)
SystemUtils.unlockScreen(this)
}
override fun onNumberPicked(newValue: Double) {
behavior.setValue(data.habit, data.timestamp, (newValue * 1000).toInt())
widgetUpdater.updateWidgets()
finish()
}
override fun onNumberPickerDismissed() {
finish()
}
private fun showNumberSelector(context: Context) {
val app = this.applicationContext as HabitsApplication
AndroidThemeSwitcher(this, app.component.preferences).apply()
val numberPickerFactory = NumberPickerFactory(context)
val today = DateUtils.getTodayWithOffset()
val entry = data.habit.computedEntries.get(today)
numberPickerFactory.create(
entry.value / 1000.0,
data.habit.unit,
this
).show()
}
companion object {
const val ACTION_SHOW_NUMERICAL_VALUE_ACTIVITY = "org.isoron.uhabits.ACTION_SHOW_NUMERICAL_VALUE_ACTIVITY"
}
}

View File

@@ -1,4 +1,4 @@
يساعدك Loop Habit Tracker على إنشاء عادات إيجابية طويلة المدى والحفاظ عليها في حياتك. تعطيكم الرسوم البيانية والإحصاءات التفصيلية صورة واضحة للكيفية التي تحسنت بها عاداتك مع مرور الوقت. التطبيق خالٍ تمامًا من الإعلانات ومفتوح المصدر ويحترم خصوصيتك. حلقة تعقب الحبوب تساعدك على إنشاء عادات إيجابية طويلة الأجل والحفاظ عليها في حياتك. تعطيكم الرسوم البيانية والإحصاءات التفصيلية صورة واضحة للكيفية التي تحسنت بها عاداتك مع مرور الوقت. التطبيق خالٍ تمامًا من الإعلانات ومفتوح المصدر ويحترم خصوصيتك.
<b>واجهة جميلة وبسيطة</b> <b>واجهة جميلة وبسيطة</b>
يحتوي Loop على واجهة أنيقة وبسيطة وسهلة الاستخدام للغاية ، حتى للمستخدمين لأول مرة. تم تحسين التطبيق ليكون ذي سرعة عالية ، ويعمل التطبيق بشكل جيد حتى على الهواتف القديمة. يحتوي Loop على واجهة أنيقة وبسيطة وسهلة الاستخدام للغاية ، حتى للمستخدمين لأول مرة. تم تحسين التطبيق ليكون ذي سرعة عالية ، ويعمل التطبيق بشكل جيد حتى على الهواتف القديمة.

View File

@@ -1,29 +1,29 @@
Loop Habit Tracker Vám pomáhá vytvořit a udržet si dlouhodobé pozitivní vyky. Podrobné grafy a statistiky Vám ukáží jasný přehled, jak se Vaše vyky postupem času zlepšily. Aplikace je zcela bez reklam, open-source a respektuje Vaše soukromí. 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í.
<b>Krásné, minimalistické a přehledné rozhraní</b> <b>Krásné, minimalistické a přehledné rozhraní</b>
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. 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.
<b>Skóre návyků</b> <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í. 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. 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".
<b>Flexibilní plánování</b> <b>Flexibilní plánování</b>
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. 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.
<b>Připomenutí</b> <b>Připomenutí</b>
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í. 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.
<b>Widgety</b> <b>Widgety</b>
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. 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.
<b>Převezměte kontrolu nad svými daty</b> <b>Převezměte kontrolu nad svými daty</b>
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. 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.
<b>Bez omezení</b> <b>Bez omezení</b>
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. 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.
<b>Zcela bez reklam a open source</b> <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 ani nikdy 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 nikdy zde nebudou. Aplikace je zcela open-source (GPLv3).
<b>Funguje off-line a respektuje vaše soukromí</b> <b>Funguje offline 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. 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.

View File

@@ -1 +1 @@
عادت‌سنج لوپ Loop Habit Tracker

View File

@@ -22,7 +22,7 @@ Si vous souhaitez analyser davantage vos données ou les déplacer vers un autre
Suivez autant d'habitudes que vous le souhaitez. Loop n'impose aucune limite artificielle sur le nombre d'habitudes que vous pouvez avoir. Toutes les fonctionnalités sont disponibles pour tous les utilisateurs. Il n'y a aucun achat dans l'application. Suivez autant d'habitudes que vous le souhaitez. Loop n'impose aucune limite artificielle sur le nombre d'habitudes que vous pouvez avoir. Toutes les fonctionnalités sont disponibles pour tous les utilisateurs. Il n'y a aucun achat dans l'application.
<b>Entièrement sans publicité et open source</b> <b>Entièrement sans publicité et open source</b>
Il n'y a pas de publicités, de notifications agaçantes ou d'autorisations intrusives dans cette application. L'application est entièrement open-source (GPLv3). Il n'y a pas de publicités, de notifications agaçantes ou d'autorisations intrusives dans cette application. Il n'y en aura jamais. L'application est entièrement open-source (GPLv3).
<b>Fonctionne hors ligne et respecte votre vie privée</b> <b>Fonctionne hors ligne et respecte votre vie privée</b>
Loop ne nécessite pas de connexion Internet ni d'enregistrement d'un compte en ligne. Vos données confidentielles ne sont jamais envoyées à personne. Ni les développeurs ni aucun tiers n'y ont accès. Loop ne nécessite pas de connexion Internet ni d'enregistrement d'un compte en ligne. Vos données confidentielles ne sont jamais envoyées à personne. Ni les développeurs ni aucun tiers n'y ont accès.

View File

@@ -1,13 +1,13 @@
יישום ”Loop לניהול הרגלים מסייע לך ביצירה ובשימור הרגלים טובים וארוכי טווח. תרשימים וסטטיסטיקה מפורטים נותנים לך תמונה ברורה כיצד ההרגלים שלך השתפרו לאורך זמן. היישום נטול פרסומות לחלוטין, קוד המקור שלו פתוח והוא מכבד את הפרטיות שלך. „Loop למעקב אחר ההרגלים מסייע לך ביצירה ובשימור הרגלים טובים וארוכי טווח. תרשימים וסטטיסטיקה מפורטים נותנים לך תמונה ברורה כיצד ההרגלים שלך השתפרו לאורך זמן. היישום נטול פרסומות לחלוטין, קוד המקור שלו פתוח והוא מכבד את הפרטיות שלך.
<b>ממשק יפה, חסכוני וקליל</b> <b>ממשק יפה, חסכוני וקליל</b>
הממשק של Loop אלגנטי, חסכוני וקל לשימוש אפילו בפעם הראשונה. היישום מותאם במיוחד למהירות, ועובד היטב גם בטלפונים ישנים. ל־Loop ממשק אלגנטי, חסכוני וקל לשימוש אפילו בפעם הראשונה. היישום מותאם במיוחד למהירות, ועובד היטב גם בטלפונים ישנים.
<b>ציון הרגל</b> <b>ציון הרגל</b>
ל־Loop יש נוסחה מתקדמת לחישוב חוזק ההרגלים שלך. כל חזרה על ההרגל מחזקת אותו, וכל יום שהוחמץ מחליש אותו. כמה ימים שהוחמצו לאחר רצף ארוך, לעומת זאת, לא יהרסו לחלוטין את ההתקדמות, בניגוד ליישומים רבים אחרים מסוג לא לשבור את השרשרת. ל־Loop יש נוסחה מתקדמת לחישוב חוזק ההרגלים שלך. כל חזרה על ההרגל מחזקת אותו, וכל יום שהוחמץ מחליש אותו. כמה ימים שהוחמצו לאחר רצף ארוך, לעומת זאת, לא יהרסו לחלוטין את ההתקדמות, בניגוד ליישומים רבים אחרים מסוג לא לשבור את השרשרת.
<b>לוחות זמנים גמישים</b> <b>לוחות זמנים גמישים</b>
בנוסף להרגלים יומיומיים, Loop תומך בהרגלים עם לוחות זמנים מורכבים יותר, כמו 3 פעמים בשבוע או יום כן ויום לא. בנוסף להרגלים יומיומיים, Loop תומך בהרגלים עם לוחות זמנים מורכבים יותר, כמו 3 פעמים בשבוע או יום כן יום לא.
<b>תזכורות</b> <b>תזכורות</b>
אפשר לתזמן התראות קבועות עם תזכורות לגבי ההרגלים שלך. לכל הרגל יכולה להיות תזכורת משלו, בשעה שנבחרה ביום. אפשר לסמן או לדחות את עשיית ההרגל ישירות מההתראה בקלות. אפשר לתזמן התראות קבועות עם תזכורות לגבי ההרגלים שלך. לכל הרגל יכולה להיות תזכורת משלו, בשעה שנבחרה ביום. אפשר לסמן או לדחות את עשיית ההרגל ישירות מההתראה בקלות.

View File

@@ -1 +1 @@
Loop לניהול הרגלים Loop Habit Tracker

View File

@@ -1,4 +1,4 @@
Loop Habit Tracker to aplikacja która pomaga tworzyć i podtrzymać długoterminowe pozytywne nawyki w Twoim życiu. Szczegółowe wykresy i statystyki dają jasny obraz tego, jak Twoje nawyki uległy poprawie z upływem czasu. Aplikacja jest całkowicie wolna od reklam, otwartoźródłowa i szanuje Twoją prywatność. Aplikacja "Śledzenie nawyków Loop" pomaga tworzyć i utrzymywać długotrwałe pozytywne nawyki w twoim życiu. Szczegółowe wykresy i statystyki dają jasny obraz tego, jak Twoje nawyki uległy poprawie z upływem czasu. Aplikacja jest całkowicie wolna od reklam, otwartoźródłowa i szanuje Twoją prywatność.
<b>Piękny, minimalistyczny i lekki interfejs</b> <b>Piękny, minimalistyczny i lekki interfejs</b>
Aplikacja "Śledzenie nawyków Loop" ma elegancki i minimalistyczny interfejs, który jest bardzo łatwy w obsłudze, nawet dla początkujących użytkowników. Jest ona wysoce zoptymalizowana pod kątem szybkości działania nawet na starszych telefonach. Aplikacja "Śledzenie nawyków Loop" ma elegancki i minimalistyczny interfejs, który jest bardzo łatwy w obsłudze, nawet dla początkujących użytkowników. Jest ona wysoce zoptymalizowana pod kątem szybkości działania nawet na starszych telefonach.

View File

@@ -1 +1 @@
Twórz dobre nawyki i śledź postępy (bez reklam) Twórz dobre nawyki i śledź ich postępy w czasie (bez reklam)

View File

@@ -1 +1 @@
Loop Habit Tracker Śledzenie Nawyków Loop

View File

@@ -16,7 +16,7 @@ Agende notificações para lembrá-lo de seus hábitos. Cada hábito pode ter se
Lembre-se de seus hábitos sempre que desbloquear o telefone. Widgets coloridos permitem que você acompanhe seus hábitos diretamente na tela inicial, sem abrir o aplicativo. Lembre-se de seus hábitos sempre que desbloquear o telefone. Widgets coloridos permitem que você acompanhe seus hábitos diretamente na tela inicial, sem abrir o aplicativo.
<b>Assuma o controle de seus dados</b> <b>Assuma o controle de seus dados</b>
Se você deseja analisar melhor seus dados ou movê-los para outro serviço, o Loop permite exportá-los para planilhas (CSV) ou para um arquivo de banco de dados (SQLite). Para usuários avançados, dados podem ser adicionadas através de outros aplicativos, como o Tasker. Se você deseja analisar melhor seus dados ou movê-los para outro serviço, o Loop permite exportá-los para planilhas (CSV) ou para um arquivo de banco de dados (SQLite). Para usuários avançados, marcas de seleção podem ser adicionadas através de outros aplicativos, como o Tasker.
<b>Sem limitações</b> <b>Sem limitações</b>
Acompanhe quantos hábitos desejar. O Loop não impõe limites artificiais sobre quantos hábitos você pode ter. Todos os recursos estão disponíveis para todos os usuários. Não há nenhum item à venda no aplicativo. Acompanhe quantos hábitos desejar. O Loop não impõe limites artificiais sobre quantos hábitos você pode ter. Todos os recursos estão disponíveis para todos os usuários. Não há nenhum item à venda no aplicativo.

View File

@@ -1,4 +1,4 @@
Loop Alışkanlık Takibi, yaşamınızda uzun vadeli olumlu alışkanlıklar yaratmanıza ve sürdürmenize yardımcı olur. Ayrıntılı grafik ve istatistikler, alışkanlıklarınızın zaman içinde nasıl geliştiğine dair size net bir resim sunar. Uygulama tamamen reklamsız ve açık kaynaklıdır. Loop Alışkanlık Takibi, yaşamınızda uzun vadeli olumlu alışkanlıklar yaratmanıza ve sürdürmenize yardımcı olur. Ayrıntılı grafik ve istatistikler, alışkanlıklarınızın zaman içinde nasıl geliştiğine dair size net bir resim sunar. Uygulama tamamen reklamsız ve açık kaynaklıdır. Gizliliğinize saygı duyar.
<b>Güzel, minimalist ve basit arayüz</b> <b>Güzel, minimalist ve basit arayüz</b>
Loop, ilk kez kullananlar için bile kullanımı çok kolay olan zarif ve minimalist bir arayüze sahiptir. Hız için son derece optimize edilmiş olan bu uygulama, eski telefonlarda bile gayet iyi çalışır. Loop, ilk kez kullananlar için bile kullanımı çok kolay olan zarif ve minimalist bir arayüze sahiptir. Hız için son derece optimize edilmiş olan bu uygulama, eski telefonlarda bile gayet iyi çalışır.

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