├── .gitignore
├── LICENSE
├── PRIVACY.md
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── debug.keystore
├── proguard-rules.pro
├── schemas
│ └── org.kde.bettercounter.boilerplate.AppDatabase
│ │ └── 3.json
└── src
│ ├── debug
│ └── res
│ │ └── values
│ │ └── strings.xml
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── org
│ │ │ └── kde
│ │ │ └── bettercounter
│ │ │ ├── BetterApplication.kt
│ │ │ ├── boilerplate
│ │ │ ├── AppDatabase.kt
│ │ │ ├── Converters.kt
│ │ │ ├── CreateFileResultContract.kt
│ │ │ ├── DragAndSwipeTouchHelper.kt
│ │ │ ├── ImeHelper.kt
│ │ │ └── OpenFileResultContract.kt
│ │ │ ├── extensions
│ │ │ ├── CalendarExtension.kt
│ │ │ ├── ChronoUnitExtension.kt
│ │ │ ├── DateExtension.kt
│ │ │ ├── Dimensions.kt
│ │ │ ├── LogExtension.kt
│ │ │ ├── LongExtensions.kt
│ │ │ └── ZonedDateTime.kt
│ │ │ ├── persistence
│ │ │ ├── AverageMode.kt
│ │ │ ├── CounterColors.kt
│ │ │ ├── CounterMetadata.kt
│ │ │ ├── CounterSummary.kt
│ │ │ ├── Entry.kt
│ │ │ ├── EntryDao.kt
│ │ │ ├── Exporter.kt
│ │ │ ├── Interval.kt
│ │ │ ├── Repository.kt
│ │ │ └── Tutorial.kt
│ │ │ └── ui
│ │ │ ├── chart
│ │ │ ├── BetterChart.kt
│ │ │ ├── ChartHolder.kt
│ │ │ └── ChartsAdapter.kt
│ │ │ ├── editdialog
│ │ │ ├── ColorAdapter.kt
│ │ │ ├── CounterSettingsDialogBuilder.kt
│ │ │ └── IntervalAdapter.kt
│ │ │ ├── main
│ │ │ ├── BetterRelativeTimeTextView.kt
│ │ │ ├── DateTimePicker.kt
│ │ │ ├── EntryListViewAdapter.kt
│ │ │ ├── EntryViewHolder.kt
│ │ │ ├── MainActivity.kt
│ │ │ └── MainActivityViewModel.kt
│ │ │ ├── settings
│ │ │ ├── SettingsActivity.kt
│ │ │ └── SettingsViewModel.kt
│ │ │ └── widget
│ │ │ ├── WidgetConfigureActivity.kt
│ │ │ ├── WidgetProvider.kt
│ │ │ └── WidgetViewModel.kt
│ └── res
│ │ ├── color
│ │ └── outlinebox.xml
│ │ ├── drawable-nodpi
│ │ └── widget_preview.png
│ │ ├── drawable
│ │ ├── bg_color_circle.xml
│ │ ├── button_state_color.xml
│ │ ├── ic_add.xml
│ │ ├── ic_check.xml
│ │ ├── ic_edit.xml
│ │ ├── ic_minusone.xml
│ │ ├── ic_plusone.xml
│ │ ├── ic_time.xml
│ │ ├── ripple_color_1.xml
│ │ ├── ripple_color_2.xml
│ │ ├── ripple_color_3.xml
│ │ ├── ripple_color_4.xml
│ │ ├── ripple_color_5.xml
│ │ ├── ripple_color_6.xml
│ │ ├── ripple_color_7.xml
│ │ ├── ripple_color_8.xml
│ │ └── ripple_color_default.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ ├── activity_settings.xml
│ │ ├── color_circle.xml
│ │ ├── counter_settings.xml
│ │ ├── fragment_chart.xml
│ │ ├── fragment_entry.xml
│ │ ├── progress_dialog.xml
│ │ ├── widget.xml
│ │ ├── widget_configure.xml
│ │ └── widget_preview.xml
│ │ ├── menu
│ │ ├── main.xml
│ │ └── popup_menu.xml
│ │ ├── mipmap-anydpi-v26
│ │ └── ic_launcher.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_adaptive_back.png
│ │ ├── ic_launcher_adaptive_fore.png
│ │ └── ic_launcher_monochrome.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_adaptive_back.png
│ │ ├── ic_launcher_adaptive_fore.png
│ │ └── ic_launcher_monochrome.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_adaptive_back.png
│ │ ├── ic_launcher_adaptive_fore.png
│ │ └── ic_launcher_monochrome.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_adaptive_back.png
│ │ ├── ic_launcher_adaptive_fore.png
│ │ └── ic_launcher_monochrome.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_adaptive_back.png
│ │ ├── ic_launcher_adaptive_fore.png
│ │ └── ic_launcher_monochrome.png
│ │ ├── resources.properties
│ │ ├── values-ca
│ │ └── strings.xml
│ │ ├── values-da
│ │ └── strings.xml
│ │ ├── values-de
│ │ └── strings.xml
│ │ ├── values-es
│ │ └── strings.xml
│ │ ├── values-fr
│ │ └── strings.xml
│ │ ├── values-ru
│ │ └── strings.xml
│ │ ├── values-tr
│ │ └── strings.xml
│ │ ├── values-zh-rCN
│ │ └── strings.xml
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── size_constants.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ │ └── xml
│ │ └── widget_info.xml
│ └── test
│ └── java
│ └── org
│ └── kde
│ └── bettercounter
│ └── extensions
│ ├── CalendarExtensionTest.kt
│ ├── ChronoUnitExtensionTest.kt
│ └── ImportTest.kt
├── build.gradle.kts
├── fastlane
├── Appfile
└── metadata
│ └── android
│ ├── ca
│ ├── full_description.txt
│ ├── short_description.txt
│ └── title.txt
│ ├── da-DK
│ ├── full_description.txt
│ ├── short_description.txt
│ └── title.txt
│ ├── en-US
│ ├── changelogs
│ │ ├── 30000.txt
│ │ ├── 30001.txt
│ │ ├── 30100.txt
│ │ ├── 30200.txt
│ │ ├── 40000.txt
│ │ ├── 40001.txt
│ │ ├── 40100.txt
│ │ ├── 40200.txt
│ │ ├── 40300.txt
│ │ ├── 40301.txt
│ │ ├── 40400.txt
│ │ ├── 40401.txt
│ │ ├── 40500.txt
│ │ ├── 40600.txt
│ │ ├── 40601.txt
│ │ ├── 40602.txt
│ │ ├── 40603.txt
│ │ ├── 40700.txt
│ │ ├── 40800.txt
│ │ ├── 40900.txt
│ │ ├── 40901.txt
│ │ ├── 40902.txt
│ │ ├── 40903.txt
│ │ ├── 40904.txt
│ │ ├── 41000.txt
│ │ ├── 41001.txt
│ │ ├── 41002.txt
│ │ ├── 50001.txt
│ │ ├── 50003.txt
│ │ └── 50100.txt
│ ├── full_description.txt
│ ├── images
│ │ ├── featureGraphic.png
│ │ ├── icon.png
│ │ └── phoneScreenshots
│ │ │ ├── 1.png
│ │ │ ├── 2.png
│ │ │ ├── 3.png
│ │ │ └── 4.png
│ ├── short_description.txt
│ └── title.txt
│ ├── es-ES
│ ├── full_description.txt
│ ├── short_description.txt
│ └── title.txt
│ └── fr-FR
│ ├── full_description.txt
│ ├── short_description.txt
│ └── title.txt
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── screenshot.png
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 | .kotlin
11 |
--------------------------------------------------------------------------------
/PRIVACY.md:
--------------------------------------------------------------------------------
1 | BetterCounter works offline, so no personal information is sent to any server or ever leaves your device.
2 |
3 | Note, however, that if your Android phone has [Backup by Google One](https://support.google.com/android/answer/2819582) enabled, then Google will make a periodic backup of the app's data to their servers.
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BetterCounter
2 |
3 |
4 |
5 | ### A multi-purpose counter app
6 |
7 | - Track good and bad habits (eg: exercising, smoking, drinking...)
8 | - Track when's the last time you did something (eg: water your plants, change your bedsheets, poop...)
9 | - Count your lives in MtG
10 |
11 | ### Get it now!
12 |
13 | - From [F-Droid](https://f-droid.org/vi/packages/org.kde.bettercounter/)
14 | - From the [Play Store](https://play.google.com/store/apps/details?id=org.kde.bettercounter)
15 |
16 | ### Features
17 |
18 | - Records the date and time of each individual counter increase.
19 | - Graphs your data over time and calculates statistics.
20 | - Lets you export your data (eg: to analyze it with your choice of tools).
21 | - Your data is never sent to any server (except for Google's app backup, if enabled).
22 | - Simple as hell and will stay this way.
23 |
24 | ### Screens
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | buildscript {
2 | dependencies {
3 | classpath(libs.android.gradlePlugin)
4 | classpath(libs.kotlin.gradlePlugin)
5 | }
6 | }
7 |
8 | plugins {
9 | alias(libs.plugins.android.application)
10 | alias(libs.plugins.kotlin.android)
11 | alias(libs.plugins.google.devtools.ksp)
12 | }
13 |
14 | android {
15 | namespace = "org.kde.bettercounter"
16 | compileSdk = 35
17 | defaultConfig {
18 | applicationId = "org.kde.bettercounter"
19 | minSdk = 21
20 | targetSdk = 35
21 | versionCode = 50100
22 | versionName = "5.1.0"
23 |
24 | javaCompileOptions {
25 | annotationProcessorOptions {
26 | argument("room.schemaLocation", "$projectDir/schemas")
27 | }
28 | }
29 | }
30 | buildFeatures {
31 | viewBinding = true
32 | buildConfig = true
33 | }
34 | compileOptions {
35 | isCoreLibraryDesugaringEnabled = true
36 | sourceCompatibility = JavaVersion.VERSION_17
37 | targetCompatibility = JavaVersion.VERSION_17
38 | }
39 | kotlinOptions {
40 | jvmTarget = "17"
41 | }
42 | androidResources {
43 | generateLocaleConfig = true
44 | }
45 | signingConfigs {
46 | getByName("debug") {
47 | storeFile = file("debug.keystore")
48 | storePassword = "android"
49 | keyAlias = "androiddebugkey"
50 | keyPassword = "android"
51 | }
52 | }
53 | buildTypes {
54 | getByName("release") {
55 | isMinifyEnabled = true
56 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
57 | }
58 | getByName("debug") {
59 | signingConfig = signingConfigs.getByName("debug")
60 | applicationIdSuffix = ".debug"
61 | versionNameSuffix = " Dev"
62 | }
63 | }
64 | }
65 |
66 | dependencies {
67 | implementation(libs.douglasjunior.android.simple.tooltip)
68 | implementation(libs.appdevnext.mpAndroidChart)
69 | implementation(libs.androidx.core.ktx)
70 | implementation(libs.androidx.appcompat)
71 | implementation(libs.androidx.lifecycle.viewmodel.ktx)
72 | implementation(libs.androidx.room.runtime)
73 | ksp(libs.androidx.room.compiler)
74 | implementation(libs.androidx.room.ktx)
75 | implementation(libs.androidx.recyclerview)
76 | implementation(libs.material)
77 |
78 | coreLibraryDesugaring(libs.android.desugarJdkLibs) // Chrono.UNITS for Android < 26
79 | testImplementation(libs.junit)
80 | }
81 |
--------------------------------------------------------------------------------
/app/debug.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/app/debug.keystore
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
23 | -dontobfuscate
--------------------------------------------------------------------------------
/app/schemas/org.kde.bettercounter.boilerplate.AppDatabase/3.json:
--------------------------------------------------------------------------------
1 | {
2 | "formatVersion": 1,
3 | "database": {
4 | "version": 3,
5 | "identityHash": "82d3ea18843988649a6790a509244339",
6 | "entities": [
7 | {
8 | "tableName": "Entry",
9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `date` INTEGER NOT NULL, `name` TEXT NOT NULL)",
10 | "fields": [
11 | {
12 | "fieldPath": "id",
13 | "columnName": "id",
14 | "affinity": "INTEGER",
15 | "notNull": false
16 | },
17 | {
18 | "fieldPath": "date",
19 | "columnName": "date",
20 | "affinity": "INTEGER",
21 | "notNull": true
22 | },
23 | {
24 | "fieldPath": "name",
25 | "columnName": "name",
26 | "affinity": "TEXT",
27 | "notNull": true
28 | }
29 | ],
30 | "primaryKey": {
31 | "columnNames": [
32 | "id"
33 | ],
34 | "autoGenerate": true
35 | },
36 | "indices": [
37 | {
38 | "name": "index_Entry_name",
39 | "unique": false,
40 | "columnNames": [
41 | "name"
42 | ],
43 | "orders": [],
44 | "createSql": "CREATE INDEX IF NOT EXISTS `index_Entry_name` ON `${TABLE_NAME}` (`name`)"
45 | }
46 | ],
47 | "foreignKeys": []
48 | }
49 | ],
50 | "views": [],
51 | "setupQueries": [
52 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
53 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '82d3ea18843988649a6790a509244339')"
54 | ]
55 | }
56 | }
--------------------------------------------------------------------------------
/app/src/debug/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | AAA Dev BetterCounter
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
30 |
31 |
32 |
35 |
36 |
37 |
38 |
39 |
40 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/BetterApplication.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter
2 |
3 | import android.app.Application
4 | import android.util.Log
5 |
6 |
7 | class BetterApplication : Application() {
8 |
9 | override fun onCreate() {
10 | super.onCreate()
11 | Log.d("BetterApplication", "onCreate")
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/boilerplate/AppDatabase.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.boilerplate
2 |
3 | import android.content.Context
4 | import androidx.room.Database
5 | import androidx.room.Room
6 | import androidx.room.RoomDatabase
7 | import androidx.room.TypeConverters
8 | import org.kde.bettercounter.persistence.Entry
9 | import org.kde.bettercounter.persistence.EntryDao
10 |
11 | @Database(entities = [Entry::class], version = 3)
12 | @TypeConverters(Converters::class)
13 | abstract class AppDatabase : RoomDatabase() {
14 | abstract fun entryDao(): EntryDao
15 |
16 | companion object {
17 | @Volatile
18 | private var INSTANCE: AppDatabase? = null
19 |
20 | fun getInstance(context: Context): AppDatabase =
21 | INSTANCE ?: synchronized(this) {
22 | INSTANCE ?: Room.databaseBuilder(
23 | context.applicationContext,
24 | AppDatabase::class.java, "appdb"
25 | ).build().also { INSTANCE = it }
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/boilerplate/Converters.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.boilerplate
2 |
3 | import androidx.room.TypeConverter
4 | import org.json.JSONArray
5 | import org.json.JSONException
6 | import java.util.Date
7 |
8 | class Converters {
9 | companion object {
10 | @TypeConverter
11 | @JvmStatic
12 | fun dateFromTimestamp(value: Long?): Date? {
13 | return value?.let { Date(it) }
14 | }
15 |
16 | @TypeConverter
17 | @JvmStatic
18 | fun dateToTimestamp(date: Date?): Long? {
19 | return date?.time
20 | }
21 |
22 | @JvmStatic
23 | fun stringListToString(list: List?): String {
24 | return JSONArray(list).toString()
25 | }
26 |
27 | @JvmStatic
28 | fun stringToStringList(jsonStr: String?): List {
29 | return try {
30 | val json = JSONArray(jsonStr)
31 | val ret: MutableList = ArrayList()
32 | for (i in 0 until json.length()) {
33 | ret.add(json.getString(i))
34 | }
35 | ret
36 | } catch (e: JSONException) {
37 | e.printStackTrace()
38 | emptyList()
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/boilerplate/CreateFileResultContract.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.boilerplate
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.net.Uri
7 | import androidx.activity.result.contract.ActivityResultContract
8 |
9 | data class CreateFileParams(
10 | val fileMimeType: String,
11 | val suggestedFileName: String,
12 | )
13 |
14 | class CreateFileResultContract : ActivityResultContract() {
15 |
16 | override fun createIntent(context: Context, input: CreateFileParams): Intent =
17 | Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
18 | addCategory(Intent.CATEGORY_OPENABLE)
19 | setTypeAndNormalize(input.fileMimeType)
20 | putExtra(Intent.EXTRA_TITLE, input.suggestedFileName)
21 | addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
22 | addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
23 | }
24 |
25 | override fun parseResult(resultCode: Int, intent: Intent?): Uri? = when (resultCode) {
26 | Activity.RESULT_OK -> intent?.data
27 | else -> null
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/boilerplate/DragAndSwipeTouchHelper.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.boilerplate
2 |
3 | import androidx.recyclerview.widget.ItemTouchHelper
4 | import androidx.recyclerview.widget.RecyclerView
5 |
6 | class DragAndSwipeTouchHelper(private val mAdapter: ListGesturesCallback) :
7 | ItemTouchHelper.Callback() {
8 |
9 | private var isDragging: Boolean = false
10 |
11 | override fun isLongPressDragEnabled(): Boolean = false // we start drag manually
12 | override fun isItemViewSwipeEnabled(): Boolean = true
13 |
14 | override fun getMovementFlags(
15 | recyclerView: RecyclerView,
16 | viewHolder: RecyclerView.ViewHolder,
17 | ): Int {
18 | val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
19 | val swipeFlags = 0
20 | return makeMovementFlags(dragFlags, swipeFlags)
21 | }
22 |
23 | override fun onSwiped(viewHolder: RecyclerView.ViewHolder, swipeDir: Int) {
24 | }
25 |
26 | override fun onMove(
27 | recyclerView: RecyclerView,
28 | viewHolder: RecyclerView.ViewHolder,
29 | target: RecyclerView.ViewHolder
30 | ): Boolean {
31 | mAdapter.onMove(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition)
32 | return true
33 | }
34 |
35 | override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
36 | if (isDragging) {
37 | isDragging = false
38 | mAdapter.onDragEnd(viewHolder)
39 | }
40 | super.clearView(recyclerView, viewHolder)
41 | }
42 |
43 | override fun onSelectedChanged(
44 | viewHolder: RecyclerView.ViewHolder?,
45 | actionState: Int,
46 | ) {
47 | if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
48 | isDragging = true
49 | mAdapter.onDragStart(viewHolder)
50 | }
51 | super.onSelectedChanged(viewHolder, actionState)
52 | }
53 |
54 | interface ListGesturesCallback {
55 | fun onMove(fromPosition: Int, toPosition: Int)
56 | fun onDragStart(viewHolder: RecyclerView.ViewHolder?)
57 | fun onDragEnd(viewHolder: RecyclerView.ViewHolder?)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/boilerplate/ImeHelper.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.boilerplate
2 |
3 | import android.content.Context.INPUT_METHOD_SERVICE
4 | import android.view.View
5 | import android.view.inputmethod.InputMethodManager
6 | import androidx.core.view.ViewCompat
7 | import androidx.core.view.WindowInsetsCompat
8 |
9 | fun isKeyboardVisible(rootView: View): Boolean {
10 | val imeInsets = ViewCompat.getRootWindowInsets(rootView)?.getInsets(WindowInsetsCompat.Type.ime()) ?: return false
11 | return imeInsets.bottom > 0
12 | }
13 |
14 | fun hideKeyboard(rootView: View) {
15 | val imm = rootView.context.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
16 | imm.hideSoftInputFromWindow(rootView.windowToken, 0)
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/boilerplate/OpenFileResultContract.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.boilerplate
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.net.Uri
7 | import androidx.activity.result.contract.ActivityResultContract
8 |
9 | data class OpenFileParams(
10 | val fileMimeType: String,
11 | )
12 |
13 | class OpenFileResultContract : ActivityResultContract() {
14 |
15 | override fun createIntent(context: Context, input: OpenFileParams): Intent =
16 | Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
17 | addCategory(Intent.CATEGORY_OPENABLE)
18 | setTypeAndNormalize(input.fileMimeType)
19 | }
20 |
21 | override fun parseResult(resultCode: Int, intent: Intent?): Uri? = when (resultCode) {
22 | Activity.RESULT_OK -> intent?.data
23 | else -> null
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/extensions/CalendarExtension.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.extensions
2 |
3 | import org.kde.bettercounter.persistence.Interval
4 | import java.text.SimpleDateFormat
5 | import java.util.Calendar
6 | import java.util.Locale
7 |
8 | fun Calendar.truncated(field: Int): Calendar {
9 | val cal = copy()
10 | cal.set(Calendar.MILLISECOND, 0)
11 | cal.set(Calendar.SECOND, 0)
12 | if (field == Calendar.MINUTE) return cal
13 | cal.set(Calendar.MINUTE, 0)
14 | if (field == Calendar.HOUR_OF_DAY || field == Calendar.HOUR) return cal
15 | cal.set(Calendar.HOUR_OF_DAY, 0)
16 | if (field in listOf(Calendar.DATE, Calendar.DAY_OF_WEEK, Calendar.DAY_OF_MONTH, Calendar.DAY_OF_YEAR)) return cal
17 | if (field in listOf(Calendar.WEEK_OF_YEAR, Calendar.WEEK_OF_MONTH)) {
18 | val dow = cal.get(Calendar.DAY_OF_WEEK)
19 | val offset = if (dow < firstDayOfWeek) 7 - (firstDayOfWeek - dow) else dow - firstDayOfWeek
20 | cal.add(Calendar.DAY_OF_MONTH, -offset)
21 | return cal
22 | }
23 | cal.set(Calendar.DATE, 1)
24 | if (field == Calendar.MONTH) return cal
25 | cal.set(Calendar.MONTH, Calendar.JANUARY)
26 | if (field == Calendar.YEAR) return cal
27 | throw UnsupportedOperationException("truncate by $field not implemented")
28 | }
29 |
30 | fun Calendar.truncated(field: Interval): Calendar = truncated(field.toChronoUnit().toCalendarField())
31 |
32 | fun Calendar.debugToSimpleDateString(): String {
33 | val dateFormat = SimpleDateFormat("dd-MM-yyyy HH:mm", Locale.US)
34 | return dateFormat.format(time)
35 | }
36 |
37 | fun Calendar.copy(): Calendar = clone() as Calendar
38 |
39 | fun Calendar.plusInterval(interval: Interval, times: Int): Calendar {
40 | val cal = copy()
41 | when (interval) {
42 | Interval.HOUR -> cal.add(Calendar.HOUR_OF_DAY, 1 * times)
43 | Interval.DAY -> cal.add(Calendar.DAY_OF_YEAR, 1 * times)
44 | Interval.WEEK -> cal.add(Calendar.DAY_OF_YEAR, 7 * times)
45 | Interval.MONTH -> cal.add(Calendar.MONTH, 1 * times)
46 | Interval.YEAR -> cal.add(Calendar.YEAR, 1 * times)
47 | Interval.LIFETIME -> cal.add(Calendar.YEAR, 1000)
48 | }
49 | return cal
50 | }
51 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/extensions/ChronoUnitExtension.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.extensions
2 |
3 | import java.time.LocalDateTime
4 | import java.time.ZoneId
5 | import java.time.temporal.ChronoUnit
6 | import java.util.Calendar
7 | import java.util.Date
8 |
9 | // Rounds up to the nearest integer. Both dates included. Eg: returns 2 weeks from Monday at 00:00 to next Monday at 00:00
10 | fun ChronoUnit.count(fromDate: Date, toDate: Date): Int {
11 | val calendarField = toCalendarField()
12 | val truncatedFrom = fromDate.toCalendar().truncated(calendarField)
13 | val truncatedTo = toDate.toCalendar().truncated(calendarField)
14 | return between(truncatedFrom, truncatedTo).toInt() + 1
15 | }
16 |
17 | fun ChronoUnit.between(from: Calendar, to: Calendar): Long {
18 | val systemTz = ZoneId.systemDefault()
19 | // ChronoUnit.between can use both ZonedDateTime and Instant, however,
20 | // ChronoUnit.WEEK.between doesn't work for Instant for some reason.
21 | val fromZonedDateTime = from.toInstant().atZone(systemTz)
22 | val toZonedDateTime = to.toInstant().atZone(systemTz)
23 | return between(fromZonedDateTime, toZonedDateTime)
24 | }
25 |
26 | fun ChronoUnit.toCalendarField(): Int =
27 | when (this) {
28 | ChronoUnit.HOURS -> Calendar.HOUR
29 | ChronoUnit.DAYS -> Calendar.DAY_OF_WEEK
30 | ChronoUnit.WEEKS -> Calendar.WEEK_OF_YEAR
31 | ChronoUnit.MONTHS -> Calendar.MONTH
32 | ChronoUnit.YEARS -> Calendar.YEAR
33 | else -> throw UnsupportedOperationException("$this can't be converted to Calendar field")
34 | }
35 |
36 | fun millisecondsUntilNextHour(): Long {
37 | val current = LocalDateTime.now()
38 | val nextHour = current.truncatedTo(ChronoUnit.HOURS).plusHours(1)
39 | return ChronoUnit.MILLIS.between(current, nextHour)
40 | }
41 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/extensions/DateExtension.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.extensions
2 |
3 | import java.util.Calendar
4 | import java.util.Date
5 |
6 | fun Date.toCalendar(): Calendar {
7 | val cal = Calendar.getInstance()
8 | cal.time = this
9 | return cal
10 | }
11 |
12 | fun max(a: Date, b: Date): Date {
13 | return if (a.time > b.time) a else b
14 | }
15 |
16 | fun min(a: Date, b: Date): Date {
17 | return if (a.time < b.time) a else b
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/extensions/Dimensions.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.extensions
2 |
3 | import android.content.Context
4 |
5 | fun Int.dpToPx(context: Context): Int {
6 | val density = context.resources.displayMetrics.density
7 | return (this * density).toInt()
8 | }
9 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/extensions/LogExtension.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.extensions
2 |
3 | import android.util.Log
4 |
5 | fun T.andLog(tag: String = "LOGGERINO"): T {
6 | Log.e(tag, this.toString())
7 | return this
8 | }
9 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/extensions/LongExtensions.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.extensions
2 |
3 | import java.time.Instant
4 | import java.time.LocalDateTime
5 | import java.time.ZoneId
6 | import java.time.ZoneOffset
7 |
8 | fun Long.toLocalDateTime(): LocalDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(this), ZoneId.systemDefault())
9 |
10 | fun Long.toUTCLocalDateTime(): LocalDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(this), ZoneId.ofOffset("UTC", ZoneOffset.UTC))
11 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/extensions/ZonedDateTime.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.extensions
2 |
3 | import java.time.ZonedDateTime
4 |
5 | fun ZonedDateTime.toEpochMilli(): Long = toInstant().toEpochMilli()
6 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/persistence/AverageMode.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.persistence
2 |
3 | enum class AverageMode {
4 | FIRST_TO_NOW, // Average between first entry and now
5 | FIRST_TO_LAST // Average between first entry and last entry
6 | }
7 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/persistence/CounterColors.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.persistence
2 |
3 | import android.content.Context
4 | import androidx.core.content.ContextCompat
5 | import org.kde.bettercounter.R
6 |
7 | @JvmInline
8 | value class CounterColor(val colorInt: Int)
9 |
10 | class CounterColors {
11 | val defaultColor : CounterColor
12 | val allColors : List
13 | val rippleDrawables : Map
14 | val defaultColorIntForChart : Int
15 |
16 | private constructor(context : Context) {
17 | defaultColor = CounterColor(ContextCompat.getColor(context, R.color.colorLightBackground))
18 | defaultColorIntForChart = ContextCompat.getColor(context, R.color.colorAccent)
19 | rippleDrawables = mapOf(
20 | defaultColor.colorInt to R.drawable.ripple_color_default,
21 | ContextCompat.getColor(context, R.color.color1) to R.drawable.ripple_color_1,
22 | ContextCompat.getColor(context, R.color.color2) to R.drawable.ripple_color_2,
23 | ContextCompat.getColor(context, R.color.color3) to R.drawable.ripple_color_3,
24 | ContextCompat.getColor(context, R.color.color4) to R.drawable.ripple_color_4,
25 | ContextCompat.getColor(context, R.color.color5) to R.drawable.ripple_color_5,
26 | ContextCompat.getColor(context, R.color.color6) to R.drawable.ripple_color_6,
27 | ContextCompat.getColor(context, R.color.color7) to R.drawable.ripple_color_7,
28 | ContextCompat.getColor(context, R.color.color8) to R.drawable.ripple_color_8,
29 | )
30 | allColors = listOf(
31 | defaultColor,
32 | CounterColor(ContextCompat.getColor(context, R.color.color1)),
33 | CounterColor(ContextCompat.getColor(context, R.color.color2)),
34 | CounterColor(ContextCompat.getColor(context, R.color.color3)),
35 | CounterColor(ContextCompat.getColor(context, R.color.color4)),
36 | CounterColor(ContextCompat.getColor(context, R.color.color5)),
37 | CounterColor(ContextCompat.getColor(context, R.color.color6)),
38 | CounterColor(ContextCompat.getColor(context, R.color.color7)),
39 | CounterColor(ContextCompat.getColor(context, R.color.color8)),
40 | )
41 | }
42 |
43 | fun getRippleDrawableRes(counterColor: CounterColor): Int? {
44 | return rippleDrawables[counterColor.colorInt]
45 | }
46 |
47 | fun getColorIntForChart(counterColor: CounterColor): Int {
48 | return if (counterColor == defaultColor) {
49 | defaultColorIntForChart
50 | } else {
51 | counterColor.colorInt
52 | }
53 | }
54 |
55 | companion object {
56 | @Volatile
57 | private var INSTANCE: CounterColors? = null
58 |
59 | fun getInstance(context: Context): CounterColors =
60 | INSTANCE ?: synchronized(this) {
61 | INSTANCE ?: CounterColors(context.applicationContext)
62 | .also { INSTANCE = it }
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/persistence/CounterMetadata.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.persistence
2 |
3 | data class CounterMetadata(
4 | var name: String,
5 | var interval: Interval,
6 | var goal: Int,
7 | var color: CounterColor,
8 | )
9 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/persistence/CounterSummary.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.persistence
2 |
3 | import java.util.Calendar
4 | import java.util.Date
5 |
6 | class CounterSummary(
7 | var name: String,
8 | var interval: Interval,
9 | var goal: Int,
10 | var color: CounterColor,
11 | var lastIntervalCount: Int,
12 | var totalCount: Int,
13 | var leastRecent: Date?,
14 | var mostRecent: Date?,
15 | ) {
16 | fun latestBetweenNowAndMostRecentEntry(): Date {
17 | val now = Calendar.getInstance().time
18 | val lastEntry = mostRecent
19 | return if (lastEntry != null && lastEntry > now) lastEntry else now
20 | }
21 |
22 | fun isGoalMet(): Boolean {
23 | return goal in 1..lastIntervalCount
24 | }
25 |
26 | fun getFormattedCount(): CharSequence = buildString {
27 | append(lastIntervalCount)
28 | if (goal > 0) {
29 | append('/')
30 | append(goal)
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/persistence/Entry.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.persistence
2 |
3 | import androidx.room.Entity
4 | import androidx.room.Index
5 | import androidx.room.PrimaryKey
6 | import androidx.room.TypeConverters
7 | import org.kde.bettercounter.boilerplate.Converters
8 | import java.util.Date
9 |
10 | @Entity(indices = [Index("name")])
11 | @TypeConverters(Converters::class)
12 | data class Entry(
13 | @PrimaryKey(autoGenerate = true) val id: Int? = null,
14 | val date: Date,
15 | val name: String,
16 | )
17 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/persistence/EntryDao.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.persistence
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Delete
5 | import androidx.room.Insert
6 | import androidx.room.OnConflictStrategy
7 | import androidx.room.Query
8 | import java.util.Date
9 |
10 | @Dao
11 | interface EntryDao {
12 |
13 | @Query("SELECT * FROM entry WHERE name = (:name) ORDER BY id DESC LIMIT 1")
14 | suspend fun getLastAdded(name: String): Entry?
15 |
16 | @Query("SELECT * FROM entry WHERE name = (:name) ORDER BY date DESC LIMIT 1")
17 | suspend fun getMostRecent(name: String): Entry?
18 |
19 | @Query("SELECT * FROM entry WHERE name = (:name) ORDER BY date ASC LIMIT 1")
20 | suspend fun getLeastRecent(name: String): Entry?
21 |
22 | @Query("SELECT COUNT(*) FROM entry WHERE name = (:name)")
23 | suspend fun getCount(name: String): Int
24 |
25 | @Query("SELECT date FROM entry WHERE name = (:name) ORDER BY date ASC LIMIT 1")
26 | suspend fun getFirstDate(name: String): Date?
27 |
28 | @Query("SELECT date FROM entry WHERE name = (:name) ORDER BY date DESC LIMIT 1")
29 | suspend fun getLastDate(name: String): Date?
30 |
31 | @Query("SELECT COUNT(*) FROM entry WHERE name = (:name) AND date >= (:since) AND date <= (:until)")
32 | suspend fun getCountInRange(name: String, since: Date, until: Date): Int
33 |
34 | @Query("UPDATE entry set name = (:newName) WHERE name = (:oldName)")
35 | suspend fun renameAllEntries(oldName: String, newName: String): Int
36 |
37 | @Query("SELECT * FROM entry WHERE name = (:name) AND date >= (:since) AND date <= (:until) ORDER BY date ASC")
38 | suspend fun getAllEntriesInRangeSortedByDate(name: String, since: Date, until: Date): List
39 |
40 | @Query("SELECT * FROM entry WHERE name = (:name) ORDER BY date ASC")
41 | suspend fun getAllEntriesSortedByDate(name: String): List
42 |
43 | @Insert(onConflict = OnConflictStrategy.REPLACE)
44 | suspend fun insert(entry: Entry)
45 |
46 | @Insert(onConflict = OnConflictStrategy.REPLACE)
47 | suspend fun bulkInsert(entry: List)
48 |
49 | @Delete
50 | suspend fun delete(entry: Entry)
51 |
52 | @Query("DELETE FROM entry WHERE name = (:name)")
53 | suspend fun deleteAll(name: String)
54 | }
55 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/persistence/Exporter.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.persistence
2 |
3 | import android.app.Application
4 | import android.widget.Toast
5 | import androidx.core.net.toUri
6 | import kotlinx.coroutines.CoroutineScope
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.Job
9 | import kotlinx.coroutines.launch
10 | import org.kde.bettercounter.R
11 | import java.io.OutputStream
12 |
13 | class Exporter(val application: Application, val repo: Repository) {
14 |
15 | companion object {
16 | private var autoExportJob : Job? = null
17 | }
18 |
19 | fun autoExportIfEnabled() {
20 | if (!repo.isAutoExportOnSaveEnabled()) {
21 | return
22 | }
23 | autoExportJob?.cancel() // Ensure we don't have two exports running in parallel
24 | autoExportJob = CoroutineScope(Dispatchers.Main).launch {
25 | try {
26 | val uri = repo.getAutoExportFileUri()!!.toUri()
27 | application.contentResolver.openOutputStream(uri, "wt")?.let { stream ->
28 | exportAll(stream) { }
29 | }
30 | } catch (_: Exception) {
31 | repo.setAutoExportOnSave(false)
32 | Toast.makeText(application, R.string.export_error, Toast.LENGTH_SHORT).show()
33 | }
34 | }
35 | }
36 |
37 | suspend fun exportAll(stream: OutputStream, progressCallback: (progress: Int) -> Unit) {
38 | stream.use {
39 | it.bufferedWriter().use { writer ->
40 | for ((i, name) in repo.getCounterList().withIndex()) {
41 | progressCallback(i)
42 | val entries = repo.getAllEntriesSortedByDate(name)
43 | writer.write(name)
44 | for (entry in entries) {
45 | writer.write(",")
46 | writer.write(entry.date.time.toString())
47 | }
48 | writer.write("\n")
49 | }
50 | progressCallback(repo.getCounterList().size)
51 | }
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/persistence/Interval.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.persistence
2 |
3 | import android.content.Context
4 | import org.kde.bettercounter.R
5 | import java.time.temporal.ChronoUnit
6 | import java.util.Calendar
7 |
8 | enum class Interval(val humanReadableResource: Int) {
9 | HOUR(R.string.interval_hour),
10 | DAY(R.string.interval_day),
11 | WEEK(R.string.interval_week),
12 | MONTH(R.string.interval_month),
13 | YEAR(R.string.interval_year),
14 | LIFETIME(R.string.interval_lifetime),
15 | ;
16 |
17 | companion object {
18 | val DEFAULT = LIFETIME
19 |
20 | fun humanReadableValues(context: Context): List {
21 | return entries.map { context.getString(it.humanReadableResource) }
22 | }
23 | }
24 |
25 | fun asCalendarField(): Int = when (this) {
26 | HOUR -> Calendar.MINUTE
27 | DAY -> Calendar.HOUR_OF_DAY
28 | WEEK -> Calendar.DAY_OF_WEEK
29 | MONTH -> Calendar.DAY_OF_MONTH
30 | YEAR -> Calendar.MONTH
31 | LIFETIME -> Calendar.MONTH // Not really, but :shrug:
32 | }
33 |
34 | fun toChronoUnit(): ChronoUnit =
35 | when (this) {
36 | HOUR -> ChronoUnit.HOURS
37 | DAY -> ChronoUnit.DAYS
38 | WEEK -> ChronoUnit.WEEKS
39 | MONTH -> ChronoUnit.MONTHS
40 | YEAR -> ChronoUnit.YEARS
41 | LIFETIME -> throw UnsupportedOperationException("$this can't be converted to ChronoUnit")
42 | }
43 |
44 | fun toHumanReadableResourceId(): Int = humanReadableResource
45 |
46 | fun toChartDisplayableInterval(): Interval {
47 | // When displaying in a chart, LIFETIME counters will still display year by year
48 | return if (this == LIFETIME) YEAR else this
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/persistence/Repository.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.persistence
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import android.content.SharedPreferences
6 | import androidx.core.content.edit
7 | import org.kde.bettercounter.boilerplate.AppDatabase
8 | import org.kde.bettercounter.boilerplate.Converters
9 | import org.kde.bettercounter.extensions.plusInterval
10 | import org.kde.bettercounter.extensions.truncated
11 | import java.util.Calendar
12 | import java.util.Date
13 |
14 | const val COUNTERS_PREFS_KEY = "counters"
15 | const val COUNTERS_INTERVAL_PREFS_KEY = "interval.%s"
16 | const val COUNTERS_COLOR_PREFS_KEY = "color.%s"
17 | const val COUNTERS_GOAL_PREFS_KEY = "goal.%s"
18 | const val TUTORIALS_PREFS_KEY = "tutorials"
19 | const val AUTO_EXPORT_ENABLED_KEY = "auto_export_enabled"
20 | const val AVERAGE_CALCULATION_MODE_KEY = "average_calculation_mode"
21 | const val AUTO_EXPORT_FILE_URI_KEY = "auto_export_file_uri"
22 |
23 | class Repository(
24 | private val application: Application,
25 | private val entryDao: EntryDao,
26 | private val sharedPref: SharedPreferences
27 | ) {
28 | companion object {
29 | fun create(application: Application): Repository {
30 | val db = AppDatabase.getInstance(application)
31 | val prefs : SharedPreferences = application.getSharedPreferences("prefs", Context.MODE_PRIVATE)
32 | return Repository(application, db.entryDao(), prefs)
33 | }
34 | }
35 |
36 | fun getCounterList(): List {
37 | val countersStr = sharedPref.getString(COUNTERS_PREFS_KEY, "[]")
38 | return Converters.stringToStringList(countersStr)
39 | }
40 |
41 | fun setCounterList(list: List) {
42 | val jsonStr = Converters.stringListToString(list)
43 | sharedPref.edit { putString(COUNTERS_PREFS_KEY, jsonStr) }
44 | }
45 |
46 | private fun getCounterColor(name: String): CounterColor {
47 | val key = COUNTERS_COLOR_PREFS_KEY.format(name)
48 | return CounterColor(sharedPref.getInt(key, CounterColors.getInstance(application).defaultColor.colorInt))
49 | }
50 |
51 | private fun getCounterInterval(name: String): Interval {
52 | val key = COUNTERS_INTERVAL_PREFS_KEY.format(name)
53 | val str = sharedPref.getString(key, null)
54 | return when (str) {
55 | "YTD" -> Interval.YEAR
56 | null -> Interval.DEFAULT
57 | else -> Interval.valueOf(str)
58 | }
59 | }
60 |
61 | private fun getCounterGoal(name: String): Int {
62 | val key = COUNTERS_GOAL_PREFS_KEY.format(name)
63 | return sharedPref.getInt(key, 0)
64 | }
65 |
66 | fun deleteCounterMetadata(name: String) {
67 | val colorKey = COUNTERS_COLOR_PREFS_KEY.format(name)
68 | val intervalKey = COUNTERS_INTERVAL_PREFS_KEY.format(name)
69 | val goalKey = COUNTERS_GOAL_PREFS_KEY.format(name)
70 | sharedPref.edit {
71 | remove(colorKey)
72 | remove(intervalKey)
73 | remove(goalKey)
74 | }
75 | }
76 |
77 | fun setCounterMetadata(counter: CounterMetadata) {
78 | val colorKey = COUNTERS_COLOR_PREFS_KEY.format(counter.name)
79 | val intervalKey = COUNTERS_INTERVAL_PREFS_KEY.format(counter.name)
80 | val goalKey = COUNTERS_GOAL_PREFS_KEY.format(counter.name)
81 | sharedPref.edit {
82 | putInt(colorKey, counter.color.colorInt)
83 | putString(intervalKey, counter.interval.toString())
84 | putInt(goalKey, counter.goal)
85 | }
86 | }
87 |
88 | suspend fun getLeastRecentEntry(name:String): Date? = entryDao.getFirstDate(name)
89 |
90 | suspend fun getCounterSummary(name: String): CounterSummary {
91 | val interval = getCounterInterval(name)
92 | val color = getCounterColor(name)
93 | val goal = getCounterGoal(name)
94 | val intervalStartDate = when (interval) {
95 | Interval.LIFETIME -> Calendar.getInstance().apply { set(Calendar.YEAR, 1990) }
96 | else -> Calendar.getInstance().truncated(interval)
97 | }
98 | val intervalEndDate = intervalStartDate.plusInterval(interval, 1)
99 | return CounterSummary(
100 | name = name,
101 | color = color,
102 | interval = interval,
103 | goal = goal,
104 | lastIntervalCount = entryDao.getCountInRange(name, intervalStartDate.time, intervalEndDate.time),
105 | totalCount = entryDao.getCount(name),
106 | leastRecent = entryDao.getFirstDate(name),
107 | mostRecent = entryDao.getLastDate(name),
108 | )
109 | }
110 |
111 | suspend fun renameCounter(oldName: String, newName: String) {
112 | entryDao.renameAllEntries(oldName, newName)
113 | }
114 |
115 | suspend fun addEntry(name: String, date: Date = Calendar.getInstance().time) {
116 | entryDao.insert(Entry(name = name, date = date))
117 | }
118 |
119 | suspend fun removeEntry(name: String): Date? {
120 | val entry = entryDao.getLastAdded(name)
121 | if (entry != null) {
122 | entryDao.delete(entry)
123 | }
124 | return entry?.date
125 | }
126 |
127 | suspend fun removeAllEntries(name: String) {
128 | entryDao.deleteAll(name)
129 | }
130 |
131 | suspend fun getEntriesForRangeSortedByDate(name: String, since: Date, until: Date): List {
132 | return entryDao.getAllEntriesInRangeSortedByDate(name, since, until)
133 | }
134 |
135 | suspend fun getAllEntriesSortedByDate(name: String): List {
136 | return entryDao.getAllEntriesSortedByDate(name)
137 | }
138 |
139 | suspend fun bulkAddEntries(entries: List) {
140 | entryDao.bulkInsert(entries)
141 | }
142 |
143 | fun getTutorialsShown() : Set {
144 | return sharedPref.getStringSet(TUTORIALS_PREFS_KEY, setOf())!!
145 | }
146 |
147 | fun setTutorialsShown(tutorials: Set) {
148 | sharedPref.edit { putStringSet(TUTORIALS_PREFS_KEY, tutorials) }
149 | }
150 |
151 | fun isAutoExportOnSaveEnabled(): Boolean {
152 | return sharedPref.getBoolean(AUTO_EXPORT_ENABLED_KEY, false)
153 | }
154 |
155 | fun setAutoExportOnSave(enabled: Boolean) {
156 | sharedPref.edit { putBoolean(AUTO_EXPORT_ENABLED_KEY, enabled) }
157 | }
158 |
159 | fun getAutoExportFileUri(): String? {
160 | return sharedPref.getString(AUTO_EXPORT_FILE_URI_KEY, null)
161 | }
162 |
163 | fun setAutoExportFileUri(uriString: String) {
164 | sharedPref.edit { putString(AUTO_EXPORT_FILE_URI_KEY, uriString) }
165 | }
166 |
167 | fun getAverageCalculationMode(): AverageMode {
168 | val ordinal = sharedPref.getInt(AVERAGE_CALCULATION_MODE_KEY, AverageMode.FIRST_TO_LAST.ordinal)
169 | return AverageMode.entries[ordinal]
170 | }
171 |
172 | fun setAverageCalculationMode(mode: AverageMode) {
173 | sharedPref.edit { putInt(AVERAGE_CALCULATION_MODE_KEY, mode.ordinal) }
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/persistence/Tutorial.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.persistence
2 |
3 | import android.content.Context
4 | import android.view.Gravity
5 | import android.view.View
6 | import io.github.douglasjunior.androidSimpleTooltip.SimpleTooltip
7 | import org.kde.bettercounter.R
8 |
9 | enum class Tutorial {
10 | DRAG,
11 | PICK_DATE,
12 | CHANGE_GRAPH_INTERVAL,
13 | ;
14 |
15 | private fun getText() = when(this) {
16 | DRAG -> R.string.tutorial_drag
17 | PICK_DATE -> R.string.tutorial_pickdate
18 | CHANGE_GRAPH_INTERVAL -> R.string.tutorial_change_graph_interval
19 | }
20 |
21 | fun show(context: Context, anchorView : View, onDismissListener: SimpleTooltip.OnDismissListener? = null) {
22 | SimpleTooltip.Builder(context)
23 | .anchorView(anchorView)
24 | .text(getText())
25 | .gravity(Gravity.BOTTOM)
26 | .animated(true)
27 | .focusable(true) // modal requires focusable
28 | .modal(true)
29 | .onDismissListener(onDismissListener)
30 | .build()
31 | .show()
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/ui/chart/ChartsAdapter.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.ui.chart
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.appcompat.app.AppCompatActivity
6 | import androidx.recyclerview.widget.RecyclerView
7 | import kotlinx.coroutines.CoroutineScope
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.cancel
10 | import kotlinx.coroutines.flow.combine
11 | import kotlinx.coroutines.launch
12 | import org.kde.bettercounter.databinding.FragmentChartBinding
13 | import org.kde.bettercounter.extensions.between
14 | import org.kde.bettercounter.extensions.count
15 | import org.kde.bettercounter.extensions.plusInterval
16 | import org.kde.bettercounter.extensions.toCalendar
17 | import org.kde.bettercounter.extensions.truncated
18 | import org.kde.bettercounter.persistence.CounterSummary
19 | import org.kde.bettercounter.persistence.Interval
20 | import org.kde.bettercounter.ui.main.MainActivityViewModel
21 | import java.util.Calendar
22 |
23 | class ChartsAdapter(
24 | private val activity: AppCompatActivity,
25 | private val viewModel: MainActivityViewModel,
26 | private val counter: CounterSummary,
27 | private val interval: Interval,
28 | private val onIntervalChange: (Interval) -> Unit,
29 | private val onDateChange: ChartsAdapter.(Calendar) -> Unit,
30 | private val onDataDisplayed: () -> Unit,
31 | ) : RecyclerView.Adapter() {
32 |
33 | val coroutineScope = CoroutineScope(Dispatchers.Main)
34 |
35 | private val boundViewHolders = mutableListOf()
36 |
37 | private val inflater: LayoutInflater = LayoutInflater.from(activity)
38 |
39 | private var numCharts: Int = countNumCharts(counter)
40 | override fun getItemCount(): Int = numCharts
41 |
42 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChartHolder {
43 | val binding = FragmentChartBinding.inflate(inflater, parent, false)
44 | return ChartHolder(activity, viewModel, binding)
45 | }
46 |
47 | override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
48 | super.onDetachedFromRecyclerView(recyclerView)
49 | coroutineScope.cancel()
50 | }
51 |
52 | override fun onBindViewHolder(holder: ChartHolder, position: Int) {
53 | val rangeStart = findRangeStartForPosition(position)
54 | val rangeEnd = rangeStart.plusInterval(interval, 1)
55 | boundViewHolders.add(holder)
56 |
57 | val maxCountFlow = viewModel.getMaxCountForInterval(counter.name, interval)
58 | val entriesFlow = viewModel.getEntriesForRangeSortedByDate(
59 | counter.name,
60 | rangeStart.time,
61 | rangeEnd.time
62 | )
63 |
64 | coroutineScope.launch {
65 | combine(maxCountFlow, entriesFlow, ::Pair).collect { (maxCount, entries) ->
66 | holder.display(
67 | counter,
68 | entries,
69 | interval,
70 | rangeStart,
71 | rangeEnd,
72 | maxCount,
73 | onIntervalChange,
74 | ) { onDateChange(it) }
75 | onDataDisplayed()
76 | }
77 | }
78 | }
79 |
80 | private fun findRangeStartForPosition(position: Int): Calendar {
81 | val counterBegin = counter.leastRecent?.toCalendar() ?: Calendar.getInstance()
82 | val firstInterval = counterBegin.truncated(interval)
83 | return firstInterval.plusInterval(interval, position)
84 | }
85 |
86 | fun findPositionForRangeStart(cal: Calendar): Int {
87 | val endRange = counter.leastRecent
88 | if (endRange != null) {
89 | val endCal = endRange.toCalendar().truncated(interval)
90 | val count = interval.toChronoUnit().between(endCal, cal).toInt()
91 | return count.coerceIn(0, numCharts - 1)
92 | }
93 | return 0
94 | }
95 |
96 | override fun onViewRecycled(holder: ChartHolder) {
97 | boundViewHolders.remove(holder)
98 | }
99 |
100 | private fun countNumCharts(counter: CounterSummary): Int {
101 | val firstDate = counter.leastRecent ?: return 1
102 | val lastDate = counter.latestBetweenNowAndMostRecentEntry()
103 | return interval.toChronoUnit().count(firstDate, lastDate)
104 | }
105 |
106 | /*
107 | fun animate() {
108 | for (holder in boundViewHolders) {
109 | holder.binding.chart.animateY(200)
110 | }
111 | }
112 | */
113 | }
114 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/ui/editdialog/ColorAdapter.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.ui.editdialog
2 |
3 | import android.content.Context
4 | import android.graphics.Color
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import androidx.annotation.ColorInt
9 | import androidx.appcompat.widget.AppCompatButton
10 | import androidx.core.graphics.drawable.DrawableCompat
11 | import androidx.recyclerview.widget.RecyclerView
12 | import org.kde.bettercounter.R
13 | import org.kde.bettercounter.persistence.CounterColor
14 | import org.kde.bettercounter.persistence.CounterColors
15 |
16 | // Based on https://github.com/kristiyanP/colorpicker
17 | class ColorAdapter(val context: Context) : RecyclerView.Adapter() {
18 |
19 | var selectedColor: CounterColor
20 | get() {
21 | return colors[selectedPosition]
22 | }
23 | set(color) {
24 | for (i in colors.indices) {
25 | val colorPal = colors[i]
26 | if (colorPal == color) {
27 | selectedPosition = i
28 | }
29 | }
30 | }
31 |
32 | private var selectedPosition = 0
33 | set(pos) {
34 | val prevSelected = selectedPosition
35 | field = pos
36 | notifyItemChanged(selectedPosition)
37 | notifyItemChanged(prevSelected)
38 | }
39 |
40 | private var colors: List = CounterColors.getInstance(context).allColors
41 |
42 | inner class ViewHolder(val colorButton: AppCompatButton) :
43 | RecyclerView.ViewHolder(colorButton),
44 | View.OnClickListener {
45 | init {
46 | colorButton.setOnClickListener(this)
47 | }
48 |
49 | override fun onClick(v: View) {
50 | selectedPosition = layoutPosition
51 | }
52 | }
53 |
54 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
55 | val view = LayoutInflater.from(parent.context).inflate(R.layout.color_circle, parent, false)
56 | return ViewHolder(view as AppCompatButton)
57 | }
58 |
59 | private fun isDarkColor(@ColorInt color: Int): Boolean {
60 | val red = Color.red(color)
61 | val green = Color.green(color)
62 | val blue: Int = Color.blue(color)
63 |
64 | // https://en.wikipedia.org/wiki/YIQ
65 | // https://24ways.org/2010/calculating-color-contrast/
66 | val yiq: Int = ((red * 299) + (green * 587) + (blue * 114)) / 1000
67 | return yiq < 192
68 | }
69 |
70 | override fun onBindViewHolder(holder: ViewHolder, position: Int) {
71 | val colorInt = colors[position].colorInt
72 | val textColor = if (isDarkColor(colorInt)) Color.WHITE else Color.BLACK
73 | val tickText = if (position == selectedPosition) "✔" else ""
74 |
75 | holder.colorButton.text = tickText
76 | holder.colorButton.setTextColor(textColor)
77 | holder.colorButton.contentDescription = context.getString(R.string.color_hint, position + 1)
78 | val background = DrawableCompat.wrap(holder.colorButton.background)
79 | DrawableCompat.setTint(background, colorInt)
80 | holder.colorButton.background = background
81 | }
82 |
83 | override fun getItemCount(): Int = colors.size
84 | }
85 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/ui/editdialog/IntervalAdapter.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.ui.editdialog
2 |
3 | import android.content.Context
4 | import android.widget.ArrayAdapter
5 | import org.kde.bettercounter.persistence.Interval
6 |
7 | class IntervalAdapter(
8 | context: Context
9 | ) : ArrayAdapter(
10 | context,
11 | android.R.layout.simple_spinner_dropdown_item,
12 | Interval.humanReadableValues(context)
13 | ) {
14 | fun positionOf(interval: Interval): Int = Interval.entries.indexOf(interval)
15 |
16 | fun itemAt(position: Int): Interval = Interval.entries[position]
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/ui/main/BetterRelativeTimeTextView.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.ui.main
2 |
3 | import android.content.Context
4 | import android.os.Handler
5 | import android.os.Looper
6 | import android.os.Parcel
7 | import android.os.Parcelable
8 | import android.text.format.DateUtils
9 | import android.util.AttributeSet
10 | import android.view.View
11 | import androidx.appcompat.widget.AppCompatTextView
12 | import org.kde.bettercounter.R
13 | import java.lang.ref.WeakReference
14 | import kotlin.math.abs
15 |
16 | /**
17 | * A `TextView` that, given a reference time, renders that time as a time period relative to the current time.
18 | * @author Based on RelativeTimeTextView by Kiran Rao
19 | */
20 | open class BetterRelativeTimeTextView : AppCompatTextView {
21 | var referenceTime: Long = -1L
22 | set(value) {
23 | field = value
24 |
25 | // Note that this method could be called when a row in a ListView is recycled.
26 | // Hence, we need to first stop any currently running schedules (for example from the recycled view.
27 | stopTaskForPeriodicallyUpdatingRelativeTime()
28 |
29 | // Instantiate a new runnable with the new reference time
30 | initUpdateTimeTask()
31 |
32 | // Start a new schedule.
33 | startTaskForPeriodicallyUpdatingRelativeTime()
34 |
35 | // Finally, update the text display.
36 | updateTextDisplay()
37 | }
38 |
39 | private val mHandler = Handler(Looper.getMainLooper())
40 | private var mUpdateTimeTask: UpdateTimeRunnable? = null
41 | private var isUpdateTaskRunning = false
42 |
43 | constructor(ctx: Context, attrs: AttributeSet?) : super(ctx, attrs)
44 | constructor(ctx: Context, attrs: AttributeSet?, defStyle: Int) : super(ctx, attrs, defStyle)
45 |
46 | private fun updateTextDisplay() {
47 | if (referenceTime < 0) {
48 | setText(R.string.never)
49 | } else {
50 | text = getRelativeTimeDisplayString(referenceTime, System.currentTimeMillis())
51 | }
52 | }
53 |
54 | /**
55 | * Get the text to display for relative time.
56 | *
57 | * You can override this method to customize the string returned. For example you could add prefixes or suffixes, or use Spans to style the string etc
58 | * @param referenceTime The reference time passed in through [.setReferenceTime] or through `reference_time` attribute
59 | * @param now The current time
60 | * @return The display text for the relative time
61 | */
62 | open fun getRelativeTimeDisplayString(referenceTime: Long, now: Long): CharSequence {
63 | var difference: Long = abs(now - referenceTime)
64 | val days = difference / DateUtils.DAY_IN_MILLIS
65 | difference -= days * DateUtils.DAY_IN_MILLIS
66 | val hours = difference / DateUtils.HOUR_IN_MILLIS
67 | difference -= hours * DateUtils.HOUR_IN_MILLIS
68 | val minutes = difference / DateUtils.MINUTE_IN_MILLIS
69 | return if (referenceTime > now) {
70 | when {
71 | days > 0 -> context.getString(R.string.time_in_dhm, days, hours, minutes)
72 | hours > 0 -> context.getString(R.string.time_in_hm, hours, minutes)
73 | minutes > 0 -> context.getString(R.string.time_in_m, minutes)
74 | else -> context.getString(R.string.just_now)
75 | }
76 | } else {
77 | when {
78 | days > 0 -> context.getString(R.string.time_ago_dhm, days, hours, minutes)
79 | hours > 0 -> context.getString(R.string.time_ago_hm, hours, minutes)
80 | minutes > 0 -> context.getString(R.string.time_ago_m, minutes)
81 | else -> context.getString(R.string.just_now)
82 | }
83 | }
84 | }
85 |
86 | override fun onAttachedToWindow() {
87 | super.onAttachedToWindow()
88 | startTaskForPeriodicallyUpdatingRelativeTime()
89 | }
90 |
91 | override fun onDetachedFromWindow() {
92 | super.onDetachedFromWindow()
93 | stopTaskForPeriodicallyUpdatingRelativeTime()
94 | }
95 |
96 | override fun onVisibilityChanged(changedView: View, visibility: Int) {
97 | super.onVisibilityChanged(changedView, visibility)
98 | if (visibility == GONE || visibility == INVISIBLE) {
99 | stopTaskForPeriodicallyUpdatingRelativeTime()
100 | } else {
101 | startTaskForPeriodicallyUpdatingRelativeTime()
102 | }
103 | }
104 |
105 | private fun startTaskForPeriodicallyUpdatingRelativeTime() {
106 | if (mUpdateTimeTask == null || mUpdateTimeTask!!.isDetached) initUpdateTimeTask()
107 | mHandler.post(mUpdateTimeTask!!)
108 | isUpdateTaskRunning = true
109 | }
110 |
111 | private fun initUpdateTimeTask() {
112 | mUpdateTimeTask = UpdateTimeRunnable(this, referenceTime)
113 | }
114 |
115 | private fun stopTaskForPeriodicallyUpdatingRelativeTime() {
116 | if (isUpdateTaskRunning) {
117 | mUpdateTimeTask!!.detach()
118 | mHandler.removeCallbacks(mUpdateTimeTask!!)
119 | isUpdateTaskRunning = false
120 | }
121 | }
122 |
123 | override fun onSaveInstanceState(): Parcelable? {
124 | val superState = super.onSaveInstanceState()
125 | val ss = SavedState(superState)
126 | ss.referenceTime = referenceTime
127 | return ss
128 | }
129 |
130 | override fun onRestoreInstanceState(state: Parcelable) {
131 | if (state !is SavedState) {
132 | super.onRestoreInstanceState(state)
133 | return
134 | }
135 | referenceTime = state.referenceTime
136 | super.onRestoreInstanceState(state.superState)
137 | }
138 |
139 | private class SavedState : BaseSavedState {
140 | var referenceTime: Long = 0
141 |
142 | constructor(superState: Parcelable?) : super(superState)
143 | constructor(`in`: Parcel) : super(`in`) {
144 | referenceTime = `in`.readLong()
145 | }
146 |
147 | override fun writeToParcel(dest: Parcel, flags: Int) {
148 | super.writeToParcel(dest, flags)
149 | dest.writeLong(referenceTime)
150 | }
151 |
152 | companion object {
153 | @JvmField
154 | val CREATOR = object : Parcelable.Creator {
155 | override fun createFromParcel(source: Parcel): SavedState {
156 | return SavedState(source)
157 | }
158 |
159 | override fun newArray(size: Int): Array {
160 | return arrayOfNulls(size)
161 | }
162 | }
163 | }
164 | }
165 |
166 | private class UpdateTimeRunnable(view: BetterRelativeTimeTextView?, private val mRefTime: Long) : Runnable {
167 | private val viewWeakRef: WeakReference = WeakReference(view)
168 |
169 | val isDetached: Boolean
170 | get() = viewWeakRef.get() == null
171 |
172 | fun detach() {
173 | viewWeakRef.clear()
174 | }
175 |
176 | override fun run() {
177 | val view = viewWeakRef.get() ?: return
178 | val difference = abs(System.currentTimeMillis() - mRefTime)
179 | val differenceRoundedToMinute = (difference / DateUtils.MINUTE_IN_MILLIS) * DateUtils.MINUTE_IN_MILLIS
180 | view.updateTextDisplay()
181 | view.mHandler.postDelayed(this, difference - differenceRoundedToMinute)
182 | }
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/ui/main/DateTimePicker.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.ui.main
2 |
3 | import android.text.format.DateFormat
4 | import androidx.appcompat.app.AppCompatActivity
5 | import com.google.android.material.datepicker.MaterialDatePicker
6 | import com.google.android.material.timepicker.MaterialTimePicker
7 | import com.google.android.material.timepicker.TimeFormat
8 | import org.kde.bettercounter.extensions.toEpochMilli
9 | import org.kde.bettercounter.extensions.toLocalDateTime
10 | import org.kde.bettercounter.extensions.toUTCLocalDateTime
11 | import java.time.ZoneId
12 | import java.time.ZoneOffset
13 | import java.util.Calendar
14 |
15 | fun showDateTimePicker(activity: AppCompatActivity, initialDateTime: Calendar, callback: (Calendar) -> Unit) {
16 | val initialHour = initialDateTime.get(Calendar.HOUR_OF_DAY)
17 | val initialMinute = initialDateTime.get(Calendar.MINUTE)
18 | val timeFormat = if (DateFormat.is24HourFormat(activity)) TimeFormat.CLOCK_24H else TimeFormat.CLOCK_12H
19 | showDatePicker(activity, initialDateTime) { cal ->
20 | MaterialTimePicker.Builder()
21 | .setTimeFormat(timeFormat)
22 | .setHour(initialHour)
23 | .setMinute(initialMinute)
24 | .setInputMode(MaterialTimePicker.INPUT_MODE_CLOCK)
25 | .build().apply {
26 | addOnPositiveButtonClickListener {
27 | cal.set(Calendar.MINUTE, minute)
28 | cal.set(Calendar.HOUR_OF_DAY, hour)
29 | callback(cal)
30 | }
31 | }.show(activity.supportFragmentManager, "timePicker")
32 | }
33 | }
34 |
35 | fun showDatePicker(activity: AppCompatActivity, initialDateTime: Calendar, callback: (Calendar) -> Unit) {
36 | // MaterialDatePicker needs UTC, see https://stackoverflow.com/questions/63929730/materialdatepicker-returning-wrong-value/71541489#71541489
37 | val initialTime = initialDateTime.timeInMillis.toLocalDateTime().atZone(ZoneId.ofOffset("UTC", ZoneOffset.UTC)).toEpochMilli()
38 | MaterialDatePicker.Builder.datePicker()
39 | .setSelection(initialTime)
40 | .build().apply {
41 | addOnPositiveButtonClickListener {
42 | val cal = Calendar.getInstance()
43 | // MaterialDatePicker returns UTC
44 | cal.timeInMillis = it.toUTCLocalDateTime().atZone(ZoneId.systemDefault()).toEpochMilli()
45 | callback(cal)
46 | }
47 | }
48 | .show(activity.supportFragmentManager, "datePicker")
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/ui/main/EntryViewHolder.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.ui.main
2 |
3 | import android.view.HapticFeedbackConstants
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.recyclerview.widget.ItemTouchHelper
6 | import androidx.recyclerview.widget.RecyclerView
7 | import io.github.douglasjunior.androidSimpleTooltip.SimpleTooltip.OnDismissListener
8 | import org.kde.bettercounter.R
9 | import org.kde.bettercounter.databinding.FragmentEntryBinding
10 | import org.kde.bettercounter.persistence.CounterColors
11 | import org.kde.bettercounter.persistence.CounterSummary
12 | import org.kde.bettercounter.persistence.Tutorial
13 | import java.util.Calendar
14 |
15 | class EntryViewHolder(
16 | private val activity: AppCompatActivity,
17 | val binding: FragmentEntryBinding,
18 | private var viewModel: MainActivityViewModel,
19 | private val touchHelper: ItemTouchHelper,
20 | private val onClickListener: (counter: CounterSummary) -> Unit?,
21 | private val canDrag: () -> Boolean
22 | ) : RecyclerView.ViewHolder(binding.root) {
23 |
24 | fun onBind(counter: CounterSummary) {
25 | binding.root.setBackgroundColor(counter.color.colorInt)
26 | val rippleRes = CounterColors.getInstance(activity).getRippleDrawableRes(counter.color)
27 | if (rippleRes != null) {
28 | binding.increaseButton.setBackgroundResource(rippleRes)
29 | binding.decreaseButton.setBackgroundResource(rippleRes)
30 | } else {
31 | binding.increaseButton.background = null
32 | binding.decreaseButton.background = null
33 | }
34 | binding.increaseButton.setOnClickListener {
35 | viewModel.incrementCounter(counter.name)
36 | if (!viewModel.isTutorialShown(Tutorial.PICK_DATE)) {
37 | viewModel.setTutorialShown(Tutorial.PICK_DATE)
38 | showPickDateTutorial()
39 | }
40 | }
41 | binding.increaseButton.setOnLongClickListener {
42 | showDateTimePicker(activity, Calendar.getInstance()) { pickedDateTime ->
43 | viewModel.incrementCounter(counter.name, pickedDateTime.time)
44 | }
45 | true
46 | }
47 | binding.decreaseButton.setOnClickListener { viewModel.decrementCounter(counter.name) }
48 | binding.draggableArea.setOnClickListener { onClickListener(counter) }
49 | binding.draggableArea.setOnLongClickListener {
50 | if (!canDrag()) return@setOnLongClickListener false
51 | touchHelper.startDrag(this@EntryViewHolder)
52 | @Suppress("DEPRECATION")
53 | binding.draggableArea.performHapticFeedback(
54 | HapticFeedbackConstants.LONG_PRESS,
55 | HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING,
56 | )
57 | true
58 | }
59 | binding.nameText.text = counter.name
60 | binding.countText.text = counter.getFormattedCount()
61 |
62 | val checkDrawable = if (counter.isGoalMet()) R.drawable.ic_check else 0
63 | binding.countText.setCompoundDrawablesRelativeWithIntrinsicBounds(checkDrawable, 0, 0, 0)
64 |
65 | val mostRecentDate = counter.mostRecent
66 | if (mostRecentDate != null) {
67 | binding.timestampText.referenceTime = mostRecentDate.time
68 | binding.decreaseButton.isEnabled = true
69 | } else {
70 | binding.timestampText.referenceTime = -1L
71 | binding.decreaseButton.isEnabled = false
72 | }
73 | }
74 |
75 | fun showPickDateTutorial(onDismissListener: OnDismissListener? = null) {
76 | Tutorial.PICK_DATE.show(activity, binding.increaseButton, onDismissListener)
77 | }
78 |
79 | }
80 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/ui/settings/SettingsActivity.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.ui.settings
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import android.os.Bundle
6 | import android.provider.MediaStore
7 | import android.view.MenuItem
8 | import android.view.View
9 | import androidx.activity.result.ActivityResultLauncher
10 | import androidx.appcompat.app.AppCompatActivity
11 | import androidx.core.net.toUri
12 | import com.google.android.material.snackbar.Snackbar
13 | import org.kde.bettercounter.R
14 | import org.kde.bettercounter.boilerplate.CreateFileParams
15 | import org.kde.bettercounter.boilerplate.CreateFileResultContract
16 | import org.kde.bettercounter.databinding.ActivitySettingsBinding
17 | import org.kde.bettercounter.persistence.AverageMode
18 |
19 | class SettingsActivity : AppCompatActivity() {
20 |
21 | private val viewModel : SettingsViewModel by lazy { SettingsViewModel(application) }
22 | private val binding: ActivitySettingsBinding by lazy { ActivitySettingsBinding.inflate(layoutInflater) }
23 |
24 | override fun onCreate(savedInstanceState: Bundle?) {
25 | super.onCreate(savedInstanceState)
26 | setContentView(binding.root)
27 |
28 | setSupportActionBar(binding.settingsToolbar)
29 | supportActionBar?.setDisplayHomeAsUpEnabled(true)
30 | supportActionBar?.title = getString(R.string.settings)
31 |
32 | // Auto-export
33 | binding.switchAutoExport.isChecked = viewModel.isAutoExportOnSaveEnabled()
34 | binding.switchAutoExport.setOnCheckedChangeListener { _, isChecked ->
35 | viewModel.setAutoExportOnSave(isChecked)
36 | if (isChecked && viewModel.getAutoExportFileUri() == null) {
37 | selectAutoExportFile()
38 | } else {
39 | updateAutoExportFileButtonVisibility(isChecked)
40 | }
41 | }
42 | binding.buttonChangeAutoExportFile.setOnClickListener {
43 | selectAutoExportFile()
44 | }
45 | updateAutoExportFileButtonVisibility(binding.switchAutoExport.isChecked)
46 |
47 | // Average calculation mode
48 | when (viewModel.getAverageCalculationMode()) {
49 | AverageMode.FIRST_TO_NOW -> binding.radioFirstToNow.isChecked = true
50 | AverageMode.FIRST_TO_LAST -> binding.radioFirstToLast.isChecked = true
51 | }
52 |
53 | binding.radioGroupAverageMode.setOnCheckedChangeListener { _, checkedId ->
54 | val mode = when (checkedId) {
55 | R.id.radioFirstToNow -> AverageMode.FIRST_TO_NOW
56 | R.id.radioFirstToLast -> AverageMode.FIRST_TO_LAST
57 | else -> AverageMode.FIRST_TO_LAST
58 | }
59 | viewModel.setAverageCalculationMode(mode)
60 | }
61 | }
62 |
63 | private fun updateAutoExportFileButtonVisibility(autoExportEnabled: Boolean) {
64 | val existingUri = viewModel.getAutoExportFileUri()?.toUri()
65 | if (autoExportEnabled && existingUri != null) {
66 | displayCurrentExportFileName(existingUri)
67 | binding.buttonChangeAutoExportFile.visibility = View.VISIBLE
68 | } else {
69 | binding.textCurrentExportFile.text = getString(R.string.export_disabled)
70 | binding.buttonChangeAutoExportFile.visibility = View.GONE
71 | }
72 | }
73 |
74 | private fun displayCurrentExportFileName(uri: Uri) {
75 | val fileName = getFileNameFromUri(uri) ?: uri.toString()
76 | binding.textCurrentExportFile.text = getString(R.string.current_export_file, fileName)
77 | }
78 |
79 | private fun getFileNameFromUri(uri: Uri): String? {
80 | try {
81 | return when (uri.scheme) {
82 | "content" -> {
83 | contentResolver.query(uri, null, null, null, null)?.use { cursor ->
84 | if (cursor.moveToFirst()) {
85 | val displayNameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)
86 | if (displayNameIndex != -1) {
87 | cursor.getString(displayNameIndex)
88 | } else null
89 | } else null
90 | }
91 | }
92 | "file" -> {
93 | uri.lastPathSegment
94 | }
95 | else -> null
96 | }
97 | } catch (e: Exception) {
98 | e.printStackTrace()
99 | return uri.toString()
100 | }
101 | }
102 |
103 | private fun selectAutoExportFile() {
104 | val fileName = "bettercounter-auto-export.csv"
105 | autoExportFilePicker.launch(CreateFileParams("text/csv", fileName))
106 | }
107 |
108 | private val autoExportFilePicker: ActivityResultLauncher = registerForActivityResult(
109 | CreateFileResultContract()
110 | ) { uri: Uri? ->
111 | if (uri != null) {
112 | try {
113 | contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
114 |
115 | viewModel.setAutoExportFileUri(uri.toString())
116 |
117 | displayCurrentExportFileName(uri)
118 | } catch (e: Exception) {
119 | e.printStackTrace()
120 | Snackbar.make(binding.root, getString(R.string.export_error), Snackbar.LENGTH_LONG).show()
121 |
122 | viewModel.setAutoExportOnSave(false)
123 | binding.switchAutoExport.isChecked = false
124 | }
125 | } else {
126 | if (viewModel.getAutoExportFileUri() == null) {
127 | viewModel.setAutoExportOnSave(false)
128 | binding.switchAutoExport.isChecked = false
129 | }
130 | }
131 | }
132 |
133 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
134 | if (item.itemId == android.R.id.home) {
135 | finish()
136 | return true
137 | }
138 | return super.onOptionsItemSelected(item)
139 | }
140 |
141 | }
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/ui/settings/SettingsViewModel.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.ui.settings
2 |
3 | import android.app.Application
4 | import org.kde.bettercounter.persistence.AverageMode
5 | import org.kde.bettercounter.persistence.Repository
6 |
7 | class SettingsViewModel(application: Application) {
8 |
9 | private val repo: Repository = Repository.create(application)
10 |
11 | fun isAutoExportOnSaveEnabled(): Boolean {
12 | return repo.isAutoExportOnSaveEnabled()
13 | }
14 |
15 | fun setAutoExportOnSave(enabled: Boolean) {
16 | repo.setAutoExportOnSave(enabled)
17 | }
18 |
19 | fun getAutoExportFileUri(): String? {
20 | return repo.getAutoExportFileUri()
21 | }
22 |
23 | fun setAutoExportFileUri(uriString: String) {
24 | repo.setAutoExportFileUri(uriString)
25 | }
26 |
27 | fun getAverageCalculationMode(): AverageMode {
28 | return repo.getAverageCalculationMode()
29 | }
30 |
31 | fun setAverageCalculationMode(mode: AverageMode) {
32 | repo.setAverageCalculationMode(mode)
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/ui/widget/WidgetConfigureActivity.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.ui.widget
2 |
3 | import android.appwidget.AppWidgetManager
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import android.widget.ArrayAdapter
7 | import android.widget.Toast
8 | import androidx.appcompat.app.AppCompatActivity
9 | import kotlinx.coroutines.CoroutineScope
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.launch
12 | import org.kde.bettercounter.R
13 | import org.kde.bettercounter.databinding.WidgetConfigureBinding
14 |
15 | class WidgetConfigureActivity : AppCompatActivity() {
16 |
17 | private val viewModel : WidgetViewModel by lazy { WidgetViewModel(application) }
18 | private val binding: WidgetConfigureBinding by lazy { WidgetConfigureBinding.inflate(layoutInflater) }
19 |
20 | private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
21 |
22 | public override fun onCreate(savedInstanceState: Bundle?) {
23 | super.onCreate(savedInstanceState)
24 | setContentView(binding.root)
25 | setSupportActionBar(binding.toolbar)
26 |
27 | // Cancel widget placement if the user presses the back button.
28 | setResult(RESULT_CANCELED)
29 |
30 | val extras = intent.extras
31 | if (extras != null) {
32 | appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
33 | }
34 |
35 | if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
36 | finish()
37 | return
38 | }
39 |
40 | val counterNames = viewModel.getCounterList()
41 | if (counterNames.isEmpty()) {
42 | Toast.makeText(this, R.string.no_counters, Toast.LENGTH_SHORT).show()
43 | finish()
44 | }
45 |
46 | binding.counterNamesList.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, counterNames)
47 | binding.counterNamesList.setOnItemClickListener { _, _, position, _ ->
48 |
49 | val counterName = counterNames[position]
50 | WidgetViewModel.saveWidgetCounterNamePref(this, appWidgetId, counterName)
51 |
52 | val appWidgetManager = AppWidgetManager.getInstance(this)
53 |
54 | CoroutineScope(Dispatchers.Main).launch {
55 | WidgetProvider.updateAppWidget(application, viewModel, appWidgetManager, appWidgetId)
56 | }
57 |
58 | val resultValue = Intent()
59 | resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
60 | setResult(RESULT_OK, resultValue)
61 | finish()
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/src/main/java/org/kde/bettercounter/ui/widget/WidgetViewModel.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.ui.widget
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import android.content.Intent
6 | import androidx.core.content.edit
7 | import androidx.localbroadcastmanager.content.LocalBroadcastManager
8 | import kotlinx.coroutines.CoroutineScope
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.flow.Flow
11 | import kotlinx.coroutines.flow.flow
12 | import kotlinx.coroutines.flow.flowOn
13 | import kotlinx.coroutines.launch
14 | import org.kde.bettercounter.persistence.CounterSummary
15 | import org.kde.bettercounter.persistence.Exporter
16 | import org.kde.bettercounter.persistence.Repository
17 | import org.kde.bettercounter.ui.main.MainActivity
18 | import java.util.Calendar
19 | import java.util.Date
20 |
21 | class WidgetViewModel(val application: Application) {
22 |
23 | private val repo: Repository = Repository.create(application)
24 | private val exporter: Exporter = Exporter(application, repo)
25 |
26 | fun incrementCounter(name: String, date: Date = Calendar.getInstance().time) {
27 | CoroutineScope(Dispatchers.IO).launch {
28 | repo.addEntry(name, date)
29 | WidgetProvider.refreshWidget(application, name)
30 | exporter.autoExportIfEnabled()
31 | LocalBroadcastManager.getInstance(application)
32 | .sendBroadcast(Intent(MainActivity.ACTION_REFRESH_COUNTER).apply {
33 | putExtra(MainActivity.EXTRA_COUNTER_NAME, name)
34 | })
35 | }
36 | }
37 |
38 | fun getCounterSummary(name: String): Flow = flow {
39 | emit(repo.getCounterSummary(name))
40 | }.flowOn(Dispatchers.IO)
41 |
42 | fun counterExists(name: String): Boolean = repo.getCounterList().contains(name)
43 |
44 | fun getCounterList() = repo.getCounterList()
45 |
46 | companion object {
47 | private const val PREFS_NAME = "org.kde.bettercounter.ui.widget.WidgetProvider"
48 | private const val PREF_PREFIX_KEY = "appwidget_"
49 |
50 | fun saveWidgetCounterNamePref(
51 | context: Context,
52 | appWidgetId: Int,
53 | counterName: String
54 | ) {
55 | context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit {
56 | putString(PREF_PREFIX_KEY + appWidgetId, counterName)
57 | }
58 | }
59 |
60 | fun loadWidgetCounterNamePref(context: Context, appWidgetId: Int): String {
61 | val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
62 | return prefs.getString(PREF_PREFIX_KEY + appWidgetId, null)
63 | ?: throw NoSuchElementException("Counter preference not found for widget id: $appWidgetId")
64 | }
65 |
66 | fun deleteWidgetCounterNamePref(context: Context, appWidgetId: Int) {
67 | context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit {
68 | remove(PREF_PREFIX_KEY + appWidgetId)
69 | }
70 | }
71 |
72 | fun existsWidgetCounterNamePref(context: Context, appWidgetId: Int): Boolean {
73 | val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
74 | return prefs.contains(PREF_PREFIX_KEY + appWidgetId)
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------
/app/src/main/res/color/outlinebox.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-nodpi/widget_preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/app/src/main/res/drawable-nodpi/widget_preview.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_color_circle.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/button_state_color.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_add.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_check.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_edit.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_minusone.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_plusone.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_time.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ripple_color_1.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
5 |
6 | 4
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ripple_color_2.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
5 |
6 | 4
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ripple_color_3.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
5 |
6 | 4
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ripple_color_4.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
5 |
6 | 4
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ripple_color_5.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
5 |
6 | 4
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ripple_color_6.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
5 |
6 | 4
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ripple_color_7.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
5 |
6 | 4
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ripple_color_8.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
5 |
6 | 4
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ripple_color_default.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
5 |
6 | 4
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
15 |
16 |
21 |
22 |
23 |
24 |
33 |
34 |
46 |
47 |
57 |
58 |
64 |
65 |
66 |
67 |
76 |
77 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
13 |
19 |
20 |
21 |
22 |
25 |
26 |
31 |
32 |
38 |
39 |
44 |
45 |
51 |
52 |
60 |
61 |
67 |
68 |
69 |
75 |
76 |
80 |
81 |
86 |
87 |
92 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/color_circle.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/counter_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
24 |
25 |
32 |
33 |
34 |
35 |
41 |
42 |
47 |
48 |
63 |
64 |
73 |
74 |
75 |
76 |
81 |
82 |
83 |
84 |
85 |
104 |
105 |
116 |
117 |
118 |
119 |
120 |
127 |
128 |
129 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_chart.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
17 |
22 |
23 |
33 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_entry.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
20 |
21 |
22 |
23 |
30 |
31 |
39 |
40 |
51 |
52 |
62 |
63 |
64 |
65 |
66 |
67 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/progress_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/widget.xml:
--------------------------------------------------------------------------------
1 |
14 |
15 |
26 |
27 |
42 |
43 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/widget_configure.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
14 |
15 |
20 |
21 |
22 |
23 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/widget_preview.xml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
24 |
25 |
40 |
41 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/main.xml:
--------------------------------------------------------------------------------
1 |
2 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/popup_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_back.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_fore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_fore.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_back.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_fore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_fore.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_back.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/app/src/main/res/resources.properties:
--------------------------------------------------------------------------------
1 | unqualifiedResLocale=en-US
2 |
--------------------------------------------------------------------------------
/app/src/main/res/values-ca/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | BetterCounter
3 | El nom no pot ser buit
4 | Ja existeix un comptador amb aquest nom
5 | Nom del comptador
6 | Decrementa
7 | Incrementa
8 | Cap vegada
9 | Afegeix un comptador
10 | Cancel·la
11 | Desa
12 | Edita el comptador
13 | Esborra
14 | Esborra/Reinicia
15 | Horari
16 | Diari
17 | Setmanal
18 | Mensual
19 | Annual
20 | De per vida
21 | Fa %1$dd %2$dh %3$dm
22 | Fa %1$dh %2$dm
23 | Fa %1$dm
24 | En %1$dd %2$dh %3$dm
25 | En %1$dh %2$dm
26 | En %1$dm
27 | Ara mateix
28 | Mitjana: %1$s, de per vida: %2$s
29 | Mitjana: %1$s\nDe per vida: %2$s
30 | %1$.2f vegades cada hora
31 | cada %1$.2f hores
32 | %1$.2f vegades al dia
33 | cada %1$.2f dies
34 | N/A
35 | No hi ha entrades en aquest període
36 | Feu una pulsació llarga per escollir la data
37 | Feu una pulsació llarga i arrossegueu per a reordenar
38 | Exporta les dades
39 | Importa les dades
40 | Mostra el tutorial
41 | Exportats %1$d/%2$d comptadors
42 | Importats %1$d comptadors
43 | Segur que voleu esborrar aquest comptador?
44 |
45 | - %1$s (%2$d vegada)
46 | - %1$s (%2$d de vegades)
47 | - %1$s (%2$d vegades)
48 |
49 | Decrementat \"%1$s\"
50 | Desfés
51 | Error: el format no és correcte
52 | Reinicia les dades
53 | Sortir a córrer
54 | Afegeix un comptador a la pantalla principal
55 | No hi ha cap comptador
56 | Color %1$d
57 | Interval a mostrar
58 | Objectiu
59 | Premeu per canviar l\'interval de temps
60 | Preferències
61 | Exportació automàtica
62 | Exporta automàticament quan canviï un comptador
63 | Els comptadors s\'exportaran automàticament a %1$s
64 | Sempre podeu exportar els comptadors manualment des del menú contextual de la pantalla principal.
65 | Canvia
66 | Error en exportar
67 | Càlcul de la mitjana
68 | Mitjana entre la primera i l\'última entrada
69 | Mitjana entre la primera entrada i ara
70 | Filtra
71 |
72 |
--------------------------------------------------------------------------------
/app/src/main/res/values-da/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | BedreTæller
3 | Giv tælleren et navn
4 | Der findes allerede en tæller med dette navn
5 | Tællernavn
6 | Reducer
7 | Forøg
8 | Aldrig
9 | Tilføj tæller
10 | Annuller
11 | Gem
12 | Rediger tæller
13 | Slet
14 | Slet/Nulstil
15 | Timelig
16 | Daglig
17 | Ugentlig
18 | Månedlig
19 | Årlig
20 | Livstid
21 | %1$dd %2$dh %3$dm siden
22 | %1$dh %2$dm siden
23 | %1$dm siden
24 | Om %1$dd %2$dh %3$dm
25 | Om %1$dh %2$dm
26 | Om %1$dm
27 | Netop nu
28 | Gns: %1$s, livstid: %2$s
29 | Gns: %1$s\nLivstid: %2$s
30 | %1$.2f gange/time
31 | hver %1$.2f time
32 | %1$.2f gange/dag
33 | hver %1$.2f dag
34 | N/A
35 | Der er ingen ændringer i denne periode
36 | Tryk i længere tid for at vælge dato
37 | Tryk i længere tid og træk for at ændre rækkefølgen
38 | Eksporter data
39 | Importer data
40 | Vis vejledning
41 | Eksporterede %1$d/%2$d tællere
42 | Importederde %1$d tællere
43 | Fejl: ugyldigt filformat
44 | Er du sikker på, at du vil slette denne tæller?
45 | Reducerede \'%1$s\'
46 | Fortryd
47 | Nulstil men gem
48 |
49 | - %1$s (%2$d gang)
50 | - %1$s (%2$d gange)
51 |
52 | Løb en tur
53 | Tilføj en tæller til dit skrivebord
54 | Der er ikke oprettet nogen tællere: Lave nogle først.
55 | Farve %1$d
56 | Visningsinterval
57 | Mål
58 | Tryk for at ændre tidsintervallet
59 | Indstillinger
60 | Automatisk eksport
61 | Eksporter automatisk når en tæller ændres
62 | Tællere vil automatisk blive eksporteret til %1$s
63 | Du kan altid eksportere tællere manuelt fra hovedskærmens kontekstmenu.
64 | Skift
65 | Eksport mislykkedes
66 | Gennemsnitsberegning
67 | Gennemsnit mellem første og sidste indtastning
68 | Gennemsnit mellem første indtastning og nu
69 | Filtrer
70 |
71 |
--------------------------------------------------------------------------------
/app/src/main/res/values-de/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | BetterCounter
3 | Geben Sie dem Zähler einen Namen
4 | Ein Zähler mit diesem Namen existiert bereits
5 | Zählername
6 | Verringern
7 | Erhöhen
8 | Niemals
9 | Zähler hinzufügen
10 | Abbrechen
11 | Speichern
12 | Zähler bearbeiten
13 | Löschen
14 | Löschen/Zurücksetzen
15 | Stunde
16 | Tag
17 | Woche
18 | Monat
19 | Jahr
20 | Lebenslang
21 | Vor %1$dd %2$dh %3$dm
22 | Vor %1$dh %2$dm
23 | Vor %1$dm
24 | In %1$dd %2$dh %3$dm
25 | In %1$dh %2$dm
26 | In %1$dm
27 | Gerade eben
28 | Durchschnitt: %1$s, lebenslang: %2$s
29 | Durchschnitt: %1$s\nLebenslang: %2$s
30 | %1$.2f-mal/Stunde
31 | alle %1$.2f Stunden
32 | %1$.2f-mal/Tag
33 | alle %1$.2f Tage
34 | n. v.
35 | Es gibt keine Einträge in diesem Zeitraum
36 | Langes Drücken zur Auswahl des Datums
37 | Langes Drücken und Ziehen zum Anordnen
38 | Daten exportieren
39 | Daten importieren
40 | Tutorial anzeigen
41 | %1$d/%2$d Zähler exportiert
42 | %1$d Zähler importiert
43 | Fehler: Ungültiges Dateiformat
44 | Sind Sie sicher, dass Sie diesen Zähler löschen wollen?
45 | \"%1$s\" verringert
46 | Rückgängig
47 | Zurücksetzen, aber behalten
48 |
49 | - %1$s (%2$d-mal)
50 | - %1$s (%2$d-mal)
51 |
52 | Joggen gehen
53 | Einen Zähler zum Startbildschirm hinzufügen
54 | Keine Zähler gefunden: Erstellen Sie zuerst welche
55 | Farbe %1$d
56 | Anzeigezeitraum
57 | Ziel
58 | Tippen Sie, um das Zeitintervall zu ändern
59 | Einstellungen
60 | Automatischer Export
61 | Automatisch exportieren, wenn sich ein Zähler ändert
62 | Zähler werden automatisch nach %1$s exportiert
63 | Sie können die Zähler jederzeit manuell über das Kontextmenü des Hauptbildschirms exportieren.
64 | Ändern
65 | Export fehlgeschlagen
66 | Durchschnittsberechnung
67 | Durchschnitt zwischen erstem und letztem Eintrag
68 | Durchschnitt zwischen erstem Eintrag und jetzt
69 | Filter
70 |
71 |
--------------------------------------------------------------------------------
/app/src/main/res/values-es/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | BetterCounter
3 | El nombre no puede estar vacío
4 | Ya existe un contador con este nombre
5 | Nombre del contador
6 | Decrementar
7 | Incrementar
8 | Ninguna vez
9 | Añadir un contador
10 | Cancelar
11 | Guardar
12 | Editar el contador
13 | Eliminar
14 | Eliminar/Reiniciar
15 | Horario
16 | Diario
17 | Semanal
18 | Mensual
19 | Anual
20 | Para siempre
21 | Hace %1$dd %2$dh %3$dm
22 | Hace %1$dh %2$dm
23 | Hace %1$dm
24 | En %1$dd %2$dh %3$dm
25 | En %1$dh %2$dm
26 | En %1$dm
27 | Ahora mismo
28 | Media: %1$s, de por vida: %2$s
29 | Media: %1$s\nDe por vida: %2$s
30 | %1$.2f veces cada hora
31 | cada %1$.2f horas
32 | %1$.2f veces al día
33 | cada %1$.2f días
34 | N/A
35 | No hay entradas en este período
36 | Realizar una pulsación larga para elegir la fecha
37 | Realizar una pulsación larga y arrastrar para reordenar
38 | Exportar los datos
39 | Importar los datos
40 | Mostrar tutorial
41 | Exportados %1$d/%2$d contadores
42 | Importados %1$d contadores
43 | ¿Seguro que deseas borrar este contador?
44 |
45 | - %1$s (%2$d vez)
46 | - %1$s (%2$d de veces)
47 | - %1$s (%2$d veces)
48 |
49 | Decrementar \"%1$s\"
50 | Deshacer
51 | Error: el formato no es correcto
52 | Reinicia los datos
53 | Salir a correr
54 | Añadir un contador a la pantalla principal
55 | No hay ningún contador
56 | Color %1$d
57 | Intervalo a mostrar
58 | Objetivo
59 | Pulsar para cambiar el intervalo de tiempo
60 | Ajustes
61 | Exportación automática
62 | Exportar automáticamente cuando cambie un contador
63 | Los contadores se exportarán automáticamente a %1$s
64 | Siempre puedes exportar los contadores manualmente desde el menú contextual de la pantalla principal.
65 | Cambiar
66 | Error al exportar
67 | Cálculo de la media
68 | Media entre la primera y la última entrada
69 | Media entre la primera entrada y ahora
70 | Filtrar
71 |
72 |
--------------------------------------------------------------------------------
/app/src/main/res/values-fr/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | BetterCounter
3 | Le nom ne peut pas être vide
4 | Un compteur existe déjà avec ce nom
5 | Nom du compteur
6 | Diminuer
7 | Augmenter
8 | Jamais
9 | Ajout d\'un compteur
10 | Annuler
11 | Sauvegarder
12 | Editer un compteur
13 | Supprimer
14 | Supprimer/Redémarrer
15 | Horaire
16 | Journalier
17 | Hebdomadaire
18 | Mensuel
19 | Annuel
20 | Pour toujours
21 | Il y a %1$dd %2$dh %3$dm
22 | Il y a %1$dh %2$dm
23 | Il y a %1$dm
24 | Dans %1$dd %2$dh %3$dm
25 | Dans %1$dh %2$dm
26 | Dans %1$dm
27 | A l\'instant
28 | Moyenne: %1$s, vie entière: %2$s
29 | Moyenne: %1$s\nVie entière: %2$s
30 | %1$.2f fois par heure
31 | toutes les %1$.2f heures
32 | %1$.2f fois par jour
33 | tous les %1$.2f jours
34 | N/A
35 | Aucune donnée
36 | Appuyer longuement pour choisir la date
37 | Appuyer longuement et faite glisser pour réorganiser
38 | Exporter les données
39 | Importer les données
40 | Afficher le tutoriel
41 | %1$d/%2$d Compteurs exportés
42 | %1$d Compteurs importés
43 | Êtes-vous sûr de vouloir supprimer ce compteur ?
44 |
45 | - %1$s (%2$d fois)
46 | - %1$s (%2$d fois)
47 | - %1$s (%2$d fois)
48 |
49 | Diminution de \"%1$s\"
50 | Annuler
51 | Erreur : le format n\'est pas correct
52 | Réinitialiser les données
53 | Salir a correr
54 | Ajouter un compteur à l\'écran principal
55 | Il n\'y a pas de compteur
56 | Couleur %1$d
57 | Intervalle à afficher
58 | Objetifs
59 | Appuyer pour modifier l\'intervalle de temps
60 | Paramètres
61 | Exportation automatique
62 | Exporter automatiquement lors d\'un changement de compteur
63 | Les compteurs seront exportés automatiquement vers %1$s
64 | Vous pouvez toujours exporter les compteurs manuellement depuis le menu contextuel de l\'écran principal.
65 | Changer
66 | Échec de l\'exportation
67 | Calcul de la moyenne
68 | Moyenne entre la première et la dernière entrée
69 | Moyenne entre la première entrée et maintenant
70 | Filtrer
71 |
72 |
--------------------------------------------------------------------------------
/app/src/main/res/values-ru/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | BetterCounter
3 | Укажите название счетчика
4 | Счетчик с таким именем уже существует
5 | Имя счетчика
6 | Уменьшить
7 | Увеличить
8 | Никогда
9 | Добавить счетчик
10 | Отмена
11 | Сохранить
12 | Редактировать счетчик
13 | Удалить
14 | Удалить/сбросить
15 | Ежечасно
16 | Ежедневно
17 | Еженедельно
18 | Ежемесячно
19 | Ежегодно
20 | Пожизненно
21 | %1$dd %2$dh %3$dm назад
22 | %1$dh %2$dm назад
23 | %1$dm назад
24 | На %1$dd %2$dh %3$dm
25 | На %1$dh %2$dm
26 | На %1$dm
27 | Только что
28 | В среднем: %1$s, Пожизненно: %2$s
29 | В среднем: %1$s\nПожизненно: %2$s
30 | %1$.2f раз/час
31 | каждые %1$.2f часов
32 | %1$.2f раз/день
33 | каждые %1$.2f дней
34 | Н/Д
35 | Отсутствуют записи в выбранном периоде
36 | Долгое нажатие для выбора даты
37 | Длительное нажатие и перетаскивание для изменения порядка
38 | Экспорт данных
39 | импортировать данные
40 | Показать руководство
41 | Экспортировано %1$d/%2$d счетчиков
42 | Импортировано %1$d счетчиков
43 | Вы уверены, что хотите удалить этот счетчик?
44 |
45 | - %1$s (%2$d)
46 | - %1$s (%2$d)
47 | - %1$s (%2$d)
48 |
49 | Уменьшенный \'%1$s\'
50 | Отменить
51 | Ошибка: неправильный формат файла
52 | сбросить на ноль
53 | Пробежку
54 | Добавьте счетчик на главный экран
55 | Счетчиков не существует
56 | Цвет %1$d
57 | интервал для отображения
58 | Цель
59 | Нажмите, чтобы изменить временной интервал
60 | Настройки
61 | Автоматический экспорт
62 | Автоматически экспортировать при изменении счетчика
63 | Счетчики будут автоматически экспортированы в %1$s
64 | Вы всегда можете экспортировать счетчики вручную из контекстного меню главного экрана.
65 | Изменить
66 | Ошибка экспорта
67 | Расчет среднего значения
68 | Среднее между первой и последней записью
69 | Среднее между первой записью и настоящим временем
70 | Фильтр
71 |
72 |
--------------------------------------------------------------------------------
/app/src/main/res/values-tr/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | BetterCounter
3 | Sayaca bir ad ver
4 | Bu ada sahip bir sayaç zaten mevcut
5 | Sayaç adı
6 | Azalt
7 | Artır
8 | Asla
9 | Sayaç ekle
10 | İptal
11 | Kaydet
12 | Sayacı düzenle
13 | Sil
14 | Sil/Sıfırla
15 | Saatlik
16 | Günlük
17 | Haftalık
18 | Aylık
19 | Yıllık
20 | Ömür boyu
21 | %1$dd %2$dh %3$dm önce
22 | %1$dh %2$dm önce
23 | %1$dm önce
24 | %1$dd %2$dh %3$dm içinde
25 | %1$dh %2$dm içinde
26 | %1$dm içinde
27 | Şu anda
28 | Ort: %1$s, ömür boyu: %2$s
29 | Ort: %1$s\nÖmür boyu: %2$s
30 | %1$.2f kez/saat
31 | her %1$.2f saatte bir
32 | Günde %1$.2f kez
33 | her %1$.2f günde bir
34 | Yok
35 | Bu döneme ait herhangi bir kayıt bulunmamaktadır
36 | Tarih seçmek için uzun basın
37 | Yeniden sıralamak için uzun basın ve sürükleyin
38 | Verileri dışa aktar
39 | Verileri içe aktar
40 | Öğreticiyi göster
41 | %1$d/%2$d sayaç dışa aktarıldı
42 | %1$d sayaç içe aktarıldı
43 | Hata: geçersiz dosya biçimi
44 | Bu sayacı silmek istediğinizden emin misiniz?
45 | \'%1$s\' azaldı
46 | Geri al
47 | Sıfırla ama sakla
48 |
49 | - %1$s (%2$d kez)
50 | - %1$s (%2$d kez)
51 |
52 | Koşuya çık
53 | Ana ekranınıza bir sayaç ekleyin
54 | Hiçbir sayaç yok: Önce bir miktar oluşturun
55 | Renk %1$d
56 | Görüntülenecek aralık
57 | Hedef
58 | Zaman aralığını değiştirmek için dokunun
59 | Ayarlar
60 | Otomatik dışa aktarma
61 | Bir sayaç değiştiğinde otomatik olarak dışa aktar
62 | Sayaçlar otomatik olarak %1$s konumuna dışa aktarılacak
63 | Sayaçları her zaman ana ekran bağlam menüsünden manuel olarak dışa aktarabilirsiniz.
64 | Değiştir
65 | Dışa aktarma başarısız
66 | Ortalama hesaplama
67 | İlk ve son kayıt arasındaki ortalama
68 | İlk kayıt ile şimdi arasındaki ortalama
69 | Filtrele
70 |
71 |
--------------------------------------------------------------------------------
/app/src/main/res/values-zh-rCN/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | BetterCounter
3 | 为计数器设置一个名称吧
4 | 这个名称的计数器已经存在了
5 | 计数器名称
6 | 减少
7 | 增加
8 | 从未有过
9 | 增加计数器
10 | 取消
11 | 保存
12 | 编辑计数器
13 | 删除
14 | 按小时统计
15 | 按天统计
16 | 按周统计
17 | 按月统计
18 | 按年统计
19 | 完整统计
20 | %1$d天 %2$d小时 %3$d分钟 以前
21 | %1$d小时 %2$d分钟 以前
22 | %1$d分钟 以前
23 | %1$d天 %2$d小时 %3$d分钟 后
24 | %1$d小时 %2$d分钟 后
25 | %1$d分钟 后
26 | 刚刚
27 | 周期内: %1$s, 所有时间: %2$s
28 | 周期内: %1$s\n所有时间: %2$s
29 | 每小时 %1$.2f 次
30 | 每 %1$.2f 小时一次
31 | 每天 %1$.2f 次
32 | 每 %1$.2f 天一次
33 | 无"
34 | 在这段时期内没有记录
35 | 长按来选择执行的时间
36 | 长按并拖动来重新排序
37 | 导出数据
38 | 导入数据
39 | 显示教程
40 | 导出了 %1$d/%2$d 个计数器
41 | 导入了 %1$d 个计数器
42 | 出错了:文件格式无效
43 | 确定要删除这个计数器吗?
44 | 减少一次 \'%1$s\'
45 | 撤销
46 | 仅重置数据,不删除
47 |
48 | - %1$s (%2$d 次)
49 |
50 | 去慢跑
51 | 在主屏幕添加一个计数器
52 | 还没有计数器,创建一个吧
53 | 颜色 %1$d
54 | 统计间隔
55 | 删除/重置
56 | 目标次数
57 | 点击可更改时间间隔
58 | 设置
59 | 自动导出
60 | 计数器变更时自动导出
61 | 计数器将自动导出到 %1$s
62 | 您随时可以从主屏幕的上下文菜单手动导出计数器。
63 | 更改
64 | 导出失败
65 | 平均值计算
66 | 从第一次到最后一次的平均值
67 | 从第一次到现在的平均值
68 | 筛选
69 |
70 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #303030
4 | #171717
5 | #D0D0D0
6 | #505050
7 | #D0D0D0
8 |
9 | #1B5496
10 | #6232B0
11 | #833434
12 | #67506F
13 | #0A616C
14 | #AB1E27
15 | #8D1E6A
16 | #085816
17 |
18 |
19 | #454545
20 | #2064b2
21 | #6e38c6
22 | #9a3d3d
23 | #785d82
24 | #0d8190
25 | #c5232d
26 | #a9247f
27 | #0b7e1f
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/values/size_constants.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 80dp
4 | 4dp
5 | 4dp
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | BetterCounter
3 | Give the counter a name
4 | A counter with this name already exists
5 | Counter name
6 | Decrease
7 | Increase
8 | Never
9 | Add counter
10 | Cancel
11 | Save
12 | Edit counter
13 | Delete
14 | Delete/Reset
15 | Hourly
16 | Daily
17 | Weekly
18 | Monthly
19 | Yearly
20 | Lifetime
21 | %1$dd %2$dh %3$dm ago
22 | %1$dh %2$dm ago
23 | %1$dm ago
24 | In %1$dd %2$dh %3$dm
25 | In %1$dh %2$dm
26 | In %1$dm
27 | Just now
28 | Avg: %1$s, lifetime: %2$s
29 | Avg: %1$s\nLifetime: %2$s
30 | %1$.2f times/hour
31 | every %1$.2f hours
32 | %1$.2f times/day
33 | every %1$.2f days
34 | N/A
35 | There are no entries in this period
36 | Long press to pick the date
37 | Tap to change the time interval
38 | Long press and drag to reorder
39 | Export data
40 | Import data
41 | Show tutorial
42 | Exported %1$d/%2$d counters
43 | Imported %1$d counters
44 | Error: invalid file format
45 | Are you sure you want to delete this counter?
46 | Decreased \'%1$s\'
47 | Undo
48 | Reset but keep
49 |
50 | - %1$s (%2$d time)
51 | - %1$s (%2$d times)
52 |
53 | Go jogging
54 | Add a counter to your home screen
55 | No counters exist: Create some first
56 | Color %1$d
57 | Interval to display
58 | Goal
59 | Settings
60 | Auto-export
61 | Auto-export when a counter changes
62 | Counters will be exported automatically to %1$s
63 | You can always export the counters manually from the main screen\'s context menu.
64 | Change
65 | Export failed
66 | Average Calculation
67 | Average between first entry and last entry
68 | Average between first entry and now
69 | Filter
70 |
71 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
35 |
36 |
37 |
42 |
46 |
47 |
48 |
53 |
57 |
58 |
59 |
61 |
65 |
69 |
70 |
71 |
74 |
75 |
76 |
79 |
80 |
83 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/widget_info.xml:
--------------------------------------------------------------------------------
1 |
2 |
15 |
--------------------------------------------------------------------------------
/app/src/test/java/org/kde/bettercounter/extensions/CalendarExtensionTest.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.extensions
2 |
3 | import org.junit.Assert
4 | import org.junit.Test
5 | import java.util.Calendar
6 | import java.util.Date
7 | import java.util.Locale
8 |
9 | class CalendarExtensionTest {
10 |
11 | @Test
12 | fun `week truncation respects the first day of the week in US locale`() {
13 | Locale.setDefault(Locale.Category.FORMAT, Locale.US)
14 | Assert.assertEquals(Calendar.getInstance().firstDayOfWeek, Calendar.SUNDAY)
15 | val sundayDate = Date(1697995920502).toCalendar().truncated(Calendar.WEEK_OF_YEAR) // Sunday 22 October 2023 17:32:00.502 UTC
16 | val mondayDate = Date(1691995920502).toCalendar().truncated(Calendar.WEEK_OF_YEAR) // Monday 14 August 2023 6:52:00.502 UTC
17 | Assert.assertEquals(22, sundayDate.get(Calendar.DAY_OF_MONTH))
18 | Assert.assertEquals(13, mondayDate.get(Calendar.DAY_OF_MONTH))
19 |
20 | }
21 |
22 | @Test
23 | fun `week truncation respects the first day of the week in normal countries locale`() {
24 | Locale.setDefault(Locale.Category.FORMAT, Locale.GERMANY)
25 | Assert.assertEquals(Calendar.getInstance().firstDayOfWeek, Calendar.MONDAY)
26 | val sundayDate = Date(1697995920502).toCalendar().truncated(Calendar.WEEK_OF_YEAR) // Sunday 22 October 2023 17:32:00.502 UTC
27 | val mondayDate = Date(1691995920502).toCalendar().truncated(Calendar.WEEK_OF_YEAR) // Monday 14 August 2023 6:52:00.502 UTC
28 | Assert.assertEquals(16, sundayDate.get(Calendar.DAY_OF_MONTH))
29 | Assert.assertEquals(14, mondayDate.get(Calendar.DAY_OF_MONTH))
30 |
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/test/java/org/kde/bettercounter/extensions/ChronoUnitExtensionTest.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.extensions
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Test
5 | import java.time.temporal.ChronoUnit
6 | import java.util.Date
7 |
8 | class ChronoUnitExtensionTest {
9 |
10 | @Test
11 | fun `millis with less than one second diff`() {
12 | val from = Date(1697146570502) // 12 October 2023 21:36:10.502 UTC
13 | val to = Date(1700780933742) // 23 November 2023 23:08:53.742 UTC
14 | val count = ChronoUnit.DAYS.count(from, to)
15 | assertEquals(44, count)
16 | }
17 |
18 | @Test
19 | fun `millis with more than one second diff`() {
20 | val from = Date(1697146570502) // 12 October 2023 21:36:10.502 UTC
21 | val to = Date(1700780934441) // 23 November 2023 23:08:54.441 UTC
22 | val count = ChronoUnit.DAYS.count(from, to)
23 | assertEquals(44, count)
24 | }
25 |
26 | @Test
27 | fun `one second later is one day`() {
28 | val from = Date(1700781095000) // 23 November 2023 23:11:35 UTC
29 | val to = Date(1700781096000) // 23 November 2023 23:11:36 UTC
30 | val count = ChronoUnit.DAYS.count(from, to)
31 | assertEquals(1, count)
32 | }
33 |
34 | @Test
35 | fun `one exact day later are two days`() {
36 | val from = Date(1700781095000) // 23 November 2023 23:11:35 UTC
37 | val to = Date(1700867495000) // 24 November 2023 23:11:35 UTC
38 | val count = ChronoUnit.DAYS.count(from, to)
39 | assertEquals(2, count)
40 | }
41 |
42 | @Test
43 | fun `one second later is one week`() {
44 | val from = Date(1700781095000) // 23 November 2023 23:11:35 UTC
45 | val to = Date(1700781096000) // 23 November 2023 23:11:36 UTC
46 | val count = ChronoUnit.WEEKS.count(from, to)
47 | assertEquals(1, count)
48 | }
49 |
50 | @Test
51 | fun `one exact week later are two weeks`() {
52 | val from = Date(1700781095000) // 23 November 2023 23:11:35 UTC
53 | val to = Date(1701385895000) // 30 November 2023 23:11:35 UTC
54 | val count = ChronoUnit.WEEKS.count(from, to)
55 | assertEquals(2, count)
56 | }
57 |
58 | @Test
59 | fun `one second later is one month`() {
60 | val from = Date(1700781095000) // 23 November 2023 23:11:35 UTC
61 | val to = Date(1700781096000) // 23 November 2023 23:11:36 UTC
62 | val count = ChronoUnit.MONTHS.count(from, to)
63 | assertEquals(1, count)
64 | }
65 |
66 | @Test
67 | fun `one exact month later are two months`() {
68 | val from = Date(1700781095000) // 23 November 2023 23:11:35 UTC
69 | val to = Date(1703373095000) // 23 December 2023 23:11:35 UTC
70 | val count = ChronoUnit.MONTHS.count(from, to)
71 | assertEquals(2, count)
72 | }
73 |
74 | @Test
75 | fun `one second later is one year`() {
76 | val from = Date(1700781095000) // 23 November 2023 23:11:35 UTC
77 | val to = Date(1700781096000) // 23 November 2023 23:11:36 UTC
78 | val count = ChronoUnit.YEARS.count(from, to)
79 | assertEquals(1, count)
80 | }
81 |
82 | @Test
83 | fun `one exact year later are two years`() {
84 | val from = Date(1700781095000) // 23 November 2023 23:11:35 UTC
85 | val to = Date(1732403495000) // 23 November 2024 23:11:35 UTC
86 | val count = ChronoUnit.YEARS.count(from, to)
87 | assertEquals(2, count)
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/app/src/test/java/org/kde/bettercounter/extensions/ImportTest.kt:
--------------------------------------------------------------------------------
1 | package org.kde.bettercounter.extensions
2 |
3 | import org.junit.Assert.assertArrayEquals
4 | import org.junit.Assert.assertEquals
5 | import org.junit.Assert.assertTrue
6 | import org.junit.Test
7 | import org.kde.bettercounter.persistence.Entry
8 | import org.kde.bettercounter.ui.main.MainActivityViewModel
9 |
10 | class ImportTest {
11 |
12 | private val testTimestamp = System.currentTimeMillis()
13 |
14 | @Test
15 | fun `happy case`() {
16 | val namesToImport: MutableList = mutableListOf()
17 | val entriesToImport: MutableList = mutableListOf()
18 | val line = "hola,$testTimestamp,$testTimestamp,42"
19 | MainActivityViewModel.parseImportLine(line, namesToImport, entriesToImport)
20 | assertArrayEquals(namesToImport.toTypedArray(), arrayOf("hola"))
21 | assertEquals(entriesToImport.size, 3)
22 | assertEquals(entriesToImport.firstOrNull()?.date?.time, testTimestamp)
23 | assertEquals(entriesToImport.lastOrNull()?.date?.time, 42L)
24 | }
25 |
26 | @Test
27 | fun `names can be numbers`() {
28 | val namesToImport: MutableList = mutableListOf()
29 | val entriesToImport: MutableList = mutableListOf()
30 | val line = "0,$testTimestamp"
31 | MainActivityViewModel.parseImportLine(line, namesToImport, entriesToImport)
32 | assertArrayEquals(namesToImport.toTypedArray(), arrayOf("0"))
33 | assertEquals(entriesToImport.firstOrNull()?.date?.time, testTimestamp)
34 | }
35 |
36 | @Test
37 | fun `names can contain commas`() {
38 | val namesToImport: MutableList = mutableListOf()
39 | val entriesToImport: MutableList = mutableListOf()
40 | val line = "0,0,$testTimestamp"
41 | MainActivityViewModel.parseImportLine(line, namesToImport, entriesToImport)
42 | assertArrayEquals(namesToImport.toTypedArray(), arrayOf("0,0"))
43 | assertEquals(entriesToImport.firstOrNull()?.date?.time, testTimestamp)
44 | }
45 |
46 | @Test
47 | fun `works if no entries`() {
48 | val namesToImport: MutableList = mutableListOf()
49 | val entriesToImport: MutableList = mutableListOf()
50 | val line = "hola"
51 | MainActivityViewModel.parseImportLine(line, namesToImport, entriesToImport)
52 | assertArrayEquals(namesToImport.toTypedArray(), arrayOf("hola"))
53 | assertTrue(entriesToImport.isEmpty())
54 | }
55 |
56 | @Test(expected = NumberFormatException::class)
57 | fun `fails if text after timestamps`() {
58 | val namesToImport: MutableList = mutableListOf()
59 | val entriesToImport: MutableList = mutableListOf()
60 | val line = "hola,$testTimestamp,hola"
61 | MainActivityViewModel.parseImportLine(line, namesToImport, entriesToImport)
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
--------------------------------------------------------------------------------
/fastlane/Appfile:
--------------------------------------------------------------------------------
1 | json_key_file("/please/pass/the/key/using/--json-key/instead")
2 | package_name("org.kde.bettercounter")
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/ca/full_description.txt:
--------------------------------------------------------------------------------
1 | Fes-la servir per al que vulguis:
2 | - Seguiment d'hàbits, bons o dolents (pe: fer esport, fumar, beure alcohol...)
3 | - Recordar quan és l'última vegada que vas fer alguna cosa (pe: regar les plantes, canviar els llençols...)
4 | - Contar vides a una partida de Màgic
5 |
6 | Funcions principals:
7 | - Crea comptadors amb diferents noms i colors
8 | - Visualitza l'històric de cada comptador
9 | - Exporta les dades
10 | - Les teves dades mai surten del teu dispositiu (exceptuant, si estan activades, les còpies de seguretat de Google)
11 | - Una app molt senzilla, que continuarà sent així
12 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/ca/short_description.txt:
--------------------------------------------------------------------------------
1 | Aplicació de comptadors senzilla
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/ca/title.txt:
--------------------------------------------------------------------------------
1 | BetterCounter
--------------------------------------------------------------------------------
/fastlane/metadata/android/da-DK/full_description.txt:
--------------------------------------------------------------------------------
1 | – Log gode og dårlige vaner (f.eks. træning, rygning, alkoholindtag...)
2 | – Log hvornår du sidst gjorde noget (f.eks. vandede dine planter, skiftede sengetøj, lavede nummer to...)
3 | – Tæl dine liv i MtG
4 |
5 | Funktioner:
6 | – Gemmer dato og tidspunkt for hver gang hver individuelle tæller forøges.
7 | – Laver en graf over dine data og regner statistik over dem.
8 | – Lader dig eksportere dine data (f.eks. for at analysere dem med andre værktøjer).
9 | – Dine data bliver aldrig sendt til en server (undtagen Google's app backup, hvis det er slået til).
10 | – Skide enkel og vil forblive sådan.
11 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/da-DK/short_description.txt:
--------------------------------------------------------------------------------
1 | En enkel, alsidig tæller app
--------------------------------------------------------------------------------
/fastlane/metadata/android/da-DK/title.txt:
--------------------------------------------------------------------------------
1 | BedreTæller
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/30000.txt:
--------------------------------------------------------------------------------
1 | 3.0:
2 | * Added widgets
3 |
4 | 2.5:
5 | * Counters can be reset
6 |
7 | 2.4:
8 | * Per-app language in Android 13+
9 |
10 | 2.3:
11 | * Support for importing counters
12 |
13 | 2.2:
14 | * Adds an undo button after decreasing a counter.
15 |
16 | 2.0:
17 | * Allows scrolling through past data in the bottom panel graph.
18 | * All intervals are now tumbling windows instead of sliding windows.
19 | * Removes the YTD interval type, since Year now behaves like YTD.
20 | * Counter details now show the average for the current interval and the lifetime of the counter.
21 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/30001.txt:
--------------------------------------------------------------------------------
1 | 3.1:
2 | * Added content description for color buttons to improve accessibility
3 |
4 | 3.0:
5 | * Added widgets
6 |
7 | 2.5:
8 | * Counters can be reset
9 |
10 | 2.4:
11 | * Per-app language in Android 13+
12 |
13 | 2.3:
14 | * Support for importing counters
15 |
16 | 2.2:
17 | * Adds an undo button after decreasing a counter.
18 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/30100.txt:
--------------------------------------------------------------------------------
1 | 3.1:
2 | * Click on a chart header to temporarily change the time interval of the chart.
3 |
4 | 3.0:
5 | * Added widgets
6 | * Improved accessibility.
7 |
8 | 2.5:
9 | * Counters can be reset
10 |
11 | 2.4:
12 | * Per-app language in Android 13+
13 |
14 | 2.3:
15 | * Support for importing counters
16 |
17 | 2.2:
18 | * Adds an undo button after decreasing a counter.
19 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/30200.txt:
--------------------------------------------------------------------------------
1 | 3.2:
2 | * UI improvements.
3 |
4 | 3.1:
5 | * Click on a chart header to temporarily change the time interval of the chart.
6 |
7 | 3.0:
8 | * Added widgets
9 | * Improved accessibility.
10 |
11 | 2.5:
12 | * Counters can be reset
13 |
14 | 2.4:
15 | * Per-app language in Android 13+
16 |
17 | 2.3:
18 | * Support for importing counters
19 |
20 | 2.2:
21 | * Adds an undo button after decreasing a counter.
22 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/40000.txt:
--------------------------------------------------------------------------------
1 | 4.0:
2 | * Set goals for your counters.
3 | * German and Chinese translations.
4 |
5 | 3.2:
6 | * UI improvements.
7 |
8 | 3.1:
9 | * Click on a chart header to temporarily change the time interval of the chart.
10 |
11 | 3.0:
12 | * Added widgets
13 | * Improved accessibility.
14 |
15 | 2.5:
16 | * Counters can be reset
17 |
18 | 2.4:
19 | * Per-app language in Android 13+
20 |
21 | 2.3:
22 | * Support for importing counters
23 |
24 | 2.2:
25 | * Adds an undo button after decreasing a counter.
26 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/40001.txt:
--------------------------------------------------------------------------------
1 | 4.0.1:
2 | * Minor fixes to the translations and the UI.
3 |
4 | 4.0:
5 | * Set goals for your counters.
6 | * German and Chinese translations.
7 |
8 | 3.2:
9 | * UI improvements.
10 |
11 | 3.1:
12 | * Click on a chart header to temporarily change the time interval of the chart.
13 |
14 | 3.0:
15 | * Added widgets
16 | * Improved accessibility.
17 |
18 | 2.5:
19 | * Counters can be reset.
20 |
21 | 2.4:
22 | * Per-app language in Android 13+.
23 |
24 | 2.3:
25 | * Support for importing counters.
26 |
27 | 2.2:
28 | * Adds an undo button after decreasing a counter.
29 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/40100.txt:
--------------------------------------------------------------------------------
1 | 4.1:
2 | * Format dates with the device's locale.
3 |
4 | 4.0:
5 | * Set goals for your counters.
6 | * German and Chinese translations.
7 |
8 | 3.2:
9 | * UI improvements.
10 |
11 | 3.1:
12 | * Click on a chart header to temporarily change the time interval of the chart.
13 |
14 | 3.0:
15 | * Added widgets
16 | * Improved accessibility.
17 |
18 | 2.5:
19 | * Counters can be reset.
20 |
21 | 2.4:
22 | * Per-app language in Android 13+.
23 |
24 | 2.3:
25 | * Support for importing counters.
26 |
27 | 2.2:
28 | * Adds an undo button after decreasing a counter.
29 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/40200.txt:
--------------------------------------------------------------------------------
1 | 4.2:
2 | * Added Danish translation.
3 |
4 | 4.1:
5 | * Format dates with the device's locale.
6 |
7 | 4.0:
8 | * Set goals for your counters.
9 | * German and Chinese translations.
10 |
11 | 3.2:
12 | * UI improvements.
13 |
14 | 3.1:
15 | * Click on a chart header to temporarily change the time interval of the chart.
16 |
17 | 3.0:
18 | * Added widgets
19 | * Improved accessibility.
20 |
21 | 2.5:
22 | * Counters can be reset.
23 |
24 | 2.4:
25 | * Per-app language in Android 13+.
26 |
27 | 2.3:
28 | * Support for importing counters.
29 |
30 | 2.2:
31 | * Adds an undo button after decreasing a counter.
32 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/40300.txt:
--------------------------------------------------------------------------------
1 | 4.3:
2 | * Danish and Spanish translations.
3 | * Fixed crash in Catalan.
4 |
5 | 4.1:
6 | * Format dates with the device's locale.
7 |
8 | 4.0:
9 | * Set goals for your counters.
10 | * German and Chinese translations.
11 |
12 | 3.1:
13 | * Click on a chart header to temporarily change the time interval of the chart.
14 |
15 | 3.0:
16 | * Added widgets
17 | * Improved accessibility.
18 |
19 | 2.5:
20 | * Counters can be reset.
21 |
22 | 2.4:
23 | * Per-app language in Android 13+.
24 |
25 | 2.3:
26 | * Support for importing counters.
27 |
28 | 2.2:
29 | * Adds an undo button after decreasing a counter.
30 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/40301.txt:
--------------------------------------------------------------------------------
1 | 4.3:
2 | * Danish and Spanish translations.
3 |
4 | 4.1:
5 | * Format dates with the device's locale.
6 |
7 | 4.0:
8 | * Set goals for your counters.
9 | * German and Chinese translations.
10 |
11 | 3.1:
12 | * Click on a chart header to temporarily change the time interval of the chart.
13 |
14 | 3.0:
15 | * Added widgets
16 | * Improved accessibility.
17 |
18 | 2.5:
19 | * Counters can be reset.
20 |
21 | 2.4:
22 | * Per-app language in Android 13+.
23 |
24 | 2.3:
25 | * Support for importing counters.
26 |
27 | 2.2:
28 | * Adds an undo button after decreasing a counter.
29 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/40400.txt:
--------------------------------------------------------------------------------
1 | 4.4:
2 | * Long tap the cart title to jump to a specific date.
3 |
4 | 4.1:
5 | * Format dates with the device's locale.
6 |
7 | 4.0:
8 | * Set goals for your counters.
9 |
10 | 3.1:
11 | * Click on a chart title to temporarily change the time interval of the chart.
12 |
13 | 3.0:
14 | * Added widgets
15 | * Improved accessibility.
16 |
17 | 2.5:
18 | * Counters can be reset.
19 |
20 | 2.4:
21 | * Per-app language in Android 13+.
22 |
23 | 2.3:
24 | * Support for importing counters.
25 |
26 | 2.2:
27 | * Adds an undo button after decreasing a counter.
28 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/40401.txt:
--------------------------------------------------------------------------------
1 | 4.4.1:
2 | * Fixes calculating the averages.
3 |
4 | 4.4:
5 | * Long tap the cart title to jump to a specific date.
6 |
7 | 4.1:
8 | * Format dates with the device's locale.
9 |
10 | 4.0:
11 | * Set goals for your counters.
12 |
13 | 3.1:
14 | * Click on a chart title to temporarily change the time interval of the chart.
15 |
16 | 3.0:
17 | * Added widgets
18 | * Improved accessibility.
19 |
20 | 2.5:
21 | * Counters can be reset.
22 |
23 | 2.4:
24 | * Per-app language in Android 13+.
25 |
26 | 2.3:
27 | * Support for importing counters.
28 |
29 | 2.2:
30 | * Adds an undo button after decreasing a counter.
31 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/40500.txt:
--------------------------------------------------------------------------------
1 | 4.5:
2 | * Allow using the keyboard to enter goal numbers.
3 |
4 | 4.4.1:
5 | * Fixes calculating the averages.
6 |
7 | 4.4:
8 | * Long tap the chart title to jump to a specific date.
9 |
10 | 4.1:
11 | * Format dates with the device's locale.
12 |
13 | 4.0:
14 | * Set goals for your counters.
15 |
16 | 3.1:
17 | * Click on a chart title to temporarily change the time interval of the chart.
18 |
19 | 3.0:
20 | * Added widgets.
21 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/40600.txt:
--------------------------------------------------------------------------------
1 | 4.6:
2 | * UI improvements.
3 |
4 | 4.5:
5 | * Allow using the keyboard to enter goal numbers.
6 |
7 | 4.4.1:
8 | * Fixes calculating the averages.
9 |
10 | 4.4:
11 | * Long tap the chart title to jump to a specific date.
12 |
13 | 4.1:
14 | * Format dates with the device's locale.
15 |
16 | 4.0:
17 | * Set goals for your counters.
18 |
19 | 3.1:
20 | * Click on a chart title to temporarily change the time interval of the chart.
21 |
22 | 3.0:
23 | * Added widgets.
24 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/40601.txt:
--------------------------------------------------------------------------------
1 | 4.6.1:
2 | * Fixed issues when the week doesn't start in Monday.
3 |
4 | 4.6:
5 | * UI improvements.
6 |
7 | 4.5:
8 | * Allow using the keyboard to enter goal numbers.
9 |
10 | 4.4.1:
11 | * Fixes calculating the averages.
12 |
13 | 4.4:
14 | * Long tap the chart title to jump to a specific date.
15 |
16 | 4.1:
17 | * Format dates with the device's locale.
18 |
19 | 4.0:
20 | * Set goals for your counters.
21 |
22 | 3.1:
23 | * Click on a chart title to temporarily change the time interval of the chart.
24 |
25 | 3.0:
26 | * Added widgets.
27 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/40602.txt:
--------------------------------------------------------------------------------
1 | 4.6.2:
2 | * Fixed date sometimes being offset by one day when using the selector.
3 |
4 | 4.6.1:
5 | * Fixed issues when the week doesn't start in Monday.
6 |
7 | 4.6:
8 | * UI improvements.
9 |
10 | 4.5:
11 | * Allow using the keyboard to enter goal numbers.
12 |
13 | 4.4:
14 | * Long tap the chart title to jump to a specific date.
15 |
16 | 4.1:
17 | * Format dates with the device's locale.
18 |
19 | 4.0:
20 | * Set goals for your counters.
21 |
22 | 3.1:
23 | * Click on a chart title to temporarily change the time interval of the chart.
24 |
25 | 3.0:
26 | * Added widgets.
27 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/40603.txt:
--------------------------------------------------------------------------------
1 | 4.6.3:
2 | * Added Turkish translation.
3 | * Fixed day names in charts.
4 |
5 | 4.6.2:
6 | * Fixed date sometimes being offset by one day when using the selector.
7 |
8 | 4.6.1:
9 | * Fixed issues when the week doesn't start in Monday.
10 |
11 | 4.6:
12 | * UI improvements.
13 |
14 | 4.5:
15 | * Allow using the keyboard to enter goal numbers.
16 |
17 | 4.4:
18 | * Long tap the chart title to jump to a specific date.
19 |
20 | 4.1:
21 | * Format dates with the device's locale.
22 |
23 | 4.0:
24 | * Set goals for your counters.
25 |
26 | 3.1:
27 | * Click on a chart title to view a different time interval.
28 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/40700.txt:
--------------------------------------------------------------------------------
1 | 4.7.0:
2 | * Added Turkish and French translation.
3 | * Changed counter colors to higher contrast ones.
4 |
5 | 4.6.3:
6 | * Fixed day names in charts.
7 |
8 | 4.6.2:
9 | * Fixed date sometimes being offset by one day.
10 |
11 | 4.6:
12 | * UI improvements.
13 |
14 | 4.5:
15 | * Allow using the keyboard to enter goal numbers.
16 |
17 | 4.4:
18 | * Long tap the chart title to jump to a specific date.
19 |
20 | 4.1:
21 | * Format dates with the device's locale.
22 |
23 | 4.0:
24 | * Set goals for your counters.
25 |
26 | 3.1:
27 | * Click on a chart title to view a different time interval.
28 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/40800.txt:
--------------------------------------------------------------------------------
1 | 4.8:
2 | * You can now show an app tutorial from the top right menu.
3 |
4 | 4.7:
5 | * Added Turkish and French translation.
6 | * Changed counter colors to higher contrast ones.
7 |
8 | 4.6:
9 | * UI improvements.
10 |
11 | 4.5:
12 | * Allow using the keyboard to enter goal numbers.
13 |
14 | 4.4:
15 | * Long tap the chart title to jump to a specific date.
16 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/40900.txt:
--------------------------------------------------------------------------------
1 | 4.9:
2 | * Added hourly intervals.
3 | * Fixed numbers not refreshing automatically if app was kept open for long.
4 |
5 | 4.8:
6 | * You can now show an app tutorial from the top right menu.
7 |
8 | 4.7:
9 | * Added Turkish and French translation.
10 | * Changed counter colors to higher contrast ones.
11 |
12 | 4.6:
13 | * UI improvements.
14 |
15 | 4.5:
16 | * Allow using the keyboard to enter goal numbers.
17 |
18 | 4.4:
19 | * Long tap the chart title to jump to a specific date.
20 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/40901.txt:
--------------------------------------------------------------------------------
1 | 4.9.1:
2 | * Fixed numbers not refreshing automatically if app was kept open for long.
3 |
4 | 4.9:
5 | * Added hourly intervals.
6 |
7 | 4.8:
8 | * You can now show an app tutorial from the top right menu.
9 |
10 | 4.7:
11 | * Added Turkish and French translation.
12 | * Changed counter colors to higher contrast ones.
13 |
14 | 4.6:
15 | * UI improvements.
16 |
17 | 4.5:
18 | * Allow using the keyboard to enter goal numbers.
19 |
20 | 4.4:
21 | * Long tap the chart title to jump to a specific date.
22 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/40902.txt:
--------------------------------------------------------------------------------
1 | 4.9.2:
2 | * Fixed graphs covering the counter list.
3 |
4 | 4.9.1:
5 | * Fixed numbers not refreshing automatically if app was kept open for long.
6 |
7 | 4.9:
8 | * Added hourly intervals.
9 |
10 | 4.8:
11 | * You can now show an app tutorial from the top right menu.
12 |
13 | 4.7:
14 | * Added Turkish and French translation.
15 | * Changed counter colors to higher contrast ones.
16 |
17 | 4.6:
18 | * UI improvements.
19 |
20 | 4.5:
21 | * Allow using the keyboard to enter goal numbers.
22 |
23 | 4.4:
24 | * Long tap the chart title to jump to a specific date.
25 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/40903.txt:
--------------------------------------------------------------------------------
1 | 4.9.3:
2 | * Fixed crash.
3 |
4 | 4.9.2:
5 | * Fixed graphs covering the counter list.
6 |
7 | 4.9.1:
8 | * Fixed numbers not refreshing automatically if app was kept open for long.
9 |
10 | 4.9:
11 | * Added hourly intervals.
12 |
13 | 4.8:
14 | * You can now show an app tutorial from the top right menu.
15 |
16 | 4.7:
17 | * Added Turkish and French translation.
18 | * Changed counter colors to higher contrast ones.
19 |
20 | 4.6:
21 | * UI improvements.
22 |
23 | 4.5:
24 | * Allow using the keyboard to enter goal numbers.
25 |
26 | 4.4:
27 | * Long tap the chart title to jump to a specific date.
28 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/40904.txt:
--------------------------------------------------------------------------------
1 | 4.9.4:
2 | * Fixed widgets refresh.
3 |
4 | 4.9.2:
5 | * Fixed graphs covering the counter list.
6 |
7 | 4.9:
8 | * Added hourly intervals.
9 |
10 | 4.8:
11 | * You can now show an app tutorial from the top right menu.
12 |
13 | 4.7:
14 | * Added Turkish and French translation.
15 | * Changed counter colors to higher contrast ones.
16 |
17 | 4.6:
18 | * UI improvements.
19 |
20 | 4.5:
21 | * Allow using the keyboard to enter goal numbers.
22 |
23 | 4.4:
24 | * Long tap the chart title to jump to a specific date.
25 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/41000.txt:
--------------------------------------------------------------------------------
1 | 4.10:
2 | * Calculate average until last entry, not inclusive (unless showing historical data).
3 | * Made undo notification not cover the charts
4 | * Restarting the app is no longer needed after importing data.
5 |
6 | 4.9:
7 | * Added hourly intervals.
8 |
9 | 4.8:
10 | * You can now show an app tutorial from the top right menu.
11 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/41001.txt:
--------------------------------------------------------------------------------
1 | 4.10.1:
2 | * Fixed app showing behind the title bar in Android 15
3 |
4 | 4.10:
5 | * Calculate average until last entry, not inclusive (unless showing historical data).
6 | * Made undo notification not cover the charts
7 | * Restarting the app is no longer needed after importing data.
8 |
9 | 4.9:
10 | * Added hourly intervals.
11 |
12 | 4.8:
13 | * You can now show an app tutorial from the top right menu.
14 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/41002.txt:
--------------------------------------------------------------------------------
1 | 4.10.2:
2 | * Increase font size in charts
3 |
4 | 4.10.1:
5 | * Fixed app showing behind the title bar in Android 15
6 |
7 | 4.10:
8 | * Calculate average until last entry, not inclusive (unless showing historical data).
9 | * Made undo notification not cover the charts
10 | * Restarting the app is no longer needed after importing data.
11 |
12 | 4.9:
13 | * Added hourly intervals.
14 |
15 | 4.8:
16 | * You can now show an app tutorial from the top right menu.
17 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/50001.txt:
--------------------------------------------------------------------------------
1 | 5.0:
2 | - Add a filter/search
3 | - Add settings to configure average calculation and auto-export
4 | - Keep Y axis the same for all charts
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/50003.txt:
--------------------------------------------------------------------------------
1 | 5.0.3:
2 | - Fix widgets not updating properly when the app wasn't running
3 |
4 | 5.0:
5 | - Add a filter/search
6 | - Add settings to configure average calculation and auto-export
7 | - Keep Y axis the same for all charts
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/50100.txt:
--------------------------------------------------------------------------------
1 | 5.1.0:
2 | - Improved widget responsiveness
3 |
4 | 5.0:
5 | - Add a filter/search
6 | - Add settings to configure average calculation and auto-export
7 | - Keep Y axis the same for all charts
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | - Track good and bad habits (eg: exercising, smoking, drinking...)
2 | - Track when's the last time you did something (eg: water your plants, change your bedsheets, poop...)
3 | - Count your lives in MtG
4 |
5 | Features:
6 | - Records the date and time of each individual counter increase.
7 | - Graphs your data over time and calculates statistics.
8 | - Lets you export your data (eg: to analyze it with your choice of tools).
9 | - Your data is never sent to any server (except for Google's app backup, if enabled).
10 | - Simple as hell and will stay this way.
11 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/featureGraphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/fastlane/metadata/android/en-US/images/featureGraphic.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | A simple, multi-purpose counter app
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/title.txt:
--------------------------------------------------------------------------------
1 | BetterCounter
--------------------------------------------------------------------------------
/fastlane/metadata/android/es-ES/full_description.txt:
--------------------------------------------------------------------------------
1 | Úsala para lo que quieras:
2 | - Seguimiento de hábitos, buenos o malos (por ejemplo: hacer deporte, fumar, beber alcohol...)
3 | - Recordar cuándo fue la última vez que hiciste algo (por ejemplo: regar las plantas, cambiar las sábanas...)
4 | - Contar vidas en una partida de Magic
5 |
6 | Funciones principales:
7 | - Crea contadores con distintos nombres y colores
8 | - Visualiza el histórico de cada contador
9 | - Exporta los datos
10 | - Tus datos nunca salen de tu dispositivo (exceptuando, si están activadas, las copias de seguridad de Google)
11 | - Una app muy sencilla, que seguirá siendo así
12 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/es-ES/short_description.txt:
--------------------------------------------------------------------------------
1 | Aplicación de contadores sencilla
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/es-ES/title.txt:
--------------------------------------------------------------------------------
1 | BetterCounter
--------------------------------------------------------------------------------
/fastlane/metadata/android/fr-FR/full_description.txt:
--------------------------------------------------------------------------------
1 | Utilisez-le pour ce que vous voulez :
2 | - Suivre ses habitudes, bonnes ou mauvaises (ex : faire du sport, fumer, boire de l'alcool...).
3 | - Se souvenir de la dernière fois que l'on a fait quelque chose (par exemple : arroser les plantes, changer les draps...).
4 | - Compter les vies dans un jeu de magie
5 |
6 | Fonctions principales :
7 | - Créer des compteurs avec des noms et des couleurs différentes
8 | - Visualiser l'historique de chaque compteur
9 | - Exporter des données
10 | - Vos données ne quittent jamais votre appareil (sauf, si elles sont activées, les sauvegardes Google)
11 | - Une application très simple, qui le restera.
--------------------------------------------------------------------------------
/fastlane/metadata/android/fr-FR/short_description.txt:
--------------------------------------------------------------------------------
1 | Simple application de compteur
--------------------------------------------------------------------------------
/fastlane/metadata/android/fr-FR/title.txt:
--------------------------------------------------------------------------------
1 | BetterCounter
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=false
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 | org.gradle.configuration-cache=true
23 |
24 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | gradle = "8.10.1"
3 | kotlin = "2.1.20"
4 | ksp = "2.1.20-1.0.32"
5 | coreKtx = "1.16.0"
6 | lifecycle = "2.9.0"
7 | recyclerview = "1.4.0"
8 | appcompat = "1.7.0"
9 | material = "1.12.0"
10 | room = "2.7.1"
11 | androidDesugarJdkLibs = "2.1.5"
12 | simpleTooltip = "1.1.0"
13 | mpAndroidChart = "3.1.0.25"
14 | junit = "4.13.2"
15 |
16 | [libraries]
17 | android-gradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "gradle" }
18 | kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
19 | androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
20 | androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
21 | androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
22 | androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
23 | material = { module = "com.google.android.material:material", version.ref = "material" }
24 | androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
25 | androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
26 | androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
27 | android-desugarJdkLibs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" }
28 | douglasjunior-android-simple-tooltip = { module = "com.github.douglasjunior:android-simple-tooltip", version.ref = "simpleTooltip" }
29 | appdevnext-mpAndroidChart = { module = "com.github.AppDevNext:AndroidChart", version.ref = "mpAndroidChart" }
30 | junit = { module = "junit:junit", version.ref = "junit" }
31 |
32 | [plugins]
33 | android-application = { id = "com.android.application", version.ref = "gradle" }
34 | kotlin-android = { id ="org.jetbrains.kotlin.android", version.ref = "kotlin" }
35 | google-devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
36 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Dec 23 12:41:31 CET 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/albertvaka/bettercounter/146940c0bcee1ea1b0d0ad8a4ce853a5161cc09d/screenshot.png
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | maven {
14 | url = uri("https://jitpack.io")
15 | }
16 | }
17 | }
18 | rootProject.name = "BetterCounter"
19 |
20 | include(":app")
21 |
--------------------------------------------------------------------------------