diff --git a/build.gradle.kts b/build.gradle.kts index 125978a..1e80dcc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,14 +20,17 @@ repositories { dependencies { detektPlugins(libs.detekt.formatting) - implementation(libs.chinese.transliteration) - implementation(libs.cedict.parser) - implementation(libs.sqlite.jdbc) implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.html.jvm) + implementation(libs.segment) + + implementation(libs.ikonli.javafx) + + implementation(libs.slf4j.nop) + testImplementation(libs.kotest.core) testImplementation(libs.kotest.assertions) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c75a353..5d12827 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,22 +2,22 @@ kotlin = "2.0.20" detekt = "1.23.7" jfx-plugin = "0.1.0" -javafx = "22.0.1" +javafx = "23" kotest = "5.9.1" -cedict-parser = "1.0.1" -chinese-transliteration = "1.0.1" - sqlite-jdbc = "3.46.0.1" kotlinx-serialization-json = "1.7.1" kotlinx-html-jvm = "0.11.0" -[libraries] -chinese-transliteration = { module = "com.marvinelsen:chinese-transliteration", version.ref = "chinese-transliteration" } -cedict-parser = { module = "com.marvinelsen:cedict-parser", version.ref = "cedict-parser" } +segment = "0.3.1" +ikonli-javafx = "12.3.1" + +slf4j = "2.0.16" + +[libraries] # Kotest # See: https://kotest.io kotest-core = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" } @@ -28,6 +28,11 @@ sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite-jdbc" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } kotlinx-html-jvm = { module = "org.jetbrains.kotlinx:kotlinx-html-jvm", version.ref = "kotlinx-html-jvm" } +segment = { module = "com.github.houbb:segment", version.ref = "segment" } +slf4j-nop = { module = "org.slf4j:slf4j-nop", version.ref = "slf4j" } + +ikonli-javafx = { module = "org.kordamp.ikonli:ikonli-javafx", version.ref = "ikonli-javafx" } + # Detekt # See: https://detekt.dev detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } diff --git a/src/main/kotlin/com/marvinelsen/willow/Model.kt b/src/main/kotlin/com/marvinelsen/willow/Model.kt index 50d832c..a8e4fa2 100644 --- a/src/main/kotlin/com/marvinelsen/willow/Model.kt +++ b/src/main/kotlin/com/marvinelsen/willow/Model.kt @@ -2,9 +2,13 @@ package com.marvinelsen.willow import com.marvinelsen.willow.domain.SearchMode import com.marvinelsen.willow.ui.DictionaryEntryFx +import com.marvinelsen.willow.ui.SentenceFx +import com.marvinelsen.willow.ui.services.FindCharacterService +import com.marvinelsen.willow.ui.services.FindSentencesService +import com.marvinelsen.willow.ui.services.FindWordsBeginningService +import com.marvinelsen.willow.ui.services.FindWordsContainingService +import com.marvinelsen.willow.ui.services.SearchService import com.marvinelsen.willow.ui.util.ClipboardHelper -import com.marvinelsen.willow.ui.util.FindWordsService -import com.marvinelsen.willow.ui.util.SearchService import javafx.beans.property.ObjectProperty import javafx.beans.property.ReadOnlyBooleanProperty import javafx.beans.property.ReadOnlyObjectProperty @@ -13,27 +17,54 @@ import javafx.collections.FXCollections import javafx.collections.ObservableList import javafx.event.EventHandler -class Model(private val searchService: SearchService, private val findWordsService: FindWordsService) { +class Model( + private val searchService: SearchService, + private val findWordsBeginningService: FindWordsBeginningService, + private val findWordsContainingService: FindWordsContainingService, + private val findCharacterService: FindCharacterService, + private val findSentencesService: FindSentencesService, +) { private val internalSelectedEntry: ObjectProperty = SimpleObjectProperty() private val internalSearchResults: ObservableList = FXCollections.observableArrayList() + private val internalWordsBeginning: ObservableList = FXCollections.observableArrayList() private val internalWordsContaining: ObservableList = FXCollections.observableArrayList() + private val internalCharacters: ObservableList = FXCollections.observableArrayList() + private val internalSentences: ObservableList = FXCollections.observableArrayList() val selectedEntry: ReadOnlyObjectProperty = internalSelectedEntry val searchResults: ObservableList = FXCollections.unmodifiableObservableList(internalSearchResults) + val wordsBeginning: ObservableList = + FXCollections.unmodifiableObservableList(internalWordsBeginning) val wordsContaining: ObservableList = FXCollections.unmodifiableObservableList(internalWordsContaining) + val characters: ObservableList = + FXCollections.unmodifiableObservableList(internalCharacters) + val sentences: ObservableList = + FXCollections.unmodifiableObservableList(internalSentences) val isSearching: ReadOnlyBooleanProperty = searchService.runningProperty() - val isFindingWords: ReadOnlyBooleanProperty = findWordsService.runningProperty() + val isFindingWordsBeginning: ReadOnlyBooleanProperty = findWordsBeginningService.runningProperty() + val isFindingWordsContaining: ReadOnlyBooleanProperty = findWordsContainingService.runningProperty() + val isFindingCharacters: ReadOnlyBooleanProperty = findCharacterService.runningProperty() + val isFindingSentences: ReadOnlyBooleanProperty = findSentencesService.runningProperty() init { searchService.onSucceeded = EventHandler { internalSearchResults.setAll(searchService.value) } - findWordsService.onSucceeded = EventHandler { - internalWordsContaining.setAll(findWordsService.value) + findWordsBeginningService.onSucceeded = EventHandler { + internalWordsBeginning.setAll(findWordsBeginningService.value) + } + findWordsContainingService.onSucceeded = EventHandler { + internalWordsContaining.setAll(findWordsContainingService.value) + } + findCharacterService.onSucceeded = EventHandler { + internalCharacters.setAll(findCharacterService.value) + } + findSentencesService.onSucceeded = EventHandler { + internalSentences.setAll(findSentencesService.value) } } @@ -43,20 +74,39 @@ class Model(private val searchService: SearchService, private val findWordsServi searchService.restart() } - fun findWords() { - findWordsService.entry = internalSelectedEntry.value - findWordsService.restart() + fun findWordsBeginning() { + findWordsBeginningService.entry = internalSelectedEntry.value + findWordsBeginningService.restart() + } + + fun findWordsContaining() { + findWordsContainingService.entry = internalSelectedEntry.value + findWordsContainingService.restart() + } + + fun findCharacters() { + findCharacterService.entry = internalSelectedEntry.value + findCharacterService.restart() + } + + fun findSentences() { + findSentencesService.entry = internalSelectedEntry.value + findSentencesService.restart() } fun selectEntry(entry: DictionaryEntryFx) { + internalWordsBeginning.setAll(emptyList()) + internalWordsContaining.setAll(emptyList()) + internalCharacters.setAll(emptyList()) + internalSentences.setAll(emptyList()) internalSelectedEntry.value = entry } fun copyHeadwordOfSelectedEntry() { - ClipboardHelper.copyHeadword(internalSelectedEntry.get()) + ClipboardHelper.copyString(internalSelectedEntry.value.traditionalProperty.value) } fun copyPronunciationOfSelectedEntry() { - ClipboardHelper.copyPronunciation(internalSelectedEntry.get()) + ClipboardHelper.copyString(internalSelectedEntry.value.pinyinWithToneMarksProperty.value) } } diff --git a/src/main/kotlin/com/marvinelsen/willow/WillowApplication.kt b/src/main/kotlin/com/marvinelsen/willow/WillowApplication.kt index 4a01aa5..a9a5c18 100644 --- a/src/main/kotlin/com/marvinelsen/willow/WillowApplication.kt +++ b/src/main/kotlin/com/marvinelsen/willow/WillowApplication.kt @@ -1,21 +1,26 @@ package com.marvinelsen.willow +import com.marvinelsen.willow.config.Config import com.marvinelsen.willow.domain.SqliteDictionary import com.marvinelsen.willow.ui.controllers.DetailsController import com.marvinelsen.willow.ui.controllers.MainController import com.marvinelsen.willow.ui.controllers.MenuController import com.marvinelsen.willow.ui.controllers.SearchController -import com.marvinelsen.willow.ui.util.FindWordsService -import com.marvinelsen.willow.ui.util.SearchService +import com.marvinelsen.willow.ui.controllers.SearchResultsController +import com.marvinelsen.willow.ui.services.FindCharacterService +import com.marvinelsen.willow.ui.services.FindSentencesService +import com.marvinelsen.willow.ui.services.FindWordsBeginningService +import com.marvinelsen.willow.ui.services.FindWordsContainingService +import com.marvinelsen.willow.ui.services.SearchService import javafx.application.Application import javafx.fxml.FXMLLoader import javafx.scene.Scene +import javafx.scene.image.Image import javafx.scene.layout.BorderPane import javafx.scene.text.Font import javafx.stage.Stage import javafx.util.Callback import java.sql.DriverManager -import java.util.Locale import java.util.ResourceBundle class WillowApplication : Application() { @@ -28,7 +33,7 @@ class WillowApplication : Application() { private const val FONT_SIZE = 12.0 - private const val JDBC_CONNECTION_STRING = "jdbc:sqlite:dictionary.db" + private const val JDBC_CONNECTION_STRING = "jdbc:sqlite::resource:data/dictionary.db" } override fun init() { @@ -41,17 +46,29 @@ class WillowApplication : Application() { } val dictionary = SqliteDictionary(connection) val searchService = SearchService(dictionary) - val findWordsService = FindWordsService(dictionary) - val model = Model(searchService, findWordsService) + val findWordsBeginningService = FindWordsBeginningService(dictionary) + val findWordsContainingService = FindWordsContainingService(dictionary) + val findCharacterService = FindCharacterService(dictionary) + val findSentenceService = FindSentencesService(dictionary) + val model = Model( + searchService, + findWordsBeginningService, + findWordsContainingService, + findCharacterService, + findSentenceService + ) + val config = Config() + config.load() val fxmlLoader = FXMLLoader() - fxmlLoader.resources = ResourceBundle.getBundle("i18n/willow", Locale.US) + fxmlLoader.resources = ResourceBundle.getBundle("i18n/willow", config.locale.value) fxmlLoader.controllerFactory = Callback { type -> when (type) { MainController::class.java -> MainController(model) - MenuController::class.java -> MenuController(model) - DetailsController::class.java -> DetailsController(model) + MenuController::class.java -> MenuController(model, config) + DetailsController::class.java -> DetailsController(model, config) SearchController::class.java -> SearchController(model) + SearchResultsController::class.java -> SearchResultsController(model, config) else -> error("Trying to instantiate unknown controller type $type") } } @@ -59,19 +76,22 @@ class WillowApplication : Application() { val root = fxmlLoader.load(javaClass.getResourceAsStream("/fxml/main.fxml")) as BorderPane val primaryScene = Scene(root, WINDOW_WIDTH, WINDOW_HEIGHT) + // primaryScene.stylesheets.add(javaClass.getResource("/css/dark.css")?.toExternalForm()!!) primaryStage.apply { title = WINDOW_TITLE minWidth = WINDOW_MIN_WIDTH minHeight = WINDOW_MIN_HEIGHT scene = primaryScene + icons.add(Image(javaClass.getResourceAsStream("/img/icon.png"))) }.show() } private fun loadFonts() { Font.loadFont(javaClass.getResourceAsStream("/fonts/inter.ttf"), FONT_SIZE) Font.loadFont(javaClass.getResourceAsStream("/fonts/tw-kai.ttf"), FONT_SIZE) - Font.loadFont(javaClass.getResourceAsStream("/fonts/noto-sans-tc.ttf"), FONT_SIZE) + Font.loadFont(javaClass.getResourceAsStream("/fonts/noto-sans-tc-regular.ttf"), FONT_SIZE) + Font.loadFont(javaClass.getResourceAsStream("/fonts/noto-sans-tc-bold.ttf"), FONT_SIZE) } } diff --git a/src/main/kotlin/com/marvinelsen/willow/cedict/CreateDatabase.kt b/src/main/kotlin/com/marvinelsen/willow/cedict/CreateDatabase.kt deleted file mode 100644 index 5b61c09..0000000 --- a/src/main/kotlin/com/marvinelsen/willow/cedict/CreateDatabase.kt +++ /dev/null @@ -1,93 +0,0 @@ -package com.marvinelsen.willow.cedict - -import com.marvinelsen.cedict.api.CedictParser -import com.marvinelsen.chinese.transliteration.TransliterationSystem -import com.marvinelsen.chinese.transliteration.Zhuyin -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.builtins.serializer -import kotlinx.serialization.json.Json -import java.sql.DriverManager -import java.util.zip.GZIPInputStream - -const val JDBC_CONNECTION_STRING = "jdbc:sqlite:dictionary.db" - -@Suppress("MagicNumber", "LongMethod", "MaximumLineLength", "MaxLineLength") -fun main() { - val connection = DriverManager.getConnection(JDBC_CONNECTION_STRING).apply { - autoCommit = false - } - - val statement = connection.createStatement() - statement.executeUpdate( - """ - CREATE TABLE IF NOT EXISTS cedict( - id INTEGER PRIMARY KEY, - traditional TEXT NOT NULL, - simplified TEXT NOT NULL, - pinyin_with_tone_marks TEXT NOT NULL, - pinyin_with_tone_numbers TEXT NOT NULL, - zhuyin TEXT NOT NULL, - definitions JSON NOT NULL, - character_count INTEGER NOT NULL, - CONSTRAINT character_count_gte CHECK(character_count > 0) - ); - """.trimIndent() - ) - statement.executeUpdate("CREATE INDEX IF NOT EXISTS idx_cedict_traditional ON cedict (traditional)") - statement.executeUpdate("CREATE INDEX IF NOT EXISTS idx_cedict_simplified ON cedict (simplified)") - statement.executeUpdate("CREATE INDEX IF NOT EXISTS idx_cedict_character_count ON cedict (character_count)") - - val cedictParser = CedictParser.instance - val cedictEntries = - cedictParser.parseCedict( - GZIPInputStream(object {}.javaClass.getResourceAsStream("/data/cedict_1_0_ts_utf-8_mdbg.txt.gz")!!) - ) - - val insertStatement = - connection.prepareStatement( - "INSERT OR IGNORE INTO cedict(traditional, simplified, pinyin_with_tone_marks, pinyin_with_tone_numbers, zhuyin, definitions, character_count) VALUES(?,?,?,?,?,?,?)" - ) - for (entry in cedictEntries) { - try { - insertStatement.setString(1, entry.traditional) - insertStatement.setString(2, entry.simplified) - insertStatement.setString( - 3, - entry.pinyinSyllables.joinToString( - separator = " " - ) { it.format(TransliterationSystem.PINYIN_WITH_TONE_MARKS) } - ) - insertStatement.setString( - 4, - entry.pinyinSyllables.joinToString( - separator = " " - ) { it.format(TransliterationSystem.PINYIN_WITH_TONE_NUMBERS) } - ) - insertStatement.setString( - 5, - entry.pinyinSyllables.joinToString( - separator = Zhuyin.SEPARATOR - ) { it.format(TransliterationSystem.ZHUYIN) } - ) - insertStatement.setString( - 6, - Json.encodeToString( - ListSerializer(ListSerializer(String.serializer())), - entry.definitions.map { it.glosses } - ) - ) - insertStatement.setInt(7, entry.traditional.length) - } catch (_: Exception) { - // no-op - } - - insertStatement.addBatch() - } - - insertStatement.executeBatch() - connection.commit() - - insertStatement.close() - statement.close() - connection.close() -} diff --git a/src/main/kotlin/com/marvinelsen/willow/config/Config.kt b/src/main/kotlin/com/marvinelsen/willow/config/Config.kt new file mode 100644 index 0000000..1a3b423 --- /dev/null +++ b/src/main/kotlin/com/marvinelsen/willow/config/Config.kt @@ -0,0 +1,166 @@ +package com.marvinelsen.willow.config + +import javafx.beans.property.BooleanProperty +import javafx.beans.property.IntegerProperty +import javafx.beans.property.ObjectProperty +import javafx.beans.property.SimpleBooleanProperty +import javafx.beans.property.SimpleIntegerProperty +import javafx.beans.property.SimpleObjectProperty +import java.util.Locale +import java.util.prefs.Preferences + +class Config { + companion object { + private const val LOCALE_KEY = "locale" + private const val THEME_KEY = "theme" + private const val SCRIPT_KEY = "script" + + private val DEFAULT_THEME = Theme.SYSTEM + private val DEFAULT_SCRIPT = Script.SIMPLIFIED + private val DEFAULT_LOCALE = Locale.ENGLISH + } + + private val preferences = Preferences.userNodeForPackage(this::class.java) + val searchResults = SearchResultsConfig(preferences) + val details = DetailsConfig(preferences) + + val locale: ObjectProperty = SimpleObjectProperty(DEFAULT_LOCALE) + val theme: ObjectProperty = SimpleObjectProperty(DEFAULT_THEME) + val script: ObjectProperty