Initial commit
77
.gitignore
vendored
Normal 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
|
|
@ -0,0 +1,3 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
1
.idea/.name
generated
Normal file
|
|
@ -0,0 +1 @@
|
|||
Headache
|
||||
6
.idea/AndroidProjectSystem.xml
generated
Normal 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>
|
||||
48
.idea/androidTestResultsUserPreferences.xml
generated
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
||||
61
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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
|
|
@ -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
17
.idea/runConfigurations.xml
generated
Normal 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
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
122
app/build.gradle.kts
Normal 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
|
|
@ -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
|
||||
BIN
app/release/baselineProfiles/0/app-release.dm
Normal file
BIN
app/release/baselineProfiles/1/app-release.dm
Normal file
37
app/release/output-metadata.json
Normal 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
|
||||
}
|
||||
BIN
app/src/debug/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
5
app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
BIN
app/src/debug/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
app/src/debug/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
app/src/debug/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
app/src/debug/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
app/src/debug/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
app/src/debug/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
app/src/debug/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
app/src/debug/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
app/src/debug/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 21 KiB |
4
app/src/debug/res/values/ic_launcher_background.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#212121</color>
|
||||
</resources>
|
||||
31
app/src/main/AndroidManifest.xml
Normal 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>
|
||||
BIN
app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 200 KiB |
|
|
@ -0,0 +1,7 @@
|
|||
package com.pixelized.headache
|
||||
|
||||
import android.app.Application
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
@HiltAndroidApp
|
||||
class HeadacheApplication : Application()
|
||||
50
app/src/main/java/com/pixelized/headache/MainActivity.kt
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
app/src/main/java/com/pixelized/headache/module/AppModule.kt
Normal 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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
14
app/src/main/java/com/pixelized/headache/ui/page/MainPage.kt
Normal 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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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 = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
11
app/src/main/java/com/pixelized/headache/ui/theme/Color.kt
Normal 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)
|
||||
58
app/src/main/java/com/pixelized/headache/ui/theme/Theme.kt
Normal 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
|
||||
)
|
||||
}
|
||||
34
app/src/main/java/com/pixelized/headache/ui/theme/Type.kt
Normal 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
|
||||
)
|
||||
*/
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
9
app/src/main/res/drawable/ic_error_24px.xml
Normal 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>
|
||||
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal 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>
|
||||
30
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_spellcheck_24px.xml
Normal 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>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal 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>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal 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>
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 24 KiB |
7
app/src/main/res/values-fr/strings.xml
Normal 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>
|
||||