diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ba3f2d6..bbbc351 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,13 @@ on: pull_request: branches: [main] +concurrency: + group: build-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest @@ -14,12 +21,12 @@ jobs: - uses: actions/setup-java@v4 with: distribution: temurin - java-version: 17 + java-version: 21 - uses: gradle/actions/setup-gradle@v4 - name: Run tests - run: ./gradlew :composeApp:testDebugUnitTest + run: ./gradlew :composeApp:testAndroidHostTest - name: Build debug APK run: ./gradlew :androidApp:assembleDebug diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d4c80b9..a1c06d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,10 @@ on: release: types: [created] +concurrency: + group: release-${{ github.event.release.tag_name }} + cancel-in-progress: false + jobs: build: runs-on: ubuntu-latest @@ -16,12 +20,14 @@ jobs: - uses: actions/setup-java@v4 with: distribution: temurin - java-version: 17 + java-version: 21 - uses: gradle/actions/setup-gradle@v4 - name: Decode keystore - run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > ${{ runner.temp }}/keystore.jks + env: + KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} + run: echo "$KEYSTORE_BASE64" | base64 -d > "${{ runner.temp }}/keystore.jks" - name: Build signed release APK env: @@ -34,6 +40,7 @@ jobs: - name: Upload release APK env: GH_TOKEN: ${{ github.token }} + TAG: ${{ github.event.release.tag_name }} run: | - mv androidApp/build/outputs/apk/release/androidApp-release.apk nikki-${{ github.event.release.tag_name }}.apk - gh release upload ${{ github.event.release.tag_name }} nikki-${{ github.event.release.tag_name }}.apk + mv androidApp/build/outputs/apk/release/androidApp-release.apk "nikki-${TAG}.apk" + gh release upload "${TAG}" "nikki-${TAG}.apk" diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index e753ae7..4d10797 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -36,11 +36,17 @@ kotlin { sourceSets { commonMain.dependencies { + @Suppress("DEPRECATION") implementation(compose.runtime) + @Suppress("DEPRECATION") implementation(compose.foundation) + @Suppress("DEPRECATION") implementation(compose.material3) + @Suppress("DEPRECATION") implementation(compose.materialIconsExtended) + @Suppress("DEPRECATION") implementation(compose.ui) + @Suppress("DEPRECATION") implementation(compose.components.resources) implementation(libs.androidx.lifecycle.viewmodel) diff --git a/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/TaskNotificationManager.kt b/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/TaskNotificationManager.kt index 0c52deb..fe1d83d 100644 --- a/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/TaskNotificationManager.kt +++ b/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/TaskNotificationManager.kt @@ -28,17 +28,17 @@ object TaskNotificationManager { manager.createNotificationChannel(NotificationChannel(CHANNEL_P1, "P1 — Urgent", NotificationManager.IMPORTANCE_HIGH).apply { description = "High priority — alarm sound, strong vibration, wakes screen" enableVibration(true); vibrationPattern = longArrayOf(0, 500, 200, 500, 200, 500) - setSound(alarmUri, audioAttr); lockscreenVisibility = android.app.Notification.VISIBILITY_PUBLIC; setShowBadge(true); setBypassDnd(true) + setSound(alarmUri, audioAttr); lockscreenVisibility = android.app.Notification.VISIBILITY_PRIVATE; setShowBadge(true); setBypassDnd(true) }) manager.createNotificationChannel(NotificationChannel(CHANNEL_P2, "P2 — Medium", NotificationManager.IMPORTANCE_HIGH).apply { description = "Medium priority — notification sound, vibration" enableVibration(true); vibrationPattern = longArrayOf(0, 300, 200, 300) - setSound(soundUri, audioAttr); lockscreenVisibility = android.app.Notification.VISIBILITY_PUBLIC; setShowBadge(true) + setSound(soundUri, audioAttr); lockscreenVisibility = android.app.Notification.VISIBILITY_PRIVATE; setShowBadge(true) }) manager.createNotificationChannel(NotificationChannel(CHANNEL_P3, "P3 — Low", NotificationManager.IMPORTANCE_DEFAULT).apply { description = "Low priority — notification sound, short vibration" enableVibration(true); vibrationPattern = longArrayOf(0, 200) - setSound(soundUri, audioAttr); lockscreenVisibility = android.app.Notification.VISIBILITY_PUBLIC + setSound(soundUri, audioAttr); lockscreenVisibility = android.app.Notification.VISIBILITY_PRIVATE }) manager.createNotificationChannel(NotificationChannel(CHANNEL_DEFAULT, "No Priority", NotificationManager.IMPORTANCE_LOW).apply { description = "No priority — silent notification" @@ -89,7 +89,7 @@ object TaskNotificationManager { .setShowWhen(true) .setPriority(notifPriority) .setCategory(NotificationCompat.CATEGORY_REMINDER) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setVisibility(NotificationCompat.VISIBILITY_PRIVATE) .setAutoCancel(true) .setOnlyAlertOnce(false) .setContentIntent(pendingOpen) @@ -99,7 +99,6 @@ object TaskNotificationManager { when (priority) { 1 -> { - builder.setFullScreenIntent(pendingOpen, true) builder.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)) builder.setVibrate(longArrayOf(0, 500, 200, 500, 200, 500)) } diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/App.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/App.kt index 06b9204..e754bb3 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/App.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/App.kt @@ -2,12 +2,14 @@ package com.avinal.memos import androidx.compose.runtime.Composable import coil3.ImageLoader +import coil3.annotation.ExperimentalCoilApi import coil3.compose.setSingletonImageLoaderFactory import coil3.network.ktor3.KtorNetworkFetcherFactory import com.avinal.memos.ui.navigation.AppNavHost import com.avinal.memos.ui.theme.NikkiTheme import com.avinal.memos.util.LocalAppDependencies +@OptIn(ExperimentalCoilApi::class) @Composable fun App(sharedText: String? = null) { val deps = LocalAppDependencies.current diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/AppDependencies.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/AppDependencies.kt index 2ccf814..d082f5b 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/AppDependencies.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/AppDependencies.kt @@ -39,7 +39,11 @@ class AppDependencies( ) } - val authRepository: AuthRepository by lazy { AuthRepository(apiClient, tokenStore) } + val authRepository: AuthRepository by lazy { + AuthRepository(apiClient, tokenStore).also { + it.onLogout = { cachedToken = null; cachedServerUrl = null } + } + } val memoRepository: MemoRepository by lazy { MemoRepository(apiClient, database.memoDao()) { com.avinal.memos.util.triggerReminderCheck() diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/domain/AuthRepository.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/domain/AuthRepository.kt index 96513fc..4ee3096 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/domain/AuthRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/domain/AuthRepository.kt @@ -52,5 +52,8 @@ class AuthRepository( suspend fun logout() { tokenStore.clear() _currentUser.value = null + onLogout?.invoke() } + + var onLogout: (() -> Unit)? = null } diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/domain/MemoRepository.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/domain/MemoRepository.kt index 09598f9..3411bc3 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/domain/MemoRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/domain/MemoRepository.kt @@ -127,7 +127,12 @@ class MemoRepository( is ApiResult.NetworkError -> { pendingSyncDao?.insert(PendingSyncEntity( memoId = null, action = "CREATE", - payload = """{"content":"${content.replace("\"", "\\\"")}","visibility":"${visibility.toApiString()}"}""", + payload = kotlinx.serialization.json.Json.encodeToString( + kotlinx.serialization.json.JsonObject(mapOf( + "content" to kotlinx.serialization.json.JsonPrimitive(content), + "visibility" to kotlinx.serialization.json.JsonPrimitive(visibility.toApiString()), + )) + ), createdAt = nowMillis(), )) result @@ -155,14 +160,14 @@ class MemoRepository( } is ApiResult.Error -> result is ApiResult.NetworkError -> { - val payloadParts = buildList { - if (content != null) add(""""content":"${content.replace("\"", "\\\"")}"""") - if (visibility != null) add(""""visibility":"${visibility.toApiString()}"""") - if (pinned != null) add(""""pinned":$pinned""") + val fields = buildMap { + if (content != null) put("content", kotlinx.serialization.json.JsonPrimitive(content)) + if (visibility != null) put("visibility", kotlinx.serialization.json.JsonPrimitive(visibility.toApiString())) + if (pinned != null) put("pinned", kotlinx.serialization.json.JsonPrimitive(pinned)) } pendingSyncDao?.insert(PendingSyncEntity( memoId = id, action = "UPDATE", - payload = "{${payloadParts.joinToString(",")}}", + payload = kotlinx.serialization.json.Json.encodeToString(kotlinx.serialization.json.JsonObject(fields)), createdAt = nowMillis(), )) result diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/components/MemoCard.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/components/MemoCard.kt index eb5b95d..040f9df 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/components/MemoCard.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/components/MemoCard.kt @@ -1,3 +1,4 @@ +@file:Suppress("DEPRECATION") package com.avinal.memos.ui.components import androidx.compose.animation.animateContentSize @@ -236,7 +237,7 @@ private fun InlineEditor( confirmButton = { TextButton(onClick = { dateState.selectedDateMillis?.let { ms -> - val d = kotlinx.datetime.Instant.fromEpochMilliseconds(ms) + val d = Instant.fromEpochMilliseconds(ms) .toLocalDateTime(kotlinx.datetime.TimeZone.UTC).date onContentChange(content.trimEnd() + " $d") } diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoListScreen.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoListScreen.kt index a1e02cc..3b4416b 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoListScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoListScreen.kt @@ -375,7 +375,7 @@ fun MemoListScreen( confirmButton = { TextButton(onClick = { dateState.selectedDateMillis?.let { ms -> - val d = kotlinx.datetime.Instant.fromEpochMilliseconds(ms) + val d = kotlin.time.Instant.fromEpochMilliseconds(ms) .toLocalDateTime(kotlinx.datetime.TimeZone.UTC).date val r = composeField.text.trimEnd() + " $d"; composeField = TextFieldValue(r, TextRange(r.length)) } diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/tasks/TaskDetailSheet.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/tasks/TaskDetailSheet.kt index 554f684..127d572 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/tasks/TaskDetailSheet.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/tasks/TaskDetailSheet.kt @@ -41,7 +41,7 @@ import com.avinal.memos.domain.ReminderUnit import com.avinal.memos.domain.Task import com.avinal.memos.parser.TaskParser import com.avinal.memos.ui.theme.LocalAccentColor -import kotlinx.datetime.Instant +import kotlin.time.Instant import kotlinx.datetime.LocalTime import kotlinx.datetime.TimeZone import kotlinx.datetime.atStartOfDayIn