├── .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 | --------------------------------------------------------------------------------