Compare commits

..
15 Commits
Author SHA1 Message Date
dev-clawandGitHub 0108b670c5 fixes #314 (#315) 2026-07-04 20:46:37 +01:00
dev-claw 378d493e2a Update version 2026-07-04 20:23:21 +01:00
dev-clawandGitHub 00468b7ed3 fixes #312 (#313) 2026-07-04 20:20:14 +01:00
dev-clawandGitHub 0978ff37b0 fixes #310 (#311) 2026-07-04 13:06:20 +01:00
dev-claw 5e52af0e00 Update version 2026-05-01 08:24:21 +01:00
dev-clawandGitHub 0dec4bd3f3 fixes #308 (#309) 2026-05-01 08:23:30 +01:00
dev-claw bb053ca314 Update README.md 2026-05-01 07:59:24 +01:00
dev-claw 681ee81d1e Update version 2026-04-30 23:09:11 +01:00
dev-claw bd027691b2 Change default for #303 2026-04-30 23:08:04 +01:00
dev-clawandGitHub 419f7e2ca7 Feature/issue 303/create host specific settings (#307)
* fixes #303

* fixes #303
2026-04-30 22:38:50 +01:00
dev-clawandGitHub 123ee14dd6 fixes #300 (#304) 2026-04-30 21:51:43 +01:00
dev-clawandGitHub a549105f7a fixes #302 (#305) 2026-04-30 21:51:33 +01:00
dev-clawandGitHub 8d1d74c5e8 fixes #303 (#306) 2026-04-30 21:51:24 +01:00
dev-claw eec3d5d75c Update version 2026-04-20 17:41:37 +01:00
dev-clawandGitHub 3f5127cbe3 fixes #298 (#299) 2026-04-20 17:40:32 +01:00
33 changed files with 393 additions and 83 deletions
+1 -1
View File
@@ -39,7 +39,7 @@ The following hosts are supported:
<img src="https://github.com/stashapp/stash/raw/develop/docs/readme_assets/windows_logo.svg" width="100%" height="75"> Windows | <img src="https://github.com/stashapp/stash/raw/develop/docs/readme_assets/mac_logo.svg" width="100%" height="75"> macOS (Intel) | <img src="https://github.com/stashapp/stash/raw/develop/docs/readme_assets/mac_logo.svg" width="100%" height="75"> macOS (Apple silicon) | <img src="https://github.com/stashapp/stash/raw/develop/docs/readme_assets/linux_logo.svg" width="100%" height="75"> Linux | <img src="https://images.vexels.com/media/users/3/166401/isolated/preview/b82aa7ac3f736dd78570dd3fa3fa9e24-java-programming-language-icon-by-vexels.png" width="100%" height="75"> Java
:---:|:---:|:---:|:---:|:---:
[Installer (EXE)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-windows-installer-6.5.8.exe) <br /> [Installer (MSI)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-windows-installer-6.5.8.msi) <br /> [Portable (ZIP)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-windows-portable-6.5.8.zip) | [Installer (DMG)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-macos-6.5.8.x86_64.dmg) <br /> [Installer (PKG)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-macos-6.5.8.x86_64.pkg) <br /> [Portable (ZIP)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-macos-portable-6.5.8.x86_64.zip) | [Installer (DMG)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-macos-6.5.8.arm64.dmg) <br /> [Installer (PKG)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-macos-6.5.8.arm64.pkg) <br /> [Portable (ZIP)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-macos-portable-6.5.8.arm64.zip) | [Linux (amd64) (DEB)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-linux-6.5.3_amd64.deb) <br /> [Linux (x86_64) (RPM)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-linux-6.5.8.x86_64.rpm) <br /> [Portable (ZIP)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-linux-portable-6.5.8.zip) | [Java GUI (noarch)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-noarch-gui-6.5.8.jar) <br /> [Java Web (noarch)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-noarch-web-6.5.8.jar)
[Installer (EXE)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-windows-installer-6.10.0.exe) <br /> [Installer (MSI)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-windows-installer-6.10.0.msi) <br /> [Portable (ZIP)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-windows-portable-6.10.0.zip) | [Installer (DMG)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-macos-6.10.0.x86_64.dmg) <br /> [Installer (PKG)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-macos-6.10.0.x86_64.pkg) <br /> [Portable (ZIP)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-macos-portable-6.10.0.x86_64.zip) | [Installer (DMG)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-macos-6.10.0.arm64.dmg) <br /> [Installer (PKG)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-macos-6.10.0.arm64.pkg) <br /> [Portable (ZIP)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-macos-portable-6.10.0.arm64.zip) | [Linux (amd64) (DEB)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-linux-6.5.3_amd64.deb) <br /> [Linux (x86_64) (RPM)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-linux-6.10.0.x86_64.rpm) <br /> [Portable (ZIP)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-linux-portable-6.10.0.zip) | [Java GUI (noarch)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-noarch-gui-6.10.0.jar) <br /> [Java Web (noarch)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-noarch-web-6.10.0.jar)
Source code and previous versions are available on
the [Releases page](https://github.com/dev-claw/vripper-project/releases).
+2 -2
View File
@@ -15,8 +15,8 @@
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<revision>6.8.0</revision>
<app-version>6.8.0</app-version>
<revision>6.10.0</revision>
<app-version>6.10.0</app-version>
<kotlin.version>2.2.20</kotlin.version>
<slf4j.version>2.0.16</slf4j.version>
<logback.version>1.5.12</logback.version>
@@ -171,9 +171,9 @@ internal abstract class Host(
fun <T> fetch(
url: String,
context: Context,
referer: String = "https://vipergirls.to/",
transformer: (ClassicHttpResponse) -> T
): T {
val referer = context.headers["Referer"] ?: "https://vipergirls.to/"
val httpGet =
HttpGet(url).also {
it.addHeader("Referer", referer)
@@ -23,11 +23,18 @@ internal class ImageBamHost : Host("imagebam.com", 2) {
val doc = try {
log.debug(String.format("Looking for xpath expression %s in %s", CONTINUE_XPATH, context.imageEntity.url))
if (XpathUtils.getAsNode(document, CONTINUE_XPATH) != null) {
val sfwCookie = BasicClientCookie("sfw_inter", "1")
sfwCookie.domain = "www.imagebam.com"
sfwCookie.path = "/"
sfwCookie.setExpiryDate(LocalDateTime.now().plusDays(3).atZone(ZoneId.systemDefault()).toInstant())
val nsfwCookie = BasicClientCookie("nsfw_inter", "1").apply {
domain = "www.imagebam.com"
path = "/"
setExpiryDate(LocalDateTime.now().plusDays(3).atZone(ZoneId.systemDefault()).toInstant())
}
val sfwCookie = BasicClientCookie("sfw_inter", "1").apply {
domain = "www.imagebam.com"
path = "/"
setExpiryDate(LocalDateTime.now().plusDays(3).atZone(ZoneId.systemDefault()).toInstant())
}
context.httpContext.cookieStore.addCookie(nsfwCookie)
context.httpContext.cookieStore.addCookie(sfwCookie)
fetch(context.imageEntity.url, context) {
HtmlUtils.clean(it.entity.content)
@@ -2,6 +2,8 @@ package me.vripper.host
import me.vripper.entities.ImageEntity
import me.vripper.exception.HostException
import me.vripper.model.HostName
import me.vripper.model.HostSettingKey
import me.vripper.services.download.ImageDownloadRunnable
import me.vripper.utilities.HtmlUtils
import me.vripper.utilities.LoggerDelegate
@@ -20,9 +22,7 @@ internal class ImxHost : Host("imx.to", 8) {
context: ImageDownloadRunnable.Context
): Pair<String, String> {
log.debug("Resolving name and image url for ${context.imageEntity.url}")
val imgTitle = getTitle(context).ifEmpty {
getDefaultImageName(context.imageEntity.thumbUrl)
}
val imgTitle = getTitle(context)
val imgUrl = findPattern(context.imageEntity)
return Pair(
imgTitle, imgUrl
@@ -30,7 +30,10 @@ internal class ImxHost : Host("imx.to", 8) {
}
private fun getTitle(context: ImageDownloadRunnable.Context): String {
return try {
return if (context.settings.hostSettings[HostName.IMX]?.get(HostSettingKey.TRY_TO_FETCH_ORIGINAL_FILENAME)
.toBoolean()
) {
val httpsUrl = context.imageEntity.url.replace("http:", "https:")
val document = fetchDocument(httpsUrl, context)
var value: String? = null
@@ -58,9 +61,10 @@ internal class ImxHost : Host("imx.to", 8) {
log.debug("Resolving name for $httpsUrl")
val imgTitle = imgNode?.attributes?.getNamedItem("alt")?.textContent?.trim() ?: ""
return imgTitle
} catch (_: Exception) {
""
imgTitle
} else {
getDefaultImageName(context.imageEntity.thumbUrl)
}
}
@@ -69,10 +73,14 @@ internal class ImxHost : Host("imx.to", 8) {
.replace("http:", "https:")
return if (url.startsWith("https://image.imx.to/u/t/")) {
"https://image.imx.to/u/i/" + url.replace("https://image.imx.to/u/t/", "")
} else if (url.startsWith("https://imx.to/u/t")) {
"https://image.imx.to/u/i/" + url.replace("https://imx.to/u/t", "")
} else if (url.startsWith("https://t.imx.to/t/")) {
"https://image.imx.to/u/i/" + url.replace("https://t.imx.to/t/", "")
} else if (url.startsWith("https://imx.to/upload/small/")) {
"https://image.imx.to/u/i/" + url.replace("https://imx.to/upload/small/", "")
} else if (url.startsWith("https://i.imx.to/t/")) {
"https://image.imx.to/u/i/" + url.replace("https://i.imx.to/t/", "")
} else if (url.startsWith("https://image.imx.to/u/i/")) {
url
} else {
@@ -35,6 +35,7 @@ internal class ViprImHost : Host("vipr.im", 15) {
.map { obj: Node -> obj.textContent }
.map { obj: String -> obj.trim() }.orElse(null)
val imgUrl = imgNode.attributes.getNamedItem("src").textContent.trim()
context.headers["Referer"] = "https://vipr.im/"
Pair(imgTitle!!, imgUrl)
} catch (e: Exception) {
throw HostException("Unexpected error occurred", e)
@@ -5,12 +5,27 @@ import me.vripper.utilities.ApplicationProperties.VRIPPER_DIR
import java.nio.file.Files
import kotlin.io.path.pathString
enum class HostName {
IMX
}
enum class HostSettingKey(val type: SettingType) {
TRY_TO_FETCH_ORIGINAL_FILENAME(SettingType.BOOLEAN)
}
enum class SettingType {
STRING, BOOLEAN, INT
}
@Serializable
data class Settings(
val connectionSettings: ConnectionSettings = ConnectionSettings(),
val downloadSettings: DownloadSettings = DownloadSettings(),
val viperSettings: ViperSettings = ViperSettings(),
val systemSettings: SystemSettings = SystemSettings()
val systemSettings: SystemSettings = SystemSettings(),
val hostSettings: Map<HostName, Map<HostSettingKey, String>> = mapOf(
HostName.IMX to mapOf(HostSettingKey.TRY_TO_FETCH_ORIGINAL_FILENAME to "false")
)
)
@Serializable
@@ -51,3 +66,4 @@ data class SystemSettings(
val clipboardPollingRate: Int = 500,
val maxEventLog: Int = 1_000,
)
@@ -41,7 +41,7 @@ internal class HTTPService {
connectionExpiryJob = coroutineScope.launch {
while (isActive) {
it.closeExpired()
delay(60_000)
delay(300_000)
}
}
}
@@ -42,15 +42,18 @@ internal class ImageDownloadRunnable(
class Context(val imageEntity: ImageEntity, val settings: Settings) {
private val log by LoggerDelegate()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private lateinit var coroutineScope: CoroutineScope
private val jobs = mutableListOf<Job>()
val httpContext: HttpClientContext = HttpClientContext.create().apply {
cookieStore = HTTPService.cookieStore
}
val requests = mutableListOf<HttpUriRequestBase>()
val headers = mutableMapOf<String, String>()
var completed = false
var stopped = false
init {
fun init() {
coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
ApplicationProperties.VRIPPER_DIR.listDirectoryEntries()
.filter { it.fileName.pathString.startsWith("cookies") }.forEach { cookiesPath ->
loadCookies(cookiesPath).also { cookies ->
@@ -82,6 +85,7 @@ internal class ImageDownloadRunnable(
}
}
@Synchronized
fun clear() {
runBlocking {
requests.forEach { it.abort() }
@@ -101,8 +105,6 @@ internal class ImageDownloadRunnable(
private val vgAuthService: VGAuthService by inject()
private val hosts: List<Host> = getKoin().getAll()
val context = Context(imageEntity, settings)
var completed = false
var stopped = false
fun download() {
try {
@@ -139,7 +141,7 @@ internal class ImageDownloadRunnable(
dataAccessService.updateImage(context.imageEntity)
}
} catch (e: Exception) {
if (stopped) {
if (context.stopped) {
return
}
context.imageEntity.status = Status.ERROR
@@ -188,20 +190,24 @@ internal class ImageDownloadRunnable(
override fun run() {
try {
if (stopped) {
context.init()
if (context.stopped) {
return
}
download()
} finally {
completed = true
context.completed = true
context.clear()
if (context.stopped && context.imageEntity.downloaded != context.imageEntity.size) {
context.imageEntity.status = Status.STOPPED
}
dataAccessService.updateImage(context.imageEntity)
}
}
fun stop() {
stopped = true
context.stopped = true
context.clear()
dataAccessService.updateImage(context.imageEntity)
}
override fun equals(other: Any?): Boolean {
@@ -55,7 +55,7 @@ internal class QueueManager(
val toProcess =
if (postEntityId != null) running.filter { it.context.imageEntity.postEntityId == postEntityId } else running
toProcess.forEach { it.stop() }
while (toProcess.count { !it.completed } > 0) {
while (toProcess.count { !it.context.completed } > 0) {
Thread.sleep(100)
}
running.removeAll(toProcess)
@@ -158,7 +158,7 @@ internal class QueueManager(
downloadManagerLock.withLock {
val image = imageDownloadRunnable.context.imageEntity
clearRunningRunnable(imageDownloadRunnable.context.imageEntity.id)
if (!isPending(image.postEntityId) && !isRunning(image.postEntityId) && !imageDownloadRunnable.stopped) {
if (!isPending(image.postEntityId) && !isRunning(image.postEntityId) && !imageDownloadRunnable.context.stopped) {
dataAccessService.finishPost(image.postEntityId, true)
}
reportQueueState()
@@ -36,9 +36,14 @@ message SystemSettings {
int32 maxEventLog = 4;
}
message HostSettingsMap {
map<string, string> settings = 1;
}
message Settings {
ConnectionSettings connectionSettings = 1;
DownloadSettings downloadSettings = 2;
ViperSettings viperSettings = 3;
SystemSettings systemSettings = 4;
map<string, HostSettingsMap> hostSettings = 5;
}
+15 -3
View File
@@ -25,6 +25,18 @@
<appender name="RING" class="me.vripper.RingAppender"/>
<appender name="ASYNC-STDOUT" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="STDOUT"/>
</appender>
<appender name="ASYNC-FILE" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE"/>
</appender>
<appender name="ASYNC-RING" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="RING"/>
</appender>
<logger name="me.vripper" level="info"/>
<logger name="Exposed" level="off"/>
<logger name="org.springframework.web.socket.config.WebSocketMessageBrokerStats" level="off"/>
@@ -32,8 +44,8 @@
<Logger name="org.apache.hc.client5.http.wire" level="off"/>
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
<appender-ref ref="RING"/>
<appender-ref ref="ASYNC-STDOUT"/>
<appender-ref ref="ASYNC-FILE"/>
<appender-ref ref="ASYNC-RING"/>
</root>
</configuration>
@@ -0,0 +1,76 @@
package me.vripper.gui.components.fragments
import atlantafx.base.util.IntegerStringConverter
import javafx.scene.control.Spinner
import me.vripper.gui.model.settings.HostSettingsModel
import me.vripper.model.HostName
import me.vripper.model.HostSettingKey
import me.vripper.model.SettingType
import tornadofx.*
class HostSettingsFragment : Fragment("Host Settings") {
val hostSettings: Map<HostName, Map<HostSettingKey, String>> by param()
val hostSettingsModels: List<HostSettingsModel>
override val root = vbox {}
init {
val mutableHostSettingsModels = mutableListOf<HostSettingsModel>()
// Build models dynamically from hostSettings map
hostSettings.forEach { (hostName, settingsMap) ->
settingsMap.forEach { (settingKey, settingValue) ->
try {
val key = settingKey
val model = HostSettingsModel(
host = hostName,
settingKey = key,
initialValue = settingValue
)
mutableHostSettingsModels.add(model)
} catch (e: IllegalArgumentException) {
// Skip unknown settings
}
}
}
hostSettingsModels = mutableHostSettingsModels.toList()
// Build UI dynamically
with(root) {
val groupedByHost = hostSettingsModels.groupBy { it.host }
form {
groupedByHost.forEach { (hostName, settings) ->
fieldset(hostName.name) {
settings.forEach { model ->
field(
model.settingKey.name.replace("_", " ").lowercase()
.replaceFirstChar { it.uppercase() }) {
when (model.settingKey.type) {
SettingType.STRING -> textfield(model.valueProperty as javafx.beans.property.StringProperty)
SettingType.BOOLEAN -> checkbox(
"",
model.valueProperty as javafx.beans.property.BooleanProperty
)
SettingType.INT -> {
add(Spinner<Int>(Int.MIN_VALUE, Int.MAX_VALUE, model.getValue().toInt()).apply {
valueProperty().onChange {
(model.valueProperty as javafx.beans.property.IntegerProperty).value =
it ?: 0
}
isEditable = true
IntegerStringConverter.createFor(this)
})
}
}
}
}
}
}
}
}
}
}
@@ -24,6 +24,7 @@ class SettingsFragment : Fragment("Settings") {
val connectionSettings: ConnectionSettings by param()
val viperSettings: ViperSettings by param()
val systemSettings: SystemSettings by param()
val hostSettings: Map<String, Map<String, String>> by param()
private val settingsController: SettingsController by inject()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val downloadSettingsFragment: DownloadSettingsFragment =
@@ -34,6 +35,9 @@ class SettingsFragment : Fragment("Settings") {
find(mapOf(ViperSettingsFragment::viperSettings to viperSettings))
private val systemSettingsFragment: SystemSettingsFragment =
find(mapOf(SystemSettingsFragment::systemSettings to systemSettings))
private val hostSettingsFragment: HostSettingsFragment =
find(mapOf(HostSettingsFragment::hostSettings to hostSettings))
override val root = vbox(alignment = Pos.CENTER_RIGHT) {
spacing = 5.0
@@ -60,6 +64,10 @@ class SettingsFragment : Fragment("Settings") {
add(viperSettingsFragment)
graphic = FontIcon.of(Feather.LINK_2)
}
tab(hostSettingsFragment.title) {
add(hostSettingsFragment)
graphic = FontIcon.of(Feather.IMAGE)
}
}
borderpane {
right {
@@ -75,7 +83,8 @@ class SettingsFragment : Fragment("Settings") {
downloadSettingsFragment.downloadSettingsModel,
connectionSettingsFragment.connectionSettingsModel,
viperSettingsFragment.viperSettingsModel,
systemSettingsFragment.systemSettingsModel
systemSettingsFragment.systemSettingsModel,
hostSettingsFragment.hostSettingsModels
)
runLater {
close()
@@ -97,6 +97,7 @@ class ActionBarView : View() {
val connectionSettings = settingsController.findConnectionSettings()
val viperGirlsSettings = settingsController.findViperGirlsSettings()
val systemSettings = settingsController.findSystemSettings()
val hostSettings = settingsController.findHostSettings()
runLater {
find<SettingsFragment>(
mapOf(
@@ -104,6 +105,7 @@ class ActionBarView : View() {
SettingsFragment::connectionSettings to connectionSettings,
SettingsFragment::viperSettings to viperGirlsSettings,
SettingsFragment::systemSettings to systemSettings,
SettingsFragment::hostSettings to hostSettings,
)
).openModal()
}
@@ -2,11 +2,19 @@ package me.vripper.gui.components.views
import atlantafx.base.theme.*
import javafx.application.Application.setUserAgentStylesheet
import javafx.event.EventHandler
import javafx.scene.input.TransferMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import me.vripper.gui.controller.AppController
import me.vripper.gui.controller.MainController
import me.vripper.gui.controller.WidgetsController
import tornadofx.View
import tornadofx.onChange
import tornadofx.vbox
import java.net.URI
class AppView : View() {
@@ -17,6 +25,8 @@ class AppView : View() {
private val statusBarView: StatusBarView by inject()
private val actionBarView: ActionBarView by inject()
private val widgetsController: WidgetsController by inject()
private val appController: AppController by inject()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
init {
title = "VRipper ${mainController.version}"
with(root) {
@@ -30,6 +40,24 @@ class AppView : View() {
}
prefWidth = widgetsController.currentSettings.width
prefHeight = widgetsController.currentSettings.height
onDragOver = EventHandler { event ->
if (event.dragboard.hasString()) {
event.acceptTransferModes(TransferMode.COPY)
}
event.consume()
}
onDragDropped = EventHandler { event ->
val dragBoard = event.dragboard
if (dragBoard != null && dragBoard.hasString()) {
val url = dragBoard.string.trim()
runCatching {
val uri = URI(url)
coroutineScope.launch {
appController.scan(uri.toString())
}
}
}
}
}
widgetsController.currentSettings.visibleToolbarPanelProperty.onChange { it ->
@@ -33,9 +33,11 @@ class LoadingView : View("VRipper") {
init {
DatabaseManager.connect()
ClipboardManager.init()
coroutineScope.launch {
GuiEventBus.events.filterIsInstance(GuiEventBus.ApplicationInitialized::class).collect {
runLater {
ClipboardManager.init()
}
message.set("")
AppManager.stop()
grpcEndpointService.disconnect()
@@ -200,6 +200,11 @@ class LogTableView : View() {
tableView.placeholder = Label("Loading")
}
is GuiEventBus.RemoteError -> runLater {
items.clear()
tableView.placeholder = Label("Connection Failure")
}
else -> {}
}
}
@@ -99,6 +99,7 @@ class MenuBarView : View() {
val connectionSettings = settingsController.findConnectionSettings()
val viperGirlsSettings = settingsController.findViperGirlsSettings()
val systemSettings = settingsController.findSystemSettings()
val hostSettings = settingsController.findHostSettings()
runLater {
find<SettingsFragment>(
mapOf(
@@ -106,6 +107,7 @@ class MenuBarView : View() {
SettingsFragment::connectionSettings to connectionSettings,
SettingsFragment::viperSettings to viperGirlsSettings,
SettingsFragment::systemSettings to systemSettings,
SettingsFragment::hostSettings to hostSettings,
)
).openModal()
}
@@ -516,6 +516,11 @@ class PostsTableView : View() {
items.clear()
}
is GuiEventBus.RemoteError -> runLater {
items.clear()
tableView.placeholder = Label("Connection Failure")
}
else -> {}
}
}
@@ -29,15 +29,15 @@ class StatusBarView : View("Status bar") {
GuiEventBus.events.collect {
when (it) {
GuiEventBus.LocalSession, GuiEventBus.RemoteSession -> {
while (isActive) {
val result = runCatching { statusBarController.loggedInUser() }
if (result.isSuccess) {
runLater {
loggedUser.set(result.getOrNull())
}
break
val result = runCatching { statusBarController.loggedInUser() }
if (result.isSuccess) {
runLater {
loggedUser.set(result.getOrNull())
}
} else {
runLater {
loggedUser.set(null)
}
delay(1000)
}
}
@@ -214,6 +214,11 @@ class ThreadTableView : View() {
items.clear()
}
is GuiEventBus.RemoteError -> runLater {
items.clear()
tableView.placeholder = Label("Connection Failure")
}
else -> {}
}
}
@@ -0,0 +1,10 @@
package me.vripper.gui.controller
import me.vripper.gui.utils.AppEndpointManager.currentAppEndpointService
import tornadofx.Controller
class AppController : Controller() {
suspend fun scan(postLinks: String) {
runCatching { currentAppEndpointService().scanLinks(postLinks) }
}
}
@@ -1,7 +1,5 @@
package me.vripper.gui.controller
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.retryWhen
import me.vripper.gui.model.ImageModel
import me.vripper.gui.utils.AppEndpointManager.currentAppEndpointService
import me.vripper.model.Image
@@ -41,7 +39,7 @@ class ImageController : Controller() {
}
fun onUpdateImages(postId: Long) =
currentAppEndpointService().onUpdateImagesByPostEntityId(postId).retryWhen { _, _ -> delay(1000); true }
currentAppEndpointService().onUpdateImagesByPostEntityId(postId)
fun onStopped() = currentAppEndpointService().onStopped().retryWhen { _, _ -> delay(1000);true }
fun onStopped() = currentAppEndpointService().onStopped()
}
@@ -1,9 +1,6 @@
package me.vripper.gui.controller
import me.vripper.gui.model.settings.ConnectionSettingsModel
import me.vripper.gui.model.settings.DownloadSettingsModel
import me.vripper.gui.model.settings.SystemSettingsModel
import me.vripper.gui.model.settings.ViperSettingsModel
import me.vripper.gui.model.settings.*
import me.vripper.gui.utils.AppEndpointManager
import me.vripper.model.*
import tornadofx.Controller
@@ -36,11 +33,18 @@ class SettingsController : Controller() {
)
}
suspend fun findHostSettings(): Map<HostName, Map<HostSettingKey, String>> {
return runCatching { AppEndpointManager.currentAppEndpointService().getSettings().hostSettings }.getOrDefault(
emptyMap()
)
}
suspend fun saveNewSettings(
downloadSettingsModel: DownloadSettingsModel,
connectionSettingsModel: ConnectionSettingsModel,
viperSettingsModel: ViperSettingsModel,
systemSettingsModel: SystemSettingsModel
systemSettingsModel: SystemSettingsModel,
hostSettingsModel: List<HostSettingsModel>,
) {
runCatching {
AppEndpointManager.currentAppEndpointService().saveSettings(
@@ -76,7 +80,17 @@ class SettingsController : Controller() {
systemSettingsModel.enable,
systemSettingsModel.pollingRate,
systemSettingsModel.logEntries
)
),
hostSettings = run {
val hostSettingsMap = mutableMapOf<HostName, MutableMap<HostSettingKey, String>>()
hostSettingsModel.forEach { model ->
val hostName = model.host
val settingKey = model.settingKey
hostSettingsMap.computeIfAbsent(hostName) { mutableMapOf() }[settingKey] =
model.valueProperty.value.toString()
}
hostSettingsMap
}
)
)
}
@@ -12,7 +12,7 @@ class StatusBarController : Controller() {
}
fun connectionState(): String {
return currentAppEndpointService().connectionState()
return runCatching { currentAppEndpointService().connectionState() }.getOrElse { "" }
}
suspend fun getVersion(): String {
@@ -17,6 +17,8 @@ object GuiEventBus {
object ChangingSession : GUIEvent
object LocalSession : GUIEvent
object RemoteSession : GUIEvent
object Undefined : GUIEvent
data class RemoteError(val source: String, val message: String) : GUIEvent
}
sealed interface GUIEvent
@@ -0,0 +1,27 @@
package me.vripper.gui.model.settings
import javafx.beans.property.Property
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleStringProperty
import me.vripper.model.HostName
import me.vripper.model.HostSettingKey
import me.vripper.model.SettingType
class HostSettingsModel(
val host: HostName,
val settingKey: HostSettingKey,
initialValue: String,
) {
val valueProperty: Property<*> = when (settingKey.type) {
SettingType.STRING -> SimpleStringProperty(initialValue)
SettingType.BOOLEAN -> SimpleBooleanProperty(initialValue.toBoolean())
SettingType.INT -> SimpleIntegerProperty(initialValue.toIntOrNull() ?: 0)
}
fun getValue(): String = when (settingKey.type) {
SettingType.STRING -> (valueProperty as SimpleStringProperty).value
SettingType.BOOLEAN -> (valueProperty as SimpleBooleanProperty).value.toString()
SettingType.INT -> (valueProperty as SimpleIntegerProperty).value.toString()
}
}
@@ -223,6 +223,15 @@ internal class GrpcEndpointService : IAppEndpointService {
this.downloadSettings = downloadSettings
this.viperSettings = viperSettings
this.systemSettings = systemSettings
settings.hostSettings.forEach { (hostKey, hostSettings) ->
val hostSetting = with(SettingsOuterClass.HostSettingsMap.newBuilder()) {
hostSettings.forEach { (settingKey, settingValue) ->
this.putSettings(settingKey.name, settingValue)
}
build()
}
this.putHostSettings(hostKey.name, hostSetting)
}
build()
}
endpointServiceCoroutineStub!!.saveSettings(settingsRequest)
@@ -280,6 +289,14 @@ internal class GrpcEndpointService : IAppEndpointService {
clipboardPollingRate = settings.systemSettings.clipboardPollingRate,
maxEventLog = settings.systemSettings.maxEventLog,
),
hostSettings = settings.hostSettingsMap.entries.associate {
val host = HostName.valueOf(it.key)
val hostSettings = it.value.settingsMap.entries.associate { hostSettingsEntry ->
val settingKey = HostSettingKey.valueOf(hostSettingsEntry.key)
settingKey to hostSettingsEntry.value
}
host to hostSettings
}
)
@@ -385,7 +402,7 @@ internal class GrpcEndpointService : IAppEndpointService {
val result = runCatching {
val version = getVersion()
version >= "6.6.0"
isAtLeastWithAlphaBeta(version, "6.6.0")
}
return if (result.isSuccess) {
@@ -395,5 +412,26 @@ internal class GrpcEndpointService : IAppEndpointService {
}
}
override fun connectionState(): String = channel?.getState(true)?.name ?: ConnectivityState.SHUTDOWN.name
}
fun isAtLeastWithAlphaBeta(actual: String, min: String): Boolean {
fun core(s: String) = s.substringBefore('-')
fun suffix(s: String): String? {
val i = s.indexOf('-')
return if (i >= 0 && i + 1 < s.length) s.substring(i + 1) else null
}
val aCore = core(actual).split('.').map { it.toInt() }
val mCore = core(min).split('.').map { it.toInt() }
val maxLen = maxOf(aCore.size, mCore.size)
for (i in 0 until maxLen) {
val ai = aCore.getOrElse(i) { 0 }
val mi = mCore.getOrElse(i) { 0 }
if (ai != mi) return ai > mi
}
// numeric parts equal => alpha/beta is NOT >= final
return suffix(actual) == null
}
override fun connectionState(): String = channel?.getState(false)?.name ?: ConnectivityState.SHUTDOWN.name
}
@@ -16,7 +16,7 @@ object AppEndpointManager : KoinComponent {
val localAppEndpointService: IAppEndpointService by inject(named("localAppEndpointService"))
val remoteAppEndpointService: IAppEndpointService by inject(named("remoteAppEndpointService"))
private lateinit var current: GUIEvent
private var current: GUIEvent = GuiEventBus.Undefined
fun set(event: GUIEvent) {
this.current = event
@@ -4,12 +4,11 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.retryWhen
import me.vripper.gui.event.GuiEventBus
object ChannelFlowBuilder {
fun <T> build(localFlow: () -> Flow<T>, remoteFlow: () -> Flow<T>): Flow<T> {
fun <T> build(localFlow: () -> Flow<T>, remoteFlow: () -> Flow<T>, source: String = "remote"): Flow<T> {
return channelFlow {
var job: Job? = null
@@ -20,10 +19,14 @@ object ChannelFlowBuilder {
}
} else {
launch {
remoteFlow().cancellable().retryWhen { _, _ ->
delay(1000)
true
}.collect { if (isActive) send(it) }
try {
remoteFlow().cancellable().collect { if (isActive) send(it) }
} catch (e: CancellationException) {
throw e
} catch (t: Throwable) {
t.printStackTrace()
GuiEventBus.publishEvent(GuiEventBus.RemoteError(source, t.message ?: t.toString()))
}
}
}
}
@@ -2,11 +2,13 @@ package me.vripper.gui.utils
import javafx.scene.input.Clipboard
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.retryWhen
import me.vripper.gui.event.GuiEventBus
import me.vripper.model.Settings
import me.vripper.services.IAppEndpointService
import me.vripper.utilities.LoggerDelegate
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.qualifier.named
import tornadofx.runLater
object ClipboardManager : KoinComponent {
@@ -14,35 +16,44 @@ object ClipboardManager : KoinComponent {
private var current: String? = null
private var coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var pollJob: Job? = null
private var settingsUpdateJob: Job? = null
private val systemClipboard = Clipboard.getSystemClipboard()
val localAppEndpointService: IAppEndpointService by inject(named("localAppEndpointService"))
val remoteAppEndpointService: IAppEndpointService by inject(named("remoteAppEndpointService"))
var initialized = false
@Synchronized
fun init() {
coroutineScope.launch {
GuiEventBus.events.collect { event ->
when (event) {
GuiEventBus.LocalSession, GuiEventBus.RemoteSession -> {
logger.info("Clipboard manager initialized")
while (isActive) {
if (initialized) {
return
}
runBlocking {
val updateSettings = ChannelFlowBuilder.build(
localAppEndpointService::onUpdateSettings,
remoteAppEndpointService::onUpdateSettings
)
updateSettings.let { flow ->
coroutineScope.launch {
flow.collect {
update(it)
}
}
}
coroutineScope.launch {
GuiEventBus.events.collect { event ->
when (event) {
GuiEventBus.LocalSession, GuiEventBus.RemoteSession -> {
logger.info("Clipboard manager initialized")
val result = runCatching { AppEndpointManager.currentAppEndpointService().getSettings() }
if (result.isSuccess) {
update(result.getOrNull()!!)
break
}
delay(1000)
}
settingsUpdateJob?.cancelAndJoin()
settingsUpdateJob = launch {
AppEndpointManager.currentAppEndpointService().onUpdateSettings()
.retryWhen { _, _ -> delay(1000); true }.collect {
update(it)
}
}
}
else -> {}
else -> {}
}
}
}
initialized = true
}
}
@@ -213,7 +213,14 @@ class GrpcServerAppEndpointService : EndpointServiceGrpcKt.EndpointServiceCorout
enableClipboardMonitoring = request.systemSettings.enableClipboardMonitoring,
clipboardPollingRate = request.systemSettings.clipboardPollingRate,
maxEventLog = request.systemSettings.maxEventLog,
)
),
hostSettings = request.hostSettingsMap.entries.associate { hostSettings ->
HostName.valueOf(hostSettings.key) to hostSettings.value.settingsMap.entries.associate {
HostSettingKey.valueOf(
it.key
) to it.value
}
}
)
)
return EndpointServiceOuterClass.EmptyResponse.getDefaultInstance()
@@ -286,11 +293,22 @@ class GrpcServerAppEndpointService : EndpointServiceGrpcKt.EndpointServiceCorout
build()
}
val hostSettings = settings.hostSettings.entries.associate {
val hostSettings = with(SettingsOuterClass.HostSettingsMap.newBuilder()) {
this.putAllSettings(it.value.entries.associate { setting ->
setting.key.name to setting.value
})
build()
}
it.key.name to hostSettings
}
return with(SettingsOuterClass.Settings.newBuilder()) {
this.connectionSettings = connectionSettings
this.downloadSettings = downloadSettings
this.viperSettings = viperSettings
this.systemSettings = systemSettings
this.putAllHostSettings(hostSettings)
build()
}
}