├── .gitignore ├── .idea ├── .gitignore ├── .name ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── migrations.xml ├── misc.xml ├── runConfigurations.xml ├── runConfigurations │ ├── app.xml │ ├── instrumentedTests.xml │ └── tests.xml └── vcs.xml ├── COPYING ├── Gemfile ├── Makefile ├── NOTICE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro ├── schemas │ └── app.traced_it.data.local.database.AppDatabase │ │ ├── 1.json │ │ ├── 2.json │ │ └── 3.json └── src │ ├── androidTest │ └── java │ │ └── app │ │ └── traced_it │ │ ├── MigrationTest.kt │ │ └── test │ │ └── MainActivityBehaviorTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── app │ │ │ └── traced_it │ │ │ ├── Application.kt │ │ │ ├── data │ │ │ ├── EntryRepository.kt │ │ │ ├── di │ │ │ │ └── DataModule.kt │ │ │ └── local │ │ │ │ ├── database │ │ │ │ ├── AppDatabase.kt │ │ │ │ ├── EntryModel.kt │ │ │ │ ├── EntryUnit.kt │ │ │ │ └── SQLiteTools.kt │ │ │ │ └── di │ │ │ │ └── DatabaseModule.kt │ │ │ └── ui │ │ │ ├── AboutScreen.kt │ │ │ ├── MainActivity.kt │ │ │ ├── MainNavigation.kt │ │ │ ├── components │ │ │ ├── ConfirmationDialog.kt │ │ │ ├── SelectedEntryMenu.kt │ │ │ ├── TracedBottomButton.kt │ │ │ ├── TracedScaffold.kt │ │ │ ├── TracedSegmentedButton.kt │ │ │ ├── TracedTextField.kt │ │ │ └── TracedTopAppBar.kt │ │ │ ├── entry │ │ │ ├── EntryDetailDialog.kt │ │ │ ├── EntryListItem.kt │ │ │ ├── EntryListMenu.kt │ │ │ ├── EntryListScreen.kt │ │ │ ├── EntryViewModel.kt │ │ │ ├── UnitSelect.kt │ │ │ └── UnitSelectChoice.kt │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Spacing.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable │ │ ├── backspace_24px.xml │ │ └── ic_launcher_foreground.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.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 │ │ ├── resources.properties │ │ ├── values-ar │ │ └── strings.xml │ │ ├── values-b+sr+Cyrl │ │ └── strings.xml │ │ ├── values-bg │ │ └── strings.xml │ │ ├── values-bn │ │ └── strings.xml │ │ ├── values-cs │ │ └── strings.xml │ │ ├── values-de │ │ └── strings.xml │ │ ├── values-fr │ │ └── strings.xml │ │ ├── values-hu │ │ └── strings.xml │ │ ├── values-iw │ │ └── strings.xml │ │ ├── values-pl │ │ └── strings.xml │ │ ├── values-pt-rBR │ │ └── strings.xml │ │ ├── values-ru │ │ └── strings.xml │ │ ├── values-sl │ │ └── strings.xml │ │ ├── values-uk │ │ └── strings.xml │ │ ├── values-v29 │ │ └── themes.xml │ │ ├── values │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ └── locales_config.xml │ └── test │ └── java │ └── app │ └── traced_it │ ├── data │ └── local │ │ └── database │ │ └── SQLiteToolsTest.kt │ └── ui │ └── entry │ └── EntryViewModelTest.kt ├── build.gradle.kts ├── docs └── icon-54.png ├── fastlane ├── Fastfile └── metadata │ └── android │ ├── ar │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── cs-CZ │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── de-DE │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── en-US │ ├── changelogs │ │ ├── 10.txt │ │ ├── 11.txt │ │ ├── 12.txt │ │ ├── 6.txt │ │ ├── 7.txt │ │ ├── 8.txt │ │ └── 9.txt │ ├── full_description.txt │ ├── images │ │ ├── featureGraphic.png │ │ ├── icon.png │ │ ├── phoneScreenshots │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ └── 4.png │ │ ├── sevenInchScreenshots │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ └── 4.png │ │ └── tenInchScreenshots │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ └── 4.png │ ├── short_description.txt │ └── title.txt │ ├── fr-FR │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── iw-IL │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── pl-PL │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── pt-BR │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ └── ru-RU │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .DS_Store 3 | .attach_pid* 4 | .cxx 5 | .externalNativeBuild 6 | .gradle 7 | .kotlin 8 | /.idea/androidTestResultsUserPreferences.xml 9 | /.idea/assetWizardSettings.xml 10 | /.idea/caches 11 | /.idea/deploymentTargetSelector.xml 12 | /.idea/deviceManager.xml 13 | /.idea/dictionaries 14 | /.idea/libraries 15 | /.idea/modules.xml 16 | /.idea/navEditor.xml 17 | /.idea/workspace.xml 18 | /build 19 | /captures 20 | /local.properties 21 | README.html 22 | fastlane/Appfile 23 | fastlane/README.md 24 | fastlane/report.xml 25 | local.properties 26 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | traced it -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 10 | 11 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 57 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/runConfigurations/app.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 44 | -------------------------------------------------------------------------------- /.idea/runConfigurations/instrumentedTests.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 35 | -------------------------------------------------------------------------------- /.idea/runConfigurations/tests.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | false 19 | true 20 | false 21 | true 22 | 23 | 24 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | build: | check-env 3 | -rm -r app/build 4 | ./gradlew assembleRelease 5 | zipalign -v -p 4 \ 6 | app/build/outputs/apk/release/app-release-unsigned.apk \ 7 | app/build/outputs/apk/release/app-release-unsigned-aligned.apk 8 | apksigner sign \ 9 | --ks "$(STORE_FILE)" \ 10 | --ks-pass env:STORE_PASSWORD \ 11 | --ks-key-alias "$(KEY_ALIAS)" \ 12 | --key-pass env:KEY_PASSWORD \ 13 | --out \ 14 | app/build/outputs/apk/release/app-release.apk \ 15 | app/build/outputs/apk/release/app-release-unsigned-aligned.apk 16 | 17 | .PHONY: bundle 18 | bundle: | check-env 19 | ./gradlew bundleRelease \ 20 | -Pandroid.injected.signing.store.file="$(STORE_FILE)" \ 21 | -Pandroid.injected.signing.store.password="$(STORE_PASSWORD)" \ 22 | -Pandroid.injected.signing.key.alias="$(KEY_ALIAS)" \ 23 | -Pandroid.injected.signing.key.password="$(KEY_PASSWORD)" 24 | 25 | .PHONY: check-env 26 | check-env: 27 | ifeq ($(STORE_FILE),) 28 | @echo "Variable STORE_FILE is not set." 29 | @echo "Example: STORE_FILE=path/to/keystore.js" 30 | exit 1 31 | endif 32 | ifeq ($(STORE_PASSWORD),) 33 | @echo "Variable STORE_PASSWORD is not set." 34 | @echo "Example: STORE_PASSWORD=mypassword" 35 | exit 1 36 | endif 37 | ifeq ($(KEY_ALIAS),) 38 | @echo "Variable KEY_ALIAS is not set." 39 | @echo "Example: KEY_ALIAS=com.example.android" 40 | exit 1 41 | endif 42 | ifeq ($(KEY_PASSWORD),) 43 | @echo "Variable KEY_PASSWORD is not set." 44 | @echo "Example: KEY_PASSWORD=mypassword" 45 | exit 1 46 | endif 47 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2024-2025 traced it contributors 2 | 3 | This program is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation, either version 3 of the License, or 6 | (at your option) any later version. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU General Public License for more details. 12 | 13 | You should have received a copy of the GNU General Public License 14 | along with this program. If not, see . 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![](./docs/icon-54.png) traced it 2 | 3 | A simple app for short text notes, with a prominent timestamp for each entry. 4 | Add quantities to your notes using preset formats (S, M, XL, 1/4, 1/2, 3/4) or 5 | custom numbers. 6 | 7 | The uncomplicated interface makes the app easy to use even when you’re busy or 8 | distracted. 9 | 10 | 📢 **We’re looking for testers! If you use the Google Play store and would like 11 | to help us, please submit this [Google 12 | Form](https://docs.google.com/forms/d/e/1FAIpQLSe4O_-f0mjtfLFWXkf5zThnPeEb8hQU-sHt_HXwKbOc6X02eg/viewform?usp=header).** 13 | 14 | [Get it on F-Droid](https://f-droid.org/packages/app.traced_it/) 17 | [Get APK from GitHub](https://github.com/traced-it/traced-it-android/releases/latest/download/app.traced_it.apk) 21 | [Get it on Izzy on Droid](https://apt.izzysoft.de/packages/app.traced_it) 24 | 25 | ## Perfect for 26 | 27 | - Journaling 28 | - Tracking daily routines 29 | - Recording workouts 30 | - Logging supplements and water intake 31 | - Measuring cooking ingredients 32 | - Noting simple lab results 33 | 34 | ## Features 35 | 36 | - Add text notes with an optional amount (S, M, XL, 1/4, 1/2, 3/4, 5.813…). 37 | - See all your notes with the time elapsed since you wrote each note. 38 | - Tap the + button to add a new note with the same text as an existing note. 39 | - Edit a note, copy it to clipboard, delete it, delete all notes. 40 | - Export all notes as a spreadsheet file (CSV format) and import them. 41 | - Filter notes and export the filtered notes. 42 | 43 | ## Privacy & security 44 | 45 | - The app doesn’t use the internet at all – your notes are stored on your phone 46 | only. 47 | - The app doesn’t access any information about you and doesn’t collect usage 48 | data. 49 | - No ads 50 | - Free and open-source 51 | 52 | [Screenshot of a list of notes](./fastlane/metadata/android/en-US/images/phoneScreenshots/1.png) 55 | [Screenshot of the interface to add new note](./fastlane/metadata/android/en-US/images/phoneScreenshots/2.png) 58 | [Screenshot of the interface to edit or delete a note](./fastlane/metadata/android/en-US/images/phoneScreenshots/3.png) 61 | [Screenshot of the interface to export notes](./fastlane/metadata/android/en-US/images/phoneScreenshots/4.png) 64 | 65 | ## APK signature 66 | 67 | The APK released on GitHub and F-Droid is signed with a certificate with the 68 | following SHA-256 fingerprint: 69 | 70 | ``` 71 | d102417bced85dfb23c49ec1e1915963d17cf3d4c028b754877943cc9b865aa9 72 | ``` 73 | 74 | ## Contributing 75 | 76 | We welcome your contributions! To help us organize the work, please start by 77 | creating a [GitHub issue](https://github.com/traced-it/traced-it-android/issues) 78 | for the bug or feature you'd like to work on. 79 | 80 | To translate the app, register at 81 | [Toolate](https://toolate.othing.xyz/projects/traced-it/), which is an instance 82 | of the Weblate translation app that we use. Toolate will submit your translation 83 | as a GitHub pull request. 84 | 85 | ## License 86 | 87 | Distributed under GNU General Public License version 3 or later. See 88 | [COPYING](./COPYING). 89 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release 3 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.kotlin.android) 4 | alias(libs.plugins.kotlin.kapt) 5 | alias(libs.plugins.hilt.gradle) 6 | alias(libs.plugins.ksp) 7 | alias(libs.plugins.compose.compiler) 8 | } 9 | 10 | android { 11 | namespace = "app.traced_it" 12 | compileSdk = 35 13 | 14 | defaultConfig { 15 | applicationId = "app.traced_it" 16 | minSdk = 25 17 | targetSdk = 35 18 | versionCode = 11 19 | versionName = "1.4.0" 20 | 21 | vectorDrawables { 22 | useSupportLibrary = true 23 | } 24 | 25 | // Enable room auto-migrations 26 | ksp { 27 | arg("room.schemaLocation", "$projectDir/schemas") 28 | } 29 | 30 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 31 | 32 | // The following argument makes the Android Test Orchestrator run its 33 | // "pm clear" command after each test invocation. This command ensures 34 | // that the app's state is completely cleared between tests. 35 | testInstrumentationRunnerArguments += mapOf( 36 | "clearPackageData" to "true", 37 | ) 38 | 39 | // Specify supported locales manually instead of setting 40 | // `androidResources.generateLocalConfig = true`, so that languages 41 | // whose translations are in progress don't appear among the app's 42 | // supported languages. 43 | resourceConfigurations += listOf( 44 | "ar", 45 | "cs", 46 | "de", 47 | "en", 48 | "fr", 49 | "iw", 50 | "pl", 51 | "pt-rBR", 52 | "ru" 53 | ) 54 | } 55 | 56 | buildTypes { 57 | getByName("release") { 58 | isMinifyEnabled = true 59 | isShrinkResources = true 60 | proguardFiles( 61 | getDefaultProguardFile("proguard-android-optimize.txt"), 62 | "proguard-rules.pro" 63 | ) 64 | } 65 | getByName("debug") { 66 | applicationIdSuffix = ".debug" 67 | } 68 | create("demo") { 69 | initWith(getByName("debug")) 70 | } 71 | } 72 | compileOptions { 73 | sourceCompatibility = JavaVersion.VERSION_17 74 | targetCompatibility = JavaVersion.VERSION_17 75 | } 76 | kotlinOptions { 77 | jvmTarget = "17" 78 | } 79 | buildFeatures { 80 | aidl = false 81 | buildConfig = true 82 | compose = true 83 | renderScript = false 84 | shaders = false 85 | } 86 | composeOptions { 87 | kotlinCompilerExtensionVersion = 88 | libs.versions.androidxComposeCompiler.get() 89 | } 90 | packaging { 91 | resources { 92 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 93 | } 94 | } 95 | sourceSets { 96 | // Adds exported schema location as test app assets. 97 | getByName("androidTest").assets.srcDir("$projectDir/schemas") 98 | } 99 | @Suppress("UnstableApiUsage") 100 | testOptions { 101 | execution = "ANDROIDX_TEST_ORCHESTRATOR" 102 | } 103 | } 104 | 105 | dependencies { 106 | 107 | val composeBom = platform(libs.androidx.compose.bom) 108 | implementation(composeBom) 109 | androidTestImplementation(composeBom) 110 | 111 | // Core Android dependencies 112 | implementation(libs.androidx.core.ktx) 113 | implementation(libs.androidx.lifecycle.runtime.ktx) 114 | implementation(libs.androidx.activity.compose) 115 | 116 | // Hilt Dependency Injection 117 | implementation(libs.hilt.android) 118 | kapt(libs.hilt.compiler) 119 | // Hilt and instrumented tests. 120 | androidTestImplementation(libs.hilt.android.testing) 121 | kaptAndroidTest(libs.hilt.android.compiler) 122 | // Hilt and Robolectric tests. 123 | testImplementation(libs.hilt.android.testing) 124 | kaptTest(libs.hilt.android.compiler) 125 | 126 | // Arch Components 127 | implementation(libs.androidx.lifecycle.runtime.compose) 128 | implementation(libs.androidx.lifecycle.viewmodel.compose) 129 | implementation(libs.androidx.navigation.compose) 130 | implementation(libs.androidx.hilt.navigation.compose) 131 | implementation(libs.androidx.room.testing) 132 | implementation(libs.androidx.room.runtime) 133 | implementation(libs.androidx.room.ktx) 134 | ksp(libs.androidx.room.compiler) 135 | 136 | // Compose 137 | implementation(libs.androidx.compose.ui) 138 | implementation(libs.androidx.compose.ui.tooling.preview) 139 | implementation(libs.androidx.compose.material3) 140 | // Tooling 141 | debugImplementation(libs.androidx.compose.ui.tooling) 142 | // Instrumented tests 143 | androidTestImplementation(libs.androidx.compose.ui.test.junit4) 144 | debugImplementation(libs.androidx.compose.ui.test.manifest) 145 | 146 | // Local tests: jUnit, coroutines, Android runner 147 | testImplementation(libs.junit) 148 | testImplementation(libs.kotlinx.coroutines.test) 149 | 150 | // Instrumented tests: jUnit rules and runners 151 | androidTestImplementation(libs.androidx.test.core) 152 | androidTestImplementation(libs.androidx.test.ext.junit) 153 | androidTestImplementation(libs.androidx.test.runner) 154 | androidTestImplementation(libs.androidx.test.uiautomator) 155 | androidTestUtil(libs.androidx.test.orchestrator) 156 | 157 | // Paging 158 | implementation(libs.androidx.paging.runtime) 159 | implementation(libs.androidx.paging.compose) 160 | implementation(libs.androidx.paging.testing) 161 | implementation(libs.androidx.room.paging) 162 | 163 | // Third-party libs 164 | implementation(libs.commons.csv) 165 | testImplementation(libs.mockito.kotlin) 166 | } 167 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/schemas/app.traced_it.data.local.database.AppDatabase/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 1, 5 | "identityHash": "de5360c9b49503232442947d01dff1b3", 6 | "entities": [ 7 | { 8 | "tableName": "Entry", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`amount` REAL NOT NULL, `amountUnit` TEXT NOT NULL, `content` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", 10 | "fields": [ 11 | { 12 | "fieldPath": "amount", 13 | "columnName": "amount", 14 | "affinity": "REAL", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "amountUnit", 19 | "columnName": "amountUnit", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "content", 25 | "columnName": "content", 26 | "affinity": "TEXT", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "createdAt", 31 | "columnName": "createdAt", 32 | "affinity": "INTEGER", 33 | "notNull": true 34 | }, 35 | { 36 | "fieldPath": "uid", 37 | "columnName": "uid", 38 | "affinity": "INTEGER", 39 | "notNull": true 40 | } 41 | ], 42 | "primaryKey": { 43 | "autoGenerate": true, 44 | "columnNames": [ 45 | "uid" 46 | ] 47 | }, 48 | "indices": [], 49 | "foreignKeys": [] 50 | } 51 | ], 52 | "views": [], 53 | "setupQueries": [ 54 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 55 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'de5360c9b49503232442947d01dff1b3')" 56 | ] 57 | } 58 | } -------------------------------------------------------------------------------- /app/schemas/app.traced_it.data.local.database.AppDatabase/2.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 2, 5 | "identityHash": "5bc48f1182f59b8210a913ff0bdca2dd", 6 | "entities": [ 7 | { 8 | "tableName": "Entry", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`amount` REAL NOT NULL, `amountUnit` TEXT NOT NULL, `content` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `deleted` INTEGER NOT NULL DEFAULT 0, `uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", 10 | "fields": [ 11 | { 12 | "fieldPath": "amount", 13 | "columnName": "amount", 14 | "affinity": "REAL", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "amountUnit", 19 | "columnName": "amountUnit", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "content", 25 | "columnName": "content", 26 | "affinity": "TEXT", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "createdAt", 31 | "columnName": "createdAt", 32 | "affinity": "INTEGER", 33 | "notNull": true 34 | }, 35 | { 36 | "fieldPath": "deleted", 37 | "columnName": "deleted", 38 | "affinity": "INTEGER", 39 | "notNull": true, 40 | "defaultValue": "0" 41 | }, 42 | { 43 | "fieldPath": "uid", 44 | "columnName": "uid", 45 | "affinity": "INTEGER", 46 | "notNull": true 47 | } 48 | ], 49 | "primaryKey": { 50 | "autoGenerate": true, 51 | "columnNames": [ 52 | "uid" 53 | ] 54 | }, 55 | "indices": [], 56 | "foreignKeys": [] 57 | } 58 | ], 59 | "views": [], 60 | "setupQueries": [ 61 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 62 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5bc48f1182f59b8210a913ff0bdca2dd')" 63 | ] 64 | } 65 | } -------------------------------------------------------------------------------- /app/schemas/app.traced_it.data.local.database.AppDatabase/3.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 3, 5 | "identityHash": "7d0ee3764f40286c144552d40bb37a80", 6 | "entities": [ 7 | { 8 | "tableName": "Entry", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`amount` REAL NOT NULL, `amountUnit` TEXT NOT NULL, `content` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `deleted` INTEGER NOT NULL DEFAULT 0, `uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", 10 | "fields": [ 11 | { 12 | "fieldPath": "amount", 13 | "columnName": "amount", 14 | "affinity": "REAL", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "amountUnit", 19 | "columnName": "amountUnit", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "content", 25 | "columnName": "content", 26 | "affinity": "TEXT", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "createdAt", 31 | "columnName": "createdAt", 32 | "affinity": "INTEGER", 33 | "notNull": true 34 | }, 35 | { 36 | "fieldPath": "deleted", 37 | "columnName": "deleted", 38 | "affinity": "INTEGER", 39 | "notNull": true, 40 | "defaultValue": "0" 41 | }, 42 | { 43 | "fieldPath": "uid", 44 | "columnName": "uid", 45 | "affinity": "INTEGER", 46 | "notNull": true 47 | } 48 | ], 49 | "primaryKey": { 50 | "autoGenerate": true, 51 | "columnNames": [ 52 | "uid" 53 | ] 54 | } 55 | }, 56 | { 57 | "tableName": "entry_fts", 58 | "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`content` TEXT NOT NULL, content=`Entry`)", 59 | "fields": [ 60 | { 61 | "fieldPath": "content", 62 | "columnName": "content", 63 | "affinity": "TEXT", 64 | "notNull": true 65 | } 66 | ], 67 | "primaryKey": { 68 | "autoGenerate": false, 69 | "columnNames": [] 70 | }, 71 | "ftsVersion": "FTS4", 72 | "ftsOptions": { 73 | "tokenizer": "simple", 74 | "tokenizerArgs": [], 75 | "contentTable": "Entry", 76 | "languageIdColumnName": "", 77 | "matchInfo": "FTS4", 78 | "notIndexedColumns": [], 79 | "prefixSizes": [], 80 | "preferredOrder": "ASC" 81 | }, 82 | "contentSyncTriggers": [ 83 | "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_entry_fts_BEFORE_UPDATE BEFORE UPDATE ON `Entry` BEGIN DELETE FROM `entry_fts` WHERE `docid`=OLD.`rowid`; END", 84 | "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_entry_fts_BEFORE_DELETE BEFORE DELETE ON `Entry` BEGIN DELETE FROM `entry_fts` WHERE `docid`=OLD.`rowid`; END", 85 | "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_entry_fts_AFTER_UPDATE AFTER UPDATE ON `Entry` BEGIN INSERT INTO `entry_fts`(`docid`, `content`) VALUES (NEW.`rowid`, NEW.`content`); END", 86 | "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_entry_fts_AFTER_INSERT AFTER INSERT ON `Entry` BEGIN INSERT INTO `entry_fts`(`docid`, `content`) VALUES (NEW.`rowid`, NEW.`content`); END" 87 | ] 88 | } 89 | ], 90 | "setupQueries": [ 91 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 92 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7d0ee3764f40286c144552d40bb37a80')" 93 | ] 94 | } 95 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/app/traced_it/MigrationTest.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it 2 | 3 | import androidx.room.Room 4 | import androidx.room.testing.MigrationTestHelper 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import androidx.test.platform.app.InstrumentationRegistry 7 | import app.traced_it.data.local.database.AppDatabase 8 | import org.junit.Rule 9 | import org.junit.Test 10 | import org.junit.runner.RunWith 11 | import java.io.IOException 12 | 13 | @RunWith(AndroidJUnit4::class) 14 | class MigrationTest { 15 | private val testDb = "migration-test" 16 | 17 | @get:Rule 18 | val helper: MigrationTestHelper = MigrationTestHelper( 19 | InstrumentationRegistry.getInstrumentation(), 20 | AppDatabase::class.java 21 | ) 22 | 23 | @Test 24 | @Throws(IOException::class) 25 | fun migrateAll() { 26 | // Create the earliest version of the database. 27 | helper.createDatabase(testDb, 1).apply { 28 | close() 29 | } 30 | 31 | // Open latest version of the database. Room validates the schema 32 | // once all migrations execute. 33 | Room.databaseBuilder( 34 | InstrumentationRegistry.getInstrumentation().targetContext, 35 | AppDatabase::class.java, 36 | testDb 37 | ).build().apply { 38 | openHelper.writableDatabase.close() 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traced-it/traced-it-android/1a121b505e141b15b3132f1198c2180b03b7c73b/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/app/traced_it/Application.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it 2 | 3 | import android.app.Application as AppApplication 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class Application : AppApplication() 8 | -------------------------------------------------------------------------------- /app/src/main/java/app/traced_it/data/EntryRepository.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it.data 2 | 3 | import androidx.paging.PagingSource 4 | import app.traced_it.data.local.database.Entry 5 | import app.traced_it.data.local.database.EntryDao 6 | import app.traced_it.data.local.database.createFullTextQueryExpression 7 | import kotlinx.coroutines.flow.Flow 8 | import javax.inject.Inject 9 | 10 | interface EntryRepository { 11 | fun count(): Flow 12 | 13 | suspend fun getByCreatedAt(createdAt: Long): Entry? 14 | 15 | fun getLatest(): Flow 16 | 17 | fun filter(unsafeQuery: String = ""): PagingSource 18 | 19 | suspend fun insert(entry: Entry): Long 20 | 21 | suspend fun update(vararg entries: Entry) 22 | 23 | suspend fun delete(uid: Int) 24 | 25 | suspend fun restore(uid: Int) 26 | 27 | suspend fun deleteAll() 28 | 29 | suspend fun restoreAll() 30 | 31 | suspend fun cleanupDeleted() 32 | } 33 | 34 | class DefaultEntryRepository @Inject constructor( 35 | private val entryDao: EntryDao, 36 | ) : EntryRepository { 37 | override fun count(): Flow = entryDao.count() 38 | 39 | override fun filter(filterQuery: String): PagingSource = 40 | if (filterQuery.isNotEmpty()) { 41 | entryDao.search(createFullTextQueryExpression(filterQuery)) 42 | } else { 43 | entryDao.getAll() 44 | } 45 | 46 | override suspend fun getByCreatedAt(createdAt: Long): Entry? = 47 | entryDao.getByCreatedAt(createdAt) 48 | 49 | override fun getLatest(): Flow = entryDao.getLatest() 50 | 51 | override suspend fun insert(entry: Entry): Long = entryDao.insert(entry) 52 | 53 | override suspend fun update(vararg entries: Entry) = 54 | entryDao.update(*entries) 55 | 56 | override suspend fun delete(uid: Int) = entryDao.delete(uid) 57 | 58 | override suspend fun restore(uid: Int) = entryDao.restore(uid) 59 | 60 | override suspend fun deleteAll() = entryDao.deleteAll() 61 | 62 | override suspend fun restoreAll() = entryDao.restoreAll() 63 | 64 | override suspend fun cleanupDeleted() = entryDao.cleanupDeleted() 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/app/traced_it/data/local/database/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it.data.local.database 2 | 3 | import androidx.room.* 4 | import androidx.room.migration.AutoMigrationSpec 5 | import androidx.sqlite.db.SupportSQLiteDatabase 6 | 7 | class Converters { 8 | @TypeConverter 9 | fun unitIdToUnit(unitId: String): EntryUnit? = 10 | units.find { unit -> unit.id == unitId } 11 | 12 | @TypeConverter 13 | fun unitToUnitId(unit: EntryUnit): String = unit.id 14 | } 15 | 16 | @Database( 17 | entities = [Entry::class, EntryFTS::class], 18 | version = 3, 19 | autoMigrations = [ 20 | AutoMigration(from = 1, to = 2), 21 | AutoMigration( 22 | from = 2, 23 | to = 3, 24 | spec = AppDatabase.AutoMigration2To3::class, 25 | ), 26 | ], 27 | ) 28 | @TypeConverters(Converters::class) 29 | abstract class AppDatabase : RoomDatabase() { 30 | abstract fun entryDao(): EntryDao 31 | 32 | class AutoMigration2To3 : AutoMigrationSpec { 33 | override fun onPostMigrate(db: SupportSQLiteDatabase) { 34 | super.onPostMigrate(db) 35 | db.execSQL( 36 | "INSERT INTO entry_fts(entry_fts) VALUES ('rebuild')" 37 | ) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/app/traced_it/data/local/database/EntryModel.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it.data.local.database 2 | 3 | import android.content.Context 4 | import android.text.format.DateUtils 5 | import androidx.paging.PagingSource 6 | import androidx.room.* 7 | import app.traced_it.R 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlin.time.Duration.Companion.milliseconds 10 | 11 | @Entity 12 | data class Entry( 13 | val amount: Double = 0.0, 14 | val amountUnit: EntryUnit = noneUnit, 15 | val content: String = "", 16 | val createdAt: Long = System.currentTimeMillis(), 17 | @ColumnInfo(defaultValue = "0") var deleted: Boolean = false, 18 | @PrimaryKey(autoGenerate = true) val uid: Int = 0, 19 | ) { 20 | fun formatContentWithAmount(context: Context): String = 21 | buildString { 22 | append(content) 23 | if (amountUnit != noneUnit) { 24 | append(" (") 25 | append(amountUnit.format(context, amount)) 26 | append(")") 27 | } 28 | } 29 | 30 | fun format(context: Context): String = 31 | context.resources.getString( 32 | R.string.entry_formatted_content_with_amount_and_created_at, 33 | formatContentWithAmount(context), 34 | DateUtils.formatDateTime( 35 | context, 36 | createdAt, 37 | DateUtils.FORMAT_SHOW_TIME or 38 | DateUtils.FORMAT_SHOW_YEAR or 39 | DateUtils.FORMAT_SHOW_DATE, 40 | ) 41 | ) 42 | 43 | fun isSameDay(context: Context, otherEntry: Entry?): Boolean = 44 | otherEntry !== null && DateUtils.formatDateTime( 45 | context, 46 | createdAt, 47 | DateUtils.FORMAT_SHOW_YEAR or DateUtils.FORMAT_SHOW_DATE 48 | ) == DateUtils.formatDateTime( 49 | context, 50 | otherEntry.createdAt, 51 | DateUtils.FORMAT_SHOW_YEAR or DateUtils.FORMAT_SHOW_DATE 52 | ) 53 | 54 | fun formatTime(context: Context, now: Long): String = 55 | (now - createdAt).milliseconds.toComponents { hours, minutes, seconds, _ -> 56 | if (hours >= 24 || hours < 0 || minutes < 0 || seconds < 0) { 57 | formatExactTime(context) 58 | } else if (hours == 0L) { 59 | if (minutes == 0) { 60 | context.resources.getString( 61 | R.string.list_item_time_ago_seconds, 62 | seconds, 63 | ) 64 | } else { 65 | context.resources.getString( 66 | R.string.list_item_time_ago_minutes, 67 | minutes, 68 | ) 69 | } 70 | } else { 71 | context.resources.getString( 72 | R.string.list_item_time_ago_hours_and_minutes, 73 | hours, 74 | minutes, 75 | ) 76 | } 77 | } 78 | 79 | fun formatExactTime(context: Context): String = 80 | context.resources.getString( 81 | R.string.list_item_time_at, 82 | DateUtils.formatDateTime( 83 | context, 84 | createdAt, 85 | DateUtils.FORMAT_SHOW_TIME, 86 | ), 87 | ) 88 | 89 | fun getHeader(context: Context, prevEntry: Entry?): String? = 90 | if (prevEntry == null && !DateUtils.isToday(createdAt) || 91 | prevEntry != null && !isSameDay(context, prevEntry) 92 | ) { 93 | DateUtils.formatDateTime( 94 | context, 95 | createdAt, 96 | DateUtils.FORMAT_SHOW_DATE 97 | ) 98 | } else { 99 | null 100 | } 101 | } 102 | 103 | @Entity(tableName = "entry_fts") 104 | @Fts4(contentEntity = Entry::class) 105 | data class EntryFTS( 106 | @ColumnInfo(name = "content") 107 | val content: String 108 | ) 109 | 110 | @Dao 111 | interface EntryDao { 112 | @Query("SELECT COUNT(uid) FROM entry WHERE NOT deleted") 113 | fun count(): Flow 114 | 115 | @Query("SELECT * FROM entry WHERE NOT deleted ORDER BY createdAt DESC") 116 | fun getAll(): PagingSource 117 | 118 | @Query("SELECT * FROM entry WHERE createdAt = :createdAt AND NOT deleted") 119 | suspend fun getByCreatedAt(createdAt: Long): Entry? 120 | 121 | @Query("SELECT * FROM entry WHERE NOT deleted AND amountUnit != 'NONE' ORDER BY createdAt DESC LIMIT 1") 122 | fun getLatest(): Flow 123 | 124 | @Query( 125 | """ 126 | SELECT * FROM entry 127 | JOIN entry_fts ON entry_fts.rowid = entry.uid 128 | WHERE entry_fts MATCH :fullTextQueryExpression 129 | ORDER BY createdAt DESC 130 | """ 131 | ) 132 | fun search(fullTextQueryExpression: String): PagingSource 133 | 134 | @Insert 135 | suspend fun insert(entry: Entry): Long 136 | 137 | @Update 138 | suspend fun update(vararg entries: Entry) 139 | 140 | @Query("UPDATE entry SET deleted = 1 WHERE uid = :uid") 141 | suspend fun delete(uid: Int) 142 | 143 | @Query("UPDATE entry SET deleted = 0 WHERE uid = :uid") 144 | suspend fun restore(uid: Int) 145 | 146 | @Query("UPDATE entry SET deleted = 1") 147 | suspend fun deleteAll() 148 | 149 | @Query("UPDATE entry SET deleted = 0 WHERE deleted = 1") 150 | suspend fun restoreAll() 151 | 152 | @Query("DELETE FROM entry WHERE deleted = 1") 153 | suspend fun cleanupDeleted() 154 | } 155 | -------------------------------------------------------------------------------- /app/src/main/java/app/traced_it/data/local/database/EntryUnit.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it.data.local.database 2 | 3 | import android.content.Context 4 | import androidx.compose.ui.text.AnnotatedString 5 | import androidx.compose.ui.text.fromHtml 6 | import app.traced_it.R 7 | import java.text.NumberFormat 8 | import java.text.ParseException 9 | 10 | data class EntryUnitChoice( 11 | val value: Double, 12 | val nameResId: Int, 13 | val htmlResId: Int? = null, 14 | ) { 15 | fun format(context: Context): String = 16 | context.resources.getString(nameResId) 17 | 18 | fun formatHtml(context: Context): AnnotatedString? = 19 | htmlResId?.let { 20 | AnnotatedString.fromHtml(context.resources.getString(it)) 21 | } 22 | } 23 | 24 | data class EntryUnit( 25 | val id: String, 26 | val nameResId: Int, 27 | val defaultValue: Double = 0.0, 28 | val choices: List = listOf(), 29 | ) { 30 | private val numberFormat = NumberFormat.getNumberInstance() 31 | private val placeholderNumberFormat = NumberFormat.getNumberInstance() 32 | .apply { minimumFractionDigits = 1 } 33 | 34 | val placeholder: String 35 | get() = placeholderNumberFormat.format(defaultValue) 36 | 37 | fun format(context: Context, value: Double): String = 38 | choices.find { it.value == value } 39 | ?.format(context) 40 | ?: numberFormat.format(value) 41 | 42 | fun formatHtml(context: Context, value: Double): AnnotatedString? = 43 | choices.find { it.value == value } 44 | ?.formatHtml(context) 45 | 46 | fun parse(context: Context, value: String): Double = 47 | choices.find { context.resources.getString(it.nameResId) == value } 48 | ?.value 49 | ?: try { 50 | numberFormat.parse(value)?.toDouble() 51 | } catch (_: ParseException) { 52 | null 53 | } ?: defaultValue 54 | 55 | fun serialize(value: Double): String = value.toString() 56 | 57 | fun deserialize(value: String): Double = 58 | value.toDoubleOrNull() ?: defaultValue 59 | } 60 | 61 | val noneUnit = EntryUnit( 62 | id = "NONE", 63 | nameResId = R.string.entry_unit_none_name, 64 | choices = listOf( 65 | EntryUnitChoice(0.0, R.string.entry_unit_none_choice_empty), 66 | ), 67 | ) 68 | val clothingSizeUnit = EntryUnit( 69 | id = "CLOTHING_SIZE", 70 | nameResId = R.string.entry_unit_clothing_name, 71 | choices = listOf( 72 | EntryUnitChoice(0.0, R.string.entry_unit_clothing_choice_xs), 73 | EntryUnitChoice(1.0, R.string.entry_unit_clothing_choice_s), 74 | EntryUnitChoice(2.0, R.string.entry_unit_clothing_choice_m), 75 | EntryUnitChoice(3.0, R.string.entry_unit_clothing_choice_l), 76 | EntryUnitChoice(4.0, R.string.entry_unit_clothing_choice_xl), 77 | ), 78 | ) 79 | val fractionUnit = EntryUnit( 80 | id = "FRACTION", 81 | nameResId = R.string.entry_unit_fraction_name, 82 | choices = listOf( 83 | EntryUnitChoice( 84 | 0.25, 85 | R.string.entry_unit_fraction_choice_one_quarter, 86 | R.string.entry_unit_fraction_choice_one_quarter_html, 87 | ), 88 | EntryUnitChoice( 89 | 0.333, 90 | R.string.entry_unit_fraction_choice_one_third, 91 | R.string.entry_unit_fraction_choice_one_third_html, 92 | ), 93 | EntryUnitChoice( 94 | 0.5, 95 | R.string.entry_unit_fraction_choice_one_half, 96 | R.string.entry_unit_fraction_choice_one_half_html, 97 | ), 98 | EntryUnitChoice( 99 | 0.75, 100 | R.string.entry_unit_fraction_choice_three_quarters, 101 | R.string.entry_unit_fraction_choice_three_quarters_html, 102 | ), 103 | EntryUnitChoice( 104 | 1.0, 105 | R.string.entry_unit_fraction_choice_whole, 106 | R.string.entry_unit_fraction_choice_whole_html, 107 | ), 108 | ), 109 | ) 110 | val smallNumbersChoiceUnit = EntryUnit( 111 | id = "SMALL_NUMBERS_CHOICE", 112 | nameResId = R.string.entry_unit_portion_name, 113 | choices = listOf( 114 | EntryUnitChoice(1.0, R.string.entry_unit_portion_choice_1), 115 | EntryUnitChoice(2.0, R.string.entry_unit_portion_choice_2), 116 | EntryUnitChoice(3.0, R.string.entry_unit_portion_choice_3), 117 | EntryUnitChoice(4.0, R.string.entry_unit_portion_choice_4), 118 | EntryUnitChoice(5.0, R.string.entry_unit_portion_choice_5), 119 | ), 120 | ) 121 | val doubleUnit = EntryUnit( 122 | id = "DOUBLE", 123 | nameResId = R.string.entry_unit_double_name, 124 | ) 125 | val units: List = listOf( 126 | noneUnit, 127 | clothingSizeUnit, 128 | fractionUnit, 129 | smallNumbersChoiceUnit, 130 | doubleUnit, 131 | ) 132 | val defaultVisibleUnit = clothingSizeUnit 133 | val visibleUnits: List = listOf( 134 | defaultVisibleUnit, 135 | fractionUnit, 136 | doubleUnit, 137 | ) 138 | -------------------------------------------------------------------------------- /app/src/main/java/app/traced_it/data/local/database/SQLiteTools.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it.data.local.database 2 | 3 | val queryRegex: Regex = """(AND|OR|NOT)|([-^]?")([^"]*)(")|([-^]?)(\S+)""".toRegex() 4 | 5 | private fun quoteTerm(unsafeTerm: String): String = unsafeTerm 6 | .replace("\"", "\"\"") 7 | .replace(":", "\\:") 8 | 9 | fun createFullTextQueryExpression(unsafeQuery: String): String = 10 | queryRegex.findAll(unsafeQuery).map { 11 | when { 12 | // Operator 13 | it.groupValues[1].isNotEmpty() -> it.value 14 | 15 | // Phrase 16 | it.groupValues[2].isNotEmpty() -> it.groupValues[2] + quoteTerm(it.groupValues[3]) + it.groupValues[4] 17 | 18 | // Term with first or unary operator 19 | it.groupValues[5].isNotEmpty() -> it.groupValues[5] + quoteTerm(it.groupValues[6]) + "*" 20 | 21 | // Term 22 | else -> "*" + quoteTerm(it.groupValues[0]) + "*" 23 | } 24 | } 25 | .joinToString(" ") 26 | -------------------------------------------------------------------------------- /app/src/main/java/app/traced_it/data/local/di/DatabaseModule.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it.data.local.di 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import app.traced_it.data.local.database.AppDatabase 6 | import app.traced_it.data.local.database.EntryDao 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.android.qualifiers.ApplicationContext 11 | import dagger.hilt.components.SingletonComponent 12 | import javax.inject.Singleton 13 | 14 | @Module 15 | @InstallIn(SingletonComponent::class) 16 | class DatabaseModule { 17 | @Provides 18 | fun provideEntryDao(appDatabase: AppDatabase): EntryDao { 19 | return appDatabase.entryDao() 20 | } 21 | 22 | @Provides 23 | @Singleton 24 | fun provideAppDatabase(@ApplicationContext appContext: Context): AppDatabase { 25 | return Room.databaseBuilder( 26 | appContext, 27 | AppDatabase::class.java, 28 | "Entry" 29 | ).build() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/app/traced_it/ui/AboutScreen.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it.ui 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.foundation.Image 5 | import androidx.compose.foundation.layout.* 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.filled.ArrowBack 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.material3.IconButton 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.res.painterResource 18 | import androidx.compose.ui.res.stringResource 19 | import androidx.compose.ui.text.AnnotatedString 20 | import androidx.compose.ui.text.SpanStyle 21 | import androidx.compose.ui.text.TextLinkStyles 22 | import androidx.compose.ui.text.fromHtml 23 | import androidx.compose.ui.text.style.LineBreak 24 | import androidx.compose.ui.text.style.TextDecoration 25 | import androidx.compose.ui.tooling.preview.Preview 26 | import androidx.compose.ui.unit.dp 27 | import app.traced_it.BuildConfig 28 | import app.traced_it.R 29 | import app.traced_it.ui.components.TracedScaffold 30 | import app.traced_it.ui.components.TracedTopAppBar 31 | import app.traced_it.ui.theme.AppTheme 32 | import app.traced_it.ui.theme.Light 33 | import app.traced_it.ui.theme.Spacing 34 | 35 | @Composable 36 | fun AboutScreen( 37 | onNavigateToEntries: () -> Unit = {}, 38 | ) { 39 | TracedScaffold( 40 | topBar = { 41 | TracedTopAppBar( 42 | title = { Text(stringResource(R.string.about_title)) }, 43 | navigationIcon = { 44 | IconButton(onClick = onNavigateToEntries) { 45 | Icon( 46 | imageVector = Icons.AutoMirrored.Default.ArrowBack, 47 | contentDescription = stringResource(R.string.nav_back) 48 | ) 49 | } 50 | }, 51 | ) 52 | }, 53 | ) { innerPadding -> 54 | val appName = stringResource(R.string.app_name) 55 | Column( 56 | modifier = Modifier 57 | .padding(innerPadding) 58 | .consumeWindowInsets(innerPadding) 59 | .padding(horizontal = Spacing.windowPadding) 60 | .fillMaxWidth() 61 | .verticalScroll(rememberScrollState()) 62 | ) { 63 | Image( 64 | painter = painterResource(id = R.drawable.ic_launcher_foreground), 65 | contentDescription = stringResource(R.string.about_app_icon_content_description), 66 | modifier = Modifier 67 | .size(192.dp) 68 | .align(Alignment.CenterHorizontally) 69 | ) 70 | Text( 71 | appName, 72 | style = MaterialTheme.typography.headlineLarge, 73 | ) 74 | Text( 75 | AnnotatedString.fromHtml( 76 | stringResource( 77 | R.string.about_text, 78 | appName, 79 | BuildConfig.VERSION_NAME 80 | ), 81 | linkStyles = TextLinkStyles( 82 | SpanStyle( 83 | color = Light, 84 | textDecoration = TextDecoration.Underline 85 | ) 86 | ) 87 | ), 88 | style = MaterialTheme.typography.bodyMedium.copy( 89 | lineBreak = LineBreak.Paragraph, 90 | ) 91 | ) 92 | } 93 | } 94 | } 95 | 96 | // Previews 97 | 98 | @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) 99 | @Composable 100 | private fun DefaultPreview() { 101 | AppTheme { 102 | AboutScreen() 103 | } 104 | } 105 | 106 | @Preview(showBackground = true) 107 | @Composable 108 | private fun LightPreview() { 109 | AppTheme { 110 | AboutScreen() 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /app/src/main/java/app/traced_it/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it.ui 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.activity.enableEdgeToEdge 7 | import androidx.activity.viewModels 8 | import app.traced_it.ui.entry.EntryViewModel 9 | import app.traced_it.ui.theme.AppTheme 10 | import dagger.hilt.android.AndroidEntryPoint 11 | 12 | @AndroidEntryPoint 13 | class MainActivity : ComponentActivity() { 14 | 15 | private val viewModel: EntryViewModel by viewModels() 16 | 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | 20 | enableEdgeToEdge() 21 | 22 | setContent { 23 | AppTheme { 24 | MainNavigation(viewModel) 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/app/traced_it/ui/MainNavigation.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it.ui 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.navigation.compose.NavHost 5 | import androidx.navigation.compose.composable 6 | import androidx.navigation.compose.rememberNavController 7 | import app.traced_it.ui.entry.EntryListScreen 8 | import app.traced_it.ui.entry.EntryViewModel 9 | 10 | @Composable 11 | fun MainNavigation(viewModel: EntryViewModel) { 12 | val navController = rememberNavController() 13 | 14 | NavHost(navController = navController, startDestination = "entries") { 15 | composable("about") { 16 | AboutScreen( 17 | onNavigateToEntries = { navController.navigate("entries") } 18 | ) 19 | } 20 | composable("entries") { 21 | EntryListScreen( 22 | onNavigateToAboutScreen = { navController.navigate("about") }, 23 | viewModel = viewModel, 24 | ) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/app/traced_it/ui/components/ConfirmationDialog.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it.ui.components 2 | 3 | import androidx.compose.material3.* 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.ExperimentalComposeUiApi 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.graphics.Color 8 | import androidx.compose.ui.platform.testTag 9 | import androidx.compose.ui.semantics.semantics 10 | import androidx.compose.ui.semantics.testTagsAsResourceId 11 | import androidx.compose.ui.tooling.preview.Preview 12 | import app.traced_it.ui.theme.AppTheme 13 | 14 | @OptIn(ExperimentalComposeUiApi::class) 15 | @Composable 16 | fun ConfirmationDialog( 17 | title: String, 18 | text: String, 19 | confirmText: String, 20 | dismissText: String, 21 | onDismissRequest: () -> Unit = {}, 22 | onConfirmation: () -> Unit = {}, 23 | containerColor: Color = MaterialTheme.colorScheme.secondary, 24 | contentColor: Color = MaterialTheme.colorScheme.onSecondary, 25 | ) { 26 | AlertDialog( 27 | title = { Text(title) }, 28 | text = { Text(text) }, 29 | modifier = Modifier.semantics { testTagsAsResourceId = true }, 30 | onDismissRequest = { onDismissRequest() }, 31 | confirmButton = { 32 | TextButton( 33 | { onConfirmation() }, 34 | modifier = Modifier.testTag("confirmationDialogConfirmButton"), 35 | colors = ButtonDefaults.buttonColors( 36 | containerColor = Color.Transparent, 37 | contentColor = contentColor, 38 | ), 39 | ) { 40 | Text(confirmText) 41 | } 42 | }, 43 | dismissButton = { 44 | TextButton( 45 | { onDismissRequest() }, 46 | colors = ButtonDefaults.buttonColors( 47 | containerColor = Color.Transparent, 48 | contentColor = contentColor, 49 | ), 50 | ) { 51 | Text(dismissText) 52 | } 53 | }, 54 | containerColor = containerColor, 55 | textContentColor = contentColor, 56 | ) 57 | } 58 | 59 | @Preview(showBackground = true) 60 | @Composable 61 | private fun DefaultPreview() { 62 | AppTheme { 63 | ConfirmationDialog( 64 | title = "Delete all entries", 65 | text = "You are about to delete all entries. This action cannot be undone.", 66 | confirmText = "Delete all", 67 | dismissText = "Dismiss", 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/app/traced_it/ui/components/SelectedEntryMenu.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it.ui.components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.outlined.MoreVert 6 | import androidx.compose.material3.* 7 | import androidx.compose.runtime.* 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.res.stringResource 10 | import app.traced_it.R 11 | 12 | @Composable 13 | fun SelectedEntryMenu( 14 | modifier: Modifier = Modifier, 15 | onAddWithSameContent: () -> Unit = {}, 16 | onCopy: () -> Unit = {}, 17 | onFilterWithSimilarContent: () -> Unit = {}, 18 | ) { 19 | var expanded by remember { mutableStateOf(false) } 20 | 21 | Box { 22 | IconButton({ expanded = true }, modifier) { 23 | Icon( 24 | Icons.Outlined.MoreVert, 25 | contentDescription = stringResource( 26 | R.string.list_menu 27 | ), 28 | ) 29 | } 30 | DropdownMenu( 31 | expanded = expanded, 32 | onDismissRequest = { expanded = false }, 33 | ) { 34 | DropdownMenuItem( 35 | { Text(stringResource(R.string.list_item_add)) }, 36 | { 37 | expanded = false 38 | onAddWithSameContent() 39 | }, 40 | ) 41 | DropdownMenuItem( 42 | { Text(stringResource(R.string.list_item_find_with_similar_content)) }, 43 | { 44 | expanded = false 45 | onFilterWithSimilarContent() 46 | }, 47 | ) 48 | DropdownMenuItem( 49 | { Text(stringResource(R.string.list_item_copy_to_clipboard)) }, 50 | { 51 | expanded = false 52 | onCopy() 53 | }, 54 | ) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/app/traced_it/ui/components/TracedBottomButton.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it.ui.components 2 | 3 | import android.content.res.Configuration 4 | import android.os.Build 5 | import androidx.annotation.RequiresApi 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.material3.Button 8 | import androidx.compose.material3.ButtonDefaults 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.text.font.FontWeight 14 | import androidx.compose.ui.tooling.preview.Preview 15 | import androidx.compose.ui.unit.dp 16 | import app.traced_it.ui.theme.AppTheme 17 | import app.traced_it.ui.theme.Spacing 18 | 19 | @Composable 20 | fun TracedBottomButton( 21 | text: String, 22 | onClick: () -> Unit, 23 | modifier: Modifier = Modifier, 24 | enabled: Boolean = true, 25 | ) { 26 | Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { 27 | Button( 28 | onClick = onClick, 29 | modifier = modifier 30 | .padding(Spacing.small) 31 | .width(400.dp) 32 | .height(Spacing.bottomButtonHeight), 33 | enabled = enabled, 34 | colors = ButtonDefaults.buttonColors( 35 | containerColor = MaterialTheme.colorScheme.tertiary, 36 | contentColor = MaterialTheme.colorScheme.onTertiary, 37 | disabledContainerColor = MaterialTheme.colorScheme.secondary, 38 | disabledContentColor = MaterialTheme.colorScheme.onSecondary 39 | ), 40 | ) { 41 | Text(text, fontWeight = FontWeight.Bold) 42 | } 43 | } 44 | } 45 | 46 | // Previews 47 | 48 | @RequiresApi(Build.VERSION_CODES.O) 49 | @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) 50 | @Composable 51 | private fun DefaultPreview() { 52 | AppTheme { 53 | TracedBottomButton("Enabled button", {}) 54 | } 55 | } 56 | 57 | @RequiresApi(Build.VERSION_CODES.O) 58 | @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) 59 | @Composable 60 | private fun DisabledPreview() { 61 | AppTheme { 62 | TracedBottomButton("Disabled button", {}, enabled = false) 63 | } 64 | } 65 | 66 | @RequiresApi(Build.VERSION_CODES.O) 67 | @Preview( 68 | showBackground = true, 69 | uiMode = Configuration.UI_MODE_NIGHT_YES, 70 | widthDp = 480 71 | ) 72 | @Composable 73 | private fun PortraitPreview() { 74 | AppTheme { 75 | TracedBottomButton("Portrait button", {}) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/app/traced_it/ui/components/TracedScaffold.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it.ui.components 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.foundation.layout.WindowInsets 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.Scaffold 7 | import androidx.compose.material3.ScaffoldDefaults 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.ExperimentalComposeUiApi 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.semantics.semantics 12 | import androidx.compose.ui.semantics.testTagsAsResourceId 13 | 14 | @OptIn(ExperimentalComposeUiApi::class) 15 | @Composable 16 | fun TracedScaffold( 17 | topBar: @Composable (() -> Unit) = {}, 18 | bottomBar: @Composable (() -> Unit) = {}, 19 | snackbarHost: @Composable (() -> Unit) = {}, 20 | contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets, 21 | content: @Composable ((PaddingValues) -> Unit) = {} 22 | ) { 23 | Scaffold( 24 | Modifier.semantics { testTagsAsResourceId = true }, 25 | topBar = topBar, 26 | bottomBar = bottomBar, 27 | snackbarHost = snackbarHost, 28 | containerColor = MaterialTheme.colorScheme.surface, 29 | contentColor = MaterialTheme.colorScheme.onSurface, 30 | contentWindowInsets = contentWindowInsets, 31 | content = content, 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/app/traced_it/ui/components/TracedSegmentedButton.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it.ui.components 2 | 3 | import androidx.compose.foundation.focusable 4 | import androidx.compose.foundation.layout.height 5 | import androidx.compose.foundation.layout.widthIn 6 | import androidx.compose.material3.* 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.focus.FocusRequester 11 | import androidx.compose.ui.focus.focusRequester 12 | import androidx.compose.ui.tooling.preview.Preview 13 | import app.traced_it.ui.theme.AppTheme 14 | import app.traced_it.ui.theme.Spacing 15 | 16 | @Composable 17 | @OptIn(ExperimentalMaterial3Api::class) 18 | fun SingleChoiceSegmentedButtonRowScope.TracedSegmentedButton( 19 | index: Int, 20 | count: Int, 21 | selected: Boolean, 22 | onClick: () -> Unit, 23 | content: @Composable () -> Unit, 24 | ) { 25 | val focusRequester = remember { FocusRequester() } 26 | SegmentedButton( 27 | selected, 28 | onClick = { 29 | focusRequester.requestFocus() 30 | onClick() 31 | }, 32 | shape = SegmentedButtonDefaults.itemShape( 33 | index = index, 34 | count = count, 35 | baseShape = MaterialTheme.shapes.extraSmall 36 | ), 37 | modifier = Modifier 38 | .focusRequester(focusRequester) 39 | .focusable() 40 | .widthIn(min = Spacing.inputHeight) 41 | .height(Spacing.inputHeight), 42 | colors = SegmentedButtonDefaults.colors( 43 | inactiveContainerColor = MaterialTheme.colorScheme.secondaryContainer, 44 | inactiveBorderColor = MaterialTheme.colorScheme.outline, 45 | inactiveContentColor = MaterialTheme.colorScheme.onSecondaryContainer, 46 | activeContainerColor = MaterialTheme.colorScheme.tertiaryContainer, 47 | activeBorderColor = MaterialTheme.colorScheme.tertiaryContainer, 48 | activeContentColor = MaterialTheme.colorScheme.onTertiaryContainer, 49 | ), 50 | icon = {}, 51 | ) { 52 | content() 53 | } 54 | } 55 | 56 | // Previews 57 | 58 | @OptIn(ExperimentalMaterial3Api::class) 59 | @Preview(showBackground = true) 60 | @Composable 61 | private fun DefaultPreview() { 62 | AppTheme { 63 | SingleChoiceSegmentedButtonRow { 64 | TracedSegmentedButton( 65 | index = 0, 66 | count = 2, 67 | selected = false, 68 | onClick = {}, 69 | ) { 70 | Text("1x", style = MaterialTheme.typography.labelLarge) 71 | } 72 | TracedSegmentedButton( 73 | index = 1, 74 | count = 2, 75 | selected = true, 76 | onClick = {}, 77 | ) { 78 | Text("2x", style = MaterialTheme.typography.labelLarge) 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/app/traced_it/ui/components/TracedTopAppBar.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it.ui.components 2 | 3 | import androidx.compose.foundation.layout.RowScope 4 | import androidx.compose.material3.ExperimentalMaterial3Api 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.TopAppBar 7 | import androidx.compose.material3.TopAppBarDefaults 8 | import androidx.compose.runtime.Composable 9 | 10 | @OptIn(ExperimentalMaterial3Api::class) 11 | @Composable 12 | fun TracedTopAppBar( 13 | title: @Composable (() -> Unit), 14 | navigationIcon: @Composable (() -> Unit) = {}, 15 | actions: @Composable (RowScope.() -> Unit) = {}, 16 | ) { 17 | TopAppBar( 18 | title = title, 19 | navigationIcon = navigationIcon, 20 | actions = actions, 21 | colors = TopAppBarDefaults.topAppBarColors( 22 | containerColor = MaterialTheme.colorScheme.background, 23 | navigationIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer, 24 | titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer, 25 | actionIconContentColor = MaterialTheme.colorScheme.onBackground, 26 | ), 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/app/traced_it/ui/entry/EntryDetailDialog.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it.ui.entry 2 | 3 | import android.content.res.Configuration 4 | import android.os.Build 5 | import androidx.annotation.RequiresApi 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.foundation.rememberScrollState 8 | import androidx.compose.foundation.text.KeyboardOptions 9 | import androidx.compose.foundation.verticalScroll 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.outlined.Close 12 | import androidx.compose.material3.* 13 | import androidx.compose.runtime.* 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.platform.LocalContext 16 | import androidx.compose.ui.platform.testTag 17 | import androidx.compose.ui.res.stringResource 18 | import androidx.compose.ui.text.input.KeyboardCapitalization 19 | import androidx.compose.ui.tooling.preview.Preview 20 | import app.traced_it.R 21 | import app.traced_it.data.di.defaultFakeEntries 22 | import app.traced_it.data.local.database.* 23 | import app.traced_it.ui.components.TracedBottomButton 24 | import app.traced_it.ui.components.TracedScaffold 25 | import app.traced_it.ui.components.TracedTextField 26 | import app.traced_it.ui.components.TracedTopAppBar 27 | import app.traced_it.ui.theme.AppTheme 28 | import app.traced_it.ui.theme.Spacing 29 | 30 | sealed class EntryDetailAction(val entry: Entry) { 31 | class Edit(entry: Entry) : EntryDetailAction(entry) 32 | class Prefill(entry: Entry) : EntryDetailAction(entry) 33 | } 34 | 35 | @Composable 36 | fun EntryDetailDialog( 37 | action: EntryDetailAction, 38 | latestEntryUnit: EntryUnit? = null, 39 | onInsert: (Entry) -> Unit = {}, 40 | onUpdate: (Entry) -> Unit = {}, 41 | onDismiss: () -> Unit = {}, 42 | ) { 43 | val context = LocalContext.current 44 | 45 | var content by remember { mutableStateOf(action.entry.content) } 46 | var unit by remember { 47 | mutableStateOf( 48 | if (action.entry.amountUnit in visibleUnits) { 49 | action.entry.amountUnit 50 | } else if (action.entry.amount != 0.0) { 51 | // If we're editing or prefilling an entry that has a deprecated 52 | // unit (such as smallNumbersChoiceUnit), convert the unit into 53 | // doubleUnit. 54 | doubleUnit 55 | } else { 56 | noneUnit 57 | } 58 | ) 59 | } 60 | var amountRaw by remember { 61 | mutableStateOf(unit.format(context, action.entry.amount)) 62 | } 63 | var visibleUnit by remember { 64 | mutableStateOf( 65 | unit.takeIf { it in visibleUnits } 66 | ?: latestEntryUnit.takeIf { it in visibleUnits } 67 | ?: defaultVisibleUnit 68 | ) 69 | } 70 | 71 | TracedScaffold( 72 | topBar = { 73 | TracedTopAppBar( 74 | title = { 75 | Text( 76 | stringResource( 77 | if (action is EntryDetailAction.Edit) { 78 | R.string.detail_update_title 79 | } else { 80 | R.string.detail_add_title 81 | } 82 | ) 83 | ) 84 | }, 85 | actions = { 86 | IconButton({ onDismiss() }) { 87 | Icon( 88 | imageVector = Icons.Outlined.Close, 89 | contentDescription = stringResource(R.string.detail_cancel) 90 | ) 91 | } 92 | }, 93 | ) 94 | }, 95 | ) { innerPadding -> 96 | Column( 97 | modifier = Modifier 98 | .padding(innerPadding) 99 | .consumeWindowInsets(innerPadding) 100 | .imePadding() 101 | ) { 102 | HorizontalDivider() 103 | Column( 104 | modifier = Modifier 105 | .weight(1f) 106 | .verticalScroll(rememberScrollState()) 107 | ) { 108 | Text( 109 | stringResource(R.string.detail_content_label), 110 | Modifier 111 | .padding(horizontal = Spacing.windowPadding) 112 | .padding(top = Spacing.medium, bottom = Spacing.small), 113 | color = MaterialTheme.colorScheme.onSurfaceVariant, 114 | style = MaterialTheme.typography.bodyMedium, 115 | ) 116 | TracedTextField( 117 | value = content, 118 | onValueChange = { content = it }, 119 | modifier = Modifier 120 | .testTag("entryDetailContentTextField") 121 | .padding(horizontal = Spacing.windowPadding) 122 | .fillMaxWidth(), 123 | isError = content.isEmpty(), 124 | keyboardOptions = KeyboardOptions( 125 | capitalization = KeyboardCapitalization.Sentences 126 | ), 127 | ) 128 | UnitSelect( 129 | amountRaw = amountRaw, 130 | selectedUnit = unit, 131 | visibleUnit = visibleUnit, 132 | modifier = Modifier.padding(top = Spacing.medium * 2), 133 | onAmountRawChange = { amountRaw = it }, 134 | onUnitChange = { unit = it }, 135 | onVisibleUnitChange = { newVisibleUnit -> 136 | unit = noneUnit 137 | visibleUnit = newVisibleUnit 138 | amountRaw = "" 139 | }, 140 | ) 141 | } 142 | HorizontalDivider() 143 | TracedBottomButton( 144 | text = stringResource( 145 | if (action is EntryDetailAction.Edit) 146 | R.string.detail_update_save 147 | else 148 | R.string.detail_add_save, 149 | ), 150 | onClick = { 151 | val amount = unit.parse(context, amountRaw) 152 | if (action is EntryDetailAction.Edit) { 153 | onUpdate( 154 | action.entry.copy( 155 | amount = amount, 156 | amountUnit = unit, 157 | content = content, 158 | ) 159 | ) 160 | } else { 161 | onInsert( 162 | Entry( 163 | amount = amount, 164 | amountUnit = unit, 165 | content = content, 166 | ) 167 | ) 168 | } 169 | }, 170 | modifier = Modifier.testTag("entryDetailSaveButton"), 171 | enabled = content.isNotEmpty(), 172 | ) 173 | } 174 | } 175 | } 176 | 177 | // Previews 178 | 179 | @RequiresApi(Build.VERSION_CODES.O) 180 | @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) 181 | @Composable 182 | private fun DefaultPreview() { 183 | AppTheme { 184 | EntryDetailDialog(EntryDetailAction.Prefill(Entry())) 185 | } 186 | } 187 | 188 | @RequiresApi(Build.VERSION_CODES.O) 189 | @Preview(showBackground = true) 190 | @Composable 191 | private fun LightPreview() { 192 | AppTheme { 193 | EntryDetailDialog(EntryDetailAction.Prefill(Entry())) 194 | } 195 | } 196 | 197 | @RequiresApi(Build.VERSION_CODES.O) 198 | @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) 199 | @Composable 200 | private fun PrefilledPreview() { 201 | AppTheme { 202 | EntryDetailDialog(EntryDetailAction.Prefill(defaultFakeEntries[0])) 203 | } 204 | } 205 | 206 | @RequiresApi(Build.VERSION_CODES.O) 207 | @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) 208 | @Composable 209 | private fun EditPreview() { 210 | AppTheme { 211 | EntryDetailDialog(EntryDetailAction.Edit(defaultFakeEntries[0])) 212 | } 213 | } 214 | 215 | @RequiresApi(Build.VERSION_CODES.O) 216 | @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) 217 | @Composable 218 | private fun InvisibleUnitPreview() { 219 | AppTheme { 220 | EntryDetailDialog( 221 | EntryDetailAction.Edit( 222 | Entry( 223 | content = "Small numbers choice", 224 | amount = 2.0, 225 | amountUnit = smallNumbersChoiceUnit, 226 | ) 227 | ) 228 | ) 229 | } 230 | } 231 | 232 | @RequiresApi(Build.VERSION_CODES.O) 233 | @Preview( 234 | showBackground = true, 235 | uiMode = Configuration.UI_MODE_NIGHT_YES, 236 | widthDp = 1024, 237 | heightDp = 768 238 | ) 239 | @Composable 240 | private fun PortraitPreview() { 241 | AppTheme { 242 | EntryDetailDialog(EntryDetailAction.Prefill(defaultFakeEntries[0])) 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /app/src/main/java/app/traced_it/ui/entry/EntryListMenu.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it.ui.entry 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.outlined.MoreVert 6 | import androidx.compose.material3.* 7 | import androidx.compose.runtime.* 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.res.stringResource 10 | import app.traced_it.R 11 | 12 | @Composable 13 | fun EntryListMenu( 14 | enabled: Boolean, 15 | modifier: Modifier = Modifier, 16 | onDeleteAllEntries: () -> Unit = {}, 17 | onExportAllEntries: () -> Unit = {}, 18 | onImportEntries: () -> Unit = {}, 19 | onNavigateToAboutScreen: () -> Unit = {}, 20 | ) { 21 | var expanded by remember { mutableStateOf(false) } 22 | 23 | Box { 24 | IconButton({ expanded = true }, modifier) { 25 | Icon( 26 | Icons.Outlined.MoreVert, 27 | contentDescription = stringResource( 28 | R.string.list_menu 29 | ), 30 | ) 31 | } 32 | DropdownMenu( 33 | expanded = expanded, 34 | onDismissRequest = { expanded = false }, 35 | ) { 36 | DropdownMenuItem( 37 | { Text(stringResource(R.string.list_menu_import)) }, 38 | { 39 | expanded = false 40 | onImportEntries() 41 | }, 42 | ) 43 | DropdownMenuItem( 44 | { Text(stringResource(R.string.list_menu_export_all)) }, 45 | { 46 | expanded = false 47 | onExportAllEntries() 48 | }, 49 | enabled = enabled, 50 | ) 51 | DropdownMenuItem( 52 | { Text(stringResource(R.string.list_menu_delete_all)) }, 53 | { 54 | expanded = false 55 | onDeleteAllEntries() 56 | }, 57 | enabled = enabled, 58 | ) 59 | DropdownMenuItem( 60 | { Text(stringResource(R.string.about_title)) }, 61 | { 62 | expanded = false 63 | onNavigateToAboutScreen() 64 | }, 65 | ) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/app/traced_it/ui/entry/UnitSelect.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it.ui.entry 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.text.KeyboardOptions 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.filled.ArrowDropDown 8 | import androidx.compose.material3.* 9 | import androidx.compose.runtime.* 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.ExperimentalComposeUiApi 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.platform.testTag 14 | import androidx.compose.ui.res.stringResource 15 | import androidx.compose.ui.semantics.semantics 16 | import androidx.compose.ui.semantics.testTagsAsResourceId 17 | import androidx.compose.ui.text.font.FontWeight 18 | import androidx.compose.ui.text.input.KeyboardType 19 | import androidx.compose.ui.tooling.preview.Preview 20 | import app.traced_it.R 21 | import app.traced_it.data.local.database.* 22 | import app.traced_it.ui.components.TracedTextField 23 | import app.traced_it.ui.theme.AppTheme 24 | import app.traced_it.ui.theme.Spacing 25 | 26 | @OptIn(ExperimentalComposeUiApi::class) 27 | @Composable 28 | fun UnitSelect( 29 | amountRaw: String, 30 | selectedUnit: EntryUnit, 31 | visibleUnit: EntryUnit, 32 | modifier: Modifier = Modifier, 33 | onAmountRawChange: (newAmountRaw: String) -> Unit = {}, 34 | onUnitChange: (newUnit: EntryUnit) -> Unit = {}, 35 | onVisibleUnitChange: (newVisibleUnit: EntryUnit) -> Unit = {}, 36 | ) { 37 | var expanded by remember { mutableStateOf(false) } 38 | 39 | Column( 40 | modifier = modifier 41 | .fillMaxWidth() 42 | .padding(horizontal = Spacing.windowPadding), 43 | horizontalAlignment = Alignment.CenterHorizontally, 44 | ) { 45 | Row( 46 | modifier = Modifier.fillMaxWidth(), 47 | horizontalArrangement = Arrangement.SpaceBetween, 48 | verticalAlignment = Alignment.CenterVertically 49 | ) { 50 | Text( 51 | stringResource(R.string.detail_unit_label), 52 | color = MaterialTheme.colorScheme.onSurfaceVariant, 53 | style = MaterialTheme.typography.bodyMedium, 54 | ) 55 | Box { 56 | TextButton( 57 | onClick = { expanded = true }, 58 | modifier = Modifier.testTag("unitSelectButton"), 59 | shape = MaterialTheme.shapes.extraSmall, 60 | colors = ButtonDefaults.buttonColors( 61 | containerColor = MaterialTheme.colorScheme.surface 62 | ), 63 | contentPadding = PaddingValues(start = Spacing.medium), 64 | ) { 65 | Text( 66 | stringResource(visibleUnit.nameResId), 67 | fontWeight = FontWeight.Normal, 68 | ) 69 | Icon( 70 | imageVector = Icons.Default.ArrowDropDown, 71 | contentDescription = stringResource( 72 | R.string.detail_unit_dropdown_content_description 73 | ), 74 | modifier = Modifier.padding(Spacing.small), 75 | ) 76 | } 77 | DropdownMenu( 78 | expanded = expanded, 79 | onDismissRequest = { expanded = false }, 80 | modifier = Modifier 81 | .semantics { testTagsAsResourceId = true }, 82 | ) { 83 | visibleUnits.filterNot { it == visibleUnit } 84 | .forEach { unit -> 85 | DropdownMenuItem( 86 | text = { 87 | Text(stringResource(unit.nameResId)) 88 | }, 89 | onClick = { 90 | expanded = false 91 | onVisibleUnitChange(unit) 92 | }, 93 | modifier = Modifier 94 | .testTag("unitSelectDropdownMenuItem"), 95 | contentPadding = PaddingValues( 96 | horizontal = Spacing.medium, 97 | vertical = Spacing.small, 98 | ), 99 | ) 100 | } 101 | } 102 | } 103 | } 104 | if (visibleUnit.choices.isNotEmpty()) { 105 | UnitSelectChoice( 106 | amountRaw = amountRaw, 107 | unit = visibleUnit, 108 | selectedUnit = selectedUnit, 109 | modifier = Modifier.fillMaxWidth(), 110 | onAmountRawChange = { newAmountRaw -> 111 | onAmountRawChange(newAmountRaw) 112 | onUnitChange(visibleUnit) 113 | }, 114 | onDeselect = { 115 | onAmountRawChange("") 116 | onUnitChange(noneUnit) 117 | }, 118 | ) 119 | } else { 120 | TracedTextField( 121 | value = amountRaw, 122 | onValueChange = { newAmountRaw -> 123 | onAmountRawChange(newAmountRaw) 124 | if (newAmountRaw.isEmpty()) { 125 | onUnitChange(noneUnit) 126 | } else { 127 | onUnitChange(visibleUnit) 128 | } 129 | }, 130 | modifier = Modifier.fillMaxWidth(), 131 | placeholder = { Text(visibleUnit.placeholder) }, 132 | keyboardOptions = KeyboardOptions( 133 | keyboardType = KeyboardType.Decimal 134 | ), 135 | singleLine = true, 136 | ) 137 | } 138 | } 139 | } 140 | 141 | // Previews 142 | 143 | @Preview(showBackground = true) 144 | @Composable 145 | private fun DefaultPreview() { 146 | AppTheme { 147 | Box(Modifier.background(MaterialTheme.colorScheme.surface)) { 148 | UnitSelect( 149 | "", 150 | noneUnit, 151 | clothingSizeUnit, 152 | ) 153 | } 154 | } 155 | } 156 | 157 | @Preview(showBackground = true) 158 | @Composable 159 | private fun ClothingSizePreview() { 160 | AppTheme { 161 | Box(Modifier.background(MaterialTheme.colorScheme.surface)) { 162 | UnitSelect( 163 | "S", 164 | clothingSizeUnit, 165 | clothingSizeUnit, 166 | ) 167 | } 168 | } 169 | } 170 | 171 | @Preview(showBackground = true) 172 | @Composable 173 | private fun SmallNumbersChoicePreview() { 174 | AppTheme { 175 | Box(Modifier.background(MaterialTheme.colorScheme.surface)) { 176 | UnitSelect( 177 | "2x", 178 | smallNumbersChoiceUnit, 179 | smallNumbersChoiceUnit, 180 | ) 181 | } 182 | } 183 | } 184 | 185 | @Preview(showBackground = true) 186 | @Composable 187 | private fun FractionPreview() { 188 | AppTheme { 189 | Box(Modifier.background(MaterialTheme.colorScheme.surface)) { 190 | UnitSelect( 191 | "⅓", 192 | fractionUnit, 193 | fractionUnit, 194 | ) 195 | } 196 | } 197 | } 198 | 199 | @Preview(showBackground = true) 200 | @Composable 201 | private fun DoublePreview() { 202 | AppTheme { 203 | Box(Modifier.background(MaterialTheme.colorScheme.surface)) { 204 | UnitSelect( 205 | "", 206 | doubleUnit, 207 | doubleUnit, 208 | ) 209 | } 210 | } 211 | } 212 | 213 | @Preview(showBackground = true, locale = "fr-rFR") 214 | @Composable 215 | private fun DoubleFrenchPreview() { 216 | AppTheme { 217 | Box(Modifier.background(MaterialTheme.colorScheme.surface)) { 218 | UnitSelect( 219 | "", 220 | doubleUnit, 221 | doubleUnit, 222 | ) 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /app/src/main/java/app/traced_it/ui/entry/UnitSelectChoice.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it.ui.entry 2 | 3 | import androidx.compose.material3.ExperimentalMaterial3Api 4 | import androidx.compose.material3.SingleChoiceSegmentedButtonRow 5 | import androidx.compose.material3.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.platform.LocalContext 9 | import androidx.compose.ui.platform.testTag 10 | import androidx.compose.ui.res.stringResource 11 | import androidx.compose.ui.tooling.preview.Preview 12 | import app.traced_it.data.local.database.EntryUnit 13 | import app.traced_it.data.local.database.clothingSizeUnit 14 | import app.traced_it.ui.components.TracedSegmentedButton 15 | import app.traced_it.ui.theme.AppTheme 16 | 17 | @OptIn(ExperimentalMaterial3Api::class) 18 | @Composable 19 | fun UnitSelectChoice( 20 | amountRaw: String, 21 | unit: EntryUnit, 22 | selectedUnit: EntryUnit, 23 | modifier: Modifier = Modifier, 24 | onAmountRawChange: (newAmountRaw: String) -> Unit = {}, 25 | onDeselect: () -> Unit = {}, 26 | ) { 27 | val context = LocalContext.current 28 | 29 | SingleChoiceSegmentedButtonRow(modifier) { 30 | unit.choices.forEachIndexed { index, choice -> 31 | val choiceName = stringResource(choice.nameResId) 32 | TracedSegmentedButton( 33 | index = index, 34 | count = unit.choices.size, 35 | selected = unit == selectedUnit && choiceName == amountRaw, 36 | onClick = { 37 | if (unit == selectedUnit && choiceName == amountRaw) { 38 | onDeselect() 39 | } else { 40 | onAmountRawChange(choiceName) 41 | } 42 | }, 43 | ) { 44 | val modifier = Modifier.testTag("unitSelectChoiceText") 45 | choice.formatHtml(context) 46 | ?.let { Text(it, modifier) } 47 | ?: Text(choice.format(context), modifier) 48 | } 49 | } 50 | } 51 | } 52 | 53 | // Previews 54 | 55 | @Preview(showBackground = true) 56 | @Composable 57 | private fun DefaultPreview() { 58 | AppTheme { 59 | UnitSelectChoice("S", clothingSizeUnit, clothingSizeUnit) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/app/traced_it/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Darkest = Color(0xFF17122B) 6 | val Darker = Color(0xFF2A2150) 7 | val Dark = Color(0xFF513F99) 8 | val Subtle = Color(0xFF8C7FBD) 9 | val Light = Color(0xFFAAA1CE) 10 | val Lighter = Color(0xFFC8C2E0) 11 | val Lightest = Color(0xFFE8E6EF) 12 | 13 | val Green = Color(0xFFB3EE4F) 14 | val Orange = Color(0xFFEE884F) 15 | 16 | val White = Color(0xFFE6E0E9) 17 | val Gray = Color(0xFFCAC4C0) 18 | -------------------------------------------------------------------------------- /app/src/main/java/app/traced_it/ui/theme/Spacing.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it.ui.theme 2 | 3 | import androidx.compose.ui.unit.dp 4 | 5 | object Spacing { 6 | val small = 12.dp 7 | val medium = 24.dp 8 | 9 | val bottomButtonHeight = 52.dp 10 | val listButtonSize = 40.dp 11 | val inputHeight = 72.dp 12 | val inputBorderWidth = 3.dp 13 | val swipeActionWidth = 92.dp 14 | val trailingIconPadding = 8.dp 15 | val windowPadding = 16.dp 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/app/traced_it/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.darkColorScheme 6 | import androidx.compose.runtime.Composable 7 | 8 | private val darkScheme = darkColorScheme( 9 | // Bright purple button 10 | primary = Dark, 11 | onPrimary = Lightest, 12 | 13 | // Disabled purple button and dialog 14 | secondary = Darker, 15 | onSecondary = Light, 16 | 17 | // Green button 18 | tertiary = Green, 19 | onTertiary = Darkest, 20 | 21 | // Error button 22 | error = Orange, 23 | onError = Lightest, 24 | 25 | // Default background 26 | surface = Darkest, 27 | onSurface = Lightest, 28 | 29 | // Lower emphasis text and text field bottom border 30 | onSurfaceVariant = Light, 31 | 32 | // App background 33 | background = Darkest, 34 | onBackground = Lightest, 35 | 36 | // Odd list item 37 | surfaceContainerHigh = Darker.copy(alpha = 0.2f), 38 | 39 | // Text input background 40 | surfaceContainerHighest = Darker, 41 | 42 | // Title text 43 | onPrimaryContainer = White, 44 | 45 | // Inactive segmented button 46 | secondaryContainer = Darkest, 47 | onSecondaryContainer = Lighter, 48 | 49 | // Active segmented button and highlighted list item 50 | tertiaryContainer = Subtle, 51 | onTertiaryContainer = Darkest, 52 | 53 | // Text field outline 54 | outline = Lighter, 55 | 56 | // Divider 57 | outlineVariant = Darker, 58 | 59 | // Success message 60 | inverseSurface = Light, 61 | inverseOnSurface = Darkest 62 | ) 63 | 64 | val lightScheme = darkScheme.copy( 65 | background = darkScheme.inverseSurface, 66 | onBackground = darkScheme.inverseOnSurface, 67 | onPrimaryContainer = darkScheme.inverseOnSurface, 68 | ) 69 | 70 | @Composable 71 | fun AppTheme( 72 | darkTheme: Boolean = isSystemInDarkTheme(), 73 | content: @Composable () -> Unit, 74 | ) { 75 | MaterialTheme( 76 | colorScheme = if (!darkTheme) lightScheme else darkScheme, 77 | typography = AppTypography, 78 | content = content, 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/java/app/traced_it/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package app.traced_it.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | val AppTypography = Typography( 10 | bodyMedium = TextStyle( 11 | fontFamily = FontFamily.Default, 12 | fontWeight = FontWeight.Normal, 13 | fontSize = 16.sp, 14 | lineHeight = 16.sp 15 | ) 16 | ) 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/backspace_24px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 15 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traced-it/traced-it-android/1a121b505e141b15b3132f1198c2180b03b7c73b/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traced-it/traced-it-android/1a121b505e141b15b3132f1198c2180b03b7c73b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traced-it/traced-it-android/1a121b505e141b15b3132f1198c2180b03b7c73b/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traced-it/traced-it-android/1a121b505e141b15b3132f1198c2180b03b7c73b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traced-it/traced-it-android/1a121b505e141b15b3132f1198c2180b03b7c73b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traced-it/traced-it-android/1a121b505e141b15b3132f1198c2180b03b7c73b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traced-it/traced-it-android/1a121b505e141b15b3132f1198c2180b03b7c73b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traced-it/traced-it-android/1a121b505e141b15b3132f1198c2180b03b7c73b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traced-it/traced-it-android/1a121b505e141b15b3132f1198c2180b03b7c73b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traced-it/traced-it-android/1a121b505e141b15b3132f1198c2180b03b7c73b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/resources.properties: -------------------------------------------------------------------------------- 1 | unqualifiedResLocale=en 2 | -------------------------------------------------------------------------------- /app/src/main/res/values-ar/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ⅓]]> 4 | ½ 5 | ½]]> 6 | ¾ 7 | ¾]]> 8 | 1 9 | 1 10 | عدد 11 | %1$s، %2$s 12 | تحديث الملاحظة 13 | إضافة ملاحظة 14 | تم استعادة الملاحظة 15 | تم استعادة جميع الملاحظات 16 | ملاحظات %1$s %2$s.csv 17 | ملاحظات %1$s %2$s تصفية %3$s.csv 18 | تصفية ملاحظاتك 19 | مسح الإدخال 20 | تصفية 21 | إغلاق التصفية 22 | حفظ 23 | تصفية الملاحظات المتشابهة 24 | 25 | %1$d ملاحظة من أصل %2$d 26 | ملاحظة واحدة من أصل %2$d 27 | ملاحظتين من أصل %2$d 28 | %1$d ملاحظات من أصل %2$d 29 | %1$d ملاحظة من أصل %2$d 30 | %1$d ملاحظة من أصل %2$d 31 | 32 | نسخ إلى الحافظة 33 | تم النسخ 34 | إلغاء تحديد الملاحظة 35 | الملاحظة المحددة 36 | 37 | تم استيراد صفر ملاحظة. 38 | تم استيراد ملاحظة واحدة. 39 | تم استيراد ملاحظتين. 40 | تم استيراد %1$d ملاحظات. 41 | تم استيراد %1$d ملاحظة. 42 | تم استيراد %1$d ملاحظة. 43 | 44 | 45 | تم تخطي صفر ملاحظة، لأن ملاحظة بنفس الطابع الزمني موجودة بالفعل. 46 | تم تخطي ملاحظة واحدة، لأن ملاحظات بنفس الطوابع الزمنية موجودة بالفعل. 47 | تم تخطي ملاحظتين، لأن ملاحظات بنفس الطوابع الزمنية موجودة بالفعل. 48 | تم تخطي %1$d ملاحظات، لأن ملاحظة بنفس الطابع الزمني موجودة بالفعل. 49 | تم تخطي %1$d ملاحظة، لأن ملاحظة بنفس الطابع الزمني موجودة بالفعل. 50 | تم تخطي %1$d ملاحظة، لأن ملاحظة بنفس الطابع الزمني موجودة بالفعل. 51 | 52 | أيقونة التطبيق 53 | الإصدار %2$s


%1$s هو تطبيق غير تجاري صنعه ثلاثة أصدقاء للمتعة.


هل تود مشاركتنا كيفية استخدامك للتطبيق؟ راسلنا عبر البريد الإلكتروني.


هل هناك شيء لا يعمل بالشكل المطلوب أو لديك أفكار للتحسين؟ قم بتسجيل مشكلة.


موزع بموجب رخصة جنو العمومية 3.0 أو أحدث. يمكنك العثور على الكود والمشاركة على GitHub.

]]>
54 | حول 55 | أنت على وشك حذف جميع الملاحظات 56 | حذف الكل 57 | حذف الملاحظة؟ 58 | أنت على وشك حذف الملاحظة %1$s 59 | حذف الملاحظة 60 | تمت إضافة الملاحظة 61 | تم تحديث الملاحظة 62 | تم حذف الملاحظة 63 | تراجع 64 | تم حذف جميع الملاحظات 65 | اختر كمية، إذا أردت 66 | توسيع قائمة الوحدات 67 | مسح الإدخال 68 | استبعاد 69 | استبعاد 70 | بدون وحدة 71 | المقاس 72 | XS 73 | S 74 | M 75 | L 76 | XL 77 | كسر 78 | ¼ 79 | العودة إلى قائمة الملاحظات 80 | إلغاء 81 | تحديث 82 | حفظ 83 | اكتب ملاحظتك 84 | تحديث 85 | حذف 86 | القائمة 87 | استيراد من CSV 88 | حفظ كملف CSV 89 | حذف الكل 90 | ملاحظاتك (%1$d) 91 | إضافة ملاحظة 92 | إضافة ملاحظة بنفس النص 93 | حذف جميع الملاحظات؟ 94 | تراجع 95 | في %1$s 96 | %1$d ثانية مضت 97 | %1$d دقيقة مضت 98 | %1$d ساعة و%2$02d دقيقة مضت 99 | العمود %1$s مفقود 100 | قيمة العمود %1$s فارغة 101 | فشل في تحليل القيمة %2$s للعمود %1$s 102 | يجب أن تكون القيمة %3$s للعمود %1$s واحدة من %2$s 103 | ملف CSV فارغ 104 | ¼]]> 105 | 106 |
107 | -------------------------------------------------------------------------------- /app/src/main/res/values-b+sr+Cyrl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/values-bg/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/values-bn/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | অ্যাপ্লিকেশন আইকন 4 | সংরক্ষণ 5 | আপনার নোট লিখুন 6 | আপডেট 7 | ডিলিট 8 | মেনু 9 | CSV থেকে ফাইল আনুন 10 | সব গুলো ডিলিট করে দিন 11 | আপনার নোটস গুলো (%1$d) 12 | সব গুলো নোটস ডিলিট করে দিন? 13 | আপনি সব গুলো নোটস ডিলিট করে দিচ্ছেন 14 | নোট ডিলিট? 15 | আপনি “%1$s” নোট ডিলিট করে দিচ্ছেন 16 | নোট ডিলিট 17 | নোট যুক্ত হয়ে গেলো 18 | নোট ডিলিট হয়েছে 19 | ফিরে যান 20 | নোটসে ফিরে যান 21 | পূর্বাবস্থায় ফিরে যান 22 | বাতিল 23 | আপডেট 24 | সবগুলো নোট ডিলিট করা হয়েছে 25 | CSV ফরম্যাট এ সংরক্ষণ করুন 26 | সবগুলো ডিলিট করে দিন 27 | নোট যুক্ত করুন 28 | নোট আপডেট করা হয়েছে 29 | হুবহু শব্দ দিয়ে আরেকটি নোট তৈরি করুন 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/values-cs/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | v%2$s


%1$s je nekomerční aplikace vytvořená třemi přáteli pro zábavu.


Chcete se s námi podělit o to, jak aplikaci používáte? Napište nám e-mail.


Nefunguje vám něco nebo máte nápady na vylepšení? Nahlaste problém.


Distribuováno pod GNU General Public License 3.0 nebo novější. Kód najdete na GitHubu, kde se také můžete zapojit do vývoje.

]]>
4 | Ikona aplikace 5 | O aplikaci 6 | Uložit 7 | Zpět na seznam poznámek 8 | Zrušit 9 | Napište svoji poznámku 10 | Upravit 11 | Upravit 12 | Odstranit 13 | Menu 14 | Import z CSV 15 | Uložit jako CSV 16 | Odstranit vše 17 | Přidat poznámku 18 | Přidat poznámku se stejným textem 19 | Vaše poznámky (%1$d) 20 | Odstranit všechny poznámky? 21 | Odstranit vše 22 | Chystáte se odstranit všechny poznámky 23 | Odstranit poznámku? 24 | Chystáte se odstranit poznámku „%1$s“ 25 | Odstranit poznámku 26 | Poznámka přidána 27 | Poznámka upravena 28 | Poznámka odstraněna 29 | Vrátit 30 | Vrátit 31 | Všechny poznámky odstraněny 32 | v %1$s 33 | před %1$d s 34 | před %1$d min 35 | před %1$d h %2$02d min 36 | Chybí sloupec „%1$s“ 37 | Prázdná hodnota sloupce „%1$s“ 38 | Nepodařilo se analyzovat hodnotu „%2$s“ sloupce „%1$s“ 39 | Hodnota „%3$s“ sloupce „%1$s“ musí být jedna z %2$s 40 | 41 | %1$d poznámka importována. 42 | %1$d poznámky importovány. 43 | %1$d poznámek importováno. 44 | %1$d poznámek importováno. 45 | 46 | 47 | %1$d poznámka přeskočena, protože poznámka se stejným časovým údajem už existuje. 48 | %1$d poznámky přeskočeny, protože poznámky se stejnými časovými údaji už existují. 49 | %1$d poznámek přeskočeno, protože poznámky se stejnými časovými údaji už existují. 50 | %1$d poznámek přeskočeno, protože poznámky se stejnými časovými údaji už existují. 51 | 52 | Prázdný soubor CSV 53 | Zvolte množství, pokud chcete 54 | Rozbalit seznam jednotek 55 | ¾ 56 | ¾]]> 57 | 1 58 | 1 59 | číslo 60 | žádná jednotka 61 | velikost 62 | XS 63 | S 64 | M 65 | L 66 | XL 67 | ¼ 68 | ¼]]> 69 | 70 | ⅓]]> 71 | ½ 72 | ½]]> 73 | Smazat text 74 | Zrušit 75 | Zrušit 76 | zlomek 77 | Upravit poznámku 78 | Přidat poznámku 79 | Poznámka obnovena 80 | Všechny poznámky obnoveny 81 | %1$s poznámky %2$s.csv 82 | %1$s, %2$s 83 | %1$s poznámky %2$s filtr %3$s.csv 84 | Filtrovat poznámky 85 | Vymazat 86 | Filtrovat 87 | Zavřít filtr 88 | 89 | %1$d poznámka z %2$d 90 | %1$d poznámky z %2$d 91 | %1$d poznámek z %2$d 92 | %1$d poznámek z %2$d 93 | 94 | Uložit 95 | Filtrovat podobné poznámky 96 | Zkopírovat do schránky 97 | Zkopírováno 98 | Zrušit výběr poznámky 99 | Vybraná poznámka 100 |
101 | -------------------------------------------------------------------------------- /app/src/main/res/values-de/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Über 4 | Zurück zur Liste der Notizen 5 | Abbrechen 6 | Bearbeiten 7 | Speichern 8 | Anwendungssymbol 9 | um %1$s 10 | vor %1$d s 11 | vor %1$d h %2$02d min 12 | vor %1$d min 13 | 14 | %1$d Notiz wurde importiert. 15 | %1$d Notizen wurden importiert. 16 | 17 | Bearbeiten 18 | Löschen 19 | Menü 20 | Import aus CSV 21 | Als CSV speichern 22 | Alle löschen 23 | Notiz hinzufügen 24 | Notiz mit demselben Text hinzufügen 25 | Alle Notizen löschen? 26 | Alle löschen 27 | Notiz löschen? 28 | Notiz löschen 29 | Notiz hinzugefügt 30 | Notiz bearbeitet 31 | Notiz gelöscht 32 | Alle Notizen gelöscht 33 | Fehlende Spalte „%1$s“ 34 | Leerer Wert der Spalte „%1$s“ 35 | Der Wert „%2$s“ der Spalte „%1$s“ konnte nicht analysiert werden 36 | Der Wert „%3$s“ der Spalte „%1$s“ muss einer der folgenden sein %2$s 37 | 38 | %1$d Notiz wurde übersprungen, weil bereits eine Notiz mit demselben Zeitstempel existiert. 39 | %1$d Notizen wurden übersprungen, weil bereits Notizen mit denselben Zeitstempeln existieren. 40 | 41 | 42 | ⅓]]> 43 | XS 44 | S 45 | M 46 | L 47 | XL 48 | ¼ 49 | ½ 50 | ½]]> 51 | Leere CSV-Datei 52 | Eingabe löschen 53 | keine Einheit 54 | ¾ 55 | ¾]]> 56 | 1 57 | 1 58 | Nummer 59 | Größe 60 | ¼]]> 61 | Bruch 62 | Zurücknehmen 63 | Zurücknehmen 64 | Notiz bearbeiten 65 | Notiz hinzufügen 66 | Notiz wiederhergestellt 67 | Alle Notizen wiederhergestellt 68 | %1$s Notizen %2$s.csv 69 | v%2$s


%1$s ist eine nicht-kommerzielle App, die von drei FreundInnen zum Spaß gemacht wurde.


Möchtest du uns mitteilen, wie du die App benutzt? Schreib uns eine E-Mail.


Funktioniert etwas für dich nicht oder hast du Ideen für Verbesserungen? Melde ein Problem.


Verteilt unter GNU General Public License 3.0 oder höher. Du findest den Code und kannst dich auf GitHub beteiligen.

]]>
70 | Abbrechen 71 | Abbrechen 72 | Schreibe deine Notiz 73 | Deine Notizen (%1$d) 74 | Wähle eine Menge, wenn du willst 75 | Liste der Einheiten aufklappen 76 | Du bist dabei, alle Notizen zu löschen 77 | Du bist dabei, die Notiz „%1$s“ zu löschen 78 | %1$s, %2$s 79 | %1$s Notizen %2$s Filter %3$s.csv 80 | Filtere deine Notizen 81 | Eingabe löschen 82 | Filtern 83 | Filter schließen 84 | 85 | %1$d Notiz von %2$d 86 | %1$d Notizen von %2$d 87 | 88 | Speichern 89 | In Zwischenablage kopieren 90 | Ähnliche Notizen filtern 91 | Kopiert 92 | Notiz abwählen 93 | Ausgewählte Notiz 94 |
95 | -------------------------------------------------------------------------------- /app/src/main/res/values-fr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Icône de l’application 4 | v%2$s


%1$s est une application non commerciale créée par trois ami·es, pour le plaisir.


Vous souhaitez partager avec nous votre usage spécifique de l’application ? Écrivez-nous un email.


Une remarque ? une amélioration ? une incompréhension ? Faites-nous part de vos suggestions.


Distribué sous GNU General Public License 3.0 ou une version ultérieure. Le code est accessible sur GitHub.

]]>
5 | À propos 6 | Retour à la liste des notes 7 | Annuler 8 | Modifier 9 | Enregistrer 10 | Modifier 11 | Importer un fichier CSV 12 | Rédigez votre note 13 | Supprimer 14 | Menu 15 | Sauvegarder dans un fichier CSV 16 | Supprimer tout 17 | Vos notes (%1$d) 18 | Ajouter une note 19 | Ajouter une note avec le même texte 20 | Supprimer toutes les notes ? 21 | Vous êtes en train de supprimer toutes les notes 22 | Supprimer tout 23 | Supprimer la note ? 24 | Vous êtes en train de supprimer la note « %1$s » 25 | Supprimer la note 26 | Note ajoutée 27 | Note modifiée 28 | Note supprimée 29 | Annuler 30 | Toutes les notes supprimées 31 | Annuler 32 | à %1$s 33 | il y a %1$d s 34 | il y a %1$d min 35 | il y a %1$d h %2$02d min 36 | Colonne « %1$s » manquante 37 | Valeur vide de la colonne « %1$s » 38 | Échec de l’analyse de la valeur « %2$s » de la colonne « %1$s » 39 | La valeur « %3$s » de la colonne « %1$s » doit être l’une des suivantes %2$s 40 | 41 | %1$d note importée. 42 | %1$d notes importées. 43 | %1$d notes importées. 44 | 45 | 46 | %1$d note ignorée, car une note avec le même horodatage existe déjà. 47 | %1$d notes ignorées, car des notes avec les mêmes horodatages existent déjà. 48 | %1$d notes ignorées, car des notes avec les mêmes horodatages existent déjà. 49 | 50 | Fichier CSV vide 51 | Quantité (optionnel) 52 | Dérouler la liste des unités 53 | S 54 | L 55 | XL 56 | ¼ 57 | ¼]]> 58 | 59 | ⅓]]> 60 | ½ 61 | ½]]> 62 | ¾ 63 | 1 64 | M 65 | taille 66 | XS 67 | fraction 68 | ¾]]> 69 | 1 70 | nombre 71 | Effacer le texte 72 | Annuler 73 | Annuler 74 | aucune unité 75 | Modifier la note 76 | Note restaurée 77 | Toutes les notes restaurées 78 | Ajouter une note 79 | %1$s notes %2$s.csv 80 | %1$s, %2$s 81 | Filtrez vos notes 82 | Fermer le filtre 83 | 84 | %1$d note sur %2$d 85 | %1$d notes sur %2$d 86 | %1$d notes sur %2$d 87 | 88 | %1$s notes %2$s filtre %3$s.csv 89 | Effacer la saisie 90 | Filtrer 91 | Sauvegarder 92 | Filtrer les notes similaires 93 | Copier dans le presse-papiers 94 | Copié 95 | Désélectionner la note 96 | Note sélectionnée 97 |
98 | -------------------------------------------------------------------------------- /app/src/main/res/values-hu/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/values-iw/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | בחר כמות (אם ברצונך) 4 | הערך “%3$s” של העמודה “%1$s” חייב להיות אחד מ-%2$s 5 | v%2$s


%1$s היא אפליקציה לא מסחרית שנוצרה על ידי שלושה חברים בשביל הכיף.


אם תרצה לשתף אותנו כיצד אתה משתמש באפליקציה אתה מוזמן לכתוב לנו אימייל.


אם משהו לא עובד כראוי או שיש לך רעיונות לשיפור? שלח בעיה.


מופץ תחת רישיון ציבורי כללי של GNU 3.0 או האחרון. אתה יכול לצפות בקוד המקור ולהצטרף ב-GitHub.

]]>
6 | שמור כ-CSV 7 | אתה עומד למחוק את כל ההערות 8 | לפני %1$d שעות %2$02d דקות 9 | סמל אפלקציה 10 | אודות 11 | חזרה לרשימת ההערות 12 | ביטול 13 | עדכון 14 | שמור 15 | כתוב את ההערה שלך 16 | עדכון 17 | מחיקה 18 | תפריט 19 | ייבא מ-CSV 20 | מחק הכל 21 | הוסף הערה 22 | הוסף הערה עם אותו טקסט 23 | למחוק את כל ההערות? 24 | מחק הכל 25 | למחוק הערה? 26 | אתה עומד למחוק את ההערה \"%1$s\" 27 | מחק הערה 28 | הערה נוספה 29 | הערה עודכנה 30 | הערה נמחקה 31 | בטל 32 | כל ההערות נמחקו 33 | בטל 34 | ב-%1$s 35 | לפני %1$d שניות 36 | לפני %1$d דקות 37 | עמודה חסרה \"%1$s\" 38 | ערך ריק בעמודה “%1$s” 39 | כישלון בניתוח הערך “%2$s” של העמודה “%1$s” 40 | 41 | יובאה הערה %1$d. 42 | יובאו %1$d הערות. 43 | יובאו %1$d הערות. 44 | 45 | הרחב את רשימת היחידות 46 | נקה קלט 47 | סגירה 48 | סגירה 49 | אין יחידות 50 | גודל 51 | XS 52 | S 53 | M 54 | L 55 | XL 56 | חלק 57 | ¼ 58 | ¼]]> 59 | 60 | ½ 61 | ½]]> 62 | ¾ 63 | ¾]]> 64 | 1 65 | 1 66 | %1$s, %2$s 67 | עדכון הערה 68 | הוסף הערה 69 | כל ההערות שוחזרו 70 | %1$s הערות %2$s.csv 71 | %1$s הערות %2$s מסנן %3$s.csv 72 | סנן ההערות 73 | נקה פלט 74 | סינון 75 | 76 | הערה %1$d מתוך %2$d 77 | %1$d הערות מתוך %2$d 78 | %1$d הערות מתוך %2$d 79 | 80 | שמור 81 | סינון הערות דומות 82 | העתק ללוח 83 | הועתק 84 | בטל בחירת הערה 85 | הערה נבחרה 86 | ההערות שלך (%1$d) 87 | מספר 88 | קובץ CSV ריק 89 | הערה שוחזרה 90 | ⅓]]> 91 | סגירת סינון 92 | 93 | דילגנו על הערה %1$d, מכיוון שכבר קיימת הערה עם אותה חותמת זמן. 94 | דילגנו על %1$d הערות, מכיוון שכבר קיימות הערות עם אותן חותמות זמן. 95 | דילגנו על %1$d הערות, מכיוון שכבר קיימות הערות עם אותן חותמות זמן. 96 | 97 |
98 | -------------------------------------------------------------------------------- /app/src/main/res/values-pl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ikona aplikacji 4 | v%2$s


%1$s jest niekomercyjną aplikacją stworzoną przez troje przyjaciół dla przyjemności.


Czy zechciał(a)byś podzielić się z nami, jak z niej korzystasz? Napisz do nas maila.


Czy coś się u Ciebie nie sprawdza lub masz pomysły na udoskonalenie? Załóż temat.


Dystrybuowane na Powszechnej Licencji Publicznej GNU 3.0 lub późniejszej. Możesz znaleźć kod i zaangażować się na GitHub’ie.

]]>
5 | O aplikacji 6 | Anuluj 7 | Uaktualnij 8 | Zapisz 9 | Usuń 10 | Menu 11 | Importuj z CSV 12 | Zapisz jako CSV 13 | Usuń wszystko 14 | Dodaj notkę z tym samym tekstem 15 | Usunąć wszystkie notki? 16 | Napisz swoją notkę 17 | Dodaj notkę 18 | Powrót do listy notek 19 | Uaktualnij 20 | Twoje notki (%1$d) 21 | Usuń wszystkie 22 | Zamierzasz usunąć wszystkie notki 23 | Usunąć notkę? 24 | Zamierzasz usunąć notkę „%1$s” 25 | Usuń notkę 26 | Notkę uaktualniono 27 | Notkę usunięto 28 | Cofnij 29 | Wszystkie notki usunięto 30 | Cofnij 31 | o %1$s 32 | %1$d s temu 33 | %1$d min. temu 34 | %1$d h %2$02d min. temu 35 | Pusta wartość kolumny „%1$s” 36 | Nie udało się sparsować wartości „%2$s” kolumny „%1$s” 37 | 38 | Zaimportowano %1$d notkę. 39 | Zaimportowano %1$d notki. 40 | Zaimportowano %1$d notek. 41 | Zaimportowano %1$d notek. 42 | 43 | Wartość „%3$s” kolumny „%1$s” musi być jedną z %2$s 44 | Jeśli chcesz, wybierz ilość 45 | Plik CSV jest pusty 46 | Rozwiń listę jednostek 47 | Wyczyść wpis 48 | Odrzuć 49 | Odrzuć 50 | bez jednostki 51 | wielkość 52 | XS 53 | S 54 | M 55 | XL 56 | ¼ 57 | ¼]]> 58 | 59 | ⅓]]> 60 | ½ 61 | ½]]> 62 | ¾ 63 | ¾]]> 64 | 1 65 | 1 66 | liczba 67 | Uaktualnij notkę 68 | Dodaj notkę 69 | Wszystkie notki przywrócono 70 | %1$s notki %2$s.csv 71 | Notkę dodano 72 | Brakująca kolumna „%1$s” 73 | 74 | Pominięto %1$d notkę, gdyż notka o takim samym znaczniku czasowym już istnieje. 75 | Pominięto %1$d notki, gdyż notki o takich samych znacznikach czasowych już istnieją. 76 | Pominięto %1$d notek, gdyż notki o takich samych znacznikach czasowych już istnieją. 77 | Pominięto %1$d notek, gdyż notki o takich samych znacznikach czasowych już istnieją. 78 | 79 | L 80 | Notkę przywrócono 81 | ułamek 82 | Pusta treść 83 | Filtr 84 | Zamknij filtr 85 | Zapisz 86 | Kopiuj do Schowka 87 | Skopiowano 88 | Zaznacz notkę 89 | 90 | %1$d notka z %2$d 91 | %1$d notki z %2$d 92 | %1$d notek z %2$d 93 | %1$d notek z %2$d 94 | 95 | %1$s, %2$s 96 | %1$s notes %2$s filter %3$s.csv 97 | Przefiltruj swe notki 98 | Przefiltruj podobne notki 99 | Odznacz notkę 100 |
101 | -------------------------------------------------------------------------------- /app/src/main/res/values-pt-rBR/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ícone do aplicativo 4 | Sobre 5 | Voltar para a lista de anotações 6 | Cancelar 7 | Atualizar 8 | Salvar 9 | Escreva sua anotação 10 | Atualizar 11 | Remover 12 | Menu 13 | Importar de CSV 14 | Salvar como CSV 15 | Remover tudo 16 | Suas anotações (%1$d) 17 | Adicionar anotação 18 | Adicionar nova anotação com o mesmo texto 19 | Remover todas as anotações? 20 | Você está prestes a remover todas as anotações 21 | Remover tudo 22 | Remover anotação? 23 | Você está prestes a remover a nota “%1$s” 24 | Remover anotação 25 | Anotação adicionada 26 | Anotação atualizada 27 | Anotação removida 28 | Desfazer 29 | Todas as anotações removidas 30 | Desfazer 31 | em %1$s 32 | %1$d s atrás 33 | %1$d min atrás 34 | %1$d h %2$02d min atrás 35 | Coluna “%1$s” faltando 36 | Valor vazio na coluna “%1$s” 37 | Valor “%3$s” da coluna “%1$s” precisa ser um de %2$s 38 | 39 | %1$d anotação importada. 40 | %1$d anotações importadas. 41 | %1$d anotações importadas. 42 | 43 | Arquivo CSV vazio 44 | Escolha um valor, se quiser 45 | Expandir lista de unidades 46 | Limpar texto 47 | Dispensar 48 | Dispensar 49 | sem unidade 50 | tamanho 51 | PP 52 | P 53 | M 54 | G 55 | GG 56 | fração 57 | ¼ 58 | ¼]]> 59 | 60 | ⅓]]> 61 | ½ 62 | ½]]> 63 | ¾ 64 | ¾]]> 65 | 1 66 | 1 67 | número 68 | Atualizar anotação 69 | Adicionar anotação 70 | Anotação restaurada 71 | Todas as anotações restauradas 72 | %1$s anotações %2$s.csv 73 | v%2$s


%1$s é um aplicativo não-comercial feito por três amigos por diversão.


Gostaria de compartilhar como você usa o aplicativo? Escreva um email para nós.


Alguma coisa não funciona para você ou você tem ideias para melhoria? Crie uma issue.


Distribuído sob a GNU General Public License 3.0 ou superior. Você pode encontrar o código e participar no GitHub.

]]>
74 | Não foi possível converter valor “%2$s” da coluna “%1$s” 75 | 76 | %1$d anotação ignorada, porque uma anotação com a mesma data e hora já existe. 77 | %1$d anotações ignoradas, porque anotações com a mesma data e hora já existem. 78 | %1$d anotações ignoradas, porque anotações com a mesma data e hora já existem. 79 | 80 |
81 | -------------------------------------------------------------------------------- /app/src/main/res/values-ru/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Значок приложения 4 | О приложении 5 | Вернемся к списку заметок 6 | Отмена 7 | Обновить 8 | Сохранить 9 | Напиши свою заметку 10 | Обновить 11 | Удалить 12 | Меню 13 | Сохранить как CSV 14 | Удалить все 15 | Ваши заметки (%1$d) 16 | Добавить заметку 17 | Добавить новую заметку с тем же текстом 18 | Удалить все заметки? 19 | Вы собираетесь удалить все заметки 20 | Удалить все 21 | Удалить заметку? 22 | Удалить заметку 23 | Заметка добавлена 24 | Заметка обновлена 25 | Заметка удалена 26 | Вернуть 27 | Вернуть 28 | в %1$s 29 | %1$d с назад 30 | %1$d мин назад 31 | Отсутствие столбца «%1$s» 32 | Импорт из CSV 33 | %1$d ч %2$02d мин назад 34 | Вы собираетесь удалить заметку «%1$s» 35 | Все заметки были удалены 36 | Пустое значение колонки «%1$s» 37 | ¼ 38 | ¼]]> 39 | 40 | ⅓]]> 41 | ½ 42 | ½]]> 43 | ¾ 44 | ¾]]> 45 | 1 46 | 1 47 | Обновить заметку 48 | Добавить заметку 49 | Пустой CSV файл 50 | Выберите сумму, если хотите 51 | Четкий ввод данных 52 | Увольнять 53 | номер 54 | Записка восстановлена 55 | Все заметки восстановлены 56 | размер 57 | Расширить список объектов 58 | v%2$s


%1$s это некоммерческое приложение, созданное тремя друзьями для развлечения.


Не хотели бы вы поделиться с нами тем, как вы используете приложение? Напишите нам электронная почта.


У вас что-то не работает или у вас есть идеи по улучшению? Отправьте запрос вопрос.


Распространяется под GNU Общая публичная лицензия 3.0 или позже. Вы можете найти код и принять участие на GitHub.

]]>
59 | 60 | Импортированная %1$d записи. 61 | Импортированные %1$d записи. 62 | Импортированные %1$d записи. 63 | Импортированные %1$d записи. 64 | 65 | Увольнять 66 | XS 67 | S 68 | M 69 | L 70 | XL 71 | %1$s заметки %2$s.csv 72 | %1$s, %2$s 73 | Фильтруйте свои заметки 74 | Очистить ввод 75 | Отфильтровать 76 | Закрыть фильтр 77 | Сохранить 78 | Скопировано 79 | Убрать выбор заметки 80 | Выбранная заметка 81 | Фильтровать подобные заметки 82 | Копировать в буфер обмена 83 | Не удалось разобрать значение \"%2$s\" из столбца \"%1$s\" 84 | без единицы 85 | часть 86 |
87 | -------------------------------------------------------------------------------- /app/src/main/res/values-sl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/values-uk/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Значок програми 4 | В 5 | Відмінивши 6 | Оновлення 7 | Зберігши 8 | Напишіть свою записку 9 | Повернутися до списку нотаток 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values-v29/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #513F99 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | traced it 3 | Application icon 4 | v%2$s


%1$s is a non-commercial app made by three friends for fun.


Would you like to share with us how you use the app? Write us an email.


Does something not work for you or do you have ideas for improvement? File an issue.


Distributed under GNU General Public License 3.0 or later. You can find the code and get involved on GitHub.

]]>
5 | About 6 | Back to the list of notes 7 | Cancel 8 | Update 9 | Save 10 | Write your note 11 | Update 12 | Delete 13 | Menu 14 | Import from CSV 15 | Save as CSV 16 | Delete all 17 | Your notes (%1$d) 18 | Add note 19 | Add note with same text 20 | Delete all notes? 21 | You are about to delete all notes 22 | Delete all 23 | Delete note? 24 | You are about to delete the note “%1$s” 25 | Delete note 26 | Note added 27 | Note updated 28 | Note deleted 29 | Undo 30 | All notes deleted 31 | Undo 32 | at %1$s 33 | %1$d s ago 34 | %1$d min ago 35 | %1$d h %2$02d min ago 36 | Missing column “%1$s” 37 | Empty value of column “%1$s” 38 | Failed to parse value “%2$s” of column “%1$s” 39 | Value “%3$s” of column “%1$s” must be one of %2$s 40 | 41 | Imported %1$d note. 42 | Imported %1$d notes. 43 | 44 | 45 | Skipped %1$d note, because a note with the same timestamp already exists. 46 | Skipped %1$d notes, because notes with the same timestamps already exist. 47 | 48 | Empty CSV file 49 | 50 | Choose an amount, if you want 51 | Expand list of units 52 | Clear input 53 | Dismiss 54 | Dismiss 55 | no unit 56 | size 57 | XS 58 | S 59 | M 60 | L 61 | XL 62 | fraction 63 | ¼ 64 | ¼]]> 65 | 66 | ⅓]]> 67 | ½ 68 | ½]]> 69 | ¾ 70 | ¾]]> 71 | 1 72 | 1 73 | portion 74 | 1x 75 | 2x 76 | 3x 77 | 4x 78 | 5x 79 | number 80 | 81 | %1$s, %2$s 82 | Update note 83 | Add note 84 | Note restored 85 | All notes restored 86 | %1$s notes %2$s.csv 87 | %1$s notes %2$s filter %3$s.csv 88 | Filter your notes 89 | Clear input 90 | Filter 91 | Close filter 92 | 93 | %1$d note out of %2$d 94 | %1$d notes out of %2$d 95 | 96 | Save 97 | Filter similar notes 98 | Copy to clipboard 99 | Copied 100 | Deselect note 101 | Selected note 102 |
103 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 |