feat(data): entity and domain mappers
The translation either way, including the repeat mask's bit order and the nullable per-alarm overrides that mean "inherit the app default" rather than a concrete value. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
package de.jeanlucmakiola.clockula.data.alarms
|
||||
|
||||
import de.jeanlucmakiola.clockula.domain.Alarm
|
||||
import de.jeanlucmakiola.clockula.domain.AlarmDraft
|
||||
import de.jeanlucmakiola.clockula.domain.DismissChallenge
|
||||
import de.jeanlucmakiola.clockula.domain.RepeatDays
|
||||
import de.jeanlucmakiola.clockula.domain.TimeOfDay
|
||||
import de.jeanlucmakiola.floret.prefs.toEnum
|
||||
import kotlin.time.Instant
|
||||
|
||||
/** Entity ↔ domain. Reads are **forgiving**: a corrupt row degrades, never throws. */
|
||||
object AlarmMapper {
|
||||
fun toDomain(entity: AlarmEntity): Alarm = Alarm(
|
||||
id = entity.id,
|
||||
time = TimeOfDay.clamped(entity.hour, entity.minute),
|
||||
label = entity.label,
|
||||
enabled = entity.enabled,
|
||||
repeatDays = RepeatDays.fromMask(entity.repeatDays),
|
||||
skipNextOccurrence = entity.skipNextOccurrence,
|
||||
ringtoneUri = entity.ringtoneUri?.takeIf { it.isNotBlank() },
|
||||
vibrate = entity.vibrate,
|
||||
snoozeMinutes = entity.snoozeMinutes,
|
||||
snoozeLimit = entity.snoozeLimit,
|
||||
volumeRampSeconds = entity.volumeRampSeconds,
|
||||
// An absent challenge is an *override that isn't set*, so it stays null;
|
||||
// a stored name the app no longer knows degrades to NONE.
|
||||
dismissChallenge = entity.dismissChallenge?.toEnum(DismissChallenge.NONE),
|
||||
createdAt = Instant.fromEpochMilliseconds(entity.createdAt),
|
||||
updatedAt = Instant.fromEpochMilliseconds(entity.updatedAt),
|
||||
)
|
||||
|
||||
fun toDomain(entities: List<AlarmEntity>): List<Alarm> = entities.map(::toDomain)
|
||||
|
||||
fun toEntity(alarm: Alarm): AlarmEntity = AlarmEntity(
|
||||
id = alarm.id,
|
||||
hour = alarm.time.hour,
|
||||
minute = alarm.time.minute,
|
||||
label = alarm.label,
|
||||
enabled = alarm.enabled,
|
||||
repeatDays = alarm.repeatDays.mask,
|
||||
skipNextOccurrence = alarm.skipNextOccurrence,
|
||||
ringtoneUri = alarm.ringtoneUri,
|
||||
vibrate = alarm.vibrate,
|
||||
snoozeMinutes = alarm.snoozeMinutes,
|
||||
snoozeLimit = alarm.snoozeLimit,
|
||||
volumeRampSeconds = alarm.volumeRampSeconds,
|
||||
dismissChallenge = alarm.dismissChallenge?.name,
|
||||
createdAt = alarm.createdAt.toEpochMilliseconds(),
|
||||
updatedAt = alarm.updatedAt.toEpochMilliseconds(),
|
||||
)
|
||||
|
||||
/** id = 0, createdAt = updatedAt = [now]. */
|
||||
fun toEntity(draft: AlarmDraft, now: Instant): AlarmEntity = AlarmEntity(
|
||||
id = 0L,
|
||||
hour = draft.time.hour,
|
||||
minute = draft.time.minute,
|
||||
label = draft.label,
|
||||
enabled = draft.enabled,
|
||||
repeatDays = draft.repeatDays.mask,
|
||||
skipNextOccurrence = false,
|
||||
ringtoneUri = draft.ringtoneUri,
|
||||
vibrate = draft.vibrate,
|
||||
snoozeMinutes = draft.snoozeMinutes,
|
||||
snoozeLimit = draft.snoozeLimit,
|
||||
volumeRampSeconds = draft.volumeRampSeconds,
|
||||
dismissChallenge = draft.dismissChallenge?.name,
|
||||
createdAt = now.toEpochMilliseconds(),
|
||||
updatedAt = now.toEpochMilliseconds(),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package de.jeanlucmakiola.clockula.data.stopwatch
|
||||
|
||||
import de.jeanlucmakiola.clockula.domain.Lap
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
object LapMapper {
|
||||
fun toDomain(entity: LapEntity): Lap = Lap(
|
||||
index = entity.lapIndex,
|
||||
split = entity.splitMillis.coerceAtLeast(0L).milliseconds,
|
||||
cumulative = entity.cumulativeMillis.coerceAtLeast(0L).milliseconds,
|
||||
)
|
||||
|
||||
fun toDomain(entities: List<LapEntity>): List<Lap> = entities.map(::toDomain)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package de.jeanlucmakiola.clockula.data.timers
|
||||
|
||||
import de.jeanlucmakiola.clockula.domain.Timer
|
||||
import de.jeanlucmakiola.clockula.domain.TimerDraft
|
||||
import de.jeanlucmakiola.clockula.domain.TimerState
|
||||
import de.jeanlucmakiola.floret.prefs.toEnum
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Instant
|
||||
|
||||
object TimerMapper {
|
||||
fun toDomain(entity: TimerEntity): Timer = Timer(
|
||||
id = entity.id,
|
||||
label = entity.label,
|
||||
duration = entity.durationMillis.coerceAtLeast(0L).milliseconds,
|
||||
state = entity.state.toEnum(TimerState.IDLE),
|
||||
remaining = entity.remainingMillis.coerceAtLeast(0L).milliseconds,
|
||||
startedAtElapsedRealtime = entity.startedAtElapsedRealtimeMillis?.milliseconds,
|
||||
endsAtElapsedRealtime = entity.endsAtElapsedRealtimeMillis?.milliseconds,
|
||||
endsAtWallClock = entity.endsAtWallClockMillis?.let(Instant::fromEpochMilliseconds),
|
||||
ringtoneUri = entity.ringtoneUri?.takeIf { it.isNotBlank() },
|
||||
sortOrder = entity.sortOrder,
|
||||
createdAt = Instant.fromEpochMilliseconds(entity.createdAt),
|
||||
updatedAt = Instant.fromEpochMilliseconds(entity.updatedAt),
|
||||
)
|
||||
|
||||
fun toDomain(entities: List<TimerEntity>): List<Timer> = entities.map(::toDomain)
|
||||
|
||||
fun toEntity(timer: Timer): TimerEntity = TimerEntity(
|
||||
id = timer.id,
|
||||
label = timer.label,
|
||||
durationMillis = timer.duration.inWholeMilliseconds,
|
||||
state = timer.state.name,
|
||||
remainingMillis = timer.remaining.inWholeMilliseconds,
|
||||
startedAtElapsedRealtimeMillis = timer.startedAtElapsedRealtime?.inWholeMilliseconds,
|
||||
endsAtElapsedRealtimeMillis = timer.endsAtElapsedRealtime?.inWholeMilliseconds,
|
||||
endsAtWallClockMillis = timer.endsAtWallClock?.toEpochMilliseconds(),
|
||||
ringtoneUri = timer.ringtoneUri,
|
||||
sortOrder = timer.sortOrder,
|
||||
createdAt = timer.createdAt.toEpochMilliseconds(),
|
||||
updatedAt = timer.updatedAt.toEpochMilliseconds(),
|
||||
)
|
||||
|
||||
fun toEntity(draft: TimerDraft, sortOrder: Int, now: Instant): TimerEntity {
|
||||
val duration = draft.duration.coerceAtLeast(Duration.ZERO)
|
||||
return TimerEntity(
|
||||
id = 0L,
|
||||
label = draft.label,
|
||||
durationMillis = duration.inWholeMilliseconds,
|
||||
state = TimerState.IDLE.name,
|
||||
remainingMillis = duration.inWholeMilliseconds,
|
||||
startedAtElapsedRealtimeMillis = null,
|
||||
endsAtElapsedRealtimeMillis = null,
|
||||
endsAtWallClockMillis = null,
|
||||
ringtoneUri = draft.ringtoneUri,
|
||||
sortOrder = sortOrder,
|
||||
createdAt = now.toEpochMilliseconds(),
|
||||
updatedAt = now.toEpochMilliseconds(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package de.jeanlucmakiola.clockula.data.worldclocks
|
||||
|
||||
import de.jeanlucmakiola.clockula.domain.WorldClock
|
||||
|
||||
object WorldClockMapper {
|
||||
/**
|
||||
* A zone id the device no longer knows is passed through untouched —
|
||||
* `WorldClock.isKnownZone` says so and M8 shows the row as broken. Blanking
|
||||
* it here would silently lose what the user chose.
|
||||
*/
|
||||
fun toDomain(entity: WorldClockEntity): WorldClock = WorldClock(
|
||||
id = entity.id,
|
||||
zoneId = entity.zoneId,
|
||||
label = entity.label?.takeIf { it.isNotBlank() },
|
||||
sortOrder = entity.sortOrder,
|
||||
)
|
||||
|
||||
fun toDomain(entities: List<WorldClockEntity>): List<WorldClock> = entities.map(::toDomain)
|
||||
|
||||
fun toEntity(clock: WorldClock): WorldClockEntity = WorldClockEntity(
|
||||
id = clock.id,
|
||||
zoneId = clock.zoneId,
|
||||
label = clock.label,
|
||||
sortOrder = clock.sortOrder,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user