commit 2c5d9b6df1d2eaa5f3ecea1f051139fdccf2e950 Author: Andres Gomez, Thomas (ITDV RL) Date: Mon Sep 1 09:56:51 2025 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab7bd38 --- /dev/null +++ b/.gitignore @@ -0,0 +1,77 @@ +# Built application files +*.apk +*.aar +*.ap_ +*.aab +# Files for the ART/Dalvik VM +*.dex +# Java class files +*.class +# Generated files +bin/ +gen/ +out/ +# App Release Files +app/release/* + +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ +# Gradle files +.gradle/ +build/ +# Local configuration file (sdk path, etc) +local.properties +# Proguard folder generated by Eclipse +proguard/ +# Log Files +*.log +# Android Studio Navigation editor temp files +.navigation/ +# Android Studio captures folder +captures/ +# IntelliJ +*.iml +.idea/ +# .idea/workspace.xml +# .idea/tasks.xml +# .idea/gradle.xml +# .idea/assetWizardSettings.xml +# .idea/dictionaries +.idea/libraries +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +*.jks +*.keystore +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ +# Google Services (e.g. APIs or Firebase) +google-services.json +# Freeline +freeline.py +freeline/ +freeline_project_description.json +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md +# Version control +vcs.xml +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ +# MacOS +.DS_Store +# App Specific cases +app/release/output.json +.idea/codeStyles/ \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..b2cf928 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Headache \ No newline at end of file diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml new file mode 100644 index 0000000..b976aee --- /dev/null +++ b/.idea/androidTestResultsUserPreferences.xml @@ -0,0 +1,48 @@ + + + + + + \ No newline at end of file diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml new file mode 100644 index 0000000..371f2e2 --- /dev/null +++ b/.idea/appInsightsSettings.xml @@ -0,0 +1,26 @@ + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..f09be69 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml new file mode 100644 index 0000000..8102a89 --- /dev/null +++ b/.idea/dictionaries/project.xml @@ -0,0 +1,9 @@ + + + + ibuprofene + ibuprofène + spifen + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..7061a0d --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,61 @@ + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..f4211b3 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,1167 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..28b4b57 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,122 @@ +import java.nio.charset.Charset + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.jetbrains.kotlin.serialization) + alias(libs.plugins.hilt) + alias(libs.plugins.ksp) +} + +android { + namespace = "com.pixelized.headache" + compileSdk = 36 + + signingConfigs { + create("pixelized") { + storeFile = + (project.properties["PIXELIZED_RELEASE_STORE_FILE"] as? String)?.let { file(it) } + storePassword = project.properties["PIXELIZED_RELEASE_STORE_PASSWORD"] as? String + keyAlias = project.properties["PIXELIZED_RELEASE_KEY_ALIAS"] as? String + keyPassword = project.properties["PIXELIZED_RELEASE_KEY_PASSWORD"] as? String + } + } + + defaultConfig { + namespace = "com.pixelized.headache" + minSdk = 26 + targetSdk = 35 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + debug { + applicationIdSuffix = ".dev" + isDebuggable = true + isMinifyEnabled = false + defaultConfig { + versionCode = 1 + } + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + release { + isDebuggable = false + isMinifyEnabled = true + signingConfig = signingConfigs.getByName("pixelized") + defaultConfig { + versionCode = 1 // getGitBuildNumber() + } + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + compose = true + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + implementation(libs.androidx.material3.windowsizeclass) + implementation(libs.androidx.adaptive.layout) + implementation(libs.androidx.material3.navigation3) + + implementation(libs.androidx.datastore) + implementation(libs.kotlinx.serialization.core) + implementation(libs.kotlinx.serialization.json) + implementation(libs.androidx.navigation3.runtime) + implementation(libs.androidx.navigation3.ui) + implementation(libs.androidx.hilt.navigation.compose) + implementation(libs.androidx.lifecycle.viewmodel.navigation3) + implementation(libs.androidx.material.icons.extended) + + implementation(libs.hilt.android) + ksp(libs.hilt.compiler) + + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) +} + +private fun getGitBuildNumber( + charset: Charset = Charset.defaultCharset(), +): Int { + return try { + val stdout = org.apache.commons.io.output.ByteArrayOutputStream() + rootProject.exec { + commandLine("git", "rev-list", "--count", "HEAD") + standardOutput = stdout + } + stdout.toString(charset).trim().toInt() + } catch (e: Exception) { + 0 + } +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/release/baselineProfiles/0/app-release.dm b/app/release/baselineProfiles/0/app-release.dm new file mode 100644 index 0000000..00bf25f Binary files /dev/null and b/app/release/baselineProfiles/0/app-release.dm differ diff --git a/app/release/baselineProfiles/1/app-release.dm b/app/release/baselineProfiles/1/app-release.dm new file mode 100644 index 0000000..8ebb62a Binary files /dev/null and b/app/release/baselineProfiles/1/app-release.dm differ diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json new file mode 100644 index 0000000..54e13b3 --- /dev/null +++ b/app/release/output-metadata.json @@ -0,0 +1,37 @@ +{ + "version": 3, + "artifactType": { + "type": "APK", + "kind": "Directory" + }, + "applicationId": "com.pixelized.headache", + "variantName": "release", + "elements": [ + { + "type": "SINGLE", + "filters": [], + "attributes": [], + "versionCode": 1, + "versionName": "1.0", + "outputFile": "app-release.apk" + } + ], + "elementType": "File", + "baselineProfiles": [ + { + "minApi": 28, + "maxApi": 30, + "baselineProfiles": [ + "baselineProfiles/1/app-release.dm" + ] + }, + { + "minApi": 31, + "maxApi": 2147483647, + "baselineProfiles": [ + "baselineProfiles/0/app-release.dm" + ] + } + ], + "minSdkVersionForDexing": 26 +} \ No newline at end of file diff --git a/app/src/debug/ic_launcher-playstore.png b/app/src/debug/ic_launcher-playstore.png new file mode 100644 index 0000000..30cf28d Binary files /dev/null and b/app/src/debug/ic_launcher-playstore.png differ diff --git a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher.webp b/app/src/debug/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..9162fba Binary files /dev/null and b/app/src/debug/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..27baa4b Binary files /dev/null and b/app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/debug/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..fd8a5b8 Binary files /dev/null and b/app/src/debug/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher.webp b/app/src/debug/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4210ac1 Binary files /dev/null and b/app/src/debug/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/debug/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..b1fa059 Binary files /dev/null and b/app/src/debug/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/debug/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..5a6a937 Binary files /dev/null and b/app/src/debug/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher.webp b/app/src/debug/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..829018d Binary files /dev/null and b/app/src/debug/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..c999bbf Binary files /dev/null and b/app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..8dfe59d Binary files /dev/null and b/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..9b50693 Binary files /dev/null and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..c1e346d Binary files /dev/null and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9829b4e Binary files /dev/null and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..6a4f590 Binary files /dev/null and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..35831c3 Binary files /dev/null and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..18caecc Binary files /dev/null and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/debug/res/values/ic_launcher_background.xml b/app/src/debug/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..58043be --- /dev/null +++ b/app/src/debug/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #212121 + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d96f6d1 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..02b7847 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/pixelized/headache/HeadacheApplication.kt b/app/src/main/java/com/pixelized/headache/HeadacheApplication.kt new file mode 100644 index 0000000..aa5ce3f --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/HeadacheApplication.kt @@ -0,0 +1,7 @@ +package com.pixelized.headache + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class HeadacheApplication : Application() \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/MainActivity.kt b/app/src/main/java/com/pixelized/headache/MainActivity.kt new file mode 100644 index 0000000..2df193a --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/MainActivity.kt @@ -0,0 +1,50 @@ +package com.pixelized.headache + +import android.Manifest.permission.READ_CALENDAR +import android.Manifest.permission.WRITE_CALENDAR +import android.content.pm.PackageManager.PERMISSION_GRANTED +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.material3.Surface +import androidx.core.content.ContextCompat +import com.pixelized.headache.ui.navigation.Navigator +import com.pixelized.headache.ui.page.MainPage +import com.pixelized.headache.ui.theme.HeadacheTheme +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + + @Inject + lateinit var navigator: Navigator + + private val requestPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + if (ContextCompat.checkSelfPermission(this, READ_CALENDAR) != PERMISSION_GRANTED) { + requestPermissionLauncher.launch(READ_CALENDAR) + } + if (ContextCompat.checkSelfPermission(this, WRITE_CALENDAR) != PERMISSION_GRANTED) { + requestPermissionLauncher.launch(WRITE_CALENDAR) + } + + setContent { + HeadacheTheme { + Surface { + MainPage( + navigator = navigator, + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/module/AppModule.kt b/app/src/main/java/com/pixelized/headache/module/AppModule.kt new file mode 100644 index 0000000..ccb74ad --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/module/AppModule.kt @@ -0,0 +1,18 @@ +package com.pixelized.headache.module + +import com.pixelized.headache.ui.navigation.Navigator +import com.pixelized.headache.ui.navigation.destination.HomePageDestination +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent +import dagger.hilt.android.scopes.ActivityRetainedScoped + +@Module +@InstallIn(ActivityRetainedComponent::class) +object AppModule { + + @Provides + @ActivityRetainedScoped + fun provideNavigator(): Navigator = Navigator(startDestination = HomePageDestination) +} diff --git a/app/src/main/java/com/pixelized/headache/repository/calendar/GoogleCalendar.kt b/app/src/main/java/com/pixelized/headache/repository/calendar/GoogleCalendar.kt new file mode 100644 index 0000000..ae9e977 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/repository/calendar/GoogleCalendar.kt @@ -0,0 +1,9 @@ +package com.pixelized.headache.repository.calendar + +data class GoogleCalendar( + val id: Long, + val name: String, + val account: String, + val type: String, + val color: Int, +) diff --git a/app/src/main/java/com/pixelized/headache/repository/calendar/GoogleCalendarIdRepository.kt b/app/src/main/java/com/pixelized/headache/repository/calendar/GoogleCalendarIdRepository.kt new file mode 100644 index 0000000..ebb0b78 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/repository/calendar/GoogleCalendarIdRepository.kt @@ -0,0 +1,75 @@ +package com.pixelized.headache.repository.calendar + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.longPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject +import javax.inject.Singleton + + +@Singleton +class GoogleCalendarIdRepository @Inject constructor( + @param:ApplicationContext private val context: Context, +) { + val scope = CoroutineScope(Dispatchers.IO) + + // At the top level of your kotlin file: + val Context.dataStore: DataStore by preferencesDataStore(name = CALENDAR_ID_STORE) + + val calendarId: StateFlow = context.dataStore.data + .map { preferences -> preferences[ID_KEY] ?: ID_DEFAULT } + .stateIn( + scope = scope, + started = SharingStarted.Lazily, + initialValue = ID_DEFAULT, + ) + + val calendarName: StateFlow = context.dataStore.data + .map { preferences -> preferences[NAME_KEY] ?: NAME_DEFAULT } + .stateIn( + scope = scope, + started = SharingStarted.Lazily, + initialValue = NAME_DEFAULT, + ) + + val calendarType: StateFlow = context.dataStore.data + .map { preferences -> preferences[TYPE_KEY] ?: TYPE_DEFAULT } + .stateIn( + scope = scope, + started = SharingStarted.Lazily, + initialValue = TYPE_DEFAULT, + ) + + suspend fun toggleCalendar( + id: Long, + name: String, + type: String, + ) { + context.dataStore.edit { preferences -> + preferences[ID_KEY] = id + preferences[NAME_KEY] = name + preferences[TYPE_KEY] = type + } + } + + companion object Companion { + private const val CALENDAR_ID_STORE = "CalendarIdStore" + private const val ID_DEFAULT = 3L + private const val NAME_DEFAULT = "Céphalée" + private const val TYPE_DEFAULT = "com.google" + private val ID_KEY = longPreferencesKey("calendar_id") + private val NAME_KEY = stringPreferencesKey("calendar_name") + private val TYPE_KEY = stringPreferencesKey("calendar_type") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/repository/calendar/GoogleCalendarRepository.kt b/app/src/main/java/com/pixelized/headache/repository/calendar/GoogleCalendarRepository.kt new file mode 100644 index 0000000..1b34888 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/repository/calendar/GoogleCalendarRepository.kt @@ -0,0 +1,55 @@ +package com.pixelized.headache.repository.calendar + +import android.content.Context +import android.provider.CalendarContract +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class GoogleCalendarRepository @Inject constructor( + @param:ApplicationContext private val context: Context, +) { + private val _calendarFlow = MutableStateFlow(listCalendars()) + val calendarFlow: StateFlow> = _calendarFlow + + private fun listCalendars(): List { + val calendars = mutableListOf() + val projection = arrayOf( + CalendarContract.Calendars._ID, + CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, + CalendarContract.Calendars.ACCOUNT_NAME, + CalendarContract.Calendars.ACCOUNT_TYPE, + CalendarContract.Calendars.CALENDAR_COLOR, + ) + val cursor = context.contentResolver.query( + CalendarContract.Calendars.CONTENT_URI, + projection, + null, + null, + null + ) + + cursor?.use { + val idIdx = it.getColumnIndex(CalendarContract.Calendars._ID) + val nameIdx = it.getColumnIndex(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME) + val accountIdx = it.getColumnIndex(CalendarContract.Calendars.ACCOUNT_NAME) + val typeIdx = it.getColumnIndex(CalendarContract.Calendars.ACCOUNT_TYPE) + val colorIdx = it.getColumnIndex(CalendarContract.Calendars.CALENDAR_COLOR) + + while (it.moveToNext()) { + val calendar = GoogleCalendar( + id = it.getLong(idIdx), + name = it.getString(nameIdx), + account = it.getString(accountIdx), + type = it.getString(typeIdx), + color = it.getInt(colorIdx), + ) + calendars.add(calendar) + } + } + return calendars + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/repository/event/Event.kt b/app/src/main/java/com/pixelized/headache/repository/event/Event.kt new file mode 100644 index 0000000..533c8c3 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/repository/event/Event.kt @@ -0,0 +1,98 @@ +package com.pixelized.headache.repository.event + +data class Event( + val id: Long, + val title: String, + val description: String?, + val date: Date, + val pills: List, + val isValid: Boolean, + val isMisspelled: Boolean, +) { + data class Date( + val day: Int, + val month: Int, + val year: Int, + ) : Comparable { + override fun compareTo(other: Date): Int = when { + year < other.year -> -1 + year > other.year -> 1 + month < other.month -> -1 + month > other.month -> 1 + day < other.day -> -1 + day > other.day -> 1 + else -> 0 + } + } + + sealed class Pill( + val label: String, + val amount: Int, + val description: String, + val isValid: Boolean, + val isMisspelled: Boolean, + ) { + class Unknown( + amount: Int, + description: String, + ) : Pill( + label = "?", + amount = amount, + description = description, + isValid = false, + isMisspelled = false, + ) + + class Ibuprofene400( + amount: Int, + description: String, + isValid: Boolean, + isMisspelled: Boolean, + ) : Pill( + label = "Ibuprofène 400", + amount = amount, + description = description, + isValid = isValid, + isMisspelled = isMisspelled, + ) + + class Paracetamol1000( + amount: Int, + description: String, + isValid: Boolean, + isMisspelled: Boolean, + ) : Pill( + label = "Paracétamol 1000", + amount = amount, + description = description, + isValid = isValid, + isMisspelled = isMisspelled, + ) + + class Spifen400( + amount: Int, + description: String, + isValid: Boolean, + isMisspelled: Boolean, + ) : Pill( + label = "Spifen 400", + amount = amount, + description = description, + isValid = isValid, + isMisspelled = isMisspelled, + ) + + class Eletriptan40( + amount: Int, + description: String, + isValid: Boolean, + isMisspelled: Boolean, + ) : Pill( + label = "Élétriptan 40", + amount = amount, + description = description, + isValid = isValid, + isMisspelled = isMisspelled, + ) + } +} diff --git a/app/src/main/java/com/pixelized/headache/repository/event/EventFactory.kt b/app/src/main/java/com/pixelized/headache/repository/event/EventFactory.kt new file mode 100644 index 0000000..52cf15d --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/repository/event/EventFactory.kt @@ -0,0 +1,158 @@ +package com.pixelized.headache.repository.event + + +import android.icu.util.Calendar +import com.pixelized.headache.utils.extention.event +import java.util.Date +import java.util.regex.Pattern +import javax.inject.Inject + +class EventFactory @Inject constructor() { + private val amountSplitRegex = Pattern.compile("""\s[xX]\s*""") + private val calendar = Calendar.getInstance() + + fun parseEvent( + start: Long, + id: Long, + title: String, + description: String, + ): Event { + val date = calendar.apply { time = Date(start) }.event + val pills = description + .takeIf { it.isNotBlank() } + ?.split("\n") + ?.mapNotNull { + when { + it.isBlank() -> null + else -> parsePill(it) + } + } + ?: listOf() + + return Event( + id = id, + title = title, + description = description, + date = date, + pills = pills, + isValid = pills.all { it.isValid }, + isMisspelled = pills.any { it.isMisspelled }, + ) + } + + @Throws(Exception::class) + fun parsePill(pill: String): Event.Pill { + val split = pill.split(regex = amountSplitRegex) + val label = split.getOrNull(0) ?: error("missing label") + val amount = split.getOrNull(1)?.trim()?.toIntOrNull() ?: 1 + + return when { + label.contains(SPIFEN_400, ignoreCase = true) -> Event.Pill.Spifen400( + amount = amount, + description = pill, + isValid = true, + isMisspelled = false, + ) + + label.contains(ELETRIPTAN_40, ignoreCase = true) -> Event.Pill.Eletriptan40( + amount = amount, + description = pill, + isValid = true, + isMisspelled = false, + ) + + label.contains(IBUPROFENE_400, ignoreCase = true) -> Event.Pill.Ibuprofene400( + amount = amount, + description = pill, + isValid = true, + isMisspelled = false, + ) + + label.contains(PARACETAMOL_1000, ignoreCase = true) -> Event.Pill.Paracetamol1000( + amount = amount, + description = pill, + isValid = true, + isMisspelled = false, + ) + + SPIFEN_400_LIST.any { label.contains(it, ignoreCase = true) } -> Event.Pill.Spifen400( + amount = amount, + description = pill, + isValid = true, + isMisspelled = true, + ) + + ELETRIPTAN_40_LIST.any { + label.contains( + it, + ignoreCase = true + ) + } -> Event.Pill.Eletriptan40( + amount = amount, + description = pill, + isValid = true, + isMisspelled = true, + ) + + IBUPROFENE_400_LIST.any { + label.contains( + it, + ignoreCase = true + ) + } -> Event.Pill.Ibuprofene400( + amount = amount, + description = pill, + isValid = true, + isMisspelled = true, + ) + + PARACETAMOL_1000_LIST.any { + label.contains( + it, + ignoreCase = true + ) + } -> Event.Pill.Paracetamol1000( + amount = amount, + description = pill, + isValid = true, + isMisspelled = true, + ) + + else -> Event.Pill.Unknown( + amount = amount, + description = pill, + ) + } + } + + companion object { + private const val PARACETAMOL_1000 = "Paracétamol 1000" + private val PARACETAMOL_1000_LIST = listOf( + "Doliprane 1000", + ) + + private const val IBUPROFENE_400 = "Ibuprofène 400" + private val IBUPROFENE_400_LIST = listOf( + "Ibuprofene 400", + "Ibuprofen 400", + "Ibuprofen", + ) + + private const val SPIFEN_400 = "Spifen 400" + private val SPIFEN_400_LIST = listOf( + "Spifen", + ) + + private const val ELETRIPTAN_40 = "Élétriptan 40" + private val ELETRIPTAN_40_LIST = listOf( + "Eletriptan 40", + "Életriptan 40", + "Élitriptan 40", + "Elitriptan 40", + "Eletripan 40", + "Elétripan 40", + "Elitripan 40", + "Elitripan", + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/repository/event/EventRepository.kt b/app/src/main/java/com/pixelized/headache/repository/event/EventRepository.kt new file mode 100644 index 0000000..39b2de5 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/repository/event/EventRepository.kt @@ -0,0 +1,108 @@ +package com.pixelized.headache.repository.event + +import android.content.Context +import android.icu.util.Calendar +import android.provider.CalendarContract +import com.pixelized.headache.repository.calendar.GoogleCalendarIdRepository +import com.pixelized.headache.utils.extention.event +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class EventRepository @Inject constructor( + @param:ApplicationContext private val context: Context, + private val calendarIdRepository: GoogleCalendarIdRepository, + private val factory: EventFactory, +) { + private val scope = CoroutineScope(Dispatchers.IO + Job()) + private val eventFlow = hashMapOf>>() + + init { + scope.launch { + calendarIdRepository.calendarId.collect { id -> + fetchEvents( + calendarId = id, + startDate = Event.Date(day = 1, month = Calendar.JANUARY, year = 2023), + endDate = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, 1) }.event, + refresh = true, + ) + } + } + } + + fun eventFlow( + calendarId: Long, + ): StateFlow> { + return eventFlow.getOrPut(calendarId) { MutableStateFlow(emptyList()) } + } + + fun fetchEvents( + calendarId: Long, + startDate: Event.Date, + endDate: Event.Date, + refresh: Boolean, + ): List { + val flow = eventFlow.getOrPut(calendarId) { MutableStateFlow(emptyList()) } + if (flow.value.isNotEmpty() && refresh.not()) { + return flow.value + } + + val events = mutableListOf() + + val startCalendar = Calendar.getInstance().apply { this.event = startDate } + val endCalendar = Calendar.getInstance().apply { this.event = endDate } + + val projection = arrayOf( + CalendarContract.Events._ID, + CalendarContract.Events.TITLE, + CalendarContract.Events.DTSTART, + CalendarContract.Events.DESCRIPTION + ) + + val selection = ("${CalendarContract.Events.CALENDAR_ID} = ? AND " + + "${CalendarContract.Events.DTSTART} >= ? AND " + + "${CalendarContract.Events.DTSTART} <= ?") + + val selectionArgs = arrayOf( + calendarId.toString(), + startCalendar.timeInMillis.toString(), + endCalendar.timeInMillis.toString() + ) + + val cursor = context.contentResolver.query( + CalendarContract.Events.CONTENT_URI, + projection, + selection, + selectionArgs, + "${CalendarContract.Events.DTSTART} ASC" + ) + + cursor?.use { + val idIdx = it.getColumnIndex(CalendarContract.Events._ID) + val titleIdx = it.getColumnIndex(CalendarContract.Events.TITLE) + val descIdx = it.getColumnIndex(CalendarContract.Events.DESCRIPTION) + val startIdx = it.getColumnIndex(CalendarContract.Events.DTSTART) + + while (it.moveToNext()) { + val event = factory.parseEvent( + id = it.getLong(idIdx), + title = it.getString(titleIdx) ?: "", + description = it.getString(descIdx) ?: "", + start = it.getLong(startIdx), + ) + events.add(event) + } + } + + flow.value = events + + return events + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/navigation/MainNavDisplay.kt b/app/src/main/java/com/pixelized/headache/ui/navigation/MainNavDisplay.kt new file mode 100644 index 0000000..1551482 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/navigation/MainNavDisplay.kt @@ -0,0 +1,45 @@ +package com.pixelized.headache.ui.navigation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberSavedStateNavEntryDecorator +import androidx.navigation3.ui.NavDisplay +import androidx.navigation3.ui.rememberSceneSetupNavEntryDecorator +import com.pixelized.headache.ui.navigation.destination.calendarChooserDestinationEntry +import com.pixelized.headache.ui.navigation.destination.eventDestinationEntry +import com.pixelized.headache.ui.navigation.destination.homeDestinationEntry +import com.pixelized.headache.ui.navigation.destination.monthSummaryDestinationEntry + +val LocalNavigator = staticCompositionLocalOf { + error("Local Navigation no yet ready") +} + +@Composable +fun MainNavDisplay( + navigator: Navigator, +) { + CompositionLocalProvider( + LocalNavigator provides navigator, + ) { + NavDisplay( + backStack = navigator.backStack, + entryDecorators = listOf( + rememberSceneSetupNavEntryDecorator(), + rememberSavedStateNavEntryDecorator(), + rememberViewModelStoreNavEntryDecorator() + ), + onBack = { + navigator.popBackstack() + }, + entryProvider = entryProvider { + homeDestinationEntry() + calendarChooserDestinationEntry() + eventDestinationEntry() + monthSummaryDestinationEntry() + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/navigation/Navigator.kt b/app/src/main/java/com/pixelized/headache/ui/navigation/Navigator.kt new file mode 100644 index 0000000..262d9e6 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/navigation/Navigator.kt @@ -0,0 +1,18 @@ +package com.pixelized.headache.ui.navigation + +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.snapshots.SnapshotStateList +import dagger.hilt.android.scopes.ActivityRetainedScoped + +@ActivityRetainedScoped +class Navigator( + startDestination: Any, +) { + val backStack: SnapshotStateList = mutableStateListOf(startDestination) + + fun popBackstack() = backStack.removeLastOrNull() + + fun goTo(destination: Any) { + backStack.add(destination) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/navigation/destination/CalendarChooserDestination.kt b/app/src/main/java/com/pixelized/headache/ui/navigation/destination/CalendarChooserDestination.kt new file mode 100644 index 0000000..de619b0 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/navigation/destination/CalendarChooserDestination.kt @@ -0,0 +1,18 @@ +package com.pixelized.headache.ui.navigation.destination + +import androidx.navigation3.runtime.EntryProviderBuilder +import androidx.navigation3.runtime.entry +import com.pixelized.headache.ui.navigation.Navigator +import com.pixelized.headache.ui.page.calendar.CalendarChooserPage + +data object CalendarChooserDestination + +fun EntryProviderBuilder<*>.calendarChooserDestinationEntry() { + entry { + CalendarChooserPage() + } +} + +fun Navigator.navigateToCalendarChooserPage() { + goTo(CalendarChooserDestination) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/navigation/destination/EventDestination.kt b/app/src/main/java/com/pixelized/headache/ui/navigation/destination/EventDestination.kt new file mode 100644 index 0000000..2594350 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/navigation/destination/EventDestination.kt @@ -0,0 +1,44 @@ +package com.pixelized.headache.ui.navigation.destination + +import android.icu.util.Calendar +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation3.runtime.EntryProviderBuilder +import androidx.navigation3.runtime.entry +import com.pixelized.headache.repository.event.Event +import com.pixelized.headache.ui.navigation.Navigator +import com.pixelized.headache.ui.page.event.EventPage +import com.pixelized.headache.ui.page.event.EventViewModel +import com.pixelized.headache.utils.extention.event +import java.util.Date + +data class EventDestination( + val date: Event.Date?, + val misspelledFilter: Boolean, + val invalidFilter: Boolean, +) + +fun EntryProviderBuilder<*>.eventDestinationEntry() { + entry { key -> + val viewModel = hiltViewModel( + creationCallback = { factory -> + factory.create(key) + } + ) + EventPage( + viewModel = viewModel, + ) + } +} + +fun Navigator.navigateToEventPage( + misspelledFilter: Boolean = false, + invalidFilter: Boolean = false, + date: Date? = null, +) { + val route = EventDestination( + date = date?.let { Calendar.getInstance().apply { time = it }.event.copy(day = 1) }, + misspelledFilter = misspelledFilter, + invalidFilter = invalidFilter, + ) + goTo(route) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/navigation/destination/HomeDestination.kt b/app/src/main/java/com/pixelized/headache/ui/navigation/destination/HomeDestination.kt new file mode 100644 index 0000000..a53fde6 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/navigation/destination/HomeDestination.kt @@ -0,0 +1,18 @@ +package com.pixelized.headache.ui.navigation.destination + +import androidx.navigation3.runtime.EntryProviderBuilder +import androidx.navigation3.runtime.entry +import com.pixelized.headache.ui.navigation.Navigator +import com.pixelized.headache.ui.page.home.HomePage + +data object HomePageDestination + +fun EntryProviderBuilder<*>.homeDestinationEntry() { + entry { + HomePage() + } +} + +fun Navigator.navigateToHomePage() { + goTo(HomePageDestination) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/navigation/destination/MonthSummaryDestination.kt b/app/src/main/java/com/pixelized/headache/ui/navigation/destination/MonthSummaryDestination.kt new file mode 100644 index 0000000..88eeac3 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/navigation/destination/MonthSummaryDestination.kt @@ -0,0 +1,18 @@ +package com.pixelized.headache.ui.navigation.destination + +import androidx.navigation3.runtime.EntryProviderBuilder +import androidx.navigation3.runtime.entry +import com.pixelized.headache.ui.navigation.Navigator +import com.pixelized.headache.ui.page.summary.MonthSummaryPage + +data object MonthSummaryDestination + +fun EntryProviderBuilder<*>.monthSummaryDestinationEntry() { + entry { + MonthSummaryPage() + } +} + +fun Navigator.navigateToMonthSummary() { + goTo(MonthSummaryDestination) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/page/MainPage.kt b/app/src/main/java/com/pixelized/headache/ui/page/MainPage.kt new file mode 100644 index 0000000..14136e3 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/page/MainPage.kt @@ -0,0 +1,14 @@ +package com.pixelized.headache.ui.page + +import androidx.compose.runtime.Composable +import com.pixelized.headache.ui.navigation.MainNavDisplay +import com.pixelized.headache.ui.navigation.Navigator + +@Composable +fun MainPage( + navigator: Navigator, +) { + MainNavDisplay( + navigator = navigator, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/page/calendar/CalendarChooserPage.kt b/app/src/main/java/com/pixelized/headache/ui/page/calendar/CalendarChooserPage.kt new file mode 100644 index 0000000..5540219 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/page/calendar/CalendarChooserPage.kt @@ -0,0 +1,116 @@ +package com.pixelized.headache.ui.page.calendar + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.pixelized.headache.R +import com.pixelized.headache.ui.navigation.LocalNavigator +import kotlinx.coroutines.launch + + +@Composable +fun CalendarChooserPage( + viewModel: CalendarViewModel = hiltViewModel(), +) { + val navigation = LocalNavigator.current + val scope = rememberCoroutineScope() + val calendars = viewModel.calendars.collectAsStateWithLifecycle() + + CalendarContent( + modifier = Modifier.fillMaxSize(), + calendars = calendars, + onBack = { + navigation.popBackstack() + }, + onCalendar = { + scope.launch { + viewModel.toggleCalendar( + id = it.id, + name = it.name, + type = it.type, + ) + } + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun CalendarContent( + modifier: Modifier = Modifier, + calendars: State>, + onBack: () -> Unit, + onCalendar: (CalendarItemUio) -> Unit, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + navigationIcon = { + IconButton( + onClick = onBack, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null, + ) + } + }, + title = { + Text( + text = stringResource(R.string.calendar_chooser_title), + ) + }, + ) + }, + content = { paddingValues -> + LazyColumn( + modifier = Modifier + .padding(paddingValues = paddingValues) + .fillMaxSize(), + ) { + items( + items = calendars.value, + contentType = { item -> + when (item) { + is String -> "title" + is CalendarItemUio -> "item" + else -> "other" + } + } + ) { item -> + when (item) { + is String -> Text( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp), + style = MaterialTheme.typography.labelMedium, + text = item + ) + + is CalendarItemUio -> CalendarItem( + item = item, + onItem = onCalendar, + ) + } + } + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/page/calendar/CalendarItem.kt b/app/src/main/java/com/pixelized/headache/ui/page/calendar/CalendarItem.kt new file mode 100644 index 0000000..be840f6 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/page/calendar/CalendarItem.kt @@ -0,0 +1,101 @@ +package com.pixelized.headache.ui.page.calendar + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.minimumInteractiveComponentSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.pixelized.headache.ui.theme.HeadacheTheme + +@Stable +data class CalendarItemUio( + val id: Long, + val name: String, + val type: String, + val account: String, + val color: Color, + val used: Boolean, +) + +@Stable +object CalendarItemDefault { + + @Stable + val padding: PaddingValues = PaddingValues(horizontal = 16.dp) + + @Stable + val spacing: Dp = 8.dp +} + +@Composable +fun CalendarItem( + modifier: Modifier = Modifier, + padding: PaddingValues = CalendarItemDefault.padding, + spacing: Dp = CalendarItemDefault.spacing, + item: CalendarItemUio, + onItem: (CalendarItemUio) -> Unit, +) { + Row( + modifier = Modifier + .clickable { onItem(item) } + .minimumInteractiveComponentSize() + .padding(padding) + .then(other = modifier), + horizontalArrangement = Arrangement.spacedBy(space = spacing), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .background(color = item.color) + .size(24.dp), + ) + Text( + modifier = Modifier.weight(weight = 1f), + style = MaterialTheme.typography.bodyLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + text = item.name, + ) + Switch( + checked = item.used, + onCheckedChange = null, + ) + } +} + +@Composable +@Preview +private fun CalendarItemPreview() { + HeadacheTheme { + Surface { + CalendarItem( + item = CalendarItemUio( + id = 3, + name = "Céphalée", + type = "", + account = "andres.gomez.thomas@gmail.com", + color = Color.Red, + used = false, + ), + onItem = { }, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/page/calendar/CalendarItemFactory.kt b/app/src/main/java/com/pixelized/headache/ui/page/calendar/CalendarItemFactory.kt new file mode 100644 index 0000000..7d48a2d --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/page/calendar/CalendarItemFactory.kt @@ -0,0 +1,48 @@ +package com.pixelized.headache.ui.page.calendar + +import androidx.compose.ui.graphics.Color +import com.pixelized.headache.repository.calendar.GoogleCalendar +import javax.inject.Inject + +class CalendarItemFactory @Inject constructor() { + + fun convertToUio( + calendars: List, + calendarId: Long?, + ): List { + return calendars + .mapNotNull { calendar -> + convertToUio( + calendar = calendar, + used = calendar.id == calendarId, + ) + } + .groupBy { it.account } + .flatMap { + listOf(it.key) + it.value + } + + } + + fun convertToUio( + calendar: GoogleCalendar, + used: Boolean, + ): CalendarItemUio? { + val color = calendar.color.convertToColor() ?: return null + + return CalendarItemUio( + id = calendar.id, + name = calendar.name, + type = calendar.type, + account = calendar.account, + color = color, + used = used, + ) + } + + fun Int.convertToColor(): Color? = try { + Color(this) + } catch (_: Exception) { + null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/page/calendar/CalendarViewModel.kt b/app/src/main/java/com/pixelized/headache/ui/page/calendar/CalendarViewModel.kt new file mode 100644 index 0000000..bb86840 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/page/calendar/CalendarViewModel.kt @@ -0,0 +1,46 @@ +package com.pixelized.headache.ui.page.calendar + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.pixelized.headache.repository.calendar.GoogleCalendarIdRepository +import com.pixelized.headache.repository.calendar.GoogleCalendarRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +@HiltViewModel +class CalendarViewModel @Inject constructor( + private val calendarIdRepository: GoogleCalendarIdRepository, + calendarRepository: GoogleCalendarRepository, + calendarItemFactory: CalendarItemFactory, +) : ViewModel() { + + val calendars: StateFlow> = combine( + calendarRepository.calendarFlow, + calendarIdRepository.calendarId, + ) { calendars, calendarId -> + calendarItemFactory.convertToUio( + calendars = calendars, + calendarId = calendarId, + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = emptyList(), + ) + + suspend fun toggleCalendar( + id: Long, + name: String, + type: String, + ) { + calendarIdRepository.toggleCalendar( + id = id, + name = name, + type = type, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/page/event/EventItem.kt b/app/src/main/java/com/pixelized/headache/ui/page/event/EventItem.kt new file mode 100644 index 0000000..bb3a549 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/page/event/EventItem.kt @@ -0,0 +1,210 @@ +package com.pixelized.headache.ui.page.event + +import android.annotation.SuppressLint +import android.icu.text.SimpleDateFormat +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.pixelized.headache.ui.theme.HeadacheTheme +import java.util.Date +import java.util.Locale + +@Stable +data class EventItemUio( + val id: Long, + val date: Date, + val pills: List, + val isValid: Boolean, + val isMisspelled: Boolean, +) + +@Stable +data object EventItemDefault { + + @Stable + val padding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp) + + @Stable + val spacing: Dp = 4.dp + + @Stable + val minHeigh: Dp = 56.dp + + @Stable + val colorBoxWidth: Dp = 4.dp + + @Stable + val colorBoxPadding: PaddingValues = PaddingValues(vertical = 8.dp) + + @SuppressLint("ConstantLocale") + @Stable + val formatter = SimpleDateFormat("dd MMM yyyy", Locale.getDefault()) +} + +@Composable +fun EventItem( + modifier: Modifier = Modifier, + padding: PaddingValues = EventItemDefault.padding, + spacing: Dp = EventItemDefault.spacing, + minHeigh: Dp = EventItemDefault.minHeigh, + colorBoxWidth: Dp = EventItemDefault.colorBoxWidth, + colorBoxPadding: PaddingValues = EventItemDefault.colorBoxPadding, + formatter: SimpleDateFormat = EventItemDefault.formatter, + item: EventItemUio, +) { + Box( + modifier = Modifier + .heightIn(min = minHeigh) + .height(IntrinsicSize.Min) + .then(other = modifier), + contentAlignment = Alignment.CenterStart, + ) { + if (item.isMisspelled || item.isValid.not()) { + Box( + modifier = Modifier + .padding(paddingValues = colorBoxPadding) + .background( + color = when { + item.isMisspelled -> MaterialTheme.colorScheme.secondary + item.isValid.not() -> MaterialTheme.colorScheme.error + else -> Color.Transparent + } + ) + .fillMaxHeight() + .width(width = colorBoxWidth) + ) + } + Column( + modifier = Modifier.padding(paddingValues = padding), + verticalArrangement = Arrangement.spacedBy(space = spacing), + ) { + Text( + style = MaterialTheme.typography.titleLarge, + text = formatter.format(item.date), + ) + if (item.pills.isNotEmpty()) { + Column { + item.pills.forEach { pill -> + PillItem( + item = pill + ) + } + } + } + } + } +} + +@Composable +@Preview +private fun EventItemPreview( + @PreviewParameter(EventPreviewProvider::class) preview: EventItemUio, +) { + HeadacheTheme { + Surface { + EventItem( + item = preview, + ) + } + } +} + +private class EventPreviewProvider() : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + EventItemUio( + id = 0, + date = Date(), + pills = listOf( + PillUio( + label = "Spifen 400", + amount = 1, + description = "Spifen 400", + isValid = true, + isMisspelled = false, + ), + PillUio( + label = "Élétriptan 40", + amount = 1, + description = "Élétriptan 40", + isValid = true, + isMisspelled = false, + ), + ), + isValid = true, + isMisspelled = false, + ), + EventItemUio( + id = 1, + date = Date(), + pills = emptyList(), + isValid = true, + isMisspelled = false, + ), + EventItemUio( + id = 2, + date = Date(), + pills = listOf( + PillUio( + label = "Spifen 400", + amount = 1, + description = "Spifen 400", + isValid = true, + isMisspelled = false, + ), + PillUio( + label = "Élétriptan 40", + amount = 1, + description = "Élitriptan 40", + isValid = true, + isMisspelled = true, + ), + ), + isValid = true, + isMisspelled = true, + ), + EventItemUio( + id = 3, + date = Date(), + pills = listOf( + PillUio( + label = "Spifen 400", + amount = 1, + description = "Spifen 400", + isValid = true, + isMisspelled = false, + ), + PillUio( + label = "?", + amount = 1, + description = "Élitriptan 40", + isValid = false, + isMisspelled = false, + ), + ), + isValid = false, + isMisspelled = false, + ), + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/page/event/EventItemFactory.kt b/app/src/main/java/com/pixelized/headache/ui/page/event/EventItemFactory.kt new file mode 100644 index 0000000..9a29ecc --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/page/event/EventItemFactory.kt @@ -0,0 +1,45 @@ +package com.pixelized.headache.ui.page.event + +import android.icu.util.Calendar +import com.pixelized.headache.repository.event.Event +import com.pixelized.headache.utils.extention.event +import javax.inject.Inject + + +class EventItemFactory @Inject constructor() { + private val calendar = Calendar.getInstance() + + fun convertToUio( + events: List, + ): List { + return events + .sortedByDescending { it.date } + .map { convertToUio(it) } + } + + fun convertToUio( + event: Event, + ): EventItemUio { + val date = calendar.apply { this.event = event.date }.time + + return EventItemUio( + id = event.id, + date = date, + pills = event.pills.map { convertToUio(it) }, + isValid = event.isValid, + isMisspelled = event.isMisspelled, + ) + } + + fun convertToUio( + pill: Event.Pill, + ): PillUio { + return PillUio( + label = pill.label, + amount = pill.amount, + description = pill.description, + isValid = pill.isValid, + isMisspelled = pill.isMisspelled, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/page/event/EventPage.kt b/app/src/main/java/com/pixelized/headache/ui/page/event/EventPage.kt new file mode 100644 index 0000000..80db093 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/page/event/EventPage.kt @@ -0,0 +1,153 @@ +package com.pixelized.headache.ui.page.event + +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.pixelized.headache.R +import com.pixelized.headache.ui.navigation.LocalNavigator + +@Stable +data object EventPageDefault { + @Stable + val padding: PaddingValues = PaddingValues(vertical = 8.dp) + + @Stable + val spacing: Dp = 0.dp +} + +@Composable +fun EventPage( + viewModel: EventViewModel = hiltViewModel(), +) { + val navigation = LocalNavigator.current + val events = viewModel.events.collectAsStateWithLifecycle() + val misspelledFilter = viewModel.misspelledFilter.collectAsStateWithLifecycle() + val invalidFilter = viewModel.invalidFilter.collectAsStateWithLifecycle() + + EventContent( + modifier = Modifier.fillMaxSize(), + misspelledFilter = misspelledFilter, + invalidFilter = invalidFilter, + events = events, + onBack = { navigation.popBackstack() }, + onMisspelledFilter = viewModel::toggleMisspelledFilter, + onInvalidFilter = viewModel::toggleErrorFilter, + ) +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun EventContent( + modifier: Modifier = Modifier, + padding: PaddingValues = EventPageDefault.padding, + spacing: Dp = EventPageDefault.spacing, + invalidFilter: State, + misspelledFilter: State, + events: State>, + onBack: () -> Unit, + onInvalidFilter: () -> Unit, + onMisspelledFilter: () -> Unit, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + navigationIcon = { + IconButton( + onClick = onBack, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null, + ) + } + }, + title = { + Text( + text = stringResource(R.string.event_title), + ) + }, + actions = { + IconButton( + onClick = onMisspelledFilter, + ) { + val color = animateColorAsState( + when (misspelledFilter.value) { + true -> MaterialTheme.colorScheme.error + else -> MaterialTheme.colorScheme.onSurface + } + ) + Icon( + painter = painterResource(R.drawable.ic_spellcheck_24px), + tint = color.value, + contentDescription = null, + ) + } + + IconButton( + onClick = onInvalidFilter, + ) { + val color = animateColorAsState( + when (invalidFilter.value) { + true -> MaterialTheme.colorScheme.error + else -> MaterialTheme.colorScheme.onSurface + } + ) + Icon( + painter = painterResource(R.drawable.ic_error_24px), + tint = color.value, + contentDescription = null, + ) + } + } + ) + }, + content = { paddingValues -> + LazyColumn( + modifier = Modifier + .padding(paddingValues = paddingValues) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(space = spacing), + contentPadding = padding, + reverseLayout = true, + ) { + items( + items = events.value, + key = { it.id }, + contentType = { "EventItem" }, + ) { item -> + EventItem( + modifier = Modifier + .animateItem() + .fillMaxWidth(), + item = item, + ) + } + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/page/event/EventViewModel.kt b/app/src/main/java/com/pixelized/headache/ui/page/event/EventViewModel.kt new file mode 100644 index 0000000..cc062c2 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/page/event/EventViewModel.kt @@ -0,0 +1,89 @@ +package com.pixelized.headache.ui.page.event + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.pixelized.headache.repository.calendar.GoogleCalendarIdRepository +import com.pixelized.headache.repository.event.Event +import com.pixelized.headache.repository.event.EventRepository +import com.pixelized.headache.ui.navigation.destination.EventDestination +import com.pixelized.headache.utils.extention.isSameMonth +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn + +@HiltViewModel(assistedFactory = EventViewModel.Factory::class) +class EventViewModel @AssistedInject constructor( + googleCalendarIdRepository: GoogleCalendarIdRepository, + eventRepository: EventRepository, + eventItemFactory: EventItemFactory, + @Assisted val argument: EventDestination, +) : ViewModel() { + + private val misspelledFilterFlow = MutableStateFlow(argument.misspelledFilter) + val misspelledFilter: StateFlow = misspelledFilterFlow + + private val invalidFilterFlow = MutableStateFlow(argument.invalidFilter) + val invalidFilter: StateFlow = invalidFilterFlow + + @OptIn(ExperimentalCoroutinesApi::class) + val events: StateFlow> = googleCalendarIdRepository.calendarId + .flatMapLatest { id: Long? -> + when (id) { + null -> flowOf(emptyList()) + else -> { + combine( + when (argument.date) { + null -> eventRepository.eventFlow(calendarId = id) + else -> eventRepository.eventFlow(calendarId = id) + .mapLatest { events -> + events.filter { event -> + event.date isSameMonth argument.date + } + } + }, + misspelledFilterFlow, + invalidFilterFlow, + ) { events, misspelled, invalid -> + if (!misspelled && !invalid) { + events + } else { + events.filter { + misspelled && it.isMisspelled || invalid && it.isValid.not() + } + } + } + } + } + } + .mapLatest { events: List -> + eventItemFactory.convertToUio(events = events) + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = emptyList(), + ) + + fun toggleMisspelledFilter() { + misspelledFilterFlow.value = !misspelledFilterFlow.value + } + + fun toggleErrorFilter() { + invalidFilterFlow.value = !invalidFilterFlow.value + } + + @AssistedFactory + interface Factory { + fun create(navKey: EventDestination): EventViewModel + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/page/event/PillUio.kt b/app/src/main/java/com/pixelized/headache/ui/page/event/PillUio.kt new file mode 100644 index 0000000..fa8716a --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/page/event/PillUio.kt @@ -0,0 +1,102 @@ +package com.pixelized.headache.ui.page.event + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.pixelized.headache.ui.theme.HeadacheTheme + +@Stable +data class PillUio( + val label: String, + val amount: Int, + val description: String, + val isValid: Boolean, + val isMisspelled: Boolean, +) + +@Stable +data object PillItemDefault { + @Stable + val spacing = 4.dp +} + +@Composable +fun PillItem( + modifier: Modifier = Modifier, + spacing: Dp = PillItemDefault.spacing, + item: PillUio, +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(space = spacing), + ) { + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.bodyMedium, + text = "- ${item.label}: ${item.amount}", + ) + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.labelSmall, + fontStyle = FontStyle.Italic, + color = when (item.isValid.not() || item.isMisspelled) { + true -> MaterialTheme.colorScheme.error + else -> MaterialTheme.colorScheme.onSurface + }, + text = "\"${item.description}\"", + ) + } +} + +@Composable +@Preview +private fun PillItemPreview( + @PreviewParameter(PillPreviewProvider::class) preview: PillUio, +) { + HeadacheTheme { + Surface { + PillItem( + item = preview, + ) + } + } +} + +private class PillPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + PillUio( + label = "Élétriptan 40", + amount = 2, + description = "Élétriptan 400 x2", + isValid = true, + isMisspelled = false, + ), + PillUio( + label = "Élétriptan 400", + amount = 2, + description = "Élitriptan 400 x2", + isValid = true, + isMisspelled = true, + ), + PillUio( + label = "?", + amount = 2, + description = "Ilitriptan 400 x2", + isValid = false, + isMisspelled = false, + ), + ) + +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/page/home/HomePage.kt b/app/src/main/java/com/pixelized/headache/ui/page/home/HomePage.kt new file mode 100644 index 0000000..2690f35 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/page/home/HomePage.kt @@ -0,0 +1,131 @@ +package com.pixelized.headache.ui.page.home + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.pixelized.headache.R +import com.pixelized.headache.ui.navigation.LocalNavigator +import com.pixelized.headache.ui.navigation.destination.navigateToCalendarChooserPage +import com.pixelized.headache.ui.navigation.destination.navigateToEventPage +import com.pixelized.headache.ui.navigation.destination.navigateToMonthSummary + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomePage() { + val navigation = LocalNavigator.current + + HomePageContent( + modifier = Modifier.fillMaxSize(), + onCalendarChooser = { + navigation.navigateToCalendarChooserPage() + }, + onEvent = { + navigation.navigateToEventPage() + }, + onMonthSummary = { + navigation.navigateToMonthSummary() + }, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun HomePageContent( + modifier: Modifier = Modifier, + onCalendarChooser: () -> Unit, + onEvent: () -> Unit, + onMonthSummary: () -> Unit, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { + Text(text = stringResource(R.string.app_name)) + }, + ) + }, + content = { paddingValues -> + LazyColumn( + modifier = Modifier.padding(paddingValues = paddingValues), + ) { + item { + NavigationItem( + label = stringResource(R.string.calendar_chooser_title), + onClick = onCalendarChooser, + ) + } + item { + NavigationItem( + label = stringResource(R.string.event_title), + onClick = onEvent, + ) + } + item { + NavigationItem( + label = stringResource(R.string.month_summary_title), + onClick = onMonthSummary, + ) + } + } + } + ) +} + +@Stable +data object NavigationItemDefault { + @Stable + val minHeight: Dp = 56.dp + + @Stable + val padding = PaddingValues(horizontal = 16.dp) +} + +@Composable +private fun NavigationItem( + modifier: Modifier = Modifier, + padding: PaddingValues = NavigationItemDefault.padding, + minHeight: Dp = NavigationItemDefault.minHeight, + label: String, + onClick: () -> Unit, +) { + Row( + modifier = Modifier + .clickable(onClick = onClick) + .fillMaxWidth() + .heightIn(min = minHeight) + .padding(paddingValues = padding) + .then(other = modifier), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.bodyMedium, + text = label, + ) + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/page/summary/MonthSummaryBox.kt b/app/src/main/java/com/pixelized/headache/ui/page/summary/MonthSummaryBox.kt new file mode 100644 index 0000000..5605ed4 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/page/summary/MonthSummaryBox.kt @@ -0,0 +1,173 @@ +package com.pixelized.headache.ui.page.summary + +import android.annotation.SuppressLint +import android.icu.text.SimpleDateFormat +import android.icu.util.Calendar +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import com.pixelized.headache.ui.theme.HeadacheTheme +import java.util.Date +import java.util.Locale + +@Stable +data object MonthSummaryBoxDefault { + @Stable + val padding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp) + + @Stable + val spacing: DpSize = DpSize(width = 8.dp, height = 4.dp) + + @Stable + val labelWidth: Dp = 64.dp + 16.dp + + @Stable + val boxHeight: Dp = 16.dp + + @Stable + val boxPaddingValues: PaddingValues = PaddingValues(horizontal = 4.dp) + + @SuppressLint("ConstantLocale") + @Stable + val formatter = SimpleDateFormat("MMM yyyy", Locale.getDefault()) +} + +@Stable +data class MonthSummaryBoxUio( + override val date: Date, + val headacheRatio: Float, + val headacheAmount: Int, + val headacheColor: Color, + val pillRatio: Float?, + val pillAmount: Int, + val pillColor: Color, +) : MonthSummaryItem + +@Composable +fun MonthSummaryBox( + modifier: Modifier = Modifier, + padding: PaddingValues = MonthSummaryBoxDefault.padding, + spacing: DpSize = MonthSummaryBoxDefault.spacing, + boxPaddingValues: PaddingValues = MonthSummaryBoxDefault.boxPaddingValues, + labelWidth: Dp = MonthSummaryBoxDefault.labelWidth, + boxHeight: Dp = MonthSummaryBoxDefault.boxHeight, + formatter: SimpleDateFormat = MonthSummaryBoxDefault.formatter, + item: MonthSummaryBoxUio, + onItem: (MonthSummaryBoxUio) -> Unit, +) { + Row( + modifier = Modifier + .clickable { onItem(item) } + .padding(paddingValues = padding) + .then(other = modifier), + horizontalArrangement = Arrangement.spacedBy(space = spacing.width), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.width(width = labelWidth), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.End, + text = formatter.format(item.date), + ) + Column( + verticalArrangement = Arrangement.spacedBy(space = spacing.height), + ) { + Box( + modifier = Modifier + .background(color = item.headacheColor) + .height(height = boxHeight) + .fillMaxWidth(fraction = item.headacheRatio) + .padding(paddingValues = boxPaddingValues), + contentAlignment = Alignment.CenterStart, + ) { + Text( + style = MaterialTheme.typography.labelSmall, + color = Color.White, + text = "${item.headacheAmount}" + ) + } + item.pillRatio?.let { + Box( + modifier = Modifier + .background(color = item.pillColor) + .height(height = boxHeight) + .fillMaxWidth(fraction = it) + .padding(paddingValues = boxPaddingValues), + ) { + Text( + style = MaterialTheme.typography.labelSmall, + color = Color.White, + text = "${item.pillAmount}" + ) + } + } + } + } +} + +@Composable +@Preview +private fun MonthSummaryBoxPreview( + @PreviewParameter(BoxPreviewProvider::class) preview: MonthSummaryBoxUio, +) { + HeadacheTheme { + Surface { + MonthSummaryBox( + modifier = Modifier.fillMaxWidth(), + item = preview, + onItem = { }, + ) + } + } +} + +private class BoxPreviewProvider : PreviewParameterProvider { + val calendar = Calendar.getInstance().apply { + time = Date() + set(Calendar.DAY_OF_MONTH, 1) + } + override val values: Sequence + get() = sequenceOf( + MonthSummaryBoxUio( + date = calendar.apply { set(Calendar.MONTH, Calendar.DECEMBER) }.time, + headacheRatio = 0.2f, + headacheAmount = 1, + headacheColor = Color.Red, + pillRatio = 0.3f, + pillAmount = 1, + pillColor = Color.Blue, + ), + MonthSummaryBoxUio( + date = calendar.apply { set(Calendar.MONTH, Calendar.SEPTEMBER) }.time, + headacheRatio = 1f, + headacheAmount = 1, + headacheColor = Color.Red, + pillRatio = 0.3f, + pillAmount = 1, + pillColor = Color.Blue, + ), + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/page/summary/MonthSummaryFactory.kt b/app/src/main/java/com/pixelized/headache/ui/page/summary/MonthSummaryFactory.kt new file mode 100644 index 0000000..bb0104d --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/page/summary/MonthSummaryFactory.kt @@ -0,0 +1,84 @@ +package com.pixelized.headache.ui.page.summary + +import android.icu.util.Calendar +import androidx.compose.ui.graphics.Color +import com.pixelized.headache.repository.event.Event +import com.pixelized.headache.utils.extention.event +import javax.inject.Inject +import kotlin.math.max + +class MonthSummaryFactory @Inject constructor() { + private val calendar = Calendar.getInstance() + + fun convertToItemUio( + events: List, + ): List { + return events + .fold(hashMapOf>()) { acc, event -> + acc.also { it.getOrPut(event.date.copy(day = 1)) { mutableListOf() }.add(event) } + } + .mapKeys { entry -> + val pills = entry.value + .fold(hashMapOf()) { acc, event -> + event.pills.forEach { pill -> + acc[pill.label] = acc.getOrDefault(pill.label, 0) + pill.amount + } + acc + } + .mapKeys { + it.key to it.value + } + .keys + .toList() + + MonthSummaryItemUio( + date = calendar.apply { event = entry.key }.time, + days = entry.value.size, + pills = pills, + ) + } + .keys + .toList() + .sortedByDescending { it.date } + } + + fun convertToBoxUio( + events: List, + ): List { + var maxPillAmount = 0 + return events + .fold(hashMapOf>()) { acc, event -> + acc.also { it.getOrPut(event.date.copy(day = 1)) { mutableListOf() }.add(event) } + } + .mapKeys { entry -> + maxPillAmount = max( + entry.value.sumOf { events -> events.pills.sumOf { pill -> pill.amount } }, + maxPillAmount, + ) + entry.key + } + .mapKeys { entry -> + val pillAmount = + entry.value.sumOf { events -> events.pills.sumOf { pill -> pill.amount } } + val monthMaxDay = calendar.apply { + event = entry.key + add(Calendar.MONTH, 1) + add(Calendar.DAY_OF_YEAR, -1) + }.get(Calendar.DAY_OF_MONTH) + + MonthSummaryBoxUio( + date = calendar.apply { event = entry.key }.time, + headacheRatio = entry.value.size.toFloat() / monthMaxDay, + headacheAmount = entry.value.size, + headacheColor = Color.Red, + pillRatio = pillAmount.takeIf { it > 0 } + ?.let { it.toFloat() / maxPillAmount.toFloat() }, + pillAmount = pillAmount, + pillColor = Color.Blue, + ) + } + .keys + .toList() + .sortedByDescending { it.date } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/page/summary/MonthSummaryItem.kt b/app/src/main/java/com/pixelized/headache/ui/page/summary/MonthSummaryItem.kt new file mode 100644 index 0000000..0b52257 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/page/summary/MonthSummaryItem.kt @@ -0,0 +1,130 @@ +package com.pixelized.headache.ui.page.summary + +import android.annotation.SuppressLint +import android.icu.text.SimpleDateFormat +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import com.pixelized.headache.ui.theme.HeadacheTheme +import java.util.Date +import java.util.Locale + +@Stable +data class MonthSummaryItemUio( + override val date: Date, + val days: Int, + val pills: List>, +) : MonthSummaryItem + +@Stable +object MonthSummaryItemDefault { + + @Stable + val padding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp) + + @Stable + val spacing: DpSize = DpSize(width = 4.dp, height = 2.dp) + + @SuppressLint("ConstantLocale") + @Stable + val formatter = SimpleDateFormat("MMMM yyyy", Locale.getDefault()) +} + +@Composable +fun MonthSummaryItem( + modifier: Modifier = Modifier, + padding: PaddingValues = MonthSummaryItemDefault.padding, + spacing: DpSize = MonthSummaryItemDefault.spacing, + formatter: SimpleDateFormat = MonthSummaryItemDefault.formatter, + item: MonthSummaryItemUio, + onItem: (MonthSummaryItemUio) -> Unit, +) { + Column( + modifier = Modifier + .clickable { onItem(item) } + .padding(paddingValues = padding) + .then(other = modifier), + verticalArrangement = Arrangement.spacedBy(space = spacing.height) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(space = spacing.width) + ) { + formatter.format(item.date) + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.titleLarge, + text = formatter.format(item.date).capitalize(), + ) + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.titleLarge, + text = "-", + ) + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.titleMedium, + text = "${item.days} jours de migraine.", + ) + } + Column { + item.pills.forEach { + Text( + style = MaterialTheme.typography.bodyMedium, + text = "- ${it.first} : ${it.second}", + ) + } + } + } +} + +@Composable +@Stable +fun String.capitalize() = + replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } + +@Composable +@Preview +private fun MonthSummaryItemPreview( + @PreviewParameter(PreviewProvider::class) preview: MonthSummaryItemUio, +) { + HeadacheTheme { + Surface { + MonthSummaryItem( + modifier = Modifier.fillMaxWidth(), + item = preview, + onItem = { }, + ) + } + } +} + +private class PreviewProvider() : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + MonthSummaryItemUio( + date = Date(), + days = 11, + pills = listOf( + "Paracétamol 1000" to 2, + "Ibuprofen 400" to 4, + "Spifen 400" to 6, + "Eletripan 40" to 0, + ), + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/page/summary/MonthSummaryPage.kt b/app/src/main/java/com/pixelized/headache/ui/page/summary/MonthSummaryPage.kt new file mode 100644 index 0000000..a52aeec --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/page/summary/MonthSummaryPage.kt @@ -0,0 +1,161 @@ +package com.pixelized.headache.ui.page.summary + +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.BarChart +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.pixelized.headache.R +import com.pixelized.headache.ui.navigation.LocalNavigator +import com.pixelized.headache.ui.navigation.destination.navigateToEventPage +import java.util.Date + +@Stable +data object MonthSummaryPageDefault { + @Stable + val padding: PaddingValues = PaddingValues(vertical = 8.dp) + + @Stable + val spacing: Dp = 0.dp +} + +@Stable +sealed interface MonthSummaryItem { + val date: Date +} + +@Composable +fun MonthSummaryPage( + viewModel: MonthSummaryViewModel = hiltViewModel(), +) { + val navigation = LocalNavigator.current + val boxMode = viewModel.boxMode.collectAsStateWithLifecycle() + val events = viewModel.events.collectAsStateWithLifecycle() + + MonthSummaryContent( + modifier = Modifier.fillMaxSize(), + events = events, + boxMode = boxMode, + onBack = { + navigation.popBackstack() + }, + onDisplay = { + viewModel.toggleDisplay() + }, + onItem = { + navigation.navigateToEventPage(date = it.date) + } + ) +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun MonthSummaryContent( + modifier: Modifier = Modifier, + spacing: Dp = MonthSummaryPageDefault.spacing, + padding: PaddingValues = MonthSummaryPageDefault.padding, + boxMode: State, + events: State>, + onBack: () -> Unit, + onDisplay: () -> Unit, + onItem: (MonthSummaryItem) -> Unit, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + navigationIcon = { + IconButton( + onClick = onBack, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null, + ) + } + }, + title = { + Text(text = stringResource(R.string.month_summary_title)) + }, + actions = { + val color = animateColorAsState( + when (boxMode.value) { + true -> MaterialTheme.colorScheme.error + else -> MaterialTheme.colorScheme.onSurface + } + ) + IconButton( + onClick = onDisplay, + ) { + Icon( + imageVector = Icons.Default.BarChart, + tint = color.value, + contentDescription = null, + ) + } + } + ) + }, + content = { paddingValues -> + LazyColumn( + modifier = Modifier + .padding(paddingValues = paddingValues) + .fillMaxSize(), + contentPadding = padding, + verticalArrangement = Arrangement.spacedBy(space = spacing), + reverseLayout = true, + ) { + items( + items = events.value, + key = { it.date }, + contentType = { + when (it) { + is MonthSummaryBoxUio -> "MonthSummaryBoxUio" + is MonthSummaryItemUio -> "MonthSummaryItemUio" + } + }, + ) { item -> + when (item) { + is MonthSummaryBoxUio -> MonthSummaryBox( + modifier = Modifier + .animateItem() + .fillMaxWidth(), + item = item, + onItem = onItem, + ) + + is MonthSummaryItemUio -> MonthSummaryItem( + modifier = Modifier + .animateItem() + .fillMaxWidth(), + item = item, + onItem = onItem, + ) + } + } + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/page/summary/MonthSummaryViewModel.kt b/app/src/main/java/com/pixelized/headache/ui/page/summary/MonthSummaryViewModel.kt new file mode 100644 index 0000000..27f21dc --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/page/summary/MonthSummaryViewModel.kt @@ -0,0 +1,51 @@ +package com.pixelized.headache.ui.page.summary + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.pixelized.headache.repository.calendar.GoogleCalendarIdRepository +import com.pixelized.headache.repository.event.Event +import com.pixelized.headache.repository.event.EventRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +@HiltViewModel +class MonthSummaryViewModel @Inject constructor( + googleCalendarIdRepository: GoogleCalendarIdRepository, + eventRepository: EventRepository, + eventItemFactory: MonthSummaryFactory, +) : ViewModel() { + + private val displayTypeFlow = MutableStateFlow(false) + val boxMode: StateFlow = displayTypeFlow + + @OptIn(ExperimentalCoroutinesApi::class) + val events: StateFlow> = googleCalendarIdRepository.calendarId + .flatMapLatest { id: Long? -> + when (id) { + null -> flowOf(emptyList()) + else -> eventRepository.eventFlow(calendarId = id) + } + }.combine(displayTypeFlow) { events: List, display -> + when (display) { + true -> eventItemFactory.convertToBoxUio(events = events) + else -> eventItemFactory.convertToItemUio(events = events) + } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = emptyList(), + ) + + fun toggleDisplay() { + displayTypeFlow.value = displayTypeFlow.value.not() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/theme/Color.kt b/app/src/main/java/com/pixelized/headache/ui/theme/Color.kt new file mode 100644 index 0000000..cc0d976 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.pixelized.headache.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/theme/Theme.kt b/app/src/main/java/com/pixelized/headache/ui/theme/Theme.kt new file mode 100644 index 0000000..73ec851 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package com.pixelized.headache.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun HeadacheTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/theme/Type.kt b/app/src/main/java/com/pixelized/headache/ui/theme/Type.kt new file mode 100644 index 0000000..5b3a66a --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.pixelized.headache.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/utils/extention/CalendarEx.kt b/app/src/main/java/com/pixelized/headache/utils/extention/CalendarEx.kt new file mode 100644 index 0000000..6dcae29 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/utils/extention/CalendarEx.kt @@ -0,0 +1,20 @@ +package com.pixelized.headache.utils.extention + +import android.icu.util.Calendar +import com.pixelized.headache.repository.event.Event + +var Calendar.event: Event.Date + get() = Event.Date( + day = get(Calendar.DAY_OF_MONTH), + month = get(Calendar.MONTH), + year = get(Calendar.YEAR), + ) + set(value) { + set(java.util.Calendar.YEAR, value.year) + set(java.util.Calendar.MONTH, value.month) + set(java.util.Calendar.DAY_OF_MONTH, value.day) + set(java.util.Calendar.HOUR_OF_DAY, 0) + set(java.util.Calendar.MINUTE, 0) + set(java.util.Calendar.SECOND, 0) + set(java.util.Calendar.MILLISECOND, 0) + } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/utils/extention/EventDateEx.kt b/app/src/main/java/com/pixelized/headache/utils/extention/EventDateEx.kt new file mode 100644 index 0000000..c00f53e --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/utils/extention/EventDateEx.kt @@ -0,0 +1,5 @@ +package com.pixelized.headache.utils.extention + +import com.pixelized.headache.repository.event.Event + +infix fun Event.Date.isSameMonth(other: Event.Date) = month == other.month && year == other.year \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_error_24px.xml b/app/src/main/res/drawable/ic_error_24px.xml new file mode 100644 index 0000000..73356a1 --- /dev/null +++ b/app/src/main/res/drawable/ic_error_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_spellcheck_24px.xml b/app/src/main/res/drawable/ic_spellcheck_24px.xml new file mode 100644 index 0000000..4c3ffb5 --- /dev/null +++ b/app/src/main/res/drawable/ic_spellcheck_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..cbc348f Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..5a3cdf2 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9c0d24c Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..2044c8c Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..13fc28c Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..318e114 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..f04b3bc Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..09fabb0 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..c70fce0 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..2f82f48 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..6e95f76 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..649f9c0 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..5255bab Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..622c5d2 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..888ed13 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000..5858f39 --- /dev/null +++ b/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,7 @@ + + Céphalée + + Choix du calendrier + Évennement migraineux + Suivit des migraines + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..58043be --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #212121 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..51184ac --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + Headache + + Choose your calendar + Headache event + Headache summary + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..0e5dd57 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..4df9255 --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/test/java/com/pixelized/headache/EventFactoryText.kt b/app/src/test/java/com/pixelized/headache/EventFactoryText.kt new file mode 100644 index 0000000..94e2bda --- /dev/null +++ b/app/src/test/java/com/pixelized/headache/EventFactoryText.kt @@ -0,0 +1,92 @@ +package com.pixelized.headache + +import com.pixelized.headache.repository.event.EventFactory +import org.junit.Test + +class EventFactoryText { + private val factory = EventFactory() + + @Test + fun testPill() { + test( + pill = "Spifen 400", + expectedLabel = "Spifen 400", + expectedAmount = 1, + expectedIsValid = true, + expectedIsMisspelled = false, + ) + } + + @Test + fun testMisspelledPill() { + test( + pill = "Spifen", + expectedLabel = "Spifen 400", + expectedAmount = 1, + expectedIsValid = true, + expectedIsMisspelled = true, + ) + } + + @Test + fun testUnknownPill() { + test( + pill = "Spife", + expectedLabel = "?", + expectedAmount = 1, + expectedIsValid = false, + expectedIsMisspelled = false, + ) + } + + @Test + fun test2Pills_1() { + test( + pill = "Spifen 400 x2", + expectedLabel = "Spifen 400", + expectedAmount = 2, + expectedIsValid = true, + expectedIsMisspelled = false, + ) + } + + @Test + fun test2Pills_2() { + test( + pill = "Spifen 400 x 2", + expectedLabel = "Spifen 400", + expectedAmount = 2, + expectedIsValid = true, + expectedIsMisspelled = false, + ) + } + + @Test + fun text2Pills_3() { + test( + pill = "Ibuprofene 400 Extra x2", + expectedLabel = "Ibuprofène 400", + expectedAmount = 2, + expectedIsValid = true, + expectedIsMisspelled = true, + ) + } + + fun test( + pill: String, + expectedLabel: String, + expectedAmount: Int, + expectedIsValid: Boolean, + expectedIsMisspelled: Boolean, + ) { + factory.parsePill( + pill = pill, + ).let { + assert(it.label == expectedLabel) { "Label error - Expected:\"$expectedLabel\" but was:\"${it.label}\"" } + assert(it.amount == expectedAmount) { "Amount error - Expected:\"$expectedAmount\" but was:\"${it.amount}\"" } + assert(it.description == pill) { "Description error - Expected:\"$pill\" but was:\"${it.description}\"" } + assert(it.isValid == expectedIsValid) { "isValid error - Expected:\"$expectedIsValid\" but was:\"${it.isValid}\"" } + assert(it.isMisspelled == expectedIsMisspelled) { "isMisspelled error - Expected:\"$expectedIsMisspelled\" but was:\"${it.isMisspelled}\"" } + } + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..1b4e189 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,8 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.compose) apply false + alias(libs.plugins.ksp) apply false + alias(libs.plugins.hilt) apply false +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..20e2a01 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..1447d6f --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,71 @@ +# Copyright 2025 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[versions] +agp = "8.12.2" +kotlin = "2.2.0" +kotlinSerialization = "2.2.0" +coreKtx = "1.16.0" +junit = "4.13.2" +junitVersion = "1.3.0" +espressoCore = "3.7.0" +kotlinxSerializationCore = "1.9.0" +lifecycleRuntimeKtx = "2.9.2" +lifecycleViewmodel = "1.0.0-SNAPSHOT" +activityCompose = "1.12.0-alpha05" +composeBom = "2025.07.01" +navigation3 = "1.0.0-alpha07" +material3 = "1.4.0-beta01" +nav3Material = "1.0.0-SNAPSHOT" +ksp = "2.2.0-2.0.2" +hilt = "2.57" +hiltNavigationCompose = "1.2.0" +datastore = "1.1.7" + +[libraries] +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom-alpha", version.ref = "composeBom" } +androidx-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } +androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } +androidx-material3-windowsizeclass = { group = "androidx.compose.material3", name = "material3-window-size-class", version.ref = "material3" } +androidx-adaptive-layout = { group = "androidx.compose.material3.adaptive", name = "adaptive-layout" } +androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } +androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodel" } +androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "navigation3" } +androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "navigation3" } +hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } +hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" } +kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationCore" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationCore" } +androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } +androidx-material3-navigation3 = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation3", version.ref = "nav3Material" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerialization"} +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..dec6094 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Aug 29 16:04:56 CEST 2025 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..8f5ee4c --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,29 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + maven { + url = uri("https://androidx.dev/snapshots/builds/13617490/artifacts/repository") + } + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + maven { + url = uri("https://androidx.dev/snapshots/builds/13617490/artifacts/repository") + } + } +} + +rootProject.name = "Headache" +include(":app")