Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fb723fba68 | |||
|
|
ffc7ed414f | ||
| af75965a31 | |||
| 1b456d2133 | |||
| a826e82bdc | |||
| ed680b4482 |
20
CHANGELOG.md
20
CHANGELOG.md
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.1.1] — 2026-06-08
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- F-Droid metadata format: renamed locale dirs from `de/` to `de-DE/`,
|
||||||
|
`short_description.txt` to `summary.txt`, `full_description.txt` to
|
||||||
|
`description.txt` (fastlane format that fdroidserver actually reads,
|
||||||
|
matching the working HouseHoldKeaper convention)
|
||||||
|
- Added `icon.png` (512x512) per locale; fdroidserver does NOT
|
||||||
|
auto-extract icons from APKs that only contain XML adaptive icons
|
||||||
|
(which is what minSdk-29 apps produce), so the app was rendered
|
||||||
|
blank-iconed in F-Droid clients
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- CI pipeline cleanup: `lintDebug`/`testDebugUnitTest` instead of full
|
||||||
|
`lint`/`test` (cuts ~50% of lint work since release variant lint is
|
||||||
|
redundant for V1 single-variant build)
|
||||||
|
- Release workflow drops the lint step from its CI-sanity job since
|
||||||
|
the same lint already ran via `ci.yaml` when the underlying commit
|
||||||
|
hit main
|
||||||
|
|
||||||
## [0.1.0] — 2026-06-08
|
## [0.1.0] — 2026-06-08
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -74,7 +74,10 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
testOptions {
|
testOptions {
|
||||||
unitTests.all { it.useJUnitPlatform() }
|
unitTests {
|
||||||
|
all { it.useJUnitPlatform() }
|
||||||
|
isReturnDefaultValues = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +90,7 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
|
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
|
|
||||||
implementation(platform(libs.androidx.compose.bom))
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
@@ -96,10 +100,14 @@ dependencies {
|
|||||||
implementation(libs.androidx.material3)
|
implementation(libs.androidx.material3)
|
||||||
|
|
||||||
implementation(libs.hilt.android)
|
implementation(libs.hilt.android)
|
||||||
|
implementation(libs.androidx.hilt.navigation.compose)
|
||||||
ksp(libs.hilt.compiler)
|
ksp(libs.hilt.compiler)
|
||||||
|
|
||||||
implementation(libs.androidx.datastore.preferences)
|
implementation(libs.androidx.datastore.preferences)
|
||||||
|
|
||||||
|
implementation(libs.kotlinx.datetime)
|
||||||
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
|
||||||
debugImplementation(libs.androidx.ui.tooling)
|
debugImplementation(libs.androidx.ui.tooling)
|
||||||
debugImplementation(libs.androidx.ui.test.manifest)
|
debugImplementation(libs.androidx.ui.test.manifest)
|
||||||
|
|
||||||
@@ -107,9 +115,12 @@ dependencies {
|
|||||||
testRuntimeOnly(libs.junit.jupiter.engine)
|
testRuntimeOnly(libs.junit.jupiter.engine)
|
||||||
testRuntimeOnly(libs.junit.platform.launcher)
|
testRuntimeOnly(libs.junit.platform.launcher)
|
||||||
testImplementation(libs.truth)
|
testImplementation(libs.truth)
|
||||||
|
testImplementation(libs.turbine)
|
||||||
|
testImplementation(libs.kotlinx.coroutines.test)
|
||||||
|
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
androidTestImplementation(libs.androidx.test.rules)
|
||||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.data.calendar
|
||||||
|
|
||||||
|
import android.provider.CalendarContract
|
||||||
|
|
||||||
|
internal object CalendarProjection {
|
||||||
|
val COLUMNS: Array<String> = arrayOf(
|
||||||
|
CalendarContract.Calendars._ID,
|
||||||
|
CalendarContract.Calendars.CALENDAR_DISPLAY_NAME,
|
||||||
|
CalendarContract.Calendars.ACCOUNT_NAME,
|
||||||
|
CalendarContract.Calendars.ACCOUNT_TYPE,
|
||||||
|
CalendarContract.Calendars.CALENDAR_COLOR,
|
||||||
|
CalendarContract.Calendars.VISIBLE,
|
||||||
|
)
|
||||||
|
|
||||||
|
const val IDX_ID = 0
|
||||||
|
const val IDX_DISPLAY_NAME = 1
|
||||||
|
const val IDX_ACCOUNT_NAME = 2
|
||||||
|
const val IDX_ACCOUNT_TYPE = 3
|
||||||
|
const val IDX_COLOR = 4
|
||||||
|
const val IDX_VISIBLE = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object InstanceProjection {
|
||||||
|
val COLUMNS: Array<String> = arrayOf(
|
||||||
|
CalendarContract.Instances._ID,
|
||||||
|
CalendarContract.Instances.EVENT_ID,
|
||||||
|
CalendarContract.Instances.CALENDAR_ID,
|
||||||
|
CalendarContract.Instances.TITLE,
|
||||||
|
CalendarContract.Instances.BEGIN,
|
||||||
|
CalendarContract.Instances.END,
|
||||||
|
CalendarContract.Instances.ALL_DAY,
|
||||||
|
CalendarContract.Instances.EVENT_COLOR,
|
||||||
|
CalendarContract.Instances.CALENDAR_COLOR,
|
||||||
|
CalendarContract.Instances.EVENT_LOCATION,
|
||||||
|
)
|
||||||
|
|
||||||
|
const val IDX_INSTANCE_ID = 0
|
||||||
|
const val IDX_EVENT_ID = 1
|
||||||
|
const val IDX_CALENDAR_ID = 2
|
||||||
|
const val IDX_TITLE = 3
|
||||||
|
const val IDX_BEGIN = 4
|
||||||
|
const val IDX_END = 5
|
||||||
|
const val IDX_ALL_DAY = 6
|
||||||
|
const val IDX_EVENT_COLOR = 7
|
||||||
|
const val IDX_CALENDAR_COLOR = 8
|
||||||
|
const val IDX_LOCATION = 9
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object EventDetailProjection {
|
||||||
|
val COLUMNS: Array<String> = arrayOf(
|
||||||
|
CalendarContract.Events._ID,
|
||||||
|
CalendarContract.Events.TITLE,
|
||||||
|
CalendarContract.Events.DESCRIPTION,
|
||||||
|
CalendarContract.Events.ORGANIZER,
|
||||||
|
CalendarContract.Events.RRULE,
|
||||||
|
CalendarContract.Events.EVENT_COLOR,
|
||||||
|
CalendarContract.Events.CALENDAR_COLOR,
|
||||||
|
CalendarContract.Events.DTSTART,
|
||||||
|
CalendarContract.Events.DTEND,
|
||||||
|
CalendarContract.Events.ALL_DAY,
|
||||||
|
CalendarContract.Events.EVENT_LOCATION,
|
||||||
|
CalendarContract.Events.CALENDAR_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
const val IDX_EVENT_ID = 0
|
||||||
|
const val IDX_TITLE = 1
|
||||||
|
const val IDX_DESCRIPTION = 2
|
||||||
|
const val IDX_ORGANIZER = 3
|
||||||
|
const val IDX_RRULE = 4
|
||||||
|
const val IDX_EVENT_COLOR = 5
|
||||||
|
const val IDX_CALENDAR_COLOR = 6
|
||||||
|
const val IDX_DTSTART = 7
|
||||||
|
const val IDX_DTEND = 8
|
||||||
|
const val IDX_ALL_DAY = 9
|
||||||
|
const val IDX_LOCATION = 10
|
||||||
|
const val IDX_CALENDAR_ID = 11
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object AttendeeProjection {
|
||||||
|
val COLUMNS: Array<String> = arrayOf(
|
||||||
|
CalendarContract.Attendees.ATTENDEE_NAME,
|
||||||
|
CalendarContract.Attendees.ATTENDEE_EMAIL,
|
||||||
|
CalendarContract.Attendees.ATTENDEE_STATUS,
|
||||||
|
)
|
||||||
|
|
||||||
|
const val IDX_NAME = 0
|
||||||
|
const val IDX_EMAIL = 1
|
||||||
|
const val IDX_STATUS = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object Fallbacks {
|
||||||
|
const val UNNAMED_CALENDAR = "(Unbenannter Kalender)"
|
||||||
|
const val UNTITLED_EVENT = "(Ohne Titel)"
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.data.calendar
|
||||||
|
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
|
fun Long.toKotlinInstantFromEpochMillis(): Instant = Instant.fromEpochMilliseconds(this)
|
||||||
|
|
||||||
|
fun Instant.toEpochMillis(): Long = toEpochMilliseconds()
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.domain
|
||||||
|
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
|
data class CalendarSource(
|
||||||
|
val id: Long,
|
||||||
|
val displayName: String,
|
||||||
|
val accountName: String,
|
||||||
|
val accountType: String,
|
||||||
|
val color: Int,
|
||||||
|
val isVisibleInSystem: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class EventInstance(
|
||||||
|
val instanceId: Long,
|
||||||
|
val eventId: Long,
|
||||||
|
val calendarId: Long,
|
||||||
|
val title: String,
|
||||||
|
val start: Instant,
|
||||||
|
val end: Instant,
|
||||||
|
val isAllDay: Boolean,
|
||||||
|
val color: Int,
|
||||||
|
val location: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class EventDetail(
|
||||||
|
val instance: EventInstance,
|
||||||
|
val description: String?,
|
||||||
|
val organizer: String?,
|
||||||
|
val attendees: List<Attendee>,
|
||||||
|
val rrule: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Attendee(
|
||||||
|
val name: String,
|
||||||
|
val email: String?,
|
||||||
|
val status: AttendeeStatus,
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class AttendeeStatus {
|
||||||
|
Accepted,
|
||||||
|
Declined,
|
||||||
|
Tentative,
|
||||||
|
NeedsAction,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class FailureReason {
|
||||||
|
PermissionRevoked,
|
||||||
|
NoCalendarsConfigured,
|
||||||
|
ProviderUnavailable,
|
||||||
|
EventNotFound,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.data.calendar
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import kotlin.time.Instant
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class TimeBridgeTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `epoch millis round-trips through Instant`() {
|
||||||
|
val original = 1_717_840_800_000L // 2024-06-08T10:00:00Z
|
||||||
|
val instant = original.toKotlinInstantFromEpochMillis()
|
||||||
|
assertThat(instant.toEpochMillis()).isEqualTo(original)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `zero millis maps to Instant epoch`() {
|
||||||
|
assertThat(0L.toKotlinInstantFromEpochMillis()).isEqualTo(Instant.fromEpochMilliseconds(0L))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `negative epoch millis is supported`() {
|
||||||
|
val original = -1_000_000L
|
||||||
|
assertThat(original.toKotlinInstantFromEpochMillis().toEpochMillis()).isEqualTo(original)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.domain
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import kotlin.time.Instant
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class ModelsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `CalendarSource is a data class with structural equality`() {
|
||||||
|
val a = CalendarSource(1L, "Work", "x@y", "com.google", 0xFF112233.toInt(), true)
|
||||||
|
val b = CalendarSource(1L, "Work", "x@y", "com.google", 0xFF112233.toInt(), true)
|
||||||
|
assertThat(a).isEqualTo(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `EventInstance is a data class with structural equality`() {
|
||||||
|
val start = Instant.fromEpochMilliseconds(0L)
|
||||||
|
val end = Instant.fromEpochMilliseconds(3_600_000L)
|
||||||
|
val a = EventInstance(10L, 1L, 1L, "Meet", start, end, false, 0xFF000000.toInt(), null)
|
||||||
|
val b = EventInstance(10L, 1L, 1L, "Meet", start, end, false, 0xFF000000.toInt(), null)
|
||||||
|
assertThat(a).isEqualTo(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `AttendeeStatus enum has all five variants`() {
|
||||||
|
assertThat(AttendeeStatus.values().toSet()).isEqualTo(
|
||||||
|
setOf(
|
||||||
|
AttendeeStatus.Accepted,
|
||||||
|
AttendeeStatus.Declined,
|
||||||
|
AttendeeStatus.Tentative,
|
||||||
|
AttendeeStatus.NeedsAction,
|
||||||
|
AttendeeStatus.Unknown,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `FailureReason enum has all five variants`() {
|
||||||
|
assertThat(FailureReason.values().toSet()).isEqualTo(
|
||||||
|
setOf(
|
||||||
|
FailureReason.PermissionRevoked,
|
||||||
|
FailureReason.NoCalendarsConfigured,
|
||||||
|
FailureReason.ProviderUnavailable,
|
||||||
|
FailureReason.EventNotFound,
|
||||||
|
FailureReason.Unknown,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `EventDetail composes EventInstance plus extras`() {
|
||||||
|
val instance = EventInstance(
|
||||||
|
instanceId = 10L,
|
||||||
|
eventId = 1L,
|
||||||
|
calendarId = 1L,
|
||||||
|
title = "Meet",
|
||||||
|
start = Instant.fromEpochMilliseconds(0L),
|
||||||
|
end = Instant.fromEpochMilliseconds(60_000L),
|
||||||
|
isAllDay = false,
|
||||||
|
color = 0xFFAABBCC.toInt(),
|
||||||
|
location = null,
|
||||||
|
)
|
||||||
|
val detail = EventDetail(
|
||||||
|
instance = instance,
|
||||||
|
description = "Brief description",
|
||||||
|
organizer = "x@y",
|
||||||
|
attendees = listOf(Attendee("Alice", "alice@x", AttendeeStatus.Accepted)),
|
||||||
|
rrule = "FREQ=WEEKLY",
|
||||||
|
)
|
||||||
|
assertThat(detail.instance.title).isEqualTo("Meet")
|
||||||
|
assertThat(detail.attendees).hasSize(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
BIN
fdroid-metadata/de.jeanlucmakiola.calendula/de-DE/icon.png
Normal file
BIN
fdroid-metadata/de.jeanlucmakiola.calendula/de-DE/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
BIN
fdroid-metadata/de.jeanlucmakiola.calendula/en-US/icon.png
Normal file
BIN
fdroid-metadata/de.jeanlucmakiola.calendula/en-US/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
@@ -17,6 +17,12 @@ junitPlatform = "6.1.0"
|
|||||||
truth = "1.4.5"
|
truth = "1.4.5"
|
||||||
androidxJunit = "1.3.0"
|
androidxJunit = "1.3.0"
|
||||||
espressoCore = "3.7.0"
|
espressoCore = "3.7.0"
|
||||||
|
kotlinxDatetime = "0.7.0"
|
||||||
|
kotlinxCoroutines = "1.10.2"
|
||||||
|
turbine = "1.2.0"
|
||||||
|
hiltNavigationCompose = "1.3.0"
|
||||||
|
lifecycleCompose = "2.10.0"
|
||||||
|
androidxTestRules = "1.7.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
# AndroidX core
|
# AndroidX core
|
||||||
@@ -53,6 +59,25 @@ truth = { group = "com.google.truth", name = "truth", version.ref = "truth" }
|
|||||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidxJunit" }
|
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidxJunit" }
|
||||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||||
|
|
||||||
|
# Domain time
|
||||||
|
kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" }
|
||||||
|
|
||||||
|
# Coroutines (transitively pulled by hilt-android, pinned explicit)
|
||||||
|
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
|
||||||
|
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
|
||||||
|
|
||||||
|
# Test - Flow assertions
|
||||||
|
turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine" }
|
||||||
|
|
||||||
|
# Hilt navigation-compose (for hiltViewModel() in Composables)
|
||||||
|
androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
|
||||||
|
|
||||||
|
# Lifecycle compose (for collectAsStateWithLifecycle)
|
||||||
|
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleCompose" }
|
||||||
|
|
||||||
|
# Android tests - GrantPermissionRule
|
||||||
|
androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidxTestRules" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
|
|||||||
Reference in New Issue
Block a user