mirror of
https://github.com/avinal/nikki.git
synced 2026-07-03 21:40:09 +05:30
Add data layer: Memos API client, domain models, repository
API client (Ktor): - Auth via Bearer token HttpSend interceptor - CRUD endpoints: listMemos, getMemo, createMemo, updateMemo, deleteMemo - PATCH uses updateMask as query parameter (gRPC-Gateway format) - Reaction upsert/delete endpoints - ApiResult sealed class for error handling Domain models: - Memo with string IDs (Memos uses short UUIDs, not integers) - Attachment, Reaction, User, Task, MemoVisibility - MemoRepository: offline-first with Room cache, 5-min TTL - AuthRepository: PAT login, token validation DTOs match actual Memos API JSON (camelCase, no @SerialName needed). Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
package com.avinal.memos.api
|
||||
|
||||
sealed class ApiResult<out T> {
|
||||
data class Success<T>(val data: T) : ApiResult<T>()
|
||||
data class Error(val code: Int, val message: String) : ApiResult<Nothing>()
|
||||
data class NetworkError(val exception: Throwable) : ApiResult<Nothing>()
|
||||
}
|
||||
|
||||
inline fun <T, R> ApiResult<T>.map(transform: (T) -> R): ApiResult<R> = when (this) {
|
||||
is ApiResult.Success -> ApiResult.Success(transform(data))
|
||||
is ApiResult.Error -> this
|
||||
is ApiResult.NetworkError -> this
|
||||
}
|
||||
|
||||
inline fun <T> ApiResult<T>.onSuccess(action: (T) -> Unit): ApiResult<T> {
|
||||
if (this is ApiResult.Success) action(data)
|
||||
return this
|
||||
}
|
||||
|
||||
inline fun <T> ApiResult<T>.onError(action: (String) -> Unit): ApiResult<T> {
|
||||
when (this) {
|
||||
is ApiResult.Error -> action(message)
|
||||
is ApiResult.NetworkError -> action(exception.message ?: "Network error")
|
||||
else -> {}
|
||||
}
|
||||
return this
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.avinal.memos.api
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.HttpSend
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.plugins.plugin
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
object HttpClientFactory {
|
||||
|
||||
fun create(tokenProvider: () -> String?): HttpClient {
|
||||
val client = HttpClient {
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
ignoreUnknownKeys = true
|
||||
isLenient = true
|
||||
encodeDefaults = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
client.plugin(HttpSend).intercept { request ->
|
||||
tokenProvider()?.let { token ->
|
||||
request.headers.append(HttpHeaders.Authorization, "Bearer $token")
|
||||
}
|
||||
execute(request)
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.avinal.memos.api
|
||||
|
||||
import com.avinal.memos.api.model.CreateMemoRequest
|
||||
import com.avinal.memos.api.model.FieldMask
|
||||
import com.avinal.memos.api.model.ListMemosResponse
|
||||
import com.avinal.memos.api.model.MemoDto
|
||||
import com.avinal.memos.api.model.ReactionDto
|
||||
import com.avinal.memos.api.model.UpsertReactionRequest
|
||||
import com.avinal.memos.api.model.UpdateMemoBody
|
||||
import com.avinal.memos.api.model.UpdateMemoRequest
|
||||
import com.avinal.memos.api.model.UserDto
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.delete
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.parameter
|
||||
import io.ktor.client.request.patch
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.http.isSuccess
|
||||
|
||||
class MemosApiClient(
|
||||
private val httpClient: HttpClient,
|
||||
val baseUrlProvider: () -> String,
|
||||
) {
|
||||
private fun url(path: String): String = "${baseUrlProvider().trimEnd('/')}/api/v1$path"
|
||||
|
||||
suspend fun getMe(): ApiResult<UserDto> = apiCall {
|
||||
httpClient.get(url("/auth/me")).body()
|
||||
}
|
||||
|
||||
suspend fun listMemos(
|
||||
pageSize: Int = 20,
|
||||
pageToken: String = "",
|
||||
filter: String = "",
|
||||
): ApiResult<ListMemosResponse> = apiCall {
|
||||
httpClient.get(url("/memos")) {
|
||||
parameter("pageSize", pageSize)
|
||||
if (pageToken.isNotEmpty()) parameter("pageToken", pageToken)
|
||||
if (filter.isNotEmpty()) parameter("filter", filter)
|
||||
}.body()
|
||||
}
|
||||
|
||||
suspend fun getMemo(id: String): ApiResult<MemoDto> = apiCall {
|
||||
httpClient.get(url("/memos/$id")).body()
|
||||
}
|
||||
|
||||
suspend fun createMemo(
|
||||
content: String,
|
||||
visibility: String = "PRIVATE",
|
||||
): ApiResult<MemoDto> = apiCall {
|
||||
httpClient.post(url("/memos")) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(CreateMemoRequest(content = content, visibility = visibility))
|
||||
}.body()
|
||||
}
|
||||
|
||||
suspend fun updateMemo(
|
||||
id: String,
|
||||
content: String? = null,
|
||||
visibility: String? = null,
|
||||
pinned: Boolean? = null,
|
||||
state: String? = null,
|
||||
): ApiResult<MemoDto> = apiCall {
|
||||
val paths = buildList {
|
||||
if (content != null) add("content")
|
||||
if (visibility != null) add("visibility")
|
||||
if (pinned != null) add("pinned")
|
||||
if (state != null) add("state")
|
||||
}
|
||||
httpClient.patch(url("/memos/$id")) {
|
||||
parameter("updateMask", paths.joinToString(","))
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(UpdateMemoBody(
|
||||
content = content,
|
||||
visibility = visibility,
|
||||
pinned = pinned,
|
||||
state = state,
|
||||
))
|
||||
}.body()
|
||||
}
|
||||
|
||||
suspend fun searchMemos(query: String): ApiResult<ListMemosResponse> = apiCall {
|
||||
httpClient.get(url("/memos")) {
|
||||
parameter("pageSize", 50)
|
||||
parameter("filter", "content.contains(\"$query\")")
|
||||
}.body()
|
||||
}
|
||||
|
||||
suspend fun upsertReaction(memoId: String, reactionType: String): ApiResult<ReactionDto> = apiCall {
|
||||
httpClient.post(url("/memos/$memoId/reactions")) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(UpsertReactionRequest(
|
||||
reaction = ReactionDto(reactionType = reactionType, contentId = "memos/$memoId")
|
||||
))
|
||||
}.body()
|
||||
}
|
||||
|
||||
suspend fun deleteReaction(memoId: String, reactionId: String): ApiResult<Unit> = apiCall {
|
||||
val response = httpClient.delete(url("/memos/$memoId/reactions/$reactionId"))
|
||||
if (!response.status.isSuccess()) {
|
||||
throw ApiException(response.status.value, response.bodyAsText())
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteMemo(id: String): ApiResult<Unit> = apiCall {
|
||||
val response = httpClient.delete(url("/memos/$id"))
|
||||
if (!response.status.isSuccess()) {
|
||||
throw ApiException(response.status.value, response.bodyAsText())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun <T> apiCall(block: () -> T): ApiResult<T> =
|
||||
try {
|
||||
ApiResult.Success(block())
|
||||
} catch (e: ApiException) {
|
||||
ApiResult.Error(e.code, e.message ?: "Unknown error")
|
||||
} catch (e: io.ktor.client.plugins.ClientRequestException) {
|
||||
ApiResult.Error(e.response.status.value, e.message)
|
||||
} catch (e: io.ktor.client.plugins.ServerResponseException) {
|
||||
ApiResult.Error(e.response.status.value, e.message)
|
||||
} catch (e: Exception) {
|
||||
ApiResult.NetworkError(e)
|
||||
}
|
||||
}
|
||||
|
||||
class ApiException(val code: Int, message: String) : Exception(message)
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.avinal.memos.api.model
|
||||
|
||||
import com.avinal.memos.domain.Attachment
|
||||
import com.avinal.memos.domain.Memo
|
||||
import com.avinal.memos.domain.MemoVisibility
|
||||
import com.avinal.memos.domain.Reaction
|
||||
import com.avinal.memos.domain.User
|
||||
import kotlin.time.Instant
|
||||
|
||||
private fun String.extractId(): String = substringAfterLast("/")
|
||||
|
||||
fun MemoDto.toDomain(): Memo = Memo(
|
||||
id = name.extractId(),
|
||||
uid = uid,
|
||||
content = content,
|
||||
visibility = MemoVisibility.fromApiString(visibility),
|
||||
pinned = pinned,
|
||||
state = state,
|
||||
createTime = parseTimestamp(createTime),
|
||||
updateTime = parseTimestamp(updateTime),
|
||||
displayTime = parseTimestamp(displayTime.ifEmpty { updateTime }),
|
||||
creator = creator.extractId(),
|
||||
hasTaskList = property?.hasTaskList ?: false,
|
||||
hasIncompleteTasks = property?.hasIncompleteTasks ?: false,
|
||||
title = property?.title ?: "",
|
||||
tags = tags,
|
||||
snippet = snippet,
|
||||
attachments = attachments.map { it.toDomain() },
|
||||
reactions = reactions.map { it.toDomain() },
|
||||
)
|
||||
|
||||
fun AttachmentDto.toDomain(): Attachment = Attachment(
|
||||
name = name,
|
||||
filename = filename,
|
||||
mimeType = type,
|
||||
size = size.toLongOrNull() ?: 0L,
|
||||
externalLink = externalLink,
|
||||
)
|
||||
|
||||
fun ReactionDto.toDomain(): Reaction = Reaction(
|
||||
id = name.extractId(),
|
||||
creator = creator.extractId(),
|
||||
reactionType = reactionType,
|
||||
)
|
||||
|
||||
fun UserDto.toDomain(): User = User(
|
||||
id = name.extractId(),
|
||||
username = username,
|
||||
nickname = nickname,
|
||||
email = email,
|
||||
avatarUrl = avatarUrl,
|
||||
role = role,
|
||||
)
|
||||
|
||||
private fun parseTimestamp(value: String): Instant =
|
||||
if (value.isNotEmpty()) {
|
||||
try {
|
||||
Instant.parse(value)
|
||||
} catch (_: Exception) {
|
||||
Instant.DISTANT_PAST
|
||||
}
|
||||
} else {
|
||||
Instant.DISTANT_PAST
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.avinal.memos.api.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class MemoDto(
|
||||
val name: String = "",
|
||||
val uid: String = "",
|
||||
val creator: String = "",
|
||||
val createTime: String = "",
|
||||
val updateTime: String = "",
|
||||
val displayTime: String = "",
|
||||
val content: String = "",
|
||||
val state: String = "NORMAL",
|
||||
val visibility: String = "PRIVATE",
|
||||
val pinned: Boolean = false,
|
||||
val parent: String = "",
|
||||
val property: MemoPropertyDto? = null,
|
||||
val tags: List<String> = emptyList(),
|
||||
val snippet: String = "",
|
||||
val attachments: List<AttachmentDto> = emptyList(),
|
||||
val reactions: List<ReactionDto> = emptyList(),
|
||||
val relations: List<RelationDto> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MemoPropertyDto(
|
||||
val hasLink: Boolean = false,
|
||||
val hasTaskList: Boolean = false,
|
||||
val hasCode: Boolean = false,
|
||||
val hasIncompleteTasks: Boolean = false,
|
||||
val title: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AttachmentDto(
|
||||
val name: String = "",
|
||||
val filename: String = "",
|
||||
val type: String = "",
|
||||
val size: String = "0",
|
||||
val externalLink: String = "",
|
||||
val createTime: String = "",
|
||||
val memo: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReactionDto(
|
||||
val name: String = "",
|
||||
val creator: String = "",
|
||||
val contentId: String = "",
|
||||
val reactionType: String = "",
|
||||
val createTime: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class RelationDto(
|
||||
val memo: String = "",
|
||||
val relatedMemo: String = "",
|
||||
val type: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ListMemosResponse(
|
||||
val memos: List<MemoDto> = emptyList(),
|
||||
val nextPageToken: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CreateMemoRequest(
|
||||
val content: String,
|
||||
val visibility: String = "PRIVATE",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UpdateMemoBody(
|
||||
val content: String? = null,
|
||||
val visibility: String? = null,
|
||||
val pinned: Boolean? = null,
|
||||
val state: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UpdateMemoRequest(
|
||||
val memo: UpdateMemoBody,
|
||||
val updateMask: FieldMask,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class FieldMask(
|
||||
val paths: List<String>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UpsertReactionRequest(
|
||||
val reaction: ReactionDto,
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.avinal.memos.api.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserDto(
|
||||
val name: String = "",
|
||||
val role: String = "USER",
|
||||
val username: String = "",
|
||||
val email: String = "",
|
||||
val nickname: String = "",
|
||||
val avatarUrl: String = "",
|
||||
val createTime: String = "",
|
||||
val updateTime: String = "",
|
||||
)
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.avinal.memos.domain
|
||||
|
||||
import com.avinal.memos.api.ApiResult
|
||||
import com.avinal.memos.api.MemosApiClient
|
||||
import com.avinal.memos.api.model.toDomain
|
||||
import com.avinal.memos.util.TokenStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class AuthRepository(
|
||||
private val apiClient: MemosApiClient,
|
||||
private val tokenStore: TokenStore,
|
||||
) {
|
||||
private val _currentUser = MutableStateFlow<User?>(null)
|
||||
val currentUser: StateFlow<User?> = _currentUser.asStateFlow()
|
||||
|
||||
val isLoggedIn: Flow<Boolean> = tokenStore.accessToken.map { it != null }
|
||||
|
||||
suspend fun login(serverUrl: String, token: String): ApiResult<User> {
|
||||
val originalBaseUrl = apiClient.baseUrlProvider
|
||||
val tempClient = MemosApiClient(
|
||||
httpClient = com.avinal.memos.api.HttpClientFactory.create { token },
|
||||
baseUrlProvider = { serverUrl },
|
||||
)
|
||||
|
||||
return when (val result = tempClient.getMe()) {
|
||||
is ApiResult.Success -> {
|
||||
val user = result.data.toDomain()
|
||||
tokenStore.saveCredentials(serverUrl, token)
|
||||
_currentUser.value = user
|
||||
ApiResult.Success(user)
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
is ApiResult.NetworkError -> result
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun validateToken(): ApiResult<User> {
|
||||
return when (val result = apiClient.getMe()) {
|
||||
is ApiResult.Success -> {
|
||||
val user = result.data.toDomain()
|
||||
_currentUser.value = user
|
||||
ApiResult.Success(user)
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
is ApiResult.NetworkError -> result
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun logout() {
|
||||
tokenStore.clear()
|
||||
_currentUser.value = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.avinal.memos.domain
|
||||
|
||||
import kotlin.time.Instant
|
||||
|
||||
data class Memo(
|
||||
val id: String,
|
||||
val uid: String,
|
||||
val content: String,
|
||||
val visibility: MemoVisibility,
|
||||
val pinned: Boolean,
|
||||
val state: String,
|
||||
val createTime: Instant,
|
||||
val updateTime: Instant,
|
||||
val displayTime: Instant,
|
||||
val creator: String,
|
||||
val hasTaskList: Boolean,
|
||||
val hasIncompleteTasks: Boolean,
|
||||
val title: String,
|
||||
val tags: List<String>,
|
||||
val snippet: String,
|
||||
val attachments: List<Attachment> = emptyList(),
|
||||
val reactions: List<Reaction> = emptyList(),
|
||||
)
|
||||
|
||||
data class Attachment(
|
||||
val name: String,
|
||||
val filename: String,
|
||||
val mimeType: String,
|
||||
val size: Long,
|
||||
val externalLink: String,
|
||||
) {
|
||||
val isImage: Boolean get() = mimeType.startsWith("image/")
|
||||
}
|
||||
|
||||
data class Reaction(
|
||||
val id: String,
|
||||
val creator: String,
|
||||
val reactionType: String,
|
||||
)
|
||||
@@ -0,0 +1,167 @@
|
||||
package com.avinal.memos.domain
|
||||
|
||||
import com.avinal.memos.api.ApiResult
|
||||
import com.avinal.memos.api.MemosApiClient
|
||||
import com.avinal.memos.api.model.toDomain
|
||||
import com.avinal.memos.db.dao.MemoDao
|
||||
import com.avinal.memos.db.entity.toEntity
|
||||
import com.avinal.memos.db.entity.toDomain
|
||||
import kotlin.time.Clock
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MemoRepository(
|
||||
private val apiClient: MemosApiClient,
|
||||
private val memoDao: MemoDao,
|
||||
) {
|
||||
private var nextPageToken: String = ""
|
||||
private var hasMorePages: Boolean = true
|
||||
private var lastFetchTime: Long = 0L
|
||||
|
||||
private fun nowMillis(): Long = Clock.System.now().toEpochMilliseconds()
|
||||
|
||||
private fun isCacheStale(): Boolean = (nowMillis() - lastFetchTime) > CACHE_TTL_MS
|
||||
|
||||
fun observeMemos(): Flow<List<Memo>> {
|
||||
if (isCacheStale()) {
|
||||
CoroutineScope(Dispatchers.IO).launch { refreshMemos() }
|
||||
}
|
||||
return memoDao.observeAll().map { entities -> entities.map { it.toDomain() } }
|
||||
}
|
||||
|
||||
fun observeMemo(id: String): Flow<Memo?> =
|
||||
memoDao.observeById(id).map { it?.toDomain() }
|
||||
|
||||
suspend fun getMemo(id: String): Memo? {
|
||||
val cached = memoDao.getById(id)
|
||||
if (cached != null) return cached.toDomain()
|
||||
return when (val result = apiClient.getMemo(id)) {
|
||||
is ApiResult.Success -> {
|
||||
val memo = result.data.toDomain()
|
||||
memoDao.upsert(memo.toEntity(nowMillis()))
|
||||
memo
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun refreshMemos(): ApiResult<List<Memo>> {
|
||||
nextPageToken = ""
|
||||
hasMorePages = true
|
||||
return when (val result = apiClient.listMemos(pageSize = 50)) {
|
||||
is ApiResult.Success -> {
|
||||
val memos = result.data.memos.map { it.toDomain() }
|
||||
val now = nowMillis()
|
||||
lastFetchTime = now
|
||||
memoDao.upsertAll(memos.map { it.toEntity(now) })
|
||||
nextPageToken = result.data.nextPageToken
|
||||
hasMorePages = nextPageToken.isNotEmpty()
|
||||
ApiResult.Success(memos)
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
is ApiResult.NetworkError -> result
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadNextPage(): ApiResult<List<Memo>> {
|
||||
if (!hasMorePages) return ApiResult.Success(emptyList())
|
||||
return when (val result = apiClient.listMemos(pageSize = 50, pageToken = nextPageToken)) {
|
||||
is ApiResult.Success -> {
|
||||
val memos = result.data.memos.map { it.toDomain() }
|
||||
val now = nowMillis()
|
||||
memoDao.upsertAll(memos.map { it.toEntity(now) })
|
||||
nextPageToken = result.data.nextPageToken
|
||||
hasMorePages = nextPageToken.isNotEmpty()
|
||||
ApiResult.Success(memos)
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
is ApiResult.NetworkError -> result
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createMemo(
|
||||
content: String,
|
||||
visibility: MemoVisibility = MemoVisibility.PRIVATE,
|
||||
): ApiResult<Memo> {
|
||||
return when (val result = apiClient.createMemo(content, visibility.toApiString())) {
|
||||
is ApiResult.Success -> {
|
||||
val memo = result.data.toDomain()
|
||||
memoDao.upsert(memo.toEntity(nowMillis()))
|
||||
ApiResult.Success(memo)
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
is ApiResult.NetworkError -> result
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateMemo(
|
||||
id: String,
|
||||
content: String? = null,
|
||||
visibility: MemoVisibility? = null,
|
||||
pinned: Boolean? = null,
|
||||
): ApiResult<Memo> {
|
||||
return when (val result = apiClient.updateMemo(
|
||||
id = id,
|
||||
content = content,
|
||||
visibility = visibility?.toApiString(),
|
||||
pinned = pinned,
|
||||
)) {
|
||||
is ApiResult.Success -> {
|
||||
val memo = result.data.toDomain()
|
||||
memoDao.upsert(memo.toEntity(nowMillis()))
|
||||
ApiResult.Success(memo)
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
is ApiResult.NetworkError -> result
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun reactToMemo(memoId: String, emoji: String): ApiResult<Unit> {
|
||||
return when (apiClient.upsertReaction(memoId, emoji)) {
|
||||
is ApiResult.Success -> {
|
||||
refreshMemos()
|
||||
ApiResult.Success(Unit)
|
||||
}
|
||||
is ApiResult.Error -> ApiResult.Error(0, "Failed to react")
|
||||
is ApiResult.NetworkError -> ApiResult.NetworkError(Exception("Network error"))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun archiveMemo(id: String): ApiResult<Memo> {
|
||||
return when (val result = apiClient.updateMemo(id = id, state = "ARCHIVED")) {
|
||||
is ApiResult.Success -> {
|
||||
val memo = result.data.toDomain()
|
||||
memoDao.deleteById(id)
|
||||
ApiResult.Success(memo)
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
is ApiResult.NetworkError -> result
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteMemo(id: String): ApiResult<Unit> {
|
||||
return when (val result = apiClient.deleteMemo(id)) {
|
||||
is ApiResult.Success -> {
|
||||
memoDao.deleteById(id)
|
||||
result
|
||||
}
|
||||
is ApiResult.Error -> result
|
||||
is ApiResult.NetworkError -> result
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clearCache() {
|
||||
memoDao.deleteAll()
|
||||
lastFetchTime = 0L
|
||||
nextPageToken = ""
|
||||
hasMorePages = true
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CACHE_TTL_MS = 5 * 60 * 1000L
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.avinal.memos.domain
|
||||
|
||||
enum class MemoVisibility {
|
||||
PRIVATE, PROTECTED, PUBLIC;
|
||||
|
||||
fun toApiString(): String = name
|
||||
|
||||
companion object {
|
||||
fun fromApiString(value: String): MemoVisibility =
|
||||
entries.firstOrNull { it.name == value } ?: PRIVATE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.avinal.memos.domain
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
data class Task(
|
||||
val id: String,
|
||||
val memoId: String,
|
||||
val lineIndex: Int,
|
||||
val text: String,
|
||||
val rawText: String = text,
|
||||
val originalLine: String = "",
|
||||
val isCompleted: Boolean,
|
||||
val dueDate: LocalDate? = null,
|
||||
val priority: Int? = null,
|
||||
val labels: List<String> = emptyList(),
|
||||
val lists: List<String> = emptyList(),
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.avinal.memos.domain
|
||||
|
||||
data class User(
|
||||
val id: String,
|
||||
val username: String,
|
||||
val nickname: String,
|
||||
val email: String,
|
||||
val avatarUrl: String,
|
||||
val role: String,
|
||||
)
|
||||
Reference in New Issue
Block a user