Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
- DI uses Metro (`AppGraph`, `@Provides`); access via `LocalAppGraph` (avoid singletons).

## Localization & Resources
- Never hardcode user-visible text; add keys to `SeforimApp/src/commonMain/composeResources/strings.xml`.
- Never hardcode user-visible text; add keys to `appkit/src/commonMain/composeResources/strings.xml`.
- The required locale for user-facing text is Hebrew (`he`); provide Hebrew strings for all new keys.

## Testing Guidelines
Expand Down
17 changes: 15 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,21 @@ see `SeforimLibrary/JVM_RUNTIME.md`. The view models reach the
desktop shell only through the ports of `SearchHost.kt` (`SearchTabs`, `SearchSessionStore`, `SearchSettings`,
`SearchHistory`); keep `java.*`, AWT and desktop types out of `searchkit/src/commonMain`.

### The app without its shell: `appkit`
Everything else that doesn't need the desktop is the KMP module `appkit` (JVM, Android 10+, iOS), packages unchanged:
the reader (`BookContentViewModel`, its state, use cases, paging), `TabsViewModel`, the session (`SessionManager`), the
user's data (stores, `UserSettingsDb` schema), the settings and onboarding view models, the catalog, the Compose
resources (`appkit/src/commonMain/composeResources`, `Res` still in `seforimapp.seforimapp.generated.resources`) and
`FontCatalog`. `earthwidget` and `pagination` are multiplatform too. It reaches the desktop shell only through ports,
which `SeforimApp` implements: `TabsHost` (`DesktopTabsHost`), `TabsResolver` (`DesktopManager`), `SessionRestoreState`
/`SessionShell` (`SessionManager`, `DesktopSessionShell`). `AppKitBindings` (contributed to `AppScope`) provides the
common bindings; each app's graph provides `AppDatabases` (its SQLite connections, `expect`/`actual`) and its ports.
`DesktopManager`, the windows and the Jewel screens stay in `SeforimApp`. Keep `java.*`, AWT, Jewel and Nucleus out of
`appkit/src/commonMain`.

The verse quote links of the Talmud (temporary, until the DB carries them) follow the same pattern: the private
`io.github.kdroidfilter:seforim-quotelinks` package (repo `SeforimQuoteLinks`, read with the siddur's token by default,
or `quotelinks.token`/`SEFORIM_QUOTELINKS_TOKEN`/`gh`, or `-Pquotelinks.local=true` to include `../SeforimQuoteLinks/jvm`) and `addons/quotelinks/enabled`, which `LinesPagingSource` calls on each page it loads, in
or `quotelinks.token`/`SEFORIM_QUOTELINKS_TOKEN`/`gh`, or `-Pquotelinks.local=true` to include `../SeforimQuoteLinks/jvm`) and `appkit/addons/quotelinks/enabled` (appkit's `jvmMain`; null on mobile), which `LinesPagingSource` calls on each page it loads, in
the same trip off the UI thread; `disabled` makes `installedQuoteLinks` null. Core code reaches it only through
`QuoteLinksFeature`/`installedQuoteLinks`; only the tractates in the package pay anything (a set lookup otherwise).

Expand Down Expand Up @@ -170,6 +182,7 @@ See `TAB_SYSTEM_README.md` for complete details. Key points:
- `texteffects`: Text rendering effects and styling
- `network`: Network utilities and Ktor client setup
- `searchkit`: the search without its UI, multiplatform (see "Search on mobile")
- `appkit`: the app without its desktop shell, multiplatform (see "The app without its shell")
- `backup`: Backup core, Android-BackupAgent/Transport style: `BackupSource` (what: the app's `UserDataBackup`, user DB + portable prefs), `BackupDestination` (where: `LocalFileDestination`, …), `BackupManager` (the only code linking both)
- `backup-drive`: Google Drive `BackupDestination` + `GoogleDriveSync` (OAuth PKCE loopback, hidden `appDataFolder`, append-only with the 10 latest kept as IntelliJ settings sync, never restores without asking, on-quit + daily auto backup paused while a restore is pending)

Expand Down Expand Up @@ -281,7 +294,7 @@ class MyViewModel(

- **Main Entry**: `SeforimApp/src/jvmMain/kotlin/main.kt`
- **Features**: `SeforimApp/src/jvmMain/kotlin/io/github/kdroidfilter/seforimapp/features/`
- **Resources**: `SeforimApp/src/commonMain/composeResources/`
- **Resources**: `appkit/src/commonMain/composeResources/`
- **Desktop Assets**: `SeforimApp/src/jvmMain/assets/`
- **Tab System**: See `TAB_SYSTEM_README.md`
- **Version Catalog**: `gradle/libs.versions.toml`
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ import io.github.kdroidfilter.seforimapp.features.home.widgets.luach.moladInfo
import io.github.kdroidfilter.seforimapp.features.home.widgets.luach.rememberOpenInLibrary
import io.github.kdroidfilter.seforimapp.features.home.widgets.rememberAccentColor
import io.github.kdroidfilter.seforimapp.features.onboarding.userprofile.Community
import io.github.kdroidfilter.seforimapp.features.zmanim.data.timeZone
import io.github.kdroidfilter.seforimapp.framework.desktop.LocalOpenWindow
import io.github.kdroidfilter.seforimapp.framework.di.LocalAppGraph
import io.github.kdroidfilter.seforimapp.framework.session.SiddurPersistedState
Expand Down
23 changes: 6 additions & 17 deletions SeforimApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ plugins {
alias(libs.plugins.buildConfig)
alias(libs.plugins.metro)
alias(libs.plugins.stability.analyzer)
alias(libs.plugins.sqlDelight)
alias(libs.plugins.kover)
alias(libs.plugins.nucleus)
alias(libs.plugins.structured.coroutines)
Expand All @@ -43,8 +42,7 @@ sentry {
// (settings.gradle.kts); a community build has none and addons/siddur/disabled says so
val withSiddur = gradle.extensions.extraProperties["siddurEnabled"] == true

// The verse quote links of the Talmud (open core, temporary until the DB carries them): its package, or
// addons/quotelinks/disabled and the reader shows the lines as they are
// The verse quote links of the Talmud (open core, temporary until the DB carries them): appkit compiles its addon
val withQuoteLinks = gradle.extensions.extraProperties["quoteLinksEnabled"] == true

kotlin {
Expand Down Expand Up @@ -141,10 +139,7 @@ kotlin {
}
if (withSiddur) jvmTest { kotlin.srcDir("addons/siddur/test/kotlin") }

jvmMain {
kotlin.srcDir(if (withQuoteLinks) "addons/quotelinks/enabled/kotlin" else "addons/quotelinks/disabled/kotlin")
if (withQuoteLinks) dependencies { implementation(libs.seforim.quotelinks) }
}
// The quote links' addon itself is appkit's (its jvmMain); its test reads the real DB with the desktop's driver
if (withQuoteLinks) jvmTest { kotlin.srcDir("addons/quotelinks/test/kotlin") }

jvmTest.dependencies {
Expand Down Expand Up @@ -181,7 +176,7 @@ kotlin {
implementation(libs.slf4j.simple)
implementation(libs.split.pane.desktop)
implementation(libs.sqlite.driver)
implementation(libs.zstd.jni)
implementation(libs.okio)
implementation(libs.ktor.client.okhttp)
implementation(libs.lucene.core)
implementation(libs.reorderable)
Expand All @@ -194,6 +189,9 @@ kotlin {
// semantic search (open core) comes with it in the official builds
implementation(project(":searchkit"))

// The app without its desktop shell (reader, user data, settings, onboarding), shared with the mobile apps
implementation(project(":appkit"))

// SeforimLibrary CLI: lets the desktop binary run headless search commands
// (`zayit cli search ...`) by delegating to its runCli() entry point.
implementation(libs.seforimlibrary.cli)
Expand Down Expand Up @@ -320,15 +318,6 @@ nucleus.application {
}
}

sqldelight {
databases {
create("UserSettingsDb") {
packageName.set("io.github.kdroidfilter.seforimapp.db")
dialect("app.cash.sqldelight:sqlite-3-24-dialect:${libs.versions.sqlDelight.get()}")
}
}
}

tasks.withType<ComposeHotRun>().configureEach {
mainClass.set("io.github.kdroidfilter.seforimapp.MainKt")
}
Expand Down
31 changes: 31 additions & 0 deletions SeforimApp/src/graalvm/reachability-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -1343,6 +1343,34 @@
},
{
"type": "sun.util.resources.cldr.CalendarData"
},
{
"type": "com.squareup.zstd.ZstdCompressor",
"jniAccessible": true,
"fields": [
{
"name": "outputBytesProcessed",
"jniAccessible": true
},
{
"name": "inputBytesProcessed",
"jniAccessible": true
}
]
},
{
"type": "com.squareup.zstd.ZstdDecompressor",
"jniAccessible": true,
"fields": [
{
"name": "outputBytesProcessed",
"jniAccessible": true
},
{
"name": "inputBytesProcessed",
"jniAccessible": true
}
]
}
],
"foreign": {
Expand Down Expand Up @@ -2934,6 +2962,9 @@
},
{
"glob": "io/github/kdroidfilter/seforim/quotelinks/**"
},
{
"glob": "jni/*/*zstd-kmp*"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.github.kdroidfilter.seforimapp.core.e2e

import com.russhwolf.settings.PreferencesSettings
import io.github.kdroidfilter.seforimapp.core.settings.AppSettings
import io.github.kdroidfilter.seforimapp.features.onboarding.extract.ExtractUseCase
import okio.Path.Companion.toOkioPath
import java.io.File
import java.security.MessageDigest
import java.util.HexFormat
import java.util.prefs.Preferences

/**
* The database bundle's extraction, as the onboarding runs it (`ZAYIT_E2E_SCENARIO=extract`, `ZAYIT_E2E_BUNDLE` naming a
* `.tar.zst` or its `.part01`): into the run's output, with throwaway settings, never the user's databases. Notes the
* extracted database's size and sha256, to compare with the one packed.
*/
object E2eExtractScenario {
private const val SETTINGS_NODE = "io/github/kdroidfilter/seforimapp/e2e-extract"

suspend fun run(sc: E2eScenario) {
if (E2e.scenario != "extract") return
val bundle = System.getenv("ZAYIT_E2E_BUNDLE") ?: error("ZAYIT_E2E_BUNDLE names no bundle")
val dbDir = File(E2e.outDir, "extracted")
val node = Preferences.userRoot().node(SETTINGS_NODE)
try {
var lastProgress = 0f
val dbPath =
ExtractUseCase(AppSettings(PreferencesSettings(node))) { dbDir.toOkioPath() }
.extractToDatabase(bundle) { lastProgress = it }
val db = File(dbPath)
sc.note("extract: $dbPath, ${db.length()} bytes, sha256 ${sha256(db)}, progress $lastProgress")
dbDir.walkTopDown().filter { it.isFile }.forEach { sc.note("extract: file ${it.relativeTo(dbDir)} ${it.length()}") }
} finally {
node.removeNode()
}
}

private fun sha256(file: File): String {
val digest = MessageDigest.getInstance("SHA-256")
file.inputStream().use { input ->
val buffer = ByteArray(1 shl 20)
while (true) {
val read = input.read(buffer)
if (read < 0) break
digest.update(buffer, 0, read)
}
}
return HexFormat.of().formatHex(digest.digest())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import androidx.compose.ui.semantics.SemanticsNode
import dev.nucleusframework.window.ExperimentalNucleusApi
import dev.nucleusframework.window.tao.TaoRootsForTest
import io.github.kdroidfilter.seforim.tabs.TabsDestination
import io.github.kdroidfilter.seforimapp.features.bookcontent.quotelinks.installedQuoteLinks
import io.github.kdroidfilter.seforimapp.core.presentation.theme.IntUiThemes
import io.github.kdroidfilter.seforimapp.features.bookcontent.quotelinks.installedQuoteLinks
import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeoutOrNull
import java.util.UUID
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.github.kdroidfilter.seforimapp.core.presentation.theme

import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import dev.nucleusframework.systemcolor.systemAccentColor
import org.jetbrains.jewel.intui.core.theme.IntUiDarkTheme
import org.jetbrains.jewel.intui.core.theme.IntUiLightTheme

/**
* Returns the static color for non-System presets.
* For [AccentColor.System], returns the Jewel default blue (use [resolveColor] in composable contexts).
*/
fun AccentColor.forMode(isDark: Boolean): Color =
when (this) {
AccentColor.System, AccentColor.Default ->
if (isDark) {
IntUiDarkTheme.colors.blueOrNull(6) ?: Color(0xFF3574F0)
} else {
IntUiLightTheme.colors.blueOrNull(4) ?: Color(0xFF4682FA)
}
AccentColor.Teal -> if (isDark) Color(0xFF2FC2B6) else Color(0xFF1A998E)
AccentColor.Green -> if (isDark) Color(0xFF5AB869) else Color(0xFF3D9A50)
AccentColor.Gold -> if (isDark) Color(0xFFD4A843) else Color(0xFFBE9117)
}

/**
* Composable-aware resolution that queries the OS accent color for [AccentColor.System].
* Falls back to [forMode] when the platform doesn't provide an accent color.
*/
@Composable
fun AccentColor.resolveColor(isDark: Boolean): Color =
when (this) {
AccentColor.System -> systemAccentColor() ?: forMode(isDark)
else -> forMode(isDark)
}

/** Resolve for display in the settings UI. */
@Composable
fun AccentColor.displayColor(isDark: Boolean): Color = resolveColor(isDark)
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import io.github.kdroidfilter.seforimapp.core.e2e.E2e
import io.github.kdroidfilter.seforimapp.core.presentation.components.rememberPillTextFieldStyle
import io.github.kdroidfilter.seforimapp.core.presentation.tabs.LocalTabSelected
import io.github.kdroidfilter.seforimapp.core.presentation.theme.AccentColor
import io.github.kdroidfilter.seforimapp.core.presentation.theme.forMode
import io.github.kdroidfilter.seforimapp.core.presentation.utils.LocalWindowViewModelStoreOwner
import io.github.kdroidfilter.seforimapp.features.bookcontent.BookContentEvent
import io.github.kdroidfilter.seforimapp.features.bookcontent.ui.panels.bookcontent.components.CatalogRow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ fun BookTocPanel(
val search = uiState.toc.search
// While searching, the matches of both trees replace them
val searching = search != null && search.query.isNotBlank()
val showAlt = uiState.navigation.selectedBook.hasAltStructures && !searching
val showAlt = uiState.navigation.selectedBook?.hasAltStructures == true && !searching
Column(modifier = Modifier.fillMaxHeight()) {
PaneSearchLayout(
search = search,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,20 +84,6 @@ import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter

/**
* A pending (not-yet-persisted) note anchored to a resolved character range. Created from the
* "add note" context-menu action, or implicitly as a blank whole-line note when the selected line
* carries none yet.
*
* @property quote The anchored passage, shown as a greyed preview above the editor.
*/
data class NoteDraftAnchor(
val lineId: Long,
val startOffset: Int,
val endOffset: Int,
val quote: String,
)

/** Compact "dd.MM.yy · HH:mm" stamp shown in each note's footer. Locale-neutral on purpose. */
private val NOTE_TIME_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.yy · HH:mm")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import io.github.kdroidfilter.seforimapp.earthwidget.KiddushLevanaLatestOpinion
import io.github.kdroidfilter.seforimapp.earthwidget.ZmanimOpinion
import io.github.kdroidfilter.seforimapp.features.onboarding.userprofile.Community
import io.github.kdroidfilter.seforimapp.features.zmanim.data.ISRAEL_COUNTRY_NAME
import io.github.kdroidfilter.seforimapp.features.zmanim.data.timeZone
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.time.Instant
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import io.github.kdroidfilter.seforimapp.core.presentation.theme.resolveColor
import io.github.kdroidfilter.seforimapp.framework.di.LocalAppGraph
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.ui.component.CircularProgressIndicator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import io.github.kdroidfilter.seforimapp.features.home.widgets.WidgetCard
import io.github.kdroidfilter.seforimapp.features.home.widgets.playMillisHere
import io.github.kdroidfilter.seforimapp.features.home.widgets.rememberAccentColor
import io.github.kdroidfilter.seforimapp.features.home.widgets.shownDateHere
import io.github.kdroidfilter.seforimapp.features.zmanim.data.timeZone
import io.github.kdroidfilter.seforimapp.features.zmanim.data.worldPlaces
import io.github.kdroidfilter.seforimapp.framework.platform.PlatformInfo
import org.jetbrains.jewel.foundation.theme.JewelTheme
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ fun ExtractView(
// Error banner with retry
if (state.errorMessage != null) {
val generic = stringResource(Res.string.onboarding_error_occurred)
val detail = state.errorMessage.takeIf { it.isNotBlank() }
val detail = state.errorMessage?.takeIf { it.isNotBlank() }
val message = detail?.let { stringResource(Res.string.onboarding_error_with_detail, it) } ?: generic
val retryLabel = stringResource(Res.string.retry_button)
DefaultErrorBanner(
Expand Down
Loading
Loading