From a9452a220b0b18aaded0997ad0536d895c42e8e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 20:21:48 +0000 Subject: [PATCH 1/6] Add LOCATION to the iCalendar entry model Parse and serialize the RFC 5545 LOCATION property (as written by Thunderbird and others) into a new IcalEntry.location field, and persist it in a new IcalEntryDto.location column (migration 13.sqm). LOCATION used to be an unknown property kept verbatim in extraProperties. Rows stored that way are lifted into the new field when read from the database, so an edited location is not serialized twice. The Claude AI response already returned a location; it is now applied. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JkZW9tpCWK1Aib8cuFGrmk --- CLAUDE.md | 2 +- .../core/data/claude/ClaudeResponseDto.kt | 3 +- .../core/data/ics/KnownIcsPropertyName.kt | 1 + .../repository/IcalEntryRepositoryImpl.kt | 6 +- .../screens/core/domain/IcalEntry.kt | 3 +- .../core/mapper/dto/IcalEntryMapper.kt | 18 +++++- .../core/mapper/ics/IcalEntryIcsParser.kt | 3 +- .../core/mapper/ics/IcalEntryIcsSerializer.kt | 1 + .../at/techbee/spectacled/sqldelight/13.sqm | 1 + .../spectacled/sqldelight/icalentry_dto.sq | 10 +++- .../core/mapper/dto/IcalEntryMapperTest.kt | 56 +++++++++++++++++++ .../core/mapper/ics/IcalEntryRoundTripTest.kt | 25 +++++++++ 12 files changed, 117 insertions(+), 12 deletions(-) create mode 100644 shared/src/commonMain/sqldelight/at/techbee/spectacled/sqldelight/13.sqm create mode 100644 shared/src/commonTest/kotlin/at/techbee/spectacled/screens/core/mapper/dto/IcalEntryMapperTest.kt diff --git a/CLAUDE.md b/CLAUDE.md index e8861644..a8576f37 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,7 +55,7 @@ per-variant entry (`ListScreenJournals/Notes/Tasks.kt`). - **Repositories:** interface in `core/domain/repository/`, implementation in `core/data/repository/`. - **Database:** SQLDelight (`shared/src/commonMain/sqldelight/…`). Schema changes need a - new numbered migration (`13.sqm` next) alongside the `.sq` edit — never edit an + new numbered migration (`14.sqm` next) alongside the `.sq` edit — never edit an existing `.sqm`. - **Strings:** source strings only in `shared/src/commonMain/composeResources/values/strings.xml`. Never hand-edit diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/data/claude/ClaudeResponseDto.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/data/claude/ClaudeResponseDto.kt index 62d12f02..601446fb 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/data/claude/ClaudeResponseDto.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/data/claude/ClaudeResponseDto.kt @@ -28,6 +28,7 @@ data class ClaudeResponseDto( description = data.description ?: icalEntry.description, dtStart = if(icalEntry.isTask() || icalEntry.isJournal()) parseIcsDateTime(data.dtstart) else null, due = if(icalEntry.isTask()) parseIcsDateTime(data.due) else null, + location = data.location ?: icalEntry.location, categories = data.categories ?: icalEntry.categories ) } catch (e: Exception) { @@ -52,7 +53,7 @@ data class ClaudeIcalDataDto( @SerialName("description") val description: String? = null, @SerialName("dtstart") val dtstart: String? = null, @SerialName("due") val due: String? = null, - @SerialName("location") val location: String? = null, // Note: IcalEntry currently doesn't have a location field + @SerialName("location") val location: String? = null, @SerialName("categories") val categories: List? = null ) diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/data/ics/KnownIcsPropertyName.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/data/ics/KnownIcsPropertyName.kt index 3a5aaad8..906889f8 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/data/ics/KnownIcsPropertyName.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/data/ics/KnownIcsPropertyName.kt @@ -19,5 +19,6 @@ enum class KnownIcsPropertyName(val propertyName: String) { PERCENT_COMPLETE("PERCENT-COMPLETE"), RELATED_TO("RELATED-TO"), URL("URL"), + LOCATION("LOCATION"), ATTACH("ATTACH") } diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/data/repository/IcalEntryRepositoryImpl.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/data/repository/IcalEntryRepositoryImpl.kt index c9f48795..01174c38 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/data/repository/IcalEntryRepositoryImpl.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/data/repository/IcalEntryRepositoryImpl.kt @@ -203,7 +203,8 @@ class IcalEntryRepositoryImpl( calendarComponent = icalEntryDto.calendarComponent, parentUid = icalEntryDto.parentUid, relType = icalEntryDto.relType, - url = icalEntryDto.url + url = icalEntryDto.url, + location = icalEntryDto.location ) // insert, but if the UID exists, it will be ignored db.icalentry_dtoQueries.insertIcalEntry( @@ -234,7 +235,8 @@ class IcalEntryRepositoryImpl( calendarComponent = icalEntryDto.calendarComponent, parentUid = icalEntryDto.parentUid, relType = icalEntryDto.relType, - url = icalEntryDto.url + url = icalEntryDto.url, + location = icalEntryDto.location ) } diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/domain/IcalEntry.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/domain/IcalEntry.kt index 272df030..3dd3baa5 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/domain/IcalEntry.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/domain/IcalEntry.kt @@ -83,7 +83,8 @@ data class IcalEntry( val calendarComponent: CalendarComponent, val parentUid: String? = null, val relType: String? = null, - val url: Url? = null + val url: Url? = null, + val location: String? = null ) { companion object { diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/mapper/dto/IcalEntryMapper.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/mapper/dto/IcalEntryMapper.kt index c3b0e729..400fec48 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/mapper/dto/IcalEntryMapper.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/mapper/dto/IcalEntryMapper.kt @@ -3,6 +3,7 @@ package at.techbee.spectacled.screens.core.mapper.dto import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import at.techbee.spectacled.screens.core.data.ics.IcsDateTime +import at.techbee.spectacled.screens.core.data.ics.KnownIcsPropertyName import at.techbee.spectacled.screens.core.data.ics.RawIcsProperty import at.techbee.spectacled.screens.core.domain.Attachment import at.techbee.spectacled.screens.core.domain.CalendarComponent @@ -13,6 +14,7 @@ import at.techbee.spectacled.screens.core.domain.SyncState import at.techbee.spectacled.screens.core.mapper.ics.escapeIcsValue import at.techbee.spectacled.screens.core.mapper.ics.formatIcsDateTime import at.techbee.spectacled.screens.core.mapper.ics.parseIcsDateTime +import at.techbee.spectacled.screens.core.mapper.ics.parseProperty import at.techbee.spectacled.screens.core.mapper.ics.splitIcsList import at.techbee.spectacled.sqldelight.IcalEntryDto import io.ktor.http.Url @@ -34,6 +36,14 @@ fun IcalEntryDto.toDomain(attachments: List = emptyList()): IcalEntr emptyList() // Fallback if data is corrupted } + // Rows stored before LOCATION became a known property kept it as a raw extra property. + // Lift it into the dedicated field so it isn't serialized twice once the location is edited. + val legacyLocationProp = if (this.location == null) + extraProps.firstOrNull { it.name == KnownIcsPropertyName.LOCATION.propertyName } + else null + val location = this.location + ?: legacyLocationProp?.let { parseProperty(it.unfoldedLine).value.ifEmpty { null } } + return IcalEntry( id = this.id, calendarId = this.calendarId, @@ -53,7 +63,7 @@ fun IcalEntryDto.toDomain(attachments: List = emptyList()): IcalEntr categories = this.categories?.let { splitIcsList(it) } ?: emptyList(), created = parseIcsDateTime(this.created) ?: IcsDateTime.now(), lastModified = parseIcsDateTime(this.lastModified) ?: IcsDateTime.now(), - extraProperties = extraProps, + extraProperties = if (legacyLocationProp != null) extraProps - legacyLocationProp else extraProps, attachments = attachments, orderNo = this.orderNo, syncState = this.syncState?.let { SyncState.entries.find { it.name == this.syncState } } ?: SyncState.LOCAL_MODIFIED, @@ -62,7 +72,8 @@ fun IcalEntryDto.toDomain(attachments: List = emptyList()): IcalEntr calendarComponent = CalendarComponent.entries.find { it.name == this.calendarComponent } ?: CalendarComponent.VJOURNAL, parentUid = this.parentUid, relType = this.relType, - url = this.url?.let { Url(it) } + url = this.url?.let { Url(it) }, + location = location ) } @@ -105,6 +116,7 @@ fun IcalEntry.toDto(): IcalEntryDto { calendarComponent = this.calendarComponent.name, parentUid = this.parentUid, relType = this.relType, - url = this.url?.toString() + url = this.url?.toString(), + location = this.location?.ifEmpty { null } ) } diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/mapper/ics/IcalEntryIcsParser.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/mapper/ics/IcalEntryIcsParser.kt index cdb384ae..dfd4de18 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/mapper/ics/IcalEntryIcsParser.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/mapper/ics/IcalEntryIcsParser.kt @@ -358,7 +358,8 @@ fun parseIcalEntryBlock( calendarComponent = calendarComponent, parentUid = parentUid, relType = relType, - url = url + url = url, + location = knownProps[KnownIcsPropertyName.LOCATION.propertyName]?.firstOrNull()?.value?.ifEmpty { null } ) } diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/mapper/ics/IcalEntryIcsSerializer.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/mapper/ics/IcalEntryIcsSerializer.kt index c092ee35..3def334f 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/mapper/ics/IcalEntryIcsSerializer.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/mapper/ics/IcalEntryIcsSerializer.kt @@ -163,6 +163,7 @@ fun serializeVJournal(icalEntry: IcalEntry, fileManager: FileManager? = null): S } icalEntry.summary?.let { lines += "${KnownIcsPropertyName.SUMMARY.propertyName}:${escapeIcsValue(it)}" } icalEntry.description?.let { lines += "${KnownIcsPropertyName.DESCRIPTION.propertyName}:${escapeIcsValue(it)}" } + icalEntry.location?.let { lines += "${KnownIcsPropertyName.LOCATION.propertyName}:${escapeIcsValue(it)}" } icalEntry.color?.let { lines += "${KnownIcsPropertyName.COLOR.propertyName}:${it.toArgb()}" } icalEntry.status?.let { lines += "${KnownIcsPropertyName.STATUS.propertyName}:${it.rfcName}" } icalEntry.classification?.let { lines += "${KnownIcsPropertyName.CLASSIFICATION.propertyName}:${it.name}" } diff --git a/shared/src/commonMain/sqldelight/at/techbee/spectacled/sqldelight/13.sqm b/shared/src/commonMain/sqldelight/at/techbee/spectacled/sqldelight/13.sqm new file mode 100644 index 00000000..ce5a1032 --- /dev/null +++ b/shared/src/commonMain/sqldelight/at/techbee/spectacled/sqldelight/13.sqm @@ -0,0 +1 @@ +ALTER TABLE IcalEntryDto ADD COLUMN location TEXT; diff --git a/shared/src/commonMain/sqldelight/at/techbee/spectacled/sqldelight/icalentry_dto.sq b/shared/src/commonMain/sqldelight/at/techbee/spectacled/sqldelight/icalentry_dto.sq index 9c59f1e7..e7c27fd7 100644 --- a/shared/src/commonMain/sqldelight/at/techbee/spectacled/sqldelight/icalentry_dto.sq +++ b/shared/src/commonMain/sqldelight/at/techbee/spectacled/sqldelight/icalentry_dto.sq @@ -29,6 +29,7 @@ CREATE TABLE IcalEntryDto ( parentUid TEXT, relType TEXT, url TEXT, + location TEXT, UNIQUE(calendarId, uid) ); @@ -100,7 +101,8 @@ INSERT OR IGNORE INTO IcalEntryDto ( calendarComponent, parentUid, relType, - url + url, + location ) VALUES ( :calendarId, @@ -130,7 +132,8 @@ VALUES ( :calendarComponent, :parentUid, :relType, - :url + :url, + :location ); updateIcalEntry: @@ -164,7 +167,8 @@ SET calendarComponent = :calendarComponent, parentUid = :parentUid, relType = :relType, - url = :url + url = :url, + location = :location WHERE calendarId = :calendarId AND uid = :uid; deleteTrashed: diff --git a/shared/src/commonTest/kotlin/at/techbee/spectacled/screens/core/mapper/dto/IcalEntryMapperTest.kt b/shared/src/commonTest/kotlin/at/techbee/spectacled/screens/core/mapper/dto/IcalEntryMapperTest.kt new file mode 100644 index 00000000..55ca8443 --- /dev/null +++ b/shared/src/commonTest/kotlin/at/techbee/spectacled/screens/core/mapper/dto/IcalEntryMapperTest.kt @@ -0,0 +1,56 @@ +package at.techbee.spectacled.screens.core.mapper.dto + +import at.techbee.spectacled.screens.core.data.ics.RawIcsProperty +import at.techbee.spectacled.screens.core.domain.CalendarComponent +import at.techbee.spectacled.screens.core.domain.IcalEntry +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class IcalEntryMapperTest { + + @Test + fun locationSurvivesDtoRoundTrip() { + val entry = IcalEntry(uid = "loc-uid", location = "Stadtpark", calendarComponent = CalendarComponent.VTODO) + + assertEquals("Stadtpark", entry.toDto().toDomain().location) + } + + @Test + fun emptyLocationIsStoredAsNull() { + val entry = IcalEntry(uid = "loc-uid", location = "", calendarComponent = CalendarComponent.VTODO) + + assertNull(entry.toDto().location) + } + + @Test + fun legacyLocationInExtraPropertiesIsLiftedIntoField() { + // Rows written before LOCATION was a known property carry it as a raw extra property. + val other = RawIcsProperty(name = "X-CUSTOM", unfoldedLine = "X-CUSTOM:keep me") + val legacy = IcalEntry( + uid = "legacy-uid", + extraProperties = listOf( + RawIcsProperty(name = "LOCATION", unfoldedLine = "LOCATION;LANGUAGE=de:Hauptstraße 1\\, Wien"), + other + ), + calendarComponent = CalendarComponent.VJOURNAL + ) + + val mapped = legacy.toDto().toDomain() + + assertEquals("Hauptstraße 1, Wien", mapped.location) + assertEquals(listOf(other), mapped.extraProperties) + } + + @Test + fun dedicatedLocationColumnWinsOverLegacyExtraProperty() { + val entry = IcalEntry( + uid = "both-uid", + location = "New place", + extraProperties = listOf(RawIcsProperty(name = "LOCATION", unfoldedLine = "LOCATION:Old place")), + calendarComponent = CalendarComponent.VJOURNAL + ) + + assertEquals("New place", entry.toDto().toDomain().location) + } +} diff --git a/shared/src/commonTest/kotlin/at/techbee/spectacled/screens/core/mapper/ics/IcalEntryRoundTripTest.kt b/shared/src/commonTest/kotlin/at/techbee/spectacled/screens/core/mapper/ics/IcalEntryRoundTripTest.kt index 30193c65..49e2e587 100644 --- a/shared/src/commonTest/kotlin/at/techbee/spectacled/screens/core/mapper/ics/IcalEntryRoundTripTest.kt +++ b/shared/src/commonTest/kotlin/at/techbee/spectacled/screens/core/mapper/ics/IcalEntryRoundTripTest.kt @@ -92,6 +92,7 @@ class IcalEntryRoundTripTest { status = Status.IN_PROCESS, percentComplete = 50L, priority = 1L, + location = "Stadtpark, Bench 3; near the pond", dtstamp = utcDateTime, created = utcDateTime, lastModified = utcDateTime, @@ -106,6 +107,30 @@ class IcalEntryRoundTripTest { assertEquals(Status.IN_PROCESS, parsed.status) assertEquals(50L, parsed.percentComplete) assertEquals(1L, parsed.priority) + assertEquals(original.location, parsed.location) + assertTrue(parsed.extraProperties.none { it.name == "LOCATION" }) + } + + @Test + fun thunderbirdTaskLocationIsParsed() { + val ics = """ + BEGIN:VCALENDAR + PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN + VERSION:2.0 + BEGIN:VTODO + UID:tb-task-1 + DTSTAMP:20260711T101530Z + SUMMARY:Buy groceries + LOCATION:Main Street 12\, Vienna + END:VTODO + END:VCALENDAR + """.trimIndent().replace("\n", "\r\n") + + val parsed = parseIcalEntries(ics).single() + + assertEquals("Main Street 12, Vienna", parsed.location) + assertTrue(parsed.extraProperties.none { it.name == "LOCATION" }) + assertTrue(serializeVCalendar(parsed).contains("LOCATION:Main Street 12\\, Vienna")) } @Test From 4695cf630e8303ab51286bbafcd35716c9bb16fd Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 23:36:33 +0000 Subject: [PATCH 2/6] Drop the lift of legacy LOCATION extra properties Entries stored before LOCATION became a known property keep it in extraProperties. Only a few testers are affected, so they are told instead of carrying migration code in the mapper. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JkZW9tpCWK1Aib8cuFGrmk --- .../core/mapper/dto/IcalEntryMapper.kt | 14 ++------ .../core/mapper/dto/IcalEntryMapperTest.kt | 32 ------------------- 2 files changed, 2 insertions(+), 44 deletions(-) diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/mapper/dto/IcalEntryMapper.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/mapper/dto/IcalEntryMapper.kt index 400fec48..ca8a3000 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/mapper/dto/IcalEntryMapper.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/mapper/dto/IcalEntryMapper.kt @@ -3,7 +3,6 @@ package at.techbee.spectacled.screens.core.mapper.dto import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import at.techbee.spectacled.screens.core.data.ics.IcsDateTime -import at.techbee.spectacled.screens.core.data.ics.KnownIcsPropertyName import at.techbee.spectacled.screens.core.data.ics.RawIcsProperty import at.techbee.spectacled.screens.core.domain.Attachment import at.techbee.spectacled.screens.core.domain.CalendarComponent @@ -14,7 +13,6 @@ import at.techbee.spectacled.screens.core.domain.SyncState import at.techbee.spectacled.screens.core.mapper.ics.escapeIcsValue import at.techbee.spectacled.screens.core.mapper.ics.formatIcsDateTime import at.techbee.spectacled.screens.core.mapper.ics.parseIcsDateTime -import at.techbee.spectacled.screens.core.mapper.ics.parseProperty import at.techbee.spectacled.screens.core.mapper.ics.splitIcsList import at.techbee.spectacled.sqldelight.IcalEntryDto import io.ktor.http.Url @@ -36,14 +34,6 @@ fun IcalEntryDto.toDomain(attachments: List = emptyList()): IcalEntr emptyList() // Fallback if data is corrupted } - // Rows stored before LOCATION became a known property kept it as a raw extra property. - // Lift it into the dedicated field so it isn't serialized twice once the location is edited. - val legacyLocationProp = if (this.location == null) - extraProps.firstOrNull { it.name == KnownIcsPropertyName.LOCATION.propertyName } - else null - val location = this.location - ?: legacyLocationProp?.let { parseProperty(it.unfoldedLine).value.ifEmpty { null } } - return IcalEntry( id = this.id, calendarId = this.calendarId, @@ -63,7 +53,7 @@ fun IcalEntryDto.toDomain(attachments: List = emptyList()): IcalEntr categories = this.categories?.let { splitIcsList(it) } ?: emptyList(), created = parseIcsDateTime(this.created) ?: IcsDateTime.now(), lastModified = parseIcsDateTime(this.lastModified) ?: IcsDateTime.now(), - extraProperties = if (legacyLocationProp != null) extraProps - legacyLocationProp else extraProps, + extraProperties = extraProps, attachments = attachments, orderNo = this.orderNo, syncState = this.syncState?.let { SyncState.entries.find { it.name == this.syncState } } ?: SyncState.LOCAL_MODIFIED, @@ -73,7 +63,7 @@ fun IcalEntryDto.toDomain(attachments: List = emptyList()): IcalEntr parentUid = this.parentUid, relType = this.relType, url = this.url?.let { Url(it) }, - location = location + location = this.location ) } diff --git a/shared/src/commonTest/kotlin/at/techbee/spectacled/screens/core/mapper/dto/IcalEntryMapperTest.kt b/shared/src/commonTest/kotlin/at/techbee/spectacled/screens/core/mapper/dto/IcalEntryMapperTest.kt index 55ca8443..44b9d9d4 100644 --- a/shared/src/commonTest/kotlin/at/techbee/spectacled/screens/core/mapper/dto/IcalEntryMapperTest.kt +++ b/shared/src/commonTest/kotlin/at/techbee/spectacled/screens/core/mapper/dto/IcalEntryMapperTest.kt @@ -1,6 +1,5 @@ package at.techbee.spectacled.screens.core.mapper.dto -import at.techbee.spectacled.screens.core.data.ics.RawIcsProperty import at.techbee.spectacled.screens.core.domain.CalendarComponent import at.techbee.spectacled.screens.core.domain.IcalEntry import kotlin.test.Test @@ -22,35 +21,4 @@ class IcalEntryMapperTest { assertNull(entry.toDto().location) } - - @Test - fun legacyLocationInExtraPropertiesIsLiftedIntoField() { - // Rows written before LOCATION was a known property carry it as a raw extra property. - val other = RawIcsProperty(name = "X-CUSTOM", unfoldedLine = "X-CUSTOM:keep me") - val legacy = IcalEntry( - uid = "legacy-uid", - extraProperties = listOf( - RawIcsProperty(name = "LOCATION", unfoldedLine = "LOCATION;LANGUAGE=de:Hauptstraße 1\\, Wien"), - other - ), - calendarComponent = CalendarComponent.VJOURNAL - ) - - val mapped = legacy.toDto().toDomain() - - assertEquals("Hauptstraße 1, Wien", mapped.location) - assertEquals(listOf(other), mapped.extraProperties) - } - - @Test - fun dedicatedLocationColumnWinsOverLegacyExtraProperty() { - val entry = IcalEntry( - uid = "both-uid", - location = "New place", - extraProperties = listOf(RawIcsProperty(name = "LOCATION", unfoldedLine = "LOCATION:Old place")), - calendarComponent = CalendarComponent.VJOURNAL - ) - - assertEquals("New place", entry.toDto().toDomain().location) - } } From 776a72488c30d6082eef2939c602403f433f4ebe Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Sat, 3 Oct 2026 04:18:50 +0300 Subject: [PATCH 3/6] Added UI to add location --- .../composeResources/values/strings.xml | 2 + .../details/presentation/DetailsAction.kt | 1 + .../details/presentation/DetailsScreen.kt | 13 ++ .../details/presentation/DetailsScreenRoot.kt | 21 +++ .../details/presentation/DetailsState.kt | 2 +- .../details/presentation/DetailsViewModel.kt | 17 ++ .../components/EditLocationBottomSheet.kt | 157 ++++++++++++++++++ .../presentation/components/LocationCard.kt | 99 +++++++++++ 8 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/EditLocationBottomSheet.kt create mode 100644 shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/LocationCard.kt diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index d8afb58b..522c63c4 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -157,6 +157,8 @@ Not supported in this collection URL Add URL + Location + Add location Add Link file by URL The file stays on its server - Spectacled only stores the link and downloads it when opened. diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsAction.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsAction.kt index ee5ef312..46c960d2 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsAction.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsAction.kt @@ -21,6 +21,7 @@ sealed interface DetailsAction { data class OnUpdateSubtaskProgress(val percent: Long, val subtaskIcalEntryId: Long): DetailsAction data class OnAddSubtask(val summary: String): DetailsAction data class OnUpdateUrl(val url: Url?): DetailsAction + data class OnUpdateLocation(val location: String?): DetailsAction data class OnAddAttachment(val fileName: String, val bytes: ByteArray, val mimeType: String?): DetailsAction data class OnAddUrlAttachment(val url: Url): DetailsAction data class OnOpenAttachment(val attachmentUid: String): DetailsAction diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsScreen.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsScreen.kt index 903ac0d5..9f7a3c1d 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsScreen.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsScreen.kt @@ -85,6 +85,7 @@ import at.techbee.spectacled.screens.core.presentation.components.WavyHorizontal import at.techbee.spectacled.screens.details.presentation.components.AttachmentCard import at.techbee.spectacled.screens.details.presentation.components.DateTimeCard import at.techbee.spectacled.screens.details.presentation.components.FormattingBar +import at.techbee.spectacled.screens.details.presentation.components.LocationCard import at.techbee.spectacled.screens.details.presentation.components.UrlCard import at.techbee.spectacled.screens.list.presentation.components.MetaInfoCard import at.techbee.spectacled.screens.list.presentation.components.TaskListItem @@ -388,6 +389,18 @@ fun DetailsScreen( } } + AnimatedVisibility(state.icalEntry.location != null) { + Column(modifier = Modifier.fillMaxWidth()) { + + LocationCard( + location = state.icalEntry.location?:"", + allowEditing = state.allowEditing(), + onClick = onAction, + modifier = Modifier.padding(vertical = 8.dp) + ) + } + } + AnimatedVisibility(state.icalEntry.attachments.isNotEmpty()) { Column( verticalArrangement = Arrangement.spacedBy(2.dp), diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsScreenRoot.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsScreenRoot.kt index 83ad9fee..8b5355f7 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsScreenRoot.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsScreenRoot.kt @@ -19,6 +19,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.Label import androidx.compose.material.icons.outlined.AddBox import androidx.compose.material.icons.outlined.AddLink +import androidx.compose.material.icons.outlined.AddLocation import androidx.compose.material.icons.outlined.AddTask import androidx.compose.material.icons.outlined.Attachment import androidx.compose.material.icons.outlined.DatasetLinked @@ -79,6 +80,7 @@ import at.techbee.spectacled.screens.details.presentation.components.DeleteIcalE import at.techbee.spectacled.screens.details.presentation.components.DetailsMoreBottomSheet import at.techbee.spectacled.screens.details.presentation.components.DetailsTopBar import at.techbee.spectacled.screens.details.presentation.components.DrawingCanvasBottomSheet +import at.techbee.spectacled.screens.details.presentation.components.EditLocationBottomSheet import at.techbee.spectacled.screens.details.presentation.components.EditUrlBottomSheet import at.techbee.spectacled.screens.details.presentation.components.JournalStatusPickerBottomSheet import at.techbee.spectacled.screens.details.presentation.components.MoveIcalEntryDialog @@ -91,6 +93,7 @@ import spectacled.shared.generated.resources.Res import spectacled.shared.generated.resources.add_attachment import spectacled.shared.generated.resources.add_drawing import spectacled.shared.generated.resources.add_from_gallery +import spectacled.shared.generated.resources.add_location import spectacled.shared.generated.resources.add_photo import spectacled.shared.generated.resources.add_subtask import spectacled.shared.generated.resources.add_url @@ -253,6 +256,12 @@ fun DetailsScreenRoot( onUrlEdited = { detailsViewModel.onAction(DetailsAction.OnUpdateUrl(it)) }, onDismiss = { detailsViewModel.onAction(DetailsAction.OnShowSheetOrDialog(null)) } ) + DetailsSheetOrDialog.EDIT_LOCATION -> + EditLocationBottomSheet( + initialLocation = detailsState.icalEntry.location, + onLocationEdited = { detailsViewModel.onAction(DetailsAction.OnUpdateLocation(it)) }, + onDismiss = { detailsViewModel.onAction(DetailsAction.OnShowSheetOrDialog(null)) } + ) DetailsSheetOrDialog.ADD_ATTACHMENT_URL -> AddUrlAttachmentBottomSheet( onUrlConfirmed = { detailsViewModel.onAction(DetailsAction.OnAddUrlAttachment(it)) }, @@ -464,6 +473,18 @@ fun DetailsScreenRoot( }, ) + if(detailsState.icalEntry.isTask()) { + DropdownMenuItem( + text = { Text(stringResource(Res.string.add_location)) }, + leadingIcon = { Icon(Icons.Outlined.AddLocation, stringResource(Res.string.add_location)) }, + enabled = detailsState.icalEntry.location == null, + onClick = { + detailsViewModel.onAction(DetailsAction.OnShowSheetOrDialog(DetailsSheetOrDialog.EDIT_LOCATION)) + addMoreExpanded = false + }, + ) + } + DropdownMenuItem( text = { Column { diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsState.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsState.kt index c3ecb28c..6b61c4ce 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsState.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsState.kt @@ -55,7 +55,7 @@ data class DetailsState @OptIn(ExperimentalTime::class) constructor( } enum class DetailsSheetOrDialog { - DELETE, MOVE, MORE, COLOR_SELECTOR, CATEGORY_SELECTOR, JOURNAL_STATUS_PICKER, TASK_STATUS_PICKER, ADD_SUBTASKS, EDIT_URL, ADD_ATTACHMENT_URL + DELETE, MOVE, MORE, COLOR_SELECTOR, CATEGORY_SELECTOR, JOURNAL_STATUS_PICKER, TASK_STATUS_PICKER, ADD_SUBTASKS, EDIT_URL, EDIT_LOCATION, ADD_ATTACHMENT_URL } // Action to perform right after the details screen opens diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsViewModel.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsViewModel.kt index 889c551d..9343b27c 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsViewModel.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsViewModel.kt @@ -315,6 +315,7 @@ class DetailsViewModel( is DetailsAction.OnUpdateCategories -> onUpdateCategories(action.addCategory, action.removeCategory) is DetailsAction.OnUpdateColor -> onUpdateColor(action.color) is DetailsAction.OnUpdateUrl -> onUpdateUrl(action.url) + is DetailsAction.OnUpdateLocation -> onUpdateLocation(action.location) is DetailsAction.OnUpdateDescription -> onUpdateDescription(action.description) is DetailsAction.OnUpdateSummary -> onUpdateSummary(action.summary) DetailsAction.OnDelete -> { @@ -532,6 +533,22 @@ class DetailsViewModel( } } + private fun onUpdateLocation(newLocation: String?) { + + if(!state.value.allowEditing()) + return + + _state.update { + it.copy( + icalEntry = it.icalEntry.copy( + location = newLocation, + lastModified = IcsDateTime.now(), + syncState = it.icalEntry.syncState.afterLocalEdit() + ) + ) + } + } + @OptIn(ExperimentalTime::class) private fun onUpdateDtStart(newDtStart: IcsDateTime?) { diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/EditLocationBottomSheet.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/EditLocationBottomSheet.kt new file mode 100644 index 00000000..2a4dcb4b --- /dev/null +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/EditLocationBottomSheet.kt @@ -0,0 +1,157 @@ +package at.techbee.spectacled.screens.details.presentation.components + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Clear +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.TextButton +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.Preview +import at.techbee.spectacled.SpectacledVariant +import at.techbee.spectacled.screens.core.presentation.components.BottomSheetWithMenu +import at.techbee.spectacled.theme.AppTheme +import kotlinx.coroutines.delay +import org.jetbrains.compose.resources.stringResource +import spectacled.shared.generated.resources.Res +import spectacled.shared.generated.resources.delete +import spectacled.shared.generated.resources.done +import spectacled.shared.generated.resources.location +import kotlin.time.Duration.Companion.milliseconds + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EditLocationBottomSheet( + initialLocation: String?, + onLocationEdited: (String?) -> Unit, + onDismiss: () -> Unit + ) { + + var textFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf( + TextFieldValue( + text = initialLocation ?: "", + selection = TextRange(initialLocation?.length ?: 0) + ) + ) + } + val keyboardController = LocalSoftwareKeyboardController.current + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + delay(300.milliseconds) + focusRequester.requestFocus() + } + + BottomSheetWithMenu( + onDismiss = { onDismiss() }, + headline = stringResource(Res.string.location), + menuActionRight = { + TextButton( + onClick = { + onDismiss() + }, + ) { + Text(stringResource(Res.string.done)) + } + }, + menuActionLeft = { + TextButton( + onClick = { + textFieldValue = TextFieldValue("") + onLocationEdited(null) + onDismiss() + } + ) { + Text(stringResource(Res.string.delete)) + } + } + ) { + + TextField( + value = textFieldValue, + onValueChange = { + textFieldValue = it + onLocationEdited(it.text.ifBlank { null }) + }, + placeholder = { Text(stringResource(Res.string.location)) }, + trailingIcon = { + IconButton( + onClick = { + if (textFieldValue.text.isNotBlank()) { + onLocationEdited(null) + textFieldValue = TextFieldValue("") + } + keyboardController?.hide() + }, + enabled = textFieldValue.text.isNotBlank(), + content = { Icon(Icons.Outlined.Clear, stringResource(Res.string.delete)) } + ) + }, + singleLine = true, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Words, + keyboardType = KeyboardType.PostalAddress, + autoCorrectEnabled = false, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { onDismiss() } + ), + shape = MaterialTheme.shapes.small, + modifier = Modifier.fillMaxWidth().focusRequester(focusRequester) + ) + + } +} + + +@Preview +@Composable +private fun EditLocationBottomSheet_Preview() { + AppTheme(spectacledVariant = SpectacledVariant.JOURNALS) { + Scaffold { + EditLocationBottomSheet( + initialLocation = "Am Stadtpark 3a/4/c, 1020 Wien", + onLocationEdited = {}, + onDismiss = { } + ) + } + } +} + +@Preview +@Composable +private fun EditLocationBottomSheet_empty_Preview() { + AppTheme(spectacledVariant = SpectacledVariant.JOURNALS) { + Scaffold { + EditLocationBottomSheet( + initialLocation = null, + onLocationEdited = {}, + onDismiss = { } + ) + } + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/LocationCard.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/LocationCard.kt new file mode 100644 index 00000000..4acb2262 --- /dev/null +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/LocationCard.kt @@ -0,0 +1,99 @@ +package at.techbee.spectacled.screens.details.presentation.components + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material.icons.outlined.PinDrop +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.unit.dp +import at.techbee.spectacled.SpectacledVariant +import at.techbee.spectacled.screens.details.presentation.DetailsAction +import at.techbee.spectacled.screens.details.presentation.DetailsSheetOrDialog +import at.techbee.spectacled.theme.AppTheme +import org.jetbrains.compose.resources.stringResource +import spectacled.shared.generated.resources.Res +import spectacled.shared.generated.resources.edit + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LocationCard( + location: String, + allowEditing: Boolean, + onClick: (DetailsAction) -> Unit, + modifier: Modifier = Modifier +) { + + Card( + onClick = { + TODO() + }, + elevation = CardDefaults.cardElevation(0.dp), + colors = CardDefaults.cardColors(containerColor = Color.Transparent, disabledContainerColor = Color.Transparent), + modifier = modifier + ) { + + Row(verticalAlignment = Alignment.CenterVertically) { + + IconButton( + onClick = {}, + enabled = false + ) { + Icon(Icons.Outlined.PinDrop, null) + } + + Text( + text = location, + modifier = Modifier.weight(1f) + ) + + if(allowEditing) { + IconButton( + onClick = { + onClick(DetailsAction.OnShowSheetOrDialog(DetailsSheetOrDialog.EDIT_LOCATION)) + } + ) { + Icon(Icons.Outlined.Edit, stringResource(Res.string.edit)) + } + } + } + } +} + + +@Preview +@Composable +private fun LocationCard_Preview() { + AppTheme(spectacledVariant = SpectacledVariant.TASKS) { + LocationCard( + location = "Am Stadtpark 123/4c, 1030 Wien", + allowEditing = true, + onClick = {}, + modifier = Modifier.padding(8.dp) + ) + } +} + +@Preview +@Composable +private fun LocationCard_readonly_Preview() { + AppTheme(spectacledVariant = SpectacledVariant.TASKS) { + LocationCard( + location = "Am Stadtpark 123/4c, 1030 Wien", + allowEditing = false, + onClick = {}, + modifier = Modifier.padding(8.dp) + ) + } +} + From 89f1064a7a4ae786b102b090e6ac2549341c8552 Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Sat, 3 Oct 2026 12:00:47 -0700 Subject: [PATCH 4/6] Added option to open map from LocationCard.kt --- .../presentation/components/LocationCard.kt | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/LocationCard.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/LocationCard.kt index 4acb2262..aa3fca8b 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/LocationCard.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/LocationCard.kt @@ -15,12 +15,17 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import at.techbee.spectacled.SpectacledVariant +import at.techbee.spectacled.screens.core.Platforms +import at.techbee.spectacled.screens.core.getPlatform import at.techbee.spectacled.screens.details.presentation.DetailsAction import at.techbee.spectacled.screens.details.presentation.DetailsSheetOrDialog import at.techbee.spectacled.theme.AppTheme +import io.github.aakira.napier.Napier +import io.ktor.http.encodeURLParameter import org.jetbrains.compose.resources.stringResource import spectacled.shared.generated.resources.Res import spectacled.shared.generated.resources.edit @@ -34,9 +39,15 @@ fun LocationCard( modifier: Modifier = Modifier ) { + val uriHandler = LocalUriHandler.current + Card( onClick = { - TODO() + try { + uriHandler.openUri(mapsUriFor(location)) + } catch (e: Exception) { + Napier.w(e.stackTraceToString()) + } }, elevation = CardDefaults.cardElevation(0.dp), colors = CardDefaults.cardColors(containerColor = Color.Transparent, disabledContainerColor = Color.Transparent), @@ -70,6 +81,15 @@ fun LocationCard( } } +fun mapsUriFor(location: String, platform: Platforms = getPlatform().platform): String { + val query = location.encodeURLParameter() + return when (platform) { + Platforms.ANDROID -> "geo:0,0?q=$query" + Platforms.IOS -> "https://maps.apple.com/?q=$query" + Platforms.DESKTOP, Platforms.WASM -> "https://www.openstreetmap.org/search?query=$query" + } +} + @Preview @Composable From 02c2237a43e62a52e2222268861e46879a1f631e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 19:32:03 +0000 Subject: [PATCH 5/6] Raise Kotlin daemon heap to 6 GB to stop CI out-of-memory failures :shared:compileTestDevelopmentExecutableKotlinJs and the WasmJs counterpart intermittently fail on CI with "Not enough memory to run compilation" (GC overhead limit exceeded) at the 3 GB daemon heap, on main and on feature branches alike. The ubuntu-latest runner has 16 GB, so 4 GB Gradle plus 6 GB Kotlin daemon still leaves headroom for the lint workers. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JkZW9tpCWK1Aib8cuFGrmk --- gradle.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index de3d049d..5a5d25a2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,7 @@ #Kotlin kotlin.code.style=official -kotlin.daemon.jvmargs=-Xmx3072M +# The JS and Wasm test executables are compiled in this daemon and ran out of memory at 3 GB on CI. +kotlin.daemon.jvmargs=-Xmx6144M #Gradle org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 From b8ec5d0f49c935871ec696a933faf6143d010f19 Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Sun, 4 Oct 2026 12:57:30 -0700 Subject: [PATCH 6/6] Changed LocationCard.kt and UrlCard.kt to open through button instead of card, but edit through card click. --- .../presentation/components/LocationCard.kt | 25 +++++++++--------- .../presentation/components/UrlCard.kt | 26 +++++++++---------- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/LocationCard.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/LocationCard.kt index aa3fca8b..eb07081d 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/LocationCard.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/LocationCard.kt @@ -3,7 +3,7 @@ package at.techbee.spectacled.screens.details.presentation.components import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material.icons.automirrored.outlined.OpenInNew import androidx.compose.material.icons.outlined.PinDrop import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -28,7 +28,7 @@ import io.github.aakira.napier.Napier import io.ktor.http.encodeURLParameter import org.jetbrains.compose.resources.stringResource import spectacled.shared.generated.resources.Res -import spectacled.shared.generated.resources.edit +import spectacled.shared.generated.resources.open_in_browser @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -43,11 +43,8 @@ fun LocationCard( Card( onClick = { - try { - uriHandler.openUri(mapsUriFor(location)) - } catch (e: Exception) { - Napier.w(e.stackTraceToString()) - } + if(allowEditing) + onClick(DetailsAction.OnShowSheetOrDialog(DetailsSheetOrDialog.EDIT_LOCATION)) }, elevation = CardDefaults.cardElevation(0.dp), colors = CardDefaults.cardColors(containerColor = Color.Transparent, disabledContainerColor = Color.Transparent), @@ -68,14 +65,16 @@ fun LocationCard( modifier = Modifier.weight(1f) ) - if(allowEditing) { - IconButton( - onClick = { - onClick(DetailsAction.OnShowSheetOrDialog(DetailsSheetOrDialog.EDIT_LOCATION)) + IconButton( + onClick = { + try { + uriHandler.openUri(mapsUriFor(location)) + } catch (e: Exception) { + Napier.w(e.stackTraceToString()) } - ) { - Icon(Icons.Outlined.Edit, stringResource(Res.string.edit)) } + ) { + Icon(Icons.AutoMirrored.Outlined.OpenInNew, stringResource(Res.string.open_in_browser)) } } } diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/UrlCard.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/UrlCard.kt index 51ac9d4f..3036275b 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/UrlCard.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/UrlCard.kt @@ -3,7 +3,7 @@ package at.techbee.spectacled.screens.details.presentation.components import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material.icons.automirrored.outlined.OpenInNew import androidx.compose.material.icons.outlined.Link import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -28,6 +28,7 @@ import io.ktor.http.Url import org.jetbrains.compose.resources.stringResource import spectacled.shared.generated.resources.Res import spectacled.shared.generated.resources.edit +import spectacled.shared.generated.resources.open_in_browser @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -42,12 +43,9 @@ fun UrlCard( Card( onClick = { - try { - uriHandler.openUri(url.toString()) - } catch (e: Exception) { - Napier.w(e.stackTraceToString()) - } - }, + if(allowEditing) + onClick(DetailsAction.OnShowSheetOrDialog(DetailsSheetOrDialog.EDIT_URL)) + }, elevation = CardDefaults.cardElevation(0.dp), colors = CardDefaults.cardColors(containerColor = Color.Transparent, disabledContainerColor = Color.Transparent), modifier = modifier @@ -69,14 +67,16 @@ fun UrlCard( modifier = Modifier.weight(1f) ) - if(allowEditing) { - IconButton( - onClick = { - onClick(DetailsAction.OnShowSheetOrDialog(DetailsSheetOrDialog.EDIT_URL)) + IconButton( + onClick = { + try { + uriHandler.openUri(url.toString()) + } catch (e: Exception) { + Napier.w(e.stackTraceToString()) } - ) { - Icon(Icons.Outlined.Edit, stringResource(Res.string.edit)) } + ) { + Icon(Icons.AutoMirrored.Outlined.OpenInNew, stringResource(Res.string.open_in_browser)) } } }