* Enhance Cloud TTS with navigation controls and shared UI overlay in desktop app * Refactor Cloud TTS voice settings and remove standalone settings overlay on desktop app * Persist reader window state and improve slider interaction in desktop app * Improve PDF page transitions and refine focus management in desktop app * Refactor scrollbar interaction and adjust desktop modal focus handling * Improve PDF sidecar synchronization and cross-platform metadata compatibility * Refactor PDF annotation comment logic to shared module and implement Desktop UI * Refactor reader screen to use tap-to-toggle and full-width styling in desktop app * Refactor reader workspace layout and chrome-panel interactions * Implement global search keyboard shortcuts and focusable chrome layers * Implement flavor-specific legal links and update the About UI * Refactor reader UI controls on desktop app * Enhance desktop folder sync with background metadata extraction and improved error handling * Refactor Library UI and remove redundant Home tab in desktop app * Add custom tooltips to reader icon buttons in desktop app * Integrate app theme controls into reader interfaces on desktop * Add right-to-left pagination support and improve focus restoration on desktop app * Improve EPUB pagination geometry and diagnostic logging for layout cutoffs on desktop app * Update desktop reader defaults and implement settings migration * Implement block-based position tracking in ReaderLocator * Enhance EPUB highlighting reliability in desktop app * Add support for custom reader themes and update highlight palette logic in desktop app * Replace the Tools panel with a "More" dropdown menu and refactor account UI * Implement account profile header in desktop sidebar * Implement cloud sync reliability improvements and sidebar toggle on desktop app * Improve EPUB annotation synchronization and highlight mapping accuracy in desktop app * Integrate WebView2 for EPUB vertical rendering on Windows * Refactor reader layout logic and enhance WebView2 diagnostics * Improve vertical reading layout and WebView2 resizing on Desktop * Refine vertical reading mode layout and margin handling * Enhance reader locator precision and Desktop mode-switching reliability * Implement chapter-level caching and warm-start pagination in desktop app * Replace bundled KCEF with native system webviews via SWT * Refactor EPUB page info bar visibility and layout logic * Improve PDF toolbar persistence and fix tab reactivation logic * Enable multi-selection and bulk operations for custom fonts * Refactor instrumentation tests * Add EPUB UI test fixture and initial instrumentation tests * Expand EpubReader UI tests and improve accessibility * Add instrumentation tests and test tags for library and reader screens * Enhance OPDS parser logic and catalog integration * Add support for toggling local synchronization on a per-folder basis. * Implement tri-state sizing for the TTS overlay * Persist TTS overlay size across sessions * Refactor reader brightness control and add incremental step buttons * Improve CSS support, pagination control, and style-aware semantic caching * Improve link handling, interaction, and diagnostics in the paginated reader * crash fixes * Implement persistent pending removal for external files * Implement book-specific word replacements * Add native vertical reading mode with custom renderer * Implement text selection and navigation improvements for the native vertical reader * Implement locator-based navigation and improved vertical scrolling in native vertical mode in epub * Implement lazy loading and chapter prefetching for native vertical reader * Improve window lifecycle and disposal handling on Desktop * Optimize vertical reading performance in desktop app * Enhance TTS start accuracy and diagnostic logging on desktop * Refactor AI settings visibility on desktop * Improve pagination height measurement and enhance cutoff diagnostics * Implement lifecycle management and improve justified text splitting for pagination * Refine AI usage tracking and force AI feature visibility on Desktop * Add descriptive context comments and usage examples to string and plural resources. * Optimize performance and memory usage in search and state mapping * Replace reader page sliders with minimal slider and navigation controls * Add support for CBT comic archives * Harden file path validation and XML parsing to prevent security vulnerabilities * Implement local account profile caching and optimize desktop performance * Improve desktop persistence reliability and add Linux secure storage support * Improved PDF zoom stability and layout prediction during zoom commits * Improved PDF spread layout prediction, reader focus restoration, and account profile caching * Enhance highlight precision and scoping using block-local offsets and CFIs * Enhance cloud book content synchronization and background downloads * Implement granular timestamp tracking for reading positions and PDF annotations * Restrict diagnostic logging and stack traces to debug builds * Refine PDF page gaps and reader chrome interaction logic * Refactor PDF highlight rendering and overhaul Desktop sidebar UI * Implement a new interaction dock and undo/redo history for PDF annotations in desktop * Enhance PDF color picker and improve navigation scroll restoration * Add highlight palette customization and improve selection menu UI in desktop app epub reader * Enhance desktop shelf management and library organization
869 lines
33 KiB
Kotlin
869 lines
33 KiB
Kotlin
import org.gradle.api.GradleException
|
|
import org.gradle.api.DefaultTask
|
|
import org.gradle.api.file.RegularFileProperty
|
|
import org.gradle.api.provider.ListProperty
|
|
import org.gradle.api.provider.MapProperty
|
|
import org.gradle.api.provider.Property
|
|
import org.gradle.api.tasks.JavaExec
|
|
import org.gradle.api.tasks.Input
|
|
import org.gradle.api.tasks.OutputFile
|
|
import org.gradle.api.tasks.PathSensitivity
|
|
import org.gradle.api.tasks.Sync
|
|
import org.gradle.api.tasks.TaskAction
|
|
import org.gradle.jvm.tasks.Jar
|
|
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
|
import org.gradle.work.DisableCachingByDefault
|
|
import java.io.File
|
|
import java.nio.file.AtomicMoveNotSupportedException
|
|
import java.nio.file.Files
|
|
import java.nio.file.StandardCopyOption
|
|
import java.util.Properties
|
|
import java.util.zip.ZipEntry
|
|
import java.util.zip.ZipFile
|
|
import java.util.zip.ZipOutputStream
|
|
|
|
plugins {
|
|
alias(libs.plugins.kotlin.multiplatform)
|
|
alias(libs.plugins.kotlin.compose)
|
|
alias(libs.plugins.compose.multiplatform)
|
|
}
|
|
|
|
@DisableCachingByDefault(because = "Verification task has no outputs.")
|
|
abstract class CheckBundledPdfiumRuntimeTask : DefaultTask() {
|
|
@get:Input
|
|
abstract val bundleRootPath: Property<String>
|
|
|
|
@get:Input
|
|
abstract val libraryPath: Property<String>
|
|
|
|
@TaskAction
|
|
fun checkRuntime() {
|
|
val bundleRoot = File(bundleRootPath.get())
|
|
val library = bundleRoot.resolve(libraryPath.get())
|
|
if (!library.isFile) {
|
|
throw GradleException(
|
|
"Missing bundled Pdfium runtime at ${library.absolutePath}. " +
|
|
"Expected ${libraryPath.get()} inside ${bundleRoot.absolutePath}."
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@DisableCachingByDefault(because = "Renames package output produced by jpackage.")
|
|
abstract class RenameDesktopMsiOutputTask : DefaultTask() {
|
|
@get:Input
|
|
abstract val msiDirectoryPath: Property<String>
|
|
|
|
@get:Input
|
|
abstract val packageName: Property<String>
|
|
|
|
@get:Input
|
|
abstract val packageVersion: Property<String>
|
|
|
|
@get:Input
|
|
abstract val architecture: Property<String>
|
|
|
|
@TaskAction
|
|
fun renameOutput() {
|
|
val msiDirectory = File(msiDirectoryPath.get())
|
|
val outputPackageName = packageName.get()
|
|
val outputPackageVersion = packageVersion.get()
|
|
val source = msiDirectory.resolve("$outputPackageName-$outputPackageVersion.msi")
|
|
if (!source.isFile) return
|
|
|
|
val target = msiDirectory.resolve("$outputPackageName-$outputPackageVersion-${architecture.get()}.msi")
|
|
if (target.exists() && !target.delete()) {
|
|
throw GradleException("Could not replace existing MSI at ${target.absolutePath}.")
|
|
}
|
|
if (!source.renameTo(target)) {
|
|
throw GradleException("Could not rename MSI from ${source.absolutePath} to ${target.absolutePath}.")
|
|
}
|
|
}
|
|
}
|
|
|
|
@DisableCachingByDefault(because = "Generates local desktop service config for native packages.")
|
|
abstract class GenerateDesktopCloudConfigTask : DefaultTask() {
|
|
@get:Input
|
|
abstract val configValues: MapProperty<String, String>
|
|
|
|
@get:OutputFile
|
|
abstract val outputFile: RegularFileProperty
|
|
|
|
@TaskAction
|
|
fun generate() {
|
|
val file = outputFile.get().asFile
|
|
file.parentFile.mkdirs()
|
|
file.writeText(
|
|
configValues.get().entries.joinToString(separator = "\n", postfix = "\n") { (key, value) ->
|
|
"$key=${value.replace("\\", "\\\\").replace("\n", "")}"
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
@DisableCachingByDefault(because = "Verification task has no outputs.")
|
|
abstract class VerifyDesktopNativePackagingTask : DefaultTask() {
|
|
@get:Input
|
|
abstract val supportedHost: Property<Boolean>
|
|
|
|
@get:Input
|
|
abstract val hostOsId: Property<String>
|
|
|
|
@get:Input
|
|
abstract val hostArchId: Property<String>
|
|
|
|
@get:Input
|
|
abstract val missingStandardServiceConfig: ListProperty<String>
|
|
|
|
@TaskAction
|
|
fun verify() {
|
|
if (!supportedHost.get()) {
|
|
throw GradleException(
|
|
"Desktop native packaging is currently release-supported only on Windows x64 and Linux x64. " +
|
|
"Current host: ${hostOsId.get()} ${hostArchId.get()}."
|
|
)
|
|
}
|
|
val missing = missingStandardServiceConfig.get()
|
|
if (missing.isNotEmpty()) {
|
|
throw GradleException(
|
|
"Standard desktop packages require account/sync service config. Missing: " +
|
|
missing.joinToString(", ") + ". " +
|
|
"Set DESKTOP_FIREBASE_WEB_API_KEY and DESKTOP_GOOGLE_OAUTH_CLIENT_ID, " +
|
|
"use -PdesktopFlavor=oss for the offline build, or set " +
|
|
"-PdesktopAllowUnconfiguredStandardServices=true for a local non-GA package."
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@DisableCachingByDefault(because = "Strips stale jar signatures in-place after ProGuard rewrites signed dependencies.")
|
|
abstract class StripInvalidJarSignaturesTask : DefaultTask() {
|
|
@get:Input
|
|
abstract val jarDirectoryPath: Property<String>
|
|
|
|
@TaskAction
|
|
fun stripSignatures() {
|
|
val jarDirectory = File(jarDirectoryPath.get())
|
|
if (!jarDirectory.isDirectory) return
|
|
|
|
var strippedJarCount = 0
|
|
jarDirectory.walkTopDown()
|
|
.filter { it.isFile && it.extension.equals("jar", ignoreCase = true) }
|
|
.forEach { jar ->
|
|
val strippedEntries = stripInvalidJarSignatures(jar)
|
|
if (strippedEntries > 0) {
|
|
strippedJarCount += 1
|
|
logger.lifecycle("Stripped $strippedEntries stale jar signature entr${if (strippedEntries == 1) "y" else "ies"} from ${jar.name}")
|
|
}
|
|
}
|
|
|
|
if (strippedJarCount > 0) {
|
|
logger.lifecycle("Stripped stale jar signatures from $strippedJarCount ProGuard output jar${if (strippedJarCount == 1) "" else "s"}.")
|
|
}
|
|
}
|
|
|
|
private fun stripInvalidJarSignatures(jar: File): Int {
|
|
val temp = Files.createTempFile(jar.parentFile.toPath(), "${jar.nameWithoutExtension}-unsigned-", ".jar")
|
|
var strippedEntries = 0
|
|
|
|
ZipFile(jar).use { source ->
|
|
ZipOutputStream(Files.newOutputStream(temp)).use { target ->
|
|
val seenEntries = mutableSetOf<String>()
|
|
val entries = source.entries()
|
|
while (entries.hasMoreElements()) {
|
|
val sourceEntry = entries.nextElement()
|
|
val entryName = sourceEntry.name
|
|
if (!seenEntries.add(entryName)) continue
|
|
if (isJarSignatureResource(entryName)) {
|
|
strippedEntries += 1
|
|
continue
|
|
}
|
|
|
|
val targetEntry = ZipEntry(entryName)
|
|
if (sourceEntry.time >= 0) {
|
|
targetEntry.time = sourceEntry.time
|
|
}
|
|
target.putNextEntry(targetEntry)
|
|
if (!sourceEntry.isDirectory) {
|
|
source.getInputStream(sourceEntry).use { input ->
|
|
input.copyTo(target)
|
|
}
|
|
}
|
|
target.closeEntry()
|
|
}
|
|
}
|
|
}
|
|
|
|
if (strippedEntries > 0) {
|
|
try {
|
|
Files.move(temp, jar.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE)
|
|
} catch (_: AtomicMoveNotSupportedException) {
|
|
Files.move(temp, jar.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
|
}
|
|
} else {
|
|
Files.deleteIfExists(temp)
|
|
}
|
|
|
|
return strippedEntries
|
|
}
|
|
|
|
private fun isJarSignatureResource(entryName: String): Boolean {
|
|
val normalized = entryName.replace('\\', '/').uppercase()
|
|
if (!normalized.startsWith("META-INF/")) return false
|
|
|
|
val metaInfName = normalized.removePrefix("META-INF/")
|
|
if (metaInfName.contains("/")) return false
|
|
|
|
return metaInfName.startsWith("SIG-") ||
|
|
metaInfName.endsWith(".SF") ||
|
|
metaInfName.endsWith(".DSA") ||
|
|
metaInfName.endsWith(".RSA") ||
|
|
metaInfName.endsWith(".EC")
|
|
}
|
|
}
|
|
|
|
fun desktopOsId(osName: String = System.getProperty("os.name")): String {
|
|
val normalized = osName.lowercase()
|
|
return when {
|
|
normalized.startsWith("windows") -> "windows"
|
|
normalized == "linux" || normalized.contains("linux") -> "linux"
|
|
normalized.startsWith("mac") || normalized.contains("darwin") -> "macos"
|
|
else -> "other"
|
|
}
|
|
}
|
|
|
|
fun desktopArchId(osArch: String = System.getProperty("os.arch")): String {
|
|
return when (osArch.lowercase()) {
|
|
"amd64", "x86_64", "x64" -> "x64"
|
|
"aarch64", "arm64" -> "arm64"
|
|
"x86", "i386", "i686" -> "x86"
|
|
else -> "unknown"
|
|
}
|
|
}
|
|
|
|
fun desktopSwtArtifactId(
|
|
osName: String = System.getProperty("os.name"),
|
|
osArch: String = System.getProperty("os.arch")
|
|
): String? {
|
|
return when (desktopOsId(osName)) {
|
|
"windows" -> when (desktopArchId(osArch)) {
|
|
"arm64" -> "org.eclipse.swt.win32.win32.aarch64"
|
|
else -> "org.eclipse.swt.win32.win32.x86_64"
|
|
}
|
|
|
|
"linux" -> when (desktopArchId(osArch)) {
|
|
"arm64" -> "org.eclipse.swt.gtk.linux.aarch64"
|
|
else -> "org.eclipse.swt.gtk.linux.x86_64"
|
|
}
|
|
|
|
"macos" -> when (desktopArchId(osArch)) {
|
|
"arm64" -> "org.eclipse.swt.cocoa.macosx.aarch64"
|
|
else -> "org.eclipse.swt.cocoa.macosx.x86_64"
|
|
}
|
|
|
|
else -> null
|
|
}
|
|
}
|
|
|
|
fun desktopPdfiumDirectoryName(
|
|
osName: String = System.getProperty("os.name"),
|
|
osArch: String = System.getProperty("os.arch")
|
|
): String {
|
|
return when (desktopOsId(osName)) {
|
|
"windows" -> "win-${desktopArchId(osArch)}-v8"
|
|
"linux" -> "linux-${desktopArchId(osArch)}-v8"
|
|
"macos" -> "mac-${desktopArchId(osArch)}-v8"
|
|
else -> "${desktopArchId(osArch)}-v8"
|
|
}
|
|
}
|
|
|
|
fun desktopPdfiumLibraryPath(
|
|
osName: String = System.getProperty("os.name"),
|
|
osArch: String = System.getProperty("os.arch")
|
|
): String {
|
|
return when (desktopOsId(osName)) {
|
|
"windows" -> "bin/pdfium.dll"
|
|
"linux" -> "lib/libpdfium.so"
|
|
"macos" -> "lib/libpdfium.dylib"
|
|
else -> "lib/pdfium"
|
|
}
|
|
}
|
|
|
|
fun desktopJdkToolName(toolName: String, osName: String = System.getProperty("os.name")): String {
|
|
return if (desktopOsId(osName) == "windows") "$toolName.exe" else toolName
|
|
}
|
|
|
|
fun File.asDesktopJdkHome(): File {
|
|
val absolute = absoluteFile
|
|
return when {
|
|
absolute.isFile && absolute.parentFile?.name?.equals("bin", ignoreCase = true) == true ->
|
|
absolute.parentFile?.parentFile ?: absolute
|
|
|
|
absolute.isDirectory && absolute.name.equals("bin", ignoreCase = true) ->
|
|
absolute.parentFile ?: absolute
|
|
|
|
absolute.resolve("Contents/Home/bin").isDirectory ->
|
|
absolute.resolve("Contents/Home")
|
|
|
|
else -> absolute
|
|
}
|
|
}
|
|
|
|
fun File.desktopJdkTool(toolName: String, osName: String = System.getProperty("os.name")): File {
|
|
return resolve("bin/${desktopJdkToolName(toolName, osName)}")
|
|
}
|
|
|
|
fun File.isDesktopPackagingJdk(osName: String = System.getProperty("os.name")): Boolean {
|
|
return desktopJdkTool("java", osName).isFile &&
|
|
desktopJdkTool("jlink", osName).isFile &&
|
|
desktopJdkTool("jpackage", osName).isFile
|
|
}
|
|
|
|
fun File.desktopJdkMajorVersion(): Int? {
|
|
val releaseFile = resolve("release")
|
|
if (!releaseFile.isFile) return null
|
|
|
|
val version = runCatching {
|
|
releaseFile.useLines { lines ->
|
|
lines.firstOrNull { it.startsWith("JAVA_VERSION=") }
|
|
?.substringAfter("=")
|
|
?.trim()
|
|
?.trim('"')
|
|
}
|
|
}.getOrNull() ?: return null
|
|
|
|
return version.removePrefix("1.").substringBefore(".").toIntOrNull()
|
|
}
|
|
|
|
fun safeChildDirectories(root: File): List<File> {
|
|
return runCatching {
|
|
root.listFiles()?.filter { it.isDirectory }.orEmpty()
|
|
}.getOrDefault(emptyList())
|
|
}
|
|
|
|
fun stableDesktopJdkCandidates(candidates: List<File>, preferredMajorVersion: Int = 21): List<File> {
|
|
return candidates
|
|
.map { it.asDesktopJdkHome() }
|
|
.distinctBy { runCatching { it.canonicalPath }.getOrElse { _ -> it.absolutePath }.lowercase() }
|
|
.sortedWith(
|
|
compareBy<File> { if (it.desktopJdkMajorVersion() == preferredMajorVersion) 0 else 1 }
|
|
.thenBy { it.desktopJdkMajorVersion() ?: Int.MAX_VALUE }
|
|
.thenBy { it.absolutePath.lowercase() }
|
|
)
|
|
}
|
|
|
|
fun desktopPathJdkCandidates(osName: String = System.getProperty("os.name")): List<File> {
|
|
return System.getenv("PATH")
|
|
?.split(File.pathSeparator)
|
|
.orEmpty()
|
|
.asSequence()
|
|
.map { it.trim() }
|
|
.filter { it.isNotEmpty() }
|
|
.map { File(it).resolve(desktopJdkToolName("jpackage", osName)) }
|
|
.filter { it.isFile }
|
|
.mapNotNull { it.parentFile?.parentFile }
|
|
.toList()
|
|
}
|
|
|
|
fun desktopGradleJdkCandidates(): List<File> {
|
|
val userHome = System.getProperty("user.home")?.let(::File) ?: return emptyList()
|
|
return stableDesktopJdkCandidates(safeChildDirectories(userHome.resolve(".gradle/jdks")))
|
|
}
|
|
|
|
fun desktopPlatformJdkCandidates(osName: String = System.getProperty("os.name")): List<File> {
|
|
val roots = when (desktopOsId(osName)) {
|
|
"windows" -> listOfNotNull(
|
|
System.getenv("ProgramFiles")?.let { File(it, "Java") },
|
|
System.getenv("ProgramFiles")?.let { File(it, "Eclipse Adoptium") },
|
|
System.getenv("ProgramFiles")?.let { File(it, "Microsoft") },
|
|
System.getenv("ProgramFiles(x86)")?.let { File(it, "Java") }
|
|
)
|
|
|
|
"linux" -> listOf(
|
|
File("/usr/lib/jvm"),
|
|
File("/usr/java"),
|
|
File("/opt/java"),
|
|
File("/opt/jdk")
|
|
)
|
|
|
|
"macos" -> listOfNotNull(
|
|
File("/Library/Java/JavaVirtualMachines"),
|
|
System.getProperty("user.home")?.let { File(it, "Library/Java/JavaVirtualMachines") }
|
|
)
|
|
|
|
else -> emptyList()
|
|
}
|
|
|
|
return stableDesktopJdkCandidates(roots + roots.flatMap(::safeChildDirectories))
|
|
}
|
|
|
|
fun findDesktopPackagingJavaHome(
|
|
explicitCandidates: List<String>,
|
|
implicitCandidates: List<File>,
|
|
osName: String = System.getProperty("os.name")
|
|
): File? {
|
|
val explicitJavaHome = explicitCandidates.firstOrNull { it.isNotBlank() }
|
|
if (explicitJavaHome != null) {
|
|
val candidate = File(explicitJavaHome).asDesktopJdkHome()
|
|
if (!candidate.isDesktopPackagingJdk(osName)) {
|
|
throw GradleException(
|
|
"Desktop packaging JDK must include java, jlink, and jpackage under " +
|
|
"${candidate.resolve("bin").absolutePath}. " +
|
|
"Set -PdesktopPackagingJavaHome=<jdk-home> or DESKTOP_PACKAGING_JAVA_HOME to a full JDK."
|
|
)
|
|
}
|
|
return candidate
|
|
}
|
|
|
|
return implicitCandidates
|
|
.map { it.asDesktopJdkHome() }
|
|
.distinctBy { runCatching { it.canonicalPath }.getOrElse { _ -> it.absolutePath }.lowercase() }
|
|
.firstOrNull { it.isDesktopPackagingJdk(osName) }
|
|
}
|
|
|
|
fun normalizeDesktopPackageVersion(rawVersion: String): String {
|
|
val coreVersion = rawVersion.trim()
|
|
.substringBefore("-")
|
|
.substringBefore("+")
|
|
.takeIf { it.isNotBlank() }
|
|
?: "1.0.0"
|
|
val parts = coreVersion.split(".")
|
|
val numericParts = parts.map { it.toIntOrNull() }
|
|
val normalizedParts = when {
|
|
parts.size in 1..3 && numericParts.all { it != null } ->
|
|
numericParts.map { it ?: 0 } + List(3 - parts.size) { 0 }
|
|
|
|
else -> throw GradleException(
|
|
"desktopPackageVersion must be numeric MAJOR[.MINOR[.BUILD]], but was '$rawVersion'."
|
|
)
|
|
}
|
|
val (major, minor, build) = normalizedParts
|
|
if (major !in 0..255 || minor !in 0..255 || build !in 0..65535) {
|
|
throw GradleException(
|
|
"desktopPackageVersion '$rawVersion' is outside the Windows package version range. " +
|
|
"Expected MAJOR 0..255, MINOR 0..255, BUILD 0..65535."
|
|
)
|
|
}
|
|
return "$major.$minor.$build"
|
|
}
|
|
|
|
fun normalizeDesktopVersionName(rawVersion: String): String {
|
|
return rawVersion.trim().takeIf { it.isNotBlank() } ?: "1.0.0"
|
|
}
|
|
|
|
fun normalizeDesktopFlavor(rawFlavor: String): String {
|
|
val flavor = rawFlavor.trim().lowercase()
|
|
return when (flavor) {
|
|
"oss", "oss-offline", "episteme-oss" -> "oss-offline"
|
|
else -> "standard"
|
|
}
|
|
}
|
|
|
|
fun normalizeDesktopPackageArchitecture(osArch: String): String {
|
|
val normalizedArch = desktopArchId(osArch)
|
|
return if (normalizedArch != "unknown") {
|
|
normalizedArch
|
|
} else {
|
|
osArch.lowercase()
|
|
.replace(Regex("[^a-z0-9]+"), "-")
|
|
.trim('-')
|
|
.ifBlank { "unknown" }
|
|
}
|
|
}
|
|
|
|
fun desktopDefaultPackageFormats(osName: String = System.getProperty("os.name")): String {
|
|
return when (desktopOsId(osName)) {
|
|
"windows" -> "msi"
|
|
"linux" -> "deb,rpm"
|
|
"macos" -> "dmg"
|
|
else -> ""
|
|
}
|
|
}
|
|
|
|
fun desktopTargetFormatForId(format: String): TargetFormat {
|
|
return when (format.lowercase()) {
|
|
"exe" -> TargetFormat.Exe
|
|
"msi" -> TargetFormat.Msi
|
|
"deb" -> TargetFormat.Deb
|
|
"rpm" -> TargetFormat.Rpm
|
|
"dmg" -> TargetFormat.Dmg
|
|
"pkg" -> TargetFormat.Pkg
|
|
else -> throw GradleException(
|
|
"Unsupported desktopPackageFormats entry '$format'. " +
|
|
"Use one or more of: msi, exe, deb, rpm, dmg, pkg."
|
|
)
|
|
}
|
|
}
|
|
|
|
fun desktopPackageFormatId(format: TargetFormat): String {
|
|
return when (format) {
|
|
TargetFormat.Exe -> "exe"
|
|
TargetFormat.Msi -> "msi"
|
|
TargetFormat.Deb -> "deb"
|
|
TargetFormat.Rpm -> "rpm"
|
|
TargetFormat.Dmg -> "dmg"
|
|
TargetFormat.Pkg -> "pkg"
|
|
else -> format.name.lowercase()
|
|
}
|
|
}
|
|
|
|
fun desktopPackageFormatSupportedOnHost(
|
|
format: TargetFormat,
|
|
osName: String = System.getProperty("os.name")
|
|
): Boolean {
|
|
return when (desktopOsId(osName)) {
|
|
"windows" -> format == TargetFormat.Msi || format == TargetFormat.Exe
|
|
"linux" -> format == TargetFormat.Deb || format == TargetFormat.Rpm
|
|
"macos" -> format == TargetFormat.Dmg || format == TargetFormat.Pkg
|
|
else -> false
|
|
}
|
|
}
|
|
|
|
fun normalizeDesktopPackageFormats(
|
|
rawFormats: String,
|
|
osName: String = System.getProperty("os.name")
|
|
): List<TargetFormat> {
|
|
val formats = rawFormats
|
|
.split(',', ';', ' ', '\n', '\t')
|
|
.map { it.trim() }
|
|
.filter { it.isNotBlank() }
|
|
.map(::desktopTargetFormatForId)
|
|
.distinct()
|
|
if (formats.isEmpty()) {
|
|
throw GradleException(
|
|
"desktopPackageFormats resolved to no package formats for ${desktopOsId(osName)}. " +
|
|
"Set -PdesktopPackageFormats=msi on Windows or -PdesktopPackageFormats=deb,rpm on Linux."
|
|
)
|
|
}
|
|
val unsupported = formats.filterNot { desktopPackageFormatSupportedOnHost(it, osName) }
|
|
if (unsupported.isNotEmpty()) {
|
|
throw GradleException(
|
|
"desktopPackageFormats=${formats.joinToString(",") { desktopPackageFormatId(it) }} does not match " +
|
|
"the current packaging host ${desktopOsId(osName)}. Unsupported here: " +
|
|
unsupported.joinToString(",") { desktopPackageFormatId(it) } + "."
|
|
)
|
|
}
|
|
return formats
|
|
}
|
|
|
|
val desktopVersionName = "1.0.1"
|
|
val desktopFlavor = providers.gradleProperty("desktopFlavor")
|
|
.orElse("standard")
|
|
.map(::normalizeDesktopFlavor)
|
|
.get()
|
|
val isOssOfflineDesktop = desktopFlavor == "oss-offline"
|
|
val desktopDiagnostics = providers.gradleProperty("desktopDiagnostics")
|
|
.map { it.equals("true", ignoreCase = true) }
|
|
.orElse(false)
|
|
val desktopDiagnosticTags = providers.gradleProperty("desktopDiagnosticTags")
|
|
.orElse("")
|
|
val desktopResolvedVersionName = providers.gradleProperty("desktopVersionName")
|
|
.orElse(providers.gradleProperty("desktopVersion"))
|
|
.orElse(desktopVersionName)
|
|
.map(::normalizeDesktopVersionName)
|
|
val desktopPackageVersion = providers.gradleProperty("desktopPackageVersion")
|
|
.orElse(desktopResolvedVersionName)
|
|
.map(::normalizeDesktopPackageVersion)
|
|
val desktopPackageName = if (isOssOfflineDesktop) "Episteme oss" else "Episteme"
|
|
val desktopPackageDescription = if (isOssOfflineDesktop) {
|
|
"Episteme oss offline desktop reader"
|
|
} else {
|
|
"Episteme desktop reader"
|
|
}
|
|
val desktopVendor = providers.gradleProperty("desktopVendor").orElse("Aryan")
|
|
val desktopOsName = System.getProperty("os.name")
|
|
val desktopOsArch = System.getProperty("os.arch")
|
|
val desktopPackageArchitecture = normalizeDesktopPackageArchitecture(desktopOsArch)
|
|
val desktopPackageTargetFormats = providers.gradleProperty("desktopPackageFormats")
|
|
.orElse(desktopDefaultPackageFormats(desktopOsName))
|
|
.map { normalizeDesktopPackageFormats(it, desktopOsName) }
|
|
.get()
|
|
val desktopNativePackageSupportedHost = desktopOsId(desktopOsName) in setOf("windows", "linux") &&
|
|
desktopArchId(desktopOsArch) == "x64"
|
|
val desktopReleaseProguardEnabled = providers.gradleProperty("desktopReleaseProguard")
|
|
.map { it.equals("true", ignoreCase = true) }
|
|
.orElse(false)
|
|
.get()
|
|
val desktopSwtVersion = "3.133.0"
|
|
val desktopSwtDependency = desktopSwtArtifactId(desktopOsName, desktopOsArch)
|
|
?.let { artifactId -> "org.eclipse.platform:$artifactId:$desktopSwtVersion" }
|
|
val generatedDesktopResourcesDir = layout.buildDirectory.dir("generated/desktopAppResources")
|
|
val generatedDesktopCloudConfigFile = layout.buildDirectory.file("generated/desktopCloudConfig/desktop-cloud.properties")
|
|
val generatedDesktopStringResourcesDir = layout.buildDirectory.dir("generated/desktopStringResources")
|
|
val rootLocalProperties = Properties()
|
|
val rootLocalPropertiesFile = rootProject.file("local.properties")
|
|
if (rootLocalPropertiesFile.exists()) {
|
|
rootLocalPropertiesFile.inputStream().use(rootLocalProperties::load)
|
|
}
|
|
fun desktopConfigValue(vararg keys: String): String {
|
|
return keys.firstNotNullOfOrNull { key ->
|
|
providers.gradleProperty(key).orNull
|
|
?: rootLocalProperties.getProperty(key)
|
|
?: System.getenv(key)
|
|
}?.trim().orEmpty()
|
|
}
|
|
val desktopCloudConfig = mapOf(
|
|
"AI_WORKER_URL" to desktopConfigValue("DESKTOP_AI_WORKER_URL", "AI_WORKER_URL"),
|
|
"TTS_WORKER_URL" to desktopConfigValue("DESKTOP_TTS_WORKER_URL", "TTS_WORKER_URL"),
|
|
"FIREBASE_WEB_API_KEY" to desktopConfigValue("DESKTOP_FIREBASE_WEB_API_KEY", "FIREBASE_WEB_API_KEY", "GOOGLE_API_KEY"),
|
|
"FIREBASE_PROJECT_ID" to desktopConfigValue("DESKTOP_FIREBASE_PROJECT_ID", "FIREBASE_PROJECT_ID").ifBlank { "reader-9fc469d7" },
|
|
"GOOGLE_OAUTH_CLIENT_ID" to desktopConfigValue("DESKTOP_GOOGLE_OAUTH_CLIENT_ID", "GOOGLE_OAUTH_CLIENT_ID", "GOOGLE_WEB_CLIENT_ID", "DEFAULT_WEB_CLIENT_ID"),
|
|
"GOOGLE_OAUTH_CLIENT_SECRET" to desktopConfigValue("DESKTOP_GOOGLE_OAUTH_CLIENT_SECRET", "GOOGLE_OAUTH_CLIENT_SECRET", "GOOGLE_WEB_CLIENT_SECRET", "DEFAULT_WEB_CLIENT_SECRET")
|
|
)
|
|
val desktopAllowUnconfiguredStandardServices = providers.gradleProperty("desktopAllowUnconfiguredStandardServices")
|
|
.map { it.equals("true", ignoreCase = true) }
|
|
.orElse(false)
|
|
.get()
|
|
val desktopMissingStandardServiceConfig = if (isOssOfflineDesktop || desktopAllowUnconfiguredStandardServices) {
|
|
emptyList()
|
|
} else {
|
|
listOf("FIREBASE_WEB_API_KEY", "GOOGLE_OAUTH_CLIENT_ID")
|
|
.filter { key -> desktopCloudConfig[key].isNullOrBlank() }
|
|
}
|
|
val bundledPdfiumDir = layout.projectDirectory.dir(
|
|
"../third_party/pdfium/${desktopPdfiumDirectoryName(desktopOsName, desktopOsArch)}"
|
|
)
|
|
val bundledPdfiumLibraryPath = desktopPdfiumLibraryPath(desktopOsName, desktopOsArch)
|
|
val desktopWindowsIconFile = layout.projectDirectory.file("src/desktopMain/resources/episteme.ico")
|
|
val desktopLinuxIconFile = layout.projectDirectory.file("src/desktopMain/resources/episteme_icon.png")
|
|
val desktopWindowsUpgradeUuid = if (isOssOfflineDesktop) {
|
|
"ca13b201-940a-420a-8a3f-16e7d83d12a8"
|
|
} else {
|
|
"c04c5823-b25a-4f38-a1cf-0da7b02ac397"
|
|
}
|
|
val desktopPackagingJavaHome = findDesktopPackagingJavaHome(
|
|
explicitCandidates = listOfNotNull(
|
|
providers.gradleProperty("desktopPackagingJavaHome").orNull,
|
|
providers.environmentVariable("DESKTOP_PACKAGING_JAVA_HOME").orNull,
|
|
providers.environmentVariable("JPACKAGE_HOME").orNull
|
|
),
|
|
implicitCandidates = buildList {
|
|
addAll(desktopGradleJdkCandidates())
|
|
providers.gradleProperty("org.gradle.java.home").orNull?.let { add(File(it)) }
|
|
providers.environmentVariable("GRADLE_LOCAL_JAVA_HOME").orNull?.let { add(File(it)) }
|
|
providers.environmentVariable("JAVA_HOME").orNull?.let { add(File(it)) }
|
|
providers.environmentVariable("JDK_HOME").orNull?.let { add(File(it)) }
|
|
add(File(System.getProperty("java.home")))
|
|
addAll(desktopPathJdkCandidates(desktopOsName))
|
|
addAll(desktopPlatformJdkCandidates(desktopOsName))
|
|
},
|
|
osName = desktopOsName
|
|
)?.absolutePath
|
|
|
|
val checkBundledPdfiumRuntime by tasks.registering(CheckBundledPdfiumRuntimeTask::class) {
|
|
bundleRootPath.set(bundledPdfiumDir.asFile.absolutePath)
|
|
libraryPath.set(bundledPdfiumLibraryPath)
|
|
}
|
|
|
|
val generateDesktopCloudConfig by tasks.registering(GenerateDesktopCloudConfigTask::class) {
|
|
configValues.set(desktopCloudConfig)
|
|
outputFile.set(generatedDesktopCloudConfigFile)
|
|
}
|
|
|
|
val prepareBundledDesktopResources by tasks.registering(Sync::class) {
|
|
dependsOn(checkBundledPdfiumRuntime, generateDesktopCloudConfig)
|
|
from(bundledPdfiumDir) {
|
|
into("common/third_party/pdfium/${desktopPdfiumDirectoryName(desktopOsName, desktopOsArch)}")
|
|
}
|
|
into("common") {
|
|
from(generatedDesktopCloudConfigFile)
|
|
}
|
|
into(generatedDesktopResourcesDir)
|
|
}
|
|
|
|
val prepareDesktopStringResources by tasks.registering(Sync::class) {
|
|
// Reuse Android string resources as the localization source for desktop.
|
|
from(rootProject.layout.projectDirectory.dir("app/src/main/res")) {
|
|
include("values*/strings.xml")
|
|
include("values*/plurals.xml")
|
|
into("desktop-android-res")
|
|
}
|
|
into(generatedDesktopStringResourcesDir)
|
|
}
|
|
|
|
val verifyDesktopNativePackaging by tasks.registering(VerifyDesktopNativePackagingTask::class) {
|
|
supportedHost.set(desktopNativePackageSupportedHost)
|
|
hostOsId.set(desktopOsId(desktopOsName))
|
|
hostArchId.set(desktopArchId(desktopOsArch))
|
|
missingStandardServiceConfig.set(desktopMissingStandardServiceConfig)
|
|
}
|
|
|
|
kotlin {
|
|
jvm("desktop")
|
|
jvmToolchain(21)
|
|
|
|
sourceSets {
|
|
val desktopMain by getting {
|
|
resources.srcDir(prepareDesktopStringResources)
|
|
dependencies {
|
|
implementation(project(":shared"))
|
|
implementation(compose.desktop.currentOs)
|
|
implementation(compose.material3)
|
|
desktopSwtDependency?.let { dependency ->
|
|
compileOnly(dependency)
|
|
runtimeOnly(dependency)
|
|
}
|
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.8.1")
|
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
|
|
implementation("net.java.dev.jna:jna:5.17.0")
|
|
implementation("org.apache.commons:commons-compress:1.28.0")
|
|
implementation("org.tukaani:xz:1.10")
|
|
implementation("com.twelvemonkeys.imageio:imageio-webp:3.13.1")
|
|
}
|
|
}
|
|
val desktopTest by getting {
|
|
dependencies {
|
|
implementation(kotlin("test"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
compose.desktop {
|
|
application {
|
|
mainClass = "com.aryan.reader.desktop.LauncherKt"
|
|
desktopPackagingJavaHome?.let { javaHome = it }
|
|
|
|
jvmArgs("--add-opens", "java.desktop/sun.awt=ALL-UNNAMED")
|
|
jvmArgs("--add-opens", "java.desktop/java.awt.peer=ALL-UNNAMED")
|
|
jvmArgs("-Depisteme.desktop.flavor=$desktopFlavor")
|
|
jvmArgs("-Depisteme.desktop.diagnostics=${desktopDiagnostics.get()}")
|
|
jvmArgs("-Depisteme.desktop.diagnostics.tags=${desktopDiagnosticTags.get()}")
|
|
jvmArgs("-Depisteme.desktop.version=${desktopResolvedVersionName.get()}")
|
|
|
|
buildTypes.release.proguard {
|
|
// ProGuard still rewrites and shrinks release jars even when optimization and
|
|
// obfuscation are disabled. That has produced invalid stack-map frames in large
|
|
// Compose/PDF lambdas and stripped WebView bridge behavior in packaged MSIs.
|
|
isEnabled.set(desktopReleaseProguardEnabled)
|
|
obfuscate.set(false)
|
|
// Compose/Kotlin generated methods can produce very large stack-map frames.
|
|
// ProGuard optimization has emitted invalid frames for SharedAppTheme in release builds.
|
|
optimize.set(false)
|
|
configurationFiles.from(project.file("compose-desktop.pro"))
|
|
}
|
|
|
|
nativeDistributions {
|
|
targetFormats(*desktopPackageTargetFormats.toTypedArray())
|
|
modules(
|
|
"java.datatransfer",
|
|
"java.desktop",
|
|
"java.logging",
|
|
"java.management",
|
|
"java.net.http",
|
|
"jdk.charsets",
|
|
"jdk.httpserver",
|
|
"jdk.unsupported"
|
|
)
|
|
packageName = desktopPackageName
|
|
packageVersion = desktopPackageVersion.get()
|
|
description = desktopPackageDescription
|
|
vendor = desktopVendor.get()
|
|
appResourcesRootDir.set(generatedDesktopResourcesDir)
|
|
windows {
|
|
iconFile.set(desktopWindowsIconFile)
|
|
dirChooser = true
|
|
shortcut = true
|
|
menu = true
|
|
menuGroup = "Episteme"
|
|
perUserInstall = true
|
|
upgradeUuid = desktopWindowsUpgradeUuid
|
|
}
|
|
linux {
|
|
iconFile.set(desktopLinuxIconFile)
|
|
packageName = if (isOssOfflineDesktop) "episteme-oss" else "episteme"
|
|
debMaintainer = "epistemereader@gmail.com"
|
|
menuGroup = "Office"
|
|
appCategory = "Office"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
tasks.withType<JavaExec>().configureEach {
|
|
jvmArgs("--add-opens", "java.desktop/sun.awt=ALL-UNNAMED")
|
|
jvmArgs("--add-opens", "java.desktop/java.awt.peer=ALL-UNNAMED")
|
|
jvmArgs("-Depisteme.desktop.flavor=$desktopFlavor")
|
|
jvmArgs("-Depisteme.desktop.diagnostics=${desktopDiagnostics.get()}")
|
|
jvmArgs("-Depisteme.desktop.diagnostics.tags=${desktopDiagnosticTags.get()}")
|
|
jvmArgs("-Depisteme.desktop.version=${desktopResolvedVersionName.get()}")
|
|
if (System.getProperty("os.name").contains("Mac")) {
|
|
jvmArgs("--add-opens", "java.desktop/sun.lwawt=ALL-UNNAMED")
|
|
jvmArgs("--add-opens", "java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
|
}
|
|
}
|
|
|
|
tasks.withType<Jar>().configureEach {
|
|
manifest {
|
|
attributes(
|
|
"Implementation-Title" to desktopPackageName,
|
|
"Implementation-Version" to desktopResolvedVersionName.get(),
|
|
"Implementation-Vendor" to desktopVendor.get()
|
|
)
|
|
}
|
|
}
|
|
|
|
val stripReleaseProguardJarSignatures = if (desktopReleaseProguardEnabled) {
|
|
tasks.registering(StripInvalidJarSignaturesTask::class) {
|
|
dependsOn("proguardReleaseJars")
|
|
jarDirectoryPath.set(layout.buildDirectory.dir("compose/tmp/main-release/proguard").map { it.asFile.absolutePath })
|
|
}
|
|
} else {
|
|
null
|
|
}
|
|
|
|
tasks.matching {
|
|
it.name in setOf(
|
|
"createReleaseDistributable",
|
|
"packageReleaseDistributionForCurrentOS",
|
|
"packageReleaseExe",
|
|
"packageReleaseMsi",
|
|
"packageReleaseDeb",
|
|
"packageReleaseRpm",
|
|
"runReleaseDistributable"
|
|
)
|
|
}.configureEach {
|
|
stripReleaseProguardJarSignatures?.let { dependsOn(it) }
|
|
}
|
|
|
|
mapOf(
|
|
"packageMsi" to "main",
|
|
"packageReleaseMsi" to "main-release"
|
|
).forEach { (taskName, distributionName) ->
|
|
val renameTask = tasks.register<RenameDesktopMsiOutputTask>("rename${taskName.replaceFirstChar(Char::titlecase)}Output") {
|
|
msiDirectoryPath.set(layout.buildDirectory.dir("compose/binaries/$distributionName/msi").get().asFile.absolutePath)
|
|
packageName.set(desktopPackageName)
|
|
packageVersion.set(desktopPackageVersion.get())
|
|
architecture.set(desktopPackageArchitecture)
|
|
}
|
|
tasks.matching { it.name == taskName }.configureEach {
|
|
finalizedBy(renameTask)
|
|
}
|
|
}
|
|
|
|
tasks.matching {
|
|
it.name in setOf(
|
|
"createDistributable",
|
|
"createReleaseDistributable",
|
|
"prepareAppResources",
|
|
"prepareReleaseAppResources",
|
|
"packageDistributionForCurrentOS",
|
|
"packageReleaseDistributionForCurrentOS",
|
|
"packageExe",
|
|
"packageReleaseExe",
|
|
"packageMsi",
|
|
"packageReleaseMsi",
|
|
"packageDeb",
|
|
"packageReleaseDeb",
|
|
"packageRpm",
|
|
"packageReleaseRpm",
|
|
"runDistributable",
|
|
"runReleaseDistributable"
|
|
)
|
|
}.configureEach {
|
|
dependsOn(verifyDesktopNativePackaging)
|
|
dependsOn(prepareBundledDesktopResources)
|
|
inputs.dir(generatedDesktopResourcesDir)
|
|
.withPropertyName("bundledDesktopResources")
|
|
.withPathSensitivity(PathSensitivity.RELATIVE)
|
|
}
|