Initial commit

This commit is contained in:
Andres Gomez, Thomas (ITDV RL) 2025-09-01 09:56:51 +02:00
commit 2c5d9b6df1
115 changed files with 5026 additions and 0 deletions

77
.gitignore vendored Normal file
View file

@ -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/

3
.idea/.gitignore generated vendored Normal file
View file

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
.idea/.name generated Normal file
View file

@ -0,0 +1 @@
Headache

6
.idea/AndroidProjectSystem.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidTestResultsUserPreferences">
<option name="androidTestResultsTableState">
<map>
<entry key="-1407511392">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Google Pixel 6a" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="-279031010">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Google Pixel 6a" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="914341672">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Google Pixel 6a" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
</map>
</option>
</component>
</project>

26
.idea/appInsightsSettings.xml generated Normal file
View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AppInsightsSettings">
<option name="tabSettings">
<map>
<entry key="Firebase Crashlytics">
<value>
<InsightsFilterSettings>
<option name="connection">
<ConnectionSetting>
<option name="appId" value="PLACEHOLDER" />
<option name="mobileSdkAppId" value="" />
<option name="projectId" value="" />
<option name="projectNumber" value="" />
</ConnectionSetting>
</option>
<option name="signal" value="SIGNAL_UNSPECIFIED" />
<option name="timeIntervalDays" value="THIRTY_DAYS" />
<option name="visibilityType" value="ALL" />
</InsightsFilterSettings>
</value>
</entry>
</map>
</option>
</component>
</project>

6
.idea/compiler.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

19
.idea/deploymentTargetSelector.xml generated Normal file
View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="testPill()">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="EventFactoryText">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="text2Pills_3()">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

13
.idea/deviceManager.xml generated Normal file
View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

9
.idea/dictionaries/project.xml generated Normal file
View file

@ -0,0 +1,9 @@
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>ibuprofene</w>
<w>ibuprofène</w>
<w>spifen</w>
</words>
</dictionary>
</component>

19
.idea/gradle.xml generated Normal file
View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

View file

@ -0,0 +1,61 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
</profile>
</component>

10
.idea/migrations.xml generated Normal file
View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

1167
.idea/misc.xml generated Normal file

File diff suppressed because it is too large Load diff

17
.idea/runConfigurations.xml generated Normal file
View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

1
app/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

122
app/build.gradle.kts Normal file
View file

@ -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
}
}

21
app/proguard-rules.pro vendored Normal file
View file

@ -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

Binary file not shown.

Binary file not shown.

View file

@ -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
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#212121</color>
</resources>

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
<application
android:allowBackup="true"
android:name=".HeadacheApplication"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Headache">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.Headache">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

View file

@ -0,0 +1,7 @@
package com.pixelized.headache
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class HeadacheApplication : Application()

View file

@ -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,
)
}
}
}
}
}

View file

@ -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)
}

View file

@ -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,
)

View file

@ -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<Preferences> by preferencesDataStore(name = CALENDAR_ID_STORE)
val calendarId: StateFlow<Long> = context.dataStore.data
.map { preferences -> preferences[ID_KEY] ?: ID_DEFAULT }
.stateIn(
scope = scope,
started = SharingStarted.Lazily,
initialValue = ID_DEFAULT,
)
val calendarName: StateFlow<String> = context.dataStore.data
.map { preferences -> preferences[NAME_KEY] ?: NAME_DEFAULT }
.stateIn(
scope = scope,
started = SharingStarted.Lazily,
initialValue = NAME_DEFAULT,
)
val calendarType: StateFlow<String> = 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")
}
}

View file

@ -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<List<GoogleCalendar>> = _calendarFlow
private fun listCalendars(): List<GoogleCalendar> {
val calendars = mutableListOf<GoogleCalendar>()
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
}
}

View file

@ -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<Pill>,
val isValid: Boolean,
val isMisspelled: Boolean,
) {
data class Date(
val day: Int,
val month: Int,
val year: Int,
) : Comparable<Date> {
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,
)
}
}

View file

@ -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<String>(
"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<String>(
"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",
)
}
}

View file

@ -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<Long, MutableStateFlow<List<Event>>>()
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<List<Event>> {
return eventFlow.getOrPut(calendarId) { MutableStateFlow(emptyList()) }
}
fun fetchEvents(
calendarId: Long,
startDate: Event.Date,
endDate: Event.Date,
refresh: Boolean,
): List<Event> {
val flow = eventFlow.getOrPut(calendarId) { MutableStateFlow(emptyList()) }
if (flow.value.isNotEmpty() && refresh.not()) {
return flow.value
}
val events = mutableListOf<Event>()
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
}
}

View file

@ -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<Navigator> {
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()
}
)
}
}

View file

@ -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<Any> = mutableStateListOf(startDestination)
fun popBackstack() = backStack.removeLastOrNull()
fun goTo(destination: Any) {
backStack.add(destination)
}
}

View file

@ -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<CalendarChooserDestination> {
CalendarChooserPage()
}
}
fun Navigator.navigateToCalendarChooserPage() {
goTo(CalendarChooserDestination)
}

View file

@ -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<EventDestination> { key ->
val viewModel = hiltViewModel<EventViewModel, EventViewModel.Factory>(
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)
}

View file

@ -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<HomePageDestination> {
HomePage()
}
}
fun Navigator.navigateToHomePage() {
goTo(HomePageDestination)
}

View file

@ -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<MonthSummaryDestination> {
MonthSummaryPage()
}
}
fun Navigator.navigateToMonthSummary() {
goTo(MonthSummaryDestination)
}

View file

@ -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,
)
}

View file

@ -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<List<Any>>,
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,
)
}
}
}
}
)
}

View file

@ -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 = { },
)
}
}
}

View file

@ -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<GoogleCalendar>,
calendarId: Long?,
): List<Any> {
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
}
}

View file

@ -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<List<Any>> = 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,
)
}
}

View file

@ -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<PillUio>,
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<EventItemUio> {
override val values: Sequence<EventItemUio>
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,
),
)
}

View file

@ -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<Event>,
): List<EventItemUio> {
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,
)
}
}

View file

@ -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<Boolean>,
misspelledFilter: State<Boolean>,
events: State<List<EventItemUio>>,
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,
)
}
}
}
)
}

View file

@ -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<Boolean> = misspelledFilterFlow
private val invalidFilterFlow = MutableStateFlow(argument.invalidFilter)
val invalidFilter: StateFlow<Boolean> = invalidFilterFlow
@OptIn(ExperimentalCoroutinesApi::class)
val events: StateFlow<List<EventItemUio>> = 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<Event> ->
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
}
}

View file

@ -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<PillUio> {
override val values: Sequence<PillUio>
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,
),
)
}

View file

@ -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,
)
}
}

View file

@ -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<MonthSummaryBoxUio> {
val calendar = Calendar.getInstance().apply {
time = Date()
set(Calendar.DAY_OF_MONTH, 1)
}
override val values: Sequence<MonthSummaryBoxUio>
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,
),
)
}

View file

@ -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<Event>,
): List<MonthSummaryItemUio> {
return events
.fold(hashMapOf<Event.Date, MutableList<Event>>()) { acc, event ->
acc.also { it.getOrPut(event.date.copy(day = 1)) { mutableListOf() }.add(event) }
}
.mapKeys { entry ->
val pills = entry.value
.fold(hashMapOf<String, Int>()) { 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<Event>,
): List<MonthSummaryBoxUio> {
var maxPillAmount = 0
return events
.fold(hashMapOf<Event.Date, MutableList<Event>>()) { 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 }
}
}

View file

@ -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<Pair<String, Int>>,
) : 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<MonthSummaryItemUio> {
override val values: Sequence<MonthSummaryItemUio>
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,
),
)
)
}

View file

@ -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<Boolean>,
events: State<List<MonthSummaryItem>>,
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,
)
}
}
}
}
)
}

View file

@ -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<Boolean> = displayTypeFlow
@OptIn(ExperimentalCoroutinesApi::class)
val events: StateFlow<List<MonthSummaryItem>> = googleCalendarIdRepository.calendarId
.flatMapLatest { id: Long? ->
when (id) {
null -> flowOf(emptyList())
else -> eventRepository.eventFlow(calendarId = id)
}
}.combine(displayTypeFlow) { events: List<Event>, 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()
}
}

View file

@ -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)

View file

@ -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
)
}

View file

@ -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
)
*/
)

View file

@ -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)
}

View file

@ -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

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,680Q497,680 508.5,668.5Q520,657 520,640Q520,623 508.5,611.5Q497,600 480,600Q463,600 451.5,611.5Q440,623 440,640Q440,657 451.5,668.5Q463,680 480,680ZM440,520L520,520L520,280L440,280L440,520ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z" />
</vector>

View file

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View file

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M564,880L394,710L450,654L564,768L790,542L846,598L564,880ZM120,640L314,120L408,120L602,640L510,640L464,508L254,508L208,640L120,640ZM282,432L438,432L362,216L358,216L282,432Z" />
</vector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,7 @@
<resources>
<string name="app_name">Céphalée</string>
<string name="calendar_chooser_title">Choix du calendrier</string>
<string name="event_title">Évennement migraineux</string>
<string name="month_summary_title">Suivit des migraines</string>
</resources>

Some files were not shown because too many files have changed in this diff Show more