Compare commits

...
Author SHA1 Message Date
dev-clawandGitHub 39ac5b925b Merge branch 'main' into feature/issue-303/create-host-specific-settings 2026-04-30 22:38:34 +01:00
dev-claw c7d683ff53 fixes #303 2026-04-30 22:35:56 +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 fcc0cecaf3 fixes #303 2026-04-30 21:49:10 +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
dev-claw b28fbb98c2 Update version 2026-04-18 22:00:37 +01:00
dev-clawandGitHub c806eb9d25 Merge pull request #297
* fixes #296
2026-04-18 21:59:57 +01:00
dev-claw 4ba422eee1 Update version 2026-04-18 01:39:04 +01:00
dev-claw e80d704b03 fixes #295
fixes #294
fixes #293
fixes #291
fixes #286
fixes #285
2026-04-18 01:37:49 +01:00
EVgZcQvc8iandGitHub 562c36bf28 Fix open file directory on Linux when the path contains spaces (#282) 2026-04-15 22:09:15 +01:00
dev-claw c3d07a8503 Update release.yml 2025-12-23 14:19:38 +01:00
dev-claw 30c9c9f24d Update version 2025-12-23 14:13:02 +01:00
dev-claw ecf74b7b00 bug fixes 2025-12-23 14:11:22 +01:00
dev-claw ed0c5f76a6 bug fixes 2025-10-12 14:08:00 +01:00
dev-claw 46ddb66dc7 bug fixes 2025-10-11 22:35:40 +01:00
52 changed files with 731 additions and 382 deletions
+6 -6
View File
@@ -8,7 +8,7 @@ jobs:
build:
strategy:
matrix:
os: [ ubuntu-latest, windows-latest, macos-13, macos-latest ]
os: [ ubuntu-latest, windows-latest, macos-15-intel, macos-latest ]
runs-on: ${{ matrix.os }}
permissions:
contents: write
@@ -105,8 +105,8 @@ jobs:
jpackage --app-version ${{ github.event.release.tag_name }} "@jpackage.cfg" "@jpackage-macos.cfg" --resource-dir resources --type dmg
mv dist/VRipper-${{ github.event.release.tag_name }}.pkg dist/vripper-macos-${{ github.event.release.tag_name }}.arm64.pkg
mv dist/VRipper-${{ github.event.release.tag_name }}.dmg dist/vripper-macos-${{ github.event.release.tag_name }}.arm64.dmg
- if: matrix.os == 'macos-13'
- if: matrix.os == 'macos-15-intel'
name: Package for macOS(x86_64)
run: |
cd jpackage
@@ -142,8 +142,8 @@ jobs:
directory: 'jpackage/dist'
path: 'VRipper.app'
filename: 'vripper-macos-portable-${{ github.event.release.tag_name }}.arm64.zip'
- if: matrix.os == 'macos-13'
- if: matrix.os == 'macos-15-intel'
name: Zip macOS(x86_64) portable
uses: thedoctor0/zip-release@0.7.1
with:
@@ -179,7 +179,7 @@ jobs:
jpackage/dist/vripper-macos-${{ github.event.release.tag_name }}.arm64.dmg
jpackage/dist/vripper-macos-portable-${{ github.event.release.tag_name }}.arm64.zip
- if: matrix.os == 'macos-13'
- if: matrix.os == 'macos-15-intel'
name: Release packages for macOS(x86_64)
uses: softprops/action-gh-release@v1
with:
+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.6.1</revision>
<app-version>6.6.1-alpha</app-version>
<revision>6.8.1</revision>
<app-version>6.8.1</app-version>
<kotlin.version>2.2.20</kotlin.version>
<slf4j.version>2.0.16</slf4j.version>
<logback.version>1.5.12</logback.version>
@@ -76,7 +76,7 @@ val coreModule = module {
MetadataService(get(), get())
}
single {
AcidimgHost(get())
AcidimgHost()
} bind Host::class
single {
DPicMeHost()
@@ -2,7 +2,6 @@ package me.vripper.host
import me.vripper.exception.HostException
import me.vripper.exception.XpathException
import me.vripper.services.HTTPService
import me.vripper.services.download.ImageDownloadRunnable.Context
import me.vripper.utilities.HtmlUtils
import me.vripper.utilities.LoggerDelegate
@@ -12,9 +11,7 @@ import org.apache.hc.client5.http.entity.UrlEncodedFormEntity
import org.apache.hc.core5.http.message.BasicNameValuePair
import org.w3c.dom.Node
internal class AcidimgHost(
private val httpService: HTTPService,
) : Host("acidimg.cc", 0) {
internal class AcidimgHost : Host("acidimg.cc", 0) {
private val log by LoggerDelegate()
@Throws(HostException::class)
@@ -31,7 +31,7 @@ internal abstract class Host(
) : KoinComponent {
private val log by LoggerDelegate()
private val httpService: HTTPService by inject()
protected val httpService: HTTPService by inject()
private val dataAccessService: DataAccessService by inject()
private val downloadSpeedService: DownloadSpeedService by inject()
@@ -61,7 +61,7 @@ internal abstract class Host(
val imageMimeType = getImageMimeType(headers)
val downloadedImage = if (imageMimeType != null) {
// a direct link, awesome
val downloadedImage = fetch(context.imageEntity.url, context.imageEntity.url, context) {
val downloadedImage = fetch(context.imageEntity.url, context) {
handleImageDownload(it, context)
}
DownloadedImage(getDefaultImageName(context.imageEntity.url), downloadedImage.first, downloadedImage.second)
@@ -84,7 +84,7 @@ internal abstract class Host(
private fun downloadByHost(context: Context): DownloadedImage {
val resolvedImage = resolve(context)
val downloadImage: Pair<Path, ImageMimeType> =
fetch(resolvedImage.second, context.imageEntity.url, context) {
fetch(resolvedImage.second, context) {
handleImageDownload(it, context)
}
return DownloadedImage(resolvedImage.first, downloadImage.first, downloadImage.second)
@@ -152,6 +152,7 @@ internal abstract class Host(
fun head(context: Context): Array<Header> {
val httpHead = HttpHead(context.imageEntity.url).also {
it.addHeader("Referer", "https://vipergirls.to/")
it.setAbsoluteRequestUri(true)
context.requests.add(it)
}
@@ -169,10 +170,10 @@ internal abstract class Host(
fun <T> fetch(
url: String,
referer: String,
context: Context,
transformer: (ClassicHttpResponse) -> T
): T {
val referer = context.headers["Referer"] ?: "https://vipergirls.to/"
val httpGet =
HttpGet(url).also {
it.addHeader("Referer", referer)
@@ -191,7 +192,7 @@ internal abstract class Host(
url: String,
context: Context
): Document {
return fetch(url, url, context) {
return fetch(url, context) {
HtmlUtils.clean(it.entity.content)
}.also {
if (log.isDebugEnabled) {
@@ -8,7 +8,6 @@ import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.XpathUtils
import org.apache.hc.client5.http.impl.cookie.BasicClientCookie
import org.w3c.dom.Node
import java.sql.Date
import java.time.LocalDateTime
import java.time.ZoneId
import java.util.*
@@ -24,15 +23,20 @@ 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 clientCookie = BasicClientCookie("nsfw_inter", "1")
clientCookie.domain = "www.imagebam.com"
clientCookie.path = "/"
clientCookie.expiryDate =
Date.from(
LocalDateTime.now().plusDays(3).atZone(ZoneId.systemDefault()).toInstant()
)
context.httpContext.cookieStore.addCookie(clientCookie)
fetch(context.imageEntity.url, context.imageEntity.url, context) {
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)
}
} else {
@@ -26,7 +26,7 @@ internal class ImageVenueHost : Host("imagevenue.com", 4) {
)
if (XpathUtils.getAsNode(document, CONTINUE_BUTTON_XPATH) != null) {
// Button detected. No need to actually click it, just make the call again.
fetch(context.imageEntity.url, context.imageEntity.url, context) {
fetch(context.imageEntity.url, context) {
HtmlUtils.clean(it.entity.content)
}
} else {
@@ -2,8 +2,15 @@ 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
import me.vripper.utilities.XpathUtils
import org.apache.hc.client5.http.classic.methods.HttpPost
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity
import org.apache.hc.core5.http.message.BasicNameValuePair
internal class ImxHost : Host("imx.to", 8) {
@@ -15,15 +22,72 @@ 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 = String.format("IMG_%04d", context.imageEntity.index + 1)
val imgTitle = getTitle(context)
val imgUrl = findPattern(context.imageEntity)
return Pair(
imgTitle.ifEmpty { getDefaultImageName(imgUrl) }, imgUrl
imgTitle, imgUrl
)
}
private fun findPattern(image: ImageEntity): String = image.thumbUrl
.replace("http:", "https:")
.replace("upload/small/", "u/i/")
.replace("u/t/", "u/i/")
private fun getTitle(context: ImageDownloadRunnable.Context): String {
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
log.debug("Looking for xpath expression $CONTINUE_BUTTON_XPATH in $httpsUrl")
val contDiv = XpathUtils.getAsNode(document, CONTINUE_BUTTON_XPATH)
?: throw HostException("$CONTINUE_BUTTON_XPATH cannot be found")
val node = contDiv.attributes.getNamedItem("value")
if (node != null) {
value = node.textContent
}
log.debug("Click button found for $httpsUrl")
val httpPost: HttpPost = HttpPost(httpsUrl).also {
it.entity = UrlEncodedFormEntity(listOf(BasicNameValuePair("imgContinue", value)))
}.also { context.requests.add(it) }
log.debug("Requesting {}", httpPost)
val doc = httpService.client.execute(
httpPost, context.httpContext
) { response ->
log.debug("Cleaning response for {}", httpPost)
HtmlUtils.clean(response.entity.content)
}
log.debug("Looking for xpath expression $IMG_XPATH in $httpsUrl")
val imgNode = XpathUtils.getAsNode(doc, IMG_XPATH)
log.debug("Resolving name for $httpsUrl")
val imgTitle = imgNode?.attributes?.getNamedItem("alt")?.textContent?.trim() ?: ""
imgTitle
} else {
getDefaultImageName(context.imageEntity.thumbUrl)
}
}
private fun findPattern(image: ImageEntity): String {
val url = image.thumbUrl
.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://image.imx.to/u/i/")) {
url
} else {
throw HostException("Cannot find pattern for url ${image.thumbUrl}")
}
}
companion object {
private const val CONTINUE_BUTTON_XPATH = "//*[@name='imgContinue']"
private const val IMG_XPATH = "//img[@class='centred']"
}
}
@@ -26,7 +26,7 @@ internal class PimpandhostHost : Host("pimpandhost.com", 9) {
} catch (e: Exception) {
throw HostException(e)
}
val doc = fetch(newUrl, context.imageEntity.url, context) {
val doc = fetch(newUrl, context) {
HtmlUtils.clean(it.entity.content)
}
val imgNode: Node = try {
@@ -1,12 +1,9 @@
package me.vripper.host
import me.vripper.exception.HostException
import me.vripper.exception.XpathException
import me.vripper.services.download.ImageDownloadRunnable
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.XpathUtils
import org.w3c.dom.Node
import java.util.*
internal class PostImgHost : Host("postimg.cc", 13) {
private val log by LoggerDelegate()
@@ -15,44 +12,22 @@ internal class PostImgHost : Host("postimg.cc", 13) {
override fun resolve(
context: ImageDownloadRunnable.Context
): Pair<String, String> {
val document = fetchDocument(context.imageEntity.url, context)
val titleNode = try {
log.debug(String.format("Looking for xpath expression %s in %s", TITLE_XPATH, context.imageEntity.url))
XpathUtils.getAsNode(document, TITLE_XPATH)
} catch (e: XpathException) {
throw HostException(e)
} ?: throw HostException(
String.format(
"Xpath '%s' cannot be found in '%s'",
TITLE_XPATH,
context.imageEntity.url
)
)
val urlNode = try {
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, context.imageEntity.url))
XpathUtils.getAsNode(document, IMG_XPATH)
} catch (e: XpathException) {
throw HostException(e)
} ?: throw HostException(
val document = fetchDocument(context.imageEntity.url.replace("http:", "https:"), context)
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, context.imageEntity.url))
val node = XpathUtils.getAsNode(document, IMG_XPATH) ?: throw HostException(
String.format(
"Xpath '%s' cannot be found in '%s'",
IMG_XPATH,
context.imageEntity.url
)
)
return try {
log.debug(String.format("Resolving name and image url for %s", context.imageEntity.url))
val imgTitle = Optional.ofNullable(titleNode)
.map { node: Node -> node.textContent.trim { it <= ' ' } }
.orElseGet { getDefaultImageName(context.imageEntity.url) }
Pair(imgTitle, urlNode.attributes.getNamedItem("href").textContent.trim { it <= ' ' })
} catch (e: Exception) {
throw HostException("Unexpected error occurred", e)
}
return Pair(
node.attributes.getNamedItem("alt").textContent.trim(),
node.attributes.getNamedItem("src").textContent.trim()
)
}
companion object {
private const val TITLE_XPATH = "//span[contains(@class,'imagename')]"
private const val IMG_XPATH = "//a[@id='download']"
private const val IMG_XPATH = "//img[contains(@class,'img-fluid')]"
}
}
@@ -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)
@@ -0,0 +1,3 @@
package me.vripper.model
data class PostIdentifier(val siteProxy: String, val threadId: Long, val postId: Long)
@@ -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 "true")
)
)
@Serializable
@@ -51,3 +66,4 @@ data class SystemSettings(
val clipboardPollingRate: Int = 500,
val maxEventLog: Int = 1_000,
)
@@ -14,11 +14,8 @@ import me.vripper.services.download.MovePosition
import me.vripper.services.download.QueueManager
import me.vripper.tasks.AddPostTask
import me.vripper.tasks.ThreadLookupTask
import me.vripper.utilities.ApplicationProperties
import me.vripper.utilities.*
import me.vripper.utilities.ApplicationProperties.VRIPPER_DIR
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.PathUtils
import me.vripper.utilities.taskRunner
import org.h2.jdbc.JdbcSQLNonTransientConnectionException
import java.sql.DriverManager
import java.time.Duration
@@ -50,15 +47,17 @@ internal class AppEndpointService(
}
val urlList = postLinks.split(Pattern.compile("\\r?\\n")).dropLastWhile { it.isBlank() }.map { it.trim() }
.filter { it.isNotEmpty() }
val proxies = settingsService.getProxies()
for (link in urlList) {
log.debug("Scanning: $link")
if (!link.startsWith(settingsService.settings.viperSettings.host)) {
val matchingProxy = proxies.find { link.startsWith(it) }
if (matchingProxy == null) {
continue
}
var threadId: Long
var postId: Long?
val m = Pattern.compile(
Pattern.quote(settingsService.settings.viperSettings.host) + "/threads/(\\d+)((.*p=)(\\d+))?"
Pattern.quote(matchingProxy) + "/threads/(\\d+)((.*p=)(\\d+))?"
).matcher(link)
if (m.find()) {
threadId = m.group(1).toLong()
@@ -66,13 +65,13 @@ internal class AppEndpointService(
if (postId == null) {
taskRunner.submit(
ThreadLookupTask(
threadId, settingsService.settings
matchingProxy, threadId, settingsService.settings
)
)
} else {
taskRunner.submit(
AddPostTask(
listOf(ThreadPostId(threadId, postId))
listOf(PostIdentifier(matchingProxy, threadId, postId))
)
)
}
@@ -92,7 +91,13 @@ internal class AppEndpointService(
}
override suspend fun download(posts: List<ThreadPostId>) {
taskRunner.submit(AddPostTask(posts))
posts.map {
val thread = dataAccessService.findThreadByThreadId(it.threadId).getOrNull()
?: throw PostParseException("Could not find thread with id ${it.threadId}")
PostIdentifier(thread.link.extractBaseUrl(), it.threadId, it.postId)
}.also {
taskRunner.submit(AddPostTask(it))
}
}
override suspend fun stopAll(postEntityIds: List<Long>) {
@@ -9,6 +9,7 @@ import me.vripper.data.repositories.ThreadRepository
import me.vripper.entities.*
import me.vripper.event.*
import me.vripper.model.ErrorCount
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.PathUtils
import me.vripper.utilities.PathUtils.sanitize
import me.vripper.vgapi.PostItem
@@ -26,6 +27,8 @@ internal class DataAccessService(
private val eventBus: EventBus,
) {
private val log by LoggerDelegate()
private val postEntityIdCache: LoadingCache<Long, PostEntity> =
Caffeine.newBuilder().expireAfterAccess(5, TimeUnit.MINUTES).build { id ->
transaction { postRepository.findById(id) }
@@ -40,9 +43,10 @@ internal class DataAccessService(
val savedPost =
postRepository.save(listOf(postEntity)).first()
save(images.map { it.copy(postEntityId = savedPost.id) })
// Publish event inside transaction for consistency
eventBus.publishEvent(PostCreateEvent(listOf(savedPost)))
savedPost
}
eventBus.publishEvent(PostCreateEvent(listOf(savedPost)))
return savedPost
}
@@ -51,27 +55,34 @@ internal class DataAccessService(
postEntities.forEach { postEntity ->
postEntityIdCache.put(postEntity.id, postEntity)
}
// Log and publish event after transaction commit but before returning
log.debug("[{}] Publishing event: PostUpdateEvent for {} posts", System.currentTimeMillis(), postEntities.size)
eventBus.publishEvent(PostUpdateEvent(postEntities))
}
fun updatePost(postEntity: PostEntity) {
transaction { postRepository.update(postEntity) }
postEntityIdCache.put(postEntity.id, postEntity)
// Log and publish event after transaction commit but before returning
log.debug("[{}] Publishing event: PostUpdateEvent for post {}", System.currentTimeMillis(), postEntity.id)
eventBus.publishEvent(PostUpdateEvent(listOf(postEntity)))
}
fun save(threadEntity: ThreadEntity) {
val savedThread = transaction { threadRepository.save(threadEntity) }
log.debug("[{}] Publishing event: ThreadCreateEvent for thread {}", System.currentTimeMillis(), savedThread.id)
eventBus.publishEvent(ThreadCreateEvent(savedThread))
}
fun update(threadEntity: ThreadEntity) {
transaction { threadRepository.update(threadEntity) }
log.debug("[{}] Publishing event: ThreadUpdateEvent for thread {}", System.currentTimeMillis(), threadEntity.id)
eventBus.publishEvent(ThreadUpdateEvent(threadEntity))
}
fun updateImages(imageEntities: List<ImageEntity>) {
transaction { imageRepository.update(imageEntities) }
log.debug("[{}] Publishing event: ImageEvent for {} images", System.currentTimeMillis(), imageEntities.size)
eventBus.publishEvent(ImageEvent(imageEntities))
}
@@ -81,6 +92,7 @@ internal class DataAccessService(
imageRepository.update(imageEntity)
}
}
log.debug("[{}] Publishing event: ImageEvent for image {}", System.currentTimeMillis(), imageEntity.id)
eventBus.publishEvent(ImageEvent(listOf(imageEntity)))
}
@@ -183,12 +195,15 @@ internal class DataAccessService(
postEntityIdCache.get(postEntityId)?.let { postEntityIdCache.invalidate(it.id) }
postEntityIdCache.invalidate(postEntityId)
}
log.debug("[{}] Publishing event: PostDeleteEvent for {} posts", System.currentTimeMillis(), postEntityIds.size)
eventBus.publishEvent(PostDeleteEvent(postEntityIds = postEntityIds))
log.debug("[{}] Publishing event: ErrorCountEvent after post deletion", System.currentTimeMillis())
eventBus.publishEvent(ErrorCountEvent(ErrorCount(countImagesInError())))
}
fun removeThread(threadId: Long) {
transaction { threadRepository.deleteByThreadId(threadId) }
log.debug("[{}] Publishing event: ThreadDeleteEvent for thread {}", System.currentTimeMillis(), threadId)
eventBus.publishEvent(ThreadDeleteEvent(threadId))
}
@@ -216,11 +231,17 @@ internal class DataAccessService(
fun saveMetadata(metadataEntity: MetadataEntity) {
transaction { metadataRepository.save(metadataEntity) }
log.debug(
"[{}] Publishing event: MetadataUpdateEvent for post {}",
System.currentTimeMillis(),
metadataEntity.postIdRef
)
eventBus.publishEvent(MetadataUpdateEvent(metadataEntity))
}
fun clearQueueLinks() {
transaction { threadRepository.deleteAll() }
log.debug("[{}] Publishing event: ThreadClearEvent", System.currentTimeMillis())
eventBus.publishEvent(ThreadClearEvent())
}
@@ -6,6 +6,7 @@ import me.vripper.event.DownloadSpeedEvent
import me.vripper.event.EventBus
import me.vripper.event.QueueStateEvent
import me.vripper.model.DownloadSpeed
import me.vripper.utilities.LoggerDelegate
import java.util.concurrent.atomic.AtomicLong
internal class DownloadSpeedService(
@@ -16,6 +17,7 @@ internal class DownloadSpeedService(
const val DOWNLOAD_POLL_RATE = 2500
}
private val log by LoggerDelegate()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val bytesCount = AtomicLong(0)
private var job: Job? = null
@@ -32,7 +34,13 @@ internal class DownloadSpeedService(
while (isActive) {
delay(DOWNLOAD_POLL_RATE.toLong())
val newValue = bytesCount.getAndSet(0)
eventBus.publishEvent(DownloadSpeedEvent(DownloadSpeed(((newValue * 1000) / DOWNLOAD_POLL_RATE))))
val speed = DownloadSpeed(((newValue * 1000) / DOWNLOAD_POLL_RATE))
log.debug(
"[{}] Publishing event: DownloadSpeedEvent({})",
System.currentTimeMillis(),
speed
)
eventBus.publishEvent(DownloadSpeedEvent(speed))
}
}
}
@@ -40,6 +48,7 @@ internal class DownloadSpeedService(
job?.cancel()
coroutineScope.launch {
delay(DOWNLOAD_POLL_RATE + 500L)
log.debug("[{}] Publishing event: DownloadSpeedEvent(0)", System.currentTimeMillis())
eventBus.publishEvent(DownloadSpeedEvent(DownloadSpeed(0L)))
}
}
@@ -2,29 +2,21 @@ package me.vripper.services
import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.LoadingCache
import me.vripper.utilities.extractBaseUrl
import me.vripper.vgapi.ThreadItem
import me.vripper.vgapi.ThreadLookupAPIParser
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeUnit
import kotlin.jvm.optionals.getOrNull
internal class ThreadCacheService(val dataAccessService: DataAccessService) {
private val cache: LoadingCache<Long, ThreadItem> =
Caffeine.newBuilder().expireAfterWrite(20, TimeUnit.MINUTES).build { threadId ->
val threadItem = ThreadLookupAPIParser(threadId).parse()
dataAccessService.findThreadByThreadId(threadItem.threadId).ifPresent {
if (threadItem.postItemList.isNotEmpty()) {
dataAccessService.update(it.copy(total = threadItem.postItemList.size))
}
}
threadItem
}
Caffeine.newBuilder().expireAfterWrite(20, TimeUnit.MINUTES).build(::cacheLoader)
fun invalidate() {
cache.invalidateAll()
}
@Throws(ExecutionException::class)
operator fun get(threadId: Long): ThreadItem {
return cache[threadId]
}
@@ -32,4 +24,31 @@ internal class ThreadCacheService(val dataAccessService: DataAccessService) {
fun getIfPresent(threadId: Long): ThreadItem? {
return cache.getIfPresent(threadId)
}
fun loadThenCache(threadId: Long, siteProxy: String): ThreadItem {
return cache.get(threadId) {
cacheLoader(threadId, siteProxy)
}
}
private fun cacheLoader(threadId: Long, siteProxy: String? = null): ThreadItem? {
val result = if (siteProxy != null) {
ThreadLookupAPIParser(siteProxy, threadId).parse()
} else {
dataAccessService.findThreadByThreadId(threadId).map { threadEntity ->
ThreadLookupAPIParser(threadEntity.link.extractBaseUrl(), threadId).parse()
}.getOrNull()
}
if (result == null) {
throw Exception("Failed to load thread $threadId")
}
dataAccessService.findThreadByThreadId(result.threadId).ifPresent {
if (result.postItemList.isNotEmpty()) {
dataAccessService.update(it.copy(total = result.postItemList.size))
}
}
return result
}
}
@@ -29,11 +29,14 @@ internal class VGAuthService(
fun authenticate(settings: Settings) {
if (!settings.viperSettings.login) {
log.debug("Authentication option is disabled")
authenticated = false
loggedUser = ""
synchronized(this) {
authenticated = false
loggedUser = ""
}
synchronized(vgCookies) {
vgCookies.clear()
}
log.debug("[{}] Publishing event: VGUserLoginEvent", System.currentTimeMillis())
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
return
}
@@ -41,11 +44,14 @@ internal class VGAuthService(
val password = settings.viperSettings.password
if (username.isEmpty() || password.isEmpty()) {
log.error("Cannot authenticate with ViperGirls credentials, username or password is empty")
authenticated = false
loggedUser = ""
synchronized(this) {
authenticated = false
loggedUser = ""
}
synchronized(vgCookies) {
vgCookies.clear()
}
log.debug("[{}] Publishing event: VGUserLoginEvent (empty credentials)", System.currentTimeMillis())
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
return
}
@@ -82,6 +88,7 @@ internal class VGAuthService(
"Failed to authenticate user with {}, missing vg_userid/vg_password cookie",
settings.viperSettings.host
)
log.debug("[${System.currentTimeMillis()}] Publishing event: VGUserLoginEvent (missing cookies)")
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
return
}
@@ -98,15 +105,21 @@ internal class VGAuthService(
log.error(
"Failed to authenticate user with " + settings.viperSettings.host, e
)
authenticated = false
loggedUser = ""
synchronized(this) {
authenticated = false
loggedUser = ""
}
log.debug("[{}] Publishing event: VGUserLoginEvent (exception)", System.currentTimeMillis())
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
return
}
authenticated = true
loggedUser = username
synchronized(this) {
authenticated = true
loggedUser = username
}
log.debug("[{}] Publishing event: VGUserLoginEvent for user {}", System.currentTimeMillis(), username)
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
log.info("Successfully logged in as: $loggedUser")
log.info("Successfully logged in as: {}", loggedUser)
}
fun leaveThanks(postEntity: PostEntity) {
@@ -54,12 +54,20 @@ internal class DownloadService(
}
fun stop(postEntityIds: List<Long> = emptyList()) {
if (postEntityIds.isNotEmpty()) {
stopInternal(postEntityIds)
eventBus.publishEvent(StoppedEvent(postEntityIds))
} else {
stopAll()
eventBus.publishEvent(StoppedEvent(listOf(-1)))
downloadManagerLock.withLock {
if (postEntityIds.isNotEmpty()) {
stopInternal(postEntityIds)
log.debug(
"[{}] Publishing event: StoppedEvent for {} posts",
System.currentTimeMillis(),
postEntityIds.size
)
eventBus.publishEvent(StoppedEvent(postEntityIds))
} else {
stopAll()
log.debug("[{}] Publishing event: StoppedEvent for all posts", System.currentTimeMillis())
eventBus.publishEvent(StoppedEvent(listOf(-1)))
}
}
}
@@ -125,24 +133,20 @@ internal class DownloadService(
}
private fun stopAll() {
downloadManagerLock.withLock {
queueManager.clearPending()
queueManager.clearRunning()
dataAccessService.findAllNonCompletedPostEntityIds().forEach {
dataAccessService.stopImagesByPostEntityIdAndIsNotCompleted(it)
dataAccessService.finishPost(it)
}
queueManager.clearPending()
queueManager.clearRunning()
dataAccessService.findAllNonCompletedPostEntityIds().forEach {
dataAccessService.stopImagesByPostEntityIdAndIsNotCompleted(it)
dataAccessService.finishPost(it)
}
}
private fun stopInternal(postEntityIds: List<Long>) {
downloadManagerLock.withLock {
postEntityIds.forEach {
queueManager.clearPending(it)
queueManager.clearRunning(it)
dataAccessService.stopImagesByPostEntityIdAndIsNotCompleted(it)
dataAccessService.finishPost(it)
}
postEntityIds.forEach {
queueManager.clearPending(it)
queueManager.clearRunning(it)
dataAccessService.stopImagesByPostEntityIdAndIsNotCompleted(it)
dataAccessService.finishPost(it)
}
}
@@ -49,6 +49,7 @@ internal class ImageDownloadRunnable(
cookieStore = HTTPService.cookieStore
}
val requests = mutableListOf<HttpUriRequestBase>()
val headers = mutableMapOf<String, String>()
init {
ApplicationProperties.VRIPPER_DIR.listDirectoryEntries()
@@ -66,19 +66,21 @@ internal class QueueManager(
}
fun accept(accepted: List<ImageQueueElement>) {
pending.forEach {
it.removeAll(accepted)
downloadManagerLock.withLock {
pending.forEach {
it.removeAll(accepted)
}
pending.removeIf { it.isEmpty() }
accepted.map {
ImageDownloadRunnable(
dataAccessService.findImageById(it.imageEntityId).orElseThrow(), settingsService.settings.copy()
)
}.forEach {
launch(it)
running.add(it)
}
reportQueueState()
}
pending.removeIf { it.isEmpty() }
accepted.map {
ImageDownloadRunnable(
dataAccessService.findImageById(it.imageEntityId).orElseThrow(), settingsService.settings.copy()
)
}.forEach {
launch(it)
running.add(it)
}
reportQueueState()
}
fun pending(): List<ImageQueueElement> {
@@ -121,8 +123,8 @@ internal class QueueManager(
}
}
}
reportQueueState()
}
reportQueueState()
}
fun getQueueState(): QueueState {
@@ -146,8 +148,6 @@ internal class QueueManager(
dataAccessService.updateImage(image)
}.onComplete {
afterJobFinish(runnable)
reportQueueState()
eventBus.publishEvent(ErrorCountEvent(ErrorCount(dataAccessService.countImagesInError())))
log.debug(
"Finished downloading ${runnable.context.imageEntity.url}"
)
@@ -161,12 +161,21 @@ internal class QueueManager(
if (!isPending(image.postEntityId) && !isRunning(image.postEntityId) && !imageDownloadRunnable.stopped) {
dataAccessService.finishPost(image.postEntityId, true)
}
reportQueueState()
log.debug("[{}] Event published: ErrorCountEvent after job finish", System.currentTimeMillis())
eventBus.publishEvent(ErrorCountEvent(ErrorCount(dataAccessService.countImagesInError())))
downloadManagerCondition.signal()
}
}
private fun reportQueueState() {
val queueState = getQueueState()
log.debug(
"[{}] Publishing event: QueueStateEvent(running={}, remaining={})",
System.currentTimeMillis(),
queueState.running,
queueState.remaining
)
eventBus.publishEvent(QueueStateEvent(queueState))
}
}
@@ -1,6 +1,6 @@
package me.vripper.tasks
import me.vripper.model.ThreadPostId
import me.vripper.model.PostIdentifier
import me.vripper.services.DataAccessService
import me.vripper.services.MetadataService
import me.vripper.services.SettingsService
@@ -12,7 +12,7 @@ import me.vripper.vgapi.PostLookupAPIParser
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
internal class AddPostTask(private val items: List<ThreadPostId>) : KoinComponent, Runnable {
internal class AddPostTask(private val items: List<PostIdentifier>) : KoinComponent, Runnable {
private val log by LoggerDelegate()
private val dataAccessService: DataAccessService by inject()
private val settingsService: SettingsService by inject()
@@ -24,19 +24,19 @@ internal class AddPostTask(private val items: List<ThreadPostId>) : KoinComponen
try {
Tasks.increment()
val toProcess = mutableListOf<PostItem>()
for ((threadId, postId) in items) {
for ((siteProxy, threadId, postId) in items) {
if (dataAccessService.existsPostId(postId)) {
log.info("Post $postId already loaded")
continue
}
val link =
"${settingsService.settings.viperSettings.host}/threads/$threadId?p=$postId&viewfull=1#post$postId"
"$siteProxy/threads/$threadId?p=$postId&viewfull=1#post$postId"
val cachedThread = threadCacheService.getIfPresent(threadId)
val threadItem = cachedThread ?:
PostLookupAPIParser(
threadId, postId
siteProxy, threadId, postId
).parse()
@@ -1,28 +1,30 @@
package me.vripper.tasks
import me.vripper.entities.ThreadEntity
import me.vripper.model.PostIdentifier
import me.vripper.model.Settings
import me.vripper.model.ThreadPostId
import me.vripper.services.DataAccessService
import me.vripper.services.SettingsService
import me.vripper.services.ThreadCacheService
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.taskRunner
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
internal class ThreadLookupTask(private val threadId: Long, private val settings: Settings) : KoinComponent, Runnable {
internal class ThreadLookupTask(
private val siteProxy: String,
private val threadId: Long,
private val settings: Settings
) : KoinComponent, Runnable {
private val log by LoggerDelegate()
private val dataAccessService by inject<DataAccessService>()
private val settingsService by inject<SettingsService>()
private val threadCacheService by inject<ThreadCacheService>()
private val link: String = "${settingsService.settings.viperSettings.host}/threads/$threadId"
private val link: String = "$siteProxy/threads/$threadId"
override fun run() {
try {
Tasks.increment()
if (dataAccessService.findThreadByThreadId(threadId).isEmpty) {
val threadLookupResult = threadCacheService[threadId]
val threadLookupResult = threadCacheService.loadThenCache(threadId, siteProxy)
if (threadLookupResult.error.isNotBlank()) {
log.error("Error loading $link: ${threadLookupResult.error}")
return
@@ -35,8 +37,8 @@ internal class ThreadLookupTask(private val threadId: Long, private val settings
if (threadLookupResult.postItemList.size <= settings.downloadSettings.autoQueueThreshold) {
taskRunner.submit(
AddPostTask(threadLookupResult.postItemList.map {
ThreadPostId(
it.threadId, it.postId
PostIdentifier(
siteProxy, it.threadId, it.postId
)
})
)
@@ -36,3 +36,7 @@ fun String.hash256(): String {
val digest = md.digest(bytes)
return digest.fold("") { str, it -> str + "%02x".format(it) }
}
fun String.extractBaseUrl(): String {
return this.replaceFirst(Regex("(https?://[^/]+).*"), "$1")
}
@@ -18,7 +18,11 @@ import org.koin.core.component.inject
import java.io.ByteArrayInputStream
import javax.xml.parsers.SAXParserFactory
internal class PostLookupAPIParser(private val threadId: Long, private val postId: Long) : KoinComponent {
internal class PostLookupAPIParser(
private val siteProxy: String,
private val threadId: Long,
private val postId: Long
) : KoinComponent {
private val log by LoggerDelegate()
private val retryPolicyService: RetryPolicyService by inject()
private val httpService: HTTPService by inject()
@@ -33,7 +37,7 @@ internal class PostLookupAPIParser(private val threadId: Long, private val postI
"p", postId.toString()
)
}.build()).also { it.setAbsoluteRequestUri(true) }
val threadLookupAPIResponseHandler = ThreadLookupAPIResponseHandler()
val threadLookupAPIResponseHandler = ThreadLookupAPIResponseHandler(siteProxy)
Tasks.increment()
return try {
Failsafe.with(retryPolicyService.buildRetryPolicy<Any>("Failed to parse $httpGet: ")).onFailure {
@@ -18,7 +18,7 @@ import org.koin.core.component.inject
import java.io.ByteArrayInputStream
import javax.xml.parsers.SAXParserFactory
internal class ThreadLookupAPIParser(private val threadId: Long) : KoinComponent {
internal class ThreadLookupAPIParser(private val siteProxy: String, private val threadId: Long) : KoinComponent {
private val log by LoggerDelegate()
private val cm: HTTPService by inject()
private val retryPolicyService: RetryPolicyService by inject()
@@ -34,7 +34,7 @@ internal class ThreadLookupAPIParser(private val threadId: Long) : KoinComponent
threadId.toString()
)
}.build()).also { it.setAbsoluteRequestUri(true) }
val threadLookupAPIResponseHandler = ThreadLookupAPIResponseHandler()
val threadLookupAPIResponseHandler = ThreadLookupAPIResponseHandler(siteProxy)
Tasks.increment()
return try {
Failsafe.with(retryPolicyService.buildRetryPolicy<Any>("Failed to parse $httpGet: ")).onFailure {
@@ -1,15 +1,12 @@
package me.vripper.vgapi
import me.vripper.host.Host
import me.vripper.services.SettingsService
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.xml.sax.Attributes
import org.xml.sax.helpers.DefaultHandler
internal class ThreadLookupAPIResponseHandler : KoinComponent, DefaultHandler() {
internal class ThreadLookupAPIResponseHandler(private val siteProxy: String) : KoinComponent, DefaultHandler() {
private val supportedHosts: List<Host> = getKoin().getAll()
private val settingsService: SettingsService by inject()
private var error: String = ""
private val hostMap: MutableMap<Host, Int> = mutableMapOf()
private val postItemList: MutableList<PostItem> = mutableListOf()
@@ -74,7 +71,7 @@ internal class ThreadLookupAPIResponseHandler : KoinComponent, DefaultHandler()
postCounter,
postTitle,
imageItemList.size,
"${settingsService.settings.viperSettings.host}/threads/$threadId?p=$postId&viewfull=1#post$postId",
"$siteProxy/threads/$threadId?p=$postId&viewfull=1#post$postId",
hostMap.toMap().map { Pair(it.key.hostName, it.value) },
securityToken,
forum,
@@ -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;
}
@@ -31,8 +31,10 @@ class AddLinksFragment : Fragment("Add thread links") {
action {
coroutineScope.launch {
postController.scan(textAreaProperty.value)
runLater {
close()
}
}
close()
}
}
}
@@ -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)
})
}
}
}
}
}
}
}
}
}
}
@@ -63,6 +63,9 @@ class SessionFragment : Fragment("Change Session") {
addClass(Styles.ACCENT)
isDefaultButton = true
action {
runBlocking {
GuiEventBus.publishEvent(GuiEventBus.ChangingSession)
}
val selectedToggle = toggleGroup.selectedToggle
runLater {
find<AppView>().replaceWith(find<LoadingView>())
@@ -80,7 +83,6 @@ class SessionFragment : Fragment("Change Session") {
else -> VripperGuiApplication.APP_INSTANCE.stop()
}
runBlocking {
GuiEventBus.publishEvent(GuiEventBus.ChangingSession)
GuiEventBus.publishEvent(GuiEventBus.ApplicationInitialized(emptyList()))
}
close()
@@ -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()
}
@@ -50,13 +50,11 @@ class LoadingView : View("VRipper") {
message.set("Unable to connect to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}")
return@collect
} else if (!check) {
println("Version Mismatch")
message.set("Version Mismatch, client must be >= 6.6.0")
return@collect
}
}
runLater {
replaceWith(find<AppView>())
val sessionType = if (widgetsController.currentSettings.localSession) {
AppManager.start()
GuiEventBus.LocalSession
@@ -64,8 +62,8 @@ class LoadingView : View("VRipper") {
GuiEventBus.RemoteSession
}
AppEndpointManager.set(sessionType)
replaceWith(find<AppView>())
runBlocking {
println("Publishing $sessionType")
GuiEventBus.publishEvent(sessionType)
}
}
@@ -153,40 +153,9 @@ class LogTableView : View() {
tableView.placeholder = Label("Loading")
tableView.sortOrder.add(tableView.columns.first { it.id == "time" })
coroutineScope.launch {
launch {
GuiEventBus.events.collect {
when (it) {
GuiEventBus.LocalSession, GuiEventBus.RemoteSession -> {
println("Collecting $it from LogTableView")
while (isActive) {
val result = runCatching { logController.getMaxEventLog() }
if (result.isSuccess) {
maxLogEvent = result.getOrNull()!!
break
}
}
while (isActive) {
val result = runCatching { logController.initLogger() }
if (result.isSuccess) {
break
}
}
}
GuiEventBus.ChangingSession -> runLater {
items.clear()
tableView.placeholder = Label("Loading")
}
else -> {}
}
}
}
launch {
logController.newLogs.collect {
logController.newLogs.let { flow ->
coroutineScope.launch {
flow.collect {
runLater {
items.sortWith(Comparator.comparing { it.sequence })
while (items.isNotEmpty() && (items.size >= maxLogEvent)) {
@@ -197,14 +166,44 @@ class LogTableView : View() {
}
}
}
}
launch {
logController.updateSettings.collect {
logController.updateSettings.let { flow ->
coroutineScope.launch {
flow.collect {
maxLogEvent = it.systemSettings.maxEventLog
}
}
}
println("${this.javaClass.name} init")
coroutineScope.launch {
GuiEventBus.events.collect {
when (it) {
GuiEventBus.LocalSession, GuiEventBus.RemoteSession -> {
while (isActive) {
val result = runCatching { logController.getMaxEventLog() }
if (result.isSuccess) {
maxLogEvent = result.getOrNull()!!
break
}
}
while (isActive) {
val result = runCatching { logController.initLogger() }
if (result.isSuccess) {
break
}
}
}
GuiEventBus.ChangingSession -> runLater {
items.clear()
tableView.placeholder = Label("Loading")
}
else -> {}
}
}
}
}
private fun openLog(item: LogModel) {
@@ -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()
}
@@ -269,13 +271,15 @@ class MenuBarView : View() {
}
}
downloadActiveProperty.bind(running.greaterThan(0))
coroutineScope.launch {
actionBarController.onQueueStateUpdate.collect {
runLater {
running.set(it.running)
actionBarController.onQueueStateUpdate.let { flow ->
coroutineScope.launch {
flow.collect {
runLater {
running.set(it.running)
}
}
}
}
}
}
@@ -18,7 +18,7 @@ class PostInfoView : View() {
private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val imagesTableView: ImagesTableView by inject()
private val postModel: PostModel = PostModel(
-1, -1, "", 0.0, "", "", 0, 0, "", "", "*", "", "", "", emptyList(), emptyList(), "", 0
-1, -1, "", 0.0, "", "", 0, 0, "", "", -1, "", "", "", emptyList(), emptyList(), "", 0
)
override val root = tabpane()
@@ -89,7 +89,7 @@ class PostInfoView : View() {
this.total = 0
this.hosts = ""
this.addedOn = ""
this.order = "*"
this.order = -1
this.path = ""
this.folderName = ""
this.progressCount = ""
@@ -127,32 +127,37 @@ class PostInfoView : View() {
}
}
}
coroutineScope.launch {
postController.updatePostsFlow.filter {
it.id == postModel.id
}.collect { post ->
runLater {
postModel.status = post.status.stringValue.lowercase().replaceFirstChar { it.uppercase() }
postModel.progressCount = postController.progressCount(
post.total, post.done, post.downloaded
)
postModel.done = post.done
postModel.progress = postController.progress(
post.total, post.done
)
postModel.path = post.getDownloadFolder()
postModel.folderName = post.folderName
postController.updatePostsFlow.let { flow ->
coroutineScope.launch {
flow.filter {
it.id == postModel.id
}.collect { post ->
runLater {
postModel.status = post.status.stringValue.lowercase().replaceFirstChar { it.uppercase() }
postModel.progressCount = postController.progressCount(
post.total, post.done, post.downloaded
)
postModel.done = post.done
postModel.progress = postController.progress(
post.total, post.done
)
postModel.path = post.getDownloadFolder()
postModel.folderName = post.folderName
}
}
}
}
coroutineScope.launch {
postController.updateMetadataFlow.filter {
it.postIdRef == postModel.id
}.collect {
runLater {
postModel.altTitles = FXCollections.observableArrayList(it.data.resolvedNames)
postModel.postedBy = it.data.postedBy
postController.updateMetadataFlow.let { flow ->
coroutineScope.launch {
flow.filter {
it.postIdRef == postModel.id
}.collect {
runLater {
postModel.altTitles = FXCollections.observableArrayList(it.data.resolvedNames)
postModel.postedBy = it.data.postedBy
}
}
}
}
@@ -64,6 +64,5 @@ class PostsTabView : View() {
delay(1_000)
}
}
println("${this.javaClass.name} init")
}
}
@@ -14,7 +14,6 @@ import javafx.scene.input.MouseButton
import javafx.util.Callback
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.javafx.asFlow
import me.vripper.gui.components.Shared
import me.vripper.gui.components.cells.PreviewTableCell
@@ -51,12 +50,10 @@ class PostsTableView : View() {
init {
items.filterWhen(Shared.searchInput) { query, item ->
item.title.contains(query, ignoreCase = true)
|| item.vgPostId.toString().contains(query)
|| item.vgThreadId.toString().contains(query)
|| item.hosts.contains(query, ignoreCase = true)
|| item.status.contains(query, ignoreCase = true)
|| item.path.contains(query, ignoreCase = true)
item.title.contains(query, ignoreCase = true) || item.vgPostId.toString()
.contains(query) || item.vgThreadId.toString().contains(query) || item.hosts.contains(
query, ignoreCase = true
) || item.status.contains(query, ignoreCase = true) || item.path.contains(query, ignoreCase = true)
}
with(root) {
@@ -206,7 +203,38 @@ class PostsTableView : View() {
}
}
cellFactory = Callback {
TextFieldTableCell<PostModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
object : TableCell<PostModel, Number>() {
override fun updateItem(item: Number?, empty: Boolean) {
super.updateItem(item, empty)
text = when {
empty -> null
item == null -> null
item.toLong() == -1L -> "*" // Display "*" for -1
else -> item.toString() // Display the actual value
}
alignment = Pos.CENTER_LEFT
}
}
}
comparator = Comparator { a, b ->
when {
a == null && b == null -> 0
a == null -> 1 // Nulls to bottom
b == null -> -1
a.toLong() == -1L && b.toLong() == -1L -> 0 // Both -1, equal
a.toLong() == -1L -> {
// -1 always goes to bottom
// In ascending: return 1 (a is greater, goes last)
// In descending: return -1 (a is smaller, still goes last in reverse)
if (sortType == TableColumn.SortType.ASCENDING) 1 else -1
}
b.toLong() == -1L -> {
if (sortType == TableColumn.SortType.ASCENDING) -1 else 1
}
else -> a.toLong().compareTo(b.toLong()) // Normal numeric comparison
}
}
}
column("Preview", PostModel::previewListProperty) {
@@ -237,8 +265,7 @@ class PostsTableView : View() {
preview.cleanup()
if (cell.tableRow.item != null && cell.tableRow.item.previewList.isNotEmpty()) {
preview.display(
cell.tableRow.item.vgThreadId,
cell.tableRow.item.previewList
cell.tableRow.item.vgThreadId, cell.tableRow.item.previewList
)
preview.previewPopup.apply {
x = mouseEvent.screenX + 20
@@ -406,34 +433,9 @@ class PostsTableView : View() {
tableView.prefHeightProperty().bind(root.heightProperty())
tableView.placeholder = Label("Loading")
coroutineScope.launch {
launch {
GuiEventBus.events.collect {
when (it) {
GuiEventBus.LocalSession, GuiEventBus.RemoteSession -> {
println("Collecting $it from PostsTableView")
val postModelList = postController.findAllPosts().toList()
val queueState = postController.getQueueState()
runLater {
items.addAll(postModelList)
tableView.sort()
tableView.placeholder = Label("No content in table")
updateQueueState(queueState)
}
}
GuiEventBus.ChangingSession -> runLater {
tableView.placeholder = Label("Loading")
items.clear()
}
else -> {}
}
}
}
launch {
postController.updateMetadataFlow.collect { metadataEntity ->
postController.updateMetadataFlow.let { flow ->
coroutineScope.launch {
flow.collect { metadataEntity ->
runLater {
val postModel = items.find { it.id == metadataEntity.postIdRef } ?: return@runLater
@@ -442,18 +444,22 @@ class PostsTableView : View() {
}
}
}
}
launch {
postController.deletedPostsFlow.collect {
postController.deletedPostsFlow.let { flow ->
coroutineScope.launch {
flow.collect {
runLater {
items.items.removeIf { p -> p.id == it }
tableView.sort()
}
}
}
}
launch {
postController.updatePostsFlow.collect { post ->
postController.updatePostsFlow.let { flow ->
coroutineScope.launch {
flow.collect { post ->
runLater {
val postModel = items.find { it.id == post.id } ?: return@runLater
@@ -470,15 +476,19 @@ class PostsTableView : View() {
}
}
}
}
launch {
postController.queueStateUpdate.collect { queueState ->
updateQueueState(queueState)
postController.queueStateUpdate.let { flow ->
coroutineScope.launch {
flow.collect {
updateQueueState(it)
}
}
}
launch {
postController.newPostsFlow.collect {
postController.newPostsFlow.let { flow ->
coroutineScope.launch {
flow.collect {
runLater {
items.addAll(it)
tableView.sort()
@@ -486,7 +496,30 @@ class PostsTableView : View() {
}
}
}
println("${this.javaClass.name} init")
coroutineScope.launch {
GuiEventBus.events.collect {
when (it) {
GuiEventBus.LocalSession, GuiEventBus.RemoteSession -> {
val postModelList = postController.findAllPosts()
val queueState = postController.getQueueState()
runLater {
items.addAll(postModelList)
tableView.sort()
tableView.placeholder = Label("No content in table")
updateQueueState(queueState)
}
}
GuiEventBus.ChangingSession -> runLater {
tableView.placeholder = Label("Loading")
items.clear()
}
else -> {}
}
}
}
}
private fun updateQueueState(queueState: QueueState) {
@@ -494,11 +527,11 @@ class PostsTableView : View() {
val rank = queueState.rank.find { it.postEntityId == post.id }
if (rank != null) {
runLater {
post.order = rank.rank.toString()
post.order = rank.rank
}
} else if (post.order != "*") {
} else if (post.order != -1L) {
runLater {
post.order = "*"
post.order = -1L
}
}
}
@@ -29,7 +29,6 @@ class StatusBarView : View("Status bar") {
GuiEventBus.events.collect {
when (it) {
GuiEventBus.LocalSession, GuiEventBus.RemoteSession -> {
println("Collecting $it from StatusBarView")
while (isActive) {
val result = runCatching { statusBarController.loggedInUser() }
if (result.isSuccess) {
@@ -47,43 +46,53 @@ class StatusBarView : View("Status bar") {
}
}
coroutineScope.launch {
statusBarController.vgUserUpdate.collect {
runLater {
loggedUser.set(it)
statusBarController.vgUserUpdate.let { flow ->
coroutineScope.launch {
flow.collect {
runLater {
loggedUser.set(it)
}
}
}
}
coroutineScope.launch {
statusBarController.tasksRunning.collect {
runLater {
tasksRunning.set(it)
statusBarController.tasksRunning.let { flow ->
coroutineScope.launch {
flow.collect {
runLater {
tasksRunning.set(it)
}
}
}
}
coroutineScope.launch {
statusBarController.downloadSpeed.collect {
runLater {
downloadSpeed.set(it.speed.formatSI())
statusBarController.downloadSpeed.let { flow ->
coroutineScope.launch {
flow.collect {
runLater {
downloadSpeed.set(it.speed.formatSI())
}
}
}
}
coroutineScope.launch {
statusBarController.queueStateUpdate.collect {
runLater {
running.set(it.running)
pending.set(it.remaining)
statusBarController.queueStateUpdate.let { flow ->
coroutineScope.launch {
flow.collect {
runLater {
running.set(it.running)
pending.set(it.remaining)
}
}
}
}
coroutineScope.launch {
statusBarController.errorCountUpdate.collect {
runLater {
error.set(it.count)
statusBarController.errorCountUpdate.let { flow ->
coroutineScope.launch {
flow.collect {
runLater {
error.set(it.count)
}
}
}
}
@@ -11,7 +11,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.javafx.asFlow
import kotlinx.coroutines.launch
import me.vripper.gui.components.fragments.ThreadSelectionTableFragment
@@ -156,41 +155,19 @@ class ThreadTableView : View() {
tableView.prefHeightProperty().bind(root.heightProperty())
tableView.placeholder = Label("Loading")
coroutineScope.launch {
launch {
GuiEventBus.events.collect {
when (it) {
GuiEventBus.LocalSession, GuiEventBus.RemoteSession -> {
println("Collecting $it from ThreadTableView")
val list = threadController.findAll().toList()
runLater {
items.clear()
items.addAll(list)
tableView.placeholder = Label("No content in table")
}
}
GuiEventBus.ChangingSession -> runLater {
tableView.placeholder = Label("Loading")
items.clear()
}
else -> {}
}
}
}
launch {
threadController.newThread.collect {
threadController.newThread.let { flow ->
coroutineScope.launch {
flow.collect {
runLater {
items.add(it)
}
}
}
}
launch {
threadController.updateThread.collect { thread ->
threadController.updateThread.let { flow ->
coroutineScope.launch {
flow.collect { thread ->
runLater {
val threadModel = items.find { it.threadId == thread.threadId } ?: return@runLater
threadModel.total = thread.total
@@ -198,24 +175,49 @@ class ThreadTableView : View() {
}
}
}
}
launch {
threadController.deleteThread.collect { threadId ->
threadController.deleteThread.let { flow ->
coroutineScope.launch {
flow.collect { threadId ->
runLater {
tableView.items.removeIf { it.threadId == threadId }
}
}
}
}
launch {
threadController.clearThreads.collect {
threadController.clearThreads.let {
coroutineScope.launch {
it.collect {
runLater {
tableView.items.clear()
}
}
}
}
println("${this.javaClass.name} init")
coroutineScope.launch {
GuiEventBus.events.collect {
when (it) {
GuiEventBus.LocalSession, GuiEventBus.RemoteSession -> {
val list = threadController.findAll()
runLater {
items.clear()
items.addAll(list)
tableView.placeholder = Label("No content in table")
}
}
GuiEventBus.ChangingSession -> runLater {
tableView.placeholder = Label("Loading")
items.clear()
}
else -> {}
}
}
}
}
private fun isCurrentTab(): Boolean = mainView.root.selectionModel.selectedItem.id == "thread-tab"
@@ -1,13 +1,11 @@
package me.vripper.gui.controller
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import me.vripper.gui.model.PostModel
import me.vripper.gui.utils.AppEndpointManager.currentAppEndpointService
import me.vripper.gui.utils.AppEndpointManager.localAppEndpointService
import me.vripper.gui.utils.AppEndpointManager.remoteAppEndpointService
import me.vripper.gui.utils.ChannelFlowBuilder
import me.vripper.gui.utils.ChannelFlowBuilder.toFlow
import me.vripper.model.Post
import me.vripper.model.QueueState
import me.vripper.services.download.MovePosition
@@ -85,8 +83,8 @@ class PostController : Controller() {
return runCatching { mapper(currentAppEndpointService().findPost(postEntityId)) }.getOrNull()
}
fun findAllPosts(): Flow<PostModel> {
return toFlow { currentAppEndpointService().findAllPosts().map(::mapper) }
suspend fun findAllPosts(): List<PostModel> {
return currentAppEndpointService().findAllPosts().map(::mapper)
}
suspend fun getQueueState(): QueueState {
@@ -117,7 +115,7 @@ class PostController : Controller() {
post.total,
post.hosts.joinToString(separator = ", "),
post.addedOn.format(dateTimeFormatter),
"*",
-1,
post.getDownloadFolder(),
post.folderName,
progressCount(post.total, post.done, post.downloaded),
@@ -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
}
)
)
}
@@ -1,6 +1,5 @@
package me.vripper.gui.controller
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import me.vripper.entities.ThreadEntity
import me.vripper.gui.model.ThreadModel
@@ -9,7 +8,6 @@ import me.vripper.gui.utils.AppEndpointManager.currentAppEndpointService
import me.vripper.gui.utils.AppEndpointManager.localAppEndpointService
import me.vripper.gui.utils.AppEndpointManager.remoteAppEndpointService
import me.vripper.gui.utils.ChannelFlowBuilder
import me.vripper.gui.utils.ChannelFlowBuilder.toFlow
import me.vripper.model.ThreadPostId
import org.koin.core.component.KoinComponent
import tornadofx.Controller
@@ -40,8 +38,8 @@ class ThreadController : KoinComponent, Controller() {
remoteAppEndpointService::onClearThreads,
)
fun findAll(): Flow<ThreadModel> {
return toFlow { currentAppEndpointService().findAllThreads().map(::threadModelMapper) }
suspend fun findAll(): List<ThreadModel> {
return currentAppEndpointService().findAllThreads().map(::threadModelMapper)
}
private fun threadModelMapper(it: ThreadEntity): ThreadModel {
@@ -1,9 +1,6 @@
package me.vripper.gui.model
import javafx.beans.property.SimpleDoubleProperty
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleListProperty
import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.*
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import tornadofx.getValue
@@ -20,7 +17,7 @@ class PostModel(
total: Int,
hosts: String,
addedOn: String,
order: String,
order: Long,
path: String,
folderName: String,
progressCount: String,
@@ -54,8 +51,8 @@ class PostModel(
val addedOnProperty = SimpleStringProperty(addedOn)
var addedOn: String by addedOnProperty
val orderProperty = SimpleStringProperty(order)
var order: String by orderProperty
val orderProperty = SimpleLongProperty(order)
var order: Long by orderProperty
val pathProperty = SimpleStringProperty(path)
var path: String by pathProperty
@@ -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
}
)
@@ -44,10 +44,4 @@ object ChannelFlowBuilder {
}
}
}
fun <T> toFlow(source: suspend () -> List<T>): Flow<T> {
return channelFlow {
source().forEach { if (isActive) send(it) }
}.retryWhen { _, _ -> delay(1000); true }
}
}
@@ -15,11 +15,12 @@ object ClipboardManager : KoinComponent {
private var coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var pollJob: Job? = null
private var settingsUpdateJob: Job? = null
private val systemClipboard = Clipboard.getSystemClipboard()
fun init() {
coroutineScope.launch {
GuiEventBus.events.collect {
when (it) {
GuiEventBus.events.collect { event ->
when (event) {
GuiEventBus.LocalSession, GuiEventBus.RemoteSession -> {
logger.info("Clipboard manager initialized")
while (isActive) {
@@ -53,9 +54,8 @@ object ClipboardManager : KoinComponent {
var value: String? = null
while (isActive) {
runLater {
val clipboard = Clipboard.getSystemClipboard()
if (clipboard.hasString()) {
value = clipboard.string
if (systemClipboard.hasString()) {
value = systemClipboard.string
}
}
if (!value.isNullOrBlank() && value != current) {
@@ -8,7 +8,7 @@ fun openFileDirectory(path: String) {
if(os.contains("Windows")) {
Shell32.INSTANCE.ShellExecuteW(null, WString("open"), WString(path), null, null, 1)
} else if(os.contains("Linux")) {
Runtime.getRuntime().exec("xdg-open $path")
Runtime.getRuntime().exec(arrayOf("xdg-open", path))
} else if(os.contains("Mac")) {
Runtime.getRuntime().exec("open -R $path")
}
@@ -56,12 +56,6 @@ object PreviewCacheManager : KoinComponent {
init {
coroutineScope.launch {
while (isActive) {
// println("Cache state check")
// println("Cache entries = ${entries.size}")
// println("Cache entries limit = $MAX_ENTRIES")
// println("Cache size = ${cacheSize.get().formatSI()}")
// println("Cache size LIMIT = ${THRESHOLD.formatSI()}")
val entriesDelta = entries.size - MAX_ENTRIES
if (entriesDelta > 0) {
val deleted = mutableListOf<Entry>()
@@ -85,8 +79,6 @@ object PreviewCacheManager : KoinComponent {
cacheSize.addAndGet(element.size * -1L)
} while (cacheSize.get() - THRESHOLD > 0)
}
// println("Cache state check completed")
delay(30_000)
}
}
@@ -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()
}
}