mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-06-30 09:24:34 -05:00
Localize WebView and Login pages (#1522)
* Localize WebView and Login pages * Switch to JTE for page rendering * Lint * Add gradle task dependency * JTE -> KTE * ShouldRunAfter * I guess we must --------- Co-authored-by: Syer10 <syer10@users.noreply.github.com>
This commit is contained in:
@@ -11,6 +11,7 @@ plugins {
|
||||
alias(libs.plugins.download)
|
||||
alias(libs.plugins.kotlin.multiplatform) apply false
|
||||
alias(libs.plugins.moko) apply false
|
||||
alias(libs.plugins.jte) apply false
|
||||
}
|
||||
|
||||
allprojects {
|
||||
|
||||
@@ -4,6 +4,7 @@ coroutines = "1.10.2"
|
||||
serialization = "1.9.0"
|
||||
okhttp = "5.1.0" # Major version is locked by Tachiyomi extensions
|
||||
javalin = "6.7.0"
|
||||
jte = "3.2.1"
|
||||
jackson = "2.18.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
||||
exposed = "0.61.0"
|
||||
dex2jar = "v64" # Stuck until https://github.com/ThexXTURBOXx/dex2jar/issues/27 is fixed
|
||||
@@ -49,9 +50,12 @@ okio = "com.squareup.okio:okio:3.15.0"
|
||||
# Javalin api
|
||||
javalin-core = { module = "io.javalin:javalin", version.ref = "javalin" }
|
||||
javalin-openapi = { module = "io.javalin:javalin-openapi", version.ref = "javalin" }
|
||||
javalin-rendering = { module = "io.javalin:javalin-rendering", version.ref = "javalin" }
|
||||
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }
|
||||
jackson-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" }
|
||||
jackson-annotations = { module = "com.fasterxml.jackson.core:jackson-annotations", version.ref = "jackson" }
|
||||
jte = { module = "gg.jte:jte", version.ref = "jte" }
|
||||
kte = { module = "gg.jte:jte-kotlin", version.ref = "jte" }
|
||||
|
||||
# GraphQL
|
||||
graphql-kotlin-server = { module = "com.expediagroup:graphql-kotlin-server", version.ref = "graphqlkotlin" }
|
||||
@@ -177,6 +181,9 @@ shadowjar = { id = "com.github.johnrengelman.shadow", version = "8.1.1"}
|
||||
# Moko
|
||||
moko = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko" }
|
||||
|
||||
# JTE
|
||||
jte = { id = "gg.jte.gradle", version.ref = "jte" }
|
||||
|
||||
[bundles]
|
||||
shared = [
|
||||
"kotlin-stdlib-jdk8",
|
||||
@@ -216,6 +223,8 @@ okhttp = [
|
||||
javalin = [
|
||||
"javalin-core",
|
||||
#"javalin-openapi",
|
||||
"javalin-rendering",
|
||||
"jte",
|
||||
]
|
||||
jackson = [
|
||||
"jackson-databind",
|
||||
|
||||
@@ -25,6 +25,11 @@ plugins {
|
||||
.get()
|
||||
.pluginId,
|
||||
)
|
||||
id(
|
||||
libs.plugins.jte
|
||||
.get()
|
||||
.pluginId,
|
||||
)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -96,6 +101,12 @@ dependencies {
|
||||
implementation(libs.cron4j)
|
||||
|
||||
implementation(libs.cronUtils)
|
||||
|
||||
compileOnly(libs.kte)
|
||||
}
|
||||
|
||||
jte {
|
||||
generate()
|
||||
}
|
||||
|
||||
application {
|
||||
@@ -212,4 +223,8 @@ tasks {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
runKtlintCheckOverMainSourceSet {
|
||||
mustRunAfter(generateJte)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,4 +80,23 @@
|
||||
<string name="manga_status_publishing_finished">Publishing Finished</string>
|
||||
<string name="manga_status_cancelled">Cancelled</string>
|
||||
<string name="manga_status_on_hiatus">On Hiatus</string>
|
||||
</resources>
|
||||
|
||||
<string name="label_error">Error</string>
|
||||
<string name="label_version">Version <xliff:g id="version" example="v2.0.1833">%1$s</xliff:g></string>
|
||||
|
||||
<string name="webview_label_title">Suwayomi WebView</string>
|
||||
<string name="webview_label_disconnected">Disconnected, please refresh</string>
|
||||
<string name="webview_label_reversescroll">Reverse Scrolling</string>
|
||||
<string name="webview_label_bindingshint">Note: While focus is on the WebView part, no keybinds, including refresh, will be handled by the browser.</string>
|
||||
<string name="webview_label_init">Initializing... Please wait</string>
|
||||
<string name="webview_label_getstarted">Enter a URL to get started</string>
|
||||
<string name="webview_label_loading">Loading page...</string>
|
||||
<string name="webview_placeholder_url">Enter URL...</string>
|
||||
|
||||
<string name="login_label_title">Suwayomi Login</string>
|
||||
<string name="login_label_username">Username</string>
|
||||
<string name="login_label_password">Password</string>
|
||||
<string name="login_label_login">Log In</string>
|
||||
<string name="login_placeholder_username">Type username...</string>
|
||||
<string name="login_placeholder_password">Secret...</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
@import suwayomi.tachidesk.i18n.MR
|
||||
@import suwayomi.tachidesk.server.generated.BuildConfig
|
||||
|
||||
@param locale: java.util.Locale
|
||||
@param error: String
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Suwayomi Login</title>
|
||||
<title>${MR.strings.login_label_title.localized(locale)}</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
@@ -143,22 +149,22 @@
|
||||
<h1>Suwayomi</h1>
|
||||
</header>
|
||||
<main>
|
||||
<div class="error">[ERROR]</div>
|
||||
<div class="error">${error}</div>
|
||||
<form method="POST">
|
||||
<h2>Login</h2>
|
||||
<div class="controls">
|
||||
<label for="user">Username:</label>
|
||||
<input type="text" name="user" id="user" required placeholder="Type username..."/>
|
||||
<label for="pass">Password:</label>
|
||||
<input type="password" name="pass" id="pass" required placeholder="Secret..."/>
|
||||
<label for="user">${MR.strings.login_label_username.localized(locale)}:</label>
|
||||
<input type="text" name="user" id="user" required placeholder="${MR.strings.login_placeholder_username.localized(locale)}"/>
|
||||
<label for="pass">${MR.strings.login_label_password.localized(locale)}:</label>
|
||||
<input type="password" name="pass" id="pass" required placeholder="${MR.strings.login_placeholder_password.localized(locale)}"/>
|
||||
</div>
|
||||
<div class="submit">
|
||||
<button type="submit">Log In</button>
|
||||
<button type="submit">${MR.strings.login_label_login.localized(locale)}</button>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
<footer>
|
||||
<p>Suwayomi: Version [VERSION]</p>
|
||||
<p>Suwayomi: ${MR.strings.label_version.localized(locale, BuildConfig.VERSION)}</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,8 +1,12 @@
|
||||
@import suwayomi.tachidesk.i18n.MR
|
||||
|
||||
@param locale: java.util.Locale
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content" />
|
||||
<title>Suwayomi Webview</title>
|
||||
<title>${MR.strings.webview_label_title.localized(locale)}</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
@@ -20,7 +24,7 @@
|
||||
letter-spacing: 0em;
|
||||
}
|
||||
body.disconnected::after {
|
||||
content: 'Disconnected, please refresh';
|
||||
content: "${MR.strings.webview_label_disconnected.localized(locale)}";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(150, 0, 0, 0.5);
|
||||
@@ -141,18 +145,18 @@
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1 id="title">Suwayomi: WebView</h1>
|
||||
<h1 id="title">${MR.strings.webview_label_title.localized(locale)}</h1>
|
||||
<nav>
|
||||
<form id="browseForm">
|
||||
<input type="text" id="url" name="url" placeholder="Enter URL..." disabled/>
|
||||
<input type="text" id="url" name="url" placeholder="${MR.strings.webview_placeholder_url.localized(locale)}" disabled/>
|
||||
<button type="submit" id="goButton" disabled><span class="arrow-right"></span></button>
|
||||
</form>
|
||||
<label><input type="checkbox" id="reverseScroll" disabled/> Reverse Scrolling</label>
|
||||
<label><input type="checkbox" id="reverseScroll" disabled/> ${MR.strings.webview_label_reversescroll.localized(locale)}</label>
|
||||
</nav>
|
||||
<p><i>Note: While focus is on the WebView part, no keybinds, including refresh, will be handled by the browser</i></p>
|
||||
<p><i>${MR.strings.webview_label_bindingshint.localized(locale)}</i></p>
|
||||
</header>
|
||||
<main>
|
||||
<div class="message" id="message">Initializing... Please wait</div>
|
||||
<div class="message" id="message">${MR.strings.webview_label_init.localized(locale)}</div>
|
||||
<div class="status" id="status"></div>
|
||||
<canvas id="frame"></canvas>
|
||||
<input type="text" id="inputtrap" autocomplete="off"/>
|
||||
@@ -168,6 +172,7 @@
|
||||
const urlInput = document.getElementById('url');
|
||||
const titleDiv = document.getElementById('title');
|
||||
const reverseToggle = document.getElementById('reverseScroll');
|
||||
const origTitle = document.title;
|
||||
|
||||
try {
|
||||
const socketUrl = (window.location.origin + window.location.pathname).replace(/^http/,'ws');
|
||||
@@ -200,8 +205,8 @@
|
||||
|
||||
const setTitle = (title) => {
|
||||
if (!title) {
|
||||
document.title = "Suwayomi Webview";
|
||||
titleDiv.textContent = "Suwayomi Webview";
|
||||
document.title = origTitle;
|
||||
titleDiv.textContent = origTitle;
|
||||
} else {
|
||||
document.title = "Suwayomi: " + title;
|
||||
titleDiv.textContent = "Suwayomi: " + title;
|
||||
@@ -213,11 +218,11 @@
|
||||
urlInput.value = u;
|
||||
setHash(u);
|
||||
setTitle();
|
||||
messageDiv.textContent = 'Enter a URL to get started';
|
||||
messageDiv.textContent = "${MR.strings.webview_label_getstarted.localized(locale)}";
|
||||
ctx.clearRect(0, 0, frame.width, frame.height);
|
||||
return;
|
||||
}
|
||||
messageDiv.textContent = "Loading page...";
|
||||
messageDiv.textContent = "${MR.strings.webview_label_loading.localized(locale)}";
|
||||
messageDiv.classList.remove('error');
|
||||
urlInput.value = u;
|
||||
socket.send(JSON.stringify({ type: 'loadUrl', url: u, width: frame.clientWidth, height: frame.clientHeight }));
|
||||
@@ -265,7 +270,7 @@
|
||||
break;
|
||||
case "load": {
|
||||
if (obj.error) {
|
||||
messageDiv.textContent = "Error: " + obj.error;
|
||||
messageDiv.textContent = "${MR.strings.label_error.localized(locale)}: " + obj.error;
|
||||
messageDiv.classList.add('error');
|
||||
} else {
|
||||
messageDiv.textContent = "";
|
||||
@@ -286,7 +291,7 @@
|
||||
} break;
|
||||
case "consoleMessage": {
|
||||
const lg = obj.severity == 4 ? console.error : obj.severity == 3 ? console.warn : console.log;
|
||||
lg(`${obj.source}:${obj.line}:`, obj.message);
|
||||
lg(obj.source + ':' + obj.line + ':', obj.message);
|
||||
} break;
|
||||
default:
|
||||
console.warn("Unknown event", obj.type)
|
||||
@@ -296,7 +301,7 @@
|
||||
|
||||
socket.addEventListener('close', e => {
|
||||
if (e.wasClean) {
|
||||
console.log(`WebSocket connection closed cleanly, code=${e.code}, reason=${e.reason}`);
|
||||
console.log(`WebSocket connection closed cleanly, code=` + e.code + `, reason=` + e.reason);
|
||||
} else {
|
||||
console.error('WebSocket connection died');
|
||||
}
|
||||
@@ -304,7 +309,7 @@
|
||||
});
|
||||
|
||||
socket.addEventListener('error', e => {
|
||||
messageDiv.textContent = "Error: " + (e.message || e.reason || e);
|
||||
messageDiv.textContent = "${MR.strings.label_error.localized(locale)}: " + (e.message || e.reason || e);
|
||||
messageDiv.classList.add('error');
|
||||
console.error('WebSocket error:', e);
|
||||
});
|
||||
@@ -417,7 +422,7 @@
|
||||
attachEvents();
|
||||
frameInput.focus();
|
||||
} catch (e) {
|
||||
messageDiv.textContent = "Error: " + (e.message || e);
|
||||
messageDiv.textContent = "${MR.strings.label_error.localized(locale)}: " + (e.message || e);
|
||||
messageDiv.classList.add('error');
|
||||
console.error(e);
|
||||
}
|
||||
@@ -11,21 +11,31 @@ import io.javalin.http.ContentType
|
||||
import io.javalin.http.HttpStatus
|
||||
import io.javalin.websocket.WsConfig
|
||||
import suwayomi.tachidesk.global.impl.WebView
|
||||
import suwayomi.tachidesk.i18n.LocalizationHelper
|
||||
import suwayomi.tachidesk.server.util.handler
|
||||
import suwayomi.tachidesk.server.util.queryParam
|
||||
import suwayomi.tachidesk.server.util.withOperation
|
||||
import java.util.Locale
|
||||
|
||||
object WebViewController {
|
||||
val webview =
|
||||
handler(
|
||||
queryParam<String?>("lang"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("WebView")
|
||||
description("Opens and browses WebView")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
behaviorOf = { ctx, lang ->
|
||||
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
|
||||
ctx.contentType(ContentType.TEXT_HTML)
|
||||
ctx.result(javaClass.getResourceAsStream("/webview.html")!!)
|
||||
ctx.render(
|
||||
"Webview.jte",
|
||||
mapOf(
|
||||
"locale" to locale,
|
||||
),
|
||||
)
|
||||
},
|
||||
withResults = { mime<String>(HttpStatus.OK, "text/html") },
|
||||
)
|
||||
|
||||
@@ -7,6 +7,8 @@ package suwayomi.tachidesk.server
|
||||
* 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/. */
|
||||
|
||||
import gg.jte.ContentType
|
||||
import gg.jte.TemplateEngine
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.javalin.Javalin
|
||||
import io.javalin.apibuilder.ApiBuilder.path
|
||||
@@ -15,6 +17,7 @@ import io.javalin.http.HttpStatus
|
||||
import io.javalin.http.RedirectResponse
|
||||
import io.javalin.http.UnauthorizedResponse
|
||||
import io.javalin.http.staticfiles.Location
|
||||
import io.javalin.rendering.template.JavalinJte
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -25,14 +28,15 @@ import org.eclipse.jetty.server.ServerConnector
|
||||
import suwayomi.tachidesk.global.GlobalAPI
|
||||
import suwayomi.tachidesk.graphql.GraphQL
|
||||
import suwayomi.tachidesk.graphql.types.AuthMode
|
||||
import suwayomi.tachidesk.i18n.LocalizationHelper
|
||||
import suwayomi.tachidesk.manga.MangaAPI
|
||||
import suwayomi.tachidesk.opds.OpdsAPI
|
||||
import suwayomi.tachidesk.server.generated.BuildConfig
|
||||
import suwayomi.tachidesk.server.util.Browser
|
||||
import suwayomi.tachidesk.server.util.WebInterfaceManager
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
import java.net.URLEncoder
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.time.Duration.Companion.days
|
||||
@@ -49,6 +53,8 @@ object JavalinSetup {
|
||||
fun javalinSetup() {
|
||||
val app =
|
||||
Javalin.create { config ->
|
||||
val templateEngine = TemplateEngine.createPrecompiled(ContentType.Html)
|
||||
config.fileRenderer(JavalinJte(templateEngine))
|
||||
if (serverConfig.webUIEnabled.value) {
|
||||
val serveWebUI = {
|
||||
config.spaRoot.addFile("/", applicationDirs.webUIRoot + "/index.html", Location.EXTERNAL)
|
||||
@@ -118,16 +124,17 @@ object JavalinSetup {
|
||||
}
|
||||
|
||||
app.get("/login.html") { ctx ->
|
||||
var page =
|
||||
this::class.java
|
||||
.getResourceAsStream("/static/login.html")!!
|
||||
.use { it.readAllBytes() }
|
||||
.toString(Charsets.UTF_8)
|
||||
page = page.replace("[VERSION]", BuildConfig.VERSION).replace("[ERROR]", "")
|
||||
val locale: Locale = LocalizationHelper.ctxToLocale(ctx)
|
||||
ctx.header("content-type", "text/html")
|
||||
val httpCacheSeconds = 1.days.inWholeSeconds
|
||||
ctx.header("cache-control", "max-age=$httpCacheSeconds")
|
||||
ctx.result(page)
|
||||
ctx.render(
|
||||
"Login.jte",
|
||||
mapOf(
|
||||
"locale" to locale,
|
||||
"error" to "",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
app.post("/login.html") { ctx ->
|
||||
@@ -147,15 +154,16 @@ object JavalinSetup {
|
||||
throw RedirectResponse(HttpStatus.SEE_OTHER)
|
||||
}
|
||||
|
||||
var page =
|
||||
this::class.java
|
||||
.getResourceAsStream("/static/login.html")!!
|
||||
.use { it.readAllBytes() }
|
||||
.toString(Charsets.UTF_8)
|
||||
page = page.replace("[VERSION]", BuildConfig.VERSION).replace("[ERROR]", "Invalid username or password")
|
||||
val locale: Locale = LocalizationHelper.ctxToLocale(ctx)
|
||||
ctx.header("content-type", "text/html")
|
||||
ctx.req().session.invalidate()
|
||||
ctx.result(page)
|
||||
ctx.render(
|
||||
"Login.jte",
|
||||
mapOf(
|
||||
"locale" to locale,
|
||||
"error" to "Invalid username or password",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
app.beforeMatched { ctx ->
|
||||
|
||||
Reference in New Issue
Block a user