Compare commits
282 Commits
v2.0.3
...
feature/ca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e189d17fd5 | ||
|
|
30c39853e9 | ||
|
|
c1fa6c46ee | ||
|
|
6e4ed3837f | ||
|
|
cb65604581 | ||
|
|
2db4491328 | ||
|
|
25a3509988 | ||
| 0eae43fe55 | |||
| c0fcd4e763 | |||
| 79e2402c9d | |||
| 53c270ee12 | |||
| e4b16f6d59 | |||
| 3021e408a7 | |||
| 7649119db7 | |||
| b0a4f26e7a | |||
| 0a95b6d2a0 | |||
| e6bcbb39ff | |||
| 90de1f3723 | |||
| 68740b4043 | |||
| b66a6ff717 | |||
| 7c1a91e35a | |||
| 366e9af167 | |||
| 03b02aaa06 | |||
|
|
180cf25ad3 | ||
| 3490cd183a | |||
|
|
0c292d1eaa | ||
| 0b256cb2c0 | |||
| cc03c48648 | |||
| 53e7ef2918 | |||
| 9609bee0f7 | |||
| 5b23a3f960 | |||
| d2d45991b0 | |||
| 25aeafb759 | |||
| 9045ae5c24 | |||
| 555873354c | |||
| 2a012619a7 | |||
| d1de3a852b | |||
| f04e37e905 | |||
| 825a5f2cb9 | |||
| 0de6896691 | |||
| 7187214282 | |||
|
|
9d4161a255 | ||
|
|
d82a3c145d | ||
|
|
a6cbd44e42 | ||
|
|
45c62b4ab2 | ||
|
|
d53312d261 | ||
|
|
c453810785 | ||
|
|
999057300b | ||
|
|
2245347e28 | ||
|
|
b79f7850ed | ||
|
|
4469f86e46 | ||
|
|
bdeddb149e | ||
|
|
3b12ec4bfe | ||
| ca4618579e | |||
| 2f13aba22c | |||
| dba5912ecd | |||
| 88b8663484 | |||
| 609886fd09 | |||
| a4db997e06 | |||
|
|
090216ccb7 | ||
|
|
472092d1bf | ||
|
|
6d3791de31 | ||
|
|
24a99d5791 | ||
|
|
6c631b1cf6 | ||
|
|
5d7677b354 | ||
|
|
22820f4f24 | ||
| 1d3bd48535 | |||
|
|
694446b7e3 | ||
|
|
20ae9d247e | ||
|
|
1943fac610 | ||
|
|
a07a50e635 | ||
|
|
8ccb9bbab1 | ||
|
|
743b8d26ad | ||
|
|
c47bd4c328 | ||
|
|
d84abc3a6a | ||
|
|
4ae85f1ec0 | ||
|
|
79d40d1d79 | ||
|
|
1902b8821e | ||
|
|
075542d605 | ||
|
|
0b5894ee6d | ||
|
|
92fb9dbdb6 | ||
|
|
1860abf532 | ||
|
|
3f4780c9fb | ||
|
|
d5d6e4616e | ||
|
|
18e267053d | ||
| 7ba9a7e4e7 | |||
| 5b8a7c39e2 | |||
| d40a5a89cd | |||
| 308d558347 | |||
| 9770ce187a | |||
| 535bc03b70 | |||
| 642e45af89 | |||
| 56d2307b75 | |||
| d875af8a8e | |||
| feeb4f057d | |||
| 1ad5c6b896 | |||
| baee3b9f86 | |||
| e6167baab1 | |||
|
|
1c15e7742e | ||
|
|
072ba63789 | ||
|
|
0fa0daa058 | ||
|
|
5a5ed3d631 | ||
|
|
3dfa376f59 | ||
|
|
7bf74634bb | ||
|
|
fc645a81bc | ||
|
|
35365bbdf3 | ||
|
|
779ef5dbee | ||
|
|
e82994c76b | ||
|
|
4e1d01d8d1 | ||
|
|
5de0fc86e5 | ||
| e26b643423 | |||
| 621534d610 | |||
| a01300e9c6 | |||
| ecb8ce105a | |||
| 32ef3c14f7 | |||
| 4972257635 | |||
| c98cb50baa | |||
| c331f34fa9 | |||
| a1aea532b5 | |||
| 43489aeb4c | |||
| 990c85aedd | |||
|
|
64337b9bee | ||
|
|
8bdfaa2434 | ||
|
|
5f6060858d | ||
| b62e436054 | |||
| bf63b4dbcf | |||
| a82d940bcc | |||
| ba59dc7ca9 | |||
| 181290a0f3 | |||
|
|
d553c2f3f2 | ||
|
|
7776093217 | ||
|
|
b27f3f8540 | ||
| eb041bf6b2 | |||
|
|
e3c53bf07f | ||
|
|
9ddab6ee59 | ||
|
|
2615795402 | ||
|
|
6531445d7f | ||
|
|
4fbf8a8ca2 | ||
|
|
707b2b4380 | ||
|
|
aae85c1170 | ||
|
|
c12a6c6a4d | ||
|
|
b15c02adbf | ||
|
|
9e24128675 | ||
|
|
66c61e2e6c | ||
|
|
7bddfbe5a7 | ||
|
|
8036b10ee6 | ||
|
|
71f400f587 | ||
|
|
79e302f922 | ||
|
|
af7f60fc4d | ||
|
|
7cc4b66dfd | ||
|
|
a9fddf9963 | ||
|
|
36c1504c6a | ||
|
|
d644170141 | ||
|
|
d38f83e961 | ||
|
|
c50c5af497 | ||
|
|
fa3774a32b | ||
|
|
fd124f2a6c | ||
|
|
265b65eb8a | ||
|
|
4c269c55d2 | ||
|
|
c03305120e | ||
|
|
29615b670b | ||
|
|
6ab4a696b6 | ||
|
|
23479c7765 | ||
|
|
6d98f7aafa | ||
| 75078ed52b | |||
| 2a0afedb1d | |||
| 66a2b41250 | |||
|
|
d6a7fa3d7a | ||
|
|
07e55f1c76 | ||
| 4ee5dd910b | |||
| 87f071b5b4 | |||
| bb0b5e8adf | |||
|
|
c79d1e82a5 | ||
|
|
4aebeedec6 | ||
|
|
7de94f2caf | ||
|
|
17ed85fc1b | ||
|
|
4355fb4d68 | ||
|
|
508200abeb | ||
|
|
a29943e783 | ||
|
|
3e6a9181d6 | ||
| 1fe3a3d1ca | |||
|
|
b2951a3475 | ||
|
|
9d3c63cf62 | ||
|
|
65d237254c | ||
|
|
fe1d5c66cb | ||
|
|
113a5028af | ||
|
|
1a56260757 | ||
|
|
697fffbc99 | ||
|
|
804edfa64e | ||
| 2ab6c396d0 | |||
| a55f467339 | |||
|
|
cf682f68c9 | ||
| 0e988e746c | |||
|
|
f119cbf8e7 | ||
|
|
056f5f6fce | ||
|
|
42f6125d5e | ||
|
|
3e20fc4d1d | ||
|
|
1f763feb69 | ||
|
|
6e7ad329fe | ||
|
|
5cb241475d | ||
|
|
27e76c7243 | ||
|
|
576ad04064 | ||
|
|
5f8187ef6d | ||
|
|
f16f919e27 | ||
|
|
736bb8a75e | ||
|
|
de9ad6d4a4 | ||
|
|
1d37ce54ea | ||
|
|
f88f1cfb54 | ||
|
|
fc1478645b | ||
|
|
ffab001b09 | ||
|
|
a58a8005e1 | ||
|
|
c884ada187 | ||
|
|
e3d46ad5a0 | ||
|
|
f4a2c03216 | ||
|
|
f2b8f2f98d | ||
|
|
2c5fd87a2a | ||
|
|
39768f7f04 | ||
|
|
cc3e1ced15 | ||
|
|
2e26cc104e | ||
|
|
42fd0926ef | ||
| ec202aa9a7 | |||
|
|
1fb56c8777 | ||
|
|
a5d4a37da8 | ||
|
|
4804a48549 | ||
|
|
c892a845b4 | ||
|
|
e98064b6a5 | ||
| 57f5f6ed5b | |||
| 79f5b8b7e8 | |||
| f15c660d33 | |||
| 1866743c47 | |||
|
|
a781a1f947 | ||
|
|
13e57b5026 | ||
|
|
f8c7abfff4 | ||
|
|
7fe3ce970c | ||
|
|
b9eb244b0b | ||
|
|
95a1786c4a | ||
|
|
caa1c9d72e | ||
|
|
a7afe0b309 | ||
|
|
9c03174eef | ||
|
|
da02926fa6 | ||
|
|
250dabfe58 | ||
|
|
0bba3b76bc | ||
|
|
c2479278ba | ||
|
|
499a403a06 | ||
|
|
af5d622339 | ||
|
|
5eeb54bc47 | ||
|
|
e09e899aad | ||
| 37f03aca37 | |||
|
|
0ab55f6f5a | ||
|
|
43921721d4 | ||
|
|
33c88cded3 | ||
|
|
aecce891ea | ||
|
|
2ea98a7756 | ||
|
|
e667872d83 | ||
|
|
3602a614c4 | ||
|
|
d8c5f4d93c | ||
|
|
c9f4df9dae | ||
|
|
feb384bca6 | ||
|
|
71e9160460 | ||
|
|
64966d3c86 | ||
|
|
4787df4074 | ||
|
|
5908692a5c | ||
|
|
693dce8719 | ||
| b232827dfd | |||
|
|
02f9f411ce | ||
| bbf9da44e1 | |||
|
|
ee896fb4f9 | ||
|
|
4d7d8b6206 | ||
|
|
55b841a8b4 | ||
| 7fac86b617 | |||
|
|
c31d42be2d | ||
| 31c09b9c0b | |||
|
|
db91dce57f | ||
| 12c76245e6 | |||
| 59a4d7552c | |||
| 9d7840bdd1 | |||
|
|
32db4e363b | ||
|
|
004bb8d71c | ||
|
|
21a1e88c47 | ||
|
|
86fb718896 | ||
|
|
a4e9b2f874 | ||
|
|
ac924470b8 |
18
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -6,11 +6,16 @@ labels: ''
|
|||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
<!--
|
||||||
|
Please use the template below for your bug report. Issues that do not follow this
|
||||||
|
template, or that do not contain all necessary information (namely, description of
|
||||||
|
the problem, steps to reproduce, phone, phone operating system, and app version)
|
||||||
|
will be closed without further consideration.
|
||||||
|
-->
|
||||||
|
|
||||||
## Pre-submission checklist
|
## Pre-submission checklist
|
||||||
- [ ] I am submitting a bug report, not a feature request.
|
- [ ] I am submitting a bug report, not a feature request.
|
||||||
- [ ] I am running the latest version of Loop Habit Tracker.
|
- [ ] I have searched for similar issues, but did not find any matches.
|
||||||
- [ ] I have have searched for similar issues, but did not find any matches.
|
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
A clear and concise description of what the problem is.
|
A clear and concise description of what the problem is.
|
||||||
@@ -21,10 +26,11 @@ A clear and concise description of what the problem is.
|
|||||||
3. Scroll down to ....
|
3. Scroll down to ....
|
||||||
4. See error
|
4. See error
|
||||||
|
|
||||||
## Screenshots
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
## System information
|
## System information
|
||||||
- Phone: [e.g. Google Pixel 4]
|
- Phone: [e.g. Google Pixel 4]
|
||||||
- Phone Operating System: [e.g. Android 10]
|
- Phone Operating System: [e.g. Android 10]
|
||||||
- App version: [e.g. 1.8.9]
|
- App version: [e.g. 2.0.2]
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
|||||||
2
.github/dependabot.yml
vendored
@@ -3,4 +3,4 @@ updates:
|
|||||||
- package-ecosystem: "gradle"
|
- package-ecosystem: "gradle"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "monthly"
|
||||||
|
|||||||
54
.github/workflows/main.yml
vendored
@@ -7,54 +7,24 @@ on:
|
|||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '**.md'
|
- '**.md'
|
||||||
jobs:
|
jobs:
|
||||||
Build:
|
Test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: self-hosted
|
||||||
steps:
|
steps:
|
||||||
- name: Check out source code
|
- name: Check out source code
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v1
|
||||||
|
|
||||||
- name: Install Java Development Kit 1.8
|
- name: Build project
|
||||||
uses: actions/setup-java@v1
|
|
||||||
with:
|
|
||||||
java-version: 1.8
|
|
||||||
|
|
||||||
- name: Build Project
|
|
||||||
run: ./build.sh build
|
run: ./build.sh build
|
||||||
|
|
||||||
- name: Upload Build Artifacts
|
- name: Run Android tests
|
||||||
|
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: uhabits-android
|
name: build
|
||||||
path: uhabits-android/build/outputs/
|
path: |
|
||||||
|
build/*log
|
||||||
AndroidTest:
|
uhabits-android/build/outputs
|
||||||
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: Run Android Tests
|
|
||||||
run: ./build.sh android-tests ${{ matrix.api }}
|
|
||||||
|
|
||||||
|
|||||||
35
CHANGELOG.md
@@ -1,5 +1,40 @@
|
|||||||
# 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)
|
||||||
|
|||||||
10
README.md
@@ -18,7 +18,7 @@ source.
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://play.google.com/store/apps/details?id=org.isoron.uhabits&utm_source=global_co&utm_medium=prtnr&utm_content=Mar2515&utm_campaign=PartBadge&pcampaignid=MKT-AC-global-none-all-co-pr-py-PartBadges-Oct1515-1"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/apps/en-play-badge-border.png" height="75px"/></a>
|
<a href="https://play.google.com/store/apps/details?id=org.isoron.uhabits&utm_source=global_co&utm_medium=prtnr&utm_content=Mar2515&utm_campaign=PartBadge&pcampaignid=MKT-AC-global-none-all-co-pr-py-PartBadges-Oct1515-1"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/apps/en-play-badge-border.png" height="75px"/></a>
|
||||||
<a href="http://f-droid.org/app/org.isoron.uhabits"><img alt="Get it on F-Droid" src="http://i.imgur.com/baSPE7X.png" height="75px"/></a>
|
<a href="https://f-droid.org/app/org.isoron.uhabits"><img alt="Get it on F-Droid" src="https://i.imgur.com/baSPE7X.png" height="75px"/></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
@@ -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 `docs/GUIDELINES.md`.
|
contribute with code. Please see the [guidelines](https://github.com/iSoron/uhabits/blob/dev/docs/GUIDELINES.md).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ contribute, even if you are not a software developer.
|
|||||||
more details.
|
more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License along
|
You should have received a copy of the GNU General Public License along
|
||||||
with this program. If not, see <http://www.gnu.org/licenses/>.
|
with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
[screen1]: screenshots/1.png
|
[screen1]: screenshots/1.png
|
||||||
[screen2]: screenshots/2.png
|
[screen2]: screenshots/2.png
|
||||||
@@ -123,9 +123,9 @@ contribute, even if you are not a software developer.
|
|||||||
[screen4th]: screenshots/4.thumb.png
|
[screen4th]: screenshots/4.thumb.png
|
||||||
[screen5th]: screenshots/5.thumb.png
|
[screen5th]: screenshots/5.thumb.png
|
||||||
[screen6th]: screenshots/6.thumb.png
|
[screen6th]: screenshots/6.thumb.png
|
||||||
[poedit]: http://translate.loophabits.org
|
[poedit]: https://translate.loophabits.org
|
||||||
[playstore]: https://play.google.com/store/apps/details?id=org.isoron.uhabits
|
[playstore]: https://play.google.com/store/apps/details?id=org.isoron.uhabits
|
||||||
[releases]: https://github.com/iSoron/uhabits/releases
|
[releases]: https://github.com/iSoron/uhabits/releases
|
||||||
[fdroid]: http://f-droid.org/app/org.isoron.uhabits
|
[fdroid]: https://f-droid.org/app/org.isoron.uhabits
|
||||||
[build]: https://github.com/iSoron/uhabits/blob/dev/docs/BUILD.md
|
[build]: https://github.com/iSoron/uhabits/blob/dev/docs/BUILD.md
|
||||||
[beta]: https://play.google.com/apps/testing/org.isoron.uhabits
|
[beta]: https://play.google.com/apps/testing/org.isoron.uhabits
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
plugins {
|
plugins {
|
||||||
val kotlinVersion = "1.4.21"
|
val kotlinVersion = "1.6.10"
|
||||||
id("com.android.application") version ("4.1.0") apply (false)
|
id("com.android.application") version ("7.0.3") 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.0.0"
|
id("org.jlleitschuh.gradle.ktlint") version "10.2.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply {
|
apply {
|
||||||
@@ -15,11 +15,9 @@ 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://kotlin.bintray.com/ktor")
|
maven(url = "https://jitpack.io")
|
||||||
maven(url = "https://kotlin.bintray.com/kotlin-js-wrappers")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
124
build.sh
@@ -26,6 +26,7 @@ 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"
|
||||||
@@ -69,8 +70,7 @@ core_build() {
|
|||||||
# Android
|
# Android
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
# shellcheck disable=SC2016
|
android_setup() {
|
||||||
android_test() {
|
|
||||||
API=$1
|
API=$1
|
||||||
AVDNAME=${AVD_PREFIX}${API}
|
AVDNAME=${AVD_PREFIX}${API}
|
||||||
|
|
||||||
@@ -95,15 +95,53 @@ android_test() {
|
|||||||
) 10>/tmp/uhabitsTest.lock
|
) 10>/tmp/uhabitsTest.lock
|
||||||
|
|
||||||
log_info "Launching emulator..."
|
log_info "Launching emulator..."
|
||||||
$EMULATOR -avd $AVDNAME -port 6${API}0 1>/dev/null 2>&1 &
|
$EMULATOR \
|
||||||
|
-avd $AVDNAME \
|
||||||
|
-port 6${API}0 \
|
||||||
|
1>/dev/null 2>&1 &
|
||||||
|
|
||||||
|
log_info "Waiting for emulator to boot..."
|
||||||
|
export ADB="$ADB -s emulator-6${API}0"
|
||||||
|
timeout $BOOT_TIMEOUT $ADB wait-for-device shell 'while [[ -z "$(getprop sys.boot_completed)" ]]; do echo Waiting...; sleep 1; done; input keyevent 82'
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
log_error "Emulator failed to boot after $BOOT_TIMEOUT seconds."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Saving snapshot..."
|
||||||
|
$ADB emu avd snapshot save fresh-install
|
||||||
|
}
|
||||||
|
|
||||||
|
android_boot_attempt() {
|
||||||
|
API=$1
|
||||||
|
AVDNAME=${AVD_PREFIX}${API}
|
||||||
|
|
||||||
|
log_info "Stopping Android emulator..."
|
||||||
|
while [[ -n $(pgrep -f ${AVDNAME}) ]]; do
|
||||||
|
pkill -9 -f ${AVDNAME}
|
||||||
|
done
|
||||||
|
|
||||||
|
log_info "Launching emulator..."
|
||||||
|
$EMULATOR \
|
||||||
|
-avd $AVDNAME \
|
||||||
|
-port 6${API}0 \
|
||||||
|
-snapshot fresh-install \
|
||||||
|
-no-snapshot-save \
|
||||||
|
-wipe-data \
|
||||||
|
1>/dev/null 2>&1 &
|
||||||
|
|
||||||
log_info "Waiting for emulator to boot..."
|
log_info "Waiting for emulator to boot..."
|
||||||
export ADB="$ADB -s emulator-6${API}0"
|
export ADB="$ADB -s emulator-6${API}0"
|
||||||
$ADB wait-for-device shell 'while [[ -z "$(getprop sys.boot_completed)" ]]; do echo Waiting...; sleep 1; done; input keyevent 82' || return 1
|
|
||||||
$ADB root || return 1
|
|
||||||
sleep 5
|
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..."
|
log_info "Disabling animations..."
|
||||||
|
$ADB root || return 1
|
||||||
|
sleep 5
|
||||||
$ADB shell settings put global window_animation_scale 0 || return 1
|
$ADB shell settings put global 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
|
||||||
@@ -111,6 +149,24 @@ android_test() {
|
|||||||
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
|
||||||
@@ -122,14 +178,25 @@ 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
|
||||||
$ADB shell am instrument \
|
FAILED_TESTS=""
|
||||||
-r -e coverage true -e size $size \
|
for i in {1..5}; do
|
||||||
-w ${PACKAGE_NAME}.test/androidx.test.runner.AndroidJUnitRunner \
|
log_info "Running $size instrumented tests (attempt $i)..."
|
||||||
| tee $OUT_INSTRUMENT
|
$ADB shell am instrument \
|
||||||
if grep "\(INSTRUMENTATION_STATUS_CODE.*-1\|FAILURES\|ABORTED\|onError\|Error type\|crashed\)" $OUT_INSTRUMENT; then
|
-r -e coverage true -e size "$size" $FAILED_TESTS \
|
||||||
|
-w ${PACKAGE_NAME}.test/androidx.test.runner.AndroidJUnitRunner \
|
||||||
|
| ts "%.s" | tee "$OUT_INSTRUMENT"
|
||||||
|
|
||||||
|
FAILED_TESTS=$(tools/parseInstrument.py "$OUT_INSTRUMENT")
|
||||||
|
SUCCESS=$?
|
||||||
|
if [ $SUCCESS -eq 0 ]; then
|
||||||
|
log_info "$size tests passed."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $SUCCESS -ne 0 ]; then
|
||||||
log_error "Some $size instrumented tests failed."
|
log_error "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
|
||||||
@@ -138,13 +205,14 @@ 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
|
||||||
@@ -152,12 +220,27 @@ 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. See $LOG for more details."
|
log_error "API $API: Failed"
|
||||||
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() {
|
||||||
@@ -229,12 +312,14 @@ 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
|
||||||
@@ -270,18 +355,17 @@ 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
|
||||||
for attempt in {1..5}; do
|
android_test $1
|
||||||
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 "$@"
|
||||||
|
|||||||
@@ -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 -XX:MaxPermSize=2048m
|
org.gradle.jvmargs=-Xms2048m -Xmx2048m
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
|||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,5 @@
|
|||||||
#Sat Nov 28 09:55:24 CST 2020
|
|
||||||
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
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-all.zip
|
|
||||||
|
|||||||
2
gradlew
vendored
@@ -82,6 +82,7 @@ esac
|
|||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
@@ -129,6 +130,7 @@ fi
|
|||||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
# We build the pattern for arguments to be converted via cygpath
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
|||||||
25
gradlew.bat
vendored
@@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=.
|
|||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
@@ -37,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
|||||||
|
|
||||||
set JAVA_EXE=java.exe
|
set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if "%ERRORLEVEL%" == "0" goto init
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
@@ -51,7 +54,7 @@ goto fail
|
|||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto init
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
@@ -61,28 +64,14 @@ echo location of your Java installation.
|
|||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
:init
|
|
||||||
@rem Get command-line arguments, handling Windows variants
|
|
||||||
|
|
||||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
|
||||||
|
|
||||||
:win9xME_args
|
|
||||||
@rem Slurp the command line arguments.
|
|
||||||
set CMD_LINE_ARGS=
|
|
||||||
set _SKIP=2
|
|
||||||
|
|
||||||
:win9xME_args_slurp
|
|
||||||
if "x%~1" == "x" goto execute
|
|
||||||
|
|
||||||
set CMD_LINE_ARGS=%*
|
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
pluginManagement {
|
pluginManagement {
|
||||||
repositories {
|
repositories {
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
jcenter()
|
|
||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
resolutionStrategy.eachPlugin {
|
resolutionStrategy.eachPlugin {
|
||||||
|
|||||||
67
tools/parseInstrument.py
Executable file
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Android Instrumentation Test Parser
|
||||||
|
|
||||||
|
Given a raw Android Instrumentation log (produced by "adb shell am instrument -r ...") this script
|
||||||
|
return zero if all tests pass and non-zero if some tests fail. In case of failure, this script
|
||||||
|
also prints arguments that, if passed to "am instrument", will cause it to re-run just the tests
|
||||||
|
that failed. This script additionally prints warnings about the tests on the STDERR; e.g. slow tests.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
|
STATUS_START = 1
|
||||||
|
STATUS_DISABLED = -3
|
||||||
|
SLOW_TEST_THRESHOLD = 5.0
|
||||||
|
|
||||||
|
COLOR_RED = '\033[91m'
|
||||||
|
COLOR_YELLOW = '\033[93m'
|
||||||
|
COLOR_END = '\033[0m'
|
||||||
|
|
||||||
|
def error(msg):
|
||||||
|
sys.stderr.write("%s%s%s\n" % (COLOR_RED, msg, COLOR_END))
|
||||||
|
|
||||||
|
def warning(msg):
|
||||||
|
sys.stderr.write("%s%s%s\n" % (COLOR_YELLOW, msg, COLOR_END))
|
||||||
|
|
||||||
|
log_filename = sys.argv[1]
|
||||||
|
current_class, current_method = None, None
|
||||||
|
failed_tests = []
|
||||||
|
am_args = "-e class "
|
||||||
|
exit_code = 1
|
||||||
|
|
||||||
|
for line in open(log_filename).readlines():
|
||||||
|
matches = re.findall('^([0-9.]*)', line)
|
||||||
|
current_time = float(matches[0])
|
||||||
|
|
||||||
|
matches = re.findall('INSTRUMENTATION_STATUS: class=(.*)', line)
|
||||||
|
if len(matches) > 0:
|
||||||
|
current_class = matches[0]
|
||||||
|
|
||||||
|
matches = re.findall('INSTRUMENTATION_STATUS: test=(.*)', line)
|
||||||
|
if len(matches) > 0:
|
||||||
|
current_method = matches[0]
|
||||||
|
|
||||||
|
matches = re.findall('OK \([0-9]* tests?\)', line)
|
||||||
|
if len(matches) > 0:
|
||||||
|
exit_code = 0
|
||||||
|
|
||||||
|
matches = re.findall('INSTRUMENTATION_STATUS_CODE: ([-0-9]*)', line)
|
||||||
|
if len(matches) > 0:
|
||||||
|
status_code = int(matches[0])
|
||||||
|
if (status_code < 0) and (status_code != STATUS_DISABLED):
|
||||||
|
am_args += f"{current_class}#{current_method},"
|
||||||
|
failed_tests.append(f"{current_class}#{current_method}")
|
||||||
|
if status_code == STATUS_START:
|
||||||
|
initial_time = current_time
|
||||||
|
else:
|
||||||
|
elapsed_time = current_time - initial_time
|
||||||
|
if(elapsed_time > SLOW_TEST_THRESHOLD):
|
||||||
|
warning("SLOW %s#%s (%.2f seconds)" % (current_class, current_method, elapsed_time))
|
||||||
|
|
||||||
|
if len(failed_tests) > 0:
|
||||||
|
for test in failed_tests:
|
||||||
|
error("FAIL %s" % test)
|
||||||
|
print(am_args[:-1])
|
||||||
|
|
||||||
|
sys.exit(exit_code)
|
||||||
@@ -1,377 +1,415 @@
|
|||||||
Name,Languages,"Translated (Words)","Target Words","Approved (Words)",Voted,"""+"" votes received","""-"" votes received","Winning (Words)",Joined
|
Name,Languages,"Translated (Words)","Target Words","Approved (Words)",Voted,"""+"" votes received","""-"" votes received","Winning (Words)",Joined
|
||||||
"Alinson Xavier (iSoron)","Portuguese, Brazilian; Japanese; Spanish; Portuguese; Italian; Chinese Simplified; French; Hungarian; German; Arabic; Hindi; Slovenian; Catalan; Greek; Korean; Bulgarian; Chinese Traditional; Polish; Russian; Serbian (Cyrillic); Turkish; Ukrainian; Czech; Indonesian; Croatian; Danish; Dutch; Romanian; Swedish; Basque; Persian; Finnish; Vietnamese; Telugu; Tamil; Afrikaans; Esperanto; Hebrew",14808,17227,1282,0,1779,80,4274,"2016-03-05 18:35:27"
|
"Alinson Xavier (iSoron)","Portuguese, Brazilian; Japanese; Chinese Simplified; Italian; Spanish; Portuguese; French; Hungarian; Chinese Traditional; Turkish; Russian; Polish; Arabic; German; Korean; Greek; Catalan; Bulgarian; Hindi; Slovenian; Ukrainian; Serbian (Cyrillic); Czech; Indonesian; Croatian; Danish; Dutch; Romanian; Swedish; Basque; Persian; Finnish; Vietnamese; Tamil; Telugu; Hebrew; Esperanto; Norwegian; Afrikaans; Slovak; Armenian; Serbian (Latin); Uyghur",15497,18825,1308,0,1896,84,4315,"2016-03-05 18:35:27"
|
||||||
"Slobodan Simić (Слободан Симић) (slsimic)","Serbian (Latin); Serbian (Cyrillic)",2054,1831,2114,12,33,0,1991,"2021-02-03 14:26:07"
|
"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,1000,900,1097,14,1,0,946,"2020-10-11 20:10:51"
|
"Omer I.S. (omeritzics)",Hebrew,1040,927,1122,14,1,0,975,"2020-10-11 20:10:51"
|
||||||
dukelc,Slovak,919,880,0,0,0,0,0,"2020-08-27 14:02:41"
|
"Intan Ayunda (Intan_Ayunda)",Indonesian,818,811,985,0,0,0,729,"2020-10-14 07:51:58"
|
||||||
"Intan Ayunda (Intan_Ayunda)",Indonesian,800,793,962,0,0,0,711,"2020-10-14 07:51:58"
|
"Mihail Stefanov (MStefanov)",Bulgarian,755,794,3,0,2,0,2,"2017-03-31 16:09:02"
|
||||||
KMakoto,"Chinese Traditional",745,1146,949,0,0,0,745,"2019-10-22 04:19:52"
|
KMakoto,"Chinese Traditional",745,1146,949,0,0,0,745,"2019-10-22 04:19:52"
|
||||||
"Evren (evrenkiymaz)",Turkish,688,604,0,71,5,1,0,"2020-10-04 03:39:16"
|
"Evren (evrenkiymaz)",Turkish,688,604,0,71,28,22,0,"2020-10-04 03:39:16"
|
||||||
|
andaryon,Czech,681,606,0,108,0,0,0,"2021-11-25 10:20:45"
|
||||||
|
"Antti Kallio (antti.kallio)",Finnish,668,539,0,5,0,0,0,"2021-07-03 05:54:44"
|
||||||
"David Nos (david.nos)","Catalan; Spanish",667,731,0,0,1,0,0,"2020-01-04 10:15:36"
|
"David Nos (david.nos)","Catalan; Spanish",667,731,0,0,1,0,0,"2020-01-04 10:15:36"
|
||||||
"Antti Kallio (antti.kallio)",Finnish,650,525,0,0,0,0,0,"2021-07-03 05:54:44"
|
androide74,Italian,662,681,0,2,0,0,0,"2020-02-06 15:46:28"
|
||||||
androide74,Italian,644,659,0,2,0,0,0,"2020-02-06 15:46:28"
|
Osoitz,Basque,655,595,0,9,0,0,3,"2018-01-23 14:07:47"
|
||||||
|
"Dmitriy Bogdanov (di72nn)",Russian,643,589,1197,0,36,0,515,"2017-03-31 10:00:48"
|
||||||
Tomairuka,Japanese,633,1636,909,43,0,0,564,"2020-12-12 12:14:22"
|
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"
|
||||||
Osoitz,Basque,610,545,0,9,0,0,3,"2018-01-23 14:07:47"
|
"Saeed Esmaili (saaeed.es20)",Persian,586,795,0,5,4,0,0,"2020-11-26 15:41:15"
|
||||||
"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"
|
||||||
boban77,Czech,509,461,0,2,0,0,0,"2020-04-30 13:18:24"
|
"Isti (eisti)",Hungarian,528,476,0,0,0,0,0,"2020-12-03 12:02:51"
|
||||||
"Yoav Argov (YoavArgov)",Hebrew,501,461,0,0,1,8,103,"2017-04-28 07:23:01"
|
boban77,Czech,509,461,0,2,29,0,0,"2020-04-30 13:18:24"
|
||||||
|
"Martim Parente (martimparente)",Portuguese,505,542,0,38,0,0,0,"2020-08-26 10:22:11"
|
||||||
|
"Yoav Argov (YoavArgov)",Hebrew,501,461,0,0,1,8,91,"2017-04-28 07:23:01"
|
||||||
REMOVED_USER,Norwegian,501,498,501,0,148,0,501,"2017-07-05 19:02:25"
|
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"
|
||||||
"Huy Ngo (huyngo)",Vietnamese,461,695,0,1,0,0,0,"2020-01-26 11:58:36"
|
|
||||||
"黄克 (hk13127)","Chinese Simplified",461,765,0,1,0,0,24,"2020-01-17 23:16:03"
|
"黄克 (hk13127)","Chinese Simplified",461,765,0,1,0,0,24,"2020-01-17 23:16:03"
|
||||||
"Arkadiusz Bubak (epitek)",Polish,458,416,29,24,0,3,0,"2020-11-05 05:11:58"
|
"Huy Ngo (huyngo)",Vietnamese,461,695,0,1,0,0,0,"2020-01-26 11:58:36"
|
||||||
|
"Arkadiusz Bubak (epitek)",Polish,458,416,52,24,9,4,0,"2020-11-05 05:11:58"
|
||||||
marco.baturan,Esperanto,452,452,0,0,0,0,0,"2020-06-23 02:49:46"
|
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,252,"2020-01-09 19:35:48"
|
"Thamara Andrade (tkcandrade)","Portuguese, Brazilian",380,387,0,0,1,0,239,"2020-01-09 19:35:48"
|
||||||
"Sølv Ræven (soelvraeven)",Danish,370,370,0,0,0,0,0,"2020-11-28 16:46:18"
|
"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,0,0,0,"2020-05-26 19:47:58"
|
"Michael Malak (MichaelKMalak)",Arabic,304,271,0,0,1,0,0,"2020-05-26 19:47:58"
|
||||||
|
Blinkin,Dutch,297,334,0,5,0,0,0,"2021-06-14 10:30:05"
|
||||||
"Elina Salminen (salminen.elina.m)",Finnish,297,227,0,0,0,0,0,"2021-01-06 01:28:57"
|
"Elina Salminen (salminen.elina.m)",Finnish,297,227,0,0,0,0,0,"2021-01-06 01:28:57"
|
||||||
ayane.m,Japanese,292,863,0,1,3,0,22,"2019-11-20 03:28:26"
|
ayane.m,Japanese,292,863,0,1,5,0,22,"2019-11-20 03:28:26"
|
||||||
Blinkin,Dutch,284,318,0,1,0,0,0,"2021-06-14 10:30:05"
|
"Marius Teufelweich (teufelweich)",German,267,272,611,4,13,1,146,"2021-03-12 04:11:38"
|
||||||
"Marius Teufelweich (teufelweich)",German,249,256,606,4,2,0,146,"2021-03-12 04:11:38"
|
hypnotichemionus,"Chinese Simplified",249,430,0,0,8,0,19,"2020-03-08 01:46:25"
|
||||||
cobalt59,German,237,234,0,1,24,1,132,"2017-06-05 05:18:33"
|
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,0,0,0,"2020-06-12 12:52:10"
|
easyrepro,Telugu,214,297,0,0,4,0,0,"2020-06-12 12:52:10"
|
||||||
taras-ko,Ukrainian,211,183,0,1,4,0,19,"2017-10-26 16:52:22"
|
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"
|
||||||
axmed99,Ukrainian,203,177,0,40,0,0,53,"2021-04-01 03:56:20"
|
"Andrij Mizyk (andmizyk)",Ukrainian,204,178,0,40,0,0,53,"2021-04-01 03:56:20"
|
||||||
"Heru Yen (heruyen)",Indonesian,201,201,0,0,0,0,25,"2020-06-29 18:39:15"
|
"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"
|
||||||
Ishmaeel,Turkish,193,174,0,129,6,0,0,"2017-10-04 03:54:00"
|
_translator,French,199,227,0,11,0,0,0,"2021-07-06 07:54:12"
|
||||||
|
Ishmaeel,Turkish,193,174,0,129,17,6,0,"2017-10-04 03:54:00"
|
||||||
|
oscfd,Spanish,192,201,0,2,4,0,0,"2021-05-21 17:58:22"
|
||||||
|
bruhwut,Vietnamese,189,292,0,1,0,0,0,"2021-05-21 07:16:30"
|
||||||
"Aputsiak Niels Janussen (aputtu)",Danish,187,200,0,0,0,0,0,"2019-08-28 05:47:42"
|
"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"
|
||||||
bruhwut,Vietnamese,171,268,0,1,0,0,0,"2021-05-21 07:16:30"
|
plitwin,Polish,168,151,0,2,31,0,49,"2021-01-20 06:18:37"
|
||||||
DionysosDV,Greek,165,153,0,0,0,0,0,"2021-02-27 19:05:25"
|
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"
|
||||||
oscfd,Spanish,155,166,0,1,4,0,0,"2021-05-21 17:58:22"
|
"Ravi Rami (ramiravi)",Hindi,151,248,0,0,0,0,0,"2021-10-10 09:19:40"
|
||||||
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,2,0,0,"2017-03-31 12:37:17"
|
alalloush,Arabic,118,129,0,2,14,3,0,"2017-03-31 12:37:17"
|
||||||
"Eilif Adelvice (adelvice)",Spanish,116,126,0,96,0,0,0,"2021-08-05 07:20:21"
|
|
||||||
Sebastian05067,Spanish,114,133,0,55,28,0,0,"2017-05-14 00:48:16"
|
|
||||||
"Tanya (MagicUnderHood)",Russian,114,98,0,19,0,0,54,"2019-04-21 10:44:03"
|
"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"
|
Sebastian05067,Spanish,114,133,0,55,28,0,0,"2017-05-14 00:48:16"
|
||||||
mohmans,Arabic,109,103,0,2,0,0,0,"2020-11-23 02:48:00"
|
REMOVED_USER,Arabic,111,106,0,22,22,2,0,"2018-01-05 07:01:45"
|
||||||
"Iabin Arteaga (iabin)",Spanish,108,111,0,4,21,0,0,"2017-08-26 21:08:54"
|
"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,0,0,0,"2021-03-30 05:25:33"
|
"손유정 (yuwon1213)",Korean,95,57,0,0,1,0,0,"2021-03-30 05:25:33"
|
||||||
ranmagen,Hebrew,91,78,0,0,0,0,0,"2021-02-16 05:44:31"
|
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"
|
||||||
"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"
|
ikkaz,Indonesian,89,84,0,5,0,0,4,"2019-09-02 19:58:54"
|
||||||
|
"Vo - (voyl)","Chinese Traditional",89,126,0,0,5,0,0,"2020-09-02 23:34:42"
|
||||||
"Irene K (Heaun)",Korean,88,75,0,25,0,0,0,"2020-03-16 11:31:12"
|
"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,18,"2020-07-04 03:42:09"
|
"Ohad Edri (ohadalte)",Hebrew,85,79,0,0,1,3,13,"2020-07-04 03:42:09"
|
||||||
helectron,Persian,84,102,0,1,0,0,0,"2021-03-02 04:10:51"
|
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,0,0,0,"2020-01-03 11:36:40"
|
"Jacob Roller (jdr28070)",Korean,79,61,0,0,1,0,0,"2020-01-03 11:36:40"
|
||||||
Tiralka,French,79,91,0,92,1,0,0,"2018-02-09 18:39:01"
|
Tiralka,French,79,91,0,92,1,0,0,"2018-02-09 18:39:01"
|
||||||
"Israa Z (sosozozo)",Arabic,79,87,0,43,12,0,3,"2017-11-27 14:10:50"
|
"Toni Mustonen (toni.mustonen)",Finnish,78,72,0,0,5,0,0,"2017-09-02 05:34:12"
|
||||||
"Toni Mustonen (toni.mustonen)",Finnish,78,72,0,0,0,0,0,"2017-09-02 05:34:12"
|
|
||||||
"Fauz Aladeem (topfauz)",Arabic,76,77,0,0,0,1,0,"2020-02-21 22:46:12"
|
|
||||||
"Michael (quelbs)",German,76,75,0,1,0,0,39,"2020-08-18 07:39:26"
|
"Michael (quelbs)",German,76,75,0,1,0,0,39,"2020-08-18 07:39:26"
|
||||||
"Oliver Gronowski (OliverGronowski)",German,70,69,0,5,0,0,0,"2021-05-14 16:37:10"
|
"Fauz Aladeem (topfauz)",Arabic,76,77,0,0,0,1,0,"2020-02-21 22:46:12"
|
||||||
|
"Radoslaw Biernacki (radoslaw.biernacki)",Polish,70,74,0,56,1,0,1,"2020-12-15 17:55:31"
|
||||||
|
"Oliver Gronowski (OliverGronowski)",German,70,69,0,5,2,0,0,"2021-05-14 16:37:10"
|
||||||
RealDonald,Dutch,67,69,0,121,10,0,0,"2017-06-23 20:10:12"
|
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,0,0,0,"2021-06-22 16:37:44"
|
"Nil riera (nilriera2000)",Catalan,52,61,0,1,2,0,0,"2021-06-22 16:37:44"
|
||||||
|
"Neoone (Neooneqq)",Romanian,51,54,0,0,0,0,0,"2022-05-05 20:42:11"
|
||||||
REMOVED_USER,Italian,51,52,0,2,0,0,0,"2017-08-21 05:15:31"
|
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"
|
||||||
"Andrew Firnes (Anechan)",Russian,47,47,0,3,0,0,29,"2019-09-18 09:51:59"
|
"tat bz (Tat_i)",German,48,56,0,55,0,1,27,"2021-03-26 05:12:54"
|
||||||
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"
|
vach,Armenian,47,36,0,0,0,0,0,"2020-04-18 16:53:12"
|
||||||
|
"Andrew Firnes (Anechan)",Russian,47,47,0,3,0,0,29,"2019-09-18 09:51:59"
|
||||||
|
andowero,Czech,47,38,0,0,3,0,0,"2020-01-20 02:29:01"
|
||||||
"Rahul Shishodia (rahul.shishodia.10)",Hindi,46,85,0,6,5,1,0,"2018-12-24 22:18:19"
|
"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,1,1,0,"2017-03-31 09:39:16"
|
"Boban Jagertraum (boban40)",Czech,43,38,0,2,18,1,0,"2017-03-31 09:39:16"
|
||||||
"Kamil Dziadek (prso94)",Polish,43,39,0,0,2,0,0,"2020-04-06 17:12:06"
|
"Kamil Dziadek (prso94)",Polish,43,39,0,0,6,0,0,"2020-04-06 17:12:06"
|
||||||
"Me Me (gentelwom)",Arabic,42,40,0,0,0,0,0,"2020-11-08 20:44:01"
|
|
||||||
"Ali Elsheikh (aelsheikh1987)",Arabic,42,41,0,0,0,0,0,"2021-06-16 10:17:26"
|
|
||||||
"Balázs Keresztury (belidzs)",Hungarian,42,41,501,0,7,0,38,"2017-04-06 02:40:24"
|
|
||||||
andreea.muscalagiu,Romanian,42,52,0,1,0,0,0,"2017-10-22 07:19:49"
|
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"
|
"Me Me (gentelwom)",Arabic,42,40,0,0,0,0,0,"2020-11-08 20:44:01"
|
||||||
MStefanov,Bulgarian,41,55,2,0,2,0,2,"2017-03-31 16:09:02"
|
"Balázs Keresztury (belidzs)",Hungarian,42,41,501,0,7,0,38,"2017-04-06 02:40:24"
|
||||||
"Sofia Veijonen (Suklaa) (sofia.veijonen)",Finnish,40,33,0,0,0,0,0,"2018-03-07 09:24:22"
|
"Mateusz Duda (MateuszDuda)",Polish,42,42,0,0,6,0,0,"2021-08-17 11:27:11"
|
||||||
|
"Ali Elsheikh (aelsheikh1987)",Arabic,42,41,0,0,0,0,0,"2021-06-16 10:17:26"
|
||||||
"Ali Zali (stm19951995)",Persian,40,60,0,0,0,0,0,"2020-03-23 19:57:26"
|
"Ali Zali (stm19951995)",Persian,40,60,0,0,0,0,0,"2020-03-23 19:57:26"
|
||||||
"Limin Lu (liminlu)","Chinese Simplified",39,79,503,0,0,0,39,"2017-03-31 09:49:35"
|
"Sofia Veijonen (Suklaa) (sofia.veijonen)",Finnish,40,33,0,0,0,0,0,"2018-03-07 09:24:22"
|
||||||
dusanstrgar,Slovenian,39,41,0,0,0,0,0,"2017-03-31 10:30:28"
|
dusanstrgar,Slovenian,39,41,0,0,0,0,0,"2017-03-31 10:30:28"
|
||||||
|
"Limin Lu (liminlu)","Chinese Simplified",39,79,503,0,0,0,39,"2017-03-31 09:49:35"
|
||||||
Anshoe,Tamil,38,65,0,14,0,0,0,"2018-01-02 11:06:52"
|
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"
|
||||||
"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"
|
REMOVED_USER,Swedish,36,33,0,5,1,0,0,"2018-09-29 17:47:33"
|
||||||
|
xphsis,Basque,36,31,0,0,0,0,0,"2022-01-02 08:16:19"
|
||||||
|
"Maria Chushnyakova (maria.ch)",Russian,36,31,0,3,0,0,0,"2021-08-17 03:23:58"
|
||||||
"長谷川知里 (chase0213)",Japanese,34,138,0,13,0,0,24,"2018-12-14 10:52:44"
|
"長谷川知里 (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"
|
||||||
"비니몬youtube (khj01025276475)",Korean,29,25,0,0,0,0,0,"2020-02-09 20:44:35"
|
|
||||||
"Ruud Schouten (ruudschouten)",Dutch,29,32,0,41,3,0,0,"2017-07-22 17:49:17"
|
"Ruud Schouten (ruudschouten)",Dutch,29,32,0,41,3,0,0,"2017-07-22 17:49:17"
|
||||||
"Aaron Dalton (Perlkonig)",French,26,25,0,141,1,0,0,"2018-01-14 12:58:19"
|
"비니몬youtube (khj01025276475)",Korean,29,25,0,0,0,0,0,"2020-02-09 20:44:35"
|
||||||
|
avelneve,Indonesian,29,28,0,0,0,0,0,"2022-04-13 13:26:10"
|
||||||
"Niraj Yadav (neverforgetniraj)",Hindi,26,48,0,0,0,0,0,"2017-04-11 02:26:50"
|
"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"
|
"Aaron Dalton (Perlkonig)",French,26,25,0,141,1,0,0,"2018-01-14 12:58:19"
|
||||||
"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"
|
"Jonny I (jonny99dj)",Italian,26,26,0,5,0,0,0,"2017-10-07 07:35:34"
|
||||||
|
"Guillaume Collic (gcollic)",French,26,28,0,126,11,0,0,"2017-05-05 16:13:00"
|
||||||
|
Pan_Filuta,Czech,25,21,0,5,8,0,3,"2017-04-29 12:55:14"
|
||||||
"Eddie (eddieattaboy)","Chinese Traditional",25,34,0,1,0,0,0,"2020-11-04 21:48:05"
|
"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"
|
||||||
"Caner Başaran (basarancaner)",Turkish,23,21,0,0,21,0,0,"2017-04-09 06:34:59"
|
|
||||||
hodanli,Turkish,22,26,0,0,1,0,0,"2017-11-03 14:33:41"
|
|
||||||
gnu-ewm,Polish,22,23,0,6,0,0,0,"2021-02-24 03:42:01"
|
|
||||||
"Neeraj Verma (verma.neeraj.in)",Hindi,22,37,0,0,1,0,0,"2018-07-23 07:16:41"
|
"Neeraj Verma (verma.neeraj.in)",Hindi,22,37,0,0,1,0,0,"2018-07-23 07:16:41"
|
||||||
|
gnu-ewm,Polish,22,23,0,6,2,0,0,"2021-02-24 03:42:01"
|
||||||
|
hodanli,Turkish,22,26,0,0,1,0,0,"2017-11-03 14:33:41"
|
||||||
"Alcarkse (alexis.brusle)",French,21,25,0,7,11,0,0,"2017-08-06 09:32:29"
|
"Alcarkse (alexis.brusle)",French,21,25,0,7,11,0,0,"2017-08-06 09:32:29"
|
||||||
olbotta,Italian,20,25,0,2,0,0,0,"2021-06-06 04:22:55"
|
|
||||||
"Shashwat (goforgold)",Hindi,20,33,0,0,0,0,0,"2020-05-17 10:34:42"
|
"Shashwat (goforgold)",Hindi,20,33,0,0,0,0,0,"2020-05-17 10:34:42"
|
||||||
|
olbotta,Italian,20,25,0,2,0,0,0,"2021-06-06 04:22:55"
|
||||||
|
can13,Turkish,19,14,0,8,0,0,0,"2021-01-03 10:39:03"
|
||||||
|
"사자솥 (toke1597)",Korean,19,19,0,0,0,0,0,"2020-02-04 13:36:11"
|
||||||
|
KenKailer,Arabic,19,25,0,0,0,0,0,"2022-05-10 06:16:54"
|
||||||
|
"İsa Eş (IsaEs)",Turkish,19,17,0,0,6,2,0,"2017-06-20 07:30:22"
|
||||||
"Magdalena Urbańczyk (madziia139)",Polish,19,19,0,0,0,0,0,"2017-10-21 03:01:04"
|
"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"
|
||||||
"Hoon Jung (hooni100)",Korean,17,10,0,0,0,0,0,"2021-01-03 02:26:54"
|
|
||||||
"Ceara Lopez (cealopez)",Spanish,17,18,0,0,5,1,0,"2017-08-22 22:56:13"
|
|
||||||
takoyakibento,Korean,17,13,0,3,0,0,0,"2020-08-01 08:44:15"
|
|
||||||
Adeline31,French,17,20,0,3,0,0,0,"2019-12-06 00:00:11"
|
Adeline31,French,17,20,0,3,0,0,0,"2019-12-06 00:00:11"
|
||||||
engineeringforgood,Russian,16,15,0,0,0,0,16,"2021-01-22 03:32:35"
|
"Hoon Jung (hooni100)",Korean,17,10,0,0,0,0,0,"2021-01-03 02:26:54"
|
||||||
|
takoyakibento,Korean,17,13,0,3,0,0,0,"2020-08-01 08:44:15"
|
||||||
|
"Ceara Lopez (cealopez)",Spanish,17,18,0,0,5,1,0,"2017-08-22 22:56:13"
|
||||||
bretzel15,German,16,20,0,0,0,0,0,"2020-04-06 02:49:14"
|
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"
|
DebatablySane,Bulgarian,16,15,0,48,0,0,0,"2017-07-10 15:13:18"
|
||||||
|
"Şamil Ateşoğlu (m.samilatesoglu)",Turkish,16,22,0,11,6,3,0,"2017-07-05 18:37:08"
|
||||||
|
engineeringforgood,Russian,16,15,0,0,0,0,16,"2021-01-22 03:32:35"
|
||||||
"Bhava Tharini (bhavidanush)",Tamil,15,37,0,0,0,0,0,"2019-10-09 05:43:11"
|
"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"
|
||||||
"Sonu Sharma (riteetude)",Hindi,11,23,0,0,0,0,0,"2021-05-30 19:38:00"
|
|
||||||
Vmrc,French,11,12,0,2,0,0,0,"2020-11-02 05:35:06"
|
|
||||||
"Jo Chuang (josephch405)","Chinese Traditional",11,24,0,0,0,0,11,"2017-06-16 20:21:06"
|
"Jo Chuang (josephch405)","Chinese Traditional",11,24,0,0,0,0,11,"2017-06-16 20:21:06"
|
||||||
"sathvic k (sathvictripleseven)",Telugu,10,17,0,0,0,0,0,"2020-09-11 08:11:32"
|
Vmrc,French,11,12,0,2,0,0,0,"2020-11-02 05:35:06"
|
||||||
"Brian Camacho (bmcamacho)",Polish,10,11,0,0,1,0,0,"2020-08-03 02:27:28"
|
"Ammar Naif (Ammar_Naif)",Arabic,11,11,0,4,0,0,0,"2022-01-15 05:16:41"
|
||||||
"Anonymous edgy nerd (yamentaad)",Arabic,10,13,0,1,0,0,0,"2018-05-06 09:23:57"
|
"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"
|
"Edwin van Rooij (edwinvrooij)",Dutch,10,13,0,17,0,0,0,"2018-11-05 03:59:10"
|
||||||
|
"Brian Camacho (bmcamacho)",Polish,10,11,0,0,1,1,0,"2020-08-03 02:27:28"
|
||||||
"Mihael Wagner (miha.wagner)",Slovenian,10,9,0,7,0,0,0,"2017-10-18 18:26:29"
|
"Mihael Wagner (miha.wagner)",Slovenian,10,9,0,7,0,0,0,"2017-10-18 18:26:29"
|
||||||
|
"Hrant Hakobian (hrastgh1)",Armenian,10,9,0,0,0,0,0,"2021-08-29 15:22:10"
|
||||||
|
"sathvic k (sathvictripleseven)",Telugu,10,17,0,0,0,0,0,"2020-09-11 08:11:32"
|
||||||
"Ahmed Mosaad (ahmed.mosaad2018)",Arabic,10,12,0,6,0,0,0,"2021-02-03 18:45:43"
|
"Ahmed Mosaad (ahmed.mosaad2018)",Arabic,10,12,0,6,0,0,0,"2021-02-03 18:45:43"
|
||||||
|
"Anonymous edgy nerd (yamentaad)",Arabic,10,13,0,1,0,0,0,"2018-05-06 09:23:57"
|
||||||
|
"Zesar Cebrián (Txorrota)",Spanish,10,44,0,0,0,0,0,"2022-02-09 01:34:32"
|
||||||
|
"Milan Siebenbürger (lennyd)",Czech,10,7,0,1,0,0,0,"2022-01-30 07:09:42"
|
||||||
"Suhaili Hassan (kucingsyg96)",Indonesian,9,10,0,0,0,0,0,"2018-06-10 11:55:09"
|
"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"
|
||||||
"Sourire Lucide (sourire_lucide)",Russian,9,10,0,0,1,0,0,"2018-03-22 01:37:55"
|
"Jakob Weickmann (jweickm)",Japanese,8,21,0,0,0,0,0,"2021-10-05 11:10:25"
|
||||||
Rex123,Persian,8,8,0,0,0,0,0,"2017-07-01 00:47:42"
|
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"
|
||||||
"Андрій Козицький (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"
|
ftfoi,Norwegian,7,6,0,0,0,0,0,"2020-04-11 20:42:35"
|
||||||
"Vladimir Pavlychev (KeyJoo)",Russian,7,9,0,0,0,0,0,"2017-12-18 02:46:56"
|
"Vladimir Pavlychev (vovs03)",Russian,7,9,0,0,0,0,0,"2017-12-18 02:46:56"
|
||||||
|
"Felipe Chagas (chagretes)","Portuguese, Brazilian",7,8,0,0,3,0,5,"2022-01-10 12:20:25"
|
||||||
|
"Андрій Козицький (andriikozytskyi3807)",Ukrainian,7,12,0,2,0,0,0,"2020-09-26 20:31:56"
|
||||||
pkorove,Greek,7,7,0,0,0,0,0,"2020-03-07 11:36:12"
|
pkorove,Greek,7,7,0,0,0,0,0,"2020-03-07 11:36:12"
|
||||||
erfan2927,Persian,6,6,0,0,0,0,0,"2018-04-09 02:12:44"
|
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"
|
|
||||||
"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"
|
||||||
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"
|
||||||
"Manuel Tassi (Mannivu)",Italian,5,6,0,0,0,0,0,"2021-01-03 11:00:33"
|
"Burak Ceylan (7burakceylan)",Turkish,6,6,0,0,0,0,0,"2018-05-20 17:24:19"
|
||||||
"Tomáš Hrabáček (Hrabyyy)",Czech,5,3,0,0,0,0,0,"2021-05-27 11:58:11"
|
andriikozytskyi2018,Ukrainian,5,5,0,0,0,0,0,"2017-09-03 05:24:43"
|
||||||
|
"Vitor Henrique (vitorhcl)","Portuguese, Brazilian",5,8,0,1,0,0,0,"2022-03-08 20:00:59"
|
||||||
|
"Matthias Joly (joly.matt12)",French,5,8,0,27,1,0,0,"2017-08-28 09:53:59"
|
||||||
|
"Tomáš Hrabáček (Hrabyyy)",Czech,5,3,0,0,1,0,0,"2021-05-27 11:58:11"
|
||||||
"Guerra Ivaneth (rossanaiva-04)",Spanish,5,7,0,0,0,0,0,"2019-02-03 16:48:59"
|
"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"
|
||||||
andriikozytskyi2018,Ukrainian,5,5,0,0,0,0,0,"2017-09-03 05:24:43"
|
"Manuel Tassi (Mannivu)",Italian,5,6,0,0,0,0,0,"2021-01-03 11:00:33"
|
||||||
marmo,German,4,4,0,0,0,0,0,"2021-01-13 01:16:35"
|
|
||||||
"Eli Besirov (elibesirov07)",Turkish,4,4,0,0,0,0,0,"2019-03-25 07:12:34"
|
|
||||||
"Lopo Isaac Fernández (rocapata)",Spanish,4,3,0,0,0,0,0,"2018-09-20 11:46:22"
|
|
||||||
bziuum,Polish,4,4,0,0,0,0,0,"2020-09-01 09:08:01"
|
|
||||||
"Neko123 (emandic11)","Serbian (Cyrillic)",4,4,0,57,0,0,0,"2021-04-21 15:33:29"
|
"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"
|
"Lopo Isaac Fernández (rocapata)",Spanish,4,3,0,0,0,0,0,"2018-09-20 11:46:22"
|
||||||
"mohammadali barati (mabaraty)",Persian,3,3,0,0,0,0,0,"2021-07-10 05:54:44"
|
"Eli Besirov (elibesirov07)",Turkish,4,4,0,0,0,0,0,"2019-03-25 07:12:34"
|
||||||
|
marmo,German,4,4,0,0,0,0,0,"2021-01-13 01:16:35"
|
||||||
|
bziuum,Polish,4,4,0,0,3,0,0,"2020-09-01 09:08:01"
|
||||||
|
"Craig Foobar (craig.foobar)",German,3,3,0,25,0,0,0,"2022-02-20 16:55:47"
|
||||||
|
Katarin,Ukrainian,3,3,0,0,0,0,0,"2022-03-17 14:44:59"
|
||||||
"Sarath S (CyberShark)",Tamil,3,7,0,0,0,0,0,"2020-08-27 22:43:16"
|
"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"
|
||||||
"Vagner Roberto (vagner.trompete)","Portuguese, Brazilian",3,3,0,0,0,0,0,"2017-12-30 17:54:26"
|
Magidxz,Arabic,3,3,0,0,0,0,0,"2021-01-05 05:02:54"
|
||||||
"Igor Piskun (i_piskun)",Ukrainian,3,3,0,0,0,0,0,"2018-01-19 15:20:27"
|
|
||||||
"Andrea Bianchi (andreawhite1597)",Italian,3,1,0,1,0,0,0,"2018-01-21 17:45:48"
|
|
||||||
"Cláudio Bernardo (claudiobernardo.ti)","Portuguese, Brazilian",3,4,0,1,0,0,0,"2019-01-08 14:41:10"
|
|
||||||
"Hiohana Rilary (hiohanarilary)","Portuguese, Brazilian",3,4,0,0,0,0,0,"2019-07-31 20:42:20"
|
|
||||||
"joabe gabriel (joabegabrielcma1)","Portuguese, Brazilian",3,4,0,0,0,0,0,"2018-08-21 09:08:59"
|
|
||||||
"Péter Bernát (bernatp)",Hungarian,3,2,0,0,0,0,0,"2019-11-30 15:50:33"
|
"Péter Bernát (bernatp)",Hungarian,3,2,0,0,0,0,0,"2019-11-30 15:50:33"
|
||||||
"Martin Zimdahl (zimdahlmartin)",Swedish,3,2,0,0,1,0,3,"2018-09-15 04:39:22"
|
"joabe gabriel (joabegabrielcma1)","Portuguese, Brazilian",3,4,0,0,0,0,0,"2018-08-21 09:08:59"
|
||||||
"Gabriel Cavalcante (gabrielc.alves14)","Portuguese, Brazilian",3,4,0,0,0,0,0,"2018-08-06 22:24:54"
|
"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"
|
atomjani,Hungarian,3,3,0,0,0,0,0,"2019-01-19 00:49:25"
|
||||||
"أم محمد تقي (souadboudia19)",Arabic,2,2,0,0,0,0,0,"2020-06-13 15:24:17"
|
"mohammadali barati (mabaraty)",Persian,3,3,0,0,0,0,0,"2021-07-10 05:54:44"
|
||||||
"FAy FAy (fayfayfay52)","Chinese Traditional",2,5,0,0,0,0,0,"2017-10-06 08:53:21"
|
"Hiohana Rilary (hiohanarilary)","Portuguese, Brazilian",3,4,0,0,0,0,0,"2019-07-31 20:42:20"
|
||||||
chavs1997,Russian,2,2,0,9,0,0,0,"2018-05-18 16:58:19"
|
"Tejaswini Boppana (Tejaswini)",Telugu,3,1,0,0,0,0,0,"2021-08-27 23:48:55"
|
||||||
Soroor_SI,Persian,2,2,0,0,0,0,0,"2018-06-10 06:28:27"
|
"Andrea Bianchi (andreawhite1597)",Italian,3,1,0,1,0,0,0,"2018-01-21 17:45:48"
|
||||||
"Ilyas Fekhar (il47yas)",Arabic,2,2,0,0,0,0,0,"2018-04-17 22:00:41"
|
"Ño Bí Tã (pt614553)",Arabic,2,8,0,1,0,2,0,"2021-05-22 20:41:01"
|
||||||
"hesamiranii (esam.matouri)",Persian,2,2,0,0,0,0,0,"2018-09-22 16:33:36"
|
|
||||||
"fatemeh s (fargolseifoori3)",Persian,2,2,0,0,0,0,0,"2019-01-31 12:06:57"
|
|
||||||
amei,"Portuguese, Brazilian",2,2,0,0,0,0,0,"2018-04-19 19:42:28"
|
|
||||||
"Naveen jai krishna (njsbpolymer1)",Tamil,2,5,0,0,0,0,0,"2020-01-10 14:19:41"
|
|
||||||
"Danial Agh (danialagh)",Persian,2,3,0,0,0,0,0,"2019-03-30 13:24:16"
|
|
||||||
"Walid Baazia (walidbaazia2005)",Arabic,2,1,0,0,0,0,0,"2021-01-27 12:47:34"
|
|
||||||
"Ali Zaida (alizaeda92)",Arabic,2,2,0,0,0,0,0,"2019-12-01 11:47:00"
|
|
||||||
LNDDYL,"Chinese Traditional",2,4,0,0,0,0,2,"2018-04-22 04:00:19"
|
|
||||||
"Ño Bí Tã (pt614553)",Arabic,2,8,0,1,0,0,0,"2021-05-22 20:41:01"
|
|
||||||
"Judith Ayala (Azul1612)",Spanish,2,1,0,0,0,1,0,"2021-05-18 17:07:19"
|
"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"
|
"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"
|
"Balthazar Aubard (Balatzar)",French,2,5,0,0,1,0,0,"2017-09-23 01:42:57"
|
||||||
|
"Ahmed Bazazo (ahmedbazazo)",Arabic,2,2,0,0,0,0,0,"2022-02-19 20:11:09"
|
||||||
|
"Ali Zaida (alizaeda92)",Arabic,2,2,0,0,0,0,0,"2019-12-01 11:47:00"
|
||||||
|
"FAy FAy (fayfayfay52)","Chinese Traditional",2,5,0,0,0,0,0,"2017-10-06 08:53:21"
|
||||||
|
Soroor_SI,Persian,2,2,0,0,0,0,0,"2018-06-10 06:28:27"
|
||||||
|
chavs1997,Russian,2,2,0,9,0,0,0,"2018-05-18 16:58:19"
|
||||||
|
"Naveen jai krishna (njsbpolymer1)",Tamil,2,5,0,0,0,0,0,"2020-01-10 14:19:41"
|
||||||
|
omerfarukbas,Turkish,2,3,0,19,2,0,0,"2017-08-14 16:10:35"
|
||||||
|
"Ilyas Fekhar (il47yas)",Arabic,2,2,0,0,0,0,0,"2018-04-17 22:00:41"
|
||||||
|
"Héctor Mañas García (hectodium)",Catalan,2,3,0,0,0,0,0,"2021-10-02 20:32:09"
|
||||||
|
"Walid Baazia (walidbaazia2005)",Arabic,2,1,0,0,0,0,0,"2021-01-27 12:47:34"
|
||||||
|
"fatemeh s (fargolseifoori3)",Persian,2,2,0,0,0,0,0,"2019-01-31 12:06:57"
|
||||||
|
"hesamiranii (esam.matouri)",Persian,2,2,0,0,0,0,0,"2018-09-22 16:33:36"
|
||||||
|
REMOVED_USER,Ukrainian,2,2,0,0,0,0,0,"2017-06-15 12:24:44"
|
||||||
|
"Alex Stein (diefaust1993)",Russian,2,2,0,4,4,0,2,"2017-07-13 06:56:17"
|
||||||
|
amei,"Portuguese, Brazilian",2,2,0,0,0,0,0,"2018-04-19 19:42:28"
|
||||||
|
"أم محمد تقي (souadboudia19)",Arabic,2,2,0,0,0,0,0,"2020-06-13 15:24:17"
|
||||||
|
LNDDYL,"Chinese Traditional",2,4,0,0,0,0,2,"2018-04-22 04:00:19"
|
||||||
|
"조화정 (yunjoo337)",Korean,2,2,0,0,0,0,0,"2019-06-16 22:25:31"
|
||||||
|
"Sidali Aymen (sidaliaymen950)",Arabic,2,2,0,0,0,0,0,"2022-01-31 18:50:59"
|
||||||
|
"Danial Agh (danialagh)",Persian,2,3,0,0,0,0,0,"2019-03-30 13:24:16"
|
||||||
|
iSoron2,"Portuguese, Brazilian",1,1,0,0,0,0,0,"2017-03-18 17:56:29"
|
||||||
|
"Anton (tT0NG)","Chinese Traditional",1,2,0,0,0,0,1,"2017-07-06 14:18:39"
|
||||||
"Luca Gori (grolcu)",Italian,1,2,0,0,0,0,0,"2020-09-26 23:26:15"
|
"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"
|
||||||
"Anastasiia Bondarenko (nastasya.bondarenko.97)",Russian,1,1,0,0,0,0,0,"2019-06-07 17:43:08"
|
|
||||||
"Kan Black (kanblack.va)",Vietnamese,1,2,0,0,0,1,0,"2019-01-15 03:50:10"
|
|
||||||
"Patrick Pimenta (trickap1)","Portuguese, Brazilian",1,1,0,0,0,0,0,"2018-12-01 14:31:21"
|
"Patrick Pimenta (trickap1)","Portuguese, Brazilian",1,1,0,0,0,0,0,"2018-12-01 14:31:21"
|
||||||
"Dagna Q (dagnaq)",,0,0,0,0,0,0,0,"2017-08-06 01:42:52"
|
"박찌 (perpact20)",Korean,1,1,0,0,0,0,0,"2018-02-10 10:11:44"
|
||||||
Kamalakannan,,0,0,0,0,0,0,0,"2017-05-14 11:40:23"
|
"Kan Black (kanblack.va)",Vietnamese,1,2,0,0,0,1,0,"2019-01-15 03:50:10"
|
||||||
"Éjbãss Übbeî (littlebittlebottle)",Norwegian,0,0,0,152,0,0,0,"2017-07-05 21:12:02"
|
"Anastasiia Bondarenko (nastasya.bondarenko.97)",Russian,1,1,0,0,0,0,0,"2019-06-07 17:43:08"
|
||||||
"Равиль Мифтахов (ravilmif47)",Russian,0,0,0,1,0,0,0,"2019-08-12 21:58:30"
|
"Wibi Cahyo (wbcahyoh)",Indonesian,1,3,0,0,0,0,0,"2017-12-14 06:35:58"
|
||||||
sanyoniket,,0,0,0,0,0,0,0,"2019-07-23 12:58:40"
|
sanyoniket,,0,0,0,0,0,0,0,"2019-07-23 12:58:40"
|
||||||
REMOVED_USER,,0,0,0,0,0,0,0,"2020-02-01 03:47:48"
|
"Sri Harsha Bhogi (sriharshabhogi)",,0,0,0,0,0,0,0,"2018-09-02 05:31:53"
|
||||||
"vi ve (VimalV)",,0,0,0,0,0,0,0,"2021-02-08 02:35:45"
|
Irsgram,Russian,0,0,0,1,0,0,0,"2019-09-30 16:42:20"
|
||||||
"George Merkulov (george142.emarket)",Russian,0,0,0,11,0,0,0,"2019-06-09 19:47:02"
|
"Baran Özavcı (n2141n)",Turkish,0,0,0,1,0,0,0,"2022-02-26 04:32:51"
|
||||||
"Yasin Okumus (lacivert)",Turkish,0,0,0,1,0,0,0,"2018-02-07 04:13:51"
|
"Masataka Yakura (myakura)",Japanese,0,0,0,1,0,0,0,"2021-09-03 22:10:36"
|
||||||
"Petros Bleyan (coolbleyan)",Russian,0,0,0,14,0,0,0,"2017-08-18 18:37:18"
|
|
||||||
"LeMeD (LeMeS)",French,0,0,0,2,0,0,0,"2021-02-06 15:35:00"
|
|
||||||
ava_rfie,Persian,0,0,0,1,0,0,0,"2019-06-09 16:19:24"
|
ava_rfie,Persian,0,0,0,1,0,0,0,"2019-06-09 16:19:24"
|
||||||
"Mateusz Teteruk (mttet)",Polish,0,0,0,1,0,0,0,"2021-01-23 13:09:59"
|
|
||||||
EwanB,,0,0,0,0,0,0,0,"2019-11-19 10:04:38"
|
|
||||||
Fazy1380,,0,0,0,0,0,0,0,"2021-04-10 11:02:53"
|
|
||||||
"Lori Amico (lavodkaclyde2323)",Italian,0,0,0,1,0,0,0,"2017-04-09 10:08:13"
|
|
||||||
"Florian Stuhlmann (stuhlmann)",German,0,0,0,10,0,0,0,"2017-04-15 04:04:00"
|
|
||||||
"عبد الناصر سعيد الثبيتي (asaeed)",,0,0,0,0,0,0,0,"2018-03-13 02:09:35"
|
|
||||||
"Rivo Zängov (Eraser)",,0,0,0,0,0,0,0,"2020-10-13 04:38:26"
|
|
||||||
Hayder21,,0,0,0,0,0,0,0,"2019-12-31 10:56:24"
|
|
||||||
T-v-Gerwen,Dutch,0,0,0,47,0,0,0,"2018-03-02 10:26:33"
|
T-v-Gerwen,Dutch,0,0,0,47,0,0,0,"2018-03-02 10:26:33"
|
||||||
"Eduard Boboc (edi.boboc33)",Romanian,0,0,0,4,0,0,0,"2019-12-16 09:08:39"
|
"George Merkulov (george142.emarket)",Russian,0,0,0,11,0,0,0,"2019-06-09 19:47:02"
|
||||||
|
philfr49,French,0,0,0,2,0,0,0,"2018-09-03 14:20:32"
|
||||||
|
"عبد الناصر سعيد الثبيتي (asaeed)",,0,0,0,0,0,0,0,"2018-03-13 02:09:35"
|
||||||
|
"Thomas Orlita (Thomas995)",Czech,0,0,0,1,0,0,0,"2017-12-24 04:08:27"
|
||||||
|
"Edmunds Edmundam (edmundam)",,0,0,0,0,0,0,0,"2020-06-01 14:18:18"
|
||||||
|
"Elmo (oberknecht)",,0,0,0,0,0,0,0,"2020-04-16 08:45:50"
|
||||||
|
"Равиль Мифтахов (ravilmif47)",Russian,0,0,0,1,0,0,0,"2019-08-12 21:58:30"
|
||||||
|
"Manny Farsangy (manifarsangi)",Persian,0,0,0,12,0,0,0,"2021-08-10 05:32:28"
|
||||||
"Samuel Przeździęk (samek22)",Polish,0,0,0,1,0,0,0,"2021-08-01 00:49:01"
|
"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"
|
"Saiprasath B (Saiprasath)",,0,0,0,0,0,0,0,"2021-07-11 11:10:41"
|
||||||
shuvo786,,0,0,0,0,0,0,0,"2019-11-13 00:18:12"
|
|
||||||
"Edmunds Edmundam (edmundam)",,0,0,0,0,0,0,0,"2020-06-01 14:18:18"
|
|
||||||
Itch,,0,0,0,0,0,0,0,"2017-10-16 09:18:42"
|
|
||||||
"Manny Farsangy (manifarsangi)",Persian,0,0,0,12,0,0,0,"2021-08-10 05:32:28"
|
|
||||||
"Matus Zdansky (matuszdansky)",,0,0,0,0,0,0,0,"2019-10-20 13:52:24"
|
|
||||||
"Thomas Orlita (Thomas995)",Czech,0,0,0,1,0,0,0,"2017-12-24 04:08:27"
|
|
||||||
Irsgram,Russian,0,0,0,1,0,0,0,"2019-09-30 16:42:20"
|
|
||||||
EmanAmini,,0,0,0,0,0,0,0,"2017-03-31 13:27:43"
|
|
||||||
mushin,,0,0,0,0,0,0,0,"2020-02-02 04:08:05"
|
|
||||||
"Elmo (oberknecht)",,0,0,0,0,0,0,0,"2020-04-16 08:45:50"
|
|
||||||
AnggaRifandi,,0,0,0,0,0,0,0,"2017-03-31 19:28:35"
|
|
||||||
"darkkingredian (rediancool)",,0,0,0,0,0,0,0,"2021-07-27 16:04:32"
|
|
||||||
"Sri Harsha Bhogi (sriharshabhogi)",,0,0,0,0,0,0,0,"2018-09-02 05:31:53"
|
|
||||||
"Nat Fomicheva (natac)",Russian,0,0,0,3,0,0,0,"2019-01-25 14:35:02"
|
|
||||||
mdrobulis,,0,0,0,0,0,0,0,"2018-05-24 01:40:42"
|
|
||||||
"Sarah BCNN (fsarahboucenna)",French,0,0,0,16,0,0,0,"2018-02-11 11:07:36"
|
|
||||||
"Arjun K. (arjunkdot)",,0,0,0,0,0,0,0,"2020-09-20 11:16:18"
|
|
||||||
REMOVED_USER,Czech,0,0,0,18,0,0,0,"2018-03-27 06:19:52"
|
|
||||||
martyaberger,,0,0,0,0,0,0,0,"2019-01-01 18:48:08"
|
|
||||||
BongTran,Vietnamese,0,0,0,2,0,0,0,"2018-04-24 05:16:07"
|
|
||||||
"Arttu Ylhävuori (arttu.ylhavuori)",,0,0,0,0,0,0,0,"2019-07-24 15:03:42"
|
|
||||||
"Никита Карамов (nikita.karamoff)",Russian,0,0,0,10,0,0,0,"2018-10-29 03:57:21"
|
|
||||||
rooban23,,0,0,0,0,0,0,0,"2020-09-15 11:49:14"
|
|
||||||
"Eliška Roubalová (roubaeli)",Czech,0,0,0,6,0,0,0,"2019-12-31 12:47:29"
|
|
||||||
valney.faria,"Portuguese, Brazilian",0,0,0,1,0,0,0,"2020-02-02 14:45:02"
|
|
||||||
"Алтынбек Наурызғали (altinbeknaurizgali)",Russian,0,0,0,1,0,0,0,"2020-08-12 13:03:49"
|
|
||||||
REMOVED_USER,,0,0,0,0,0,0,0,"2018-10-27 15:34:36"
|
|
||||||
REMOVED_USER,,0,0,0,0,0,0,0,"2018-08-24 00:17:43"
|
REMOVED_USER,,0,0,0,0,0,0,0,"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"
|
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"
|
"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"
|
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"
|
"akmal shafiq (mohdakmalshafiq)",,0,0,0,0,0,0,0,"2021-11-01 01:04:50"
|
||||||
philfr49,French,0,0,0,2,0,0,0,"2018-09-03 14:20:32"
|
"Sylwuskak (sylwuskak)",Polish,0,0,0,1,0,0,0,"2022-01-25 04:19:53"
|
||||||
"Ahmed Nazir (ahmednazir333)",,0,0,0,0,0,0,0,"2018-05-06 12:10:27"
|
"Yunsu Kim (yunsukim86)",Korean,0,0,0,2,0,0,0,"2022-01-14 06:33:43"
|
||||||
"Balaji Jayaraman (jkbalaji1103)",,0,0,0,0,0,0,0,"2017-10-30 22:12:27"
|
|
||||||
"Wellington Ribeiro (wellington.rib)",,0,0,0,0,0,0,0,"2017-11-16 07:32:25"
|
|
||||||
"Javid IRAN (twitteriran98)",Persian,0,0,0,1,0,0,0,"2017-11-25 16:47:25"
|
|
||||||
"박인호 (wphestiraid)",Korean,0,0,0,2,0,0,0,"2018-01-05 00:33:14"
|
|
||||||
"Pumpith Ungsupanit (pumpithu)",,0,0,0,0,0,0,0,"2019-01-19 23:47:57"
|
"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"
|
"Nat Fomicheva (natac)",Russian,0,0,0,3,0,0,0,"2019-01-25 14:35:02"
|
||||||
"Quentin Hibon (hiq)",,0,0,0,0,0,0,0,"2021-02-07 16:39:31"
|
HemanthMeda,Telugu,0,0,0,4,0,0,0,"2021-12-01 14:02:14"
|
||||||
AhmedDz,Arabic,0,0,0,1,0,0,0,"2017-12-31 10:12:31"
|
"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"
|
"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"
|
REMOVED_USER,,0,0,0,0,0,0,0,"2018-01-05 16:56:12"
|
||||||
NairaDNV,Spanish,0,0,0,9,0,0,0,"2018-01-05 19:10:33"
|
"Dagna Q (dagnaq)",,0,0,0,0,0,0,0,"2017-08-06 01:42:52"
|
||||||
|
Sandhu564.,,0,0,0,0,0,0,0,"2020-12-14 01:27:45"
|
||||||
|
AhmedDz,Arabic,0,0,0,1,0,0,0,"2017-12-31 10:12:31"
|
||||||
|
"Quentin Hibon (hiq)",,0,0,0,0,0,0,0,"2021-02-07 16:39:31"
|
||||||
|
"Ahmed Nazir (ahmednazir333)",,0,0,0,0,0,0,0,"2018-05-06 12:10:27"
|
||||||
|
"박인호 (wphestiraid)",Korean,0,0,0,2,0,0,0,"2018-01-05 00:33:14"
|
||||||
Raulbertassi,,0,0,0,0,0,0,0,"2018-01-07 17:23:18"
|
Raulbertassi,,0,0,0,0,0,0,0,"2018-01-07 17:23:18"
|
||||||
"Карлен Шаухаев (KarlenShaukhaev)",,0,0,0,0,0,0,0,"2020-04-27 08:53:49"
|
"Javid IRAN (twitteriran98)",Persian,0,0,0,1,0,0,0,"2017-11-25 16:47:25"
|
||||||
|
"Wellington Ribeiro (wellington.rib)",,0,0,0,0,0,0,0,"2017-11-16 07:32:25"
|
||||||
dimateos,,0,0,0,0,0,0,0,"2021-01-10 06:29:52"
|
dimateos,,0,0,0,0,0,0,0,"2021-01-10 06:29:52"
|
||||||
"Katherine Alexandra Flórez Ramírez (katherine.florez12)",Spanish,0,0,0,46,0,0,0,"2018-01-20 02:18:32"
|
"Balaji Jayaraman (jkbalaji1103)",,0,0,0,0,0,0,0,"2017-10-30 22:12:27"
|
||||||
"reza golestanzadeh (reza.golestanzadeh)",Persian,0,0,0,1,0,0,0,"2020-10-21 12:07:20"
|
"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"
|
"Muhammet Furkan ALMACI (furkan.almaci)",Turkish,0,0,0,1,0,0,0,"2017-10-29 13:44:56"
|
||||||
|
dongchen.yue,German,0,0,0,4,0,0,0,"2020-09-12 15:05:59"
|
||||||
|
"Алтынбек Наурызғали (altinbeknaurizgali)",Russian,0,0,0,1,0,0,0,"2020-08-12 13:03:49"
|
||||||
|
rooban23,,0,0,0,0,0,0,0,"2020-09-15 11:49:14"
|
||||||
|
NairaDNV,Spanish,0,0,0,9,0,0,0,"2018-01-05 19:10:33"
|
||||||
|
"Katherine Alexandra Flórez Ramírez (katherine.florez12)",Spanish,0,0,0,46,0,0,0,"2018-01-20 02:18:32"
|
||||||
|
Itch,,0,0,0,0,0,0,0,"2017-10-16 09:18:42"
|
||||||
|
"Yasin Okumus (lacivert)",Turkish,0,0,0,1,0,0,0,"2018-02-07 04:13:51"
|
||||||
|
"Eduard Boboc (edi.boboc33)",Romanian,0,0,0,4,0,0,0,"2019-12-16 09:08:39"
|
||||||
|
Hayder21,,0,0,0,0,0,0,0,"2019-12-31 10:56:24"
|
||||||
|
"Eliška Roubalová (roubaeli)",Czech,0,0,0,6,0,0,0,"2019-12-31 12:47:29"
|
||||||
|
Fazy1380,,0,0,0,0,0,0,0,"2021-04-10 11:02:53"
|
||||||
|
"Arttu Ylhävuori (arttu.ylhavuori)",,0,0,0,0,0,0,0,"2019-07-24 15:03:42"
|
||||||
|
EmanAmini,,0,0,0,0,0,0,0,"2017-03-31 13:27:43"
|
||||||
|
AnggaRifandi,,0,0,0,0,0,0,0,"2017-03-31 19:28:35"
|
||||||
|
"Lori Amico (lavodkaclyde2323)",Italian,0,0,0,1,0,0,0,"2017-04-09 10:08:13"
|
||||||
|
"Florian Stuhlmann (stuhlmann)",German,0,0,0,10,0,0,0,"2017-04-15 04:04:00"
|
||||||
|
Kamalakannan,,0,0,0,0,0,0,0,"2017-05-14 11:40:23"
|
||||||
|
farbod66,Persian,0,0,0,1,0,0,0,"2018-01-20 11:04:23"
|
||||||
|
"vi ve (VimalV)",,0,0,0,0,0,0,0,"2021-02-08 02:35:45"
|
||||||
|
"Éjbãss Übbeî (littlebittlebottle)",Norwegian,0,0,0,152,0,0,0,"2017-07-05 21:12:02"
|
||||||
|
"LeMeD (LeMeS)",French,0,0,0,2,0,0,0,"2021-02-06 15:35:00"
|
||||||
|
BongTran,Vietnamese,0,0,0,2,0,0,0,"2018-04-24 05:16:07"
|
||||||
|
REMOVED_USER,Czech,0,0,0,18,0,0,0,"2018-03-27 06:19:52"
|
||||||
|
mushin,,0,0,0,0,0,0,0,"2020-02-02 04:08:05"
|
||||||
|
"Mateusz Teteruk (mttet)",Polish,0,0,0,1,0,0,0,"2021-01-23 13:09:59"
|
||||||
|
"Sarah BCNN (fsarahboucenna)",French,0,0,0,16,0,0,0,"2018-02-11 11:07:36"
|
||||||
|
droidahmed,Arabic,0,0,0,7,0,0,0,"2018-01-31 02:18:49"
|
||||||
|
"Никита Карамов (nikita.karamoff)",Russian,0,0,0,10,0,0,0,"2018-10-29 03:57:21"
|
||||||
|
|||||||
|
@@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
*
|
*
|
||||||
@@ -20,7 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.github.triplet.play") version "3.2.0"
|
id("com.github.triplet.play") version "3.7.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")
|
||||||
@@ -34,15 +32,15 @@ tasks.compileLint {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
||||||
compileSdkVersion(30)
|
compileSdk = 31
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
versionCode(20003)
|
versionCode = 20100
|
||||||
versionName("2.0.3")
|
versionName = "2.1.0"
|
||||||
minSdkVersion(23)
|
minSdk = 23
|
||||||
targetSdkVersion(30)
|
targetSdk = 31
|
||||||
applicationId("org.isoron.uhabits")
|
applicationId = "org.isoron.uhabits"
|
||||||
testInstrumentationRunner("androidx.test.runner.AndroidJUnitRunner")
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
@@ -58,7 +56,7 @@ android {
|
|||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
getByName("release") {
|
getByName("release") {
|
||||||
minifyEnabled(true)
|
isMinifyEnabled = 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")
|
||||||
@@ -70,7 +68,7 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lintOptions {
|
lint {
|
||||||
isCheckReleaseBuilds = false
|
isCheckReleaseBuilds = false
|
||||||
isAbortOnError = false
|
isAbortOnError = false
|
||||||
disable("GoogleAppIndexingWarning")
|
disable("GoogleAppIndexingWarning")
|
||||||
@@ -88,11 +86,11 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
val daggerVersion = "2.34.1"
|
val daggerVersion = "2.41"
|
||||||
val kotlinVersion = "1.4.32"
|
val kotlinVersion = "1.6.21"
|
||||||
val kxCoroutinesVersion = "1.4.2"
|
val kxCoroutinesVersion = "1.6.1"
|
||||||
val ktorVersion = "1.5.3"
|
val ktorVersion = "1.6.8"
|
||||||
val espressoVersion = "3.3.0"
|
val espressoVersion = "3.4.0"
|
||||||
|
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion")
|
androidTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion")
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:$espressoVersion")
|
androidTestImplementation("androidx.test.espresso:espresso-core:$espressoVersion")
|
||||||
@@ -100,17 +98,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.2.0")
|
androidTestImplementation("androidx.annotation:annotation:1.3.0")
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.2")
|
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.3.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.paolorotolo:appintro:3.4.0")
|
implementation("com.github.AppIntro:AppIntro:6.2.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:30.1.1-android")
|
implementation("com.google.guava:guava:31.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")
|
||||||
@@ -118,11 +116,11 @@ 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.2.0")
|
implementation("androidx.appcompat:appcompat:1.4.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.3.0")
|
implementation("com.google.android.material:material:1.5.0")
|
||||||
implementation("com.opencsv:opencsv:5.4")
|
implementation("com.opencsv:opencsv:5.6")
|
||||||
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")
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 22 KiB |
@@ -23,10 +23,10 @@ import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
|
|||||||
import org.isoron.uhabits.core.models.Frequency
|
import org.isoron.uhabits.core.models.Frequency
|
||||||
import org.isoron.uhabits.core.models.Frequency.Companion.DAILY
|
import org.isoron.uhabits.core.models.Frequency.Companion.DAILY
|
||||||
import org.isoron.uhabits.core.models.Habit
|
import org.isoron.uhabits.core.models.Habit
|
||||||
import org.isoron.uhabits.core.models.Habit.Companion.AT_LEAST
|
|
||||||
import org.isoron.uhabits.core.models.Habit.Companion.NUMBER_HABIT
|
|
||||||
import org.isoron.uhabits.core.models.HabitList
|
import org.isoron.uhabits.core.models.HabitList
|
||||||
|
import org.isoron.uhabits.core.models.HabitType
|
||||||
import org.isoron.uhabits.core.models.ModelFactory
|
import org.isoron.uhabits.core.models.ModelFactory
|
||||||
|
import org.isoron.uhabits.core.models.NumericalHabitType
|
||||||
import org.isoron.uhabits.core.models.PaletteColor
|
import org.isoron.uhabits.core.models.PaletteColor
|
||||||
import org.isoron.uhabits.core.models.Timestamp
|
import org.isoron.uhabits.core.models.Timestamp
|
||||||
import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday
|
||||||
@@ -102,8 +102,8 @@ class HabitFixtures(private val modelFactory: ModelFactory, private val habitLis
|
|||||||
val habit = modelFactory.buildHabit().apply {
|
val habit = modelFactory.buildHabit().apply {
|
||||||
name = "Read"
|
name = "Read"
|
||||||
question = "How many pages did you walk today?"
|
question = "How many pages did you walk today?"
|
||||||
type = NUMBER_HABIT
|
type = HabitType.NUMERICAL
|
||||||
targetType = AT_LEAST
|
targetType = NumericalHabitType.AT_LEAST
|
||||||
targetValue = 200.0
|
targetValue = 200.0
|
||||||
unit = "pages"
|
unit = "pages"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ 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() {
|
||||||
@@ -180,6 +181,8 @@ 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%")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,6 +197,8 @@ 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")
|
||||||
|
|||||||
@@ -20,6 +20,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.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
|
||||||
@@ -39,7 +40,7 @@ fun exportFullBackup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun clearDownloadFolder() {
|
fun clearDownloadFolder() {
|
||||||
device.executeShellCommand("rm -rf /sdcard/Download/")
|
device.executeShellCommand("rm -rf /sdcard/Download")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearBackupFolder() {
|
fun clearBackupFolder() {
|
||||||
@@ -86,6 +87,7 @@ 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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
*/
|
*/
|
||||||
package org.isoron.uhabits.acceptance.steps
|
package org.isoron.uhabits.acceptance.steps
|
||||||
|
|
||||||
import android.os.Build.VERSION
|
import android.os.Build
|
||||||
|
import android.os.Build.VERSION.SDK_INT
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.test.espresso.Espresso
|
import androidx.test.espresso.Espresso
|
||||||
@@ -133,7 +134,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 (VERSION.SDK_INT <= 23) {
|
if (SDK_INT <= Build.VERSION_CODES.M) {
|
||||||
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))
|
||||||
@@ -178,6 +179,22 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,24 @@ 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)))
|
||||||
|
|||||||
@@ -120,6 +120,12 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.isoron.uhabits.acceptance.steps
|
package org.isoron.uhabits.acceptance.steps
|
||||||
|
|
||||||
import android.os.Build.VERSION
|
import android.os.Build.VERSION.SDK_INT
|
||||||
import androidx.test.uiautomator.UiScrollable
|
import androidx.test.uiautomator.UiScrollable
|
||||||
import androidx.test.uiautomator.UiSelector
|
import androidx.test.uiautomator.UiSelector
|
||||||
import junit.framework.Assert.assertFalse
|
import junit.framework.Assert.assertFalse
|
||||||
@@ -50,29 +50,21 @@ 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
|
||||||
if (VERSION.SDK_INT <= 21) {
|
val listId = "com.android.launcher3:id/widgets_list_view"
|
||||||
BaseUserInterfaceTest.device.pressHome()
|
BaseUserInterfaceTest.device.pressHome()
|
||||||
BaseUserInterfaceTest.device.waitForIdle()
|
BaseUserInterfaceTest.device.waitForIdle()
|
||||||
BaseUserInterfaceTest.device.findObject(UiSelector().description("Apps")).click()
|
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("Widgets")).click()
|
if (!button.waitForExists(1000)) {
|
||||||
} else {
|
button = BaseUserInterfaceTest.device.findObject(UiSelector().text("Widgets"))
|
||||||
val listId = "com.android.launcher3:id/widgets_list_view"
|
|
||||||
BaseUserInterfaceTest.device.pressHome()
|
|
||||||
BaseUserInterfaceTest.device.waitForIdle()
|
|
||||||
BaseUserInterfaceTest.device.drag(w / 2, h / 2, w / 2, h / 2, 8)
|
|
||||||
var button = BaseUserInterfaceTest.device.findObject(UiSelector().text("WIDGETS"))
|
|
||||||
if (!button.waitForExists(1000)) {
|
|
||||||
button = BaseUserInterfaceTest.device.findObject(UiSelector().text("Widgets"))
|
|
||||||
}
|
|
||||||
button.click()
|
|
||||||
if (VERSION.SDK_INT >= 28) {
|
|
||||||
UiScrollable(UiSelector().resourceId(listId))
|
|
||||||
.scrollForward()
|
|
||||||
}
|
|
||||||
UiScrollable(UiSelector().resourceId(listId))
|
|
||||||
.scrollIntoView(UiSelector().text("Checkmark"))
|
|
||||||
}
|
}
|
||||||
|
button.click()
|
||||||
|
if (SDK_INT >= 28) {
|
||||||
|
UiScrollable(UiSelector().resourceId(listId))
|
||||||
|
.scrollForward()
|
||||||
|
}
|
||||||
|
UiScrollable(UiSelector().resourceId(listId))
|
||||||
|
.scrollIntoView(UiSelector().text("Checkmark"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ 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)
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ 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() {
|
||||||
@@ -43,7 +44,8 @@ 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))
|
||||||
}
|
}
|
||||||
@@ -70,20 +72,28 @@ class EntryButtonViewTest : BaseViewTest() {
|
|||||||
fun testClick_withShortToggleDisabled() {
|
fun testClick_withShortToggleDisabled() {
|
||||||
prefs.isShortToggleEnabled = false
|
prefs.isShortToggleEnabled = false
|
||||||
view.performClick()
|
view.performClick()
|
||||||
assertFalse(toggled)
|
assertTrue(!toggled and edited)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testClick_withShortToggleEnabled() {
|
fun testClick_withShortToggleEnabled() {
|
||||||
prefs.isShortToggleEnabled = true
|
prefs.isShortToggleEnabled = true
|
||||||
view.performClick()
|
view.performClick()
|
||||||
assertTrue(toggled)
|
assertTrue(toggled and !edited)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testLongClick() {
|
fun testLongClick_withShortToggleDisabled() {
|
||||||
|
prefs.isShortToggleEnabled = false
|
||||||
view.performLongClick()
|
view.performLongClick()
|
||||||
assertTrue(toggled)
|
assertTrue(toggled and !edited)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testLongClick_withShortToggleEnabled() {
|
||||||
|
prefs.isShortToggleEnabled = true
|
||||||
|
view.performLongClick()
|
||||||
|
assertTrue(!toggled and edited)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assertRendersCheckedExplicitly() {
|
private fun assertRendersCheckedExplicitly() {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ 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
|
||||||
@@ -42,6 +43,7 @@ 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 }
|
||||||
@@ -74,10 +76,10 @@ class NumberButtonViewTest : BaseViewTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testRender_emptyUnits() {
|
fun testRender_atMostAboveThreshold() {
|
||||||
view.value = 500.0
|
view.value = 500.0
|
||||||
view.units = ""
|
view.targetType = NumericalHabitType.AT_MOST
|
||||||
assertRenders(view, "$PATH/render_unitless.png")
|
assertRenders(view, "$PATH/render_at_most_above.png")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -86,6 +88,13 @@ 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
|
||||||
@@ -93,15 +102,21 @@ class NumberButtonViewTest : BaseViewTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testClick_shortToggleDisabled() {
|
fun testRender_atMostBelowThreshold() {
|
||||||
prefs.isShortToggleEnabled = false
|
view.value = 0.0
|
||||||
view.performClick()
|
view.targetType = NumericalHabitType.AT_MOST
|
||||||
assertFalse(edited)
|
assertRenders(view, "$PATH/render_at_most_below.png")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testClick_shortToggleEnabled() {
|
fun testRender_emptyUnits() {
|
||||||
prefs.isShortToggleEnabled = true
|
view.value = 500.0
|
||||||
|
view.units = ""
|
||||||
|
assertRenders(view, "$PATH/render_unitless.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testClick() {
|
||||||
view.performClick()
|
view.performClick()
|
||||||
assertTrue(edited)
|
assertTrue(edited)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ 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
|
||||||
@@ -55,6 +56,7 @@ 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()
|
||||||
@@ -74,7 +76,7 @@ class NumberPanelViewTest : BaseViewTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun testEdit() {
|
fun testEdit() {
|
||||||
val timestamps = mutableListOf<Timestamp>()
|
val timestamps = mutableListOf<Timestamp>()
|
||||||
view.onEdit = { timestamps.plusAssign(it) }
|
view.onEdit = { t -> timestamps.plusAssign(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()
|
||||||
@@ -85,7 +87,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 = { timestamps += it }
|
view.onEdit = { 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()
|
||||||
|
|||||||
@@ -53,8 +53,6 @@ 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(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -21,9 +21,12 @@ 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
|
||||||
@@ -37,9 +40,12 @@ import org.isoron.uhabits.acceptance.steps.EditHabitSteps.clickSave
|
|||||||
import org.isoron.uhabits.acceptance.steps.EditHabitSteps.typeName
|
import org.isoron.uhabits.acceptance.steps.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
|
||||||
|
|
||||||
@@ -83,4 +89,37 @@ 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",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,14 +24,15 @@ 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.`is`
|
|
||||||
import org.hamcrest.CoreMatchers.equalTo
|
import org.hamcrest.CoreMatchers.equalTo
|
||||||
|
import org.hamcrest.CoreMatchers.`is`
|
||||||
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
|
||||||
@@ -42,10 +43,12 @@ 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 val today = getTodayWithOffset()
|
private lateinit var today: Timestamp
|
||||||
|
|
||||||
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()
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ 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() {
|
||||||
|
|||||||
@@ -17,9 +17,10 @@
|
|||||||
~ 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" />
|
||||||
|
|
||||||
@@ -48,11 +49,11 @@
|
|||||||
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" />
|
||||||
</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">
|
||||||
@@ -85,6 +86,7 @@
|
|||||||
|
|
||||||
<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" />
|
||||||
@@ -93,6 +95,7 @@
|
|||||||
|
|
||||||
<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" />
|
||||||
@@ -101,6 +104,7 @@
|
|||||||
|
|
||||||
<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" />
|
||||||
@@ -115,26 +119,16 @@
|
|||||||
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" />
|
||||||
@@ -152,6 +146,7 @@
|
|||||||
|
|
||||||
<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" />
|
||||||
@@ -164,6 +159,7 @@
|
|||||||
|
|
||||||
<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" />
|
||||||
@@ -176,6 +172,7 @@
|
|||||||
|
|
||||||
<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" />
|
||||||
@@ -188,6 +185,7 @@
|
|||||||
|
|
||||||
<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" />
|
||||||
@@ -200,6 +198,7 @@
|
|||||||
|
|
||||||
<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" />
|
||||||
@@ -210,13 +209,17 @@
|
|||||||
android:resource="@xml/widget_target_info" />
|
android:resource="@xml/widget_target_info" />
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<receiver android:name=".receivers.ReminderReceiver">
|
<receiver
|
||||||
|
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" />
|
||||||
@@ -267,7 +270,7 @@
|
|||||||
<!-- Locale/Tasker -->
|
<!-- Locale/Tasker -->
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".automation.FireSettingReceiver"
|
android:name=".automation.FireSettingReceiver"
|
||||||
android:exported="true">
|
android:exported="false">
|
||||||
<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>
|
||||||
|
|||||||
@@ -49,23 +49,12 @@ 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 {
|
||||||
val x: Float
|
return handleClick(e, true)
|
||||||
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?) = Unit
|
override fun onLongPress(e: MotionEvent?) {
|
||||||
|
handleClick(e)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onScroll(
|
override fun onScroll(
|
||||||
e1: MotionEvent?,
|
e1: MotionEvent?,
|
||||||
@@ -137,4 +126,22 @@ class AndroidDataView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleClick(e: MotionEvent?, isSingleTap: Boolean = false): Boolean {
|
||||||
|
val x: Float
|
||||||
|
val y: Float
|
||||||
|
try {
|
||||||
|
val pointerId = e!!.getPointerId(0)
|
||||||
|
x = e.getX(pointerId)
|
||||||
|
y = e.getY(pointerId)
|
||||||
|
} catch (ex: RuntimeException) {
|
||||||
|
// Android often throws IllegalArgumentException here. Apparently,
|
||||||
|
// the pointer id may become invalid shortly after calling
|
||||||
|
// e.getPointerId.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (isSingleTap) view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity)
|
||||||
|
else view?.onLongClick(x / canvas.innerDensity, y / canvas.innerDensity)
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
/*
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,10 @@
|
|||||||
*/
|
*/
|
||||||
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
|
||||||
|
|
||||||
@@ -32,4 +35,10 @@ class ColorPickerDialog : ColorPickerDialog() {
|
|||||||
callback.onColorPicked(pc)
|
callback.onColorPicked(pc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val dialog = super.onCreateDialog(savedInstanceState)
|
||||||
|
dialog.dismissCurrent()
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ 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
|
||||||
|
|
||||||
@@ -45,5 +46,7 @@ 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ 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
|
||||||
@@ -32,6 +31,7 @@ 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(activity!!)
|
val inflater = LayoutInflater.from(requireActivity())
|
||||||
contentView = inflater.inflate(R.layout.frequency_picker_dialog, null)
|
contentView = inflater.inflate(R.layout.frequency_picker_dialog, null)
|
||||||
|
|
||||||
addBeforeAfterText(
|
addBeforeAfterText(
|
||||||
@@ -62,15 +62,19 @@ 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
|
||||||
focus(everyXDaysTextView)
|
selectInputField(everyXDaysTextView)
|
||||||
}
|
}
|
||||||
|
|
||||||
contentView.everyXDaysTextView.setOnFocusChangeListener { v, hasFocus ->
|
contentView.everyXDaysTextView.setOnFocusChangeListener { v, hasFocus ->
|
||||||
@@ -79,7 +83,7 @@ class FrequencyPickerDialog(
|
|||||||
|
|
||||||
contentView.xTimesPerWeekRadioButton.setOnClickListener {
|
contentView.xTimesPerWeekRadioButton.setOnClickListener {
|
||||||
check(contentView.xTimesPerWeekRadioButton)
|
check(contentView.xTimesPerWeekRadioButton)
|
||||||
focus(contentView.xTimesPerWeekTextView)
|
selectInputField(contentView.xTimesPerWeekTextView)
|
||||||
}
|
}
|
||||||
|
|
||||||
contentView.xTimesPerWeekTextView.setOnFocusChangeListener { v, hasFocus ->
|
contentView.xTimesPerWeekTextView.setOnFocusChangeListener { v, hasFocus ->
|
||||||
@@ -88,17 +92,32 @@ class FrequencyPickerDialog(
|
|||||||
|
|
||||||
contentView.xTimesPerMonthRadioButton.setOnClickListener {
|
contentView.xTimesPerMonthRadioButton.setOnClickListener {
|
||||||
check(contentView.xTimesPerMonthRadioButton)
|
check(contentView.xTimesPerMonthRadioButton)
|
||||||
focus(contentView.xTimesPerMonthTextView)
|
selectInputField(contentView.xTimesPerMonthTextView)
|
||||||
}
|
}
|
||||||
|
|
||||||
contentView.xTimesPerMonthTextView.setOnFocusChangeListener { v, hasFocus ->
|
contentView.xTimesPerMonthTextView.setOnFocusChangeListener { v, hasFocus ->
|
||||||
if (hasFocus) check(contentView.xTimesPerMonthRadioButton)
|
if (hasFocus) check(contentView.xTimesPerMonthRadioButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
return AlertDialog.Builder(activity!!)
|
contentView.xTimesPerYDaysRadioButton.setOnClickListener {
|
||||||
|
check(contentView.xTimesPerYDaysRadioButton)
|
||||||
|
selectInputField(contentView.xTimesPerYDaysXTextView)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentView.xTimesPerYDaysXTextView.setOnFocusChangeListener { v, hasFocus ->
|
||||||
|
if (hasFocus) check(contentView.xTimesPerYDaysRadioButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentView.xTimesPerYDaysYTextView.setOnFocusChangeListener { v, hasFocus ->
|
||||||
|
if (hasFocus) check(contentView.xTimesPerYDaysRadioButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
@@ -106,12 +125,11 @@ class FrequencyPickerDialog(
|
|||||||
container: LinearLayout
|
container: LinearLayout
|
||||||
) {
|
) {
|
||||||
val parts = str.split("%d")
|
val parts = str.split("%d")
|
||||||
container.addView(
|
for (i in parts.indices) {
|
||||||
TextView(activity).apply { text = parts[0].trim() }, 1,
|
container.addView(
|
||||||
)
|
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() {
|
||||||
@@ -132,6 +150,12 @@ 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())
|
||||||
@@ -147,10 +171,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() {
|
||||||
@@ -163,7 +187,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())
|
||||||
focus(contentView.xTimesPerMonthTextView)
|
selectInputField(contentView.xTimesPerMonthTextView)
|
||||||
} else {
|
} else {
|
||||||
if (freqNumerator == 1) {
|
if (freqNumerator == 1) {
|
||||||
if (freqDenominator == 1) {
|
if (freqDenominator == 1) {
|
||||||
@@ -171,23 +195,23 @@ class FrequencyPickerDialog(
|
|||||||
} else {
|
} else {
|
||||||
contentView.everyXDaysRadioButton.isChecked = true
|
contentView.everyXDaysRadioButton.isChecked = true
|
||||||
contentView.everyXDaysTextView.setText(freqDenominator.toString())
|
contentView.everyXDaysTextView.setText(freqDenominator.toString())
|
||||||
focus(contentView.everyXDaysTextView)
|
selectInputField(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())
|
||||||
focus(contentView.xTimesPerWeekTextView)
|
selectInputField(contentView.xTimesPerWeekTextView)
|
||||||
} else {
|
} else {
|
||||||
Log.w("FrequencyPickerDialog", "Unknown frequency: $freqNumerator/$freqDenominator")
|
contentView.xTimesPerYDaysRadioButton.isChecked = true
|
||||||
contentView.everyDayRadioButton.isChecked = true
|
contentView.xTimesPerYDaysXTextView.setText(freqNumerator.toString())
|
||||||
|
contentView.xTimesPerYDaysYTextView.setText(freqDenominator.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun focus(view: EditText) {
|
private fun selectInputField(view: EditText) {
|
||||||
view.requestFocus()
|
|
||||||
view.setSelection(view.text.length)
|
view.setSelection(view.text.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,11 +220,6 @@ 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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
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
|
||||||
@@ -43,12 +44,13 @@ 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
|
||||||
private lateinit var dataView: AndroidDataView
|
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 = (activity!!.application as HabitsApplication).component
|
||||||
commandRunner = component.commandRunner
|
commandRunner = component.commandRunner
|
||||||
habit = component.habitList.getById(arguments!!.getLong("habit"))!!
|
habit = component.habitList.getById(arguments!!.getLong("habit"))!!
|
||||||
@@ -62,20 +64,30 @@ 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 ?: OnDateClickedListener { },
|
onDateClickedListener = onDateClickedListener ?: object : OnDateClickedListener {},
|
||||||
padding = 10.0,
|
padding = 10.0,
|
||||||
)
|
)
|
||||||
dataView = AndroidDataView(context!!, null)
|
dataView = AndroidDataView(context!!, null)
|
||||||
dataView.view = chart!!
|
dataView.view = chart!!
|
||||||
|
|
||||||
return Dialog(context!!).apply {
|
val dialog = 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() {
|
||||||
@@ -101,10 +113,22 @@ 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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.activities.common.dialogs
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.text.InputFilter
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE
|
|
||||||
import android.view.inputmethod.EditorInfo
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.NumberPicker
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import org.isoron.uhabits.R
|
|
||||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
|
|
||||||
import org.isoron.uhabits.inject.ActivityContext
|
|
||||||
import org.isoron.uhabits.utils.InterfaceUtils
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.math.roundToLong
|
|
||||||
|
|
||||||
class NumberPickerFactory
|
|
||||||
@Inject constructor(
|
|
||||||
@ActivityContext private val context: Context
|
|
||||||
) {
|
|
||||||
fun create(
|
|
||||||
value: Double,
|
|
||||||
unit: String,
|
|
||||||
callback: ListHabitsBehavior.NumberPickerCallback
|
|
||||||
): AlertDialog {
|
|
||||||
|
|
||||||
val inflater = LayoutInflater.from(context)
|
|
||||||
val view = inflater.inflate(R.layout.number_picker_dialog, null)
|
|
||||||
|
|
||||||
val picker = view.findViewById<NumberPicker>(R.id.picker)
|
|
||||||
val picker2 = view.findViewById<NumberPicker>(R.id.picker2)
|
|
||||||
val tvUnit = view.findViewById<TextView>(R.id.tvUnit)
|
|
||||||
|
|
||||||
val intValue = (value * 100).roundToLong().toInt()
|
|
||||||
|
|
||||||
picker.minValue = 0
|
|
||||||
picker.maxValue = Integer.MAX_VALUE / 100
|
|
||||||
picker.value = intValue / 100
|
|
||||||
picker.wrapSelectorWheel = false
|
|
||||||
|
|
||||||
picker2.minValue = 0
|
|
||||||
picker2.maxValue = 19
|
|
||||||
picker2.setFormatter { v -> String.format("%02d", 5 * v) }
|
|
||||||
picker2.value = intValue % 100 / 5
|
|
||||||
refreshInitialValue(picker2)
|
|
||||||
|
|
||||||
tvUnit.text = unit
|
|
||||||
|
|
||||||
val dialog = AlertDialog.Builder(context)
|
|
||||||
.setView(view)
|
|
||||||
.setTitle(R.string.change_value)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
picker.clearFocus()
|
|
||||||
val v = picker.value + 0.05 * picker2.value
|
|
||||||
callback.onNumberPicked(v)
|
|
||||||
}
|
|
||||||
.setOnDismissListener {
|
|
||||||
callback.onNumberPickerDismissed()
|
|
||||||
}
|
|
||||||
.create()
|
|
||||||
|
|
||||||
dialog.setOnShowListener {
|
|
||||||
picker.getChildAt(0)?.requestFocus()
|
|
||||||
dialog.window?.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE)
|
|
||||||
}
|
|
||||||
|
|
||||||
InterfaceUtils.setupEditorAction(
|
|
||||||
picker
|
|
||||||
) { _, actionId, _ ->
|
|
||||||
if (actionId == EditorInfo.IME_ACTION_DONE)
|
|
||||||
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick()
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
return dialog
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun refreshInitialValue(picker: NumberPicker) {
|
|
||||||
// Workaround for Android bug:
|
|
||||||
// https://code.google.com/p/android/issues/detail?id=35482
|
|
||||||
val f = NumberPicker::class.java.getDeclaredField("mInputText")
|
|
||||||
f.isAccessible = true
|
|
||||||
val inputText = f.get(picker) as EditText
|
|
||||||
inputText.filters = arrayOfNulls<InputFilter>(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
/*
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ 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
|
||||||
@@ -73,7 +74,10 @@ 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?) {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import org.isoron.uhabits.core.utils.DateUtils.Companion.getShortWeekdayNames
|
|||||||
import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendar
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.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
|
||||||
@@ -62,7 +63,6 @@ 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,7 +82,6 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,15 +90,6 @@ 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()
|
||||||
@@ -166,6 +156,7 @@ 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)
|
||||||
@@ -173,7 +164,8 @@ 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) drawMarker(canvas, rect, values[i])
|
if (values != null)
|
||||||
|
drawMarker(canvas, rect, values[i], weekDaysInMonth[i])
|
||||||
rect.offset(0f, rowHeight)
|
rect.offset(0f, rowHeight)
|
||||||
}
|
}
|
||||||
drawFooter(canvas, rect, date)
|
drawFooter(canvas, rect, date)
|
||||||
@@ -221,12 +213,16 @@ 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?) {
|
private fun drawMarker(canvas: Canvas, rect: RectF?, value: Int?, frequency: 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]
|
||||||
@@ -289,6 +285,5 @@ class FrequencyChart : ScrollableChart {
|
|||||||
frequency[Timestamp(date)] = values
|
frequency[Timestamp(date)] = values
|
||||||
date.add(Calendar.MONTH, -1)
|
date.add(Calendar.MONTH, -1)
|
||||||
}
|
}
|
||||||
maxFreq = getMaxFreq(frequency)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ class TaskProgressBar(
|
|||||||
context: Context,
|
context: Context,
|
||||||
private val runner: TaskRunner
|
private val runner: TaskRunner
|
||||||
) : ProgressBar(
|
) : ProgressBar(
|
||||||
context,
|
context,
|
||||||
null,
|
null,
|
||||||
android.R.attr.progressBarStyleHorizontal
|
android.R.attr.progressBarStyleHorizontal
|
||||||
),
|
),
|
||||||
TaskRunner.Listener {
|
TaskRunner.Listener {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|||||||
@@ -46,12 +46,15 @@ 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
|
||||||
import org.isoron.uhabits.core.commands.EditHabitCommand
|
import org.isoron.uhabits.core.commands.EditHabitCommand
|
||||||
import org.isoron.uhabits.core.models.Frequency
|
import org.isoron.uhabits.core.models.Frequency
|
||||||
import org.isoron.uhabits.core.models.Habit
|
import org.isoron.uhabits.core.models.Habit
|
||||||
|
import org.isoron.uhabits.core.models.HabitType
|
||||||
|
import org.isoron.uhabits.core.models.NumericalHabitType
|
||||||
import org.isoron.uhabits.core.models.PaletteColor
|
import org.isoron.uhabits.core.models.PaletteColor
|
||||||
import org.isoron.uhabits.core.models.Reminder
|
import org.isoron.uhabits.core.models.Reminder
|
||||||
import org.isoron.uhabits.core.models.WeekdayList
|
import org.isoron.uhabits.core.models.WeekdayList
|
||||||
@@ -67,7 +70,7 @@ fun formatFrequency(freqNum: Int, freqDen: Int, resources: Resources) = when {
|
|||||||
freqNum == 1 && freqDen == 7 -> resources.getString(R.string.every_week)
|
freqNum == 1 && freqDen == 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 -> "$freqNum/$freqDen"
|
else -> resources.getString(R.string.x_times_per_y_days, freqNum, freqDen)
|
||||||
}
|
}
|
||||||
|
|
||||||
class EditHabitActivity : AppCompatActivity() {
|
class EditHabitActivity : AppCompatActivity() {
|
||||||
@@ -77,7 +80,7 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
private lateinit var commandRunner: CommandRunner
|
private lateinit var commandRunner: CommandRunner
|
||||||
|
|
||||||
var habitId = -1L
|
var habitId = -1L
|
||||||
var habitType = -1
|
lateinit var habitType: HabitType
|
||||||
var unit = ""
|
var unit = ""
|
||||||
var color = PaletteColor(11)
|
var color = PaletteColor(11)
|
||||||
var androidColor = 0
|
var androidColor = 0
|
||||||
@@ -86,6 +89,7 @@ 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)
|
||||||
@@ -105,6 +109,7 @@ 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
|
||||||
@@ -116,12 +121,12 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
binding.unitInput.setText(habit.unit)
|
binding.unitInput.setText(habit.unit)
|
||||||
binding.targetInput.setText(habit.targetValue.toString())
|
binding.targetInput.setText(habit.targetValue.toString())
|
||||||
} else {
|
} else {
|
||||||
habitType = intent.getIntExtra("habitType", Habit.YES_NO_HABIT)
|
habitType = HabitType.fromInt(intent.getIntExtra("habitType", HabitType.YES_NO.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
habitId = state.getLong("habitId")
|
habitId = state.getLong("habitId")
|
||||||
habitType = state.getInt("habitType")
|
habitType = HabitType.fromInt(state.getInt("habitType"))
|
||||||
color = PaletteColor(state.getInt("paletteColor"))
|
color = PaletteColor(state.getInt("paletteColor"))
|
||||||
freqNum = state.getInt("freqNum")
|
freqNum = state.getInt("freqNum")
|
||||||
freqDen = state.getInt("freqDen")
|
freqDen = state.getInt("freqDen")
|
||||||
@@ -132,13 +137,17 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
updateColors()
|
updateColors()
|
||||||
|
|
||||||
if (habitType == Habit.YES_NO_HABIT) {
|
when (habitType) {
|
||||||
binding.unitOuterBox.visibility = View.GONE
|
HabitType.YES_NO -> {
|
||||||
binding.targetOuterBox.visibility = View.GONE
|
binding.unitOuterBox.visibility = View.GONE
|
||||||
} else {
|
binding.targetOuterBox.visibility = View.GONE
|
||||||
binding.nameInput.hint = getString(R.string.measurable_short_example)
|
binding.targetTypeOuterBox.visibility = View.GONE
|
||||||
binding.questionInput.hint = getString(R.string.measurable_question_example)
|
}
|
||||||
binding.frequencyOuterBox.visibility = View.GONE
|
HabitType.NUMERICAL -> {
|
||||||
|
binding.nameInput.hint = getString(R.string.measurable_short_example)
|
||||||
|
binding.questionInput.hint = getString(R.string.measurable_question_example)
|
||||||
|
binding.frequencyOuterBox.visibility = View.GONE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setSupportActionBar(binding.toolbar)
|
setSupportActionBar(binding.toolbar)
|
||||||
@@ -167,6 +176,25 @@ 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)
|
||||||
@@ -255,9 +283,9 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
habit.frequency = Frequency(freqNum, freqDen)
|
habit.frequency = Frequency(freqNum, freqDen)
|
||||||
if (habitType == Habit.NUMBER_HABIT) {
|
if (habitType == HabitType.NUMERICAL) {
|
||||||
habit.targetValue = targetInput.text.toString().toDouble()
|
habit.targetValue = targetInput.text.toString().toDouble()
|
||||||
habit.targetType = Habit.AT_LEAST
|
habit.targetType = targetType
|
||||||
habit.unit = unitInput.text.trim().toString()
|
habit.unit = unitInput.text.trim().toString()
|
||||||
}
|
}
|
||||||
habit.type = habitType
|
habit.type = habitType
|
||||||
@@ -285,7 +313,7 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
nameInput.error = getFormattedValidationError(R.string.validation_cannot_be_blank)
|
nameInput.error = getFormattedValidationError(R.string.validation_cannot_be_blank)
|
||||||
isValid = false
|
isValid = false
|
||||||
}
|
}
|
||||||
if (habitType == Habit.NUMBER_HABIT) {
|
if (habitType == HabitType.NUMERICAL) {
|
||||||
if (targetInput.text.isEmpty()) {
|
if (targetInput.text.isEmpty()) {
|
||||||
targetInput.error = getString(R.string.validation_cannot_be_blank)
|
targetInput.error = getString(R.string.validation_cannot_be_blank)
|
||||||
isValid = false
|
isValid = false
|
||||||
@@ -319,6 +347,13 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun populateTargetType() {
|
||||||
|
binding.targetTypePicker.text = when (targetType) {
|
||||||
|
NumericalHabitType.AT_MOST -> getString(R.string.target_type_at_most)
|
||||||
|
else -> getString(R.string.target_type_at_least)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateColors() {
|
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)
|
||||||
@@ -338,7 +373,7 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
super.onSaveInstanceState(state)
|
super.onSaveInstanceState(state)
|
||||||
with(state) {
|
with(state) {
|
||||||
putLong("habitId", habitId)
|
putLong("habitId", habitId)
|
||||||
putInt("habitType", habitType)
|
putInt("habitType", habitType.value)
|
||||||
putInt("paletteColor", color.paletteIndex)
|
putInt("paletteColor", color.paletteIndex)
|
||||||
putInt("androidColor", androidColor)
|
putInt("androidColor", androidColor)
|
||||||
putInt("freqNum", freqNum)
|
putInt("freqNum", freqNum)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AppCompatDialogFragment
|
import androidx.appcompat.app.AppCompatDialogFragment
|
||||||
import org.isoron.uhabits.R
|
import org.isoron.uhabits.R
|
||||||
import org.isoron.uhabits.core.models.Habit
|
import org.isoron.uhabits.core.models.HabitType
|
||||||
import org.isoron.uhabits.databinding.SelectHabitTypeBinding
|
import org.isoron.uhabits.databinding.SelectHabitTypeBinding
|
||||||
import org.isoron.uhabits.intents.IntentFactory
|
import org.isoron.uhabits.intents.IntentFactory
|
||||||
|
|
||||||
@@ -40,13 +40,13 @@ class HabitTypeDialog : AppCompatDialogFragment() {
|
|||||||
val binding = SelectHabitTypeBinding.inflate(inflater, container, false)
|
val binding = SelectHabitTypeBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
binding.buttonYesNo.setOnClickListener {
|
binding.buttonYesNo.setOnClickListener {
|
||||||
val intent = IntentFactory().startEditActivity(activity!!, Habit.YES_NO_HABIT)
|
val intent = IntentFactory().startEditActivity(activity!!, HabitType.YES_NO.value)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.buttonMeasurable.setOnClickListener {
|
binding.buttonMeasurable.setOnClickListener {
|
||||||
val intent = IntentFactory().startEditActivity(activity!!, Habit.NUMBER_HABIT)
|
val intent = IntentFactory().startEditActivity(activity!!, HabitType.NUMERICAL.value)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
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.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
|
||||||
@@ -36,11 +37,15 @@ 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() {
|
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
|
||||||
@@ -51,11 +56,16 @@ class ListHabitsActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private lateinit var menu: ListHabitsMenu
|
private lateinit var menu: ListHabitsMenu
|
||||||
|
|
||||||
|
override fun onQuestionMarksChanged() {
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
menu.behavior.onPreferencesChanged()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
val appComponent = (applicationContext as HabitsApplication).component
|
appComponent = (applicationContext as HabitsApplication).component
|
||||||
val component = DaggerHabitsActivityComponent
|
component = DaggerHabitsActivityComponent
|
||||||
.builder()
|
.builder()
|
||||||
.activityContextModule(ActivityContextModule(this))
|
.activityContextModule(ActivityContextModule(this))
|
||||||
.habitsApplicationComponent(appComponent)
|
.habitsApplicationComponent(appComponent)
|
||||||
@@ -63,6 +73,7 @@ class ListHabitsActivity : AppCompatActivity() {
|
|||||||
component.themeSwitcher.apply()
|
component.themeSwitcher.apply()
|
||||||
|
|
||||||
prefs = appComponent.preferences
|
prefs = appComponent.preferences
|
||||||
|
prefs.addListener(this)
|
||||||
pureBlack = prefs.isPureBlackEnabled
|
pureBlack = prefs.isPureBlackEnabled
|
||||||
midnightTimer = appComponent.midnightTimer
|
midnightTimer = appComponent.midnightTimer
|
||||||
rootView = component.listHabitsRootView
|
rootView = component.listHabitsRootView
|
||||||
@@ -73,11 +84,12 @@ class ListHabitsActivity : AppCompatActivity() {
|
|||||||
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.onDettached()
|
screen.onDetached()
|
||||||
adapter.cancelRefresh()
|
adapter.cancelRefresh()
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
@@ -110,4 +122,19 @@ class ListHabitsActivity : AppCompatActivity() {
|
|||||||
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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
private val behavior: ListHabitsMenuBehavior
|
val behavior: ListHabitsMenuBehavior
|
||||||
) {
|
) {
|
||||||
val activity = (context as AppCompatActivity)
|
val activity = (context as AppCompatActivity)
|
||||||
|
|
||||||
@@ -52,6 +52,11 @@ class ListHabitsMenu @Inject constructor(
|
|||||||
nightModeItem.isChecked = themeSwitcher.isNightMode
|
nightModeItem.isChecked = themeSwitcher.isNightMode
|
||||||
hideArchivedItem.isChecked = !preferences.showArchived
|
hideArchivedItem.isChecked = !preferences.showArchived
|
||||||
hideCompletedItem.isChecked = !preferences.showCompleted
|
hideCompletedItem.isChecked = !preferences.showCompleted
|
||||||
|
if (preferences.areQuestionMarksEnabled || preferences.isSkipEnabled) {
|
||||||
|
hideCompletedItem.title = activity.resources.getString(R.string.hide_entered)
|
||||||
|
} else {
|
||||||
|
hideCompletedItem.title = activity.resources.getString(R.string.hide_completed)
|
||||||
|
}
|
||||||
updateArrows(menu)
|
updateArrows(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,10 +24,12 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
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.NumberPickerFactory
|
import org.isoron.uhabits.activities.common.dialogs.NumberPopup
|
||||||
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
|
||||||
@@ -40,6 +42,7 @@ 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
|
||||||
@@ -60,6 +63,7 @@ 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
|
||||||
@@ -88,8 +92,9 @@ 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 numberPickerFactory: NumberPickerFactory,
|
private val behavior: Lazy<ListHabitsBehavior>,
|
||||||
private val behavior: Lazy<ListHabitsBehavior>
|
private val preferences: Preferences,
|
||||||
|
private val rootView: Lazy<ListHabitsRootView>,
|
||||||
) : CommandRunner.Listener,
|
) : CommandRunner.Listener,
|
||||||
ListHabitsBehavior.Screen,
|
ListHabitsBehavior.Screen,
|
||||||
ListHabitsMenuBehavior.Screen,
|
ListHabitsMenuBehavior.Screen,
|
||||||
@@ -101,7 +106,7 @@ class ListHabitsScreen
|
|||||||
commandRunner.addListener(this)
|
commandRunner.addListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDettached() {
|
fun onDetached() {
|
||||||
commandRunner.removeListener(this)
|
commandRunner.removeListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,12 +227,42 @@ class ListHabitsScreen
|
|||||||
picker.show(activity.supportFragmentManager, "picker")
|
picker.show(activity.supportFragmentManager, "picker")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showNumberPicker(
|
override fun showNumberPopup(
|
||||||
value: Double,
|
value: Double,
|
||||||
unit: String,
|
notes: String,
|
||||||
callback: ListHabitsBehavior.NumberPickerCallback
|
callback: ListHabitsBehavior.NumberPickerCallback
|
||||||
) {
|
) {
|
||||||
numberPickerFactory.create(value, unit, callback).show()
|
val view = rootView.get()
|
||||||
|
NumberPopup(
|
||||||
|
context = context,
|
||||||
|
prefs = preferences,
|
||||||
|
anchor = view,
|
||||||
|
notes = notes,
|
||||||
|
value = value,
|
||||||
|
).apply {
|
||||||
|
onToggle = { value, notes -> callback.onNumberPicked(value, notes) }
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showCheckmarkPopup(
|
||||||
|
selectedValue: Int,
|
||||||
|
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? {
|
||||||
|
|||||||
@@ -37,13 +37,15 @@ 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.dim
|
import org.isoron.uhabits.utils.drawNotesIndicator
|
||||||
import org.isoron.uhabits.utils.getFontAwesome
|
import org.isoron.uhabits.utils.getFontAwesome
|
||||||
import org.isoron.uhabits.utils.showMessage
|
import org.isoron.uhabits.utils.sp
|
||||||
import org.isoron.uhabits.utils.sres
|
import org.isoron.uhabits.utils.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,
|
||||||
@@ -71,33 +73,42 @@ class CheckmarkButtonView(
|
|||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
var onToggle: (Int) -> Unit = {}
|
var notes = ""
|
||||||
|
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() {
|
fun performToggle(delay: Long) {
|
||||||
value = if (preferences.isSkipEnabled) {
|
value = Entry.nextToggleValue(
|
||||||
Entry.nextToggleValueWithSkip(value)
|
value = value,
|
||||||
} else {
|
isSkipEnabled = preferences.isSkipEnabled,
|
||||||
Entry.nextToggleValueWithoutSkip(value)
|
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
|
||||||
}
|
)
|
||||||
onToggle(value)
|
onToggle(value, notes, delay)
|
||||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
override fun onClick(v: View) {
|
||||||
if (preferences.isShortToggleEnabled) performToggle()
|
if (preferences.isShortToggleEnabled) performToggle(TOGGLE_DELAY_MILLIS)
|
||||||
else showMessage(resources.getString(R.string.long_press_to_toggle))
|
else onEdit()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLongClick(v: View): Boolean {
|
override fun onLongClick(v: View): Boolean {
|
||||||
performToggle()
|
if (preferences.isShortToggleEnabled) onEdit()
|
||||||
|
else performToggle(TOGGLE_DELAY_MILLIS)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +142,7 @@ class CheckmarkButtonView(
|
|||||||
paint.color = when (value) {
|
paint.color = when (value) {
|
||||||
YES_MANUAL, YES_AUTO, SKIP -> color
|
YES_MANUAL, YES_AUTO, SKIP -> color
|
||||||
NO -> {
|
NO -> {
|
||||||
if (preferences.areQuestionMarksEnabled()) mediumContrastColor
|
if (preferences.areQuestionMarksEnabled) mediumContrastColor
|
||||||
else lowContrastColor
|
else lowContrastColor
|
||||||
}
|
}
|
||||||
else -> lowContrastColor
|
else -> lowContrastColor
|
||||||
@@ -140,11 +151,16 @@ class CheckmarkButtonView(
|
|||||||
SKIP -> R.string.fa_skipped
|
SKIP -> R.string.fa_skipped
|
||||||
NO -> R.string.fa_times
|
NO -> R.string.fa_times
|
||||||
UNKNOWN -> {
|
UNKNOWN -> {
|
||||||
if (preferences.areQuestionMarksEnabled()) R.string.fa_question
|
if (preferences.areQuestionMarksEnabled) R.string.fa_question
|
||||||
else R.string.fa_times
|
else R.string.fa_times
|
||||||
}
|
}
|
||||||
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
|
||||||
@@ -153,11 +169,6 @@ 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")
|
||||||
|
|
||||||
@@ -170,6 +181,8 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,19 @@ class CheckmarkPanelView(
|
|||||||
setupButtons()
|
setupButtons()
|
||||||
}
|
}
|
||||||
|
|
||||||
var onToggle: (Timestamp, Int) -> Unit = { _, _ -> }
|
var notes = arrayOf<String>()
|
||||||
|
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()
|
||||||
@@ -72,8 +84,13 @@ 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 -> onToggle(timestamp, value) }
|
button.onToggle = { value, notes, delay -> onToggle(timestamp, value, notes, delay) }
|
||||||
|
button.onEdit = { onEdit(timestamp) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,8 +124,9 @@ 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, selected)
|
listView!!.bindCardView(holder, habit, score, checkmarks, notes, selected)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewAttachedToWindow(holder: HabitCardViewHolder) {
|
override fun onViewAttachedToWindow(holder: HabitCardViewHolder) {
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ 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
|
||||||
@@ -98,6 +99,7 @@ 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 ->
|
||||||
|
|||||||
@@ -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,6 +57,13 @@ 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,
|
||||||
@@ -115,12 +122,22 @@ 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)
|
||||||
@@ -137,13 +154,22 @@ 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 >= M) breakStrategy = BREAK_STRATEGY_BALANCED
|
if (SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
breakStrategy = BREAK_STRATEGY_BALANCED
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkmarkPanel = checkmarkPanelFactory.create().apply {
|
checkmarkPanel = checkmarkPanelFactory.create().apply {
|
||||||
onToggle = { timestamp, value ->
|
onToggle = { timestamp, value, notes, delay ->
|
||||||
|
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.onToggle(it, timestamp, value) }
|
habit?.let { behavior.onEdit(it, timestamp) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,6 +205,25 @@ 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) }
|
||||||
@@ -236,6 +281,7 @@ 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
|
||||||
@@ -262,4 +308,10 @@ class HabitCardView(
|
|||||||
}
|
}
|
||||||
innerFrame.setBackgroundResource(background)
|
innerFrame.setBackgroundResource(background)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun (() -> Unit).delay(delayInMillis: Long) {
|
||||||
|
Handler(Looper.getMainLooper()).postDelayed(this, delayInMillis)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,13 +29,16 @@ import android.view.View
|
|||||||
import android.view.View.OnClickListener
|
import android.view.View.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.showMessage
|
import org.isoron.uhabits.utils.sres
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -88,13 +91,25 @@ class NumberButtonView(
|
|||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
var units = ""
|
var targetType = AT_LEAST
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
var onEdit: () -> Unit = {}
|
var units = ""
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
var notes = ""
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
var onEdit: () -> Unit = { }
|
||||||
|
|
||||||
private var drawer: Drawer = Drawer(context)
|
private var drawer: Drawer = Drawer(context)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -103,8 +118,7 @@ class NumberButtonView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
override fun onClick(v: View) {
|
||||||
if (preferences.isShortToggleEnabled) onEdit()
|
onEdit()
|
||||||
else showMessage(resources.getString(R.string.long_press_to_edit))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLongClick(v: View): Boolean {
|
override fun onLongClick(v: View): Boolean {
|
||||||
@@ -127,11 +141,16 @@ 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
|
||||||
@@ -148,15 +167,16 @@ class NumberButtonView(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
em = pNumber.measureText("m")
|
em = pNumber.measureText("m")
|
||||||
lowContrast = sr.getColor(R.attr.contrast40)
|
lowContrast = sres.getColor(R.attr.contrast40)
|
||||||
mediumContrast = sr.getColor(R.attr.contrast60)
|
mediumContrast = sres.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
|
||||||
value < threshold -> mediumContrast
|
(targetType == AT_LEAST) && (value >= threshold) -> color
|
||||||
else -> color
|
(targetType == AT_MOST) && (value <= threshold) -> color
|
||||||
|
else -> mediumContrast
|
||||||
}
|
}
|
||||||
|
|
||||||
val label: String
|
val label: String
|
||||||
@@ -164,12 +184,17 @@ 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
|
||||||
textSize = dim(R.dimen.smallTextSize)
|
textSize = dim(R.dimen.smallTextSize)
|
||||||
}
|
}
|
||||||
preferences.areQuestionMarksEnabled() -> {
|
preferences.areQuestionMarksEnabled -> {
|
||||||
label = resources.getString(R.string.fa_question)
|
label = resources.getString(R.string.fa_question)
|
||||||
typeface = getFontAwesome()
|
typeface = getFontAwesome()
|
||||||
textSize = dim(R.dimen.smallerTextSize)
|
textSize = dim(R.dimen.smallerTextSize)
|
||||||
@@ -196,6 +221,8 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
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
|
||||||
@@ -47,6 +48,12 @@ 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
|
||||||
@@ -65,7 +72,13 @@ class NumberPanelView(
|
|||||||
setupButtons()
|
setupButtons()
|
||||||
}
|
}
|
||||||
|
|
||||||
var onEdit: (Timestamp) -> Unit = {}
|
var notes = arrayOf<String>()
|
||||||
|
set(values) {
|
||||||
|
field = values
|
||||||
|
setupButtons()
|
||||||
|
}
|
||||||
|
|
||||||
|
var onEdit: (Timestamp) -> Unit = { _ -> }
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
setupButtons()
|
setupButtons()
|
||||||
@@ -83,7 +96,12 @@ 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) }
|
||||||
|
|||||||
@@ -23,21 +23,25 @@ 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.NumberPickerFactory
|
import org.isoron.uhabits.activities.common.dialogs.NumberPopup
|
||||||
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
|
||||||
@@ -45,6 +49,7 @@ 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
|
||||||
@@ -161,12 +166,49 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
|||||||
window.decorView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
|
window.decorView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showNumberPicker(
|
override fun showNumberPopup(
|
||||||
value: Double,
|
value: Double,
|
||||||
unit: String,
|
notes: String,
|
||||||
callback: ListHabitsBehavior.NumberPickerCallback,
|
preferences: Preferences,
|
||||||
|
callback: ListHabitsBehavior.NumberPickerCallback
|
||||||
) {
|
) {
|
||||||
NumberPickerFactory(this@ShowHabitActivity).create(value, unit, callback).show()
|
val anchor = getPopupAnchor() ?: return
|
||||||
|
NumberPopup(
|
||||||
|
context = this@ShowHabitActivity,
|
||||||
|
prefs = preferences,
|
||||||
|
notes = notes,
|
||||||
|
anchor = anchor,
|
||||||
|
value = value,
|
||||||
|
).apply {
|
||||||
|
onToggle = { v, n -> callback.onNumberPicked(v, n) }
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun 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) {
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ 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()
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ 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
|
||||||
@@ -65,7 +66,12 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ package org.isoron.uhabits.activities.intro
|
|||||||
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.github.paolorotolo.appintro.AppIntro2
|
import androidx.fragment.app.Fragment
|
||||||
import com.github.paolorotolo.appintro.AppIntroFragment
|
import com.github.appintro.AppIntro2
|
||||||
|
import com.github.appintro.AppIntroFragment
|
||||||
import org.isoron.uhabits.R
|
import org.isoron.uhabits.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,7 +31,9 @@ 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(
|
||||||
@@ -61,11 +64,13 @@ class IntroActivity : AppIntro2() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNextPressed() {}
|
override fun onDonePressed(currentFragment: Fragment?) {
|
||||||
|
super.onDonePressed(currentFragment)
|
||||||
override fun onDonePressed() {
|
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSlideChanged() {}
|
override fun onSkipPressed(currentFragment: Fragment?) {
|
||||||
|
super.onSkipPressed(currentFragment)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,11 @@ import android.content.Intent
|
|||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Build.VERSION
|
import android.os.Build.VERSION.SDK_INT
|
||||||
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.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceCategory
|
import androidx.preference.PreferenceCategory
|
||||||
@@ -43,6 +44,7 @@ import org.isoron.uhabits.core.ui.NotificationTray
|
|||||||
import org.isoron.uhabits.core.utils.DateUtils.Companion.getLongWeekdayNames
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.getLongWeekdayNames
|
||||||
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
|
||||||
|
|
||||||
@@ -84,13 +86,19 @@ 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 (VERSION.SDK_INT < Build.VERSION_CODES.O) return true
|
if (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)
|
||||||
@@ -112,7 +120,7 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
|
|||||||
}
|
}
|
||||||
updateWeekdayPreference()
|
updateWeekdayPreference()
|
||||||
|
|
||||||
if (VERSION.SDK_INT < Build.VERSION_CODES.O)
|
if (SDK_INT < Build.VERSION_CODES.O)
|
||||||
findPreference("reminderCustomize").isVisible = false
|
findPreference("reminderCustomize").isVisible = false
|
||||||
else {
|
else {
|
||||||
findPreference("reminderSound").isVisible = false
|
findPreference("reminderSound").isVisible = false
|
||||||
|
|||||||
@@ -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.HabitMatcherBuilder
|
import org.isoron.uhabits.core.models.HabitMatcher
|
||||||
|
|
||||||
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(
|
||||||
HabitMatcherBuilder()
|
HabitMatcher(
|
||||||
.setArchivedAllowed(false)
|
isArchivedAllowed = false,
|
||||||
.setCompletedAllowed(true)
|
isCompletedAllowed = true,
|
||||||
.build()
|
)
|
||||||
)
|
)
|
||||||
AndroidThemeSwitcher(this, app.component.preferences).apply()
|
AndroidThemeSwitcher(this, app.component.preferences).apply()
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ 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
|
||||||
@@ -58,10 +56,7 @@ class IntentScheduler
|
|||||||
)
|
)
|
||||||
return SchedulerResult.IGNORED
|
return SchedulerResult.IGNORED
|
||||||
}
|
}
|
||||||
if (SDK_INT >= M)
|
manager.setExactAndAllowWhileIdle(alarmType, timestamp, intent)
|
||||||
manager.setExactAndAllowWhileIdle(alarmType, timestamp, intent)
|
|
||||||
else
|
|
||||||
manager.setExact(alarmType, timestamp, intent)
|
|
||||||
return SchedulerResult.OK
|
return SchedulerResult.OK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,14 @@
|
|||||||
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
|
||||||
@@ -49,7 +52,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_UPDATE_CURRENT
|
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
|
||||||
)
|
)
|
||||||
|
|
||||||
fun dismissNotification(habit: Habit): PendingIntent =
|
fun dismissNotification(habit: Habit): PendingIntent =
|
||||||
@@ -60,7 +63,7 @@ class PendingIntentFactory
|
|||||||
action = WidgetReceiver.ACTION_DISMISS_REMINDER
|
action = WidgetReceiver.ACTION_DISMISS_REMINDER
|
||||||
data = Uri.parse(habit.uriString)
|
data = Uri.parse(habit.uriString)
|
||||||
},
|
},
|
||||||
FLAG_UPDATE_CURRENT
|
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
|
||||||
)
|
)
|
||||||
|
|
||||||
fun removeRepetition(habit: Habit, timestamp: Timestamp?): PendingIntent =
|
fun removeRepetition(habit: Habit, timestamp: Timestamp?): PendingIntent =
|
||||||
@@ -72,7 +75,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_UPDATE_CURRENT
|
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
|
||||||
)
|
)
|
||||||
|
|
||||||
fun showHabit(habit: Habit): PendingIntent =
|
fun showHabit(habit: Habit): PendingIntent =
|
||||||
@@ -84,7 +87,7 @@ class PendingIntentFactory
|
|||||||
habit
|
habit
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.getPendingIntent(0, FLAG_UPDATE_CURRENT)!!
|
.getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)!!
|
||||||
|
|
||||||
fun showReminder(
|
fun showReminder(
|
||||||
habit: Habit,
|
habit: Habit,
|
||||||
@@ -100,7 +103,7 @@ class PendingIntentFactory
|
|||||||
putExtra("timestamp", timestamp)
|
putExtra("timestamp", timestamp)
|
||||||
putExtra("reminderTime", reminderTime)
|
putExtra("reminderTime", reminderTime)
|
||||||
},
|
},
|
||||||
FLAG_UPDATE_CURRENT
|
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
|
||||||
)
|
)
|
||||||
|
|
||||||
fun snoozeNotification(habit: Habit): PendingIntent =
|
fun snoozeNotification(habit: Habit): PendingIntent =
|
||||||
@@ -111,7 +114,7 @@ class PendingIntentFactory
|
|||||||
data = Uri.parse(habit.uriString)
|
data = Uri.parse(habit.uriString)
|
||||||
action = ReminderReceiver.ACTION_SNOOZE_REMINDER
|
action = ReminderReceiver.ACTION_SNOOZE_REMINDER
|
||||||
},
|
},
|
||||||
FLAG_UPDATE_CURRENT
|
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
|
||||||
)
|
)
|
||||||
|
|
||||||
fun toggleCheckmark(habit: Habit, timestamp: Long?): PendingIntent =
|
fun toggleCheckmark(habit: Habit, timestamp: Long?): PendingIntent =
|
||||||
@@ -123,28 +126,9 @@ 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_UPDATE_CURRENT
|
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
|
||||||
)
|
)
|
||||||
|
|
||||||
fun setNumericalValue(
|
|
||||||
widgetContext: Context,
|
|
||||||
habit: Habit,
|
|
||||||
numericalValue: Int,
|
|
||||||
timestamp: Long?
|
|
||||||
):
|
|
||||||
PendingIntent =
|
|
||||||
getBroadcast(
|
|
||||||
widgetContext,
|
|
||||||
2,
|
|
||||||
Intent(widgetContext, WidgetReceiver::class.java).apply {
|
|
||||||
data = Uri.parse(habit.uriString)
|
|
||||||
action = WidgetReceiver.ACTION_SET_NUMERICAL_VALUE
|
|
||||||
putExtra("numericalValue", numericalValue)
|
|
||||||
if (timestamp != null) putExtra("timestamp", timestamp)
|
|
||||||
},
|
|
||||||
FLAG_UPDATE_CURRENT
|
|
||||||
)
|
|
||||||
|
|
||||||
fun updateWidgets(): PendingIntent =
|
fun updateWidgets(): PendingIntent =
|
||||||
getBroadcast(
|
getBroadcast(
|
||||||
context,
|
context,
|
||||||
@@ -152,6 +136,19 @@ 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_UPDATE_CURRENT
|
FLAG_IMMUTABLE or 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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ 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
|
||||||
@@ -113,7 +112,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.setNumericalValue(context, habit, 0, null)
|
pendingIntents.showNumberPicker(habit, timestamp)
|
||||||
)
|
)
|
||||||
|
|
||||||
val wearableBg = decodeResource(context.resources, R.drawable.stripe)
|
val wearableBg = decodeResource(context.resources, R.drawable.stripe)
|
||||||
@@ -150,16 +149,15 @@ class AndroidNotificationTray
|
|||||||
if (!disableSound)
|
if (!disableSound)
|
||||||
builder.setSound(ringtoneManager.getURI())
|
builder.setSound(ringtoneManager.getURI())
|
||||||
|
|
||||||
if (preferences.shouldMakeNotificationsLed())
|
if (SDK_INT < Build.VERSION_CODES.S) {
|
||||||
builder.setLights(Color.RED, 1000, 1000)
|
val snoozeAction = Action(
|
||||||
|
R.drawable.ic_action_snooze,
|
||||||
val snoozeAction = Action(
|
context.getString(R.string.snooze),
|
||||||
R.drawable.ic_action_snooze,
|
pendingIntents.snoozeNotification(habit)
|
||||||
context.getString(R.string.snooze),
|
)
|
||||||
pendingIntents.snoozeNotification(habit)
|
wearableExtender.addAction(snoozeAction)
|
||||||
)
|
builder.addAction(snoozeAction)
|
||||||
wearableExtender.addAction(snoozeAction)
|
}
|
||||||
builder.addAction(snoozeAction)
|
|
||||||
|
|
||||||
builder.extend(wearableExtender)
|
builder.extend(wearableExtender)
|
||||||
return builder.build()
|
return builder.build()
|
||||||
|
|||||||
@@ -90,8 +90,9 @@ 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" ->
|
"pref_unknown_enabled" -> {
|
||||||
preferences.setNotificationsLed(getBoolean(key, false))
|
preferences.areQuestionMarksEnabled = getBoolean(key, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ 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
|
||||||
@@ -76,8 +78,21 @@ class ReminderReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
ACTION_SNOOZE_REMINDER -> {
|
ACTION_SNOOZE_REMINDER -> {
|
||||||
if (habit == null) return
|
if (habit == null) return
|
||||||
Log.d("ReminderReceiver", String.format("onSnoozePressed habit=%d", habit.id))
|
if (SDK_INT < Build.VERSION_CODES.S) {
|
||||||
reminderController.onSnoozePressed(habit, context)
|
Log.d(
|
||||||
|
"ReminderReceiver",
|
||||||
|
String.format("onSnoozePressed habit=%d", habit.id)
|
||||||
|
)
|
||||||
|
reminderController.onSnoozePressed(habit, context)
|
||||||
|
} else {
|
||||||
|
Log.w(
|
||||||
|
"ReminderReceiver",
|
||||||
|
String.format(
|
||||||
|
"onSnoozePressed habit=%d, should be deactivated in recent versions.",
|
||||||
|
habit.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Intent.ACTION_BOOT_COMPLETED -> {
|
Intent.ACTION_BOOT_COMPLETED -> {
|
||||||
Log.d("ReminderReceiver", "onBootCompleted")
|
Log.d("ReminderReceiver", "onBootCompleted")
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import org.isoron.uhabits.HabitsApplication
|
|||||||
import org.isoron.uhabits.core.ui.widgets.WidgetBehavior
|
import org.isoron.uhabits.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.
|
||||||
@@ -96,15 +95,6 @@ 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()
|
||||||
@@ -126,7 +116,6 @@ 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
|
||||||
|
|||||||
@@ -22,14 +22,13 @@ 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 (isAndroidOOrLater) {
|
if (SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
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 {
|
||||||
|
|||||||
@@ -20,16 +20,22 @@
|
|||||||
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
|
||||||
@@ -199,5 +205,33 @@ 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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.setNumericalValue(context, habit, 10, null)
|
pendingIntentFactory.showNumberPicker(habit, DateUtils.getToday())
|
||||||
} else {
|
} else {
|
||||||
pendingIntentFactory.toggleCheckmark(habit, null)
|
pendingIntentFactory.toggleCheckmark(habit, null)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,10 @@ class HistoryWidget(
|
|||||||
theme = WidgetTheme(),
|
theme = WidgetTheme(),
|
||||||
)
|
)
|
||||||
(widgetView.dataView as AndroidDataView).apply {
|
(widgetView.dataView as AndroidDataView).apply {
|
||||||
(this.view as HistoryChart).series = model.series
|
val historyChart = (this.view as HistoryChart)
|
||||||
|
historyChart.series = model.series
|
||||||
|
historyChart.defaultSquare = model.defaultSquare
|
||||||
|
historyChart.notesIndicators = model.notesIndicators
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +74,8 @@ class HistoryWidget(
|
|||||||
dateFormatter = JavaLocalDateFormatter(Locale.getDefault()),
|
dateFormatter = JavaLocalDateFormatter(Locale.getDefault()),
|
||||||
firstWeekday = prefs.firstWeekday,
|
firstWeekday = prefs.firstWeekday,
|
||||||
series = listOf(),
|
series = listOf(),
|
||||||
|
defaultSquare = HistoryChart.Square.OFF,
|
||||||
|
notesIndicators = listOf(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
).apply {
|
).apply {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.widgets.activities
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.Window
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import org.isoron.uhabits.HabitsApplication
|
|
||||||
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
|
||||||
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
|
|
||||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
|
|
||||||
import org.isoron.uhabits.core.ui.widgets.WidgetBehavior
|
|
||||||
import org.isoron.uhabits.core.utils.DateUtils
|
|
||||||
import org.isoron.uhabits.intents.IntentParser
|
|
||||||
import org.isoron.uhabits.utils.SystemUtils
|
|
||||||
import org.isoron.uhabits.widgets.WidgetUpdater
|
|
||||||
|
|
||||||
class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPickerCallback {
|
|
||||||
|
|
||||||
private lateinit var behavior: WidgetBehavior
|
|
||||||
private lateinit var data: IntentParser.CheckmarkIntentData
|
|
||||||
private lateinit var widgetUpdater: WidgetUpdater
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
|
||||||
setContentView(FrameLayout(this))
|
|
||||||
val app = this.applicationContext as HabitsApplication
|
|
||||||
val component = app.component
|
|
||||||
val parser = app.component.intentParser
|
|
||||||
data = parser.parseCheckmarkIntent(intent)
|
|
||||||
behavior = WidgetBehavior(
|
|
||||||
component.habitList,
|
|
||||||
component.commandRunner,
|
|
||||||
component.notificationTray,
|
|
||||||
component.preferences
|
|
||||||
)
|
|
||||||
widgetUpdater = component.widgetUpdater
|
|
||||||
showNumberSelector(this)
|
|
||||||
|
|
||||||
SystemUtils.unlockScreen(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNumberPicked(newValue: Double) {
|
|
||||||
behavior.setValue(data.habit, data.timestamp, (newValue * 1000).toInt())
|
|
||||||
widgetUpdater.updateWidgets()
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNumberPickerDismissed() {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showNumberSelector(context: Context) {
|
|
||||||
val app = this.applicationContext as HabitsApplication
|
|
||||||
AndroidThemeSwitcher(this, app.component.preferences).apply()
|
|
||||||
val numberPickerFactory = NumberPickerFactory(context)
|
|
||||||
val today = DateUtils.getTodayWithOffset()
|
|
||||||
val entry = data.habit.computedEntries.get(today)
|
|
||||||
numberPickerFactory.create(
|
|
||||||
entry.value / 1000.0,
|
|
||||||
data.habit.unit,
|
|
||||||
this
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val ACTION_SHOW_NUMERICAL_VALUE_ACTIVITY = "org.isoron.uhabits.ACTION_SHOW_NUMERICAL_VALUE_ACTIVITY"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -103,7 +103,7 @@ class CheckmarkWidgetView : HabitWidgetView {
|
|||||||
SKIP -> resources.getString(R.string.fa_skipped)
|
SKIP -> resources.getString(R.string.fa_skipped)
|
||||||
UNKNOWN -> {
|
UNKNOWN -> {
|
||||||
run {
|
run {
|
||||||
if (preferences!!.areQuestionMarksEnabled()) {
|
if (preferences!!.areQuestionMarksEnabled) {
|
||||||
return resources.getString(R.string.fa_question)
|
return resources.getString(R.string.fa_question)
|
||||||
} else {
|
} else {
|
||||||
resources.getString(R.string.fa_times)
|
resources.getString(R.string.fa_times)
|
||||||
|
|||||||
1
uhabits-android/src/main/play/listings/af-rZA/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Loop - Gewoonte Boekhouer
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
حلقة تعقب الحبوب تساعدك على إنشاء عادات إيجابية طويلة الأجل والحفاظ عليها في حياتك. تعطيكم الرسوم البيانية والإحصاءات التفصيلية صورة واضحة للكيفية التي تحسنت بها عاداتك مع مرور الوقت. التطبيق خالٍ تمامًا من الإعلانات ومفتوح المصدر ويحترم خصوصيتك.
|
يساعدك Loop Habit Tracker على إنشاء عادات إيجابية طويلة المدى والحفاظ عليها في حياتك. تعطيكم الرسوم البيانية والإحصاءات التفصيلية صورة واضحة للكيفية التي تحسنت بها عاداتك مع مرور الوقت. التطبيق خالٍ تمامًا من الإعلانات ومفتوح المصدر ويحترم خصوصيتك.
|
||||||
|
|
||||||
<b>واجهة جميلة وبسيطة</b>
|
<b>واجهة جميلة وبسيطة</b>
|
||||||
يحتوي Loop على واجهة أنيقة وبسيطة وسهلة الاستخدام للغاية ، حتى للمستخدمين لأول مرة. تم تحسين التطبيق ليكون ذي سرعة عالية ، ويعمل التطبيق بشكل جيد حتى على الهواتف القديمة.
|
يحتوي Loop على واجهة أنيقة وبسيطة وسهلة الاستخدام للغاية ، حتى للمستخدمين لأول مرة. تم تحسين التطبيق ليكون ذي سرعة عالية ، ويعمل التطبيق بشكل جيد حتى على الهواتف القديمة.
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
لب يساعدك على خلق والحفاظ على العادات الجيدة، مما يسمح لك لتحقيق أهدافكة. الرسوم البيانية والإحصاءات التفصيلية تبين لكم كيف تحسن عاداتك مع مرور الوقت. هو تماما خالية من الاعلانات ومفتوحة المصدر.
|
|
||||||
|
|
||||||
<b>واجهة بسيطة، جميلة وحديثة </b>
|
|
||||||
لوب يحتوي على واجهة بسيطة وهي سهلة الاستخدام و تتابع نظام تصميم الماتريل دسيجن.
|
|
||||||
|
|
||||||
<b>نتيجة العادات</b>
|
|
||||||
بالإضافة إلى عرض التقدم الحالي، لوب ديه خوارزمية متقدمة لحساب قوة عاداتك. كل التكرار يجعل هذه العادة أقوى، وفي كل يوم غاب يجعلها أضعف. مع ذلك غيب أيام قليلة بعد تقدم طويلة ، لن تدمر تماما تقدمك .
|
|
||||||
|
|
||||||
<b>الرسوم البيانية والإحصاءات المفصلة</b>
|
|
||||||
نرى بوضوح كيف كنت قد تحسنت عاداتك بمرور الوقت مع الرسوم البيانية الجميله ومفصلة. انتقل إلى الوراء لنرى التاريخ الكامل لعاداتك.
|
|
||||||
|
|
||||||
<b>جداول مرنة</b>
|
|
||||||
تؤيد كل من العادات اليومية والعادات مع جداول أكثر تعقيدا، مثل 3 مرات كل أسبوع، مرة واحدة كل أسبوعين، أو مرة كل يومين.
|
|
||||||
|
|
||||||
<b>تذكير</b>
|
|
||||||
إنشاء تذكير لكل فرد من عاداتك، في ساعة اختيار من اليوم. تحقق بسهولة، رفض أو غفوة عادتك مباشرة من الإخطار، دون الحاجة إلى فتح التطبيق.
|
|
||||||
|
|
||||||
<b>خالية تماما من الإعلانات و المصدر المفتوح</b>
|
|
||||||
لا توجد على الاطلاق الإعلانات والشعارات المزعجة أو أذونات إضافية في هذا التطبيق، و سوف يكون هناك أبدا.
|
|
||||||
|
|
||||||
<b>الأمثل للساعات الذكية</b>
|
|
||||||
يمكن التحقق من رسائل التذكير، رفض أو غفوة عادتك مباشرة من ساعتك الاندرويد وير.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
خلق عادات جيدة وتتبع تقدمك على مر الزمن
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
لوب ملاحق العادة
|
|
||||||
1
uhabits-android/src/main/play/listings/bg-rBG/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Loop Следене на навици
|
||||||
1
uhabits-android/src/main/play/listings/ca-rES/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Loop - Hàbit Rastrejador
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
Loop t'ajuda a crear i mantenir bons hàbits, permetent-te aconseguir els teus objectius a llarg termini. Els gràfics i estadístiques detallades et mostren com han millorat els teus hàbits al llarg del temps. És completament de codi obert i lliure de publicitat.
|
|
||||||
|
|
||||||
<b>Senzilla, bonica i moderna interfície</b>
|
|
||||||
Loop té una interfície minimalista que és fàcil d'utilitzar i segueix les guies de "material design".
|
|
||||||
|
|
||||||
<b>Puntuació d'hàbit</b>
|
|
||||||
A més de mostrar la teva ratxa actual, Loop té un algoritme avançat per a calcular la fortalesa dels teus hàbits. Cada repetició fa el teu hàbit més fort, i cada dia que fallis el farà més dèbil. Tot i això uns quants dies que fallis després d'una llarga ratxa no malmetrà completament el teu progrès.
|
|
||||||
|
|
||||||
<b>Gràfics i estadístiques detallades</b>
|
|
||||||
Permet veure de forma clara com han millorat els teus hàbits al llarg del temps amb gràfics bonics i detallats. Pots anar enrera i veure també l'històric complet dels teus hàbits.
|
|
||||||
|
|
||||||
<b>Planificació flexible</b>
|
|
||||||
Suporta tant hàbits diaris com hàbits amb planificacions més complexes, com per exemple 3 vegades per setmana; un cop setmana si setmana no; o un dia si i altre no.
|
|
||||||
|
|
||||||
<b>Recordatoris</b>
|
|
||||||
Crea un recordatori individual per a cada hàbit, a l'hora escollida del dia. Revisa fàcilment, endarrereix o anul·la el teu hàbit directament des de la notificació, sense obrir l'app.
|
|
||||||
|
|
||||||
<b>Completament de codi obert i lliure de publicitat</b>
|
|
||||||
No hi ha cap tipus de publicitat, notificacions molestes o permisos intrusius en aquesta app, i mai n'hi haurà. El codi font complet està disponible sota la llicència GPLv3.
|
|
||||||
|
|
||||||
<b>Optimitzat per a rellotges intel·ligents</b>
|
|
||||||
Els recordatoris poden ser revisats, endarrerits o anul·lats directament des del teu rellotge Android Wear.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Crea bons hàbits i segueix el seu progrés al llarg del temps (sense publicitat)
|
|
||||||