mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-10 06:42:05 +01:00
Many Many updates and fixes!
Update to Tachidesk 0.3.7 Update to compose build198 State persistence for the sources menu! Update setup scripts for using a specific version of Tachidesk Backup support Context menu now half works Server service fixes Chapter context menu to mark as read and other features Start screen customization Extension update and obsolete support
This commit is contained in:
@@ -8,7 +8,7 @@ plugins {
|
||||
kotlin("jvm") version "1.4.32"
|
||||
kotlin("kapt") version "1.4.32"
|
||||
kotlin("plugin.serialization") version "1.4.32"
|
||||
id("org.jetbrains.compose") version "0.4.0-build185"
|
||||
id("org.jetbrains.compose") version "0.4.0-build198"
|
||||
id("de.fuerstenau.buildconfig") version "1.1.8"
|
||||
id("org.jmailen.kotlinter") version "3.4.0"
|
||||
}
|
||||
@@ -27,8 +27,8 @@ dependencies {
|
||||
// UI (Compose)
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation("br.com.devsrsouza.compose.icons.jetbrains:font-awesome:0.2.0")
|
||||
implementation("com.github.Syer10:compose-router:45a8c4fe83")
|
||||
implementation("ca.gosyer:accompanist-pager:0.8.1")
|
||||
implementation("ca.gosyer:compose-router:0.24.2-jetbrains-2")
|
||||
implementation("ca.gosyer:accompanist-pager:0.9.1")
|
||||
|
||||
// UI (Swing)
|
||||
implementation("com.github.weisj:darklaf-core:2.5.5")
|
||||
@@ -76,7 +76,7 @@ dependencies {
|
||||
|
||||
tasks {
|
||||
withType<KotlinCompile> {
|
||||
dependsOn(formatKotlin)
|
||||
dependsOn(formatKotlinMain)
|
||||
kotlinOptions {
|
||||
jvmTarget = "15"
|
||||
freeCompilerArgs = listOf(
|
||||
@@ -88,7 +88,8 @@ tasks {
|
||||
"-Xopt-in=com.russhwolf.settings.ExperimentalSettingsApi",
|
||||
"-Xopt-in=com.russhwolf.settings.ExperimentalSettingsImplementation",
|
||||
"-Xopt-in=com.google.accompanist.pager.ExperimentalPagerApi",
|
||||
"-Xopt-in=androidx.compose.animation.ExperimentalAnimationApi"
|
||||
"-Xopt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||
"-Xopt-in=androidx.compose.material.ExperimentalMaterialApi"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -157,8 +158,8 @@ buildConfig {
|
||||
packageName = project.group.toString()
|
||||
|
||||
buildConfigField("boolean", "DEBUG", project.hasProperty("debugApp").toString())
|
||||
buildConfigField("String", "TACHIDESK_SP_VERSION", "0.2.7")
|
||||
buildConfigField("String", "TACHIDESK_IM_VERSION", "0")
|
||||
buildConfigField("String", "TACHIDESK_SP_VERSION", "v0.3.7")
|
||||
buildConfigField("String", "TACHIDESK_IM_VERSION", "r66")
|
||||
}
|
||||
|
||||
kotlinter {
|
||||
|
||||
@@ -7,13 +7,14 @@ fi
|
||||
mkdir -p "tmp"
|
||||
|
||||
echo "Getting latest Tachidesk build files"
|
||||
TARBALL_LINK="$(curl -s "https://api.github.com/repos/Suwayomi/Tachidesk/releases/latest" | grep -o "https.*tarball\/[a-zA-Z0-9.]*")"
|
||||
#TARBALL_LINK="$(curl -s "https://api.github.com/repos/Suwayomi/Tachidesk/releases/latest" | grep -o "https.*tarball\/[a-zA-Z0-9.]*")"
|
||||
|
||||
curl -L "$TARBALL_LINK" -o tmp/Tachidesk.tar
|
||||
#curl -L "$TARBALL_LINK" -o tmp/Tachidesk.tar
|
||||
curl -L "https://github.com/Suwayomi/Tachidesk/archive/refs/tags/v0.3.7.tar.gz" -o tmp/Tachidesk.tar.gz
|
||||
|
||||
tar -xvf tmp/Tachidesk.tar -C tmp
|
||||
tar -xvf tmp/Tachidesk.tar.gz -C tmp
|
||||
|
||||
TACHIDESK_FOLDER=$(find tmp -type d -regex ".*Tachidesk-[a-z0-9]*")
|
||||
TACHIDESK_FOLDER=$(find tmp -type d -regex ".*Tachidesk-[a-z0-9\.]*")
|
||||
|
||||
pushd "$TACHIDESK_FOLDER" || exit
|
||||
|
||||
|
||||
@@ -6,13 +6,15 @@ Remove-Item -Recurse -Force "tmp" -ErrorAction SilentlyContinue | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path "tmp"
|
||||
|
||||
Write-Output "Getting latest Tachidesk build files"
|
||||
$zipball = (Invoke-WebRequest -Uri "https://api.github.com/repos/Suwayomi/Tachidesk/releases/latest" -UseBasicParsing).content | Select-String -Pattern 'https[\.:\/A-Za-z0-9]*zipball\/[a-zA-Z0-9.]*' -CaseSensitive
|
||||
#$zipball = (Invoke-WebRequest -Uri "https://api.github.com/repos/Suwayomi/Tachidesk/releases/latest" -UseBasicParsing).content | Select-String -Pattern 'https[\.:\/A-Za-z0-9]*zipball\/[a-zA-Z0-9.]*' -CaseSensitive
|
||||
|
||||
Invoke-WebRequest -Uri $zipball.Matches.Value -OutFile tmp/Tachidesk.zip -UseBasicParsing
|
||||
#Invoke-WebRequest -Uri $zipball.Matches.Value -OutFile tmp/Tachidesk.zip -UseBasicParsing
|
||||
|
||||
Invoke-WebRequest -Uri "https://github.com/Suwayomi/Tachidesk/archive/refs/tags/v0.3.7.zip" -OutFile tmp/Tachidesk.zip -UseBasicParsing
|
||||
|
||||
Expand-Archive -Path "tmp/Tachidesk.zip" -DestinationPath "tmp"
|
||||
|
||||
$tachidesk_folder = Get-ChildItem -Path "tmp" | Where-Object {$_.Name -match ".*Tachidesk-[a-z0-9]*"} | Select-Object FullName
|
||||
$tachidesk_folder = Get-ChildItem -Path "tmp" | Where-Object {$_.Name -match ".*Tachidesk-[a-z0-9\.]*"} | Select-Object FullName
|
||||
|
||||
Push-Location $tachidesk_folder.FullName
|
||||
|
||||
@@ -27,7 +29,7 @@ $tachidesk_jar = $(Get-ChildItem "server/build" | Where-Object { $_.Name -match
|
||||
Pop-Location
|
||||
|
||||
Write-Output "Copying Tachidesk.jar to resources folder..."
|
||||
Move-Item -Force $tachidesk_jar "src/main/resources/Tachidesk.jar"
|
||||
Move-Item -Force $tachidesk_jar "src/main/resources/Tachidesk.jar" -ErrorAction SilentlyContinue
|
||||
|
||||
Write-Output "Cleaning up..."
|
||||
Remove-Item -Recurse -Force "tmp" | Out-Null
|
||||
|
||||
15
src/main/kotlin/ca/gosyer/data/models/About.kt
Normal file
15
src/main/kotlin/ca/gosyer/data/models/About.kt
Normal file
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.data.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class About(
|
||||
val version: String,
|
||||
val revision: String,
|
||||
)
|
||||
55
src/main/kotlin/ca/gosyer/data/models/Backup.kt
Normal file
55
src/main/kotlin/ca/gosyer/data/models/Backup.kt
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.data.models
|
||||
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Backup(
|
||||
val categories: List<List<@Contextual Any?>> = emptyList(),
|
||||
val mangas: List<Manga>,
|
||||
val version: Int = 1
|
||||
) {
|
||||
@Serializable
|
||||
data class Manga(
|
||||
val manga: List<@Contextual Any?>,
|
||||
val chapters: List<Chapter> = emptyList(),
|
||||
val categories: List<String> = emptyList(),
|
||||
val history: List<List<@Contextual Any?>> = emptyList(),
|
||||
val track: List<Track> = emptyList()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Chapter(
|
||||
@SerialName("u")
|
||||
val url: String,
|
||||
@SerialName("r")
|
||||
val read: Int = 0,
|
||||
@SerialName("b")
|
||||
val bookmarked: Int = 0,
|
||||
@SerialName("l")
|
||||
val lastRead: Int = 0
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Track(
|
||||
@SerialName("l")
|
||||
val lastRead: Int,
|
||||
@SerialName("ml")
|
||||
val libraryId: Int,
|
||||
@SerialName("r")
|
||||
val mediaId: Int,
|
||||
@SerialName("s")
|
||||
val syncId: Int,
|
||||
@SerialName("t")
|
||||
val title: String,
|
||||
@SerialName("u")
|
||||
val url: String
|
||||
)
|
||||
}
|
||||
@@ -6,21 +6,20 @@
|
||||
|
||||
package ca.gosyer.data.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Chapter(
|
||||
val id: Long,
|
||||
val url: String,
|
||||
val name: String,
|
||||
@SerialName("date_upload")
|
||||
val dateUpload: Long,
|
||||
@SerialName("chapter_number")
|
||||
val uploadDate: Long,
|
||||
val chapterNumber: Float,
|
||||
val scanlator: String?,
|
||||
val mangaId: Long,
|
||||
val pageCount: Int? = null,
|
||||
val chapterIndex: Int,
|
||||
val chapterCount: Int
|
||||
val read: Boolean,
|
||||
val bookmarked: Boolean,
|
||||
val lastPageRead: Int,
|
||||
val index: Int,
|
||||
val chapterCount: Int?,
|
||||
val pageCount: Int?,
|
||||
)
|
||||
|
||||
@@ -18,7 +18,8 @@ data class Extension(
|
||||
val apkName: String,
|
||||
val iconUrl: String,
|
||||
val installed: Boolean,
|
||||
val classFQName: String,
|
||||
val hasUpdate: Boolean,
|
||||
val obsolete: Boolean,
|
||||
val nsfw: Boolean
|
||||
) {
|
||||
fun iconUrl(serverUrl: String) = serverUrl + iconUrl
|
||||
|
||||
@@ -14,15 +14,16 @@ data class Manga(
|
||||
val sourceId: Long,
|
||||
val url: String,
|
||||
val title: String,
|
||||
val thumbnailUrl: String? = null,
|
||||
val initialized: Boolean = false,
|
||||
val artist: String? = null,
|
||||
val author: String? = null,
|
||||
val description: String? = null,
|
||||
val genre: String? = null,
|
||||
val thumbnailUrl: String?,
|
||||
val initialized: Boolean,
|
||||
val artist: String?,
|
||||
val author: String?,
|
||||
val description: String?,
|
||||
val genre: String?,
|
||||
val status: String,
|
||||
val inLibrary: Boolean = false,
|
||||
val source: Source?
|
||||
val inLibrary: Boolean,
|
||||
val source: Source?,
|
||||
val freshData: Boolean
|
||||
) {
|
||||
fun cover(serverUrl: String) = thumbnailUrl?.let { serverUrl + it }
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ class ServerService @Inject constructor(
|
||||
ServerResult.UNUSED
|
||||
}
|
||||
)
|
||||
private val runtime = Runtime.getRuntime()
|
||||
var process: Process? = null
|
||||
|
||||
private fun copyJar(jarFile: File) {
|
||||
@@ -45,6 +46,12 @@ class ServerService @Inject constructor(
|
||||
}
|
||||
|
||||
init {
|
||||
runtime.addShutdownHook(
|
||||
thread(start = false) {
|
||||
process?.destroy()
|
||||
process = null
|
||||
}
|
||||
)
|
||||
host.onEach {
|
||||
process?.destroy()
|
||||
initialized.value = if (host.value) {
|
||||
@@ -55,7 +62,6 @@ class ServerService @Inject constructor(
|
||||
}
|
||||
GlobalScope.launch {
|
||||
val logger = KotlinLogging.logger("Server")
|
||||
val runtime = Runtime.getRuntime()
|
||||
|
||||
val jarFile = File(userDataDir, "Tachidesk.jar")
|
||||
if (!jarFile.exists()) {
|
||||
@@ -71,12 +77,7 @@ class ServerService @Inject constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
// TODO remove 1.0 version check when we move onto 0.3.0 of Tachidesk
|
||||
if (
|
||||
jarVersion.specification != null &&
|
||||
jarVersion.implementation != null &&
|
||||
jarVersion.implementation != "1.0"
|
||||
) {
|
||||
|
||||
if (
|
||||
jarVersion.specification != BuildConfig.TACHIDESK_SP_VERSION ||
|
||||
jarVersion.implementation != BuildConfig.TACHIDESK_IM_VERSION
|
||||
@@ -84,7 +85,6 @@ class ServerService @Inject constructor(
|
||||
logger.info { "Updating server file from resources" }
|
||||
copyJar(jarFile)
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
logger.error(e) {
|
||||
"Error accessing server jar, cannot update server, ${BuildConfig.NAME} may not work properly"
|
||||
@@ -106,12 +106,6 @@ class ServerService @Inject constructor(
|
||||
process = runtime.exec("""$javaExePath -jar "${jarFile.absolutePath}"""").also {
|
||||
reader = it.inputStream.bufferedReader()
|
||||
}
|
||||
runtime.addShutdownHook(
|
||||
thread(start = false) {
|
||||
process?.destroy()
|
||||
process = null
|
||||
}
|
||||
)
|
||||
logger.info { "Server started successfully" }
|
||||
var line: String?
|
||||
while (reader.readLine().also { line = it } != null) {
|
||||
@@ -124,6 +118,9 @@ class ServerService @Inject constructor(
|
||||
}
|
||||
logger.info { line }
|
||||
}
|
||||
if (initialized.value == ServerResult.STARTING) {
|
||||
initialized.value = ServerResult.FAILED
|
||||
}
|
||||
logger.info { "Server closed" }
|
||||
val exitVal = process?.waitFor()
|
||||
logger.info { "Process exitValue: $exitVal" }
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.data.server.interactions
|
||||
|
||||
import ca.gosyer.data.models.Backup
|
||||
import ca.gosyer.data.server.Http
|
||||
import ca.gosyer.data.server.ServerPreferences
|
||||
import ca.gosyer.data.server.requests.backupExportRequest
|
||||
import ca.gosyer.data.server.requests.backupFileExportRequest
|
||||
import ca.gosyer.data.server.requests.backupFileImportRequest
|
||||
import ca.gosyer.data.server.requests.backupImportRequest
|
||||
import io.ktor.client.request.forms.formData
|
||||
import io.ktor.client.request.forms.submitFormWithBinaryData
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.Headers
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.content.MultiPartData
|
||||
import io.ktor.http.contentType
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class BackupInteractionHandler @Inject constructor(
|
||||
client: Http,
|
||||
serverPreferences: ServerPreferences
|
||||
) : BaseInteractionHandler(client, serverPreferences) {
|
||||
|
||||
suspend fun importBackupFile(file: File) = withContext(Dispatchers.IO) {
|
||||
client.submitFormWithBinaryData<HttpResponse>(
|
||||
serverUrl + backupFileImportRequest(),
|
||||
formData = formData {
|
||||
append(
|
||||
"backup.json", file.readBytes(),
|
||||
Headers.build {
|
||||
append(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||
append(HttpHeaders.ContentDisposition, "filename=backup.json")
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun importBackup(backup: Backup) = withContext(Dispatchers.IO) {
|
||||
client.postRepeat<HttpResponse>(
|
||||
serverUrl + backupImportRequest()
|
||||
) {
|
||||
contentType(ContentType.Application.Json)
|
||||
body = backup
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun exportBackupFile() = withContext(Dispatchers.IO) {
|
||||
client.getRepeat<MultiPartData>(
|
||||
serverUrl + backupFileExportRequest()
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun exportBackup() = withContext(Dispatchers.IO) {
|
||||
client.getRepeat<Backup>(
|
||||
serverUrl + backupExportRequest()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,11 @@ import ca.gosyer.data.server.ServerPreferences
|
||||
import ca.gosyer.data.server.requests.getChapterQuery
|
||||
import ca.gosyer.data.server.requests.getMangaChaptersQuery
|
||||
import ca.gosyer.data.server.requests.getPageQuery
|
||||
import ca.gosyer.data.server.requests.updateChapterRequest
|
||||
import io.ktor.client.request.parameter
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.Parameters
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
@@ -22,17 +27,19 @@ class ChapterInteractionHandler @Inject constructor(
|
||||
serverPreferences: ServerPreferences
|
||||
) : BaseInteractionHandler(client, serverPreferences) {
|
||||
|
||||
suspend fun getChapters(mangaId: Long) = withContext(Dispatchers.IO) {
|
||||
suspend fun getChapters(mangaId: Long, refresh: Boolean = false) = withContext(Dispatchers.IO) {
|
||||
client.getRepeat<List<Chapter>>(
|
||||
serverUrl + getMangaChaptersQuery(mangaId)
|
||||
)
|
||||
) {
|
||||
url {
|
||||
if (refresh) {
|
||||
parameter("onlineFetch", true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getChapters(manga: Manga) = withContext(Dispatchers.IO) {
|
||||
client.getRepeat<Chapter>(
|
||||
serverUrl + getMangaChaptersQuery(manga.id)
|
||||
)
|
||||
}
|
||||
suspend fun getChapters(manga: Manga, refresh: Boolean = false) = getChapters(manga.id, refresh)
|
||||
|
||||
suspend fun getChapter(mangaId: Long, chapterIndex: Int) = withContext(Dispatchers.IO) {
|
||||
client.getRepeat<Chapter>(
|
||||
@@ -40,11 +47,72 @@ class ChapterInteractionHandler @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getChapter(chapter: Chapter) = getChapter(chapter.mangaId, chapter.chapterIndex)
|
||||
suspend fun getChapter(chapter: Chapter) = getChapter(chapter.mangaId, chapter.index)
|
||||
|
||||
suspend fun getChapter(manga: Manga, chapterIndex: Int) = getChapter(manga.id, chapterIndex)
|
||||
|
||||
suspend fun getChapter(manga: Manga, chapter: Chapter) = getChapter(manga.id, chapter.chapterIndex)
|
||||
suspend fun getChapter(manga: Manga, chapter: Chapter) = getChapter(manga.id, chapter.index)
|
||||
|
||||
suspend fun updateChapter(
|
||||
mangaId: Long,
|
||||
chapterIndex: Int,
|
||||
read: Boolean? = null,
|
||||
bookmarked: Boolean? = null,
|
||||
lastPageRead: Int? = null,
|
||||
markPreviousRead: Boolean? = null
|
||||
) = withContext(Dispatchers.IO) {
|
||||
client.submitFormRepeat<HttpResponse>(
|
||||
serverUrl + updateChapterRequest(mangaId, chapterIndex),
|
||||
formParameters = Parameters.build {
|
||||
if (read != null) {
|
||||
append("read", read.toString())
|
||||
}
|
||||
if (bookmarked != null) {
|
||||
append("bookmarked", bookmarked.toString())
|
||||
}
|
||||
if (lastPageRead != null) {
|
||||
append("lastPageRead", lastPageRead.toString())
|
||||
}
|
||||
if (markPreviousRead != null) {
|
||||
append("markPrevRead", markPreviousRead.toString())
|
||||
}
|
||||
}
|
||||
) {
|
||||
method = HttpMethod.Patch
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateChapter(
|
||||
manga: Manga,
|
||||
chapterIndex: Int,
|
||||
read: Boolean? = null,
|
||||
bookmarked: Boolean? = null,
|
||||
lastPageRead: Int? = null,
|
||||
markPreviousRead: Boolean? = null
|
||||
) = updateChapter(
|
||||
manga.id,
|
||||
chapterIndex,
|
||||
read,
|
||||
bookmarked,
|
||||
lastPageRead,
|
||||
markPreviousRead
|
||||
)
|
||||
|
||||
suspend fun updateChapter(
|
||||
manga: Manga,
|
||||
chapter: Chapter,
|
||||
read: Boolean? = null,
|
||||
bookmarked: Boolean? = null,
|
||||
lastPageRead: Int? = null,
|
||||
markPreviousRead: Boolean? = null
|
||||
) = updateChapter(
|
||||
manga.id,
|
||||
chapter.index,
|
||||
read,
|
||||
bookmarked,
|
||||
lastPageRead,
|
||||
markPreviousRead
|
||||
)
|
||||
|
||||
suspend fun getPage(mangaId: Long, chapterIndex: Int, pageNum: Int) = withContext(Dispatchers.IO) {
|
||||
imageFromUrl(
|
||||
@@ -53,9 +121,9 @@ class ChapterInteractionHandler @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getPage(chapter: Chapter, pageNum: Int) = getPage(chapter.mangaId, chapter.chapterIndex, pageNum)
|
||||
suspend fun getPage(chapter: Chapter, pageNum: Int) = getPage(chapter.mangaId, chapter.index, pageNum)
|
||||
|
||||
suspend fun getPage(manga: Manga, chapterIndex: Int, pageNum: Int) = getPage(manga.id, chapterIndex, pageNum)
|
||||
|
||||
suspend fun getPage(manga: Manga, chapter: Chapter, pageNum: Int) = getPage(manga.id, chapter.chapterIndex, pageNum)
|
||||
suspend fun getPage(manga: Manga, chapter: Chapter, pageNum: Int) = getPage(manga.id, chapter.index, pageNum)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import ca.gosyer.data.server.ServerPreferences
|
||||
import ca.gosyer.data.server.requests.apkIconQuery
|
||||
import ca.gosyer.data.server.requests.apkInstallQuery
|
||||
import ca.gosyer.data.server.requests.apkUninstallQuery
|
||||
import ca.gosyer.data.server.requests.apkUpdateQuery
|
||||
import ca.gosyer.data.server.requests.extensionListQuery
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -31,13 +32,19 @@ class ExtensionInteractionHandler @Inject constructor(
|
||||
|
||||
suspend fun installExtension(extension: Extension) = withContext(Dispatchers.IO) {
|
||||
client.getRepeat<HttpResponse>(
|
||||
serverUrl + apkInstallQuery(extension.apkName)
|
||||
serverUrl + apkInstallQuery(extension.pkgName)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun updateExtension(extension: Extension) = withContext(Dispatchers.IO) {
|
||||
client.getRepeat<HttpResponse>(
|
||||
serverUrl + apkUpdateQuery(extension.pkgName)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun uninstallExtension(extension: Extension) = withContext(Dispatchers.IO) {
|
||||
client.getRepeat<HttpResponse>(
|
||||
serverUrl + apkUninstallQuery(extension.apkName)
|
||||
serverUrl + apkUninstallQuery(extension.pkgName)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import ca.gosyer.data.server.Http
|
||||
import ca.gosyer.data.server.ServerPreferences
|
||||
import ca.gosyer.data.server.requests.mangaQuery
|
||||
import ca.gosyer.data.server.requests.mangaThumbnailQuery
|
||||
import io.ktor.client.request.parameter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
@@ -20,11 +21,19 @@ class MangaInteractionHandler @Inject constructor(
|
||||
serverPreferences: ServerPreferences
|
||||
) : BaseInteractionHandler(client, serverPreferences) {
|
||||
|
||||
suspend fun getManga(mangaId: Long) = withContext(Dispatchers.IO) {
|
||||
suspend fun getManga(mangaId: Long, refresh: Boolean = false) = withContext(Dispatchers.IO) {
|
||||
client.getRepeat<Manga>(
|
||||
serverUrl + mangaQuery(mangaId)
|
||||
)
|
||||
) {
|
||||
url {
|
||||
if (refresh) {
|
||||
parameter("onlineFetch", true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getManga(manga: Manga, refresh: Boolean = false) = getManga(manga.id, refresh)
|
||||
|
||||
suspend fun getMangaThumbnail(mangaId: Long) = withContext(Dispatchers.IO) {
|
||||
imageFromUrl(
|
||||
|
||||
23
src/main/kotlin/ca/gosyer/data/server/requests/Backup.kt
Normal file
23
src/main/kotlin/ca/gosyer/data/server/requests/Backup.kt
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.data.server.requests
|
||||
|
||||
@Post
|
||||
fun backupImportRequest() =
|
||||
"/api/v1/backup/legacy/import"
|
||||
|
||||
@Post
|
||||
fun backupFileImportRequest() =
|
||||
"/api/v1/backup/legacy/import/file"
|
||||
|
||||
@Post
|
||||
fun backupExportRequest() =
|
||||
"/api/v1/backup/legacy/export"
|
||||
|
||||
@Post
|
||||
fun backupFileExportRequest() =
|
||||
"/api/v1/backup/legacy/export/file"
|
||||
@@ -14,6 +14,10 @@ fun getMangaChaptersQuery(mangaId: Long) =
|
||||
fun getChapterQuery(mangaId: Long, chapterIndex: Int) =
|
||||
"/api/v1/manga/$mangaId/chapter/$chapterIndex"
|
||||
|
||||
@Patch
|
||||
fun updateChapterRequest(mangaId: Long, chapterIndex: Int) =
|
||||
"/api/v1/manga/$mangaId/chapter/$chapterIndex"
|
||||
|
||||
@Get
|
||||
fun getPageQuery(mangaId: Long, chapterIndex: Int, index: Int) =
|
||||
"/api/v1/manga/$mangaId/chapter/$chapterIndex/page/$index"
|
||||
|
||||
@@ -11,12 +11,16 @@ fun extensionListQuery() =
|
||||
"/api/v1/extension/list"
|
||||
|
||||
@Get
|
||||
fun apkInstallQuery(apkName: String) =
|
||||
"/api/v1/extension/install/$apkName"
|
||||
fun apkInstallQuery(pkgName: String) =
|
||||
"/api/v1/extension/install/$pkgName"
|
||||
|
||||
@Get
|
||||
fun apkUninstallQuery(apkName: String) =
|
||||
"/api/v1/extension/uninstall/$apkName"
|
||||
fun apkUpdateQuery(pkgName: String) =
|
||||
"/api/v1/extension/update/$pkgName"
|
||||
|
||||
@Get
|
||||
fun apkUninstallQuery(pkgName: String) =
|
||||
"/api/v1/extension/uninstall/$pkgName"
|
||||
|
||||
@Get
|
||||
fun apkIconQuery(apkName: String) =
|
||||
|
||||
11
src/main/kotlin/ca/gosyer/data/server/requests/Meta.kt
Normal file
11
src/main/kotlin/ca/gosyer/data/server/requests/Meta.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.data.server.requests
|
||||
|
||||
@Get
|
||||
fun aboutQuery() =
|
||||
"/api/v1/about/"
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.base.components
|
||||
|
||||
import androidx.compose.foundation.Indication
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.forEachGesture
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
|
||||
import androidx.compose.ui.input.pointer.PointerEvent
|
||||
import androidx.compose.ui.input.pointer.changedToDown
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.debugInspectorInfo
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import java.awt.event.MouseEvent
|
||||
|
||||
private suspend fun AwaitPointerEventScope.awaitEventFirstDown(): PointerEvent {
|
||||
var event: PointerEvent
|
||||
do {
|
||||
event = awaitPointerEvent()
|
||||
} while (
|
||||
!event.changes.all { it.changedToDown() }
|
||||
)
|
||||
return event
|
||||
}
|
||||
|
||||
fun Modifier.combinedMouseClickable(
|
||||
rightClickIsContextMenu: Boolean = true,
|
||||
onClick: (IntOffset) -> Unit = {},
|
||||
onMiddleClick: (IntOffset) -> Unit = {},
|
||||
onRightClick: (IntOffset) -> Unit = {}
|
||||
) = composed(
|
||||
inspectorInfo = debugInspectorInfo {
|
||||
name = "combinedMouseClickable"
|
||||
properties["rightClickIsContextMenu"] = rightClickIsContextMenu
|
||||
properties["onClick"] = onClick
|
||||
properties["onMiddleClick"] = onMiddleClick
|
||||
properties["onRightClick"] = onRightClick
|
||||
}
|
||||
) {
|
||||
Modifier.combinedMouseClickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = LocalIndication.current,
|
||||
rightClickIsContextMenu = rightClickIsContextMenu,
|
||||
onClick = onClick,
|
||||
onMiddleClick = onMiddleClick,
|
||||
onRightClick = onRightClick
|
||||
)
|
||||
}
|
||||
|
||||
fun Modifier.combinedMouseClickable(
|
||||
interactionSource: MutableInteractionSource,
|
||||
indication: Indication?,
|
||||
enabled: Boolean = true,
|
||||
onClickLabel: String? = null,
|
||||
role: Role? = null,
|
||||
rightClickIsContextMenu: Boolean = true,
|
||||
onClick: (IntOffset) -> Unit,
|
||||
onMiddleClick: (IntOffset) -> Unit,
|
||||
onRightClick: (IntOffset) -> Unit
|
||||
) = composed(
|
||||
inspectorInfo = debugInspectorInfo {
|
||||
name = "combinedMouseClickable"
|
||||
properties["enabled"] = enabled
|
||||
properties["onClickLabel"] = onClickLabel
|
||||
properties["role"] = role
|
||||
properties["rightClickIsContextMenu"] = rightClickIsContextMenu
|
||||
properties["onClick"] = onClick
|
||||
properties["onMiddleClick"] = onMiddleClick
|
||||
properties["onRightClick"] = onRightClick
|
||||
properties["indication"] = indication
|
||||
properties["interactionSource"] = interactionSource
|
||||
}
|
||||
) {
|
||||
var lastEvent by remember { mutableStateOf<MouseEvent?>(null) }
|
||||
Modifier
|
||||
.clickable(interactionSource, indication, enabled, onClickLabel, role) {
|
||||
val savedLastEvent = lastEvent ?: return@clickable
|
||||
val offset = savedLastEvent.let { IntOffset(it.xOnScreen, it.yOnScreen) }
|
||||
when {
|
||||
rightClickIsContextMenu && savedLastEvent.isPopupTrigger -> onRightClick(offset)
|
||||
savedLastEvent.button == MouseEvent.BUTTON1 -> onClick(offset)
|
||||
savedLastEvent.button == MouseEvent.BUTTON2 -> onMiddleClick(offset)
|
||||
savedLastEvent.button == MouseEvent.BUTTON3 -> onRightClick(offset)
|
||||
}
|
||||
}
|
||||
.pointerInput(interactionSource) {
|
||||
forEachGesture {
|
||||
awaitPointerEventScope {
|
||||
lastEvent = awaitEventFirstDown().mouseEvent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
package ca.gosyer.ui.base.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -42,11 +41,11 @@ fun MangaGridItem(
|
||||
)
|
||||
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(mangaAspectRatio)
|
||||
.padding(8.dp)
|
||||
.clickable(onClick = onClick),
|
||||
.padding(8.dp),
|
||||
elevation = 4.dp,
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
) {
|
||||
|
||||
@@ -378,8 +378,9 @@ private fun ExpandableContent(
|
||||
shrinkVertically(animationSpec = tween(COLLAPSE_ANIMATION_DURATION))
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
initiallyVisible = initiallyVisible,
|
||||
remember { MutableTransitionState(initialState = initiallyVisible) }
|
||||
.apply { targetState = visible },
|
||||
modifier = Modifier,
|
||||
enter = enterExpand + enterFadeIn,
|
||||
exit = exitCollapse + exitFadeOut
|
||||
) {
|
||||
|
||||
@@ -42,12 +42,17 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.BuildConfig
|
||||
import ca.gosyer.ui.base.vm.viewModel
|
||||
import ca.gosyer.util.compose.ThemedWindow
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import mu.KotlinLogging
|
||||
|
||||
fun openCategoriesMenu(notifyFinished: (() -> Unit)? = null) {
|
||||
val windowEvents = WindowEvents()
|
||||
ThemedWindow("TachideskJUI - Categories", events = windowEvents) {
|
||||
ThemedWindow("${BuildConfig.NAME} - Categories", events = windowEvents) {
|
||||
CategoriesMenu(notifyFinished, windowEvents)
|
||||
}
|
||||
}
|
||||
@@ -58,10 +63,16 @@ fun CategoriesMenu(notifyFinished: (() -> Unit)? = null, windowEvents: WindowEve
|
||||
val categories by vm.categories.collectAsState()
|
||||
remember {
|
||||
windowEvents.onClose = {
|
||||
vm.updateCategories()
|
||||
val logger = KotlinLogging.logger {}
|
||||
val handler = CoroutineExceptionHandler { _, throwable ->
|
||||
logger.debug { throwable }
|
||||
}
|
||||
GlobalScope.launch(handler) {
|
||||
vm.updateRemoteCategories()
|
||||
notifyFinished?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Surface {
|
||||
Box {
|
||||
|
||||
@@ -10,8 +10,6 @@ import ca.gosyer.data.models.Category
|
||||
import ca.gosyer.data.server.interactions.CategoryInteractionHandler
|
||||
import ca.gosyer.ui.base.vm.ViewModel
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -50,11 +48,7 @@ class CategoriesMenuViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCategories(manualUpdate: Boolean = false) {
|
||||
val handler = CoroutineExceptionHandler { _, throwable ->
|
||||
logger.debug { throwable }
|
||||
}
|
||||
GlobalScope.launch(handler) {
|
||||
suspend fun updateRemoteCategories(manualUpdate: Boolean = false) {
|
||||
val categories = _categories.value
|
||||
val newCategories = categories.filter { it.id == null }
|
||||
newCategories.forEach {
|
||||
@@ -81,7 +75,6 @@ class CategoriesMenuViewModel @Inject constructor(
|
||||
getCategories()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun renameCategory(category: MenuCategory, newName: String) {
|
||||
_categories.value = (_categories.value - category + category.copy(name = newName)).sortedBy { it.order }
|
||||
|
||||
@@ -85,12 +85,9 @@ fun ExtensionsMenu() {
|
||||
ExtensionItem(
|
||||
extension,
|
||||
serverUrl,
|
||||
onInstallClicked = {
|
||||
vm.install(it)
|
||||
},
|
||||
onUninstallClicked = {
|
||||
vm.uninstall(it)
|
||||
}
|
||||
onInstallClicked = vm::install,
|
||||
onUpdateClicked = vm::update,
|
||||
onUninstallClicked = vm::uninstall
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
@@ -113,6 +110,7 @@ fun ExtensionItem(
|
||||
extension: Extension,
|
||||
serverUrl: String,
|
||||
onInstallClicked: (Extension) -> Unit,
|
||||
onUpdateClicked: (Extension) -> Unit,
|
||||
onUninstallClicked: (Extension) -> Unit
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxWidth().height(64.dp).background(MaterialTheme.colors.background)) {
|
||||
@@ -133,16 +131,30 @@ fun ExtensionItem(
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("18+", fontSize = 14.sp, color = Color.Red)
|
||||
}
|
||||
if (extension.obsolete) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Obsolete", fontSize = 14.sp, color = Color.Red)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Button(
|
||||
{
|
||||
if (extension.installed) onUninstallClicked(extension) else onInstallClicked(extension)
|
||||
when {
|
||||
extension.hasUpdate -> onUpdateClicked(extension)
|
||||
extension.installed -> onUninstallClicked(extension)
|
||||
else -> onInstallClicked(extension)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.align(Alignment.CenterEnd)
|
||||
) {
|
||||
Text(if (extension.installed) "Uninstall" else "Install")
|
||||
Text(
|
||||
when {
|
||||
extension.hasUpdate -> "Update"
|
||||
extension.installed -> "Uninstall"
|
||||
else -> "Install"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,18 @@ class ExtensionsMenuViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun update(extension: Extension) {
|
||||
logger.info { "Update clicked" }
|
||||
scope.launch {
|
||||
try {
|
||||
extensionHandler.updateExtension(extension)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
}
|
||||
getExtensions()
|
||||
}
|
||||
}
|
||||
|
||||
fun uninstall(extension: Extension) {
|
||||
logger.info { "Uninstall clicked" }
|
||||
scope.launch {
|
||||
|
||||
@@ -138,7 +138,7 @@ private fun LibraryPager(
|
||||
val state = remember(categories.size, selectedPage) {
|
||||
PagerState(
|
||||
currentPage = selectedPage,
|
||||
pageCount = categories.lastIndex
|
||||
pageCount = categories.size
|
||||
)
|
||||
}
|
||||
LaunchedEffect(state.currentPage) {
|
||||
@@ -146,7 +146,7 @@ private fun LibraryPager(
|
||||
onPageChanged(state.currentPage)
|
||||
}
|
||||
}
|
||||
HorizontalPager(state = state, offscreenLimit = 1) {
|
||||
HorizontalPager(state = state) {
|
||||
val library by getLibraryForPage(it)
|
||||
when (displayMode) {
|
||||
DisplayMode.CompactGrid -> LibraryMangaCompactGrid(
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
package ca.gosyer.ui.main
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -18,11 +17,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -32,6 +33,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import ca.gosyer.BuildConfig
|
||||
import ca.gosyer.data.ui.model.StartScreen
|
||||
import ca.gosyer.ui.base.vm.viewModel
|
||||
import ca.gosyer.ui.extensions.ExtensionsMenu
|
||||
import ca.gosyer.ui.library.LibraryScreen
|
||||
@@ -47,55 +49,48 @@ import ca.gosyer.ui.settings.SettingsScreen
|
||||
import ca.gosyer.ui.settings.SettingsServerScreen
|
||||
import ca.gosyer.ui.sources.SourcesMenu
|
||||
import com.github.zsoltk.compose.router.Router
|
||||
import com.github.zsoltk.compose.savedinstancestate.Bundle
|
||||
import com.github.zsoltk.compose.savedinstancestate.BundleScope
|
||||
import compose.icons.FontAwesomeIcons
|
||||
import compose.icons.fontawesomeicons.Regular
|
||||
import compose.icons.fontawesomeicons.regular.Bookmark
|
||||
import compose.icons.fontawesomeicons.regular.Compass
|
||||
import compose.icons.fontawesomeicons.regular.Edit
|
||||
import compose.icons.fontawesomeicons.regular.Map
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import mu.KotlinLogging
|
||||
import kotlin.time.seconds
|
||||
|
||||
@Composable
|
||||
fun MainMenu() {
|
||||
fun MainMenu(rootBundle: Bundle) {
|
||||
val vm = viewModel<MainViewModel>()
|
||||
Surface {
|
||||
Router<Route>("TopLevel", Route.Library) { backStack ->
|
||||
Router("TopLevel", vm.startScreen.toRoute()) { backStack ->
|
||||
Row {
|
||||
Surface(elevation = 2.dp) {
|
||||
Column(Modifier.width(200.dp).fillMaxHeight(),) {
|
||||
Box(Modifier.fillMaxWidth().height(60.dp)) {
|
||||
Text(BuildConfig.NAME, fontSize = 30.sp, modifier = Modifier.align(Alignment.Center))
|
||||
Text(
|
||||
BuildConfig.NAME,
|
||||
fontSize = 30.sp,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(20.dp))
|
||||
remember { TopLevelMenus.values() }.forEach { topLevelMenu ->
|
||||
MainMenuItem(topLevelMenu, backStack.elements.first() == topLevelMenu.menu) {
|
||||
MainMenuItem(
|
||||
topLevelMenu,
|
||||
backStack.elements.first() == topLevelMenu.menu
|
||||
) {
|
||||
backStack.newRoot(it)
|
||||
}
|
||||
}
|
||||
|
||||
/*Button(
|
||||
onClick = ::openExtensionsMenu
|
||||
) {
|
||||
Text("Extensions")
|
||||
}
|
||||
Button(
|
||||
onClick = ::openSourcesMenu
|
||||
) {
|
||||
Text("Sources")
|
||||
}
|
||||
Button(
|
||||
onClick = ::openLibraryMenu
|
||||
) {
|
||||
Text("Library")
|
||||
}
|
||||
Button(
|
||||
onClick = ::openCategoriesMenu
|
||||
) {
|
||||
Text("Categories")
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
BundleScope("K${backStack.lastIndex}", rootBundle, false) {
|
||||
when (val routing = backStack.last()) {
|
||||
is Route.Library -> LibraryScreen {
|
||||
backStack.push(Route.Manga(it))
|
||||
@@ -124,19 +119,22 @@ fun MainMenu() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainMenuItem(menu: TopLevelMenus, selected: Boolean, onClick: (Route) -> Unit) {
|
||||
Card(
|
||||
Modifier.clickable { onClick(menu.menu) }.fillMaxWidth().height(40.dp),
|
||||
{ onClick(menu.menu) },
|
||||
Modifier.fillMaxWidth().height(40.dp),
|
||||
backgroundColor = if (!selected) {
|
||||
Color.Transparent
|
||||
} else {
|
||||
MaterialTheme.colors.primary.copy(0.30F)
|
||||
},
|
||||
contentColor = Color.Transparent,
|
||||
elevation = 0.dp
|
||||
elevation = 0.dp,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize()) {
|
||||
Spacer(Modifier.width(16.dp))
|
||||
@@ -147,6 +145,12 @@ fun MainMenuItem(menu: TopLevelMenus, selected: Boolean, onClick: (Route) -> Uni
|
||||
}
|
||||
}
|
||||
|
||||
fun StartScreen.toRoute() = when (this) {
|
||||
StartScreen.Library -> Route.Library
|
||||
StartScreen.Sources -> Route.Sources
|
||||
StartScreen.Extensions -> Route.Extensions
|
||||
}
|
||||
|
||||
enum class TopLevelMenus(val text: String, val icon: ImageVector, val menu: Route) {
|
||||
Library("Library", FontAwesomeIcons.Regular.Bookmark, Route.Library),
|
||||
Sources("Sources", FontAwesomeIcons.Regular.Compass, Route.Sources),
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.github.weisj.darklaf.theme.DarculaTheme
|
||||
import com.github.weisj.darklaf.theme.IntelliJTheme
|
||||
import com.github.zsoltk.compose.backpress.BackPressHandler
|
||||
import com.github.zsoltk.compose.backpress.LocalBackPressHandler
|
||||
import com.github.zsoltk.compose.savedinstancestate.Bundle
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import org.apache.logging.log4j.core.config.Configurator
|
||||
@@ -112,6 +113,7 @@ fun main() {
|
||||
)
|
||||
}
|
||||
|
||||
val rootBundle = Bundle()
|
||||
window.show {
|
||||
AppTheme {
|
||||
CompositionLocalProvider(
|
||||
@@ -119,7 +121,7 @@ fun main() {
|
||||
) {
|
||||
val initialized by serverService.initialized.collectAsState()
|
||||
if (initialized == ServerResult.STARTED || initialized == ServerResult.UNUSED) {
|
||||
MainMenu()
|
||||
MainMenu(rootBundle)
|
||||
} else if (initialized == ServerResult.STARTING) {
|
||||
LoadingScreen()
|
||||
} else if (initialized == ServerResult.FAILED) {
|
||||
|
||||
@@ -8,7 +8,6 @@ package ca.gosyer.ui.manga
|
||||
|
||||
import androidx.compose.foundation.VerticalScrollbar
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -28,6 +27,8 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollbarAdapter
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ContentAlpha
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
@@ -39,21 +40,24 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import ca.gosyer.BuildConfig
|
||||
import ca.gosyer.data.models.Chapter
|
||||
import ca.gosyer.data.models.Manga
|
||||
import ca.gosyer.ui.base.components.KtorImage
|
||||
import ca.gosyer.ui.base.components.LoadingScreen
|
||||
import ca.gosyer.ui.base.components.Toolbar
|
||||
import ca.gosyer.ui.base.components.combinedMouseClickable
|
||||
import ca.gosyer.ui.base.components.mangaAspectRatio
|
||||
import ca.gosyer.ui.base.vm.viewModel
|
||||
import ca.gosyer.ui.main.Route
|
||||
import ca.gosyer.ui.reader.openReaderMenu
|
||||
import ca.gosyer.util.compose.ThemedWindow
|
||||
import ca.gosyer.util.compose.contextMenu
|
||||
import com.github.zsoltk.compose.router.BackStack
|
||||
import java.util.Date
|
||||
|
||||
fun openMangaMenu(mangaId: Long) {
|
||||
ThemedWindow("TachideskJUI") {
|
||||
ThemedWindow(BuildConfig.NAME) {
|
||||
MangaMenu(mangaId)
|
||||
}
|
||||
}
|
||||
@@ -87,9 +91,14 @@ fun MangaMenu(mangaId: Long, backStack: BackStack<Route>? = null) {
|
||||
MangaItem(manga, serverUrl)
|
||||
}
|
||||
items(chapters) { chapter ->
|
||||
ChapterItem(chapter, dateFormat::format) {
|
||||
openReaderMenu(it, manga.id)
|
||||
}
|
||||
ChapterItem(
|
||||
chapter,
|
||||
dateFormat::format,
|
||||
onClick = { openReaderMenu(it, manga.id) },
|
||||
toggleRead = vm::toggleRead,
|
||||
toggleBookmarked = vm::toggleBookmarked,
|
||||
markPreviousAsRead = vm::markPreviousRead
|
||||
)
|
||||
}
|
||||
}
|
||||
VerticalScrollbar(
|
||||
@@ -178,20 +187,61 @@ private fun MangaInfo(manga: Manga, modifier: Modifier = Modifier) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChapterItem(chapter: Chapter, format: (Date) -> String, onClick: (Int) -> Unit) {
|
||||
Surface(modifier = Modifier.fillMaxWidth().clickable { onClick(chapter.chapterIndex) }.height(70.dp).padding(4.dp), elevation = 1.dp) {
|
||||
Column(Modifier.padding(4.dp)) {
|
||||
Text(chapter.name, fontSize = 20.sp, maxLines = 1)
|
||||
fun ChapterItem(
|
||||
chapter: Chapter,
|
||||
format: (Date) -> String,
|
||||
onClick: (Int) -> Unit,
|
||||
toggleRead: (Int) -> Unit,
|
||||
toggleBookmarked: (Int) -> Unit,
|
||||
markPreviousAsRead: (Int) -> Unit
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth().height(70.dp).padding(4.dp),
|
||||
elevation = 1.dp
|
||||
) {
|
||||
Column(
|
||||
Modifier.padding(4.dp)
|
||||
.combinedMouseClickable(
|
||||
onClick = {
|
||||
onClick(chapter.index)
|
||||
},
|
||||
onRightClick = {
|
||||
contextMenu(
|
||||
it
|
||||
) {
|
||||
menuItem("Toggle read") { toggleRead(chapter.index) }
|
||||
menuItem("Mark previous as read") { markPreviousAsRead(chapter.index) }
|
||||
separator()
|
||||
menuItem("Toggle bookmarked") { toggleBookmarked(chapter.index) }
|
||||
}
|
||||
}
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
chapter.name, fontSize = 20.sp, maxLines = 1,
|
||||
color = if (!chapter.read) {
|
||||
LocalContentColor.current
|
||||
} else {
|
||||
LocalContentColor.current.copy(alpha = ContentAlpha.disabled)
|
||||
}
|
||||
)
|
||||
val description = mutableListOf<String>()
|
||||
if (chapter.dateUpload != 0L) {
|
||||
description += format(Date(chapter.dateUpload))
|
||||
if (chapter.uploadDate != 0L) {
|
||||
description += format(Date(chapter.uploadDate))
|
||||
}
|
||||
if (!chapter.scanlator.isNullOrEmpty()) {
|
||||
description += chapter.scanlator
|
||||
}
|
||||
if (description.isNotEmpty()) {
|
||||
Spacer(Modifier.height(2.dp))
|
||||
Text(description.joinToString(" - "), maxLines = 1)
|
||||
Text(
|
||||
description.joinToString(" - "), maxLines = 1,
|
||||
color = if (!chapter.read) {
|
||||
LocalContentColor.current
|
||||
} else {
|
||||
LocalContentColor.current.copy(alpha = ContentAlpha.disabled)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ class MangaMenuViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun refreshChaptersAsync(mangaId: Long) = withContext(Dispatchers.IO) {
|
||||
private suspend fun refreshChaptersAsync(mangaId: Long) = withContext(Dispatchers.IO) {
|
||||
async {
|
||||
try {
|
||||
_chapters.value = chapterHandler.getChapters(mangaId)
|
||||
@@ -98,5 +98,32 @@ class MangaMenuViewModel @Inject constructor(
|
||||
else -> SimpleDateFormat(format, Locale.getDefault())
|
||||
}
|
||||
|
||||
fun toggleRead(index: Int) {
|
||||
scope.launch {
|
||||
manga.value?.let { manga ->
|
||||
chapterHandler.updateChapter(manga, index, read = !_chapters.value.first { it.index == index }.read)
|
||||
_chapters.value = chapterHandler.getChapters(manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleBookmarked(index: Int) {
|
||||
scope.launch {
|
||||
manga.value?.let { manga ->
|
||||
chapterHandler.updateChapter(manga, index, bookmarked = !_chapters.value.first { it.index == index }.bookmarked)
|
||||
_chapters.value = chapterHandler.getChapters(manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun markPreviousRead(index: Int) {
|
||||
scope.launch {
|
||||
manga.value?.let { manga ->
|
||||
chapterHandler.updateChapter(manga, index, markPreviousRead = true)
|
||||
_chapters.value = chapterHandler.getChapters(manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Params(val mangaId: Long)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
package ca.gosyer.ui.settings
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -137,6 +136,7 @@ private fun ThemeItem(
|
||||
Color.White.copy(alpha = 0.15f)
|
||||
}
|
||||
Surface(
|
||||
onClick = { onClick(theme) },
|
||||
elevation = 4.dp,
|
||||
color = theme.colors.background,
|
||||
shape = borders,
|
||||
@@ -144,7 +144,6 @@ private fun ThemeItem(
|
||||
.size(100.dp, 160.dp)
|
||||
.padding(8.dp)
|
||||
.border(1.dp, borderColor, borders)
|
||||
.clickable(onClick = { onClick(theme) })
|
||||
) {
|
||||
Column {
|
||||
Box(
|
||||
|
||||
@@ -6,28 +6,89 @@
|
||||
|
||||
package ca.gosyer.ui.settings
|
||||
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.min
|
||||
import ca.gosyer.data.server.interactions.BackupInteractionHandler
|
||||
import ca.gosyer.ui.base.components.Toolbar
|
||||
import ca.gosyer.ui.base.prefs.PreferenceRow
|
||||
import ca.gosyer.ui.base.vm.ViewModel
|
||||
import ca.gosyer.ui.base.vm.viewModel
|
||||
import ca.gosyer.ui.main.Route
|
||||
import ca.gosyer.util.system.filePicker
|
||||
import ca.gosyer.util.system.fileSaver
|
||||
import com.github.zsoltk.compose.router.BackStack
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import mu.KotlinLogging
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class SettingsBackupViewModel @Inject constructor() : ViewModel() {
|
||||
class SettingsBackupViewModel @Inject constructor(
|
||||
private val backupHandler: BackupInteractionHandler
|
||||
) : ViewModel() {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
fun setFile(file: File?) {
|
||||
private val _restoring = MutableStateFlow(false)
|
||||
val restoring = _restoring.asStateFlow()
|
||||
private val _restoreError = MutableStateFlow(false)
|
||||
val restoreError = _restoreError.asStateFlow()
|
||||
|
||||
private val _creating = MutableStateFlow(false)
|
||||
val creating = _creating.asStateFlow()
|
||||
private val _creatingError = MutableStateFlow(false)
|
||||
val creatingError = _creatingError.asStateFlow()
|
||||
|
||||
fun restoreFile(file: File?) {
|
||||
scope.launch {
|
||||
if (file == null || !file.exists()) {
|
||||
logger.info { "Invalid file ${file?.absolutePath}" }
|
||||
} else {
|
||||
logger.info { file.absolutePath }
|
||||
_restoreError.value = false
|
||||
_restoring.value = true
|
||||
try {
|
||||
backupHandler.importBackupFile(file)
|
||||
} catch (e: Exception) {
|
||||
logger.info(e) { "Error importing backup" }
|
||||
_restoreError.value = true
|
||||
} finally {
|
||||
_restoring.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createFile(file: File?) {
|
||||
scope.launch {
|
||||
if (file == null) {
|
||||
logger.info { "Invalid file ${file?.absolutePath}" }
|
||||
} else {
|
||||
if (file.exists()) file.delete()
|
||||
_creatingError.value = false
|
||||
_creating.value = true
|
||||
try {
|
||||
val backup = backupHandler.exportBackupFile()
|
||||
} catch (e: Exception) {
|
||||
logger.info(e) { "Error importing backup" }
|
||||
_creatingError.value = true
|
||||
} finally {
|
||||
_creating.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,17 +96,63 @@ class SettingsBackupViewModel @Inject constructor() : ViewModel() {
|
||||
@Composable
|
||||
fun SettingsBackupScreen(navController: BackStack<Route>) {
|
||||
val vm = viewModel<SettingsBackupViewModel>()
|
||||
val restoring by vm.restoring.collectAsState()
|
||||
val restoreError by vm.restoreError.collectAsState()
|
||||
val creating by vm.creating.collectAsState()
|
||||
val creatingError by vm.creatingError.collectAsState()
|
||||
Column {
|
||||
Toolbar("Backup Settings", navController, true)
|
||||
LazyColumn {
|
||||
item {
|
||||
PreferenceRow(
|
||||
PreferenceFile(
|
||||
"Restore Backup",
|
||||
onClick = {
|
||||
filePicker {
|
||||
vm.setFile(it.selectedFile)
|
||||
"Restore a backup into Tachidesk",
|
||||
restoring,
|
||||
restoreError
|
||||
) {
|
||||
filePicker("json") {
|
||||
vm.restoreFile(it.selectedFile)
|
||||
}
|
||||
}
|
||||
PreferenceFile(
|
||||
"Create Backup",
|
||||
"Create a backup from Tachidesk",
|
||||
creating,
|
||||
creatingError
|
||||
) {
|
||||
fileSaver("test.json", "json") {
|
||||
vm.createFile(it.selectedFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PreferenceFile(title: String, subtitle: String, working: Boolean, error: Boolean, onClick: () -> Unit) {
|
||||
PreferenceRow(
|
||||
title = title,
|
||||
onClick = onClick,
|
||||
enabled = !working,
|
||||
subtitle = subtitle
|
||||
) {
|
||||
BoxWithConstraints {
|
||||
val size = remember(maxHeight, maxWidth) {
|
||||
min(maxHeight, maxWidth) / 2
|
||||
}
|
||||
val modifier = Modifier.align(Alignment.Center)
|
||||
.size(size)
|
||||
if (working) {
|
||||
CircularProgressIndicator(
|
||||
modifier
|
||||
)
|
||||
} else if (error) {
|
||||
Icon(
|
||||
Icons.Default.Warning,
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
tint = Color.Red
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.data.models.Source
|
||||
@@ -33,7 +34,7 @@ import ca.gosyer.ui.sources.components.SourceHomeScreen
|
||||
import ca.gosyer.ui.sources.components.SourceScreen
|
||||
import ca.gosyer.util.compose.ThemedWindow
|
||||
import com.github.zsoltk.compose.savedinstancestate.Bundle
|
||||
import com.github.zsoltk.compose.savedinstancestate.BundleScope
|
||||
import com.github.zsoltk.compose.savedinstancestate.LocalSavedInstanceState
|
||||
|
||||
fun openSourcesMenu() {
|
||||
ThemedWindow(title = "TachideskJUI - Sources") {
|
||||
@@ -47,9 +48,7 @@ private const val SOURCE_MENU_KEY = "source_menu"
|
||||
|
||||
@Composable
|
||||
fun SourcesMenu(onMangaClick: (Long) -> Unit) {
|
||||
BundleScope(SOURCE_MENU_KEY, autoDispose = false) {
|
||||
SourcesMenu(it, onMangaClick)
|
||||
}
|
||||
SourcesMenu(LocalSavedInstanceState.current, onMangaClick)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -87,7 +86,6 @@ fun SourcesMenu(bundle: Bundle, onMangaClick: (Long) -> Unit) {
|
||||
}
|
||||
|
||||
val selectedSource: Source? = selectedSourceTab
|
||||
BundleScope("Sources") {
|
||||
if (selectedSource != null) {
|
||||
SourceScreen(selectedSource, onMangaClick)
|
||||
} else {
|
||||
@@ -96,5 +94,4 @@ fun SourcesMenu(bundle: Bundle, onMangaClick: (Long) -> Unit) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
49
src/main/kotlin/ca/gosyer/util/compose/ContextMenu.kt
Normal file
49
src/main/kotlin/ca/gosyer/util/compose/ContextMenu.kt
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.util.compose
|
||||
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import mu.KotlinLogging
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JMenuItem
|
||||
import javax.swing.JPopupMenu
|
||||
import javax.swing.JSeparator
|
||||
|
||||
class ContextMenu internal constructor() {
|
||||
val logger = KotlinLogging.logger {}
|
||||
internal val list = mutableListOf<Pair<Any, (() -> Unit)?>>()
|
||||
|
||||
fun popupMenu() = JPopupMenu().apply {
|
||||
fun (() -> Unit)?.andClose() {
|
||||
isVisible = false
|
||||
this?.invoke()
|
||||
}
|
||||
list.forEach { (item, block) ->
|
||||
when (item) {
|
||||
is JMenuItem -> add(item).apply {
|
||||
addActionListener {
|
||||
logger.info { it.actionCommand }
|
||||
logger.info { it.modifiers }
|
||||
block.andClose()
|
||||
}
|
||||
}
|
||||
is JSeparator -> add(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun menuItem(name: String, icon: Icon? = null, builder: JMenuItem.() -> Unit = {}, action: () -> Unit) {
|
||||
list += JMenuItem(name, icon).apply(builder) to action
|
||||
}
|
||||
fun separator() {
|
||||
list += JSeparator() to null
|
||||
}
|
||||
}
|
||||
|
||||
fun contextMenu(offset: IntOffset, contextMenu: ContextMenu.() -> Unit) {
|
||||
ContextMenu().apply(contextMenu).popupMenu().show(null, offset.x, offset.y)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import net.harawata.appdirs.AppDirsFactory
|
||||
import java.io.File
|
||||
import javax.swing.JFileChooser
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.filechooser.FileNameExtensionFilter
|
||||
|
||||
val appDirs: AppDirs by lazy {
|
||||
AppDirsFactory.getInstance()
|
||||
@@ -23,29 +24,69 @@ val userDataDir: File by lazy {
|
||||
}
|
||||
}
|
||||
|
||||
fun filePicker(
|
||||
extension: String? = null,
|
||||
builder: JFileChooser.() -> Unit = {},
|
||||
onCancel: (JFileChooser) -> Unit = {},
|
||||
onError: (JFileChooser) -> Unit = {},
|
||||
onApprove: (JFileChooser) -> Unit
|
||||
) = fileChooser(false, builder, onCancel, onError, onApprove, extension)
|
||||
|
||||
fun fileSaver(
|
||||
defaultFileName: String,
|
||||
extension: String,
|
||||
builder: JFileChooser.() -> Unit = {},
|
||||
onCancel: (JFileChooser) -> Unit = {},
|
||||
onError: (JFileChooser) -> Unit = {},
|
||||
onApprove: (JFileChooser) -> Unit
|
||||
) = fileChooser(
|
||||
true,
|
||||
builder,
|
||||
onCancel,
|
||||
onError,
|
||||
onApprove,
|
||||
extension,
|
||||
defaultFileName
|
||||
)
|
||||
|
||||
/**
|
||||
* Opens a swing file picker, in the details view by default
|
||||
*
|
||||
* @param saving true if the dialog is going to save a file, false if its going to open a file
|
||||
* @param builder invokes this builder before launching the file picker, such as adding a action listener
|
||||
* @param onCancel the listener that is called when picking a file is canceled
|
||||
* @param onError the listener that is called when picking a file exited with a error
|
||||
* @param onApprove the listener that is called when picking a file is completed
|
||||
*/
|
||||
fun filePicker(
|
||||
private fun fileChooser(
|
||||
saving: Boolean = false,
|
||||
builder: JFileChooser.() -> Unit = {},
|
||||
onCancel: (JFileChooser) -> Unit = {},
|
||||
onError: (JFileChooser) -> Unit = {},
|
||||
onApprove: (JFileChooser) -> Unit
|
||||
onApprove: (JFileChooser) -> Unit,
|
||||
extension: String? = null,
|
||||
defaultFileName: String = ""
|
||||
) = SwingUtilities.invokeLater {
|
||||
val fileChooser = JFileChooser()
|
||||
.apply {
|
||||
val details = actionMap.get("viewTypeDetails")
|
||||
details?.actionPerformed(null)
|
||||
if (extension != null) {
|
||||
fileFilter = FileNameExtensionFilter("$extension file", extension)
|
||||
}
|
||||
if (saving) {
|
||||
selectedFile = File(defaultFileName)
|
||||
}
|
||||
}
|
||||
.apply(builder)
|
||||
|
||||
val result = fileChooser
|
||||
.showOpenDialog(null)
|
||||
val result = fileChooser.let {
|
||||
if (saving) {
|
||||
it.showSaveDialog(null)
|
||||
} else {
|
||||
it.showOpenDialog(null)
|
||||
}
|
||||
}
|
||||
|
||||
when (result) {
|
||||
JFileChooser.APPROVE_OPTION -> onApprove(fileChooser)
|
||||
|
||||
Reference in New Issue
Block a user