├── .editorconfig
├── .github
├── CODEOWNERS
└── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── feature_request.yml
│ └── issue_report.yml
├── .gitignore
├── .woodpecker.yml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── RELEASES.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── debug
│ └── res
│ │ └── values
│ │ └── strings.xml
│ └── main
│ ├── AndroidManifest.xml
│ ├── assets
│ └── RELEASES.md
│ ├── ic_launcher-playstore.png
│ ├── java
│ └── com
│ │ └── dessalines
│ │ └── habitmaker
│ │ ├── MainActivity.kt
│ │ ├── db
│ │ ├── AppDB.kt
│ │ ├── AppSettings.kt
│ │ ├── Converters.kt
│ │ ├── Encouragement.kt
│ │ ├── Habit.kt
│ │ ├── HabitCheck.kt
│ │ ├── HabitReminder.kt
│ │ └── Migrations.kt
│ │ ├── notifications
│ │ ├── ReminderManager.kt
│ │ ├── ReminderWorker.kt
│ │ └── Utils.kt
│ │ ├── ui
│ │ ├── components
│ │ │ ├── about
│ │ │ │ └── AboutScreen.kt
│ │ │ ├── common
│ │ │ │ ├── AppBars.kt
│ │ │ │ ├── Dialogs.kt
│ │ │ │ ├── Other.kt
│ │ │ │ └── Sizes.kt
│ │ │ ├── habit
│ │ │ │ ├── CreateHabitScreen.kt
│ │ │ │ ├── EditHabitScreen.kt
│ │ │ │ ├── EncouragementForm.kt
│ │ │ │ ├── EncouragementsForm.kt
│ │ │ │ ├── HabitForm.kt
│ │ │ │ ├── HabitReminderForm.kt
│ │ │ │ └── habitanddetails
│ │ │ │ │ ├── HabitDetailPane.kt
│ │ │ │ │ ├── HabitsAndDetailScreen.kt
│ │ │ │ │ ├── HabitsPane.kt
│ │ │ │ │ └── calendars
│ │ │ │ │ └── HabitCalendar.kt
│ │ │ └── settings
│ │ │ │ ├── BackupAndRestoreScreen.kt
│ │ │ │ ├── BehaviorScreen.kt
│ │ │ │ ├── LookAndFeelScreen.kt
│ │ │ │ └── SettingsScreen.kt
│ │ └── theme
│ │ │ ├── Color.kt
│ │ │ ├── Shape.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ │ └── utils
│ │ ├── DateUtils.kt
│ │ ├── ScoreUtils.kt
│ │ ├── Types.kt
│ │ └── Utils.kt
│ └── res
│ ├── drawable
│ └── ic_launcher_foreground.xml
│ ├── mipmap-anydpi
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ └── ic_launcher.webp
│ ├── mipmap-mdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── values-ar
│ └── strings.xml
│ ├── values-bg
│ └── strings.xml
│ ├── values-br
│ └── strings.xml
│ ├── values-de
│ └── strings.xml
│ ├── values-es
│ └── strings.xml
│ ├── values-fr
│ └── strings.xml
│ ├── values-pl
│ └── strings.xml
│ ├── values-pt-rBR
│ └── strings.xml
│ ├── values-ru
│ └── strings.xml
│ ├── values-tr
│ └── strings.xml
│ ├── values-zh-rCN
│ └── strings.xml
│ └── values
│ ├── ic_launcher_background.xml
│ └── strings.xml
├── build.gradle.kts
├── cliff.toml
├── fastlane
└── metadata
│ └── android
│ └── en-US
│ ├── changelogs
│ ├── 1.txt
│ ├── 10.txt
│ ├── 11.txt
│ ├── 12.txt
│ ├── 13.txt
│ ├── 14.txt
│ ├── 15.txt
│ ├── 16.txt
│ ├── 17.txt
│ ├── 18.txt
│ ├── 19.txt
│ ├── 20.txt
│ ├── 21.txt
│ ├── 22.txt
│ ├── 23.txt
│ ├── 24.txt
│ ├── 25.txt
│ ├── 26.txt
│ ├── 27.txt
│ ├── 28.txt
│ ├── 29.txt
│ ├── 5.txt
│ ├── 6.txt
│ ├── 7.txt
│ ├── 8.txt
│ └── 9.txt
│ ├── full_description.txt
│ ├── images
│ ├── icon.png
│ └── phoneScreenshots
│ │ ├── 1.jpg
│ │ └── 2.jpg
│ ├── short_description.txt
│ └── title.txt
├── generate_changelog.sh
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── renovate.json
└── settings.gradle
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{kt,kts}]
2 | ktlint_function_naming_ignore_when_annotated_with=Composable
3 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @dessalines
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Discussion
4 | url: https://lemmy.ml/c/habitmaker
5 | about: Discussion board
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: 🌟 Feature request
2 | description: Suggest a feature to improve the app
3 | labels: [feature]
4 | body:
5 | - type: textarea
6 | id: feature-description
7 | attributes:
8 | label: Describe your suggested feature
9 | description: How can the app be improved?
10 | placeholder: |
11 | Example:
12 | "It should work like this..."
13 | validations:
14 | required: true
15 |
16 | - type: textarea
17 | id: other-details
18 | attributes:
19 | label: Other details
20 | placeholder: Additional details and attachments.
21 |
22 | - type: checkboxes
23 | id: acknowledgements
24 | attributes:
25 | label: Acknowledgements
26 | description: >-
27 | Read this carefully, we will close and ignore your issue if you skimmed through this.
28 | options:
29 | - label: I have written a short but informative title.
30 | required: true
31 | - label: I have updated the app to **[the latest version](https://github.com/dessalines/habit-maker/releases/latest)**.
32 | required: true
33 | - label: I have checked through the app settings for my feature.
34 | required: true
35 | - label: >-
36 | I have searched the existing issues and this is a new one, **NOT** a
37 | duplicate or related to another open issue.
38 | required: true
39 | - label: >-
40 | This is a **single** feature request, in case of multiple feature
41 | requests I will open a separate issue for each one
42 | (they can always link to each other if related)
43 | required: true
44 | - label: >-
45 | This is not a question or a discussion, in which case I should have
46 | gone to [lemmy.ml/c/habitmaker](https://lemmy.ml/c/habitmaker)
47 | required: true
48 | - label: I have admitted that I am a clown by having checked this box, as I have not read these acknowledgements. 🤡
49 | - label: I will fill out all of the requested information in this form.
50 | required: true
51 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/issue_report.yml:
--------------------------------------------------------------------------------
1 | name: 🐞 Issue report
2 | description: Report an issue or bug
3 | labels: [bug]
4 | body:
5 | - type: textarea
6 | id: reproduce-steps
7 | attributes:
8 | label: Steps to reproduce
9 | description: Provide an example of the issue.
10 | placeholder: |
11 | Example:
12 | 1. First step
13 | 2. Second step
14 | 3. Issue here
15 | validations:
16 | required: true
17 |
18 | - type: textarea
19 | id: expected-behavior
20 | attributes:
21 | label: Expected behavior
22 | description: Explain what you should expect to happen.
23 | placeholder: |
24 | Example:
25 | "This should happen..."
26 | validations:
27 | required: true
28 |
29 | - type: textarea
30 | id: actual-behavior
31 | attributes:
32 | label: Actual behavior
33 | description: Explain what actually happens.
34 | placeholder: |
35 | Example:
36 | "This happened instead..."
37 | validations:
38 | required: true
39 |
40 | - type: input
41 | id: version
42 | attributes:
43 | label: version of the program
44 | placeholder: >-
45 | Example: "2.6.14"
46 | validations:
47 | required: true
48 |
49 | - type: input
50 | id: android-version
51 | attributes:
52 | label: Android version
53 | description: You can find this somewhere in your Android settings.
54 | placeholder: |
55 | Example: "Android 14"
56 | validations:
57 | required: true
58 |
59 | - type: input
60 | id: device
61 | attributes:
62 | label: Device
63 | description: List your device and model.
64 | placeholder: |
65 | Example: "Google Pixel 8"
66 | validations:
67 | required: true
68 |
69 | - type: textarea
70 | id: other-details
71 | attributes:
72 | label: Other details
73 | placeholder: Additional details and attachments.
74 |
75 | - type: checkboxes
76 | id: acknowledgements
77 | attributes:
78 | label: Acknowledgements
79 | description: >-
80 | Read this carefully, I will close and ignore your issue if you skimmed through this.
81 | options:
82 | - label: I have written a short but informative title.
83 | required: true
84 | - label: I have updated the app to **[the latest version](https://github.com/dessalines/habit-maker/releases/latest)**.
85 | required: true
86 | - label: >-
87 | I have searched the existing issues and this is a new one, **NOT** a
88 | duplicate or related to another open issue.
89 | required: true
90 | - label: >-
91 | This is not a question or a discussion, in which case I should have
92 | gone to [lemmy.ml/c/habitmaker](https://lemmy.ml/c/habitmaker)
93 | required: true
94 | - label: >-
95 | This is a **single** bug report, in case of multiple bugs I will open
96 | a separate issue for each one
97 | (they can always link to each other if related)
98 | required: true
99 | - label: I have admitted that I am a clown by having checked this box, as I have not read these acknowledgements. 🤡
100 | - label: I have filled out all of the requested information in this form.
101 | required: true
102 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /*/build/
8 | /captures
9 | /Gemfile*
10 | .externalNativeBuild
11 | .cxx
12 | local.properties
13 | app/release
14 | app/schemas
15 | .project
16 | .settings
17 | .classpath
18 | build.sh
19 | .kotlin
20 |
--------------------------------------------------------------------------------
/.woodpecker.yml:
--------------------------------------------------------------------------------
1 | steps:
2 | prettier_markdown_check:
3 | image: tmknom/prettier
4 | commands:
5 | - prettier -c "**/*.md" "**/*.yml"
6 | when:
7 | - event: pull_request
8 |
9 | check_formatting:
10 | image: cimg/android:2025.04.1
11 | commands:
12 | - sudo chown -R circleci:circleci .
13 | - ./gradlew lintKotlin
14 | environment:
15 | GRADLE_USER_HOME: ".gradle"
16 | when:
17 | - event: pull_request
18 |
19 | check_android_lint:
20 | image: cimg/android:2025.04.1
21 | commands:
22 | - sudo chown -R circleci:circleci .
23 | - ./gradlew lint
24 | environment:
25 | GRADLE_USER_HOME: ".gradle"
26 | when:
27 | - event: pull_request
28 |
29 | build_project:
30 | image: cimg/android:2025.04.1
31 | commands:
32 | - sudo chown -R circleci:circleci .
33 | - ./gradlew assembleDebug
34 | environment:
35 | GRADLE_USER_HOME: ".gradle"
36 | when:
37 | - event: pull_request
38 |
39 | notify:
40 | image: alpine:3
41 | commands:
42 | - apk add curl
43 | - "curl -d'Habit-Maker build ${CI_PIPELINE_STATUS}: ${CI_PIPELINE_URL}' ntfy.sh/habit_maker_ci"
44 | when:
45 | - event: pull_request
46 | status: [failure, success]
47 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Habit-Maker
2 |
3 |
4 |
5 |
6 |
7 | - [Ways to contribute](#ways-to-contribute)
8 | - [Application structure](#application-structure)
9 | - [Code contributions](#code-contributions)
10 | * [Kotlin](#kotlin)
11 | * [Code quality](#code-quality)
12 | - [Adding translations](#adding-translations)
13 |
14 |
15 |
16 |
17 |
18 | ## Ways to contribute
19 |
20 | - Participate here and start answering questions.
21 | - File new bug reports for issues you find.
22 | - Add missing translations.
23 | - Code contributions.
24 |
25 | ## Application structure
26 |
27 | - Basic [Modern Android Development](https://developer.android.com/series/mad-skills) tech stack (Compose, Navigation, Coroutines, AndroidX)
28 | - Guide to [App Architecture](https://developer.android.com/topic/architecture), without domain layer. Basically, MVVM + Repositories for data access.
29 |
30 | ## Code contributions
31 |
32 | You can open in AndroidStudio, version 2022.3.1 or later (Giraffe).
33 |
34 | Use Java 11+, preferably Java 17
35 |
36 | ### Kotlin
37 |
38 | This project is full Kotlin. Please do not write Java classes.
39 |
40 | ### Code quality
41 |
42 | The code must be formatted to a [common standard](https://pinterest.github.io/ktlint/0.49.1/rules/standard/).
43 |
44 | To check for violations
45 |
46 | ```shell
47 | ./gradlew lintKotlin
48 | ```
49 |
50 | Or just run this to fix them
51 |
52 | ```shell
53 | ./gradlew formatKotlin
54 | ```
55 |
56 | Markdown and yaml files are formatted according to prettier.
57 |
58 | You can install prettier either through the plugin, or globally using npm `npm install -g prettier`
59 |
60 | To check for violations
61 |
62 | ```shell
63 | prettier -c "*.md" "*.yml"
64 | ```
65 |
66 | To fix the violations
67 |
68 | ```shell
69 | prettier --write "*.md" "*.yml"
70 | ```
71 |
72 | ## Adding translations
73 |
74 | You can add translation via [weblate](https://hosted.weblate.org/engage/habit-maker/), or add translations manually in the `app/src/main/res/values-{locale}/strings.xml` file.
75 |
76 | You can open it in android studio, right click and click open translations editor or you can
77 | directly edit the files.
78 |
79 | If you add a new locale. Also add it in `locales_config.xml`. Don't forget to escape `'` in translations.
80 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | id("org.jetbrains.kotlin.android")
4 | id("org.jetbrains.kotlin.plugin.compose")
5 | id("com.google.devtools.ksp")
6 | }
7 |
8 | android {
9 | buildToolsVersion = "35.0.0"
10 | compileSdk = 35
11 |
12 | defaultConfig {
13 | applicationId = "com.dessalines.habitmaker"
14 | minSdk = 26
15 | targetSdk = 35
16 | versionCode = 29
17 | versionName = "0.0.29"
18 |
19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
20 | vectorDrawables {
21 | useSupportLibrary = true
22 | }
23 | ksp { arg("room.schemaLocation", "$projectDir/schemas") }
24 | }
25 |
26 | // Necessary for izzyondroid releases
27 | dependenciesInfo {
28 | // Disables dependency metadata when building APKs.
29 | includeInApk = false
30 | // Disables dependency metadata when building Android App Bundles.
31 | includeInBundle = false
32 | }
33 |
34 | if (project.hasProperty("RELEASE_STORE_FILE")) {
35 | signingConfigs {
36 | create("release") {
37 | storeFile = file(project.property("RELEASE_STORE_FILE")!!)
38 | storePassword = project.property("RELEASE_STORE_PASSWORD") as String?
39 | keyAlias = project.property("RELEASE_KEY_ALIAS") as String?
40 | keyPassword = project.property("RELEASE_KEY_PASSWORD") as String?
41 |
42 | // Optional, specify signing versions used
43 | enableV1Signing = true
44 | enableV2Signing = true
45 | }
46 | }
47 | }
48 |
49 | buildTypes {
50 | release {
51 | if (project.hasProperty("RELEASE_STORE_FILE")) {
52 | signingConfig = signingConfigs.getByName("release")
53 | }
54 |
55 | isMinifyEnabled = true
56 | isShrinkResources = true
57 | proguardFiles(
58 | // Includes the default ProGuard rules files that are packaged with
59 | // the Android Gradle plugin. To learn more, go to the section about
60 | // R8 configuration files.
61 | getDefaultProguardFile("proguard-android-optimize.txt"),
62 |
63 | // Includes a local, custom Proguard rules file
64 | "proguard-rules.pro"
65 | )
66 | }
67 | debug {
68 | applicationIdSuffix = ".debug"
69 | versionNameSuffix = " (DEBUG)"
70 | }
71 | }
72 |
73 | lint {
74 | disable += "MissingTranslation"
75 | disable += "KtxExtensionAvailable"
76 | disable += "UseKtx"
77 | }
78 |
79 | compileOptions {
80 | sourceCompatibility = JavaVersion.VERSION_17
81 | targetCompatibility = JavaVersion.VERSION_17
82 | }
83 | kotlinOptions {
84 | jvmTarget = "17"
85 | freeCompilerArgs = listOf("-Xjvm-default=all-compatibility", "-opt-in=kotlin.RequiresOptIn")
86 | }
87 | buildFeatures {
88 | compose = true
89 | }
90 | namespace = "com.dessalines.habitmaker"
91 | }
92 |
93 | dependencies {
94 |
95 | // Workmanager
96 | implementation("androidx.work:work-runtime-ktx:2.10.1")
97 |
98 | // Compose-Calendar
99 | implementation("com.kizitonwose.calendar:compose:2.7.0")
100 |
101 | // Exporting / importing DB helper
102 | implementation("com.github.dessalines:room-db-export-import:0.1.0")
103 |
104 | // Compose BOM
105 | implementation(platform("androidx.compose:compose-bom:2025.05.01"))
106 | implementation("androidx.compose.ui:ui")
107 | implementation("androidx.compose.material3:material3")
108 | implementation("androidx.compose.material:material-icons-extended:1.7.8")
109 | implementation("androidx.compose.material3:material3-window-size-class")
110 | implementation("androidx.compose.ui:ui-tooling")
111 | implementation("androidx.compose.runtime:runtime-livedata:1.8.2")
112 |
113 | // Adaptive layouts
114 | implementation("androidx.compose.material3.adaptive:adaptive:1.1.0")
115 | implementation("androidx.compose.material3.adaptive:adaptive-layout:1.1.0")
116 | implementation("androidx.compose.material3.adaptive:adaptive-navigation:1.1.0")
117 | implementation("androidx.compose.material3:material3-adaptive-navigation-suite")
118 |
119 | // Activities
120 | implementation("androidx.activity:activity-compose:1.10.1")
121 | implementation("androidx.activity:activity-ktx:1.10.1")
122 |
123 | // LiveData
124 | implementation("androidx.lifecycle:lifecycle-runtime-compose:2.9.0")
125 |
126 | // Navigation
127 | implementation("androidx.navigation:navigation-compose:2.9.0")
128 |
129 | // Markdown
130 | implementation("com.github.jeziellago:compose-markdown:0.5.7")
131 |
132 | // Preferences
133 | implementation("me.zhanghai.compose.preference:library:1.1.1")
134 |
135 | // Room
136 | // To use Kotlin annotation processing tool
137 | ksp("androidx.room:room-compiler:2.7.1")
138 | implementation("androidx.room:room-runtime:2.7.1")
139 | annotationProcessor("androidx.room:room-compiler:2.7.1")
140 |
141 | // optional - Kotlin Extensions and Coroutines support for Room
142 | implementation("androidx.room:room-ktx:2.7.1")
143 |
144 | // App compat
145 | implementation("androidx.appcompat:appcompat:1.7.0")
146 | }
147 |
--------------------------------------------------------------------------------
/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.kts.
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 | -dontwarn okhttp3.internal.platform.**
23 | -dontwarn org.conscrypt.**
24 | -dontwarn org.bouncycastle.**
25 | -dontwarn org.openjsse.**
26 |
--------------------------------------------------------------------------------
/app/src/debug/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Habit Maker (Debug)
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
16 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/assets/RELEASES.md:
--------------------------------------------------------------------------------
1 | ## What's Changed in 0.0.29
2 |
3 | - Adding android lint. by @dessalines in [#182](https://github.com/dessalines/habit-maker/pull/182)
4 | - Fixing delete crash. Fixes #180 by @dessalines in [#181](https://github.com/dessalines/habit-maker/pull/181)
5 |
6 | **Full Changelog**: https://github.com/dessalines/habit-maker/compare/0.0.28...0.0.29
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dessalines/habit-maker/9ecb7a9fc4f067bd5af6859b195a319adb2244dc/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/db/AppDB.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.db
2 |
3 | import android.content.ContentValues
4 | import android.content.Context
5 | import android.database.sqlite.SQLiteDatabase.CONFLICT_IGNORE
6 | import androidx.room.Database
7 | import androidx.room.Room
8 | import androidx.room.RoomDatabase
9 | import androidx.room.TypeConverters
10 | import androidx.sqlite.db.SupportSQLiteDatabase
11 | import com.dessalines.habitmaker.utils.TAG
12 | import java.util.concurrent.Executors
13 |
14 | @Database(
15 | version = 6,
16 | entities = [
17 | AppSettings::class,
18 | Habit::class,
19 | Encouragement::class,
20 | HabitCheck::class,
21 | HabitReminder::class,
22 | ],
23 | exportSchema = true,
24 | )
25 | @TypeConverters(Converters::class)
26 | abstract class AppDB : RoomDatabase() {
27 | abstract fun appSettingsDao(): AppSettingsDao
28 |
29 | abstract fun habitDao(): HabitDao
30 |
31 | abstract fun encouragementDao(): EncouragementDao
32 |
33 | abstract fun habitCheckDao(): HabitCheckDao
34 |
35 | abstract fun habitReminderDao(): HabitReminderDao
36 |
37 | companion object {
38 | @Volatile
39 | private var instance: AppDB? = null
40 |
41 | fun getDatabase(context: Context): AppDB {
42 | // if the INSTANCE is not null, then return it,
43 | // if it is, then create the database
44 | return instance ?: synchronized(this) {
45 | val i =
46 | Room
47 | .databaseBuilder(
48 | context.applicationContext,
49 | AppDB::class.java,
50 | TAG,
51 | ).allowMainThreadQueries()
52 | .addMigrations(
53 | MIGRATION_1_2,
54 | MIGRATION_2_3,
55 | MIGRATION_3_4,
56 | MIGRATION_4_5,
57 | MIGRATION_5_6,
58 | )
59 | // Necessary because it can't insert data on creation
60 | .addCallback(
61 | object : Callback() {
62 | override fun onOpen(db: SupportSQLiteDatabase) {
63 | super.onCreate(db)
64 | Executors.newSingleThreadExecutor().execute {
65 | db.insert(
66 | "AppSettings",
67 | // Ensures it won't overwrite the existing data
68 | CONFLICT_IGNORE,
69 | ContentValues(2).apply {
70 | put("id", 1)
71 | },
72 | )
73 | }
74 | }
75 | },
76 | ).build()
77 | instance = i
78 | // return instance
79 | i
80 | }
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/db/Converters.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.db
2 |
3 | import androidx.room.TypeConverter
4 | import java.time.DayOfWeek
5 | import java.time.LocalTime
6 |
7 | class Converters {
8 | @TypeConverter
9 | fun toLocalTime(time: Long?): LocalTime? =
10 | time?.let {
11 | LocalTime.ofSecondOfDay(it)
12 | }
13 |
14 | @TypeConverter
15 | fun toTimestamp(time: LocalTime?): Long? = time?.toSecondOfDay()?.toLong()
16 |
17 | @TypeConverter
18 | fun toDayOfWeek(day: Int?): DayOfWeek? =
19 | day?.let {
20 | DayOfWeek.entries[day]
21 | }
22 |
23 | @TypeConverter
24 | fun toDay(day: DayOfWeek?): Int? = day?.ordinal
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/db/Encouragement.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.db
2 |
3 | import androidx.annotation.Keep
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.ViewModelProvider
6 | import androidx.room.ColumnInfo
7 | import androidx.room.Dao
8 | import androidx.room.Entity
9 | import androidx.room.ForeignKey
10 | import androidx.room.Index
11 | import androidx.room.Insert
12 | import androidx.room.OnConflictStrategy
13 | import androidx.room.PrimaryKey
14 | import androidx.room.Query
15 | import java.io.Serializable
16 |
17 | @Entity(
18 | foreignKeys = [
19 | ForeignKey(
20 | entity = Habit::class,
21 | parentColumns = arrayOf("id"),
22 | childColumns = arrayOf("habit_id"),
23 | onDelete = ForeignKey.CASCADE,
24 | ),
25 | ],
26 | indices = [Index(value = ["habit_id", "content"], unique = true)],
27 | )
28 | @Keep
29 | data class Encouragement(
30 | @PrimaryKey(autoGenerate = true) val id: Int,
31 | @ColumnInfo(
32 | name = "habit_id",
33 | )
34 | val habitId: Int,
35 | @ColumnInfo(
36 | name = "content",
37 | )
38 | val content: String,
39 | ) : Serializable
40 |
41 | @Entity
42 | data class EncouragementInsert(
43 | @ColumnInfo(
44 | name = "habit_id",
45 | )
46 | val habitId: Int,
47 | @ColumnInfo(
48 | name = "content",
49 | )
50 | val content: String,
51 | )
52 |
53 | private const val BY_HABIT_ID_QUERY = "SELECT * FROM Encouragement where habit_id = :habitId"
54 |
55 | @Dao
56 | interface EncouragementDao {
57 | @Query(BY_HABIT_ID_QUERY)
58 | fun listForHabitSync(habitId: Int): List
59 |
60 | @Query("SELECT * FROM Encouragement where habit_id = :habitId ORDER BY RANDOM() LIMIT 1")
61 | fun getRandomForHabit(habitId: Int): Encouragement?
62 |
63 | @Insert(entity = Encouragement::class, onConflict = OnConflictStrategy.IGNORE)
64 | fun insert(encouragement: EncouragementInsert): Long
65 |
66 | @Query("DELETE FROM Encouragement where habit_id = :habitId")
67 | fun deleteForHabit(habitId: Int)
68 | }
69 |
70 | // Declares the DAO as a private property in the constructor. Pass in the DAO
71 | // instead of the whole database, because you only need access to the DAO
72 | class EncouragementRepository(
73 | private val encouragementDao: EncouragementDao,
74 | ) {
75 | fun listForHabitSync(habitId: Int) = encouragementDao.listForHabitSync(habitId)
76 |
77 | fun getRandomForHabit(habitId: Int) = encouragementDao.getRandomForHabit(habitId)
78 |
79 | fun deleteForHabit(habitId: Int) = encouragementDao.deleteForHabit(habitId)
80 |
81 | fun insert(encouragement: EncouragementInsert) = encouragementDao.insert(encouragement)
82 | }
83 |
84 | class EncouragementViewModel(
85 | private val repository: EncouragementRepository,
86 | ) : ViewModel() {
87 | fun listForHabitSync(habitId: Int) = repository.listForHabitSync(habitId)
88 |
89 | fun getRandomForHabit(habitId: Int) = repository.getRandomForHabit(habitId)
90 |
91 | fun deleteForHabit(habitId: Int) = repository.deleteForHabit(habitId)
92 |
93 | fun insert(encouragement: EncouragementInsert) = repository.insert(encouragement)
94 | }
95 |
96 | class EncouragementViewModelFactory(
97 | private val repository: EncouragementRepository,
98 | ) : ViewModelProvider.Factory {
99 | override fun create(modelClass: Class): T {
100 | if (modelClass.isAssignableFrom(EncouragementViewModel::class.java)) {
101 | @Suppress("UNCHECKED_CAST")
102 | return EncouragementViewModel(repository) as T
103 | }
104 | throw IllegalArgumentException("Unknown ViewModel class")
105 | }
106 | }
107 |
108 | val sampleEncouragements =
109 | listOf(
110 | Encouragement(
111 | id = 1,
112 | habitId = 1,
113 | content = "Great job, keep going!",
114 | ),
115 | Encouragement(
116 | id = 2,
117 | habitId = 1,
118 | content = "Excellent! Remember why you're doing this.",
119 | ),
120 | Encouragement(
121 | id = 3,
122 | habitId = 1,
123 | content = "Nice! You're almost there!",
124 | ),
125 | )
126 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/db/HabitCheck.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.db
2 |
3 | import androidx.annotation.Keep
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.ViewModelProvider
6 | import androidx.room.ColumnInfo
7 | import androidx.room.Dao
8 | import androidx.room.Entity
9 | import androidx.room.ForeignKey
10 | import androidx.room.Index
11 | import androidx.room.Insert
12 | import androidx.room.OnConflictStrategy
13 | import androidx.room.PrimaryKey
14 | import androidx.room.Query
15 | import kotlinx.coroutines.flow.Flow
16 | import java.io.Serializable
17 |
18 | @Entity(
19 | foreignKeys = [
20 | ForeignKey(
21 | entity = Habit::class,
22 | parentColumns = arrayOf("id"),
23 | childColumns = arrayOf("habit_id"),
24 | onDelete = ForeignKey.CASCADE,
25 | ),
26 | ],
27 | indices = [Index(value = ["habit_id", "check_time"], unique = true)],
28 | )
29 | @Keep
30 | data class HabitCheck(
31 | @PrimaryKey(autoGenerate = true) val id: Int,
32 | @ColumnInfo(
33 | name = "habit_id",
34 | )
35 | val habitId: Int,
36 | @ColumnInfo(
37 | name = "check_time",
38 | )
39 | val checkTime: Long,
40 | ) : Serializable
41 |
42 | @Entity
43 | data class HabitCheckInsert(
44 | @ColumnInfo(
45 | name = "habit_id",
46 | )
47 | val habitId: Int,
48 | @ColumnInfo(
49 | name = "check_time",
50 | )
51 | val checkTime: Long,
52 | )
53 |
54 | private const val BY_HABIT_ID_QUERY = "SELECT * FROM HabitCheck where habit_id = :habitId order by check_time"
55 |
56 | @Dao
57 | interface HabitCheckDao {
58 | @Query(BY_HABIT_ID_QUERY)
59 | fun listForHabit(habitId: Int): Flow>
60 |
61 | @Query(BY_HABIT_ID_QUERY)
62 | fun listForHabitSync(habitId: Int): List
63 |
64 | @Insert(entity = HabitCheck::class, onConflict = OnConflictStrategy.IGNORE)
65 | fun insert(habitCheck: HabitCheckInsert): Long
66 |
67 | @Query("DELETE FROM HabitCheck where habit_id = :habitId and check_time = :checkTime")
68 | fun deleteForDay(
69 | habitId: Int,
70 | checkTime: Long,
71 | )
72 | }
73 |
74 | // Declares the DAO as a private property in the constructor. Pass in the DAO
75 | // instead of the whole database, because you only need access to the DAO
76 | class HabitCheckRepository(
77 | private val habitCheckDao: HabitCheckDao,
78 | ) {
79 | // Room executes all queries on a separate thread.
80 | // Observed Flow will notify the observer when the data has changed.
81 | fun listForHabit(habitId: Int) = habitCheckDao.listForHabit(habitId)
82 |
83 | fun listForHabitSync(habitId: Int) = habitCheckDao.listForHabitSync(habitId)
84 |
85 | fun insert(habitCheck: HabitCheckInsert) = habitCheckDao.insert(habitCheck)
86 |
87 | fun deleteForDay(
88 | habitId: Int,
89 | checkTime: Long,
90 | ) = habitCheckDao.deleteForDay(habitId, checkTime)
91 | }
92 |
93 | class HabitCheckViewModel(
94 | private val repository: HabitCheckRepository,
95 | ) : ViewModel() {
96 | fun listForHabit(habitId: Int) = repository.listForHabit(habitId)
97 |
98 | fun listForHabitSync(habitId: Int) = repository.listForHabitSync(habitId)
99 |
100 | fun insert(habitCheck: HabitCheckInsert) = repository.insert(habitCheck)
101 |
102 | fun deleteForDay(
103 | habitId: Int,
104 | checkTime: Long,
105 | ) = repository.deleteForDay(habitId, checkTime)
106 | }
107 |
108 | class HabitCheckViewModelFactory(
109 | private val repository: HabitCheckRepository,
110 | ) : ViewModelProvider.Factory {
111 | override fun create(modelClass: Class): T {
112 | if (modelClass.isAssignableFrom(HabitCheckViewModel::class.java)) {
113 | @Suppress("UNCHECKED_CAST")
114 | return HabitCheckViewModel(repository) as T
115 | }
116 | throw IllegalArgumentException("Unknown ViewModel class")
117 | }
118 | }
119 |
120 | val sampleHabitChecks =
121 | listOf(
122 | HabitCheck(
123 | id = 1,
124 | habitId = 1,
125 | checkTime = 0,
126 | ),
127 | HabitCheck(
128 | id = 2,
129 | habitId = 2,
130 | checkTime = 0,
131 | ),
132 | )
133 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/db/HabitReminder.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.db
2 | import androidx.annotation.Keep
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.ViewModelProvider
5 | import androidx.room.ColumnInfo
6 | import androidx.room.Dao
7 | import androidx.room.Entity
8 | import androidx.room.ForeignKey
9 | import androidx.room.Index
10 | import androidx.room.Insert
11 | import androidx.room.OnConflictStrategy
12 | import androidx.room.PrimaryKey
13 | import androidx.room.Query
14 | import java.io.Serializable
15 | import java.time.DayOfWeek
16 | import java.time.LocalTime
17 |
18 | @Entity(
19 | foreignKeys = [
20 | ForeignKey(
21 | entity = Habit::class,
22 | parentColumns = arrayOf("id"),
23 | childColumns = arrayOf("habit_id"),
24 | onDelete = ForeignKey.CASCADE,
25 | ),
26 | ],
27 | indices = [Index(value = ["habit_id", "time", "day"], unique = true)],
28 | )
29 | @Keep
30 | data class HabitReminder(
31 | @PrimaryKey(autoGenerate = true) val id: Int,
32 | @ColumnInfo(
33 | name = "habit_id",
34 | )
35 | val habitId: Int,
36 | @ColumnInfo(
37 | name = "time",
38 | )
39 | val time: LocalTime,
40 | @ColumnInfo(
41 | name = "day",
42 | )
43 | val day: DayOfWeek,
44 | ) : Serializable
45 |
46 | @Entity
47 | data class HabitReminderInsert(
48 | @ColumnInfo(
49 | name = "habit_id",
50 | )
51 | val habitId: Int,
52 | @ColumnInfo(
53 | name = "time",
54 | )
55 | val time: LocalTime,
56 | @ColumnInfo(
57 | name = "day",
58 | )
59 | val day: DayOfWeek,
60 | )
61 |
62 | private const val BY_HABIT_ID_QUERY = "SELECT * FROM HabitReminder where habit_id = :habitId"
63 |
64 | @Dao
65 | interface HabitReminderDao {
66 | @Query(BY_HABIT_ID_QUERY)
67 | fun listForHabitSync(habitId: Int): List
68 |
69 | @Insert(entity = HabitReminder::class, onConflict = OnConflictStrategy.IGNORE)
70 | fun insert(habitReminder: HabitReminderInsert): Long
71 |
72 | @Query("DELETE FROM HabitReminder where habit_id = :habitId")
73 | fun delete(habitId: Int)
74 | }
75 |
76 | // Declares the DAO as a private property in the constructor. Pass in the DAO
77 | // instead of the whole database, because you only need access to the DAO
78 | class HabitReminderRepository(
79 | private val habitReminderDao: HabitReminderDao,
80 | ) {
81 | fun listForHabitSync(habitId: Int) = habitReminderDao.listForHabitSync(habitId)
82 |
83 | fun insert(habitReminder: HabitReminderInsert) = habitReminderDao.insert(habitReminder)
84 |
85 | fun delete(habitId: Int) = habitReminderDao.delete(habitId)
86 | }
87 |
88 | class HabitReminderViewModel(
89 | private val repository: HabitReminderRepository,
90 | ) : ViewModel() {
91 | fun listForHabitSync(habitId: Int) = repository.listForHabitSync(habitId)
92 |
93 | fun insert(habitReminder: HabitReminderInsert) = repository.insert(habitReminder)
94 |
95 | fun delete(habitId: Int) = repository.delete(habitId)
96 | }
97 |
98 | class HabitReminderViewModelFactory(
99 | private val repository: HabitReminderRepository,
100 | ) : ViewModelProvider.Factory {
101 | override fun create(modelClass: Class): T {
102 | if (modelClass.isAssignableFrom(HabitReminderViewModel::class.java)) {
103 | @Suppress("UNCHECKED_CAST")
104 | return HabitReminderViewModel(repository) as T
105 | }
106 | throw IllegalArgumentException("Unknown ViewModel class")
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/db/Migrations.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.db
2 |
3 | import androidx.room.migration.Migration
4 | import androidx.sqlite.db.SupportSQLiteDatabase
5 | import com.dessalines.habitmaker.utils.toEpochMillis
6 | import java.time.LocalDate
7 |
8 | /**
9 | * Adds a context column for habits (IE when / where)
10 | */
11 | val MIGRATION_1_2 =
12 | object : Migration(1, 2) {
13 | override fun migrate(db: SupportSQLiteDatabase) {
14 | db.execSQL(
15 | "ALTER TABLE Habit ADD COLUMN context TEXT",
16 | )
17 | }
18 | }
19 |
20 | /**
21 | * Replace the completed boolean column, for a last_streak_time column.
22 | */
23 | val MIGRATION_2_3 =
24 | object : Migration(2, 3) {
25 | override fun migrate(db: SupportSQLiteDatabase) {
26 | val now = LocalDate.now().toEpochMillis()
27 | db.execSQL(
28 | "ALTER TABLE Habit ADD COLUMN last_streak_time INTEGER NOT NULL DEFAULT 0",
29 | )
30 | db.execSQL(
31 | "ALTER TABLE Habit ADD COLUMN last_completed_time INTEGER NOT NULL DEFAULT 0",
32 | )
33 | db.execSQL(
34 | "UPDATE Habit set last_streak_time = $now, last_completed_time = $now where completed = 1",
35 | )
36 | db.execSQL("CREATE INDEX IF NOT EXISTS `index_Habit_last_streak_time` ON Habit (last_streak_time)")
37 | db.execSQL("CREATE INDEX IF NOT EXISTS `index_Habit_last_completed_time` ON Habit (last_completed_time)")
38 | }
39 | }
40 |
41 | /**
42 | * Create a table for habit reminders
43 | */
44 | val MIGRATION_3_4 =
45 | object : Migration(3, 4) {
46 | override fun migrate(db: SupportSQLiteDatabase) {
47 | db.execSQL(
48 | """
49 | CREATE TABLE IF NOT EXISTS HabitReminder (
50 | `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
51 | `habit_id` INTEGER NOT NULL,
52 | `time` INTEGER NOT NULL,
53 | `day` INTEGER NOT NULL,
54 | FOREIGN KEY(`habit_id`) REFERENCES `Habit`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE
55 | )
56 | """.trimIndent(),
57 | )
58 | db.execSQL(
59 | "CREATE UNIQUE INDEX IF NOT EXISTS `index_HabitReminder_habit_id_time_day` ON `HabitReminder` (`habit_id`, `time`,`day`)",
60 | )
61 | }
62 | }
63 |
64 | /**
65 | * Add a setting to hide the chip descriptions.
66 | */
67 | val MIGRATION_4_5 =
68 | object : Migration(4, 5) {
69 | override fun migrate(db: SupportSQLiteDatabase) {
70 | db.execSQL(
71 | "ALTER TABLE AppSettings ADD COLUMN hide_chip_descriptions INTEGER NOT NULL DEFAULT 0",
72 | )
73 | }
74 | }
75 |
76 | /**
77 | * Add a setting to hide the days completed on home
78 | */
79 | val MIGRATION_5_6 =
80 | object : Migration(5, 6) {
81 | override fun migrate(db: SupportSQLiteDatabase) {
82 | db.execSQL(
83 | "ALTER TABLE AppSettings ADD COLUMN hide_days_completed_on_home INTEGER NOT NULL DEFAULT 0",
84 | )
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/notifications/ReminderManager.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.notifications
2 |
3 | import android.content.Context
4 | import androidx.work.OneTimeWorkRequestBuilder
5 | import androidx.work.WorkManager
6 | import androidx.work.workDataOf
7 | import com.dessalines.habitmaker.db.HabitReminder
8 | import java.time.LocalDate
9 | import java.time.LocalDateTime
10 | import java.time.ZoneOffset
11 | import java.time.temporal.TemporalAdjusters
12 | import java.util.concurrent.TimeUnit
13 | import kotlin.collections.component1
14 | import kotlin.collections.component2
15 |
16 | fun cancelReminders(ctx: Context) {
17 | val workManager = WorkManager.getInstance(ctx)
18 | workManager.cancelAllWork()
19 | }
20 |
21 | fun scheduleRemindersForHabit(
22 | ctx: Context,
23 | reminders: List,
24 | habitName: String,
25 | habitId: Int,
26 | skipToday: Boolean,
27 | ) {
28 | deleteRemindersForHabit(ctx, habitId)
29 |
30 | // Schedule them again
31 | reminders.forEach { reminder ->
32 | scheduleReminderForHabit(ctx, reminder, habitName, habitId, skipToday)
33 | }
34 | }
35 |
36 | private fun scheduleReminderForHabit(
37 | ctx: Context,
38 | reminder: HabitReminder,
39 | habitName: String,
40 | habitId: Int,
41 | skipToday: Boolean,
42 | ) {
43 | val workManager = WorkManager.getInstance(ctx)
44 |
45 | val myWorkRequestBuilder = OneTimeWorkRequestBuilder()
46 |
47 | val adjuster =
48 | if (skipToday) {
49 | TemporalAdjusters.next(reminder.day)
50 | } else {
51 | TemporalAdjusters.nextOrSame(reminder.day)
52 | }
53 |
54 | // Work manager cant handle specific times, so you have to use diffs from now.
55 | val nextDate = LocalDate.now().with(adjuster)
56 |
57 | val scheduledTime = reminder.time.atDate(nextDate)
58 | val diff =
59 | scheduledTime.toEpochSecond(ZoneOffset.UTC) -
60 | LocalDateTime
61 | .now()
62 | .toEpochSecond(ZoneOffset.UTC)
63 |
64 | // Only schedule it if the diff > 0
65 | if (diff > 0) {
66 | myWorkRequestBuilder
67 | .setInputData(workDataOf(HABIT_TITLE_KEY to habitName, HABIT_ID_KEY to habitId))
68 | // Only milliseconds seems to work here
69 | .setInitialDelay(diff * 1000, TimeUnit.MILLISECONDS)
70 | // Add the habit id as the tag, so they can all be canceled at once
71 | .addTag(habitId.toString())
72 | workManager.enqueue(myWorkRequestBuilder.build())
73 | }
74 | }
75 |
76 | fun deleteRemindersForHabit(
77 | ctx: Context,
78 | habitId: Int,
79 | ) {
80 | val workManager = WorkManager.getInstance(ctx)
81 |
82 | // Cancel work for the current habit
83 | workManager.cancelAllWorkByTag(habitId.toString())
84 | }
85 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/notifications/ReminderWorker.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.notifications
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.NotificationManager
5 | import android.app.PendingIntent
6 | import android.content.Context
7 | import android.content.Context.NOTIFICATION_SERVICE
8 | import android.content.Intent
9 | import androidx.core.app.NotificationCompat
10 | import androidx.work.Worker
11 | import androidx.work.WorkerParameters
12 | import com.dessalines.habitmaker.MainActivity
13 | import com.dessalines.habitmaker.R
14 | import com.dessalines.habitmaker.utils.TAG
15 |
16 | class ReminderWorker(
17 | ctx: Context,
18 | workerParams: WorkerParameters,
19 | ) : Worker(ctx, workerParams) {
20 | @SuppressLint("MissingPermission")
21 | override fun doWork(): Result {
22 | val habitTitle = inputData.getString(HABIT_TITLE_KEY)
23 | val habitId = inputData.getInt(HABIT_ID_KEY, 0)
24 |
25 | val notificationManager = applicationContext.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
26 |
27 | val mainIntent =
28 | Intent(applicationContext, MainActivity::class.java).apply {
29 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
30 | }
31 | val mainPI: PendingIntent =
32 | PendingIntent.getActivity(
33 | applicationContext,
34 | 0,
35 | mainIntent,
36 | PendingIntent.FLAG_IMMUTABLE,
37 | )
38 | val checkHabitIntent =
39 | Intent(CHECK_HABIT_INTENT_ACTION).apply {
40 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
41 | putExtra(CHECK_HABIT_INTENT_HABIT_ID, habitId)
42 | }
43 | val checkHabitPI: PendingIntent =
44 | PendingIntent.getBroadcast(applicationContext, habitId, checkHabitIntent, PendingIntent.FLAG_IMMUTABLE)
45 |
46 | val cancelHabitIntent =
47 | Intent(CANCEL_HABIT_INTENT_ACTION).apply {
48 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
49 | putExtra(CANCEL_HABIT_INTENT_HABIT_ID, habitId)
50 | putExtra("clap", "42")
51 | putExtra("num", 43)
52 | }
53 | val cancelHabitPI: PendingIntent =
54 | PendingIntent.getBroadcast(applicationContext, habitId, cancelHabitIntent, PendingIntent.FLAG_IMMUTABLE)
55 |
56 | val body = applicationContext.getString(R.string.did_you_complete)
57 | val builder =
58 | NotificationCompat
59 | .Builder(applicationContext, TAG)
60 | .setSmallIcon(R.drawable.ic_launcher_foreground)
61 | .setContentTitle(habitTitle)
62 | .setContentText(body)
63 | .setPriority(NotificationCompat.PRIORITY_DEFAULT)
64 | .setContentIntent(mainPI)
65 | .setAutoCancel(true)
66 | // The yes and no actions
67 | .addAction(0, applicationContext.getString(R.string.yes), checkHabitPI)
68 | .addAction(0, applicationContext.getString(R.string.no), cancelHabitPI)
69 |
70 | notificationManager.notify(habitId, builder.build())
71 |
72 | return Result.success()
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/notifications/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.notifications
2 |
3 | import android.app.NotificationChannel
4 | import android.app.NotificationManager
5 | import android.content.BroadcastReceiver
6 | import android.content.Context
7 | import android.content.Context.NOTIFICATION_SERVICE
8 | import android.content.Intent
9 | import android.content.IntentFilter
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.DisposableEffect
12 | import androidx.compose.runtime.getValue
13 | import androidx.compose.runtime.rememberUpdatedState
14 | import androidx.compose.ui.platform.LocalContext
15 | import androidx.core.content.ContextCompat
16 | import com.dessalines.habitmaker.utils.TAG
17 |
18 | const val CHECK_HABIT_INTENT_ACTION = "check-habit"
19 | const val CHECK_HABIT_INTENT_HABIT_ID = "check-habit-id"
20 | const val CANCEL_HABIT_INTENT_ACTION = "cancel-habit"
21 | const val CANCEL_HABIT_INTENT_HABIT_ID = "cancel-habit-id"
22 | const val HABIT_TITLE_KEY = "habit-title"
23 | const val HABIT_ID_KEY = "habit-id"
24 |
25 | fun createNotificationChannel(ctx: Context) {
26 | val importance = NotificationManager.IMPORTANCE_DEFAULT
27 | val channel = NotificationChannel(TAG, TAG, importance)
28 | // Register the channel with the system
29 | val notificationManager = ctx.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
30 | notificationManager.createNotificationChannel(channel)
31 | }
32 |
33 | @Composable
34 | fun SystemBroadcastReceiver(
35 | systemAction: String,
36 | onSystemEvent: (intent: Intent?) -> Unit,
37 | ) {
38 | val context = LocalContext.current
39 | val currentOnSystemEvent by rememberUpdatedState(onSystemEvent)
40 |
41 | DisposableEffect(context, systemAction) {
42 | val intentFilter = IntentFilter(systemAction)
43 |
44 | val receiver =
45 | object : BroadcastReceiver() {
46 | override fun onReceive(
47 | context: Context?,
48 | intent: Intent?,
49 | ) {
50 | currentOnSystemEvent(intent)
51 | }
52 | }
53 |
54 | ContextCompat.registerReceiver(
55 | context,
56 | receiver,
57 | intentFilter,
58 | ContextCompat.RECEIVER_EXPORTED,
59 | )
60 |
61 | onDispose {
62 | context.unregisterReceiver(receiver)
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/ui/components/common/AppBars.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.ui.components.common
2 |
3 | import androidx.compose.foundation.BasicTooltipBox
4 | import androidx.compose.foundation.ExperimentalFoundationApi
5 | import androidx.compose.foundation.rememberBasicTooltipState
6 | import androidx.compose.material.icons.Icons
7 | import androidx.compose.material.icons.automirrored.outlined.ArrowBack
8 | import androidx.compose.material3.ExperimentalMaterial3Api
9 | import androidx.compose.material3.Icon
10 | import androidx.compose.material3.IconButton
11 | import androidx.compose.material3.TooltipDefaults
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.res.stringResource
14 | import com.dessalines.habitmaker.R
15 |
16 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
17 | @Composable
18 | fun BackButton(onBackClick: () -> Unit) {
19 | val tooltipPosition = TooltipDefaults.rememberPlainTooltipPositionProvider()
20 | BasicTooltipBox(
21 | positionProvider = tooltipPosition,
22 | state = rememberBasicTooltipState(isPersistent = false),
23 | tooltip = {
24 | ToolTip(stringResource(R.string.go_back))
25 | },
26 | ) {
27 | IconButton(
28 | onClick = onBackClick,
29 | ) {
30 | Icon(
31 | Icons.AutoMirrored.Outlined.ArrowBack,
32 | contentDescription = stringResource(R.string.go_back),
33 | )
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/ui/components/common/Sizes.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.ui.components.common
2 |
3 | import androidx.compose.ui.unit.dp
4 |
5 | val SMALL_PADDING = 8.dp
6 | val MEDIUM_PADDING = 12.dp
7 | val LARGE_PADDING = 16.dp
8 | val SMALL_HEIGHT = 40.dp
9 | val MEDIUM_HEIGHT = 60.dp
10 | val LARGE_HEIGHT = 80.dp
11 | val MAX_HEIGHT = 160.dp
12 | val FLOATING_BUTTON_SIZE = 28.dp
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/ui/components/habit/CreateHabitScreen.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.ui.components.habit
2 |
3 | import android.widget.Toast
4 | import androidx.compose.foundation.BasicTooltipBox
5 | import androidx.compose.foundation.ExperimentalFoundationApi
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.imePadding
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.rememberBasicTooltipState
10 | import androidx.compose.foundation.rememberScrollState
11 | import androidx.compose.foundation.shape.CircleShape
12 | import androidx.compose.foundation.verticalScroll
13 | import androidx.compose.material.icons.Icons
14 | import androidx.compose.material.icons.outlined.Save
15 | import androidx.compose.material3.ExperimentalMaterial3Api
16 | import androidx.compose.material3.FloatingActionButton
17 | import androidx.compose.material3.Icon
18 | import androidx.compose.material3.Scaffold
19 | import androidx.compose.material3.Text
20 | import androidx.compose.material3.TooltipDefaults
21 | import androidx.compose.material3.TopAppBar
22 | import androidx.compose.runtime.Composable
23 | import androidx.compose.ui.Modifier
24 | import androidx.compose.ui.platform.LocalContext
25 | import androidx.compose.ui.res.stringResource
26 | import androidx.navigation.NavController
27 | import com.dessalines.habitmaker.R
28 | import com.dessalines.habitmaker.db.Encouragement
29 | import com.dessalines.habitmaker.db.EncouragementInsert
30 | import com.dessalines.habitmaker.db.EncouragementViewModel
31 | import com.dessalines.habitmaker.db.Habit
32 | import com.dessalines.habitmaker.db.HabitInsert
33 | import com.dessalines.habitmaker.db.HabitReminder
34 | import com.dessalines.habitmaker.db.HabitReminderInsert
35 | import com.dessalines.habitmaker.db.HabitReminderViewModel
36 | import com.dessalines.habitmaker.db.HabitViewModel
37 | import com.dessalines.habitmaker.notifications.scheduleRemindersForHabit
38 | import com.dessalines.habitmaker.ui.components.common.BackButton
39 | import com.dessalines.habitmaker.ui.components.common.ToolTip
40 |
41 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
42 | @Composable
43 | fun CreateHabitScreen(
44 | navController: NavController,
45 | habitViewModel: HabitViewModel,
46 | encouragementViewModel: EncouragementViewModel,
47 | reminderViewModel: HabitReminderViewModel,
48 | ) {
49 | val scrollState = rememberScrollState()
50 | val tooltipPosition = TooltipDefaults.rememberPlainTooltipPositionProvider()
51 | val ctx = LocalContext.current
52 |
53 | var habit: Habit? = null
54 | var encouragements: List = listOf()
55 | var reminders: List = listOf()
56 |
57 | Scaffold(
58 | topBar = {
59 | TopAppBar(
60 | title = { Text(stringResource(R.string.create_habit)) },
61 | navigationIcon = {
62 | BackButton(
63 | onBackClick = { navController.navigateUp() },
64 | )
65 | },
66 | )
67 | },
68 | content = { padding ->
69 | Column(
70 | modifier =
71 | Modifier
72 | .padding(padding)
73 | .verticalScroll(scrollState)
74 | .imePadding(),
75 | ) {
76 | HabitForm(
77 | onChange = { habit = it },
78 | )
79 | HabitRemindersForm(
80 | initialReminders = reminders,
81 | onChange = { reminders = it },
82 | )
83 | EncouragementsForm(
84 | initialEncouragements = encouragements,
85 | onChange = { encouragements = it },
86 | )
87 | }
88 | },
89 | floatingActionButton = {
90 | BasicTooltipBox(
91 | positionProvider = tooltipPosition,
92 | state = rememberBasicTooltipState(isPersistent = false),
93 | tooltip = {
94 | ToolTip(stringResource(R.string.save))
95 | },
96 | ) {
97 | FloatingActionButton(
98 | modifier = Modifier.imePadding(),
99 | onClick = {
100 | habit?.let {
101 | if (habitFormValid(it)) {
102 | val insert =
103 | HabitInsert(
104 | name = it.name,
105 | frequency = it.frequency,
106 | timesPerFrequency = it.timesPerFrequency,
107 | notes = it.notes,
108 | context = it.context,
109 | archived = it.archived,
110 | )
111 | val insertedHabitId = habitViewModel.insert(insert)
112 |
113 | // The id is -1 if its a failed insert
114 | if (insertedHabitId != -1L) {
115 | // Insert the reminders
116 | reminders.forEach {
117 | val insert =
118 | HabitReminderInsert(
119 | habitId = insertedHabitId.toInt(),
120 | time = it.time,
121 | day = it.day,
122 | )
123 | reminderViewModel.insert(insert)
124 | }
125 | // Reschedule the reminders for that habit
126 | scheduleRemindersForHabit(
127 | ctx,
128 | reminders,
129 | it.name,
130 | insertedHabitId.toInt(),
131 | false,
132 | )
133 |
134 | // Insert the encouragements
135 | encouragements.forEach {
136 | val insert =
137 | EncouragementInsert(
138 | habitId = insertedHabitId.toInt(),
139 | content = it.content,
140 | )
141 | encouragementViewModel.insert(insert)
142 | }
143 |
144 | navController.navigate("habits?id=$insertedHabitId") {
145 | popUpTo("habits")
146 | }
147 | } else {
148 | Toast
149 | .makeText(
150 | ctx,
151 | ctx.getString(R.string.habit_already_exists),
152 | Toast.LENGTH_SHORT,
153 | ).show()
154 | }
155 | }
156 | }
157 | },
158 | shape = CircleShape,
159 | ) {
160 | Icon(
161 | imageVector = Icons.Outlined.Save,
162 | contentDescription = stringResource(R.string.save),
163 | )
164 | }
165 | }
166 | },
167 | )
168 | }
169 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/ui/components/habit/EditHabitScreen.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.ui.components.habit
2 |
3 | import androidx.compose.foundation.BasicTooltipBox
4 | import androidx.compose.foundation.ExperimentalFoundationApi
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.imePadding
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.rememberBasicTooltipState
9 | import androidx.compose.foundation.rememberScrollState
10 | import androidx.compose.foundation.shape.CircleShape
11 | import androidx.compose.foundation.verticalScroll
12 | import androidx.compose.material.icons.Icons
13 | import androidx.compose.material.icons.outlined.Save
14 | import androidx.compose.material3.ExperimentalMaterial3Api
15 | import androidx.compose.material3.FloatingActionButton
16 | import androidx.compose.material3.Icon
17 | import androidx.compose.material3.Scaffold
18 | import androidx.compose.material3.Text
19 | import androidx.compose.material3.TooltipDefaults
20 | import androidx.compose.material3.TopAppBar
21 | import androidx.compose.runtime.Composable
22 | import androidx.compose.runtime.getValue
23 | import androidx.compose.runtime.mutableStateOf
24 | import androidx.compose.runtime.remember
25 | import androidx.compose.runtime.setValue
26 | import androidx.compose.ui.Modifier
27 | import androidx.compose.ui.platform.LocalContext
28 | import androidx.compose.ui.res.stringResource
29 | import androidx.navigation.NavController
30 | import com.dessalines.habitmaker.R
31 | import com.dessalines.habitmaker.db.EncouragementInsert
32 | import com.dessalines.habitmaker.db.EncouragementViewModel
33 | import com.dessalines.habitmaker.db.HabitReminderInsert
34 | import com.dessalines.habitmaker.db.HabitReminderViewModel
35 | import com.dessalines.habitmaker.db.HabitUpdate
36 | import com.dessalines.habitmaker.db.HabitViewModel
37 | import com.dessalines.habitmaker.notifications.scheduleRemindersForHabit
38 | import com.dessalines.habitmaker.ui.components.common.BackButton
39 | import com.dessalines.habitmaker.ui.components.common.ToolTip
40 | import com.dessalines.habitmaker.utils.isCompletedToday
41 |
42 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
43 | @Composable
44 | fun EditHabitScreen(
45 | navController: NavController,
46 | habitViewModel: HabitViewModel,
47 | encouragementViewModel: EncouragementViewModel,
48 | reminderViewModel: HabitReminderViewModel,
49 | id: Int,
50 | ) {
51 | val ctx = LocalContext.current
52 | val scrollState = rememberScrollState()
53 | val tooltipPosition = TooltipDefaults.rememberPlainTooltipPositionProvider()
54 |
55 | val habit = habitViewModel.getByIdSync(id)
56 | val encouragements = encouragementViewModel.listForHabitSync(id)
57 | val reminders = reminderViewModel.listForHabitSync(id)
58 |
59 | // Copy the habit and encouragements from the DB first
60 | var editedHabit by remember {
61 | mutableStateOf(habit)
62 | }
63 |
64 | var editedEncouragements by remember {
65 | mutableStateOf(encouragements)
66 | }
67 |
68 | var editedReminders by remember {
69 | mutableStateOf(reminders)
70 | }
71 |
72 | Scaffold(
73 | topBar = {
74 | TopAppBar(
75 | title = { Text(stringResource(R.string.edit_habit)) },
76 | navigationIcon = {
77 | BackButton(
78 | onBackClick = { navController.navigateUp() },
79 | )
80 | },
81 | )
82 | },
83 | content = { padding ->
84 | Column(
85 | modifier =
86 | Modifier
87 | .padding(padding)
88 | .verticalScroll(scrollState)
89 | .imePadding(),
90 | ) {
91 | HabitForm(
92 | habit = editedHabit,
93 | onChange = { editedHabit = it },
94 | )
95 | HabitRemindersForm(
96 | initialReminders = editedReminders,
97 | onChange = { editedReminders = it },
98 | )
99 | EncouragementsForm(
100 | initialEncouragements = editedEncouragements,
101 | onChange = { editedEncouragements = it },
102 | )
103 | }
104 | },
105 | floatingActionButton = {
106 | BasicTooltipBox(
107 | positionProvider = tooltipPosition,
108 | state = rememberBasicTooltipState(isPersistent = false),
109 | tooltip = {
110 | ToolTip(stringResource(R.string.save))
111 | },
112 | ) {
113 | FloatingActionButton(
114 | modifier = Modifier.imePadding(),
115 | onClick = {
116 | editedHabit?.let { editedHabit ->
117 | if (habitFormValid(editedHabit)) {
118 | val update =
119 | HabitUpdate(
120 | id = editedHabit.id,
121 | name = editedHabit.name,
122 | frequency = editedHabit.frequency,
123 | timesPerFrequency = editedHabit.timesPerFrequency,
124 | notes = editedHabit.notes,
125 | context = editedHabit.context,
126 | archived = editedHabit.archived,
127 | )
128 | habitViewModel.update(update)
129 |
130 | // Delete then add all the reminders
131 | reminderViewModel.delete(editedHabit.id)
132 | editedReminders.forEach {
133 | val insert =
134 | HabitReminderInsert(
135 | habitId = editedHabit.id,
136 | time = it.time,
137 | day = it.day,
138 | )
139 | reminderViewModel.insert(insert)
140 | }
141 |
142 | // Reschedule the reminders for that habit
143 | val isCompleted = isCompletedToday(editedHabit.lastCompletedTime)
144 | scheduleRemindersForHabit(
145 | ctx,
146 | editedReminders,
147 | editedHabit.name,
148 | editedHabit.id,
149 | isCompleted,
150 | )
151 |
152 | // Delete then add all the encouragements
153 | encouragementViewModel.deleteForHabit(editedHabit.id)
154 | editedEncouragements.forEach {
155 | val insert =
156 | EncouragementInsert(
157 | habitId = editedHabit.id,
158 | content = it.content,
159 | )
160 | encouragementViewModel.insert(insert)
161 | }
162 | navController.navigateUp()
163 | }
164 | }
165 | },
166 | shape = CircleShape,
167 | ) {
168 | Icon(
169 | imageVector = Icons.Outlined.Save,
170 | contentDescription = stringResource(R.string.save),
171 | )
172 | }
173 | }
174 | },
175 | )
176 | }
177 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/ui/components/habit/EncouragementForm.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.ui.components.habit
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material.icons.Icons
8 | import androidx.compose.material.icons.outlined.Close
9 | import androidx.compose.material3.Icon
10 | import androidx.compose.material3.IconButton
11 | import androidx.compose.material3.OutlinedTextField
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.getValue
15 | import androidx.compose.runtime.mutableStateOf
16 | import androidx.compose.runtime.saveable.rememberSaveable
17 | import androidx.compose.runtime.setValue
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.res.stringResource
20 | import androidx.compose.ui.tooling.preview.Preview
21 | import com.dessalines.habitmaker.R
22 | import com.dessalines.habitmaker.db.Encouragement
23 | import com.dessalines.habitmaker.ui.components.common.SMALL_PADDING
24 |
25 | @Composable
26 | fun EncouragementForm(
27 | encouragement: Encouragement? = null,
28 | onChange: (Encouragement) -> Unit,
29 | onDelete: () -> Unit,
30 | ) {
31 | var content by rememberSaveable {
32 | mutableStateOf(encouragement?.content.orEmpty())
33 | }
34 |
35 | fun encouragementChange() =
36 | onChange(
37 | Encouragement(
38 | id = encouragement?.id ?: 0,
39 | habitId = encouragement?.habitId ?: 0,
40 | content = content,
41 | ),
42 | )
43 |
44 | Column(
45 | modifier = Modifier.padding(horizontal = SMALL_PADDING),
46 | verticalArrangement = Arrangement.spacedBy(SMALL_PADDING),
47 | ) {
48 | OutlinedTextField(
49 | label = { Text(stringResource(R.string.encouragement)) },
50 | trailingIcon = {
51 | IconButton(
52 | content = {
53 | Icon(
54 | imageVector = Icons.Outlined.Close,
55 | contentDescription = null,
56 | )
57 | },
58 | onClick = onDelete,
59 | )
60 | },
61 | modifier = Modifier.fillMaxWidth(),
62 | value = content,
63 | onValueChange = {
64 | content = it
65 | encouragementChange()
66 | },
67 | )
68 | }
69 | }
70 |
71 | @Composable
72 | @Preview
73 | fun EncouragementFormPreview() {
74 | EncouragementForm(
75 | onChange = {},
76 | onDelete = {},
77 | )
78 | }
79 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/ui/components/habit/EncouragementsForm.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.ui.components.habit
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material3.OutlinedButton
6 | import androidx.compose.material3.Text
7 | import androidx.compose.material3.TextButton
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.getValue
10 | import androidx.compose.runtime.mutableStateOf
11 | import androidx.compose.runtime.saveable.rememberSaveable
12 | import androidx.compose.runtime.setValue
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.platform.LocalContext
15 | import androidx.compose.ui.res.stringResource
16 | import androidx.compose.ui.tooling.preview.Preview
17 | import com.dessalines.habitmaker.R
18 | import com.dessalines.habitmaker.db.Encouragement
19 | import com.dessalines.habitmaker.db.sampleEncouragements
20 | import com.dessalines.habitmaker.ui.components.common.SMALL_PADDING
21 | import com.dessalines.habitmaker.utils.USER_GUIDE_URL_ENCOURAGEMENTS
22 | import com.dessalines.habitmaker.utils.openLink
23 | import okhttp3.internal.toImmutableList
24 |
25 | @Composable
26 | fun EncouragementsForm(
27 | initialEncouragements: List,
28 | onChange: (List) -> Unit,
29 | ) {
30 | val ctx = LocalContext.current
31 |
32 | var encouragements by rememberSaveable {
33 | mutableStateOf(initialEncouragements)
34 | }
35 |
36 | Column {
37 | encouragements.forEachIndexed { index, encouragement ->
38 | EncouragementForm(
39 | encouragement = encouragement,
40 | onChange = {
41 | val tmp = encouragements.toMutableList()
42 | tmp[index] = it
43 | encouragements = tmp.toImmutableList()
44 | onChange(encouragements)
45 | },
46 | onDelete = {
47 | val tmp = encouragements.toMutableList()
48 | tmp.removeAt(index)
49 | encouragements = tmp.toImmutableList()
50 | onChange(encouragements)
51 | },
52 | )
53 | }
54 | if (encouragements.isEmpty()) {
55 | TextButton(
56 | onClick = { openLink(USER_GUIDE_URL_ENCOURAGEMENTS, ctx) },
57 | ) {
58 | Text(stringResource(R.string.what_are_encouragements))
59 | }
60 | }
61 | OutlinedButton(
62 | modifier = Modifier.padding(horizontal = SMALL_PADDING),
63 | onClick = {
64 | val tmp = encouragements.toMutableList()
65 | tmp.add(
66 | Encouragement(
67 | id = 0,
68 | habitId = 0,
69 | content = "",
70 | ),
71 | )
72 | encouragements = tmp.toImmutableList()
73 | onChange(encouragements)
74 | },
75 | ) {
76 | Text(stringResource(R.string.add_encouragement))
77 | }
78 | }
79 | }
80 |
81 | @Composable
82 | @Preview
83 | fun EncouragementsFormPreview() {
84 | EncouragementsForm(
85 | initialEncouragements = sampleEncouragements,
86 | onChange = {},
87 | )
88 | }
89 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/ui/components/habit/habitanddetails/calendars/HabitCalendar.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.ui.components.habit.habitanddetails.calendars
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.aspectRatio
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.material.icons.Icons
11 | import androidx.compose.material.icons.filled.Check
12 | import androidx.compose.material3.Icon
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.Text
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.remember
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.text.style.TextAlign
20 | import androidx.compose.ui.text.style.TextDecoration
21 | import androidx.compose.ui.tooling.preview.Preview
22 | import com.dessalines.habitmaker.db.HabitCheck
23 | import com.dessalines.habitmaker.db.sampleHabitChecks
24 | import com.dessalines.habitmaker.ui.components.common.MEDIUM_PADDING
25 | import com.dessalines.habitmaker.utils.epochMillisToLocalDate
26 | import com.kizitonwose.calendar.compose.HorizontalCalendar
27 | import com.kizitonwose.calendar.compose.rememberCalendarState
28 | import com.kizitonwose.calendar.core.CalendarDay
29 | import com.kizitonwose.calendar.core.CalendarMonth
30 | import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale
31 | import java.time.LocalDate
32 | import java.time.YearMonth
33 | import java.time.format.TextStyle
34 | import java.util.Locale
35 |
36 | @Composable
37 | fun HabitCalendar(
38 | habitChecks: List,
39 | onClickDay: (LocalDate) -> Unit,
40 | modifier: Modifier = Modifier,
41 | ) {
42 | val checkDates = habitChecks.map { it.checkTime.epochMillisToLocalDate() }
43 |
44 | val currentMonth = remember { YearMonth.now() }
45 | val startMonth = remember { currentMonth.minusMonths(100) }
46 | val endMonth = remember { currentMonth }
47 | val firstDayOfWeek = remember { firstDayOfWeekFromLocale() }
48 |
49 | val state =
50 | rememberCalendarState(
51 | startMonth = startMonth,
52 | endMonth = endMonth,
53 | firstVisibleMonth = currentMonth,
54 | firstDayOfWeek = firstDayOfWeek,
55 | )
56 |
57 | HorizontalCalendar(
58 | modifier = modifier,
59 | state = state,
60 | monthHeader = { month ->
61 | MonthHeader(month)
62 | },
63 | dayContent = {
64 | Day(
65 | day = it,
66 | // TODO probably a more efficient way to do this
67 | // Maybe a hashmap of dates?
68 | checked = checkDates.contains(it.date),
69 | onClick = { onClickDay(it.date) },
70 | modifier = modifier,
71 | )
72 | },
73 | )
74 | }
75 |
76 | @Composable
77 | fun MonthHeader(
78 | calendarMonth: CalendarMonth,
79 | modifier: Modifier = Modifier,
80 | ) {
81 | val daysOfWeek = calendarMonth.weekDays.first().map { it.date.dayOfWeek }
82 |
83 | Column(
84 | modifier = modifier,
85 | verticalArrangement = Arrangement.spacedBy(MEDIUM_PADDING),
86 | ) {
87 | val locale = Locale.getDefault()
88 | Text(
89 | text = calendarMonth.yearMonth.month.getDisplayName(TextStyle.SHORT, locale),
90 | textAlign = TextAlign.Center,
91 | modifier = Modifier.fillMaxWidth(),
92 | style = MaterialTheme.typography.titleLarge,
93 | textDecoration = TextDecoration.Underline,
94 | )
95 | Row(modifier = Modifier.fillMaxWidth()) {
96 | for (dayOfWeek in daysOfWeek) {
97 | Text(
98 | text = dayOfWeek.getDisplayName(TextStyle.SHORT, locale),
99 | textAlign = TextAlign.Center,
100 | modifier = Modifier.weight(1f),
101 | style = MaterialTheme.typography.labelSmall,
102 | )
103 | }
104 | }
105 | }
106 | }
107 |
108 | @Composable
109 | fun Day(
110 | day: CalendarDay,
111 | checked: Boolean,
112 | onClick: (CalendarDay) -> Unit,
113 | modifier: Modifier = Modifier,
114 | ) {
115 | // Only allow clicking dates in the past
116 | val allowedDate = day.date.isBefore(LocalDate.now().plusDays(1))
117 | val isToday = day.date == LocalDate.now()
118 |
119 | Box(
120 | modifier =
121 | modifier
122 | .aspectRatio(1f)
123 | .clickable(
124 | enabled = allowedDate,
125 | onClick = { onClick(day) },
126 | ),
127 | contentAlignment = Alignment.Center,
128 | ) {
129 | if (checked) {
130 | Icon(
131 | imageVector = Icons.Default.Check,
132 | tint = MaterialTheme.colorScheme.primary,
133 | contentDescription = null,
134 | )
135 | } else {
136 | Text(
137 | text = day.date.dayOfMonth.toString(),
138 | style = MaterialTheme.typography.bodyMedium,
139 | // Underline today's date
140 | textDecoration =
141 | if (isToday) {
142 | TextDecoration.Underline
143 | } else {
144 | TextDecoration.None
145 | },
146 | color =
147 | if (allowedDate) {
148 | MaterialTheme.colorScheme.onSurface
149 | } else {
150 | MaterialTheme.colorScheme.outline
151 | },
152 | )
153 | }
154 | }
155 | }
156 |
157 | @Preview
158 | @Composable
159 | fun HabitCalendarPreview() {
160 | HabitCalendar(
161 | habitChecks = sampleHabitChecks,
162 | onClickDay = {},
163 | )
164 | }
165 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/ui/components/settings/BackupAndRestoreScreen.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.ui.components.settings
2 |
3 | import android.widget.Toast
4 | import androidx.activity.compose.rememberLauncherForActivityResult
5 | import androidx.activity.result.contract.ActivityResultContracts
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.imePadding
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.rememberScrollState
10 | import androidx.compose.foundation.verticalScroll
11 | import androidx.compose.material.icons.Icons
12 | import androidx.compose.material.icons.outlined.Restore
13 | import androidx.compose.material.icons.outlined.Save
14 | import androidx.compose.material3.ExperimentalMaterial3Api
15 | import androidx.compose.material3.Icon
16 | import androidx.compose.material3.Scaffold
17 | import androidx.compose.material3.Text
18 | import androidx.compose.material3.TopAppBar
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.platform.LocalContext
22 | import androidx.compose.ui.res.stringResource
23 | import androidx.navigation.NavController
24 | import com.dessalines.habitmaker.R
25 | import com.dessalines.habitmaker.db.AppDB
26 | import com.dessalines.habitmaker.ui.components.common.BackButton
27 | import com.roomdbexportimport.RoomDBExportImport
28 | import me.zhanghai.compose.preference.Preference
29 | import me.zhanghai.compose.preference.ProvidePreferenceTheme
30 |
31 | @OptIn(ExperimentalMaterial3Api::class)
32 | @Composable
33 | fun BackupAndRestoreScreen(navController: NavController) {
34 | val ctx = LocalContext.current
35 |
36 | val dbSavedText = stringResource(R.string.database_backed_up)
37 | val dbRestoredText = stringResource(R.string.database_restored)
38 |
39 | val dbHelper = RoomDBExportImport(AppDB.getDatabase(ctx).openHelper)
40 |
41 | val exportDbLauncher =
42 | rememberLauncherForActivityResult(
43 | ActivityResultContracts.CreateDocument("application/zip"),
44 | ) {
45 | it?.also {
46 | dbHelper.export(ctx, it)
47 | Toast.makeText(ctx, dbSavedText, Toast.LENGTH_SHORT).show()
48 | }
49 | }
50 |
51 | val importDbLauncher =
52 | rememberLauncherForActivityResult(
53 | ActivityResultContracts.OpenDocument(),
54 | ) {
55 | it?.also {
56 | dbHelper.import(ctx, it, true)
57 | Toast.makeText(ctx, dbRestoredText, Toast.LENGTH_SHORT).show()
58 | }
59 | }
60 |
61 | val scrollState = rememberScrollState()
62 |
63 | Scaffold(
64 | topBar = {
65 | TopAppBar(
66 | title = { Text(stringResource(R.string.backup_and_restore)) },
67 | navigationIcon = {
68 | BackButton(
69 | onBackClick = { navController.navigateUp() },
70 | )
71 | },
72 | )
73 | },
74 | content = { padding ->
75 | Column(
76 | modifier =
77 | Modifier
78 | .padding(padding)
79 | .verticalScroll(scrollState)
80 | .imePadding(),
81 | ) {
82 | ProvidePreferenceTheme {
83 | Preference(
84 | title = { Text(stringResource(R.string.backup_database)) },
85 | icon = {
86 | Icon(
87 | imageVector = Icons.Outlined.Save,
88 | contentDescription = null,
89 | )
90 | },
91 | onClick = {
92 | exportDbLauncher.launch("habit-maker")
93 | },
94 | )
95 | Preference(
96 | title = { Text(stringResource(R.string.restore_database)) },
97 | summary = {
98 | Text(stringResource(R.string.restore_database_warning))
99 | },
100 | icon = {
101 | Icon(
102 | imageVector = Icons.Outlined.Restore,
103 | contentDescription = null,
104 | )
105 | },
106 | onClick = {
107 | importDbLauncher.launch(arrayOf("application/zip"))
108 | },
109 | )
110 | }
111 | }
112 | },
113 | )
114 | }
115 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/ui/components/settings/LookAndFeelScreen.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.ui.components.settings
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.imePadding
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.rememberScrollState
7 | import androidx.compose.foundation.verticalScroll
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.outlined.Colorize
10 | import androidx.compose.material.icons.outlined.Palette
11 | import androidx.compose.material3.ExperimentalMaterial3Api
12 | import androidx.compose.material3.Icon
13 | import androidx.compose.material3.Scaffold
14 | import androidx.compose.material3.Text
15 | import androidx.compose.material3.TopAppBar
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.runtime.getValue
18 | import androidx.compose.runtime.livedata.observeAsState
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.platform.LocalContext
21 | import androidx.compose.ui.res.stringResource
22 | import androidx.compose.ui.text.AnnotatedString
23 | import androidx.lifecycle.asLiveData
24 | import androidx.navigation.NavController
25 | import com.dessalines.habitmaker.R
26 | import com.dessalines.habitmaker.db.AppSettingsViewModel
27 | import com.dessalines.habitmaker.db.SettingsUpdateTheme
28 | import com.dessalines.habitmaker.ui.components.common.BackButton
29 | import com.dessalines.habitmaker.utils.ThemeColor
30 | import com.dessalines.habitmaker.utils.ThemeMode
31 | import me.zhanghai.compose.preference.ListPreference
32 | import me.zhanghai.compose.preference.ListPreferenceType
33 | import me.zhanghai.compose.preference.ProvidePreferenceTheme
34 |
35 | @OptIn(ExperimentalMaterial3Api::class)
36 | @Composable
37 | fun LookAndFeelScreen(
38 | navController: NavController,
39 | appSettingsViewModel: AppSettingsViewModel,
40 | ) {
41 | val settings by appSettingsViewModel.appSettings.asLiveData().observeAsState()
42 | val ctx = LocalContext.current
43 |
44 | var themeState = ThemeMode.entries[settings?.theme ?: 0]
45 | var themeColorState = ThemeColor.entries[settings?.themeColor ?: 0]
46 |
47 | fun updateSettings() =
48 | appSettingsViewModel.updateTheme(
49 | SettingsUpdateTheme(
50 | id = 1,
51 | theme = themeState.ordinal,
52 | themeColor = themeColorState.ordinal,
53 | ),
54 | )
55 |
56 | val scrollState = rememberScrollState()
57 |
58 | Scaffold(
59 | topBar = {
60 | TopAppBar(
61 | title = { Text(stringResource(R.string.look_and_feel)) },
62 | navigationIcon = {
63 | BackButton(
64 | onBackClick = { navController.navigateUp() },
65 | )
66 | },
67 | )
68 | },
69 | content = { padding ->
70 | Column(
71 | modifier =
72 | Modifier
73 | .padding(padding)
74 | .verticalScroll(scrollState)
75 | .imePadding(),
76 | ) {
77 | ProvidePreferenceTheme {
78 | ListPreference(
79 | type = ListPreferenceType.DROPDOWN_MENU,
80 | value = themeState,
81 | onValueChange = {
82 | themeState = it
83 | updateSettings()
84 | },
85 | values = ThemeMode.entries,
86 | valueToText = {
87 | AnnotatedString(ctx.getString(it.resId))
88 | },
89 | title = {
90 | Text(stringResource(R.string.theme))
91 | },
92 | summary = {
93 | Text(stringResource(themeState.resId))
94 | },
95 | icon = {
96 | Icon(
97 | imageVector = Icons.Outlined.Palette,
98 | contentDescription = null,
99 | )
100 | },
101 | )
102 |
103 | ListPreference(
104 | type = ListPreferenceType.DROPDOWN_MENU,
105 | value = themeColorState,
106 | onValueChange = {
107 | themeColorState = it
108 | updateSettings()
109 | },
110 | values = ThemeColor.entries,
111 | valueToText = {
112 | AnnotatedString(ctx.getString(it.resId))
113 | },
114 | title = {
115 | Text(stringResource(R.string.theme_color))
116 | },
117 | summary = {
118 | Text(stringResource(themeColorState.resId))
119 | },
120 | icon = {
121 | Icon(
122 | imageVector = Icons.Outlined.Colorize,
123 | contentDescription = null,
124 | )
125 | },
126 | )
127 | }
128 | }
129 | },
130 | )
131 | }
132 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/ui/components/settings/SettingsScreen.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.ui.components.settings
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.imePadding
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.rememberScrollState
7 | import androidx.compose.foundation.verticalScroll
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.automirrored.outlined.HelpCenter
10 | import androidx.compose.material.icons.outlined.Info
11 | import androidx.compose.material.icons.outlined.Palette
12 | import androidx.compose.material.icons.outlined.Restore
13 | import androidx.compose.material.icons.outlined.TouchApp
14 | import androidx.compose.material3.ExperimentalMaterial3Api
15 | import androidx.compose.material3.Icon
16 | import androidx.compose.material3.Scaffold
17 | import androidx.compose.material3.Text
18 | import androidx.compose.material3.TopAppBar
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.platform.LocalContext
22 | import androidx.compose.ui.res.stringResource
23 | import androidx.navigation.NavController
24 | import com.dessalines.habitmaker.R
25 | import com.dessalines.habitmaker.ui.components.common.BackButton
26 | import com.dessalines.habitmaker.utils.USER_GUIDE_URL
27 | import com.dessalines.habitmaker.utils.openLink
28 | import me.zhanghai.compose.preference.Preference
29 | import me.zhanghai.compose.preference.ProvidePreferenceTheme
30 |
31 | @OptIn(ExperimentalMaterial3Api::class)
32 | @Composable
33 | fun SettingsScreen(navController: NavController) {
34 | val ctx = LocalContext.current
35 | val scrollState = rememberScrollState()
36 |
37 | Scaffold(
38 | topBar = {
39 | TopAppBar(
40 | title = { Text(stringResource(R.string.settings)) },
41 | navigationIcon = {
42 | BackButton(
43 | onBackClick = { navController.navigateUp() },
44 | )
45 | },
46 | )
47 | },
48 | content = { padding ->
49 | Column(
50 | modifier =
51 | Modifier
52 | .padding(padding)
53 | .verticalScroll(scrollState)
54 | .imePadding(),
55 | ) {
56 | ProvidePreferenceTheme {
57 | Preference(
58 | title = { Text(stringResource(R.string.look_and_feel)) },
59 | icon = {
60 | Icon(
61 | imageVector = Icons.Outlined.Palette,
62 | contentDescription = null,
63 | )
64 | },
65 | onClick = { navController.navigate("lookAndFeel") },
66 | )
67 | Preference(
68 | title = { Text(stringResource(R.string.behavior)) },
69 | icon = {
70 | Icon(
71 | imageVector = Icons.Outlined.TouchApp,
72 | contentDescription = null,
73 | )
74 | },
75 | onClick = { navController.navigate("behavior") },
76 | )
77 | Preference(
78 | title = { Text(stringResource(R.string.backup_and_restore)) },
79 | icon = {
80 | Icon(
81 | imageVector = Icons.Outlined.Restore,
82 | contentDescription = null,
83 | )
84 | },
85 | onClick = { navController.navigate("backupAndRestore") },
86 | )
87 | Preference(
88 | title = { Text(stringResource(R.string.user_guide)) },
89 | icon = {
90 | Icon(
91 | imageVector = Icons.AutoMirrored.Outlined.HelpCenter,
92 | contentDescription = null,
93 | )
94 | },
95 | onClick = {
96 | openLink(USER_GUIDE_URL, ctx)
97 | },
98 | )
99 | Preference(
100 | title = { Text(stringResource(R.string.about)) },
101 | icon = {
102 | Icon(
103 | imageVector = Icons.Outlined.Info,
104 | contentDescription = null,
105 | )
106 | },
107 | onClick = { navController.navigate("about") },
108 | )
109 | }
110 | }
111 | },
112 | )
113 | }
114 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material3.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val Shapes =
8 | Shapes(
9 | small = RoundedCornerShape(4.dp),
10 | medium = RoundedCornerShape(4.dp),
11 | large = RoundedCornerShape(0.dp),
12 | )
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.ui.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.dynamicDarkColorScheme
7 | import androidx.compose.material3.dynamicLightColorScheme
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.platform.LocalContext
10 | import com.dessalines.habitmaker.db.AppSettings
11 | import com.dessalines.habitmaker.utils.ThemeColor
12 | import com.dessalines.habitmaker.utils.ThemeMode
13 |
14 | @Composable
15 | fun HabitMakerTheme(
16 | settings: AppSettings?,
17 | content: @Composable () -> Unit,
18 | ) {
19 | val themeMode = ThemeMode.entries[settings?.theme ?: 0]
20 | val themeColor = ThemeColor.entries[settings?.themeColor ?: 0]
21 |
22 | val ctx = LocalContext.current
23 | val android12OrLater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
24 |
25 | // Dynamic schemes crash on lower than android 12
26 | val dynamicPair =
27 | if (android12OrLater) {
28 | Pair(dynamicLightColorScheme(ctx), dynamicDarkColorScheme(ctx))
29 | } else {
30 | pink()
31 | }
32 |
33 | val colorPair =
34 | when (themeColor) {
35 | ThemeColor.Dynamic -> dynamicPair
36 | ThemeColor.Green -> green()
37 | ThemeColor.Pink -> pink()
38 | }
39 |
40 | val systemTheme =
41 | if (!isSystemInDarkTheme()) {
42 | colorPair.first
43 | } else {
44 | colorPair.second
45 | }
46 |
47 | val colors =
48 | when (themeMode) {
49 | ThemeMode.System -> systemTheme
50 | ThemeMode.Light -> colorPair.first
51 | ThemeMode.Dark -> colorPair.second
52 | }
53 |
54 | MaterialTheme(
55 | colorScheme = colors,
56 | typography = Typography,
57 | shapes = Shapes,
58 | content = content,
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 |
5 | // Set of Material typography styles to start with
6 | val Typography = Typography()
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/utils/DateUtils.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.utils
2 |
3 | import androidx.compose.material3.ExperimentalMaterial3Api
4 | import androidx.compose.material3.TimePickerState
5 | import java.time.Instant
6 | import java.time.LocalDate
7 | import java.time.LocalTime
8 | import java.time.ZoneId
9 |
10 | fun Long.epochMillisToLocalDate(): LocalDate = Instant.ofEpochMilli(this).atZone(ZoneId.systemDefault()).toLocalDate()
11 |
12 | fun LocalDate.toEpochMillis() = this.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli()
13 |
14 | @OptIn(ExperimentalMaterial3Api::class)
15 | fun TimePickerState.toLocalTime(): LocalTime = LocalTime.of(this.hour, this.minute)
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/utils/ScoreUtils.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.utils
2 |
3 | import android.util.Log
4 | import com.dessalines.habitmaker.db.HabitCheck
5 | import okhttp3.internal.toImmutableList
6 | import java.time.DayOfWeek
7 | import java.time.Duration
8 | import java.time.LocalDate
9 | import java.time.temporal.TemporalAdjusters
10 |
11 | data class Streak(
12 | val begin: LocalDate,
13 | val end: LocalDate,
14 | )
15 |
16 | /**
17 | * Gives the length of a streak.
18 | */
19 | fun Streak.duration(frequency: HabitFrequency): Long =
20 | Duration
21 | .between(
22 | this.begin.atStartOfDay(),
23 | this.end.atStartOfDay(),
24 | ).toDays()
25 | .plus(1)
26 | .div(frequency.toDays())
27 |
28 | /**
29 | * Gives the length of the current streak.
30 | */
31 | fun todayStreak(
32 | frequency: HabitFrequency,
33 | lastStreak: Streak?,
34 | ): Long {
35 | val todayStreak =
36 | lastStreak?.let {
37 | if (it.end >= LocalDate.now()) {
38 | it.duration(frequency)
39 | } else {
40 | 0
41 | }
42 | } ?: 0
43 | return todayStreak
44 | }
45 |
46 | fun calculateStreaks(
47 | frequency: HabitFrequency,
48 | timesPerFrequency: Int,
49 | dates: List,
50 | ): List {
51 | val virtualDates = buildVirtualDates(frequency, timesPerFrequency, dates).sortedDescending()
52 |
53 | if (virtualDates.isEmpty()) {
54 | return emptyList()
55 | }
56 |
57 | var begin = virtualDates[0]
58 | var end = virtualDates[0]
59 |
60 | val streaks = mutableListOf()
61 | for (i in 1 until virtualDates.size) {
62 | val current = virtualDates[i]
63 | if (current == begin.minusDays(1)) {
64 | begin = current
65 | } else {
66 | streaks.add(Streak(begin, end))
67 | begin = current
68 | end = current
69 | }
70 | }
71 | streaks.add(Streak(begin, end))
72 | streaks.reverse()
73 | Log.d(TAG, streaks.joinToString { "${it.begin} - ${it.end}" })
74 |
75 | return streaks.toImmutableList()
76 | }
77 |
78 | /**
79 | * For habits with weeks / months / years and times per frequency,
80 | * you need to create "virtual" dates.
81 | */
82 | fun buildVirtualDates(
83 | frequency: HabitFrequency,
84 | timesPerFrequency: Int,
85 | dates: List,
86 | ): List =
87 | when (frequency) {
88 | HabitFrequency.Daily -> dates
89 | else -> {
90 | val virtualDates = mutableListOf()
91 | val completedRanges = mutableListOf()
92 |
93 | var rangeFirstDate =
94 | when (frequency) {
95 | HabitFrequency.Weekly ->
96 | dates.firstOrNull()?.with(
97 | TemporalAdjusters.previousOrSame(
98 | DayOfWeek.SUNDAY,
99 | ),
100 | )
101 | HabitFrequency.Monthly -> dates.firstOrNull()?.withDayOfMonth(1)
102 | HabitFrequency.Yearly -> dates.firstOrNull()?.withDayOfYear(1)
103 | else -> null
104 | }
105 |
106 | var count = 0
107 |
108 | dates.forEach { entry ->
109 | virtualDates.add(entry)
110 | val entryRange =
111 | when (frequency) {
112 | HabitFrequency.Weekly ->
113 | entry.with(
114 | TemporalAdjusters.previousOrSame(
115 | DayOfWeek.SUNDAY,
116 | ),
117 | )
118 | HabitFrequency.Monthly -> entry.withDayOfMonth(1)
119 | HabitFrequency.Yearly -> entry.withDayOfYear(1)
120 | else -> entry
121 | }
122 | if (entryRange == rangeFirstDate && !completedRanges.contains(entryRange)) {
123 | count++
124 | } else {
125 | rangeFirstDate = entryRange
126 | count = 1
127 | }
128 | if (count >= timesPerFrequency) completedRanges.add(entryRange)
129 | }
130 |
131 | // Months have a special case where it should use the max days possible in a month,
132 | // not 28.
133 | val maxDays =
134 | when (frequency) {
135 | HabitFrequency.Monthly -> 31
136 | else -> frequency.toDays()
137 | }.minus(1)
138 |
139 | completedRanges.forEach { start ->
140 | (0..maxDays).forEach { offset ->
141 | val date = start.plusDays(offset.toLong())
142 | if (!virtualDates.any { it == date }) {
143 | virtualDates.add(date)
144 | }
145 | }
146 | }
147 | virtualDates.toImmutableList()
148 | }
149 | }
150 |
151 | /**
152 | * Get a bonus points for each day that the streak is long.
153 | *
154 | * Called nth triangle number:
155 | * https://math.stackexchange.com/a/593320
156 | */
157 | fun calculatePoints(
158 | frequency: HabitFrequency,
159 | streaks: List,
160 | ): Long {
161 | var points = 0L
162 |
163 | streaks.forEach {
164 | val duration = it.duration(frequency)
165 | points += duration.nthTriangle()
166 | }
167 | return points
168 | }
169 |
170 | fun Long.nthTriangle() = (this * this + this) / 2
171 |
172 | /**
173 | * The percent complete score.
174 | *
175 | * Calculated using the # of times you've done it.
176 | */
177 | fun calculateScore(
178 | habitChecks: List,
179 | completedCount: Int,
180 | ): Int = (100 * habitChecks.size).div(completedCount)
181 |
182 | /**
183 | * Determines whether a habit is completed or not. Virtual means that entries
184 | * may be fake, from the streak calculations, to account for non-daily habits.
185 | *
186 | * A weekly habit might be satisfied for this week, so although it wasn't checked today,
187 | * it might complete for the week.
188 | *
189 | * Used for filtering out virtually completed habits.
190 | */
191 | fun isVirtualCompleted(lastStreakTime: Long) = lastStreakTime >= LocalDate.now().toEpochMillis()
192 |
193 | /**
194 | * Determines whether a habit is completed today or not.
195 | */
196 | fun isCompletedToday(lastCompletedTime: Long) = lastCompletedTime == LocalDate.now().toEpochMillis()
197 |
198 | /**
199 | * Determines whether a habit is completed yesterday.
200 | */
201 | fun isCompletedYesterday(lastCompletedTime: Long) = lastCompletedTime == LocalDate.now().minusDays(1).toEpochMillis()
202 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/utils/Types.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.utils
2 |
3 | import androidx.annotation.StringRes
4 | import com.dessalines.habitmaker.R
5 |
6 | enum class ThemeMode(
7 | @StringRes val resId: Int,
8 | ) {
9 | System(R.string.system),
10 | Light(R.string.light),
11 | Dark(R.string.dark),
12 | }
13 |
14 | enum class ThemeColor(
15 | @StringRes val resId: Int,
16 | ) {
17 | Dynamic(R.string.dynamic),
18 | Green(R.string.green),
19 | Pink(R.string.pink),
20 | }
21 |
22 | enum class HabitFrequency(
23 | @StringRes val resId: Int,
24 | ) {
25 | Daily(R.string.daily),
26 | Weekly(R.string.weekly),
27 | Monthly(R.string.monthly),
28 | Yearly(R.string.yearly),
29 | }
30 |
31 | fun HabitFrequency.toDays() =
32 | when (this) {
33 | HabitFrequency.Daily -> 1
34 | HabitFrequency.Weekly -> 7
35 | HabitFrequency.Monthly -> 28
36 | HabitFrequency.Yearly -> 365
37 | }
38 |
39 | enum class HabitSort(
40 | @StringRes val resId: Int,
41 | ) {
42 | Streak(R.string.streak),
43 | Points(R.string.points),
44 | Score(R.string.score),
45 |
46 | /**
47 | * Whether its completed or not.
48 | */
49 | Status(R.string.status),
50 | DateCreated(R.string.date_created),
51 | Name(R.string.name),
52 | }
53 |
54 | enum class HabitSortOrder(
55 | @StringRes val resId: Int,
56 | ) {
57 | Descending(R.string.descending),
58 | Ascending(R.string.ascending),
59 | }
60 |
61 | /**
62 | * A habit status used for coloring the streak chips
63 | */
64 | enum class HabitStatus {
65 | Normal,
66 | Silver,
67 | Gold,
68 | Platinum,
69 | }
70 |
71 | /**
72 | * A frequency picker for reminders
73 | */
74 | enum class HabitReminderFrequency(
75 | @StringRes val resId: Int,
76 | ) {
77 | NoReminders(R.string.no_reminders),
78 | EveryDay(R.string.remind_every_day),
79 | SpecificDays(R.string.remind_specific_days),
80 | }
81 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/habitmaker/utils/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.habitmaker.utils
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.content.pm.PackageInfo
6 | import android.content.pm.PackageManager
7 | import android.os.Build
8 | import androidx.core.net.toUri
9 |
10 | const val TAG = "com.habitmaker"
11 |
12 | const val GITHUB_URL = "https://github.com/dessalines/habit-maker"
13 | const val USER_GUIDE_URL = GITHUB_URL
14 | const val USER_GUIDE_URL_ENCOURAGEMENTS = "$GITHUB_URL/#encouragements"
15 | const val MATRIX_CHAT_URL = "https://matrix.to/#/#habit-maker:matrix.org"
16 | const val DONATE_URL = "https://liberapay.com/dessalines"
17 | const val LEMMY_URL = "https://lemmy.ml/c/habitmaker"
18 | const val MASTODON_URL = "https://mastodon.social/@dessalines"
19 |
20 | val SUCCESS_EMOJIS = listOf("🎉", "🥳", "🎈", "🎊", "🪇", "🎂", "🙌", "💯", "⭐")
21 |
22 | fun openLink(
23 | url: String,
24 | ctx: Context,
25 | ) {
26 | val intent = Intent(Intent.ACTION_VIEW, url.toUri())
27 | ctx.startActivity(intent)
28 | }
29 |
30 | fun Context.getPackageInfo(): PackageInfo =
31 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
32 | packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0))
33 | } else {
34 | packageManager.getPackageInfo(packageName, 0)
35 | }
36 |
37 | fun Context.getVersionCode(): Int =
38 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
39 | getPackageInfo().longVersionCode.toInt()
40 | } else {
41 | @Suppress("DEPRECATION")
42 | getPackageInfo().versionCode
43 | }
44 |
45 | sealed interface SelectionVisibilityState {
46 | object NoSelection : SelectionVisibilityState
47 |
48 | data class ShowSelection- (
49 | val selectedItem: Item,
50 | ) : SelectionVisibilityState
-
51 | }
52 |
53 | fun Int.toBool() = this == 1
54 |
55 | fun Boolean.toInt() = this.compareTo(false)
56 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
19 |
28 |
37 |
46 |
53 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dessalines/habit-maker/9ecb7a9fc4f067bd5af6859b195a319adb2244dc/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dessalines/habit-maker/9ecb7a9fc4f067bd5af6859b195a319adb2244dc/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dessalines/habit-maker/9ecb7a9fc4f067bd5af6859b195a319adb2244dc/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dessalines/habit-maker/9ecb7a9fc4f067bd5af6859b195a319adb2244dc/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dessalines/habit-maker/9ecb7a9fc4f067bd5af6859b195a319adb2244dc/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dessalines/habit-maker/9ecb7a9fc4f067bd5af6859b195a319adb2244dc/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dessalines/habit-maker/9ecb7a9fc4f067bd5af6859b195a319adb2244dc/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dessalines/habit-maker/9ecb7a9fc4f067bd5af6859b195a319adb2244dc/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dessalines/habit-maker/9ecb7a9fc4f067bd5af6859b195a319adb2244dc/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values-ar/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | حول التطبيق
4 | صانع العادات
5 | الإعدادات
6 | النظام
7 | فاتح
8 | داكن
9 | ديناميكي
10 | أخضر
11 | وردي
12 | ما الجديد
13 | الإصدار %1$s
14 | الإصدارات
15 | الدعم
16 | متتبع المشاكل
17 | غرفة دردشة المطورين (Matrix)
18 | تبرع لصانع العادات
19 | اجتماعي
20 | السلوك
21 | تمت استعادة قاعدة البيانات.
22 | تم عمل نسخة احتياطية لقاعدة البيانات.
23 | النسخ الاحتياطي والاستعادة
24 | العودة
25 | هل أنت متأكد؟
26 | أنت في سلسلة %1$s أسبوع، +%2$s نقطة
27 | أنت في سلسلة %1$s شهر، +%2$s نقطة
28 | أنت في سلسلة %1$s سنة، +%2$s نقطة
29 | عمل رائع! ليس الجميع مجتهداً مثلك.
30 | إخفاء النتيجة في الصفحة الرئيسية
31 | إخفاء الأيام المكتملة في الصفحة الرئيسية
32 | السلسلة
33 | النقاط
34 | النتيجة
35 | الحالة
36 | تاريخ الإنشاء
37 | انضم إلى lemmy.ml/c/habitmaker
38 | تابعني على Mastodon
39 | مفتوح المصدر
40 | شفرة المصدر
41 | صانع العادات هو برنامج حر ومفتوح المصدر، مرخص بموجب ترخيص جنو أفيرو العمومي العام v3.0
42 | المظهر والسلوك
43 | السمة
44 | لون السمة
45 | دليل المستخدم
46 | تم
47 | نسخ احتياطي لقاعدة البيانات
48 | استعادة قاعدة البيانات
49 | تحذير: سيؤدي هذا إلى مسح قاعدة البيانات الحالية لديك
50 | نعم
51 | لا
52 | إلغاء
53 | سلسلة %1$s يوم
54 | سلسلة %1$s أسبوع
55 | سلسلة %1$s شهر
56 | سلسلة %1$s سنة
57 | %1$s نقطة
58 | %1$s%% مكتمل
59 | %1$s يوم مكتمل
60 | إنشاء عادة
61 | حفظ
62 | العادة موجودة بالفعل
63 | تعديل العادة
64 | تشجيع
65 | ما هي عبارات التشجيع؟
66 | إضافة عبارة تشجيع
67 | العنوان
68 | كم مرة
69 | %1$s مرة في اليوم
70 | %1$s مرة في الأسبوع
71 | %1$s مرة في الشهر
72 | %1$s مرة في السنة
73 | خارج النطاق
74 | ملاحظات (اختياري)
75 | متى وأين؟ (اختياري)
76 | حذف
77 | المزيد من الإجراءات
78 | نظرة عامة
79 | السجل
80 | ملاحظات
81 | أنت في سلسلة %1$s يوم، +%2$s نقطة
82 | عمل ممتاز! خذ دقيقة للتفكير لماذا هذا مهم بالنسبة لك.
83 | مذهل! أنت تسير بخطى ثابتة.
84 | بدأت في %1$s
85 | العادات
86 | إخفاء المكتملة
87 | إظهار المكتملة
88 | لا توجد عادات
89 | تم إكمال كل شيء لهذا اليوم!
90 | يومي
91 | أسبوعي
92 | شهري
93 | سنوي
94 | عدد مرات الإكمال: %1$s
95 | %1$s عادة تم إكمالها اليوم
96 | عدد المرات قبل أن يتم تعلم العادة بالكامل. (66 في المتوسط)
97 | فرز
98 | ترتيب الفرز
99 | إخفاء المؤرشفة
100 | إخفاء السلاسل في الصفحة الرئيسية
101 | إخفاء النقاط في الصفحة الرئيسية
102 | الاسم
103 | تنازلي
104 | تصاعدي
105 | مؤرشفة
106 | هل أكملت هذه العادة اليوم؟
107 | لا توجد تذكيرات
108 | ذكرني كل يوم
109 | ذكرني في أيام محددة
110 | الوقت
111 | تجاهل
112 | تأكيد
113 | الأيام
114 | طلب إذن الإشعارات
115 | إذن الإشعارات مطلوب لهذه الميزة
116 | إخفاء وصف الشرائح
117 | يعرض فقط الأيقونة والرقم لشرائح المعلومات.
118 |
119 |
--------------------------------------------------------------------------------
/app/src/main/res/values-bg/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Зелен
4 | Отворен код
5 | Системна
6 | Относно
7 | Ръководство на потребителя
8 | Резервно копие
9 | Отказ
10 | Резервното копие е възстановено.
11 | Не
12 | Тъмна
13 | Социални
14 | Назад
15 | Светла
16 | Какво е новото
17 | Стая за разработчици в Матрикс
18 | Готово
19 | Сигурни ли сте?
20 | Тема
21 | Запазване
22 | Хронология
23 | Какво са поощренията?
24 | %1$s пъти годишно
25 | Време и място (по желание)
26 | В %1$s-годишна серия сте, +%2$s точки
27 | Habit-Maker
28 | Даряване на Habit-Maker
29 | Розов
30 | Изпълнихте ли действията на навика днес?
31 | Начална дата %1$s
32 | Брой изпълнени: %1$s
33 | Липсват навици
34 | Всичко е изпълнено за днес!
35 | Резултат
36 | Напомняне в определени дни
37 | Искане на права за показване на известия
38 | Скриване на изпълнените
39 | Всеки месец
40 | Всяка година
41 | Броят на повторенията, преди даден навик да бъде трайно установен. (средно 66)
42 | Точки
43 | Показване на изпълнените
44 | Настройки
45 | Динамичен
46 | Издание %1$s
47 | Издания
48 | Поддръжка
49 | Управление на дефекти
50 | Присъединете се към lemmy.ml/c/habitmaker
51 | Последвайте ме в Mastodon
52 | Изходен код
53 | Habit-Maker е свободен софтуер с отворен код, лицензиран под GNU Affero General Public License v3.0
54 | Външен вид и усещане
55 | Поведение
56 | Цвят на темата
57 | Възстановяване на резервно копие
58 | Внимание: Заличава текущите данни
59 | Резервното копие е готово.
60 | Резервно копие и възстановяване
61 | Да
62 | %1$s поредни дни
63 | %1$s поредни години
64 | %1$s поредни месеца
65 | %1$s поредни седмици
66 | %1$s точки
67 | изпълнени %1$s%%
68 | Създаване на навик
69 | Такъв навик вече има
70 | Променяне
71 | Поощрение
72 | Ново поощрение
73 | Заглавие
74 | Колко пъти
75 | %1$s пъти на ден
76 | %1$s пъти седмично
77 | %1$s пъти месечно
78 | Извън ограничението
79 | Бележка (по желание)
80 | Премахване
81 | Повече действия
82 | Общ изглед
83 | Бележка
84 | В %1$s-дневна серия сте, +%2$s точки
85 | В %1$s-седмична серия сте, +%2$s точки
86 | В %1$s-месечна серия сте, +%2$s точки
87 | Страхотна работа! Не всеки е толкова усърден, колкото вас.
88 | Навици
89 | Всеки ден
90 | Всяка седмица
91 | Отлична работа! Отделете минута, за да помислите защо този навик е важен за вас.
92 | Изключително! Навлизате в ритъм.
93 | %1$s изпълнени навика за деня
94 | Сортиране
95 | Ред на сортиране
96 | Скриване на архивираните
97 | Скриване на поредните от първия екран
98 | Скриване на точките от първия екран
99 | Скриване на резултата от първия екран
100 | Поредни
101 | Състояние
102 | Дата на създаване
103 | Име
104 | Архивиран
105 | Низходящ
106 | Възходящ
107 | Без напомняне
108 | Напомняне всеки ден
109 | Час
110 | Затваряне
111 | Потвърждаване
112 | Дни от седмицата
113 | Тази възможност изисква показване не известия
114 | Етикетите показват само пиктограма и брой.
115 | Без описания на етикетите
116 | Скриване на изпълнените дни от първия екран
117 | %1$s изпълнени дни
118 |
119 |
--------------------------------------------------------------------------------
/app/src/main/res/values-br/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Notennoù
4 | Bep miz
5 | Skor
6 | Boneg tarzh
7 | Sturlevr an arveriad
8 | Habit-Maker
9 | Renk
10 | Deiziad kregiñ : %1$s
11 | %1$s a vloavezhioù diouzh renk
12 | Notennoù (diret)
13 | %1$s a voazioù graet hiziv
14 | Urzh rummañ
15 | Graet : %1$s%%
16 | Teñval
17 | Poentoù : %1$s
18 | War %1$s a vizvezhioù diouzh renk emaoc\'h, +%2$s a boentoù
19 | Handelv %1$s
20 | Er maez eus al lijorenn
21 | Gweredoù all
22 | Alberz
23 | Deiziad krouiñ
24 | Krouiñ ur voaz
25 | Bemdez
26 | Labour a-feson ! Ne labour ket an holl dud kement-se.
27 | Heuliañ ac\'hanon war Mastodon
28 | Rummañ
29 | Arventennoù
30 | Reizhiad
31 | Sklaer
32 | Dialuskel
33 | Gwer
34 | Roz
35 | A-zivout
36 | Petra zo nevez
37 | Handelvoù
38 | Skor
39 | Heuliañ kudennoù
40 | Sal flapiñ Matrix evit an diorroerien
41 | Arc\'hantaouiñ Habit-Maker
42 | Rouedadoù sokial
43 | Kejañ ouzh lemmy.ml/c/habitmaker
44 | Tarzh digor
45 | Neuz
46 | Emzalc\'h
47 | Dodenn
48 | Liv an dodenn
49 | Mat eo
50 | Gwarediñ ar stlennvon
51 | Assav ar stlennvon
52 | Diwallit : adderaouekaet e vo ho stlennvon
53 | Stlennvon assavet.
54 | Stlennvon gwaredet.
55 | Distreiñ
56 | Sur oc\'h ?
57 | Ya
58 | Ket
59 | Nullañ
60 | %1$s a zevezhioù diouzh renk
61 | %1$s a sizhunvezhioù diouzh renk
62 | %1$s a vizvezhioù diouzh renk
63 | Embann ar voaz
64 | Kalonekadur
65 | Petra eo ar c\'halonekadurioù ?
66 | Ouzhpennañ ur c\'halonekadur
67 | Titl
68 | Pet gwech
69 | %1$s a wechoù bemdez
70 | %1$s a wechoù bep sizhun
71 | %1$s a wechoù bep miz
72 | Pegoulz ha pelec\'h ? (diret)
73 | Roll istor
74 | War %1$s a zevezhioù diouzh renk emaoc\'h, +%2$s a boentoù
75 | War %1$s a vloavezhioù diouzh renk emaoc\'h, +%2$s a boentoù
76 | Labour dispar ! Soñjit un tamm diwar-benn perak eo ken pouezus evidoc\'h an dra-se.
77 | Boazioù
78 | Kuzhat ar re graet
79 | Diskouez ar re graet
80 | Boaz ebet
81 | Bep bloaz
82 | Niver a voazioù graet : %1$s
83 | Niver a wechoù a-raok ma vo desket ur voaz penn da benn. (66 eo ar c\'heitad)
84 | Kuzhat ar re diellaouet
85 | Kuzhat ar renk war an degemer
86 | Kuzhat ar poentoù war an degemer
87 | Kuzhat ar skor war an degemer
88 | Anv
89 | War-zigresk
90 | War-gresk
91 | Diellaouet
92 | Graet eo bet ar voaz-mañ ganit hiziv ?
93 | Adc\'halv ebet
94 | Degas soñj din bemdez
95 | Degas soñj din da zevezhioù resis
96 | Eur
97 | Argas
98 | Kadarnaat
99 | Deizioù
100 | An aotre rebuziñ zo ret evit ar c\'heweriuster-mañ
101 | Kuzhat deskrivadurioù ar padelligoù
102 | Ur meziant frank gant tarzh digor eo Habit-Maker, dindan al lañvaz GNU Affero General Public License v3.0
103 | Ar voaz zo anezhi endeo
104 | Dilemel
105 | %1$s bep bloaz
106 | Enrollañ
107 | Assav ha gwarediñ
108 | War %1$s a sizhunvezhioù diouzh renk emaoc\'h, +%2$s a boentoù
109 | Biskoazh kement-all ! Krog oc\'h da vezañ boazet.
110 | Stad
111 | Graet eo bet pep tra hiziv !
112 | Bep sizhun
113 | Poentoù
114 | Goulenn an aotre rebuziñ
115 | Diskouez an arlun hag an niver evit ar padelligoù titouroù nemetken.
116 | %1$s a zevezhioù graet
117 | Kuzhat an devezhioù graet war an degemer
118 |
119 |
--------------------------------------------------------------------------------
/app/src/main/res/values-de/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Einstellungen
4 | Dunkel
5 | Neuigkeiten
6 | Version %1$s
7 | Unterstützung
8 | Issue tracker
9 | Veröffentlichungen
10 | Spende an Habit-Maker
11 | trete lemmy.ml/c/habitmaker bei
12 | Habit-Maker ist kostenlose Open-Source-Software, lizenziert unter der GNU Affero General Public License v3.0
13 | Design
14 | Nutzer Handbuch
15 | Achtung: Dieser Vorgang wird die jetzige Datenbank löschen
16 | Bist du sicher?
17 | Abbrechen
18 | %1$s-Tage-Serie
19 | Speichern
20 | Gewohnheit bearbeiten
21 | Was sind Ermutigungen?
22 | Erstelle eine Ermutigung
23 | Wie oft
24 | %1$s mal in der Woche
25 | %1$s mal im Jahr
26 | Außerhalb des Bereichs
27 | Überblick
28 | Verlauf
29 | Herausragend! Du kommst richtig in Fahrt.
30 | Keine Gewohnheiten
31 | Abgeschlossene ausblenden
32 | Wöchentlich
33 | Monatlich
34 | Sortiere nach
35 | Sortierreihenfolge
36 | Archivierte ausblenden
37 | Status
38 | Tage
39 | Zeit
40 | Chip-Beschreibungen ausblenden
41 | Zeigt nur das Symbol und die Zahl für die Informations-Chips an.
42 | Gewohnheit erstellen
43 | Notizen (optional)
44 | Titel
45 | Löschen
46 | Notizen
47 | Du bist auf einer %1$s-Tage-Serie, +%2$s Punkte
48 | Du bist auf einer %1$s-Wochen-Serie, +%2$s Punkte
49 | Du bist auf einer %1$s-Jahre-Serie, +%2$s Punkte
50 | Archiviert
51 | Abgeschlossene anzeigen
52 | Erinnere mich an bestimmten Tagen
53 | Hell
54 | Pink
55 | Schließen
56 | Datenbank wiederhergestellt.
57 | Datenbank wiederherstellen
58 | Über
59 | Folge mir auf Mastodon
60 | Quelloffen
61 | Quellcode
62 | Datenbank gesichert.
63 | %1$s-Wochen-Serie
64 | Tolle Arbeit! Nicht jeder ist so fleißig wie du.
65 | Habit-Maker
66 | Dynamisch
67 | Soziale Medien
68 | Entwickler Matrix Chat
69 | System
70 | Grün
71 | Anzahl von Wiederholungen: %1$s
72 | Erscheinungsbild
73 | Serie
74 | Zurück
75 | %1$s Gewohnheiten heute abgeschlossen
76 | Datenbank sichern
77 | Design Farbe
78 | %1$s-Jahre-Serie
79 | %1$s mal im Monat
80 | Absteigend
81 | %1$s%% fertiggestellt
82 | Die Anzahl der Wiederholungen, bevor eine Gewohnheit vollständig erlernt ist. (Im Durchschnitt 66)
83 | Fertig
84 | Angefangen am %1$s
85 | Du bist auf einer %1$s-Monate-Serie, +%2$s Punkte
86 | Verhalten
87 | Punkte auf der Startseite ausblenden
88 | Serien auf der Startseite ausblenden
89 | Aufsteigend
90 | Wann und wo? (optional)
91 | Täglich
92 | Score auf der Startseite ausblenden
93 | Bestätigen
94 | Benachrichtigungsberechtigungen sind für diese Funktion erforderlich
95 | Ja
96 | Gewohnheit existiert bereits
97 | Punkte
98 | Hast du diese Gewohnheit heute abgeschlossen?
99 | %1$s-Monate-Serie
100 | %1$s mal am Tag
101 | Sichern und Wiederherstellen
102 | Ausgezeichnete Arbeit! Nimm dir einen Moment, um darüber nachzudenken, warum dir das wichtig ist.
103 | Erstellungsdatum
104 | Name
105 | Ermutigung
106 | %1$s Punkte
107 | Weitere Aktionen
108 | Gewohnheiten
109 | Nein
110 | Für Heute fertig!
111 | Jährlich
112 | Erinnere mich jeden Tag
113 | Score
114 | Keine Erinnerungen
115 | Benachrichtigungsberechtigung anfordern
116 |
117 |
--------------------------------------------------------------------------------
/app/src/main/res/values-es/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Estas en una racha de %1$s años, +%2$s puntos
4 | ¿Has completado este hábito hoy?
5 | Habit-Maker es software libre y de código abierto, licenciado bajo la Licencia Pública General de GNU Affero, versión 3.0
6 | No
7 | Respaldo de base de datos
8 | Versiones
9 | Claro
10 | Oscuro
11 | Dinámico
12 | Habit-Maker
13 | Verde
14 | Rosa
15 | Acerca de
16 | Novedades
17 | Versión %1$s
18 | Soporte
19 | Chat de desarrolladores (Matrix)
20 | Dona a Habit-Maker
21 | Social
22 | Sigueme en Mastodon
23 | Código abierto
24 | Apariencia
25 | Comportamiento
26 | Tema
27 | Color del tema
28 | Guía de usuario
29 | Hecho
30 | Restaurar base de datos
31 | Base de datos restaurada.
32 | Base de datos respaldada.
33 | Copia de seguridad y restauración
34 | Volver
35 | ¿Estas seguro?
36 | Cancelar
37 | Racha de %1$s días
38 | Racha de %1$s meses
39 | Racha de %1$s años
40 | %1$s dias completados
41 | Guardar
42 | Ánimo
43 | ¿Que son los ánimos?
44 | Añadir animo
45 | Título
46 | Numero de veces
47 | %1$s veces al día
48 | %1$s veces al año
49 | Notas (opcional)
50 | ¿Cuando y donde? (opcional)
51 | Eliminar
52 | Vista general
53 | Notas
54 | Estas en una racha de %1$s dias, +%2$s puntos
55 | ¡Gran trabajo! No todo el mundo es tan trabajador como tú.
56 | ¡Excelente trabajo! Tómate un minuto para pensar por qué esto es importante para ti.
57 | ¡Excelente! Vas por buen camino.
58 | Comenzó el %1$s
59 | Hábitos
60 | ¡Todo listo por hoy!
61 | Todos los dias
62 | Total completados: %1$s
63 | %1$s hábitos completados hoy
64 | El número de veces antes de que un hábito esté completamente aprendido. (66 en promedio)
65 | Ordenar por
66 | Orden
67 | Ocultar archivados
68 | Ocultar rachas en la pantalla de inicio
69 | Ocultar puntos en la pantalla de inicio
70 | Ocultar puntuación en la pantalla de inicio
71 | Ocultar dias completados en la pantalla de inicio
72 | Racha
73 | Puntos
74 | Puntuación
75 | Estado
76 | Fecha de creacion
77 | Ascendente
78 | Archivado
79 | No hay recordatorios
80 | Recuérdame todos los días
81 | Recuérdame en días específicos
82 | Rechazar
83 | Días
84 | Solicitar permiso de notificación
85 | Esta función necesita permiso para enviar notificaciones
86 | Ocultar descripciones de etiquetas
87 | Solo muestra el icono y el número en las etiquetas de información.
88 | Editar hábito
89 | El hábito ya existe
90 | Ocultar tareas completadas
91 | Unete a lemmy.ml/c/habitmaker
92 | Semanalmente
93 | Todos los años
94 | Advertencia: Esto eliminará tu base de datos actual
95 | Crear hábito
96 | Informe de errores
97 | Historial
98 | Código fuente
99 | Todos los meses
100 | Si
101 | Racha de %1$s semanas
102 | %1$s puntos
103 | %1$s%% completado
104 | %1$s veces por semana
105 | Mostrar tareas completadas
106 | %1$s veces al mes
107 | Fuera de rango
108 | No hay hábitos
109 | Sistema
110 | Más acciones
111 | Confirmar
112 | Estas en una racha de %1$s meses, +%2$s puntos
113 | Estas en una racha de %1$s semanas, +%2$s puntos
114 | Nombre
115 | Descendiente
116 | Hora
117 | Ajustes
118 |
119 |
--------------------------------------------------------------------------------
/app/src/main/res/values-fr/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | A propos
4 | Open source
5 | Système
6 | Habit-Maker
7 | Modifier l\'habitude
8 | Points
9 | Annuler
10 | Non
11 | Historique
12 | Base de données sauvegardée.
13 | Créé le
14 | Réseaux sociaux
15 | Plus d\'actions
16 | Cacher les séries sur la page d\'accueil
17 | Couleur du thème
18 | Cette habitude existe déjà
19 | Vue d\'ensemble
20 | Vous êtes sur une série de %1$s ans, +%2$s points
21 | Aspect et ressenti
22 | Avez-vous complété cette habitude aujourd\'hui ?
23 | Titre
24 | Clair
25 | Sombre
26 | Rose
27 | Nouveautés
28 | Version %1$s
29 | Mise à jour
30 | Aide
31 | Suivis des problèmes
32 | Salle de discussion Matrix pour les développeurs
33 | Rejoindre lemmy.ml/c/habitmaker
34 | Suivez-moi sur Mastodon
35 | Habit-Maker est un logiciel open-source libre, sous license GNU Affero General Public License v3.0
36 | Comportement
37 | Thème
38 | Manuel d\'utilisation
39 | Fin
40 | Sauvegarder la base de données
41 | Restaurer la base de données
42 | Retour
43 | Etes-vous sûr ?
44 | Oui
45 | Série de %1$s jours
46 | Série de %1$s semaines
47 | Série de %1$s mois
48 | Série de %1$s années
49 | %1$s points
50 | %1$s%% complété
51 | %1$s jours complétés
52 | Créer une habitude
53 | Encouragements
54 | Que sont les encouragements ?
55 | Ajouter un encouragement
56 | Nombre de fois
57 | %1$s fois par jour
58 | %1$s fois par semaine
59 | %1$s fois par mois
60 | Limite dépassée
61 | Notes (optionnel)
62 | Quand et où ? (optionnel)
63 | Supprimer
64 | Notes
65 | Vous êtes sur une série de %1$s jours, +%2$s points
66 | Vous êtes sur une série de %1$s semaines, +%2$s points
67 | Vous êtes sur une série de %1$s mois, +%2$s points
68 | Beau travail ! Prenez une minute pour réfléchir à pourquoi c\'est important pour vous.
69 | Commencé sur %1$s
70 | Habitudes
71 | Cacher les habitudes complétées
72 | Aucune habitude
73 | Terminé pour aujourd\'hui !
74 | Quotidien
75 | Hebdomadaire
76 | Complété : %1$s
77 | Trier
78 | Ordre de tri
79 | Cacher les habitudes archivées
80 | Cacher les points sur la page d\'accueil
81 | Cacher le score sur la page d\'accueil
82 | Statut
83 | Nom
84 | Croissant
85 | Archivées
86 | Aucun rappels
87 | Rappel quotidien
88 | Rappel certains jours
89 | Heure
90 | Rejeter
91 | Confirmer
92 | Jours
93 | Demande d\'autorisation de notification
94 | L\'autorisation des notifications est nécessaire pour activer cette fonctionnalité
95 | Cacher les descriptions des puces
96 | Seuls l\'icône et le numéro des puces d\'information sont affichés.
97 | Paramètres
98 | Dynamique
99 | Donnez à Habit-Maker
100 | Vert
101 | Annuel
102 | Code source
103 | Attention : Cette action supprimera la base de données actuelle
104 | Base de données restaurée.
105 | Sauvegarde et restauration
106 | %1$s fois par an
107 | Sauvegarder
108 | Excellent ! Vous atteignez votre vitesse de croisière.
109 | Série
110 | Score
111 | Bravo ! Tout le mone n\'est pas aussi travailleur que vous.
112 | Mensuel
113 | %1$s habitudes complétées aujourd\'hui
114 | Montrer les habitudes complétées
115 | Décroissant
116 | Cacher le nombre de jours complétés sur la page d\'accueil
117 | Le nombre de répétitions nécessaire pour aquérir complètement une habitude. (66 en moyenne)
118 |
119 |
--------------------------------------------------------------------------------
/app/src/main/res/values-pl/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Ustawienia
4 | Systemowy
5 | Jasny
6 | Ciemny
7 | Dynamiczny
8 | O aplikacji
9 | Co nowego?
10 | Wersja %1$s
11 | Wydania
12 | Wsparcie
13 | Dołącz do lemmy.ml/c/habitmaker
14 | Społeczność
15 | Przekaż darowiznę dla Habit-Maker
16 | Czat Matrix dla programistów
17 | Śledzenie problemów
18 | Motyw
19 | Kolor motywu
20 | Zachowanie
21 | Gotowe
22 | Open source
23 | Kopia zapasowa bazy danych
24 | Przywróć bazę danych
25 | Tak
26 | Nie
27 | Ostrzeżenie: spowoduje to usunięcie obecnej bazy danych
28 | Kopia zapasowa bazy danych utworzona.
29 | Kopia zapasowa i przywracanie
30 | Wróć
31 | Jesteś pewien?
32 | %1$s punktów
33 | Tytuł
34 | Usuń
35 | Więcej opcji
36 | Historia
37 | Nawyk już istnieje
38 | Ile razy?
39 | %1$s razy dziennie
40 | %1$s razy tygodniowo
41 | %1$s razy miesięcznie
42 | %1$s razy rocznie
43 | Notatki (opcjonalne)
44 | Gdzie i kiedy? (opcjonalnie)
45 | Utwórz nawyk
46 | Nawyki
47 | Ukryj zakończone
48 | Pokaż zakończone
49 | Codziennie
50 | Co tydzień
51 | Rozpoczęte %1$s
52 | Na dziś wszystko zrobione!
53 | Brak nawyków
54 | Ukończono: %1$s
55 | %1$s nawyków ukończonych dzisiaj
56 | Punkty
57 | Wynik
58 | Status
59 | Data utworzenia
60 | Rosnąco
61 | Ukryj punkty na stronie głównej
62 | Ukryj wynik na stronie głównej
63 | Ukryj zakończone dni na stronie głównej
64 | Liczba powtórzeń, zanim nawyk zostanie w pełni wyuczony. (średnio 66)
65 | Kolejność sortowania
66 | Brak przypomnień
67 | Przypominaj mi każdego dnia
68 | Przypominaj mi w określone dni
69 | Czas
70 | Anuluj
71 | Potwierdź
72 | Dni
73 | Ta funkcja wymaga zezwolenia na wysyłanie powiadomień
74 | Zapisz
75 | Nazwa
76 | Co rok
77 | Zarchiwizowane
78 | Udało ci się dzisiaj zrealizować ten nawyk?
79 | Co miesiąc
80 | Ukryj zarchiwizowane
81 | Przegląd
82 | Różowy
83 | Zielony
84 | Anuluj
85 | Malejąco
86 | Notatki
87 | Kod źródłowy
88 | Sortuj
89 | Baza danych przywrócona.
90 | Świetna robota! Nie każdy jest tak pracowity jak ty.
91 | Obserwuj mnie na Mastodonie
92 | Edytuj nawyk
93 | Habit-Maker to wolne otwartoźródłowe oprogramowanie, na licencji GNU Affero General Public License v3.0
94 | Kreator nawyków
95 | Wygląd
96 | Seria
97 | Ukryj serie na stronie głównej
98 | Zezwól na wyświetlanie powiadomień
99 | Przewodnik użytkownika
100 | Ukończono %1$s%%
101 | Ukończono %1$s dni
102 | Seria %1$s dni
103 | Seria %1$s tygodni
104 | Seria %1$s miesięcy
105 | Seria %1$s lat
106 | Zachęta
107 | Czym są zachęty?
108 | Dodaj zachętę
109 | Ukryj opisy kafelków
110 | Wyświetlaj tylko ikonę i liczbę kafelka informacyjnego.
111 |
112 |
--------------------------------------------------------------------------------
/app/src/main/res/values-pt-rBR/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Sistema
4 | Suporte
5 | Chatroom Matrix dos desenvolvedores
6 | Siga-me no Mastodon
7 | Você está em uma sequência de %1$s mês(es), +%2$s ponto(s)
8 | Restaurar banco de dados
9 | Configurações
10 | Claro
11 | Escuro
12 | Dinâmico
13 | Verde
14 | Rosa
15 | Sobre
16 | Novidades
17 | Versão %1$s
18 | Lançamentos
19 | Reportar problema
20 | Doe para o Habit-Maker
21 | Redes sociais
22 | Junte-se ao lemmy.ml/c/habitmaker
23 | Código aberto
24 | Código-fonte
25 | Aparência
26 | Comportamento
27 | Tema
28 | Cor do tema
29 | Guia do usuário
30 | Concluído
31 | Backup do banco de dados
32 | Banco de dados restaurado.
33 | Backup e restauração
34 | Voltar
35 | Você tem certeza?
36 | Não
37 | Sequência de %1$s dia(s)
38 | Sequência de %1$s mês(es)
39 | Sequência de %1$s ano(s)
40 | Criar hábito
41 | Salvar
42 | Hábito já existe
43 | Editar hábito
44 | Incentivo
45 | O que são incentivos?
46 | Adicionar incentivo
47 | Título
48 | Quantas vezes
49 | %1$s vezes por dia
50 | %1$s vezes por semana
51 | %1$s vezes por mês
52 | %1$s vezes por ano
53 | Fora de alcance
54 | Anotações (opcional)
55 | Quando e onde? (opcional)
56 | Excluir
57 | Mais ações
58 | Visão geral
59 | Histórico
60 | Anotações
61 | Você está em uma sequência de %1$s dia(s), +%2$s ponto(s)
62 | Você está em uma sequência de %1$s semana(s), +%2$s ponto(s)
63 | Bom trabalho! Nem todo mundo é tão esforçado como você.
64 | Excelente! Tire um minuto para pensar sobre por que isso é importante para você.
65 | Iniciado em %1$s
66 | Hábitos
67 | Ocultar concluídos
68 | Mostrar concluídos
69 | Sem hábitos
70 | Diário
71 | Semanal
72 | Mensal
73 | Contagem de concluídos: %1$s
74 | %1$s hábitos concluídos hoje
75 | Habit-Maker
76 | Habit-Maker é um software livre e de código aberto, licenciado sob a GNU Affero General Public License v3.0
77 | Sim
78 | Backup do banco de dados realizado.
79 | Atenção: Isto irá apagar o seu banco de dados atual
80 | %1$s%% concluído
81 | Cancelar
82 | Sequência de %1$s semana(s)
83 | %1$s ponto(s)
84 | %1$s dia(s) concluído(s)
85 | Você está em uma sequência de %1$s ano(s), +%2$s ponto(s)
86 | Todos concluídos por hoje!
87 | Impressionante! Você está com tudo.
88 | Anual
89 |
90 |
--------------------------------------------------------------------------------
/app/src/main/res/values-ru/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Habit-Maker
4 | Настройки
5 | Система
6 | Светлая
7 | Темная
8 | Динамический
9 | Зелёный
10 | Розовый
11 | О проекте
12 | Что нового
13 | Версия %1$s
14 | Релизы
15 | Поддержка
16 | Трекер задач
17 | Чат Matrix для программистов
18 | Сделать пожертвование для Habit-Maker
19 | Социальные сети
20 | Присоединиться к lemmy.ml/c/habitmaker
21 | Следовать за мной на Mastodon
22 | Открытый исходный код
23 | Исходный код
24 | Habit-Maker является свободным программным обеспечением с открытым исходным кодом, распространяемым под лицензией GNU Affero General Public License v3.0
25 | Поведение
26 | Тема
27 | Цвет темы
28 | Руководство пользователя
29 | Готово
30 | Создать резервную копию базы данных
31 | Восстановить базу данных из резервной копии
32 | Предупреждение: это удалит текущую базу данных
33 | База данных восстановлена.
34 | Резервная копия базы данных создана.
35 | Резервная копия и восстановление
36 | Вернуться
37 | Вы уверены?
38 | Да
39 | Закрыть
40 | %1$s дн. подряд
41 | %1$s нед. подряд
42 | %1$s мес. подряд
43 | %1$s лет подряд
44 | %1$s очк.
45 | %1$s%% завершено
46 | %1$s дн. завершено
47 | Создать привычку
48 | Сохранить
49 | Привычка уже существует
50 | Редактировать привычку
51 | Поощрение
52 | Какое поощрение?
53 | Добавить поощрение
54 | Заголовок
55 | Сколько раз
56 | %1$s раз(а) в день
57 | %1$s раз(а) в неделю
58 | %1$s раз(а) в месяц
59 | %1$s раз(а) в год
60 | Вне диапазона
61 | Заметки (опционально)
62 | Где и когда? (опционально)
63 | Удалить
64 | Больше действий
65 | Обзор
66 | История
67 | Заметки
68 | Вы в %1$s-дн. серии, +%2$s очк.
69 | Вы в %1$s-нед. серии, +%2$s очк.
70 | Вы в %1$s-мес. серии, +%2$s очк.
71 | Вы в %1$s-летней серии, +%2$s очк.
72 | Отличная работа! Не каждый так трудолюбив, как Вы.
73 | Замечательная работа! Найдите минутку, чтобы подумать о том, почему это важно для Вас.
74 | Дата начала: %1$s
75 | Привычки
76 | Скрыть завершенные
77 | Показать завершенное
78 | Нет привычек
79 | На сегодня все завершено!
80 | Ежедневно
81 | Еженедельно
82 | Ежемесячно
83 | Ежегодно
84 | Количество повторений: %1$s
85 | %1$s привычек выполнено сегодня
86 | Количество повторений до того, как привычка будет полностью усвоена. (обычно 66)
87 | Сортировка
88 | Порядок сортировки
89 | Скрыть архивированные
90 | Скрыть серию на домашней странице
91 | Скрыть счёт на домашней странице
92 | Скрыть результат на домашней странице
93 | Скрыть количество завершенных дней на домашней странице
94 | Серия
95 | Очки
96 | Результат
97 | Состояние
98 | Создано
99 | Имя
100 | Убывающий
101 | Восходящий
102 | Архивировано
103 | Вы выполнили эту привычку сегодня?
104 | Нет напоминаний
105 | Напоминать мне каждый день
106 | Напоминать мне в определенные дни
107 | Время
108 | Отклонить
109 | Подтвердить
110 | Дни недели
111 | Запросить разрешение на уведомления
112 | Эта функция требует разрешения на отправку уведомлений
113 | Скрыть описания информационных карточек
114 | Великолепно! Вы набираете обороты.
115 | Вид
116 | Нет
117 | Показывать только иконку и число на информационных карточках.
118 |
119 |
--------------------------------------------------------------------------------
/app/src/main/res/values-tr/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Habit-Maker
4 | Ayarlar
5 | Sistem
6 | Aydınlık
7 | Karanlık
8 | Pembe
9 | Yenilikler
10 | Versiyon %1$s
11 | Sürüm
12 | Geliştirici Matrix sohbet odası
13 | lemmy.ml/c/habitmaker\'a katılın
14 | Mastodon\'da takip edin
15 | Açık kaynak
16 | Kaynak kodu
17 | Habit-Maker, GNU Affero Genel Kamu Lisansı v3.0 ile lisanslanmış, özgür açık kaynaklı bir yazılımdır
18 | Görünüm ve deneyim
19 | Davranış
20 | Tema rengi
21 | Kullanıcı rehberi
22 | Tamam
23 | Veritabanını geri yükle
24 | Uyarı: Bu işlem mevcut veritabanınızı temizleyecektir
25 | Veritabanı geri yüklendi.
26 | Yedekleme ve geri yükleme
27 | Geri dön
28 | Emin misiniz?
29 | Evet
30 | Hayır
31 | %1$s günlük seri
32 | %1$s haftalık seri
33 | %1$s aylık seri
34 | %1$s puan
35 | %1$s%% tamamlandı
36 | %1$s gün tamamlandı
37 | Alışkanlık oluşturun
38 | Kaydet
39 | Teşvik
40 | Teşvik nedir?
41 | Başlık
42 | Kaç kez
43 | Günde %1$s kez
44 | Haftada %1$s kez
45 | Hata izleyici
46 | Veritabanı yedeklendi.
47 | Habit-Maker\'a bağış yapın
48 | Teşvik ekle
49 | Veritabanını yedekle
50 | Ayda %1$s kez
51 | Aralık dışı
52 | Notlar (isteğe bağlı)
53 | Sil
54 | Daha fazla eylem
55 | Genel bakış
56 | Geçmiş
57 | Notlar
58 | Tamamlananları gizle
59 | Tamamlananları göster
60 | Hiçbir alışkanlık yok
61 | Günlük
62 | Haftalık
63 | Aylık
64 | Bugün %1$s alışkanlık tamamlandı
65 | Bir alışkanlığın tam olarak öğrenilmesinden önceki tekrar sayısı. (Ortalama 66)
66 | Sıralama düzeni
67 | Arşivlenenleri gizle
68 | Ana sayfadaki serileri gizle
69 | Ana sayfadaki puanları gizle
70 | Ana sayfadaki skoru gizle
71 | Ana sayfada tamamlanan günleri gizle
72 | Seri
73 | Puanlar
74 | Skor
75 | Durum
76 | Oluşturulma tarihi
77 | İsim
78 | Azalan
79 | Artan
80 | %1$s günlük bir seri yakaladın, +%2$s puan
81 | %1$s haftalık bir seri yakaladın, +%2$s puan
82 | %1$s aylık bir seri yakaladın, +%2$s puan
83 | %1$s yıllık bir seri yakaladın, +%2$s puan
84 | %1$s tarihinde başlatıldı
85 | Mükemmel bir iş! Bunun senin için neden önemli olduğunu düşünmek için bir dakikanı ayır.
86 | Olağanüstü! Hızla ilerliyorsun.
87 | Sıralama
88 | Bugün bu alışkanlığı tamamladın mı?
89 | Hatırlatma yok
90 | Zaman
91 | Günler
92 | Bildirim izni isteyin
93 | Bu özellik için bildirim izni gereklidir
94 | Çip açıklamalarını gizle
95 | Onayla
96 | Reddet
97 | Her gün hatırlat
98 | Tema
99 | Dinamik
100 | Yıllık
101 | %1$s yıllık seri
102 | Yılda %1$s kez
103 | Alışkanlığı düzenle
104 | Ne zaman ve nerede? (isteğe bağlı)
105 | Sosyal
106 | Alışkanlık zaten mevcut
107 | Yeşil
108 | İptal
109 | Hakkında
110 | Destek
111 | Bilgi çipleri için sadece simge ve sayı gösterilir.
112 | Tamamlanan sayı: %1$s
113 | Harika bir iş çıkardın! Herkes senin kadar çalışkan değil.
114 | Alışkanlıklar
115 | Bugünlük her şey tamam!
116 | Arşivlenmiş
117 | Belirli günlerde hatırlat
118 |
119 |
--------------------------------------------------------------------------------
/app/src/main/res/values-zh-rCN/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 开发者的 Matrix 聊天室
4 | 问题追踪
5 | 加入 lemmy.ml/c/habitmaker
6 | 在 Mastodon 上关注我
7 | 开源
8 | 源代码
9 | 本软件基于 GNU Affero 通用公共许可证 v3.0 开源发布
10 | 外观与风格
11 | 行为
12 | 主题
13 | 主题颜色
14 | 用户指南
15 | 完成
16 | %1$s 月连胜
17 | %1$s 年连胜
18 | 习惯已存在
19 | 编辑习惯
20 | 捐赠给Habit-Maker
21 | 设置
22 | 系统
23 | 浅色
24 | 深色
25 | 动态
26 | 绿色
27 | 粉色
28 | 关于
29 | 新增内容
30 | 版本 %1$s
31 | 版本发布
32 | 支持
33 | 社交
34 | Habit-Maker
35 | 备份数据库
36 | 恢复数据库
37 | 警告:此操作将清空当前数据库
38 | 数据库已备份
39 | 备份与恢复
40 | 返回
41 | 确定执行此操作?
42 | 是
43 | 否
44 | 取消
45 | %1$s 天连胜
46 | %1$s 周连胜
47 | 数据库已恢复
48 | %1$s 积分
49 | 已完成 %1$s%%
50 | 已完成 %1$s 天
51 | 创建习惯
52 | 保存
53 | 时间与地点?(可选)
54 | 删除
55 | 更多操作
56 | 概览
57 | 历史记录
58 | 备注
59 | 您已连续坚持 %1$s 年,+%2$s 积分
60 | 做得好!不是所有人都像您这样勤奋。
61 | 太棒了!花一分钟想想为什么这对您很重要。
62 | 开始于 %1$s
63 | 暂无习惯
64 | 今日任务已完成!
65 | 每日
66 | 每周
67 | 每月
68 | 每年
69 | 完成次数:%1$s
70 | 今日已完成 %1$s 个习惯
71 | 习惯完全养成所需次数(平均66次)
72 | 排序
73 | 排序方式
74 | 隐藏已归档
75 | 首页隐藏连胜
76 | 首页隐藏积分
77 | 首页隐藏分数
78 | 首页隐藏完成天数
79 | 连胜
80 | 积分
81 | 分数
82 | 状态
83 | 创建日期
84 | 名称
85 | 降序
86 | 升序
87 | 已归档
88 | 今天完成这个习惯了吗?
89 | 鼓励语
90 | 什么是鼓励语?
91 | 添加鼓励语
92 | 标题
93 | 执行次数
94 | 每日 %1$s 次
95 | 每周 %1$s 次
96 | 每月 %1$s 次
97 | 每年 %1$s 次
98 | 超出范围
99 | 备注(可选)
100 | 您已连续坚持 %1$s 天,+%2$s 积分
101 | 您已连续坚持 %1$s 周,+%2$s 积分
102 | 您已连续坚持 %1$s 月,+%2$s 积分
103 | 出类拔萃!您已经渐入佳境了。
104 | 习惯
105 | 隐藏已完成
106 | 显示已完成
107 | 无提醒
108 | 每天提醒
109 | 忽略
110 | 指定日期提醒
111 | 时间
112 | 确认
113 | 天
114 | 请求通知权限
115 | 此功能需要通知权限
116 | 隐藏标签描述
117 | 仅显示信息标签的图标和数字
118 |
119 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Habit-Maker
3 | Settings
4 | System
5 | Light
6 | Dark
7 | Dynamic
8 | Green
9 | Pink
10 | About
11 | What\'s new
12 | Version %1$s
13 | Releases
14 | Support
15 | Issue tracker
16 | Developer Matrix chatroom
17 | Donate to Habit-Maker
18 | Social
19 | Join lemmy.ml/c/habitmaker
20 | Follow me on Mastodon
21 | Open source
22 | Source code
23 | Habit-Maker is libre open-source software, licensed under the GNU Affero General Public License v3.0
24 | Look and feel
25 | Behavior
26 | Theme
27 | Theme color
28 | User guide
29 | Done
30 | Backup database
31 | Restore database
32 | Warning: This will clear out your current database
33 | Database Restored.
34 | Database Backed up.
35 | Backup and restore
36 | Go back
37 | Are you sure?
38 | Yes
39 | No
40 | Cancel
41 | %1$s day streak
42 | %1$s week streak
43 | %1$s month streak
44 | %1$s year streak
45 | %1$s points
46 | %1$s%% complete
47 | %1$s days completed
48 | Create habit
49 | Save
50 | Habit already exists
51 | Edit habit
52 | Encouragement
53 | What are encouragements?
54 | Add encouragement
55 | Title
56 | How many times
57 | %1$s times per day
58 | %1$s times per week
59 | %1$s times per month
60 | %1$s times per year
61 | Out of range
62 | Notes (optional)
63 | When and where? (optional)
64 | Delete
65 | More actions
66 | Overview
67 | History
68 | Notes
69 | You\'re on a %1$s day streak, +%2$s points
70 | You\'re on a %1$s week streak, +%2$s points
71 | You\'re on a %1$s month streak, +%2$s points
72 | You\'re on a %1$s year streak, +%2$s points
73 | Great job! Not everyone is as hard-working as you are.
74 | Excellent job! Take a minute to think about why this is important to you.
75 | Outstanding! You\'re hitting your stride.
76 | Started on %1$s
77 | Habits
78 | Hide completed
79 | Show completed
80 | No habits
81 | All completed for today!
82 | Daily
83 | Weekly
84 | Monthly
85 | Yearly
86 | Completed count: %1$s
87 | %1$s habits completed today
88 | The number of times before a habit is fully learned. (66 on average)
89 | Sort
90 | Sort order
91 | Hide archived
92 | Hide streaks on home
93 | Hide points on home
94 | Hide score on home
95 | Hide days completed on home
96 | Streak
97 | Points
98 | Score
99 | Status
100 | Date created
101 | Name
102 | Descending
103 | Ascending
104 | Archived
105 | Did you complete this habit today?
106 | No reminders
107 | Remind me every day
108 | Remind me on specific days
109 | Time
110 | Dismiss
111 | Confirm
112 | Days
113 | Request notification permission
114 | Notification permission is required for this feature
115 | Hide chip descriptions
116 | Only shows icon and number for the info chips.
117 |
118 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | repositories {
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 |
9 | plugins {
10 | id("com.android.application") version "8.10.1" apply false
11 | id("com.android.library") version "8.10.1" apply false
12 | id("org.jetbrains.kotlin.android") version "2.1.21" apply false
13 | id("org.jetbrains.kotlin.plugin.compose") version "2.1.21" apply false
14 | id("org.jmailen.kotlinter") version "5.1.0" apply false
15 | id("com.google.devtools.ksp") version "2.1.21-2.0.1" apply false
16 | }
17 |
18 | subprojects {
19 | apply(plugin = "org.jmailen.kotlinter") // Version should be inherited from parent
20 | }
21 |
--------------------------------------------------------------------------------
/cliff.toml:
--------------------------------------------------------------------------------
1 | # git-cliff ~ configuration file
2 | # https://git-cliff.org/docs/configuration
3 |
4 | [remote.github]
5 | owner = "dessalines"
6 | repo = "habit-maker"
7 | # token = ""
8 |
9 | [changelog]
10 | # template for the changelog body
11 | # https://keats.github.io/tera/docs/#introduction
12 | body = """
13 | ## What's Changed
14 |
15 | {%- if version %} in {{ version }}{%- endif -%}
16 | {% for commit in commits %}
17 | {% if commit.remote.pr_title -%}
18 | {%- set commit_message = commit.remote.pr_title -%}
19 | {%- else -%}
20 | {%- set commit_message = commit.message -%}
21 | {%- endif -%}
22 | * {{ commit_message | split(pat="\n") | first | trim }}\
23 | {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}
24 | {% if commit.remote.pr_number %} in \
25 | [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) \
26 | {%- endif %}
27 | {%- endfor -%}
28 |
29 | {%- if github -%}
30 | {% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
31 | {% raw %}\n{% endraw -%}
32 | ## New Contributors
33 | {%- endif %}\
34 | {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
35 | * @{{ contributor.username }} made their first contribution
36 | {%- if contributor.pr_number %} in \
37 | [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
38 | {%- endif %}
39 | {%- endfor -%}
40 | {%- endif -%}
41 |
42 | {% if version %}
43 | {% if previous.version %}
44 | **Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}
45 | {% endif %}
46 | {% else -%}
47 | {% raw %}\n{% endraw %}
48 | {% endif %}
49 |
50 | {%- macro remote_url() -%}
51 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
52 | {%- endmacro -%}
53 | """
54 | # remove the leading and trailing whitespace from the template
55 | trim = true
56 | # template for the changelog footer
57 | footer = """
58 |
59 | """
60 | # postprocessors
61 | postprocessors = []
62 |
63 | [git]
64 | # parse the commits based on https://www.conventionalcommits.org
65 | conventional_commits = false
66 | # filter out the commits that are not conventional
67 | filter_unconventional = true
68 | # process each line of a commit as an individual commit
69 | split_commits = false
70 | # regex for preprocessing the commit messages
71 | commit_preprocessors = [
72 | # remove issue numbers from commits
73 | { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
74 | ]
75 | commit_parsers = [
76 | { field = "author.name", pattern = "renovate", skip = true },
77 | { field = "author.name", pattern = "Weblate", skip = true },
78 | { field = "message", pattern = "Upping version", skip = true },
79 | ]
80 | # filter out the commits that are not matched by commit parsers
81 | filter_commits = false
82 | # sort the tags topologically
83 | topo_order = false
84 | # sort the commits inside sections by oldest/newest order
85 | sort_commits = "newest"
86 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1.txt:
--------------------------------------------------------------------------------
1 | An initial release.
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/10.txt:
--------------------------------------------------------------------------------
1 | ## What's Changed in 0.0.10
2 |
3 | - Fix about page crash caused by adaptive icon addition. by @dessalines in [#36](https://github.com/dessalines/habit-maker/pull/36)
4 |
5 | **Full Changelog**: https://github.com/dessalines/habit-maker/compare/0.0.9...0.0.10
6 |
7 |
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/11.txt:
--------------------------------------------------------------------------------
1 | ## What's Changed in 0.0.11
2 |
3 | - Trying to fix fastlane image. by @dessalines
4 | - Adding better svg for icons from @vitrola06 by @dessalines
5 |
6 | **Full Changelog**: https://github.com/dessalines/habit-maker/compare/0.0.10...0.0.11
7 |
8 |
9 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/12.txt:
--------------------------------------------------------------------------------
1 | ## What's Changed in 0.0.12
2 |
3 | - Reminders by @dessalines in [#47](https://github.com/dessalines/habit-maker/pull/47)
4 |
5 | **Full Changelog**: https://github.com/dessalines/habit-maker/compare/0.0.11...0.0.12
6 |
7 |
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/13.txt:
--------------------------------------------------------------------------------
1 | ## What's Changed in 0.0.13
2 |
3 | - Remove notification policy permission. by @dessalines in [#49](https://github.com/dessalines/habit-maker/pull/49)
4 |
5 | **Full Changelog**: https://github.com/dessalines/habit-maker/compare/0.0.12...0.0.13
6 |
7 |
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/14.txt:
--------------------------------------------------------------------------------
1 | ## What's Changed in 0.0.14
2 |
3 | - Try to force remove network perm. by @dessalines in [#50](https://github.com/dessalines/habit-maker/pull/50)
4 |
5 | **Full Changelog**: https://github.com/dessalines/habit-maker/compare/0.0.13...0.0.14
6 |
7 |
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/15.txt:
--------------------------------------------------------------------------------
1 | ## What's Changed in 0.0.15
2 |
3 | - Fixing crash on switch screen. by @dessalines in [#56](https://github.com/dessalines/habit-maker/pull/56)
4 | - Translations update from Hosted Weblate by @weblate in [#53](https://github.com/dessalines/habit-maker/pull/53)
5 | - Use svg badge by @dessalines
6 | - Adding contributing.md. Fixes #51 by @dessalines in [#54](https://github.com/dessalines/habit-maker/pull/54)
7 |
8 | ## New Contributors
9 |
10 | - @weblate made their first contribution in [#53](https://github.com/dessalines/habit-maker/pull/53)
11 |
12 | **Full Changelog**: https://github.com/dessalines/habit-maker/compare/0.0.14...0.0.15
13 |
14 |
15 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/16.txt:
--------------------------------------------------------------------------------
1 | ## What's Changed in 0.0.16
2 |
3 | - Skipping weblate updates for git cliff. by @dessalines
4 | - Adding an expanded header. by @dessalines in [#72](https://github.com/dessalines/habit-maker/pull/72)
5 | - Add f-droid links, and weblate to readme. by @dessalines in [#60](https://github.com/dessalines/habit-maker/pull/60)
6 |
7 | **Full Changelog**: https://github.com/dessalines/habit-maker/compare/0.0.15...0.0.16
8 |
9 |
10 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/17.txt:
--------------------------------------------------------------------------------
1 | ## What's Changed in 0.0.17
2 |
3 | - Add hide_description to only show icon and number for info chips. by @dessalines in [#74](https://github.com/dessalines/habit-maker/pull/74)
4 |
5 | **Full Changelog**: https://github.com/dessalines/habit-maker/compare/0.0.16...0.0.17
6 |
7 |
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/18.txt:
--------------------------------------------------------------------------------
1 | ## What's Changed in 0.0.18
2 |
3 | - After checking habit, reschedule reminders, skipping today. by @dessalines in [#80](https://github.com/dessalines/habit-maker/pull/80)
4 |
5 | **Full Changelog**: https://github.com/dessalines/habit-maker/compare/0.0.17...0.0.18
6 |
7 |
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/19.txt:
--------------------------------------------------------------------------------
1 | ## What's Changed in 0.0.19
2 |
3 | - Make sure that reminders are removed when deleting habit. by @dessalines in [#83](https://github.com/dessalines/habit-maker/pull/83)
4 |
5 | **Full Changelog**: https://github.com/dessalines/habit-maker/compare/0.0.18...0.0.19
6 |
7 |
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/20.txt:
--------------------------------------------------------------------------------
1 | ## What's Changed in 0.0.20
2 |
3 | - Update habit streaks / score stats on startup. by @dessalines in [#97](https://github.com/dessalines/habit-maker/pull/97)
4 |
5 | **Full Changelog**: https://github.com/dessalines/habit-maker/compare/0.0.19...0.0.20
6 |
7 |
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/21.txt:
--------------------------------------------------------------------------------
1 | ## What's Changed in 0.0.21
2 |
3 | - Use not completed yesterday for stats updating. by @dessalines in [#100](https://github.com/dessalines/habit-maker/pull/100)
4 |
5 | **Full Changelog**: https://github.com/dessalines/habit-maker/compare/0.0.20...0.0.21
6 |
7 |
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/22.txt:
--------------------------------------------------------------------------------
1 | ## What's Changed in 0.0.22
2 |
3 | - Adding days completed info chip, and ability to hide it on home. by @dessalines in [#105](https://github.com/dessalines/habit-maker/pull/105)
4 | - Merge remote-tracking branch 'refs/remotes/origin/main' by @dessalines
5 |
6 | **Full Changelog**: https://github.com/dessalines/habit-maker/compare/0.0.21...0.0.22
7 |
8 |
9 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/23.txt:
--------------------------------------------------------------------------------
1 | ## What's Changed in 0.0.23
2 |
3 | - Fix section padding. by @dessalines in [#120](https://github.com/dessalines/habit-maker/pull/120)
4 |
5 | **Full Changelog**: https://github.com/dessalines/habit-maker/compare/0.0.22...0.0.23
6 |
7 |
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/24.txt:
--------------------------------------------------------------------------------
1 | ## What's Changed in 0.0.24
2 |
3 | - A few code fixes. by @dessalines in [#134](https://github.com/dessalines/habit-maker/pull/134)
4 |
5 | **Full Changelog**: https://github.com/dessalines/habit-maker/compare/0.0.23...0.0.24
6 |
7 |
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/25.txt:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/26.txt:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/27.txt:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/28.txt:
--------------------------------------------------------------------------------
1 | ## What's Changed in 0.0.28
2 |
3 | - Try to fix habit schedules on startup. #176 by @dessalines in [#178](https://github.com/dessalines/habit-maker/pull/178)
4 |
5 | **Full Changelog**: https://github.com/dessalines/habit-maker/compare/0.0.27...0.0.28
6 |
7 |
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/29.txt:
--------------------------------------------------------------------------------
1 | ## What's Changed in 0.0.29
2 |
3 | - Adding android lint. by @dessalines in [#182](https://github.com/dessalines/habit-maker/pull/182)
4 | - Fixing delete crash. Fixes #180 by @dessalines in [#181](https://github.com/dessalines/habit-maker/pull/181)
5 |
6 | **Full Changelog**: https://github.com/dessalines/habit-maker/compare/0.0.28...0.0.29
7 |
8 |
9 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/5.txt:
--------------------------------------------------------------------------------
1 | ## What's Changed in 0.0.5
2 |
3 | - Merge remote-tracking branch 'refs/remotes/origin/main'
4 | - Add x times info chip by @dessalines in [#13](https://github.com/dessalines/habit-maker/pull/13)
5 | - Merge remote-tracking branch 'refs/remotes/origin/main'
6 | - Add fastlane generation by @dessalines in [#12](https://github.com/dessalines/habit-maker/pull/12)
7 | - Removing pointless import.
8 |
9 | ## New Contributors
10 |
11 | - @dessalines made their first contribution in [#13](https://github.com/dessalines/habit-maker/pull/13)
12 |
13 | **Full Changelog**: https://github.com/dessalines/habit-maker/compare/0.0.4...0.0.5
14 |
15 |
16 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/6.txt:
--------------------------------------------------------------------------------
1 | ## What's Changed in 0.0.6
2 |
3 | - Adding a today completed count, displayed at the bottom of the list. by @dessalines in [#25](https://github.com/dessalines/habit-maker/pull/25)
4 | - Adding an optional context (IE when / where) field for habits. by @dessalines in [#24](https://github.com/dessalines/habit-maker/pull/24)
5 | - Using a different encouragement example. by @dessalines
6 | - Comment out f-droid links for now. by @dessalines
7 |
8 | **Full Changelog**: https://github.com/dessalines/habit-maker/compare/0.0.5...0.0.6
9 |
10 |
11 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/7.txt:
--------------------------------------------------------------------------------
1 | ## What's Changed in 0.0.7
2 |
3 | - Adding virtual complete for non-daily habits. by @dessalines in [#28](https://github.com/dessalines/habit-maker/pull/28)
4 | - Fixing readme error. by @dessalines
5 |
6 | **Full Changelog**: https://github.com/dessalines/habit-maker/compare/0.0.6...0.0.7
7 |
8 |
9 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/8.txt:
--------------------------------------------------------------------------------
1 | ## What's Changed in 0.0.8
2 |
3 | - Remembering scroll position in habits pane. by @dessalines in [#29](https://github.com/dessalines/habit-maker/pull/29)
4 |
5 | **Full Changelog**: https://github.com/dessalines/habit-maker/compare/0.0.7...0.0.8
6 |
7 |
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/9.txt:
--------------------------------------------------------------------------------
1 | ## What's Changed in 0.0.9
2 |
3 | - Adding adaptive / monochrome icons. by @dessalines in [#34](https://github.com/dessalines/habit-maker/pull/34)
4 |
5 | **Full Changelog**: https://github.com/dessalines/habit-maker/compare/0.0.8...0.0.9
6 |
7 |
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | Have you found it difficult to build new habits? Habit-Maker uses rewards and encouragements to help get over initial willpower required to form new habits.
2 |
3 | Habit-Maker game-ifies making habits by giving you rewards each time you check a habit. It shows the following progress metrics:
4 |
5 | - Streaks - The # of days you've completed your habit in a row.
6 | - Points - points for checking habits, with multipliers for continuing your streak.
7 | - % progress to your 66-day-ingrained habit
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dessalines/habit-maker/9ecb7a9fc4f067bd5af6859b195a319adb2244dc/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dessalines/habit-maker/9ecb7a9fc4f067bd5af6859b195a319adb2244dc/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dessalines/habit-maker/9ecb7a9fc4f067bd5af6859b195a319adb2244dc/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | A reward-based habit tracker for android.
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/title.txt:
--------------------------------------------------------------------------------
1 | Habit-Maker
2 |
--------------------------------------------------------------------------------
/generate_changelog.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 |
4 | # Creating the new tag and version code
5 | new_tag="$1"
6 | new_version_code="$2"
7 |
8 | # Replacing the versions in the app/build.gradle.kts
9 | app_build_gradle="app/build.gradle.kts"
10 | sed -i "s/versionCode = .*/versionCode = $new_version_code/" $app_build_gradle
11 | sed -i "s/versionName = .*/versionName = \"$new_tag\"/" $app_build_gradle
12 |
13 | # Writing to the Releases.md asset that's loaded inside the app, and the fastlane changelog
14 | tmp_file="tmp_release.md"
15 | fastlane_file="fastlane/metadata/android/en-US/changelogs/$new_version_code.txt"
16 | assets_releases="app/src/main/assets/RELEASES.md"
17 | git cliff --unreleased --tag "$new_tag" --output $tmp_file
18 | prettier -w $tmp_file
19 |
20 | cp $tmp_file $assets_releases
21 | cp $tmp_file $fastlane_file
22 | rm $tmp_file
23 |
24 | # Adding to RELEASES.md
25 | git cliff --tag "$new_tag" --output RELEASES.md
26 | prettier -w RELEASES.md
27 |
28 | # Add them all to git
29 | git add $assets_releases $fastlane_file $app_build_gradle RELEASES.md
30 |
--------------------------------------------------------------------------------
/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 -Dfile.encoding=UTF-8
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 | # Enables namespacing of each library's R class so that its R class includes only the
23 | # resources declared in the library itself and none from the library's dependencies,
24 | # thereby reducing the size of the R class for that library
25 | android.nonTransitiveRClass=true
26 | org.gradle.unsafe.configuration-cache=true
27 | android.nonFinalResIds=false
28 | org.gradle.daemon=false
29 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dessalines/habit-maker/9ecb7a9fc4f067bd5af6859b195a319adb2244dc/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionSha256Sum=845952a9d6afa783db70bb3b0effaae45ae5542ca2bb7929619e8af49cb634cf
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
5 | networkTimeout=10000
6 | validateDistributionUrl=true
7 | zipStoreBase=GRADLE_USER_HOME
8 | zipStorePath=wrapper/dists
9 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": ["config:recommended"],
4 | "schedule": ["every weekend"],
5 | "automerge": true
6 | }
7 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | plugins {
8 | id 'com.android.application' version '8.10.1'
9 | id 'com.android.library' version '8.10.1'
10 | id 'org.jetbrains.kotlin.android' version '2.1.21'
11 | }
12 | }
13 | dependencyResolutionManagement {
14 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
15 | repositories {
16 | google()
17 | mavenCentral()
18 | maven { url 'https://jitpack.io' }
19 | }
20 | }
21 | rootProject.name = "com.dessalines.habitmaker"
22 | include ':app'
23 |
--------------------------------------------------------------------------------