Compare commits

...
8 Commits
Author SHA1 Message Date
death-claw 331749ca91 fixes #171 2024-03-17 15:47:13 +01:00
death-claw e9b4ca24eb fixes #171 2024-03-17 15:33:02 +01:00
death-claw e5bbbd651f version bump 2024-03-17 15:19:14 +01:00
death-claw e7a2ed6e8b fixes #171 2024-03-17 15:19:00 +01:00
death-claw 2923cc350c fixes #171 2024-03-17 14:18:35 +01:00
death-clawandGitHub 53cdc8a4a1 fixes #171 (#174)
fixes #173
2024-03-17 13:53:57 +01:00
UncleRoger33andGitHub 592a6d44c3 Update README.md (#170)
- Updated direct download links for v5.4.0 in the downloads table.
- Formatted "HOME_FOLDER" description for Windows. Previous one was a bit confusing for basic users.
2024-02-29 20:49:48 +01:00
death-clawandGitHub dbf6e3436a fixes #139 (#169) 2024-02-28 15:42:57 +01:00
133 changed files with 5165 additions and 2388 deletions
+2 -2
View File
@@ -13,11 +13,11 @@ Direct access to vipergirls.to (or one of its [alternative domains](https://vipe
<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 | <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/death-claw/vripper-project/releases/download/5.3.0/vripper-windows-installer-5.3.0.exe) <br /> [Installer (MSI)](https://github.com/death-claw/vripper-project/releases/download/5.3.0/vripper-windows-installer-5.3.0.msi) <br /> [Portable (ZIP)](https://github.com/death-claw/vripper-project/releases/download/5.3.0/vripper-windows-portable-5.3.0.zip) | [Installer (DMG)](https://github.com/death-claw/vripper-project/releases/download/5.3.0/vripper-macos-5.3.0.dmg) <br /> [Installer (PKG)](https://github.com/death-claw/vripper-project/releases/download/5.3.0/vripper-macos-5.3.0.pkg) <br /> [Portable (ZIP)](https://github.com/death-claw/vripper-project/releases/download/5.3.0/vripper-macos-portable-5.3.0.zip) | [Linux (amd64) (DEB)](https://github.com/death-claw/vripper-project/releases/download/5.3.0/vripper-linux-5.3.0_amd64.deb) <br /> [Linux (x86_64) (RPM)](https://github.com/death-claw/vripper-project/releases/download/5.3.0/vripper-linux-5.3.0.x86_64.rpm) <br /> [Portable (ZIP)](https://github.com/death-claw/vripper-project/releases/download/5.3.0/vripper-linux-portable-5.3.0.zip) | [Java GUI (noarch)](https://github.com/death-claw/vripper-project/releases/download/5.3.0/vripper-noarch-gui-5.3.0.jar) <br /> [Java Web (noarch)](https://github.com/death-claw/vripper-project/releases/download/5.3.0/vripper-noarch-web-5.3.0.jar)
[Installer (EXE)](https://github.com/death-claw/vripper-project/releases/download/5.4.0/vripper-windows-installer-5.4.0.exe) <br /> [Installer (MSI)](https://github.com/death-claw/vripper-project/releases/download/5.4.0/vripper-windows-installer-5.4.0.msi) <br /> [Portable (ZIP)](https://github.com/death-claw/vripper-project/releases/download/5.4.0/vripper-windows-portable-5.4.0.zip) | [Installer (DMG)](https://github.com/death-claw/vripper-project/releases/download/5.4.0/vripper-macos-5.4.0.dmg) <br /> [Installer (PKG)](https://github.com/death-claw/vripper-project/releases/download/5.4.0/vripper-macos-5.4.0.pkg) <br /> [Portable (ZIP)](https://github.com/death-claw/vripper-project/releases/download/5.4.0/vripper-macos-portable-5.4.0.zip) | [Linux (amd64) (DEB)](https://github.com/death-claw/vripper-project/releases/download/5.4.0/vripper-linux-5.4.0_amd64.deb) <br /> [Linux (x86_64) (RPM)](https://github.com/death-claw/vripper-project/releases/download/5.4.0/vripper-linux-5.4.0.x86_64.rpm) <br /> [Portable (ZIP)](https://github.com/death-claw/vripper-project/releases/download/5.4.0/vripper-linux-portable-5.4.0.zip) | [Java GUI (noarch)](https://github.com/death-claw/vripper-project/releases/download/5.4.0/vripper-noarch-gui-5.4.0.jar) <br /> [Java Web (noarch)](https://github.com/death-claw/vripper-project/releases/download/5.4.0/vripper-noarch-web-5.4.0.jar)
Source code and previous versions are available on the [Releases page](https://github.com/death-claw/vripper-project/releases).
Application data (application logs, settings and persisted data) is stored in `HOME_FOLDER/vripper (Windows)` or `HOME_FOLDER/.config/vripper (Linux and macOS)`
Application data (application logs, settings and persisted data) is stored in `C:\USERS\<your Windows username>\vripper (Windows)` or `HOME_FOLDER/.config/vripper (Linux and macOS)`
## Supported Image Hosts
+1 -1
View File
@@ -1,6 +1,6 @@
--input jar
--main-jar vripper-gui.jar
--add-modules java.base,java.desktop,java.sql,jdk.unsupported,jdk.crypto.ec,java.security.jgss
--add-modules java.base,java.desktop,java.sql,jdk.unsupported,jdk.crypto.ec,java.security.jgss,java.net.http
--jlink-options "--strip-native-commands --strip-debug --no-man-pages --no-header-files --compress=zip-6"
--name VRipper
--description "Image ripper tool for vipergirls"
+29 -1
View File
@@ -15,7 +15,7 @@
<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>5.4.0</revision>
<revision>5.5.0</revision>
<kotlin.version>1.9.22</kotlin.version>
<slf4j.version>2.0.12</slf4j.version>
<logback.version>1.5.0</logback.version>
@@ -33,6 +33,9 @@
<commons-codec.version>1.16.1</commons-codec.version>
<liquibase-core.version>4.26.0</liquibase-core.version>
<liquibase-slf4j.version>5.0.0</liquibase-slf4j.version>
<grpc.kotlin.version>1.4.1</grpc.kotlin.version>
<java.grpc.version>1.62.2</java.grpc.version>
<protobuf.version>3.25.3</protobuf.version>
</properties>
<build>
<plugins>
@@ -97,6 +100,11 @@
<version>${kotlinx-coroutines.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-javafx</artifactId>
<version>${kotlinx-coroutines.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.exposed</groupId>
<artifactId>exposed-jdbc</artifactId>
<version>${exposed.version}</version>
@@ -176,6 +184,26 @@
<groupId>com.mattbertolini</groupId>
<version>${liquibase-slf4j.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-kotlin-stub</artifactId>
<version>${grpc.kotlin.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>${java.grpc.version}</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-kotlin</artifactId>
<version>${protobuf.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>${java.grpc.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
+104 -25
View File
@@ -35,10 +35,6 @@
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core-jvm</artifactId>
</dependency>
<dependency>
<artifactId>kotlin-reflect</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
</dependency>
<dependency>
<groupId>org.jetbrains.exposed</groupId>
<artifactId>exposed-jdbc</artifactId>
@@ -92,19 +88,42 @@
<groupId>com.mattbertolini</groupId>
<scope>runtime</scope>
</dependency>
<!-- <dependency>-->
<!-- <groupId>io.projectreactor</groupId>-->
<!-- <artifactId>reactor-core</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-serialization-json</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-serialization-json</artifactId>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-kotlin-stub</artifactId>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-kotlin</artifactId>
</dependency>
</dependencies>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<resources>
<resource>
<directory>src/main/resources</directory>
<excludes>
<exclude>version</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>version</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
@@ -119,19 +138,79 @@
</goals>
</execution>
</executions>
<configuration>
<jvmTarget>1.8</jvmTarget>
<compilerPlugins>
<plugin>kotlinx-serialization</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-serialization</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
<configuration>
<jvmTarget>1.8</jvmTarget>
<compilerPlugins>
<plugin>kotlinx-serialization</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-serialization</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.7.1</version>
<executions>
<execution>
<phase>initialize</phase>
<goals>
<goal>detect</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<executions>
<execution>
<id>compile</id>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
<configuration>
<protocArtifact>
com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}
</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>
io.grpc:protoc-gen-grpc-java:${java.grpc.version}:exe:${os.detected.classifier}
</pluginArtifact>
<protocPlugins>
<protocPlugin>
<id>grpc-kotlin</id>
<groupId>io.grpc</groupId>
<artifactId>protoc-gen-grpc-kotlin</artifactId>
<version>${grpc.kotlin.version}</version>
<classifier>jdk8</classifier>
<mainClass>io.grpc.kotlin.generator.GeneratorRunner</mainClass>
</protocPlugin>
</protocPlugins>
</configuration>
</execution>
<execution>
<id>compile-kt</id>
<goals>
<goal>compile-custom</goal>
</goals>
<configuration>
<protocArtifact>
com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}
</protocArtifact>
<outputDirectory>${project.build.directory}/generated-sources/protobuf/kotlin
</outputDirectory>
<pluginId>kotlin</pluginId>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
@@ -6,6 +6,7 @@ import me.vripper.host.*
import me.vripper.repositories.*
import me.vripper.repositories.impl.*
import me.vripper.services.*
import org.koin.core.qualifier.named
import org.koin.dsl.bind
import org.koin.dsl.module
@@ -52,9 +53,15 @@ val coreModule = module {
single<DownloadSpeedService> {
DownloadSpeedService(get())
}
single<AppEndpointService> {
AppEndpointService(get(), get(), get(), get())
AppEndpointService(get(), get(), get(), get(), get())
}
single((named("localAppEndpointService"))) {
AppEndpointService(get(), get(), get(), get(), get())
} bind IAppEndpointService::class
single<MetadataService> {
MetadataService(get(), get(), get(), get())
}
@@ -1,9 +1,12 @@
package me.vripper.download
import kotlinx.coroutines.*
import me.vripper.entities.Image
import me.vripper.entities.LogEntry
import me.vripper.entities.Post
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import me.vripper.entities.ImageEntity
import me.vripper.entities.LogEntryEntity
import me.vripper.entities.PostEntity
import me.vripper.entities.domain.Status
import me.vripper.event.ErrorCountEvent
import me.vripper.event.EventBus
@@ -38,9 +41,9 @@ class DownloadService(
private val pending: MutableMap<Byte, MutableList<ImageDownloadRunnable>> = mutableMapOf()
private val lock = ReentrantLock()
private val condition = lock.newCondition()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val coroutineScope = CoroutineScope(SupervisorJob())
fun init() {
init {
Thread.ofVirtual().name("Download Loop").unstarted(Runnable {
val accepted: MutableList<ImageDownloadRunnable> = mutableListOf()
val candidates: MutableList<ImageDownloadRunnable> = mutableListOf()
@@ -48,14 +51,14 @@ class DownloadService(
lock.withLock {
candidates.addAll(getCandidates(candidateCount()))
candidates.forEach {
if (canRun(it.context.image.host)) {
if (canRun(it.context.imageEntity.host)) {
accepted.add(it)
running[it.context.image.host]!!.add(it)
log.debug("${it.context.image.url} accepted to run")
running[it.context.imageEntity.host]!!.add(it)
log.debug("${it.context.imageEntity.url} accepted to run")
}
}
accepted.forEach {
pending[it.context.image.host]?.remove(it)
pending[it.context.imageEntity.host]?.remove(it)
scheduleForDownload(it)
}
accepted.clear()
@@ -73,26 +76,29 @@ class DownloadService(
fun stop(postIds: List<Long> = emptyList()) {
if (postIds.isNotEmpty()) {
stopInternal(postIds)
coroutineScope.launch {
eventBus.publishEvent(StoppedEvent(postIds))
}
} else {
stopAll()
}
coroutineScope.launch {
eventBus.publishEvent(StoppedEvent(postIds))
coroutineScope.launch {
eventBus.publishEvent(StoppedEvent(listOf(-1)))
}
}
}
fun restartAll(postIds: List<Post> = emptyList()) {
if (postIds.isNotEmpty()) {
restart(postIds.associateWith { dataTransaction.findByPostIdAndIsNotCompleted(it.postId) })
fun restartAll(postEntityIds: List<PostEntity> = emptyList()) {
if (postEntityIds.isNotEmpty()) {
restart(postEntityIds.associateWith { dataTransaction.findByPostIdAndIsNotCompleted(it.postId) })
} else {
restart(dataTransaction.findAllPosts()
.associateWith { dataTransaction.findByPostIdAndIsNotCompleted(it.postId) })
}
}
private fun restart(posts: Map<Post, List<Image>>) {
private fun restart(posts: Map<PostEntity, List<ImageEntity>>) {
lock.withLock {
val toProcess = mutableMapOf<Post, List<Image>>()
val toProcess = mutableMapOf<PostEntity, List<ImageEntity>>()
for ((post, images) in posts) {
if (images.isEmpty()) {
@@ -147,11 +153,11 @@ class DownloadService(
}
private fun isPending(postId: Long): Boolean {
return pending.values.flatten().any { it.context.image.postId == postId }
return pending.values.flatten().any { it.context.imageEntity.postId == postId }
}
private fun isRunning(postId: Long): Boolean {
return running.values.flatten().any { it.context.image.postId == postId }
return running.values.flatten().any { it.context.imageEntity.postId == postId }
}
private fun stopAll() {
@@ -169,10 +175,10 @@ class DownloadService(
lock.withLock {
for (postId in postIds) {
pending.values.forEach { pending ->
pending.removeIf { it.context.image.postId == postId }
pending.removeIf { it.context.imageEntity.postId == postId }
}
running.values.flatten()
.filter { p: ImageDownloadRunnable -> p.context.image.postId == postId }
.filter { p: ImageDownloadRunnable -> p.context.imageEntity.postId == postId }
.forEach { obj: ImageDownloadRunnable -> obj.stop() }
}
postIds.forEach {
@@ -208,7 +214,7 @@ class DownloadService(
val list: List<ImageDownloadRunnable> =
pending[host]!!.sortedWith(Comparator.comparingInt<ImageDownloadRunnable> { it.postRank }
.thenComparingInt { it.context.image.index })
.thenComparingInt { it.context.imageEntity.index })
for (imageDownloadRunnable in list) {
val count = hostIntegerMap[host] ?: 0
@@ -224,7 +230,7 @@ class DownloadService(
}
private fun scheduleForDownload(imageDownloadRunnable: ImageDownloadRunnable) {
log.debug("Scheduling a job for ${imageDownloadRunnable.context.image.url}")
log.debug("Scheduling a job for ${imageDownloadRunnable.context.imageEntity.url}")
CompletableFuture.runAsync({
coroutineScope.launch {
eventBus.publishEvent(QueueStateEvent(QueueState(runningCount(), pendingCount())))
@@ -233,20 +239,20 @@ class DownloadService(
.onFailure {
try {
dataTransaction.saveLog(
LogEntry(
type = LogEntry.Type.DOWNLOAD,
status = LogEntry.Status.ERROR,
message = "Failed to download ${imageDownloadRunnable.context.image.url}\n ${it.failure.formatToString()}"
LogEntryEntity(
type = LogEntryEntity.Type.DOWNLOAD,
status = LogEntryEntity.Status.ERROR,
message = "Failed to download ${imageDownloadRunnable.context.imageEntity.url}\n ${it.failure.formatToString()}"
)
)
} catch (exp: Exception) {
log.error("Failed to save event", exp)
}
log.error(
"Failed to download ${imageDownloadRunnable.context.image.url} after ${it.attemptCount} tries",
"Failed to download ${imageDownloadRunnable.context.imageEntity.url} after ${it.attemptCount} tries",
it.failure
)
val image = imageDownloadRunnable.context.image
val image = imageDownloadRunnable.context.imageEntity
image.status = Status.ERROR
dataTransaction.updateImage(image)
}.onComplete {
@@ -264,7 +270,7 @@ class DownloadService(
eventBus.publishEvent(ErrorCountEvent(ErrorCount(dataTransaction.countImagesInError())))
}
log.debug(
"Finished downloading ${imageDownloadRunnable.context.image.url}"
"Finished downloading ${imageDownloadRunnable.context.imageEntity.url}"
)
}.run(imageDownloadRunnable::run)
}, GLOBAL_EXECUTOR)
@@ -272,7 +278,7 @@ class DownloadService(
private fun afterJobFinish(imageDownloadRunnable: ImageDownloadRunnable) {
lock.withLock {
val image = imageDownloadRunnable.context.image
val image = imageDownloadRunnable.context.imageEntity
running[image.host]!!.remove(imageDownloadRunnable)
if (!isPending(image.postId) && !isRunning(
image.postId
@@ -1,17 +1,17 @@
package me.vripper.download
import me.vripper.entities.Image
import me.vripper.entities.ImageEntity
import me.vripper.model.Settings
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase
import org.apache.hc.client5.http.cookie.BasicCookieStore
import org.apache.hc.client5.http.protocol.HttpClientContext
import org.koin.core.component.KoinComponent
class ImageDownloadContext(val image: Image, val settings: Settings) : KoinComponent {
class ImageDownloadContext(val imageEntity: ImageEntity, val settings: Settings) : KoinComponent {
val httpContext: HttpClientContext =
HttpClientContext.create().apply { cookieStore = BasicCookieStore() }
val requests = mutableListOf<HttpUriRequestBase>()
val postId = image.postIdRef
val postId = imageEntity.postIdRef
var stopped = false
}
@@ -1,7 +1,7 @@
package me.vripper.download
import kotlinx.coroutines.*
import me.vripper.entities.Image
import me.vripper.entities.ImageEntity
import me.vripper.entities.domain.Status
import me.vripper.exception.DownloadException
import me.vripper.exception.HostException
@@ -25,14 +25,14 @@ import kotlin.io.path.Path
import kotlin.io.path.pathString
class ImageDownloadRunnable(
private val image: Image, val postRank: Int, private val settings: Settings
private val imageEntity: ImageEntity, val postRank: Int, private val settings: Settings
) : KoinComponent, CheckedRunnable {
private val log by me.vripper.delegate.LoggerDelegate()
private val dataTransaction: DataTransaction by inject()
private val hosts: List<Host> = getKoin().getAll()
val context: ImageDownloadContext = ImageDownloadContext(image, settings)
val context: ImageDownloadContext = ImageDownloadContext(imageEntity, settings)
private var stopped: Boolean
get() = context.stopped
set(value) {
@@ -42,43 +42,43 @@ class ImageDownloadRunnable(
@Throws(DownloadException::class)
fun download() {
try {
image.status = Status.DOWNLOADING
image.downloaded = 0
dataTransaction.updateImage(image)
synchronized(image.postId.toString().intern()) {
imageEntity.status = Status.DOWNLOADING
imageEntity.downloaded = 0
dataTransaction.updateImage(imageEntity)
synchronized(imageEntity.postId.toString().intern()) {
val post = dataTransaction.findPostById(context.postId).orElseThrow()
if (post.status != Status.DOWNLOADING) {
post.status = Status.DOWNLOADING
dataTransaction.updatePost(post)
}
}
log.debug("Getting image url and name from ${image.url} using ${image.host}")
val host = hosts.first { it.isSupported(image.url) }
val downloadedImage = host.downloadInternal(image.url, context)
log.debug("Resolved name for ${image.url}: ${downloadedImage.name}")
log.debug("Downloaded image {} to {}", image.url, downloadedImage.path)
synchronized(image.postId.toString().intern()) {
log.debug("Getting image url and name from ${imageEntity.url} using ${imageEntity.host}")
val host = hosts.first { it.isSupported(imageEntity.url) }
val downloadedImage = host.downloadInternal(imageEntity.url, context)
log.debug("Resolved name for ${imageEntity.url}: ${downloadedImage.name}")
log.debug("Downloaded image {} to {}", imageEntity.url, downloadedImage.path)
synchronized(imageEntity.postId.toString().intern()) {
val post = dataTransaction.findPostById(context.postId).orElseThrow()
val downloadDirectory = Path(post.downloadDirectory, post.folderName).pathString
checkImageTypeAndRename(
downloadDirectory, downloadedImage, image.index
downloadDirectory, downloadedImage, imageEntity.index
)
if (image.downloaded == image.size && image.size > 0) {
image.status = Status.FINISHED
if (imageEntity.downloaded == imageEntity.size && imageEntity.size > 0) {
imageEntity.status = Status.FINISHED
post.done += 1
post.downloaded += image.size
post.downloaded += imageEntity.size
dataTransaction.updatePost(post)
} else {
image.status = Status.ERROR
imageEntity.status = Status.ERROR
}
dataTransaction.updateImage(image)
dataTransaction.updateImage(imageEntity)
}
} catch (e: Exception) {
if (stopped) {
return
}
image.status = Status.ERROR
dataTransaction.updateImage(image)
imageEntity.status = Status.ERROR
dataTransaction.updateImage(imageEntity)
throw DownloadException(e)
}
}
@@ -110,7 +110,7 @@ class ImageDownloadRunnable(
"%03d_", index + 1
) else ""
}$filename"
image.filename = finalFilename
imageEntity.filename = finalFilename
val imageDownloadPath = downloadDestinationFolder.resolve(finalFilename)
Files.copy(downloadedImage.path, imageDownloadPath, StandardCopyOption.REPLACE_EXISTING)
} catch (e: Exception) {
@@ -138,11 +138,11 @@ class ImageDownloadRunnable(
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
val that = other as ImageDownloadRunnable
return image.id == that.image.id
return imageEntity.id == that.imageEntity.id
}
override fun hashCode(): Int {
return Objects.hash(image.id)
return Objects.hash(imageEntity.id)
}
fun stop() {
@@ -2,7 +2,7 @@ package me.vripper.entities
import me.vripper.entities.domain.Status
data class Image(
data class ImageEntity(
val id: Long = -1,
val postId: Long,
val url: String,
@@ -19,7 +19,7 @@ data class Image(
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Image
other as ImageEntity
if (url != other.url) return false
@@ -2,7 +2,7 @@ package me.vripper.entities
import java.time.LocalDateTime
data class LogEntry(
data class LogEntryEntity(
val id: Long = -1,
val type: Type,
val status: Status,
@@ -2,7 +2,7 @@ package me.vripper.entities
import kotlinx.serialization.Serializable
data class Metadata(val postId: Long, val data: Data) {
data class MetadataEntity(val postId: Long, val data: Data) {
@Serializable
data class Data(
val postedBy: String,
@@ -2,10 +2,8 @@ package me.vripper.entities
import me.vripper.entities.domain.Status
import java.time.LocalDateTime
import kotlin.io.path.Path
import kotlin.io.path.pathString
data class Post(
data class PostEntity(
val id: Long = -1,
val postTitle: String,
val threadTitle: String,
@@ -26,15 +24,11 @@ data class Post(
var downloaded: Long = 0,
) {
fun getDownloadFolder(): String {
return Path(downloadDirectory, folderName).pathString
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Post
other as PostEntity
return postId == other.postId
}
@@ -1,6 +1,6 @@
package me.vripper.entities
data class Thread(
data class ThreadEntity(
val id: Long = -1,
val title: String,
val link: String,
@@ -11,7 +11,7 @@ data class Thread(
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Thread
other as ThreadEntity
return threadId == other.threadId
}
@@ -6,12 +6,12 @@ import me.vripper.model.ErrorCount
import me.vripper.model.QueueState
import me.vripper.model.Settings
data class PostCreateEvent(val posts: List<Post>)
data class PostUpdateEvent(val posts: List<Post>)
data class PostCreateEvent(val postEntities: List<PostEntity>)
data class PostUpdateEvent(val postEntities: List<PostEntity>)
data class PostDeleteEvent(val postIds: List<Long>)
data class ImageEvent(val images: List<Image>)
data class ThreadCreateEvent(val thread: Thread)
data class ThreadUpdateEvent(val thread: Thread)
data class ImageEvent(val imageEntities: List<ImageEntity>)
data class ThreadCreateEvent(val threadEntity: ThreadEntity)
data class ThreadUpdateEvent(val threadEntity: ThreadEntity)
data class ThreadDeleteEvent(val threadId: Long)
class ThreadClearEvent
data class VGUserLoginEvent(val username: String)
@@ -19,11 +19,9 @@ data class DownloadSpeedEvent(val downloadSpeed: DownloadSpeed)
data class QueueStateEvent(val queueState: QueueState)
data class ErrorCountEvent(val errorCount: ErrorCount)
data class SettingsUpdateEvent(val settings: Settings)
data class MetadataUpdateEvent(val metadata: Metadata)
data class LogCreateEvent(val logEntry: LogEntry)
data class LogUpdateEvent(val logEntry: LogEntry)
data class MetadataUpdateEvent(val metadataEntity: MetadataEntity)
data class LogCreateEvent(val logEntryEntity: LogEntryEntity)
data class LogUpdateEvent(val logEntryEntity: LogEntryEntity)
data class LogDeleteEvent(val deleted: List<Long>)
data class LoadingTasks(val loading: Boolean)
data class StoppedEvent(val postIds: List<Long>)
@@ -95,7 +95,7 @@ abstract class Host(
".tmp"
)
return Files.newOutputStream(tempImage).use { fos ->
val image = context.image
val image = context.imageEntity
synchronized(image.postId.toString().intern()) {
val post = dataTransaction.findPostById(context.postId).orElseThrow()
val size = if (image.size < 0) {
@@ -155,7 +155,8 @@ abstract class Host(
context: ImageDownloadContext,
transformer: (ClassicHttpResponse) -> T
): T {
val httpGet = HttpGet(url).also { it.addHeader("Referer", context.image.url) }.also { context.requests.add(it) }
val httpGet =
HttpGet(url).also { it.addHeader("Referer", context.imageEntity.url) }.also { context.requests.add(it) }
log.debug(String.format("Requesting %s", url))
return httpService.client.execute(httpGet, context.httpContext) {
if (it.code / 100 != 2) {
@@ -68,6 +68,6 @@ class ImageVenueHost(
companion object {
private const val CONTINUE_BUTTON_XPATH = "//a[@title='Continue to ImageVenue']"
private const val IMG_XPATH = "//a[@data-toggle='full']/img"
private const val IMG_XPATH = "//a[@data-toggle='full']/img[@id='main-image']"
}
}
@@ -1,26 +1,16 @@
package me.vripper.listeners
import me.vripper.download.DownloadService
import me.vripper.services.*
import me.vripper.services.DataTransaction
import me.vripper.services.MetadataService
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
open class OnStartupListener : KoinComponent {
private val dataTransaction: DataTransaction by inject()
private val downloadService: DownloadService by inject()
private val httpService: HTTPService by inject()
private val retryPolicyService: RetryPolicyService by inject()
private val vgAuthService: VGAuthService by inject()
private val downloadSpeedService: DownloadSpeedService by inject()
private val metadataService: MetadataService by inject()
open fun run() {
dataTransaction.setDownloadingToStopped()
httpService.init()
retryPolicyService.init()
vgAuthService.init()
downloadService.init()
downloadSpeedService.init()
metadataService.init()
}
}
@@ -0,0 +1,5 @@
package me.vripper.model
import me.vripper.entities.ImageEntity
typealias Image = ImageEntity
@@ -0,0 +1,5 @@
package me.vripper.model
import me.vripper.entities.LogEntryEntity
typealias LogEntry = LogEntryEntity
@@ -0,0 +1,5 @@
package me.vripper.model
import me.vripper.entities.MetadataEntity
typealias Metadata = MetadataEntity
@@ -0,0 +1,34 @@
package me.vripper.model
import me.vripper.entities.domain.Status
import java.time.LocalDateTime
import kotlin.io.path.Path
import kotlin.io.path.pathString
data class Post(
val id: Long = -1,
val postTitle: String,
val threadTitle: String,
val forum: String,
val url: String,
val token: String,
val postId: Long,
val threadId: Long,
val total: Int,
val hosts: Set<String>,
val downloadDirectory: String,
val addedOn: LocalDateTime = LocalDateTime.now(),
var folderName: String,
var status: Status = Status.STOPPED,
var done: Int = 0,
var rank: Int = Int.MAX_VALUE,
var size: Long = -1,
var downloaded: Long = 0,
val previews: List<String>,
val postedBy: String,
val resolvedNames: List<String>
) {
fun getDownloadFolder(): String {
return Path(downloadDirectory, folderName).pathString
}
}
@@ -0,0 +1,14 @@
package me.vripper.model
data class PostSelection(
val threadId: Long,
val threadTitle: String,
val postId: Long,
val number: Int,
val title: String,
val imageCount: Int,
val url: String,
val hosts: String,
val forum: String,
val previews: List<String>
)
@@ -42,7 +42,6 @@ data class ConnectionSettings(
@Serializable
data class SystemSettings(
val tempPath: String = System.getProperty("java.io.tmpdir"),
val cachePath: String = System.getProperty("java.io.tmpdir"),
val enableClipboardMonitoring: Boolean = false,
val clipboardPollingRate: Int = 500,
val maxEventLog: Int = 1_000,
@@ -0,0 +1,5 @@
package me.vripper.model
import me.vripper.entities.ThreadEntity
typealias Thread = ThreadEntity
@@ -1,4 +1,4 @@
package me.vripper.model
package me.vripper.parser
import me.vripper.host.Host
@@ -1,4 +1,4 @@
package me.vripper.model
package me.vripper.parser
data class PostItem(
val threadId: Long,
@@ -1,9 +1,8 @@
package me.vripper.parser
import me.vripper.entities.LogEntry
import me.vripper.entities.LogEntryEntity
import me.vripper.exception.DownloadException
import me.vripper.exception.PostParseException
import me.vripper.model.PostItem
import me.vripper.services.*
import me.vripper.utilities.Tasks
import me.vripper.utilities.formatToString
@@ -42,8 +41,8 @@ class PostLookupAPIParser(private val threadId: Long, private val postId: Long)
"parsing failed for thread $threadId, post $postId", it.failure
)
dataTransaction.saveLog(
LogEntry(
type = LogEntry.Type.POST, status = LogEntry.Status.ERROR, message = """
LogEntryEntity(
type = LogEntryEntity.Type.POST, status = LogEntryEntity.Status.ERROR, message = """
Failed to process thread $threadId, post $postId
${it.failure.formatToString()}
""".trimIndent()
@@ -1,4 +1,4 @@
package me.vripper.model
package me.vripper.parser
data class ThreadItem(
val threadId: Long,
@@ -1,9 +1,8 @@
package me.vripper.parser
import me.vripper.entities.LogEntry
import me.vripper.entities.LogEntryEntity
import me.vripper.exception.DownloadException
import me.vripper.exception.PostParseException
import me.vripper.model.ThreadItem
import me.vripper.services.*
import me.vripper.utilities.Tasks
import me.vripper.utilities.formatToString
@@ -45,8 +44,8 @@ class ThreadLookupAPIParser(private val threadId: Long) : KoinComponent {
it.failure
)
dataTransaction.saveLog(
LogEntry(
type = LogEntry.Type.THREAD, status = LogEntry.Status.ERROR, message = """
LogEntryEntity(
type = LogEntryEntity.Type.THREAD, status = LogEntryEntity.Status.ERROR, message = """
Failed to process thread $threadId
${it.failure.formatToString()}
""".trimIndent()
@@ -1,9 +1,6 @@
package me.vripper.parser
import me.vripper.host.Host
import me.vripper.model.ImageItem
import me.vripper.model.PostItem
import me.vripper.model.ThreadItem
import me.vripper.services.SettingsService
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -1,20 +1,20 @@
package me.vripper.repositories
import me.vripper.entities.Image
import me.vripper.entities.ImageEntity
import java.util.*
interface ImageRepository {
fun save(image: Image): Image
fun save(imageList: List<Image>)
fun save(imageEntity: ImageEntity): ImageEntity
fun save(imageEntityList: List<ImageEntity>)
fun deleteAllByPostId(postId: Long)
fun findByPostId(postId: Long): List<Image>
fun findByPostId(postId: Long): List<ImageEntity>
fun countError(): Int
fun findByPostIdAndIsNotCompleted(postId: Long): List<Image>
fun findByPostIdAndIsNotCompleted(postId: Long): List<ImageEntity>
fun stopByPostIdAndIsNotCompleted(postId: Long): Int
fun stopByPostIdAndIsNotCompleted(): Int
fun findByPostIdAndIsError(postId: Long): List<Image>
fun findById(id: Long): Optional<Image>
fun update(image: Image)
fun update(images: List<Image>)
fun findByPostIdAndIsError(postId: Long): List<ImageEntity>
fun findById(id: Long): Optional<ImageEntity>
fun update(imageEntity: ImageEntity)
fun update(imageEntities: List<ImageEntity>)
fun deleteAllByPostId(postIds: List<Long>)
}
@@ -1,13 +1,13 @@
package me.vripper.repositories
import me.vripper.entities.LogEntry
import me.vripper.entities.LogEntryEntity
import java.util.*
interface LogRepository {
fun save(logEntry: LogEntry): LogEntry
fun update(logEntry: LogEntry)
fun findAll(): List<LogEntry>
fun findById(id: Long): Optional<LogEntry>
fun save(logEntryEntity: LogEntryEntity): LogEntryEntity
fun update(logEntryEntity: LogEntryEntity)
fun findAll(): List<LogEntryEntity>
fun findById(id: Long): Optional<LogEntryEntity>
fun deleteOldest(): List<Long>
fun deleteAll()
@@ -1,11 +1,11 @@
package me.vripper.repositories
import me.vripper.entities.Metadata
import me.vripper.entities.MetadataEntity
import java.util.*
interface MetadataRepository {
fun save(metadata: Metadata): Metadata
fun findByPostId(postId: Long): Optional<Metadata>
fun save(metadataEntity: MetadataEntity): MetadataEntity
fun findByPostId(postId: Long): Optional<MetadataEntity>
fun deleteByPostId(postId: Long): Int
fun deleteAllByPostId(postIds: List<Long>)
}
@@ -1,19 +1,19 @@
package me.vripper.repositories
import me.vripper.entities.Post
import me.vripper.entities.PostEntity
import java.util.*
interface PostDownloadStateRepository {
fun save(posts: List<Post>): List<Post>
fun findByPostId(postId: Long): Optional<Post>
fun findById(id: Long): Optional<Post>
fun save(postEntities: List<PostEntity>): List<PostEntity>
fun findByPostId(postId: Long): Optional<PostEntity>
fun findById(id: Long): Optional<PostEntity>
fun findCompleted(): List<Long>
fun findAll(): List<Post>
fun findAll(): List<PostEntity>
fun existByPostId(postId: Long): Boolean
fun setDownloadingToStopped(): Int
fun deleteByPostId(postId: Long): Int
fun update(post: Post)
fun update(posts: List<Post>)
fun update(postEntity: PostEntity)
fun update(postEntities: List<PostEntity>)
fun findMaxRank(): Int?
fun deleteAll(postIds: List<Long>)
fun stopAll()
@@ -1,14 +1,14 @@
package me.vripper.repositories
import me.vripper.entities.Thread
import me.vripper.entities.ThreadEntity
import java.util.*
interface ThreadRepository {
fun save(thread: Thread): Thread
fun update(thread: Thread)
fun findByThreadId(threadId: Long): Optional<Thread>
fun findAll(): List<Thread>
fun findById(id: Long): Optional<Thread>
fun save(threadEntity: ThreadEntity): ThreadEntity
fun update(threadEntity: ThreadEntity)
fun findByThreadId(threadId: Long): Optional<ThreadEntity>
fun findAll(): List<ThreadEntity>
fun findById(id: Long): Optional<ThreadEntity>
fun deleteByThreadId(threadId: Long): Int
fun deleteAll()
}
@@ -1,6 +1,6 @@
package me.vripper.repositories.impl
import me.vripper.entities.Image
import me.vripper.entities.ImageEntity
import me.vripper.entities.domain.Status
import me.vripper.repositories.ImageRepository
import me.vripper.tables.ImageTable
@@ -11,24 +11,24 @@ import java.sql.Connection
import java.util.*
class ImageRepositoryImpl : ImageRepository {
override fun save(image: Image): Image {
override fun save(imageEntity: ImageEntity): ImageEntity {
val id = ImageTable.insertAndGetId {
it[downloaded] = image.downloaded
it[host] = image.host
it[index] = image.index
it[postId] = image.postId
it[status] = image.status.name
it[size] = image.size
it[url] = image.url
it[thumbUrl] = image.thumbUrl
it[postIdRef] = image.postIdRef
it[filename] = image.filename
it[downloaded] = imageEntity.downloaded
it[host] = imageEntity.host
it[index] = imageEntity.index
it[postId] = imageEntity.postId
it[status] = imageEntity.status.name
it[size] = imageEntity.size
it[url] = imageEntity.url
it[thumbUrl] = imageEntity.thumbUrl
it[postIdRef] = imageEntity.postIdRef
it[filename] = imageEntity.filename
}.value
return image.copy(id = id)
return imageEntity.copy(id = id)
}
override fun save(imageList: List<Image>) {
ImageTable.batchInsert(imageList, shouldReturnGeneratedValues = false) {
override fun save(imageEntityList: List<ImageEntity>) {
ImageTable.batchInsert(imageEntityList, shouldReturnGeneratedValues = false) {
this[ImageTable.downloaded] = it.downloaded
this[ImageTable.host] = it.host
this[ImageTable.index] = it.index
@@ -46,7 +46,7 @@ class ImageRepositoryImpl : ImageRepository {
ImageTable.deleteWhere { ImageTable.postId eq postId }
}
override fun findByPostId(postId: Long): List<Image> {
override fun findByPostId(postId: Long): List<ImageEntity> {
return ImageTable.select {
ImageTable.postId eq postId
}.map(this::transform)
@@ -59,7 +59,7 @@ class ImageRepositoryImpl : ImageRepository {
.count().toInt()
}
override fun findByPostIdAndIsNotCompleted(postId: Long): List<Image> {
override fun findByPostIdAndIsNotCompleted(postId: Long): List<ImageEntity> {
return ImageTable
.select {
(ImageTable.postId eq postId) and (ImageTable.status neq Status.FINISHED.name)
@@ -78,13 +78,13 @@ class ImageRepositoryImpl : ImageRepository {
}
}
override fun findByPostIdAndIsError(postId: Long): List<Image> {
override fun findByPostIdAndIsError(postId: Long): List<ImageEntity> {
return ImageTable.select {
(ImageTable.postId eq postId) and (ImageTable.status eq Status.ERROR.name)
}.map(this::transform)
}
override fun findById(id: Long): Optional<Image> {
override fun findById(id: Long): Optional<ImageEntity> {
val result = ImageTable.select {
ImageTable.id eq id
}.map(this::transform)
@@ -95,17 +95,17 @@ class ImageRepositoryImpl : ImageRepository {
}
}
override fun update(image: Image) {
ImageTable.update({ ImageTable.id eq image.id }) {
it[status] = image.status.name
it[downloaded] = image.downloaded
it[size] = image.size
it[filename] = image.filename
override fun update(imageEntity: ImageEntity) {
ImageTable.update({ ImageTable.id eq imageEntity.id }) {
it[status] = imageEntity.status.name
it[downloaded] = imageEntity.downloaded
it[size] = imageEntity.size
it[filename] = imageEntity.filename
}
}
override fun update(images: List<Image>) {
ImageTable.batchUpsert(images, shouldReturnGeneratedValues = false) {
override fun update(imageEntities: List<ImageEntity>) {
ImageTable.batchUpsert(imageEntities, shouldReturnGeneratedValues = false) {
this[ImageTable.id] = it.id
this[ImageTable.downloaded] = it.downloaded
this[ImageTable.host] = it.host
@@ -140,10 +140,12 @@ class ImageRepositoryImpl : ImageRepository {
it.execute()
}
conn.prepareStatement("TRUNCATE TABLE IMAGES_DELETE")
conn.prepareStatement("TRUNCATE TABLE IMAGES_DELETE").use {
it.execute()
}
}
private fun transform(resultRow: ResultRow): Image {
private fun transform(resultRow: ResultRow): ImageEntity {
val id = resultRow[ImageTable.id].value
val postId = resultRow[ImageTable.postId]
val url = resultRow[ImageTable.url]
@@ -155,7 +157,7 @@ class ImageRepositoryImpl : ImageRepository {
val status = Status.valueOf(resultRow[ImageTable.status])
val postIdRef = resultRow[ImageTable.postIdRef]
val filename = resultRow[ImageTable.filename]
return Image(
return ImageEntity(
id,
postId,
url,
@@ -1,6 +1,6 @@
package me.vripper.repositories.impl
import me.vripper.entities.LogEntry
import me.vripper.entities.LogEntryEntity
import me.vripper.repositories.LogRepository
import me.vripper.services.SettingsService
import me.vripper.tables.LogTable
@@ -11,25 +11,25 @@ import java.util.*
class LogRepositoryImpl(val settingsService: SettingsService) : LogRepository {
@Synchronized
override fun save(logEntry: LogEntry): LogEntry {
override fun save(logEntryEntity: LogEntryEntity): LogEntryEntity {
val id = LogTable.insertAndGetId {
it[type] = logEntry.type.name
it[status] = logEntry.status.name
it[time] = logEntry.time
it[message] = logEntry.message.take(500)
it[type] = logEntryEntity.type.name
it[status] = logEntryEntity.status.name
it[time] = logEntryEntity.time
it[message] = logEntryEntity.message.take(500)
}.value
return logEntry.copy(id = id)
return logEntryEntity.copy(id = id)
}
override fun update(logEntry: LogEntry) {
LogTable.update({ LogTable.id eq logEntry.id }) {
it[type] = logEntry.type.name
it[status] = logEntry.status.name
it[message] = logEntry.message.take(500)
override fun update(logEntryEntity: LogEntryEntity) {
LogTable.update({ LogTable.id eq logEntryEntity.id }) {
it[type] = logEntryEntity.type.name
it[status] = logEntryEntity.status.name
it[message] = logEntryEntity.message.take(500)
}
}
override fun findById(id: Long): Optional<LogEntry> {
override fun findById(id: Long): Optional<LogEntryEntity> {
val result = LogTable.select {
LogTable.id eq id
}.map(::transform)
@@ -41,7 +41,7 @@ class LogRepositoryImpl(val settingsService: SettingsService) : LogRepository {
}
}
override fun findAll(): List<LogEntry> {
override fun findAll(): List<LogEntryEntity> {
return LogTable.selectAll().map(::transform)
}
@@ -57,13 +57,13 @@ class LogRepositoryImpl(val settingsService: SettingsService) : LogRepository {
LogTable.deleteAll()
}
private fun transform(resultRow: ResultRow): LogEntry {
private fun transform(resultRow: ResultRow): LogEntryEntity {
val id = resultRow[LogTable.id].value
val type = LogEntry.Type.valueOf(resultRow[LogTable.type])
val status = LogEntry.Status.valueOf(resultRow[LogTable.status])
val type = LogEntryEntity.Type.valueOf(resultRow[LogTable.type])
val status = LogEntryEntity.Status.valueOf(resultRow[LogTable.status])
val time = resultRow[LogTable.time]
val message = resultRow[LogTable.message]
return LogEntry(
return LogEntryEntity(
id,
type,
status,
@@ -2,7 +2,7 @@ package me.vripper.repositories.impl
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import me.vripper.entities.Metadata
import me.vripper.entities.MetadataEntity
import me.vripper.repositories.MetadataRepository
import me.vripper.tables.MetadataTable
import org.jetbrains.exposed.sql.ResultRow
@@ -16,16 +16,16 @@ import java.util.*
class MetadataRepositoryImpl: MetadataRepository {
override fun save(metadata: Metadata): Metadata {
override fun save(metadataEntity: MetadataEntity): MetadataEntity {
MetadataTable.insert {
it[postId] = metadata.postId
it[data] = Json.encodeToString(metadata.data)
it[postId] = metadataEntity.postId
it[data] = Json.encodeToString(metadataEntity.data)
}
return metadata
return metadataEntity
}
override fun findByPostId(postId: Long): Optional<Metadata> {
override fun findByPostId(postId: Long): Optional<MetadataEntity> {
val result = MetadataTable.select {
MetadataTable.postId eq postId
@@ -38,10 +38,10 @@ class MetadataRepositoryImpl: MetadataRepository {
}
}
private fun transform(row: ResultRow): Metadata {
private fun transform(row: ResultRow): MetadataEntity {
val id = row[MetadataTable.postId]
val data = Json.decodeFromString(row[MetadataTable.data]) as Metadata.Data
return Metadata(id, data)
val data = Json.decodeFromString(row[MetadataTable.data]) as MetadataEntity.Data
return MetadataEntity(id, data)
}
override fun deleteByPostId(postId: Long): Int {
@@ -68,6 +68,8 @@ class MetadataRepositoryImpl: MetadataRepository {
it.execute()
}
conn.prepareStatement("TRUNCATE TABLE METADATA_DELETE")
conn.prepareStatement("TRUNCATE TABLE METADATA_DELETE").use {
it.execute()
}
}
}
@@ -1,6 +1,6 @@
package me.vripper.repositories.impl
import me.vripper.entities.Post
import me.vripper.entities.PostEntity
import me.vripper.entities.domain.Status
import me.vripper.repositories.PostDownloadStateRepository
import me.vripper.tables.PostTable
@@ -15,8 +15,8 @@ class PostDownloadStateRepositoryImpl :
private val delimiter = ";"
override fun save(posts: List<Post>): List<Post> {
return PostTable.batchInsert(posts, shouldReturnGeneratedValues = true) { post ->
override fun save(postEntities: List<PostEntity>): List<PostEntity> {
return PostTable.batchInsert(postEntities, shouldReturnGeneratedValues = true) { post ->
this[PostTable.status] = post.status.name
this[PostTable.done] = post.done
this[PostTable.total] = post.total
@@ -37,7 +37,7 @@ class PostDownloadStateRepositoryImpl :
}.map(::transform)
}
override fun findByPostId(postId: Long): Optional<Post> {
override fun findByPostId(postId: Long): Optional<PostEntity> {
val result = PostTable.select {
PostTable.postId eq postId
}.map(::transform)
@@ -54,7 +54,7 @@ class PostDownloadStateRepositoryImpl :
}.map { it[PostTable.postId] }
}
override fun findById(id: Long): Optional<Post> {
override fun findById(id: Long): Optional<PostEntity> {
val result = PostTable.select {
PostTable.id eq id
}.map { transform(it) }
@@ -66,7 +66,7 @@ class PostDownloadStateRepositoryImpl :
}
}
override fun findAll(): List<Post> {
override fun findAll(): List<PostEntity> {
return PostTable.selectAll().map { transform(it) }
}
@@ -84,19 +84,19 @@ class PostDownloadStateRepositoryImpl :
return PostTable.deleteWhere { PostTable.postId eq postId }
}
override fun update(post: Post) {
PostTable.update({ PostTable.id eq post.id }) {
it[status] = post.status.name
it[done] = post.done
it[rank] = post.rank
it[size] = post.size
it[downloaded] = post.downloaded
it[folderName] = post.folderName
override fun update(postEntity: PostEntity) {
PostTable.update({ PostTable.id eq postEntity.id }) {
it[status] = postEntity.status.name
it[done] = postEntity.done
it[rank] = postEntity.rank
it[size] = postEntity.size
it[downloaded] = postEntity.downloaded
it[folderName] = postEntity.folderName
}
}
override fun update(posts: List<Post>) {
PostTable.batchUpsert(posts, shouldReturnGeneratedValues = false) { post ->
override fun update(postEntities: List<PostEntity>) {
PostTable.batchUpsert(postEntities, shouldReturnGeneratedValues = false) { post ->
this[PostTable.id] = post.id
this[PostTable.status] = post.status.name
this[PostTable.done] = post.done
@@ -144,7 +144,9 @@ class PostDownloadStateRepositoryImpl :
it.execute()
}
conn.prepareStatement("TRUNCATE TABLE POSTS_DELETE")
conn.prepareStatement("TRUNCATE TABLE POSTS_DELETE").use {
it.execute()
}
}
override fun stopAll() {
@@ -159,7 +161,7 @@ class PostDownloadStateRepositoryImpl :
}.map { it[PostTable.postId] }
}
private fun transform(resultRow: ResultRow): Post {
private fun transform(resultRow: ResultRow): PostEntity {
val id = resultRow[PostTable.id].value
val status = Status.valueOf(resultRow[PostTable.status])
val postId = resultRow[PostTable.postId]
@@ -179,7 +181,7 @@ class PostDownloadStateRepositoryImpl :
val rank = resultRow[PostTable.rank]
val size = resultRow[PostTable.size]
val downloaded = resultRow[PostTable.downloaded]
return Post(
return PostEntity(
id,
postTitle,
threadTitle,
@@ -1,6 +1,6 @@
package me.vripper.repositories.impl
import me.vripper.entities.Thread
import me.vripper.entities.ThreadEntity
import me.vripper.repositories.ThreadRepository
import me.vripper.tables.ThreadTable
import org.jetbrains.exposed.sql.*
@@ -9,24 +9,24 @@ import java.util.*
class ThreadRepositoryImpl : ThreadRepository {
override fun save(thread: Thread): Thread {
override fun save(threadEntity: ThreadEntity): ThreadEntity {
val id = ThreadTable.insertAndGetId {
it[title] = thread.title
it[total] = thread.total
it[url] = thread.link
it[threadId] = thread.threadId
it[title] = threadEntity.title
it[total] = threadEntity.total
it[url] = threadEntity.link
it[threadId] = threadEntity.threadId
}.value
return thread.copy(id = id)
return threadEntity.copy(id = id)
}
override fun update(thread: Thread) {
ThreadTable.update({ ThreadTable.id eq thread.id }) {
it[title] = thread.title
it[total] = thread.total
override fun update(threadEntity: ThreadEntity) {
ThreadTable.update({ ThreadTable.id eq threadEntity.id }) {
it[title] = threadEntity.title
it[total] = threadEntity.total
}
}
override fun findByThreadId(threadId: Long): Optional<Thread> {
override fun findByThreadId(threadId: Long): Optional<ThreadEntity> {
val result = ThreadTable.select {
ThreadTable.threadId eq threadId
}.map(this::transform)
@@ -37,11 +37,11 @@ class ThreadRepositoryImpl : ThreadRepository {
}
}
override fun findAll(): List<Thread> {
override fun findAll(): List<ThreadEntity> {
return ThreadTable.selectAll().map(this::transform)
}
override fun findById(id: Long): Optional<Thread> {
override fun findById(id: Long): Optional<ThreadEntity> {
val result = ThreadTable.select {
ThreadTable.id eq id
}.map(this::transform)
@@ -60,12 +60,12 @@ class ThreadRepositoryImpl : ThreadRepository {
ThreadTable.deleteAll()
}
private fun transform(resultRow: ResultRow): Thread {
private fun transform(resultRow: ResultRow): ThreadEntity {
val id = resultRow[ThreadTable.id].value
val title = resultRow[ThreadTable.title]
val url = resultRow[ThreadTable.url]
val threadId = resultRow[ThreadTable.threadId]
val total = resultRow[ThreadTable.total]
return Thread(id, title, url, threadId, total)
return ThreadEntity(id, title, url, threadId, total)
}
}
@@ -1,16 +1,21 @@
package me.vripper.services
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.time.sample
import me.vripper.download.DownloadService
import me.vripper.entities.LogEntry
import me.vripper.entities.LogEntryEntity
import me.vripper.entities.PostEntity
import me.vripper.event.*
import me.vripper.exception.PostParseException
import me.vripper.model.PostItem
import me.vripper.model.ThreadPostId
import me.vripper.services.*
import me.vripper.model.*
import me.vripper.tasks.AddPostRunnable
import me.vripper.tasks.ThreadLookupRunnable
import me.vripper.utilities.ApplicationProperties
import me.vripper.utilities.GLOBAL_EXECUTOR
import me.vripper.utilities.PathUtils
import java.util.*
import java.time.Duration
import java.util.concurrent.CompletableFuture
import java.util.regex.Pattern
import kotlin.io.path.Path
@@ -20,148 +25,165 @@ class AppEndpointService(
private val downloadService: DownloadService,
private val dataTransaction: DataTransaction,
private val threadCacheService: ThreadCacheService,
private val settingsService: SettingsService
) {
private val settingsService: SettingsService,
private val vgAuthService: VGAuthService,
) : IAppEndpointService {
private val log by me.vripper.delegate.LoggerDelegate()
@Synchronized
fun scanLinks(postLinks: String) {
if (postLinks.isBlank()) {
log.warn("Nothing to scan")
return
}
val urlList = postLinks.split(Pattern.compile("\\r?\\n")).dropLastWhile { it.isEmpty() }.map { it.trim() }
.filter { it.isNotEmpty() }
for (link in urlList) {
log.debug("Starting to process thread: $link")
if (!link.startsWith(settingsService.settings.viperSettings.host)) {
continue
private val semaphore = Semaphore(1)
override suspend fun scanLinks(postLinks: String) {
semaphore.withPermit {
if (postLinks.isBlank()) {
log.warn("Nothing to scan")
return
}
var threadId: Long
var postId: Long?
val m = Pattern.compile(
Pattern.quote(settingsService.settings.viperSettings.host) + "/threads/(\\d+)((.*p=)(\\d+))?"
).matcher(link)
if (m.find()) {
threadId = m.group(1).toLong()
postId = m.group(4)?.toLong()
if (postId == null) {
CompletableFuture.runAsync(
ThreadLookupRunnable(
threadId, settingsService.settings
),
GLOBAL_EXECUTOR
)
} else {
CompletableFuture.runAsync(
AddPostRunnable(
listOf(ThreadPostId(threadId, postId))
),
GLOBAL_EXECUTOR
)
val urlList = postLinks.split(Pattern.compile("\\r?\\n")).dropLastWhile { it.isEmpty() }.map { it.trim() }
.filter { it.isNotEmpty() }
for (link in urlList) {
log.debug("Starting to process thread: $link")
if (!link.startsWith(settingsService.settings.viperSettings.host)) {
continue
}
} else {
log.error("Cannot retrieve thread id from URL $link")
dataTransaction.saveLog(
LogEntry(
type = LogEntry.Type.SCAN,
status = LogEntry.Status.ERROR,
message = "Invalid link $link, link is missing the threadId"
var threadId: Long
var postId: Long?
val m = Pattern.compile(
Pattern.quote(settingsService.settings.viperSettings.host) + "/threads/(\\d+)((.*p=)(\\d+))?"
).matcher(link)
if (m.find()) {
threadId = m.group(1).toLong()
postId = m.group(4)?.toLong()
if (postId == null) {
CompletableFuture.runAsync(
ThreadLookupRunnable(
threadId, settingsService.settings
), GLOBAL_EXECUTOR
)
} else {
CompletableFuture.runAsync(
AddPostRunnable(
listOf(ThreadPostId(threadId, postId))
), GLOBAL_EXECUTOR
)
}
} else {
log.error("Cannot retrieve thread id from URL $link")
dataTransaction.saveLog(
LogEntryEntity(
type = LogEntryEntity.Type.SCAN,
status = LogEntryEntity.Status.ERROR,
message = "Invalid link $link, link is missing the threadId"
)
)
)
continue
continue
}
}
}
}
@Synchronized
fun restartAll(posIds: List<Long> = listOf()) {
downloadService.restartAll(posIds.map { dataTransaction.findPostByPostId(it) }.filter { it.isPresent }
.map { it.get() })
override suspend fun restartAll(posIds: List<Long>) {
semaphore.withPermit {
downloadService.restartAll(posIds.map { dataTransaction.findPostByPostId(it) }.filter { it.isPresent }
.map { it.get() })
}
}
@Synchronized
fun download(posts: List<ThreadPostId>) {
override suspend fun download(posts: List<ThreadPostId>) {
CompletableFuture.runAsync(
AddPostRunnable(posts), GLOBAL_EXECUTOR
)
}
@Synchronized
fun stopAll(postIdList: List<Long> = emptyList()) {
downloadService.stop(postIdList)
override suspend fun stopAll(postIdList: List<Long>) {
semaphore.withPermit {
downloadService.stop(postIdList)
}
}
@Synchronized
fun remove(postIdList: List<Long>) {
downloadService.stop(postIdList)
dataTransaction.removeAll(postIdList)
override suspend fun remove(postIdList: List<Long>) {
semaphore.withPermit {
downloadService.stop(postIdList)
dataTransaction.removeAll(postIdList)
}
}
@Synchronized
fun clearCompleted(): List<Long> {
return dataTransaction.clearCompleted()
override suspend fun clearCompleted(): List<Long> {
semaphore.withPermit {
return dataTransaction.clearCompleted()
}
}
@Synchronized
@Throws(PostParseException::class)
fun grab(threadId: Long): List<PostItem> {
return try {
val thread = dataTransaction.findThreadByThreadId(threadId).orElseThrow {
PostParseException(
String.format(
"Unable to find links for threadId = %s", threadId
override suspend fun grab(threadId: Long): List<PostSelection> {
semaphore.withPermit {
return try {
val thread = dataTransaction.findThreadByThreadId(threadId).orElseThrow {
PostParseException(
String.format(
"Unable to find links for threadId = %s", threadId
)
)
)
}
val threadLookupResult = threadCacheService[thread.threadId]
threadLookupResult.postItemList.ifEmpty {
log.error(
String.format(
"Failed to get links for threadId = %s", threadId
}
val threadLookupResult = threadCacheService[thread.threadId]
threadLookupResult.postItemList.map { postItem ->
PostSelection(
postItem.threadId,
postItem.threadTitle,
postItem.postId,
postItem.number,
postItem.title,
postItem.imageCount,
postItem.url,
postItem.hosts.joinToString(", ") { "${it.first} (${it.second})" },
postItem.forum,
postItem.imageItemList.map { it.thumbLink })
}.ifEmpty {
log.error(
String.format(
"Failed to get links for threadId = %s", threadId
)
)
)
throw PostParseException(
String.format(
"Failed to get links for threadId = %s", threadId
)
)
}
} catch (e: Exception) {
throw PostParseException(
String.format(
"Failed to get links for threadId = %s", threadId
"Failed to get links for threadId = %s, %s", threadId, e.message
)
)
}
} catch (e: Exception) {
throw PostParseException(
String.format(
"Failed to get links for threadId = %s, %s", threadId, e.message
)
)
}
}
@Synchronized
fun threadRemove(threadIdList: List<Long>) {
threadIdList.forEach {
dataTransaction.removeThread(it)
override suspend fun threadRemove(threadIdList: List<Long>) {
semaphore.withPermit {
threadIdList.forEach {
dataTransaction.removeThread(it)
}
}
}
@Synchronized
fun threadClear() {
dataTransaction.clearQueueLinks()
override suspend fun threadClear() {
semaphore.withPermit {
dataTransaction.clearQueueLinks()
}
}
fun logClear() {
override suspend fun logClear() {
dataTransaction.deleteAllLogs()
}
fun rename(postId: Long, newName: String) {
override suspend fun rename(postId: Long, newName: String) {
CompletableFuture.runAsync({
synchronized(postId.toString().intern()) {
dataTransaction.findPostByPostId(postId).ifPresent { post ->
if (Path(post.downloadDirectory, post.folderName).exists()) {
PathUtils.rename(
dataTransaction.findImagesByPostId(postId),
post.downloadDirectory,
post.folderName,
newName
dataTransaction.findImagesByPostId(postId), post.downloadDirectory, post.folderName, newName
)
}
post.folderName = PathUtils.sanitize(newName)
@@ -170,4 +192,136 @@ class AppEndpointService(
}
}, GLOBAL_EXECUTOR)
}
override fun onNewPosts(): Flow<Post> =
EventBus.events.filterIsInstance(PostCreateEvent::class).map { it.postEntities.map(::mapper) }
.flatMapConcat { it.asFlow() }
override fun onUpdatePosts() =
EventBus.events.filterIsInstance(PostUpdateEvent::class).map { it.postEntities.map(::mapper) }
.flatMapConcat { it.asFlow() }
override fun onDeletePosts() =
EventBus.events.filterIsInstance(PostDeleteEvent::class).flatMapConcat { it.postIds.asFlow() }
override fun onUpdateMetadata() =
EventBus.events.filterIsInstance(MetadataUpdateEvent::class).map { it.metadataEntity }
override suspend fun findAllPosts(): List<Post> {
return dataTransaction.findAllPosts().map(::mapper)
}
private fun mapper(postEntity: PostEntity): Post {
val metadata: Metadata? = dataTransaction.findMetadataByPostId(postEntity.postId).orElse(null)
val images = dataTransaction.findImagesByPostId(postEntity.postId)
return Post(
postEntity.id,
postEntity.postTitle,
postEntity.threadTitle,
postEntity.forum,
postEntity.url,
postEntity.token,
postEntity.postId,
postEntity.threadId,
postEntity.total,
postEntity.hosts,
postEntity.downloadDirectory,
postEntity.addedOn,
postEntity.folderName,
postEntity.status,
postEntity.done,
postEntity.rank,
postEntity.size,
postEntity.downloaded,
images.take(4).map { it.thumbUrl },
metadata?.data?.postedBy ?: "",
metadata?.data?.resolvedNames ?: emptyList(),
)
}
override suspend fun findPost(postId: Long): Post {
return mapper(dataTransaction.findPostByPostId(postId).orElseThrow())
}
override suspend fun findImagesByPostId(postId: Long): List<Image> {
return dataTransaction.findImagesByPostId(postId)
}
override fun onUpdateImages(postId: Long): Flow<Image> = EventBus.events.filterIsInstance(ImageEvent::class).map {
it.imageEntities.filter { imageEntity: Image -> imageEntity.postId == postId }
}.filter { it.isNotEmpty() }.flatMapConcat { it.asFlow() }
override fun onStopped(): Flow<Long> =
EventBus.events.filterIsInstance(StoppedEvent::class).flatMapConcat { it.postIds.asFlow() }
override suspend fun findAllLogs(): List<LogEntry> {
return dataTransaction.findAllLogs()
}
override fun onNewLog() = EventBus.events.filterIsInstance(LogCreateEvent::class).map { it.logEntryEntity }
override fun onUpdateLog() = EventBus.events.filterIsInstance(LogUpdateEvent::class).map {
it.logEntryEntity
}
override fun onDeleteLogs(): Flow<Long> = EventBus.events.filterIsInstance(LogDeleteEvent::class).flatMapConcat {
it.deleted.asFlow()
}
override fun onNewThread(): Flow<Thread> =
EventBus.events.filterIsInstance(ThreadCreateEvent::class).map { it.threadEntity }
override fun onUpdateThread(): Flow<Thread> =
EventBus.events.filterIsInstance(ThreadUpdateEvent::class).map { it.threadEntity }
override fun onDeleteThread(): Flow<Long> =
EventBus.events.filterIsInstance(ThreadDeleteEvent::class).map { it.threadId }
override fun onClearThreads(): Flow<Unit> = EventBus.events.filterIsInstance(ThreadClearEvent::class).map { }
override suspend fun findAllThreads(): List<Thread> {
return dataTransaction.findAllThreads()
}
override fun onDownloadSpeed(): Flow<Long> =
EventBus.events.filterIsInstance(DownloadSpeedEvent::class).map { it.downloadSpeed.speed }
override fun onVGUserUpdate(): Flow<String> =
EventBus.events.filterIsInstance(VGUserLoginEvent::class).map { it.username }
override fun onQueueStateUpdate(): Flow<QueueState> =
EventBus.events.filterIsInstance(QueueStateEvent::class).map { it.queueState }
override fun onErrorCountUpdate(): Flow<Int> =
EventBus.events.filterIsInstance(ErrorCountEvent::class).map { it.errorCount.count }
override fun onTasksRunning(): Flow<Boolean> =
EventBus.events.filterIsInstance(LoadingTasks::class).sample(Duration.ofMillis(500)).map { it.loading }
override suspend fun getSettings(): Settings = settingsService.settings
override suspend fun saveSettings(settings: Settings) = settingsService.newSettings(settings)
override suspend fun getProxies(): List<String> = settingsService.getProxies()
override fun onUpdateSettings(): Flow<Settings> =
EventBus.events.filterIsInstance(SettingsUpdateEvent::class).map { it.settings }
override suspend fun loggedInUser(): String = vgAuthService.loggedUser
override suspend fun getVersion(): String = ApplicationProperties.VERSION
}
@@ -8,7 +8,7 @@ import me.vripper.entities.*
import me.vripper.entities.domain.Status
import me.vripper.event.*
import me.vripper.model.ErrorCount
import me.vripper.model.PostItem
import me.vripper.parser.PostItem
import me.vripper.repositories.*
import me.vripper.utilities.PathUtils
import me.vripper.utilities.PathUtils.sanitize
@@ -28,49 +28,49 @@ class DataTransaction(
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private fun save(posts: List<Post>): List<Post> {
return transaction { postDownloadStateRepository.save(posts) }
private fun save(postEntities: List<PostEntity>): List<PostEntity> {
return transaction { postDownloadStateRepository.save(postEntities) }
}
fun updatePosts(posts: List<Post>) {
transaction { postDownloadStateRepository.update(posts) }
fun updatePosts(postEntities: List<PostEntity>) {
transaction { postDownloadStateRepository.update(postEntities) }
coroutineScope.launch {
eventBus.publishEvent(PostUpdateEvent(posts))
eventBus.publishEvent(PostUpdateEvent(postEntities))
}
}
fun updatePost(post: Post) {
transaction { postDownloadStateRepository.update(post) }
fun updatePost(postEntity: PostEntity) {
transaction { postDownloadStateRepository.update(postEntity) }
coroutineScope.launch {
eventBus.publishEvent(PostUpdateEvent(listOf(post)))
eventBus.publishEvent(PostUpdateEvent(listOf(postEntity)))
}
}
fun save(thread: Thread) {
val savedThread = transaction { threadRepository.save(thread) }
fun save(threadEntity: ThreadEntity) {
val savedThread = transaction { threadRepository.save(threadEntity) }
coroutineScope.launch {
eventBus.publishEvent(ThreadCreateEvent(savedThread))
}
}
fun update(thread: Thread) {
transaction { threadRepository.update(thread) }
fun update(threadEntity: ThreadEntity) {
transaction { threadRepository.update(threadEntity) }
coroutineScope.launch {
eventBus.publishEvent(ThreadUpdateEvent(thread))
eventBus.publishEvent(ThreadUpdateEvent(threadEntity))
}
}
fun updateImages(images: List<Image>) {
transaction { imageRepository.update(images) }
fun updateImages(imageEntities: List<ImageEntity>) {
transaction { imageRepository.update(imageEntities) }
coroutineScope.launch {
eventBus.publishEvent(ImageEvent(images))
eventBus.publishEvent(ImageEvent(imageEntities))
}
}
fun updateImage(image: Image) {
transaction { imageRepository.update(image) }
fun updateImage(imageEntity: ImageEntity) {
transaction { imageRepository.update(imageEntity) }
coroutineScope.launch {
eventBus.publishEvent(ImageEvent(listOf(image)))
eventBus.publishEvent(ImageEvent(listOf(imageEntity)))
}
}
@@ -79,13 +79,13 @@ class DataTransaction(
}
@Synchronized
fun newPosts(postItems: List<PostItem>): List<Post> {
fun newPosts(postItems: List<PostItem>): List<PostEntity> {
var queuePosition = transaction {
getQueuePosition()?.plus(1) ?: 0
}
val posts = postItems.associate { postItem ->
val post = Post(
val postEntity = PostEntity(
postTitle = postItem.title,
url = postItem.url,
token = postItem.securityToken,
@@ -105,8 +105,8 @@ class DataTransaction(
postItem.title
)
)
val images = postItem.imageItemList.mapIndexed { index, imageItem ->
Image(
val imageEntities = postItem.imageItemList.mapIndexed { index, imageItem ->
ImageEntity(
postId = postItem.postId,
url = imageItem.mainLink,
thumbUrl = imageItem.thumbLink,
@@ -114,7 +114,7 @@ class DataTransaction(
index = index,
)
}
Pair(post, images)
Pair(postEntity, imageEntities)
}
@@ -137,8 +137,8 @@ class DataTransaction(
return postDownloadStateRepository.findMaxRank()
}
private fun save(images: List<Image>) {
transaction { imageRepository.save(images) }
private fun save(imageEntities: List<ImageEntity>) {
transaction { imageRepository.save(imageEntities) }
}
fun finishPost(postId: Long, automatic: Boolean = false) {
@@ -163,7 +163,7 @@ class DataTransaction(
}
}
private fun findByPostIdAndIsError(postId: Long): List<Image> {
private fun findByPostIdAndIsError(postId: Long): List<ImageEntity> {
return transaction { imageRepository.findByPostIdAndIsError(postId) }
}
@@ -202,7 +202,7 @@ class DataTransaction(
if (postIds.isNotEmpty()) {
remove(postIds)
} else {
remove(findAllPosts().map(Post::postId))
remove(findAllPosts().map(PostEntity::postId))
}
}
@@ -210,10 +210,10 @@ class DataTransaction(
transaction { imageRepository.stopByPostIdAndIsNotCompleted(postId) }
}
fun saveMetadata(metadata: Metadata) {
transaction { metadataRepository.save(metadata) }
fun saveMetadata(metadataEntity: MetadataEntity) {
transaction { metadataRepository.save(metadataEntity) }
coroutineScope.launch {
eventBus.publishEvent(MetadataUpdateEvent(metadata))
eventBus.publishEvent(MetadataUpdateEvent(metadataEntity))
}
}
@@ -226,44 +226,44 @@ class DataTransaction(
@Synchronized
fun sortPostsByRank() {
val posts = findAllPosts().sortedWith(Comparator.comparing(Post::addedOn))
for (i in posts.indices) {
posts[i].rank = i
val postEntities = findAllPosts().sortedWith(Comparator.comparing(PostEntity::addedOn))
for (i in postEntities.indices) {
postEntities[i].rank = i
}
updatePosts(posts)
updatePosts(postEntities)
}
fun setDownloadingToStopped() {
transaction { postDownloadStateRepository.setDownloadingToStopped() }
}
fun findAllPosts(): List<Post> {
fun findAllPosts(): List<PostEntity> {
return transaction { postDownloadStateRepository.findAll() }
}
fun findPostById(id: Long): Optional<Post> {
fun findPostById(id: Long): Optional<PostEntity> {
return transaction { postDownloadStateRepository.findById(id) }
}
fun findImagesByPostId(postId: Long): List<Image> {
fun findImagesByPostId(postId: Long): List<ImageEntity> {
return transaction { imageRepository.findByPostId(postId) }
}
fun findImageById(id: Long): Optional<Image> {
fun findImageById(id: Long): Optional<ImageEntity> {
return transaction { imageRepository.findById(id) }
}
fun findAllThreads(): List<Thread> {
fun findAllThreads(): List<ThreadEntity> {
return transaction { threadRepository.findAll() }
}
fun findThreadById(id: Long): Optional<Thread> {
fun findThreadById(id: Long): Optional<ThreadEntity> {
return transaction {
threadRepository.findById(id)
}
}
fun findByPostIdAndIsNotCompleted(postId: Long): List<Image> {
fun findByPostIdAndIsNotCompleted(postId: Long): List<ImageEntity> {
return transaction { imageRepository.findByPostIdAndIsNotCompleted(postId) }
}
@@ -271,17 +271,17 @@ class DataTransaction(
return transaction { imageRepository.countError() }
}
fun findPostByPostId(postId: Long): Optional<Post> {
fun findPostByPostId(postId: Long): Optional<PostEntity> {
return transaction { postDownloadStateRepository.findByPostId(postId) }
}
fun findThreadByThreadId(threadId: Long): Optional<Thread> {
fun findThreadByThreadId(threadId: Long): Optional<ThreadEntity> {
return transaction { threadRepository.findByThreadId(threadId) }
}
fun saveLog(logEntry: LogEntry): LogEntry {
fun saveLog(logEntryEntity: LogEntryEntity): LogEntryEntity {
val pair = transaction {
val saved = logRepository.save(logEntry)
val saved = logRepository.save(logEntryEntity)
val deleted = logRepository.deleteOldest()
Pair(saved, deleted)
}
@@ -294,10 +294,10 @@ class DataTransaction(
return pair.first
}
fun updateLog(logEntry: LogEntry) {
transaction { logRepository.update(logEntry) }
fun updateLog(logEntryEntity: LogEntryEntity) {
transaction { logRepository.update(logEntryEntity) }
coroutineScope.launch {
eventBus.publishEvent(LogUpdateEvent(logEntry))
eventBus.publishEvent(LogUpdateEvent(logEntryEntity))
}
}
@@ -305,7 +305,7 @@ class DataTransaction(
transaction { logRepository.deleteAll() }
}
fun findAllLogs(): List<LogEntry> {
fun findAllLogs(): List<LogEntryEntity> {
return transaction { logRepository.findAll() }
}
@@ -313,7 +313,7 @@ class DataTransaction(
return transaction { postDownloadStateRepository.findAllNonCompletedPostIds() }
}
fun findMetadataByPostId(postId: Long): Optional<Metadata> {
fun findMetadataByPostId(postId: Long): Optional<MetadataEntity> {
return transaction { metadataRepository.findByPostId(postId) }
}
}
@@ -20,10 +20,9 @@ class DownloadSpeedService(
private val bytesCount = AtomicLong(0)
private var job: Job? = null
fun init() {
init {
coroutineScope.launch {
eventBus.events.filterIsInstance(QueueStateEvent::class).collect {
coroutineContext.ensureActive()
if (it.queueState.running + it.queueState.remaining > 0) {
if (job == null || job?.isActive == false) {
job = coroutineScope.launch {
@@ -35,7 +35,7 @@ class HTTPService(
lateinit var client: CloseableHttpClient
private var connectionTimeout = settingsService.settings.connectionSettings.timeout
fun init() {
init {
buildRequestConfig()
buildConnectionConfig()
buildConnectionPool()
@@ -0,0 +1,47 @@
package me.vripper.services
import kotlinx.coroutines.flow.Flow
import me.vripper.model.*
interface IAppEndpointService {
suspend fun scanLinks(postLinks: String)
suspend fun restartAll(posIds: List<Long> = listOf())
suspend fun remove(postIdList: List<Long>)
suspend fun stopAll(postIdList: List<Long> = emptyList())
suspend fun clearCompleted(): List<Long>
suspend fun findPost(postId: Long): Post
suspend fun findAllPosts(): List<Post>
suspend fun rename(postId: Long, newName: String)
fun onNewPosts(): Flow<Post>
fun onUpdatePosts(): Flow<Post>
fun onDeletePosts(): Flow<Long>
fun onUpdateMetadata(): Flow<Metadata>
suspend fun findImagesByPostId(postId: Long): List<Image>
fun onUpdateImages(postId: Long): Flow<Image>
fun onStopped(): Flow<Long>
suspend fun logClear()
suspend fun findAllLogs(): List<LogEntry>
fun onNewLog(): Flow<LogEntry>
fun onUpdateLog(): Flow<LogEntry>
fun onDeleteLogs(): Flow<Long>
fun onNewThread(): Flow<Thread>
fun onUpdateThread(): Flow<Thread>
fun onDeleteThread(): Flow<Long>
fun onClearThreads(): Flow<Unit>
suspend fun findAllThreads(): List<Thread>
suspend fun threadRemove(threadIdList: List<Long>)
suspend fun threadClear()
suspend fun grab(threadId: Long): List<PostSelection>
suspend fun download(posts: List<ThreadPostId>)
fun onDownloadSpeed(): Flow<Long>
fun onVGUserUpdate(): Flow<String>
fun onQueueStateUpdate(): Flow<QueueState>
fun onErrorCountUpdate(): Flow<Int>
fun onTasksRunning(): Flow<Boolean>
suspend fun getSettings(): Settings
suspend fun saveSettings(settings: Settings)
suspend fun getProxies(): List<String>
fun onUpdateSettings(): Flow<Settings>
suspend fun loggedInUser(): String
suspend fun getVersion(): String
}
@@ -6,7 +6,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import me.vripper.entities.Metadata
import me.vripper.entities.MetadataEntity
import me.vripper.exception.DownloadException
import me.vripper.exception.VripperException
import me.vripper.utilities.HtmlUtils
@@ -46,7 +46,7 @@ class MetadataService(
)
}.build())
val metadata = httpService.client.execute(httpGet, vgAuthService.context) {
val metadataEntity = httpService.client.execute(httpGet, vgAuthService.context) {
if (it.code / 100 != 2) {
throw DownloadException("Unexpected response code '${it.code}' for $httpGet")
}
@@ -68,9 +68,9 @@ class MetadataService(
document, java.lang.String.format("//div[@id='post_message_%s']", postId)
) ?: throw VripperException("Unable to locate post content")
val titles = findTitleInContent(node)
Metadata(postId, Metadata.Data(postedBy, titles))
MetadataEntity(postId, MetadataEntity.Data(postedBy, titles))
}
dataTransaction.saveMetadata(metadata)
dataTransaction.saveMetadata(metadataEntity)
}
}
}
@@ -18,7 +18,7 @@ class RetryPolicyService(
private var maxAttempts: Int = settingsService.settings.connectionSettings.maxAttempts
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
fun init() {
init {
coroutineScope.launch {
eventBus.events.filterIsInstance(SettingsUpdateEvent::class).collect {
if (maxAttempts != it.settings.connectionSettings.maxAttempts) {
@@ -28,6 +28,7 @@ class SettingsService(private val eventBus: EventBus) {
private val json = Json {
encodeDefaults = true
prettyPrint = true
ignoreUnknownKeys = true
}
var settings = Settings()
@@ -125,7 +126,7 @@ class SettingsService(private val eventBus: EventBus) {
save()
}
fun save() {
private fun save() {
try {
Files.writeString(
configPath,
@@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch
import me.vripper.event.EventBus
import me.vripper.event.SettingsUpdateEvent
import me.vripper.model.ThreadItem
import me.vripper.parser.ThreadItem
import me.vripper.parser.ThreadLookupAPIParser
import java.util.*
import java.util.concurrent.ExecutionException
@@ -5,7 +5,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch
import me.vripper.entities.Post
import me.vripper.entities.PostEntity
import me.vripper.event.EventBus
import me.vripper.event.SettingsUpdateEvent
import me.vripper.event.VGUserLoginEvent
@@ -34,7 +34,7 @@ class VGAuthService(
private var authenticated = false
fun init() {
init {
context.cookieStore = BasicCookieStore()
coroutineScope.launch {
eventBus.events.filterIsInstance(SettingsUpdateEvent::class).collect {
@@ -122,9 +122,9 @@ class VGAuthService(
}
}
fun leaveThanks(post: Post) {
fun leaveThanks(postEntity: PostEntity) {
CompletableFuture.runAsync(
LeaveThanksRunnable(post, authenticated, context), GLOBAL_EXECUTOR
LeaveThanksRunnable(postEntity, authenticated, context), GLOBAL_EXECUTOR
)
}
}
@@ -1,8 +1,8 @@
package me.vripper.tasks
import me.vripper.download.DownloadService
import me.vripper.model.PostItem
import me.vripper.model.ThreadPostId
import me.vripper.parser.PostItem
import me.vripper.parser.PostLookupAPIParser
import me.vripper.services.DataTransaction
import me.vripper.services.MetadataService
@@ -1,8 +1,8 @@
package me.vripper.tasks
import me.vripper.entities.LogEntry
import me.vripper.entities.LogEntry.Status.ERROR
import me.vripper.entities.Post
import me.vripper.entities.LogEntryEntity
import me.vripper.entities.LogEntryEntity.Status.ERROR
import me.vripper.entities.PostEntity
import me.vripper.services.DataTransaction
import me.vripper.services.HTTPService
import me.vripper.services.SettingsService
@@ -16,7 +16,7 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class LeaveThanksRunnable(
private val post: Post,
private val postEntity: PostEntity,
private val authenticated: Boolean,
private val context: HttpClientContext
) : KoinComponent, Runnable {
@@ -41,8 +41,8 @@ class LeaveThanksRunnable(
listOf(
BasicNameValuePair("do", "post_thanks_add"),
BasicNameValuePair("using_ajax", "1"),
BasicNameValuePair("p", post.postId.toString()),
BasicNameValuePair("securitytoken", post.token)
BasicNameValuePair("p", postEntity.postId.toString()),
BasicNameValuePair("securitytoken", postEntity.token)
)
)
it.addHeader("Referer", settingsService.settings.viperSettings.host)
@@ -54,11 +54,11 @@ class LeaveThanksRunnable(
}
cm.client.execute(postThanks, context) { }
} catch (e: Exception) {
val error = "Failed to leave a thanks for $post"
val error = "Failed to leave a thanks for $postEntity"
log.error(error, e)
dataTransaction.saveLog(
LogEntry(
type = LogEntry.Type.THANKS, status = ERROR, message = """
LogEntryEntity(
type = LogEntryEntity.Type.THANKS, status = ERROR, message = """
$error
${e.formatToString()}
""".trimIndent()
@@ -1,11 +1,12 @@
package me.vripper.tasks
import me.vripper.entities.LogEntry
import me.vripper.entities.LogEntry.Status.ERROR
import me.vripper.entities.Thread
import kotlinx.coroutines.runBlocking
import me.vripper.entities.LogEntryEntity
import me.vripper.entities.LogEntryEntity.Status.ERROR
import me.vripper.entities.ThreadEntity
import me.vripper.model.Settings
import me.vripper.model.ThreadItem
import me.vripper.model.ThreadPostId
import me.vripper.parser.ThreadItem
import me.vripper.services.AppEndpointService
import me.vripper.services.DataTransaction
import me.vripper.services.SettingsService
@@ -33,11 +34,17 @@ class ThreadLookupRunnable(private val threadId: Long, private val settings: Set
val threadLookupResult = threadCacheService[threadId]
if (threadLookupResult.postItemList.isEmpty()) {
val message = "Nothing found for $link"
dataTransaction.saveLog(LogEntry(type = LogEntry.Type.THREAD, status = ERROR, message = message))
dataTransaction.saveLog(
LogEntryEntity(
type = LogEntryEntity.Type.THREAD,
status = ERROR,
message = message
)
)
return
}
dataTransaction.save(
Thread(
ThreadEntity(
title = threadLookupResult.title,
link = link,
threadId = threadId,
@@ -52,8 +59,8 @@ class ThreadLookupRunnable(private val threadId: Long, private val settings: Set
val error = "Error when processing $link"
log.error(error, e)
dataTransaction.saveLog(
LogEntry(
type = LogEntry.Type.THREAD, status = ERROR, message = """
LogEntryEntity(
type = LogEntryEntity.Type.THREAD, status = ERROR, message = """
$error
${e.formatToString()}
""".trimIndent()
@@ -66,7 +73,9 @@ class ThreadLookupRunnable(private val threadId: Long, private val settings: Set
private fun autostart(lookupResult: ThreadItem) {
if (lookupResult.postItemList.size <= settings.downloadSettings.autoQueueThreshold) {
appEndpointService.threadRemove(listOf(lookupResult.threadId))
runBlocking {
appEndpointService.threadRemove(listOf(lookupResult.threadId))
}
CompletableFuture.runAsync(
AddPostRunnable(
lookupResult.postItemList.map { ThreadPostId(it.threadId, it.postId) }
@@ -4,42 +4,44 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import me.vripper.exception.DownloadException
import me.vripper.services.HTTPService
import org.apache.hc.client5.http.classic.methods.HttpGet
import org.apache.hc.core5.http.io.entity.EntityUtils
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.io.path.pathString
object ApplicationProperties : KoinComponent {
const val VERSION: String = "5.4.0"
object ApplicationProperties {
val VERSION: String =
ApplicationProperties.javaClass.getResourceAsStream("/version")
?.use { inputStream -> inputStream.reader().use { it.readText() } } ?: "undefined"
private const val BASE_DIR_NAME: String = "vripper"
private val portable = System.getProperty("vripper.portable", "true").toBoolean()
private val BASE_DIR: String = getBaseDir()
private val httpService: HTTPService by inject()
val VRIPPER_DIR: Path = Path(BASE_DIR, BASE_DIR_NAME)
private val json = Json {
ignoreUnknownKeys = true
}
@Serializable
internal data class ReleaseResponse(@SerialName("tag_name") val tagName: String)
init {
Files.createDirectories(VRIPPER_DIR)
System.setProperty("VRIPPER_DIR", VRIPPER_DIR.toRealPath().pathString)
}
fun latestVersion(): String {
@Serializable
data class ReleaseResponse(@SerialName("tag_name") val tagName: String)
val httpGet = HttpGet("https://api.github.com/repos/death-claw/vripper-project/releases/latest")
return httpService.client.execute(httpGet) {
if (it.code / 100 != 2) {
throw DownloadException("Unexpected response code '${it.code}' for $httpGet")
val request = HttpRequest.newBuilder()
.uri(URI.create("https://api.github.com/repos/death-claw/vripper-project/releases/latest")).build()
return HttpClient.newHttpClient().use {
val response = it.send(request, HttpResponse.BodyHandlers.ofString())
if (response.statusCode() / 100 != 2) {
throw DownloadException("Unexpected response code '${response.statusCode()}' for $request")
}
json.decodeFromString<ReleaseResponse>(EntityUtils.toString(it.entity)).tagName
json.decodeFromString<ReleaseResponse>(response.body()).tagName
}
}
@@ -1,6 +1,6 @@
package me.vripper.utilities
import me.vripper.entities.Image
import me.vripper.entities.ImageEntity
import me.vripper.exception.RenameException
import me.vripper.model.Settings
import java.io.IOException
@@ -31,7 +31,7 @@ object PathUtils {
@Throws(RenameException::class)
fun rename(
imageList: List<Image>,
imageEntityList: List<ImageEntity>,
downloadDirectory: String,
oldFolder: String,
newFolder: String
@@ -43,7 +43,7 @@ object PathUtils {
}
try {
Files.createDirectories(newDownloadDirectory)
imageList.filter { it.filename.isNotBlank() }.forEach {
imageEntityList.filter { it.filename.isNotBlank() }.forEach {
Files.move(
currentDownloadDirectory.resolve(it.filename),
newDownloadDirectory.resolve(it.filename),
@@ -0,0 +1,118 @@
syntax = "proto3";
option java_package = "me.vripper.proto";
import "post.proto";
import "posts.proto";
import "image.proto";
import "images.proto";
import "metadata.proto";
import "log.proto";
import "logs.proto";
import "thread.proto";
import "threads.proto";
import "settings.proto";
message EmptyRequest {}
message EmptyResponse {}
message Links {
string links = 1;
}
message IdList {
repeated int64 ids = 1;
}
message Id {
int64 id = 1;
}
message DownloadSpeed {
int64 speed = 1;
}
message VGUser {
string user = 1;
}
message QueueState {
int32 running = 1;
int32 remaining = 2;
}
message ErrorCount {
int32 count = 1;
}
message TasksRunning {
bool running = 1;
}
message Rename {
int64 postId = 1;
string name = 2;
}
message PostSelection {
int64 threadId = 1;
string threadTitle = 2;
int64 postId = 3;
int32 number = 4;
string title = 5;
int32 imageCount = 6;
string url = 7;
string hosts = 8;
string forum = 10;
repeated string previews = 11;
}
message PostSelectionList {
repeated PostSelection postSelectionList = 1;
}
message ThreadPostId {
int64 threadId = 1;
int64 postId = 2;
}
message ThreadPostIdList {
repeated ThreadPostId threadPostIdList = 1;
}
message ProxyList {
repeated string proxies = 1;
}
message LoggedInUser {
string user = 1;
}
message Version {
string version = 1;
}
service EndpointService {
rpc scanLinks (Links) returns (EmptyResponse) {}
rpc onNewPosts (EmptyRequest) returns (stream Post) {}
rpc onUpdatePosts (EmptyRequest) returns (stream Post) {}
rpc onDeletePosts (EmptyRequest) returns (stream Id) {}
rpc findAllPosts (EmptyRequest) returns (Posts) {}
rpc restartAll (IdList) returns (EmptyResponse) {}
rpc stopAll (IdList) returns (EmptyResponse) {}
rpc findPost (Id) returns (Post) {}
rpc findImagesByPostId (Id) returns (Images) {}
rpc onUpdateImages (Id) returns (stream Image) {}
rpc onDownloadSpeed (EmptyRequest) returns (stream DownloadSpeed) {}
rpc onVGUserUpdate (EmptyRequest) returns (stream VGUser) {}
rpc onQueueStateUpdate (EmptyRequest) returns (stream QueueState) {}
rpc onErrorCountUpdate (EmptyRequest) returns (stream ErrorCount) {}
rpc onTasksRunning (EmptyRequest) returns (stream TasksRunning) {}
rpc onUpdateMetadata (EmptyRequest) returns (stream Metadata) {}
rpc remove (IdList) returns (EmptyResponse) {}
rpc clearCompleted (EmptyRequest) returns (IdList) {}
rpc rename (Rename) returns (EmptyResponse) {}
rpc onStopped (EmptyRequest) returns (stream Id) {}
rpc logClear (EmptyRequest) returns (EmptyResponse) {}
rpc findAllLogs (EmptyRequest) returns (Logs) {}
rpc onNewLog (EmptyRequest) returns (stream Log) {}
rpc onUpdateLog (EmptyRequest) returns (stream Log) {}
rpc onDeleteLogs (EmptyRequest) returns (stream Id) {}
rpc onNewThread (EmptyRequest) returns (stream Thread) {}
rpc onUpdateThread (EmptyRequest) returns (stream Thread) {}
rpc onDeleteThread (EmptyRequest) returns (stream Id) {}
rpc onClearThreads (EmptyRequest) returns (stream EmptyResponse) {}
rpc findAllThreads (EmptyRequest) returns (Threads) {}
rpc threadRemove (IdList) returns (EmptyResponse) {}
rpc threadClear (EmptyRequest) returns (EmptyResponse) {}
rpc grab (Id) returns (PostSelectionList) {}
rpc download (ThreadPostIdList) returns (EmptyResponse) {}
rpc getSettings (EmptyRequest) returns (Settings) {}
rpc saveSettings (Settings) returns (EmptyResponse) {}
rpc getProxies (EmptyRequest) returns (ProxyList) {}
rpc onUpdateSettings (EmptyRequest) returns (stream Settings) {}
rpc loggedInUser (EmptyRequest) returns (LoggedInUser) {}
rpc getVersion (EmptyRequest) returns (Version) {}
}
+16
View File
@@ -0,0 +1,16 @@
syntax = "proto3";
option java_package = "me.vripper.proto";
message Image {
int64 id = 1;
int64 postId = 2;
string url = 3;
string thumbUrl = 4;
int32 host = 5;
int32 index = 6;
int64 postIdRef = 7;
int64 size = 8;
int64 downloaded = 9;
string status = 10;
string filename = 11;
}
+7
View File
@@ -0,0 +1,7 @@
syntax = "proto3";
option java_package = "me.vripper.proto";
import "image.proto";
message Images {
repeated Image images = 1;
}
+10
View File
@@ -0,0 +1,10 @@
syntax = "proto3";
option java_package = "me.vripper.proto";
message Log {
int64 id = 1;
string type = 2;
string status = 3;
string time = 4;
string message = 5;
}
+7
View File
@@ -0,0 +1,7 @@
syntax = "proto3";
option java_package = "me.vripper.proto";
import "log.proto";
message Logs {
repeated Log logs = 1;
}
@@ -0,0 +1,8 @@
syntax = "proto3";
option java_package = "me.vripper.proto";
message Metadata {
int64 postId = 1;
string postedBy = 2;
repeated string resolvedNames = 3;
}
+26
View File
@@ -0,0 +1,26 @@
syntax = "proto3";
option java_package = "me.vripper.proto";
message Post {
int64 id = 1;
string postTitle = 2;
string threadTitle = 3;
string forum = 4;
string url = 5;
string token = 6;
int64 postId = 7;
int64 threadId = 8;
int32 total = 9;
repeated string hosts = 10;
string downloadDirectory = 11;
string addedOn = 12;
string folderName = 13;
string status = 14;
int32 done = 15;
int32 rank = 16;
int64 size = 17;
int64 downloaded = 18;
repeated string previews = 19;
string postedBy = 20;
repeated string resolvedNames = 21;
}
+7
View File
@@ -0,0 +1,7 @@
syntax = "proto3";
option java_package = "me.vripper.proto";
import "post.proto";
message Posts {
repeated Post posts = 1;
}
@@ -0,0 +1,42 @@
syntax = "proto3";
option java_package = "me.vripper.proto";
message ViperSettings {
bool login = 1;
string username = 2;
string password = 3;
bool thanks = 4;
string host = 5;
}
message DownloadSettings {
string downloadPath = 1;
bool autoStart = 2;
int32 autoQueueThreshold = 3;
bool forceOrder = 4;
bool forumSubDirectory = 5;
bool threadSubLocation = 6;
bool clearCompleted = 7;
bool appendPostId = 8;
}
message ConnectionSettings {
int32 maxConcurrentPerHost = 1;
int32 maxGlobalConcurrent = 2;
int32 timeout = 3;
int32 maxAttempts = 4;
}
message SystemSettings {
string tempPath = 1;
bool enableClipboardMonitoring = 2;
int32 clipboardPollingRate = 3;
int32 maxEventLog = 4;
}
message Settings {
ConnectionSettings connectionSettings = 1;
DownloadSettings downloadSettings = 2;
ViperSettings viperSettings = 3;
SystemSettings systemSettings = 4;
}
+10
View File
@@ -0,0 +1,10 @@
syntax = "proto3";
option java_package = "me.vripper.proto";
message Thread {
int64 id = 1;
string title = 2;
string link = 3;
int64 threadId = 4;
int32 total = 5;
}
@@ -0,0 +1,7 @@
syntax = "proto3";
option java_package = "me.vripper.proto";
import "thread.proto";
message Threads {
repeated Thread threads = 1;
}
+1
View File
@@ -0,0 +1 @@
${project.version}
+43 -16
View File
@@ -28,6 +28,10 @@
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core-jvm</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-javafx</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-serialization-json</artifactId>
@@ -127,6 +131,22 @@
<groupId>me.vripper</groupId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-kotlin-stub</artifactId>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-kotlin</artifactId>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
</dependency>
</dependencies>
<build>
@@ -173,27 +193,34 @@
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>me.vripper.gui.VripperGuiApplicationKt</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<goals>
<goal>single</goal>
</goals>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<shadedArtifactAttached>true</shadedArtifactAttached>
<shadedClassifierName>jar-with-dependencies</shadedClassifierName>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>me.vripper.gui.VripperGuiApplicationKt</mainClass>
<manifestEntries>
<Implementation-Version>${project.version}</Implementation-Version>
<Main-Class>me.vripper.gui.VripperGuiApplicationKt</Main-Class>
</manifestEntries>
</transformer>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
</configuration>
</execution>
</executions>
<groupId>org.apache.maven.plugins</groupId>
</plugin>
</plugins>
</build>
@@ -1,12 +1,15 @@
package me.vripper.gui
import me.vripper.gui.services.ClipboardService
import me.vripper.gui.services.GrpcEndpointService
import me.vripper.services.IAppEndpointService
import org.koin.core.qualifier.named
import org.koin.dsl.bind
import org.koin.dsl.module
val guiModule = module {
single<ClipboardService> {
ClipboardService(get(), get(), get())
}
single((named("remoteAppEndpointService"))) {
GrpcEndpointService()
} bind IAppEndpointService::class
}
val modules = module {
@@ -4,19 +4,16 @@ import atlantafx.base.theme.CupertinoDark
import atlantafx.base.theme.CupertinoLight
import javafx.application.Application
import javafx.scene.image.Image
import javafx.scene.text.Font
import javafx.stage.Stage
import javafx.stage.WindowEvent
import kotlinx.coroutines.*
import me.vripper.gui.components.views.LoadingView
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.ApplicationInitialized
import me.vripper.gui.event.GuiEventBus
import me.vripper.listeners.AppLock
import me.vripper.utilities.ApplicationProperties.VRIPPER_DIR
import me.vripper.utilities.DbUtils
import org.jetbrains.exposed.sql.Database
import org.koin.core.context.GlobalContext
import org.koin.core.context.startKoin
import org.koin.core.qualifier.named
import tornadofx.App
import tornadofx.DIContainer
import tornadofx.FX
@@ -26,18 +23,17 @@ import kotlin.system.exitProcess
class VripperGuiApplication : App(
LoadingView::class
) { //The application class must be a TornadoFX application, and it must have the main view
) {
private var initialized = false
private val widgetsController: WidgetsController by inject()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val coroutineScope = CoroutineScope(SupervisorJob())
init {
APP_INSTANCE = this
}
override fun start(stage: Stage) {
Font.loadFont(this.javaClass.getResource("/Inter.ttf")!!.toExternalForm(), 12.0)
if (widgetsController.currentSettings.darkMode) {
setUserAgentStylesheet(CupertinoDark().userAgentStylesheet)
} else {
@@ -74,8 +70,6 @@ class VripperGuiApplication : App(
}
}
stage.addEventFilter(WindowEvent.WINDOW_SHOWN) {
Database.connect("jdbc:h2:file:$VRIPPER_DIR/vripper;DB_CLOSE_DELAY=-1;LOCK_TIMEOUT=30000;")
DbUtils.update()
startKoin {
modules(modules)
}
@@ -83,9 +77,15 @@ class VripperGuiApplication : App(
object : DIContainer {
override fun <T : Any> getInstance(type: KClass<T>): T =
GlobalContext.get().get(type)
override fun <T : Any> getInstance(type: KClass<T>, name: String): T =
GlobalContext.get().get(type, named(name))
}
fire(ApplicationInitialized)
initialized = true
coroutineScope.launch {
GuiEventBus.publishEvent(GuiEventBus.ApplicationInitialized)
initialized = true
}
}
super.start(stage)
}
@@ -102,6 +102,7 @@ class VripperGuiApplication : App(
}
fun main(args: Array<String>) {
System.setProperty("prism.lcdtext", "false");
AppLock.exclusiveLock()
Application.launch(VripperGuiApplication::class.java, *args)
}
@@ -13,10 +13,10 @@ class PreviewTableCell<T> : TableCell<T, ObservableList<String>>() {
override fun updateItem(item: ObservableList<String>?, empty: Boolean) {
super.updateItem(item, empty)
if (!empty) {
graphic = FontIcon.of(Feather.IMAGE)
graphic = if (!empty) {
FontIcon.of(Feather.IMAGE)
} else {
graphic = null
null
}
}
}
@@ -3,6 +3,7 @@ package me.vripper.gui.components.fragments
import javafx.geometry.Insets
import javafx.scene.control.TabPane
import javafx.scene.text.FontWeight
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.utils.openLink
import me.vripper.utilities.ApplicationProperties
import tornadofx.*
@@ -10,6 +11,8 @@ import kotlin.io.path.pathString
class AboutFragment : Fragment("About") {
private val widgetsController: WidgetsController by inject()
override val root = tabpane()
init {
@@ -48,6 +51,19 @@ class AboutFragment : Fragment("About") {
isEditable = false
}
}
field("Previews Path") {
textfield(widgetsController.currentSettings.cachePathProperty) {
isEditable = false
}
button("Browse") {
action {
val directory = chooseDirectory(title = "Select previews folder")
if (directory != null) {
widgetsController.currentSettings.cachePathProperty.set(directory.path)
}
}
}
}
}
}
}
@@ -1,158 +0,0 @@
package me.vripper.gui.components.fragments
import javafx.beans.property.SimpleBooleanProperty
import javafx.geometry.Orientation
import javafx.scene.control.ButtonType
import javafx.scene.control.ContentDisplay
import javafx.scene.input.KeyCode
import javafx.scene.input.KeyCodeCombination
import javafx.scene.input.KeyCombination
import kotlinx.coroutines.*
import me.vripper.gui.components.views.PostsTableView
import me.vripper.gui.controller.GlobalStateController
import me.vripper.gui.controller.PostController
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
class ActionBarFragment : Fragment() {
private val downloadActiveProperty = SimpleBooleanProperty(true)
private val postController: PostController by inject()
private val globalStateController: GlobalStateController by inject()
private val postsTableView: PostsTableView by inject()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override val root = toolbar {
id = "action_toolbar"
padding = insets(all = 5)
button("Add links", FontIcon.of(Feather.PLUS)) {
contentDisplay = ContentDisplay.GRAPHIC_ONLY
tooltip("Add links [Ctrl+L]")
shortcut(KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN))
action {
find<AddLinksFragment>().apply {
input.clear()
}.openModal()
}
}
separator(Orientation.VERTICAL)
button("Start All", FontIcon.of(Feather.SKIP_FORWARD)) {
contentDisplay = ContentDisplay.GRAPHIC_ONLY
tooltip("Start downloads [Ctrl+S]")
shortcut(KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN))
disableWhen(downloadActiveProperty)
action {
postController.startAll()
}
}
button("Stop All", FontIcon.of(Feather.SQUARE)) {
contentDisplay = ContentDisplay.GRAPHIC_ONLY
tooltip("Stops all running downloads [Ctrl+Q]")
shortcut(KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN))
disableWhen(downloadActiveProperty.not())
action {
postController.stopAll()
}
}
separator(Orientation.VERTICAL)
button("Start", FontIcon.of(Feather.PLAY)) {
contentDisplay = ContentDisplay.GRAPHIC_ONLY
tooltip("Start downloads for selected [Ctrl+Shift+S]")
shortcut(KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN))
enableWhen(
postsTableView.tableView.selectionModel.selectedItems.sizeProperty.greaterThan(
0
)
)
action {
postsTableView.startSelected()
}
}
button("Stop", FontIcon.of(Feather.SQUARE)) {
contentDisplay = ContentDisplay.GRAPHIC_ONLY
tooltip("Stop downloads for selected [Ctrl+Shift+Q]")
shortcut(KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN))
enableWhen(
postsTableView.tableView.selectionModel.selectedItems.sizeProperty.greaterThan(
0
)
)
action {
postsTableView.stopSelected()
}
}
button("Rename", FontIcon.of(Feather.EDIT)) {
contentDisplay = ContentDisplay.GRAPHIC_ONLY
tooltip("Rename selected [Ctrl+R]")
shortcut(KeyCodeCombination(KeyCode.R, KeyCombination.CONTROL_DOWN))
enableWhen(
postsTableView.tableView.selectionModel.selectedItems.sizeProperty.greaterThan(
0
)
)
action {
postsTableView.renameSelected()
}
}
button("Delete", FontIcon.of(Feather.TRASH)) {
contentDisplay = ContentDisplay.GRAPHIC_ONLY
tooltip("Delete selected posts [Del]")
shortcut(KeyCodeCombination(KeyCode.DELETE))
enableWhen(
postsTableView.tableView.selectionModel.selectedItems.sizeProperty.greaterThan(
0
)
)
action {
postsTableView.deleteSelected()
}
}
separator(Orientation.VERTICAL)
button("Clear", FontIcon.of(Feather.TRASH_2)) {
contentDisplay = ContentDisplay.GRAPHIC_ONLY
tooltip("Clear all finished downloads [Ctrl+Del]")
shortcut(KeyCodeCombination(KeyCode.DELETE, KeyCombination.CONTROL_DOWN))
action {
confirm(
"",
"Confirm removal of finished posts?",
ButtonType.YES,
ButtonType.NO,
owner = primaryStage,
title = "Clean finished posts"
) {
coroutineScope.launch {
val clearPosts = postController.clearPosts().await()
coroutineScope.launch {
runLater {
postsTableView.tableView.items.removeIf { clearPosts.contains(it.postId) }
}
}
}
}
}
}
button("Settings", FontIcon.of(Feather.SETTINGS)) {
contentDisplay = ContentDisplay.GRAPHIC_ONLY
tooltip("Open settings menu [S]")
shortcut(KeyCodeCombination(KeyCode.S))
action {
find<SettingsFragment>().openModal()?.apply {
minWidth = 700.0
minHeight = 400.0
}
}
}
}
override fun onDock() {
downloadActiveProperty.bind(globalStateController.globalState.runningProperty.greaterThan(0))
}
override fun onUndock() {
coroutineScope.cancel()
}
}
@@ -6,11 +6,16 @@ import javafx.geometry.Pos
import javafx.scene.control.TextArea
import javafx.scene.layout.Priority
import javafx.scene.layout.VBox
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import me.vripper.gui.controller.PostController
import tornadofx.*
class AddLinksFragment : Fragment("Add thread links") {
private val coroutineScope = CoroutineScope(SupervisorJob())
private val textAreaProperty = SimpleStringProperty()
private val postController: PostController by inject()
lateinit var input: TextArea
@@ -27,9 +32,15 @@ class AddLinksFragment : Fragment("Add thread links") {
addClass(Styles.ACCENT)
disableWhen(textAreaProperty.isEmpty)
action {
postController.scan(textAreaProperty.value)
coroutineScope.launch {
postController.scan(textAreaProperty.value)
}
close()
}
}
}
override fun onUndock() {
coroutineScope.cancel()
}
}
@@ -1,51 +1,63 @@
package me.vripper.gui.components.fragments
import atlantafx.base.util.IntegerStringConverter
import javafx.scene.control.Spinner
import kotlinx.coroutines.*
import me.vripper.gui.controller.SettingsController
import me.vripper.gui.model.settings.ConnectionSettingsModel
import me.vripper.model.ConnectionSettings
import tornadofx.*
class ConnectionSettingsFragment : Fragment("Connection Settings") {
private val settingsController: SettingsController by inject()
private val connectionSettings = settingsController.findConnectionSettings()
private val coroutineScope = CoroutineScope(SupervisorJob())
private lateinit var connectionSettings: ConnectionSettings
val connectionSettingsModel = ConnectionSettingsModel()
override fun onDock() {
}
override val root = vbox {}
override val root = vbox {
form {
fieldset {
field("Concurrent downloads per host") {
add(Spinner<Int>(1, 4, connectionSettings.maxConcurrentPerHost).apply {
connectionSettingsModel.maxThreadsProperty.bind(valueProperty())
isEditable = true
IntegerStringConverter.createFor(this)
})
}
field("Global concurrent downloads") {
add(Spinner<Int>(0, 24, connectionSettings.maxGlobalConcurrent).apply {
connectionSettingsModel.maxTotalThreadsProperty.bind(valueProperty())
isEditable = true
IntegerStringConverter.createFor(this)
})
}
field("Connection timeout (s)") {
add(Spinner<Int>(1, 300, connectionSettings.timeout.toInt()).apply {
connectionSettingsModel.timeoutProperty.bind(valueProperty())
isEditable = true
IntegerStringConverter.createFor(this)
})
}
field("Maximum attempts") {
add(Spinner<Int>(1, 10, connectionSettings.maxAttempts).apply {
connectionSettingsModel.maxAttemptsProperty.bind(valueProperty())
isEditable = true
IntegerStringConverter.createFor(this)
})
init {
coroutineScope.launch {
async { connectionSettings = settingsController.findConnectionSettings() }.await()
runLater {
with(root) {
form {
fieldset {
field("Concurrent downloads per host") {
add(Spinner<Int>(1, 4, connectionSettings.maxConcurrentPerHost).apply {
connectionSettingsModel.maxThreadsProperty.bind(valueProperty())
isEditable = true
atlantafx.base.util.IntegerStringConverter.createFor(this)
})
}
field("Global concurrent downloads") {
add(Spinner<Int>(0, 24, connectionSettings.maxGlobalConcurrent).apply {
connectionSettingsModel.maxTotalThreadsProperty.bind(valueProperty())
isEditable = true
atlantafx.base.util.IntegerStringConverter.createFor(this)
})
}
field("Connection timeout (s)") {
add(Spinner<Int>(1, 300, connectionSettings.timeout).apply {
connectionSettingsModel.timeoutProperty.bind(valueProperty())
isEditable = true
atlantafx.base.util.IntegerStringConverter.createFor(this)
})
}
field("Maximum attempts") {
add(Spinner<Int>(1, 10, connectionSettings.maxAttempts).apply {
connectionSettingsModel.maxAttemptsProperty.bind(valueProperty())
isEditable = true
atlantafx.base.util.IntegerStringConverter.createFor(this)
})
}
}
}
}
}
}
}
override fun onUndock() {
coroutineScope.cancel()
}
}
@@ -2,80 +2,99 @@ package me.vripper.gui.components.fragments
import atlantafx.base.util.IntegerStringConverter
import javafx.scene.control.Spinner
import kotlinx.coroutines.*
import me.vripper.gui.controller.SettingsController
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.model.settings.DownloadSettingsModel
import me.vripper.model.DownloadSettings
import tornadofx.*
class DownloadSettingsFragment : Fragment("Download Settings") {
private val settingsController: SettingsController by inject()
private val downloadSettings = settingsController.findDownloadSettings()
private val widgetsController: WidgetsController by inject()
private val coroutineScope = CoroutineScope(SupervisorJob())
private lateinit var downloadSettings: DownloadSettings
val downloadSettingsModel = DownloadSettingsModel()
override fun onDock() {
downloadSettingsModel.downloadPath = downloadSettings.downloadPath
downloadSettingsModel.autoStart = downloadSettings.autoStart
downloadSettingsModel.forceOrder = downloadSettings.forceOrder
downloadSettingsModel.forumSubfolder = downloadSettings.forumSubDirectory
downloadSettingsModel.threadSubLocation = downloadSettings.threadSubLocation
downloadSettingsModel.clearCompleted = downloadSettings.clearCompleted
downloadSettingsModel.appendPostId = downloadSettings.appendPostId
}
override val root = vbox {}
override val root = vbox {
form {
fieldset {
field("Download Path") {
textfield(downloadSettingsModel.downloadPathProperty) {
isEditable = false
}
button("Browse") {
action {
val directory = chooseDirectory(title = "Select download folder")
if (directory != null) {
downloadSettingsModel.downloadPathProperty.set(directory.path)
init {
coroutineScope.launch {
async {
downloadSettings = settingsController.findDownloadSettings()
downloadSettingsModel.downloadPath = downloadSettings.downloadPath
downloadSettingsModel.autoStart = downloadSettings.autoStart
downloadSettingsModel.forceOrder = downloadSettings.forceOrder
downloadSettingsModel.forumSubfolder = downloadSettings.forumSubDirectory
downloadSettingsModel.threadSubLocation = downloadSettings.threadSubLocation
downloadSettingsModel.clearCompleted = downloadSettings.clearCompleted
downloadSettingsModel.appendPostId = downloadSettings.appendPostId
}.await()
runLater {
with(root) {
form {
fieldset {
field("Download Path") {
textfield(downloadSettingsModel.downloadPathProperty) {
editableWhen(widgetsController.currentSettings.localSessionProperty.not())
}
button("Browse") {
visibleWhen(widgetsController.currentSettings.localSessionProperty)
action {
val directory = chooseDirectory(title = "Select download folder")
if (directory != null) {
downloadSettingsModel.downloadPathProperty.set(directory.path)
}
}
}
}
field("Auto start downloads") {
checkbox {
bind(downloadSettingsModel.autoStartProperty)
}
}
field("Auto queue thread if post count is below or equal to") {
add(Spinner<Int>(1, Int.MAX_VALUE, downloadSettings.autoQueueThreshold).apply {
downloadSettingsModel.autoQueueThresholdProperty.bind(valueProperty())
isEditable = true
IntegerStringConverter.createFor(this)
})
}
field("Organize by category") {
checkbox {
bind(downloadSettingsModel.forumSubfolderProperty)
}
}
field("Organize by thread") {
checkbox {
bind(downloadSettingsModel.threadSubLocationProperty)
}
}
field("Order images") {
checkbox {
bind(downloadSettingsModel.forceOrderProperty)
}
}
field("Append post id to download folder") {
checkbox {
bind(downloadSettingsModel.appendPostIdProperty)
}
}
field("Clear Finished") {
checkbox {
bind(downloadSettingsModel.clearCompletedProperty)
}
}
}
}
}
field("Auto start downloads") {
checkbox {
bind(downloadSettingsModel.autoStartProperty)
}
}
field("Auto queue thread if post count is below or equal to") {
add(Spinner<Int>(1, Int.MAX_VALUE, downloadSettings.autoQueueThreshold).apply {
downloadSettingsModel.autoQueueThresholdProperty.bind(valueProperty())
isEditable = true
IntegerStringConverter.createFor(this)
})
}
field("Organize by category") {
checkbox {
bind(downloadSettingsModel.forumSubfolderProperty)
}
}
field("Organize by thread") {
checkbox {
bind(downloadSettingsModel.threadSubLocationProperty)
}
}
field("Order images") {
checkbox {
bind(downloadSettingsModel.forceOrderProperty)
}
}
field("Append post id to download folder") {
checkbox {
bind(downloadSettingsModel.appendPostIdProperty)
}
}
field("Clear Finished") {
checkbox {
bind(downloadSettingsModel.clearCompletedProperty)
}
}
}
}
}
override fun onUndock() {
coroutineScope.cancel()
}
}
@@ -1,251 +0,0 @@
package me.vripper.gui.components.fragments
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.event.EventHandler
import javafx.geometry.Pos
import javafx.scene.control.*
import javafx.scene.control.cell.TextFieldTableCell
import javafx.scene.input.MouseButton
import javafx.util.Callback
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.map
import me.vripper.entities.Image
import me.vripper.entities.domain.Status
import me.vripper.event.EventBus
import me.vripper.event.ImageEvent
import me.vripper.event.StoppedEvent
import me.vripper.gui.components.cells.PreviewTableCell
import me.vripper.gui.components.cells.ProgressTableCell
import me.vripper.gui.components.cells.StatusTableCell
import me.vripper.gui.controller.ImageController
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.model.ImageModel
import me.vripper.gui.utils.Preview
import me.vripper.gui.utils.openLink
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
class ImagesTableFragment : Fragment("Photos") {
private lateinit var tableView: TableView<ImageModel>
private val imageController: ImageController by inject()
private val widgetsController: WidgetsController by inject()
private val eventBus: EventBus by di()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val items: ObservableList<ImageModel> = FXCollections.observableArrayList()
private var preview: Preview? = null
fun setPostId(postId: Long) {
coroutineScope.launch {
val list = coroutineScope.async {
imageController.findImages(postId)
}.await()
runLater {
items.addAll(list)
tableView.sort()
tableView.placeholder = Label("No content in table")
}
}
coroutineScope.launch {
eventBus.events.filterIsInstance(ImageEvent::class).map {
it.copy(images = it.images.filter { image: Image -> image.postId == postId })
}.filter {
it.images.isNotEmpty()
}.collect { imageEvent ->
runLater {
for (image in imageEvent.images) {
val imageModel = items.find { it.id == image.id } ?: continue
imageModel.size = image.size
imageModel.status = image.status.name
imageModel.filename = image.filename
imageModel.downloaded = image.downloaded
imageModel.progress = imageController.progress(
image.size, image.downloaded
)
}
}
}
}
coroutineScope.launch {
eventBus.events.filterIsInstance(StoppedEvent::class).collect { stoppedEvent ->
runLater {
items.forEach { imageModel ->
if (imageModel.status != Status.FINISHED.name) {
imageModel.status = Status.STOPPED.name
}
}
}
}
}
}
override fun onDock() {
tableView.prefWidthProperty().bind(root.widthProperty())
tableView.prefHeightProperty().bind(root.heightProperty())
modalStage?.width = 550.0
}
override val root = vbox(alignment = Pos.CENTER_RIGHT) {
tableView = tableview(items) {
setRowFactory {
val tableRow = TableRow<ImageModel>()
val urlItem = MenuItem("Open link").apply {
setOnAction {
openLink(tableRow.item.url)
}
graphic = FontIcon.of(Feather.LINK)
}
val contextMenu = ContextMenu()
contextMenu.items.addAll(urlItem)
tableRow.contextMenuProperty().bind(tableRow.emptyProperty()
.map { empty -> if (empty) null else contextMenu })
tableRow
}
contextMenu = ContextMenu()
contextMenu.items.add(MenuItem("Setup columns").apply {
setOnAction {
find<ColumnSelectionFragment>(
mapOf(
ColumnSelectionFragment::map to mapOf(
Pair(
"Preview",
widgetsController.currentSettings.imagesColumnsModel.previewProperty
),
Pair(
"Index",
widgetsController.currentSettings.imagesColumnsModel.indexProperty
),
Pair(
"Link",
widgetsController.currentSettings.imagesColumnsModel.linkProperty
),
Pair(
"Progress",
widgetsController.currentSettings.imagesColumnsModel.progressProperty
),
Pair(
"Filename",
widgetsController.currentSettings.imagesColumnsModel.filenameProperty
),
Pair(
"Status",
widgetsController.currentSettings.imagesColumnsModel.statusProperty
),
Pair(
"Size",
widgetsController.currentSettings.imagesColumnsModel.sizeProperty
),
Pair(
"Downloaded",
widgetsController.currentSettings.imagesColumnsModel.downloadedProperty
),
)
)
).openModal()
}
graphic = FontIcon.of(Feather.COLUMNS)
})
column("Preview", ImageModel::thumbUrlProperty) {
visibleProperty().bind(widgetsController.currentSettings.imagesColumnsModel.previewProperty)
prefWidth = 100.0
cellFactory = Callback {
val cell = PreviewTableCell<ImageModel>()
cell.onMouseExited = EventHandler {
preview?.hide()
}
cell.onMouseMoved = EventHandler {
preview?.previewPopup?.apply {
x = it.screenX + 20
y = it.screenY + 10
}
}
cell.onMouseEntered = EventHandler { mouseEvent ->
preview?.hide()
if (cell.tableRow.item != null && cell.tableRow.item.thumbUrl.isNotEmpty()) {
preview = Preview(currentStage!!, cell.tableRow.item.thumbUrl)
preview?.previewPopup?.apply {
x = mouseEvent.screenX + 20
y = mouseEvent.screenY + 10
}
}
}
cell
}
}
column("Index", ImageModel::indexProperty) {
visibleProperty().bind(widgetsController.currentSettings.imagesColumnsModel.indexProperty)
prefWidth = 100.0
sortOrder.add(this)
cellFactory = Callback {
TextFieldTableCell<ImageModel?, Number?>().apply { alignment = Pos.CENTER_LEFT }
}
}
column("Link", ImageModel::urlProperty) {
visibleProperty().bind(widgetsController.currentSettings.imagesColumnsModel.linkProperty)
prefWidth = 200.0
cellFactory = Callback {
TextFieldTableCell<ImageModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
}
}
column("Progress", ImageModel::progressProperty) {
visibleProperty().bind(widgetsController.currentSettings.imagesColumnsModel.progressProperty)
prefWidth = 100.0
cellFactory = Callback {
val cell = ProgressTableCell<ImageModel>()
cell.alignment = Pos.CENTER
cell.setOnMouseClick {
when (it.clickCount) {
1 -> {
this@tableview.requestFocus()
this@tableview.focusModel.focus(cell.tableRow.index)
if (it.button.equals(MouseButton.PRIMARY)) {
this@tableview.selectionModel.clearSelection()
this@tableview.selectionModel.select(cell.tableRow.index)
}
}
}
}
cell as TableCell<ImageModel, Number>
}
}
column("Filename", ImageModel::filenameProperty) {
visibleProperty().bind(widgetsController.currentSettings.imagesColumnsModel.filenameProperty)
prefWidth = 150.0
cellFactory = Callback {
TextFieldTableCell<ImageModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
}
}
column("Status", ImageModel::statusProperty) {
visibleProperty().bind(widgetsController.currentSettings.imagesColumnsModel.statusProperty)
prefWidth = 100.0
cellFactory = Callback {
StatusTableCell()
}
}
column("Size", ImageModel::sizeProperty) {
visibleProperty().bind(widgetsController.currentSettings.imagesColumnsModel.sizeProperty)
prefWidth = 75.0
cellFactory = Callback {
TextFieldTableCell<ImageModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
}
}
column("Downloaded", ImageModel::downloadedProperty) {
visibleProperty().bind(widgetsController.currentSettings.imagesColumnsModel.downloadedProperty)
prefWidth = 125.0
cellFactory = Callback {
TextFieldTableCell<ImageModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
}
}
}
}
override fun onUndock() {
coroutineScope.cancel()
}
}
@@ -1,131 +0,0 @@
package me.vripper.gui.components.fragments
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.map
import me.vripper.entities.Metadata
import me.vripper.entities.Post
import me.vripper.event.EventBus
import me.vripper.event.PostUpdateEvent
import me.vripper.gui.controller.PostController
import me.vripper.gui.model.PostModel
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
class PostInfoPanelFragment : Fragment() {
private val postController: PostController by inject()
private val eventBus: EventBus by di()
private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val imagesTableFragment: ImagesTableFragment = find<ImagesTableFragment>()
private val postModel: PostModel = PostModel(
-1, "", 0.0, "", "", 0, 0, "", "", -1, "", "", "", emptyList(), Metadata(-1, Metadata.Data("", emptyList()))
)
override val root = tabpane()
init {
with(root) {
id = "postinfo_panel"
tabClosingPolicy = javafx.scene.control.TabPane.TabClosingPolicy.UNAVAILABLE
tab("General") {
graphic = FontIcon(Feather.INFO)
scrollpane {
form {
fieldset {
field("Post Link:") {
textfield(postModel.urlProperty) {
isEditable = false
visibleWhen(postModel.urlProperty.isNotEmpty)
}
}
field("Posted By:") {
label(postModel.postedByProperty)
}
field("Title:") {
label(postModel.titleProperty)
}
field("More titles:") {
label(postModel.altTitlesProperty.map { it.joinToString(", ") })
}
field("Status:") {
label(postModel.statusProperty.map { it ->
it.lowercase().replaceFirstChar { it.uppercase() }
})
}
field("Path:") {
label(postModel.pathProperty)
}
field("Total:") {
label(postModel.progressCountProperty)
}
field("Hosts:") {
label(postModel.hostsProperty)
}
field("Added On:") {
label(postModel.addedOnProperty)
}
}
}
}
}
tab("Photos") {
graphic = FontIcon(Feather.IMAGE)
add(imagesTableFragment)
}
}
}
fun setPostId(postId: Long) {
imagesTableFragment.setPostId(postId)
coroutineScope.launch {
val model = postController.find(postId).await()
postModel.apply {
this.postId = model.postId
this.title = model.title
this.progress = model.progress
this.status = model.status.lowercase().replaceFirstChar { it.uppercase() }
this.url = model.url
this.done = model.done
this.total = model.total
this.hosts = model.hosts
this.addedOn = model.addedOn
this.order = model.order
this.path = model.path
this.folderName = model.folderName
this.progressCount = model.progressCount
this.previewList = model.previewList
this.altTitles = model.altTitles
this.postedby = model.postedby
}
}
coroutineScope.launch {
eventBus.events.filterIsInstance(PostUpdateEvent::class)
.map { event -> event.posts.lastOrNull { it.postId == postId } }.collect(::updatePostModel)
}
}
fun updatePostModel(post: Post?) {
if (post == null) return
runLater {
postModel.status = post.status.stringValue.lowercase().replaceFirstChar { it.uppercase() }
postModel.progressCount = postController.progressCount(
post.total, post.done, post.downloaded
)
postModel.order = post.rank + 1
postModel.done = post.done
postModel.progress = postController.progress(
post.total, post.done
)
postModel.path = post.getDownloadFolder()
postModel.folderName = post.folderName
}
}
override fun onUndock() {
imagesTableFragment.onUndock()
coroutineScope.cancel()
}
}
@@ -3,6 +3,7 @@ package me.vripper.gui.components.fragments
import javafx.beans.property.SimpleStringProperty
import javafx.geometry.Pos
import javafx.scene.control.ComboBox
import kotlinx.coroutines.*
import me.vripper.gui.controller.PostController
import tornadofx.*
@@ -11,6 +12,7 @@ class RenameFragment : Fragment("Rename download post") {
val postId: Long by param()
val name: String by param()
val altTitles: List<String> by param()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val textInputProperty = SimpleStringProperty()
private val postController: PostController by inject()
private lateinit var comboBox: ComboBox<String>
@@ -35,9 +37,15 @@ class RenameFragment : Fragment("Rename download post") {
button("Rename") {
disableWhen(comboBox.editor.textProperty().isEmpty)
action {
postController.rename(postId, comboBox.editor.text.trim())
coroutineScope.launch {
postController.rename(postId, comboBox.editor.text.trim())
}
close()
}
}
}
override fun onUndock() {
coroutineScope.cancel()
}
}
@@ -0,0 +1,144 @@
package me.vripper.gui.components.fragments
import atlantafx.base.theme.Styles
import atlantafx.base.util.IntegerStringConverter
import io.grpc.ConnectivityState
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleStringProperty
import javafx.geometry.Pos
import javafx.scene.control.RadioButton
import javafx.scene.control.Spinner
import javafx.scene.control.ToggleGroup
import javafx.scene.layout.VBox
import kotlinx.coroutines.*
import me.vripper.gui.VripperGuiApplication
import me.vripper.gui.components.views.AppView
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.services.DatabaseManager
import me.vripper.gui.services.GrpcEndpointService
import tornadofx.*
class SessionFragment : Fragment("Change Session") {
val component: UIComponent by param()
private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val widgetsController: WidgetsController by inject()
private val grpcEndpointService: GrpcEndpointService by di("remoteAppEndpointService")
private val toggleGroup = ToggleGroup()
private val disableConfirm = SimpleBooleanProperty(false)
private val message = SimpleStringProperty()
override val root = VBox().apply {
alignment = Pos.CENTER
padding = insets(all = 5)
spacing = 5.0
}
init {
with(root) {
form {
fieldset("Source") {
radiobutton(text = "Start Local Session", group = toggleGroup) {
id = "localSession"
isSelected = widgetsController.currentSettings.localSession
}
val remoteRadio = radiobutton(text = "Connect to Remote Session", group = toggleGroup) {
id = "remoteSession"
isSelected = !widgetsController.currentSettings.localSession
}
field("Host") {
enableWhen { remoteRadio.selectedProperty() }
textfield(widgetsController.currentSettings.remoteSessionModel.hostProperty) {}
}
field("Port") {
enableWhen { remoteRadio.selectedProperty() }
add(Spinner<Int>(0, 65535, widgetsController.currentSettings.remoteSessionModel.port).apply {
widgetsController.currentSettings.remoteSessionModel.portProperty.bind(valueProperty())
isEditable = true
IntegerStringConverter.createFor(this)
})
}
}
}
borderpane {
left {
padding = insets(all = 5.0)
label(message) {
disableWhen { message.isBlank() }
}
}
right {
padding = insets(all = 5.0)
button("Ok") {
enableWhen { toggleGroup.selectedToggleProperty().isNotNull.and(disableConfirm.not()) }
addClass(Styles.ACCENT)
isDefaultButton = true
action {
val selectedToggle = toggleGroup.selectedToggle
if (selectedToggle == null) {
VripperGuiApplication.APP_INSTANCE.stop()
}
when ((selectedToggle as RadioButton).id) {
"localSession" -> {
coroutineScope.launch {
runLater {
disableConfirm.set(true)
message.set("Starting...")
}
widgetsController.currentSettings.localSession = true
DatabaseManager.connect()
GuiEventBus.publishEvent(GuiEventBus.ChangingSession)
grpcEndpointService.disconnect()
GuiEventBus.publishEvent(GuiEventBus.LocalSession)
runLater {
component.replaceWith(find<AppView>())
close()
}
}
}
"remoteSession" -> {
coroutineScope.launch {
runLater {
disableConfirm.set(true)
message.set("Connecting...")
}
widgetsController.currentSettings.localSession = false
DatabaseManager.disconnect()
GuiEventBus.publishEvent(GuiEventBus.ChangingSession)
grpcEndpointService.disconnect()
grpcEndpointService.connect(
widgetsController.currentSettings.remoteSessionModel.host,
widgetsController.currentSettings.remoteSessionModel.port
)
repeat(10) {
if (grpcEndpointService.connectionState() == ConnectivityState.READY) {
GuiEventBus.publishEvent(GuiEventBus.RemoteSession)
runLater {
component.replaceWith(find<AppView>())
close()
}
cancel()
}
delay(333)
}
runLater {
disableConfirm.set(false)
message.set("Unable to connect")
}
}
}
else -> VripperGuiApplication.APP_INSTANCE.stop()
}
}
}
}
}
}
}
override fun onUndock() {
coroutineScope.cancel()
}
}
@@ -6,6 +6,10 @@ import javafx.scene.control.Alert
import javafx.scene.control.TabPane
import javafx.scene.layout.Priority
import javafx.scene.layout.VBox
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import me.vripper.exception.ValidationException
import me.vripper.gui.controller.SettingsController
import org.kordamp.ikonli.feather.Feather
@@ -16,7 +20,7 @@ import tornadofx.*
class SettingsFragment : Fragment("Settings") {
private val settingsController: SettingsController by inject()
private val coroutineScope = CoroutineScope(SupervisorJob())
private val downloadSettingsFragment: DownloadSettingsFragment = find()
private val connectionSettingsFragment: ConnectionSettingsFragment = find()
private val viperSettingsFragment: ViperSettingsFragment = find()
@@ -52,21 +56,28 @@ class SettingsFragment : Fragment("Settings") {
addClass(Styles.ACCENT)
isDefaultButton = true
action {
try {
settingsController.saveNewSettings(
downloadSettingsFragment.downloadSettingsModel,
connectionSettingsFragment.connectionSettingsModel,
viperSettingsFragment.viperSettingsModel,
systemSettingsFragment.systemSettingsModel
)
close()
} catch (e: ValidationException) {
alert(Alert.AlertType.ERROR, "Invalid settings", e.message)
coroutineScope.launch {
try {
settingsController.saveNewSettings(
downloadSettingsFragment.downloadSettingsModel,
connectionSettingsFragment.connectionSettingsModel,
viperSettingsFragment.viperSettingsModel,
systemSettingsFragment.systemSettingsModel
)
runLater {
close()
}
} catch (e: ValidationException) {
alert(Alert.AlertType.ERROR, "Invalid settings", e.message)
}
}
}
}
}
}
}
override fun onUndock() {
coroutineScope.cancel()
}
}
@@ -1,43 +0,0 @@
package me.vripper.gui.components.fragments
import javafx.geometry.Orientation
import javafx.geometry.Pos
import me.vripper.gui.controller.GlobalStateController
import tornadofx.*
class StatusBarFragment : Fragment("Status bar") {
private val globalStateController: GlobalStateController by inject()
override val root = borderpane {
id = "statusbar"
left {
hbox {
text(globalStateController.globalState.loggedUserProperty.map { "Logged in as: $it" }) {
visibleWhen(globalStateController.globalState.loggedUserProperty.isNotBlank())
}
}
}
right {
padding = insets(right = 5, left = 5, top = 3, bottom = 3)
hbox {
spacing = 3.0
progressbar {
visibleWhen(globalStateController.globalState.loadingProperty)
}
separator(Orientation.VERTICAL)
text(globalStateController.globalState.downloadSpeedProperty.map { "$it/s" })
separator(Orientation.VERTICAL)
label("Downloading")
text(globalStateController.globalState.runningProperty.asString())
separator(Orientation.VERTICAL)
label("Pending")
text(globalStateController.globalState.remainingProperty.asString())
separator(Orientation.VERTICAL)
label("Error")
text(globalStateController.globalState.errorProperty.asString())
alignment = Pos.CENTER_RIGHT
}
}
}
}
@@ -3,75 +3,84 @@ package me.vripper.gui.components.fragments
import atlantafx.base.controls.ToggleSwitch
import atlantafx.base.util.IntegerStringConverter
import javafx.scene.control.Spinner
import kotlinx.coroutines.*
import me.vripper.gui.controller.SettingsController
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.model.settings.SystemSettingsModel
import me.vripper.model.SystemSettings
import tornadofx.*
class SystemSettingsFragment : Fragment("System Settings") {
private val settingsController: SettingsController by inject()
private val systemSettings = settingsController.findSystemSettings()
private val widgetsController: WidgetsController by inject()
private val coroutineScope = CoroutineScope(SupervisorJob())
private lateinit var systemSettings: SystemSettings
val systemSettingsModel = SystemSettingsModel()
override fun onDock() {
systemSettingsModel.tempPath = systemSettings.tempPath
systemSettingsModel.cachePath = systemSettings.cachePath
}
override val root = vbox {}
override val root = vbox {
form {
fieldset {
field("Temporary Path") {
textfield(systemSettingsModel.tempPathProperty) {
isEditable = false
}
button("Browse") {
action {
val directory = chooseDirectory(title = "Select temporary folder")
if (directory != null) {
systemSettingsModel.tempPathProperty.set(directory.path)
init {
coroutineScope.launch {
async {
systemSettings = settingsController.findSystemSettings()
systemSettingsModel.tempPath = systemSettings.tempPath
}.await()
runLater {
with(root) {
form {
fieldset {
field("Temporary Path") {
textfield(systemSettingsModel.tempPathProperty) {
editableWhen(widgetsController.currentSettings.localSessionProperty.not())
}
button("Browse") {
visibleWhen(widgetsController.currentSettings.localSessionProperty)
action {
val directory = chooseDirectory(title = "Select temporary folder")
if (directory != null) {
systemSettingsModel.tempPathProperty.set(directory.path)
}
}
}
}
}
}
}
field("Cache Path") {
textfield(systemSettingsModel.cachePathProperty) {
isEditable = false
}
button("Browse") {
action {
val directory = chooseDirectory(title = "Select temporary folder")
if (directory != null) {
systemSettingsModel.cachePathProperty.set(directory.path)
field("Max log entries") {
add(Spinner<Int>(10, 10000, systemSettings.maxEventLog).apply {
systemSettingsModel.logEntriesProperty.bind(valueProperty())
isEditable = true
IntegerStringConverter.createFor(this)
})
}
fieldset {
field("Clipboard monitoring") {
add(ToggleSwitch().apply {
isSelected = systemSettings.enableClipboardMonitoring
systemSettingsModel.enableProperty.bind(selectedProperty())
})
}
fieldset {
visibleWhen(systemSettingsModel.enableProperty)
field("Polling rate (ms)") {
add(
Spinner<Int>(
500,
kotlin.Int.MAX_VALUE,
systemSettings.clipboardPollingRate
).apply {
systemSettingsModel.pollingRateProperty.bind(valueProperty())
isEditable = true
IntegerStringConverter.createFor(this)
})
}
}
}
}
}
}
field("Max log entries") {
add(Spinner<Int>(10, 10000, systemSettings.maxEventLog).apply {
systemSettingsModel.logEntriesProperty.bind(valueProperty())
isEditable = true
IntegerStringConverter.createFor(this)
})
}
fieldset {
field("Clipboard monitoring") {
add(ToggleSwitch().apply {
isSelected = systemSettings.enableClipboardMonitoring
systemSettingsModel.enableProperty.bind(selectedProperty())
})
}
fieldset {
visibleWhen(systemSettingsModel.enableProperty)
field("Polling rate (ms)") {
add(Spinner<Int>(500, Int.MAX_VALUE, systemSettings.clipboardPollingRate).apply {
systemSettingsModel.pollingRateProperty.bind(valueProperty())
isEditable = true
IntegerStringConverter.createFor(this)
})
}
}
}
}
}
}
override fun onUndock() {
coroutineScope.cancel()
}
}
@@ -11,6 +11,8 @@ import javafx.scene.input.MouseButton
import javafx.scene.layout.Priority
import javafx.util.Callback
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.javafx.asFlow
import me.vripper.gui.components.cells.PreviewTableCell
import me.vripper.gui.controller.ThreadController
import me.vripper.gui.controller.WidgetsController
@@ -20,6 +22,7 @@ import me.vripper.gui.utils.openLink
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
import kotlin.io.path.Path
class ThreadSelectionTableFragment : Fragment("Thread") {
@@ -37,7 +40,7 @@ class ThreadSelectionTableFragment : Fragment("Thread") {
tableView.prefHeightProperty().bind(root.heightProperty())
tableView.placeholder = Label("Loading")
coroutineScope.launch {
val list = threadController.grab(threadId).await()
val list = async { threadController.grab(threadId) }.await()
runLater {
items.addAll(list)
}
@@ -58,14 +61,19 @@ class ThreadSelectionTableFragment : Fragment("Thread") {
}
tableView = tableview(items) {
addClass(Styles.DENSE)
selectionModel.selectionMode = SelectionMode.MULTIPLE
setRowFactory {
val tableRow = TableRow<ThreadSelectionModel>()
tableRow.setOnMouseClicked {
if (it.button.equals(MouseButton.PRIMARY) && it.clickCount == 2 && tableRow.item != null) {
threadController.download(listOf(tableRow.item))
close()
coroutineScope.launch {
async { threadController.download(listOf(tableRow.item)) }.await()
runLater {
close()
}
}
}
}
@@ -131,7 +139,11 @@ class ThreadSelectionTableFragment : Fragment("Thread") {
cell.onMouseEntered = EventHandler { mouseEvent ->
preview?.hide()
if (cell.tableRow.item != null && cell.tableRow.item.previewList.isNotEmpty()) {
preview = Preview(currentStage!!, cell.tableRow.item.previewList)
preview = Preview(
currentStage!!,
cell.tableRow.item.previewList,
Path(widgetsController.currentSettings.cachePath)
)
preview?.previewPopup?.apply {
x = mouseEvent.screenX + 20
y = mouseEvent.screenY + 10
@@ -142,7 +154,12 @@ class ThreadSelectionTableFragment : Fragment("Thread") {
}
}
column("Post Index", ThreadSelectionModel::indexProperty) {
prefWidth = 100.0
prefWidth = widgetsController.currentSettings.threadSelectionColumnsWidthModel.index
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.threadSelectionColumnsWidthModel.index = it as Double
}
}
visibleProperty().bind(widgetsController.currentSettings.threadSelectionColumnsModel.indexProperty)
sortOrder.add(this)
cellFactory = Callback {
@@ -151,20 +168,35 @@ class ThreadSelectionTableFragment : Fragment("Thread") {
}
column("Title", ThreadSelectionModel::titleProperty) {
visibleProperty().bind(widgetsController.currentSettings.threadSelectionColumnsModel.titleProperty)
prefWidth = 200.0
prefWidth = widgetsController.currentSettings.threadSelectionColumnsWidthModel.title
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.threadSelectionColumnsWidthModel.title = it as Double
}
}
cellFactory = Callback {
TextFieldTableCell<ThreadSelectionModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
}
}
column("URL", ThreadSelectionModel::urlProperty) {
visibleProperty().bind(widgetsController.currentSettings.threadSelectionColumnsModel.linkProperty)
prefWidth = 200.0
prefWidth = widgetsController.currentSettings.threadSelectionColumnsWidthModel.link
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.threadSelectionColumnsWidthModel.link = it as Double
}
}
cellFactory = Callback {
TextFieldTableCell<ThreadSelectionModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
}
}
column("Hosts", ThreadSelectionModel::hostsProperty) {
prefWidth = 100.0
prefWidth = widgetsController.currentSettings.threadSelectionColumnsWidthModel.hosts
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.threadSelectionColumnsWidthModel.hosts = it as Double
}
}
visibleProperty().bind(widgetsController.currentSettings.threadSelectionColumnsModel.hostsProperty)
cellFactory = Callback {
TextFieldTableCell<ThreadSelectionModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
@@ -181,8 +213,12 @@ class ThreadSelectionTableFragment : Fragment("Thread") {
tooltip("Download selected posts")
enableWhen { tableView.selectionModel.selectedItems.sizeProperty.greaterThan(0) }
action {
threadController.download(tableView.selectionModel.selectedItems)
close()
coroutineScope.launch {
async { threadController.download(tableView.selectionModel.selectedItems) }.await()
runLater {
close()
}
}
}
}
}
@@ -2,52 +2,66 @@ package me.vripper.gui.components.fragments
import atlantafx.base.controls.ToggleSwitch
import javafx.collections.FXCollections
import kotlinx.coroutines.*
import me.vripper.gui.controller.SettingsController
import me.vripper.gui.model.settings.ViperSettingsModel
import me.vripper.model.ViperSettings
import tornadofx.*
class ViperSettingsFragment : Fragment("Viper Settings") {
private val settingsController: SettingsController by inject()
private val proxies = FXCollections.observableArrayList<String>()
private val viperGirlsSettings = settingsController.findViperGirlsSettings()
private val coroutineScope = CoroutineScope(SupervisorJob())
private lateinit var viperGirlsSettings: ViperSettings
val viperSettingsModel = ViperSettingsModel()
override fun onDock() {
proxies.clear()
proxies.addAll(settingsController.getProxies())
viperSettingsModel.username = viperGirlsSettings.username
viperSettingsModel.password = viperGirlsSettings.password
viperSettingsModel.thanks = viperGirlsSettings.thanks
viperSettingsModel.host = viperGirlsSettings.host
}
override val root = vbox {}
override val root = vbox {
form {
fieldset {
field("Select a proxy") {
combobox(viperSettingsModel.hostProperty, proxies)
}
field("Enable ViperGirls Authentication") {
add(ToggleSwitch().apply {
isSelected = viperGirlsSettings.login
viperSettingsModel.loginProperty.bind(selectedProperty())
})
}
fieldset {
visibleWhen(viperSettingsModel.loginProperty)
field("ViperGirls Username") {
textfield(viperSettingsModel.usernameProperty)
}
field("ViperGirls Password") {
passwordfield(viperSettingsModel.passwordProperty)
}
field("Leave like") {
checkbox {
bind(viperSettingsModel.thanksProperty)
init {
coroutineScope.launch {
async {
viperGirlsSettings = settingsController.findViperGirlsSettings()
viperSettingsModel.username = viperGirlsSettings.username
viperSettingsModel.password = viperGirlsSettings.password
viperSettingsModel.thanks = viperGirlsSettings.thanks
viperSettingsModel.host = viperGirlsSettings.host
proxies.addAll(settingsController.getProxies())
}.await()
runLater {
with(root) {
form {
fieldset {
field("Select a proxy") {
combobox(viperSettingsModel.hostProperty, proxies)
}
field("Enable ViperGirls Authentication") {
add(ToggleSwitch().apply {
isSelected = viperGirlsSettings.login
viperSettingsModel.loginProperty.bind(selectedProperty())
})
}
fieldset {
visibleWhen(viperSettingsModel.loginProperty)
field("ViperGirls Username") {
textfield(viperSettingsModel.usernameProperty)
}
field("ViperGirls Password") {
passwordfield(viperSettingsModel.passwordProperty)
}
field("Leave like") {
checkbox {
bind(viperSettingsModel.thanksProperty)
}
}
}
}
}
}
}
}
}
override fun onUndock() {
coroutineScope.cancel()
}
}
@@ -0,0 +1,198 @@
package me.vripper.gui.components.views
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleIntegerProperty
import javafx.geometry.Orientation
import javafx.scene.control.ButtonType
import javafx.scene.control.ContentDisplay
import javafx.scene.input.KeyCode
import javafx.scene.input.KeyCodeCombination
import javafx.scene.input.KeyCombination
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import me.vripper.delegate.LoggerDelegate
import me.vripper.gui.components.fragments.AddLinksFragment
import me.vripper.gui.components.fragments.SettingsFragment
import me.vripper.gui.controller.PostController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.services.GrpcEndpointService
import me.vripper.services.AppEndpointService
import me.vripper.services.IAppEndpointService
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
class ActionBarView : View() {
private val logger by LoggerDelegate()
private val downloadActiveProperty = SimpleBooleanProperty(true)
private val postController: PostController by inject()
private val postsTableView: PostsTableView by inject()
private val coroutineScope = CoroutineScope(SupervisorJob())
private val grpcEndpointService: GrpcEndpointService by di("remoteAppEndpointService")
private val localEndpointService: AppEndpointService by di("localAppEndpointService")
private val running = SimpleIntegerProperty(0)
private val jobs = mutableListOf<Job>()
override val root = toolbar {}
init {
coroutineScope.launch {
GuiEventBus.events.collect { event ->
when (event) {
is GuiEventBus.LocalSession -> {
connect(localEndpointService)
}
is GuiEventBus.RemoteSession -> {
connect(grpcEndpointService)
}
is GuiEventBus.ChangingSession -> {
jobs.forEach { it.cancel() }
jobs.clear()
}
}
}
}
with(root) {
id = "action_toolbar"
padding = insets(all = 5)
button("Add links", FontIcon.of(Feather.PLUS)) {
contentDisplay = ContentDisplay.GRAPHIC_ONLY
tooltip("Add links [Ctrl+L]")
shortcut(KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN))
action {
find<AddLinksFragment>().apply {
input.clear()
}.openModal()
}
}
separator(Orientation.VERTICAL)
button("Start All", FontIcon.of(Feather.SKIP_FORWARD)) {
contentDisplay = ContentDisplay.GRAPHIC_ONLY
tooltip("Start downloads [Ctrl+S]")
shortcut(KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN))
disableWhen(downloadActiveProperty)
action {
coroutineScope.launch {
postController.startAll()
}
}
}
button("Stop All", FontIcon.of(Feather.SQUARE)) {
contentDisplay = ContentDisplay.GRAPHIC_ONLY
tooltip("Stops all running downloads [Ctrl+Q]")
shortcut(KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN))
disableWhen(downloadActiveProperty.not())
action {
coroutineScope.launch {
postController.stopAll()
}
}
}
separator(Orientation.VERTICAL)
button("Start", FontIcon.of(Feather.PLAY)) {
contentDisplay = ContentDisplay.GRAPHIC_ONLY
tooltip("Start downloads for selected [Ctrl+Shift+S]")
shortcut(KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN))
enableWhen(
postsTableView.tableView.selectionModel.selectedItems.sizeProperty.greaterThan(
0
)
)
action {
postsTableView.startSelected()
}
}
button("Stop", FontIcon.of(Feather.SQUARE)) {
contentDisplay = ContentDisplay.GRAPHIC_ONLY
tooltip("Stop downloads for selected [Ctrl+Shift+Q]")
shortcut(KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN))
enableWhen(
postsTableView.tableView.selectionModel.selectedItems.sizeProperty.greaterThan(
0
)
)
action {
postsTableView.stopSelected()
}
}
button("Rename", FontIcon.of(Feather.EDIT)) {
contentDisplay = ContentDisplay.GRAPHIC_ONLY
tooltip("Rename selected [Ctrl+R]")
shortcut(KeyCodeCombination(KeyCode.R, KeyCombination.CONTROL_DOWN))
enableWhen(
postsTableView.tableView.selectionModel.selectedItems.sizeProperty.greaterThan(
0
)
)
action {
postsTableView.renameSelected()
}
}
button("Delete", FontIcon.of(Feather.TRASH)) {
contentDisplay = ContentDisplay.GRAPHIC_ONLY
tooltip("Delete selected posts [Del]")
shortcut(KeyCodeCombination(KeyCode.DELETE))
enableWhen(
postsTableView.tableView.selectionModel.selectedItems.sizeProperty.greaterThan(
0
)
)
action {
postsTableView.deleteSelected()
}
}
separator(Orientation.VERTICAL)
button("Clear", FontIcon.of(Feather.TRASH_2)) {
contentDisplay = ContentDisplay.GRAPHIC_ONLY
tooltip("Clear all finished downloads [Ctrl+Del]")
shortcut(KeyCodeCombination(KeyCode.DELETE, KeyCombination.CONTROL_DOWN))
action {
confirm(
"",
"Confirm removal of finished posts?",
ButtonType.YES,
ButtonType.NO,
owner = primaryStage,
title = "Clean finished posts"
) {
coroutineScope.launch {
val clearPosts = async { postController.clearPosts() }.await()
runLater {
postsTableView.tableView.items.removeIf { clearPosts.contains(it.postId) }
}
}
}
}
}
button("Settings", FontIcon.of(Feather.SETTINGS)) {
contentDisplay = ContentDisplay.GRAPHIC_ONLY
tooltip("Open settings menu [S]")
shortcut(KeyCodeCombination(KeyCode.S))
action {
find<SettingsFragment>().openModal(owner = primaryStage)?.apply {
minWidth = 700.0
minHeight = 400.0
}
}
}
}
downloadActiveProperty.bind(running.greaterThan(0))
}
private fun connect(appEndpointService: IAppEndpointService) {
coroutineScope.launch {
appEndpointService.onQueueStateUpdate().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}.collect {
runLater {
running.set(it.running)
}
}
}.also { jobs.add(it) }
}
}
@@ -2,119 +2,63 @@ package me.vripper.gui.components.views
import atlantafx.base.theme.CupertinoDark
import atlantafx.base.theme.CupertinoLight
import io.grpc.ConnectivityState
import javafx.application.Application
import javafx.geometry.Orientation
import javafx.scene.control.SplitPane
import javafx.scene.layout.Priority
import javafx.scene.layout.VBox
import kotlinx.coroutines.*
import me.vripper.gui.components.fragments.ActionBarFragment
import me.vripper.gui.components.fragments.PostInfoPanelFragment
import me.vripper.gui.components.fragments.StatusBarFragment
import me.vripper.gui.controller.GlobalStateController
import me.vripper.gui.components.fragments.SessionFragment
import me.vripper.gui.controller.MainController
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.listener.GuiStartupLister
import me.vripper.gui.model.PostModel
import tornadofx.*
import me.vripper.gui.services.GrpcEndpointService
import tornadofx.View
import tornadofx.onChange
import tornadofx.runLater
import tornadofx.vbox
class AppView : View() {
override val root = VBox()
override val root = vbox { }
private val mainController: MainController by inject()
private val postsTableView: PostsTableView by inject()
private val menuBarView: MenuBarView by inject()
private val mainView: MainView by inject()
private val statusBarView: StatusBarView by inject()
private val actionBarView: ActionBarView by inject()
private val widgetsController: WidgetsController by inject()
private val globalStateController: GlobalStateController by inject()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var splitpane: SplitPane
private var lastSelectedInfoPanelTab = 0
private val grpcEndpointService: GrpcEndpointService by di("remoteAppEndpointService")
private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob())
private var job: Job? = null
init {
title = "VRipper ${mainController.version}"
with(root) {
style {
fontFamily = "Inter"
}
add(menuBarView)
if (widgetsController.currentSettings.visibleToolbarPanel) {
add(find<ActionBarFragment>())
}
splitpane = splitpane {
add(mainView)
if (widgetsController.currentSettings.visibleInfoPanel) {
add(buildInfoPanel(null))
setDividerPositions(widgetsController.currentSettings.infoPanelDividerPosition)
}
orientation = Orientation.VERTICAL
vboxConstraints { vGrow = Priority.ALWAYS }
add(actionBarView)
}
add(mainView)
if (widgetsController.currentSettings.visibleStatusBarPanel) {
add(find<StatusBarFragment>())
add(statusBarView)
}
prefWidth = widgetsController.currentSettings.width
prefHeight = widgetsController.currentSettings.height
}
}
override fun onDock() {
globalStateController.init()
GuiStartupLister().run()
coroutineScope.launch {
var lastDividerPosition = widgetsController.currentSettings.infoPanelDividerPosition
while (isActive) {
if (widgetsController.currentSettings.visibleInfoPanel) {
val dividerPosition = splitpane.dividerPositions.firstOrNull()
if (dividerPosition != null && lastDividerPosition != dividerPosition) {
lastDividerPosition = dividerPosition
widgetsController.currentSettings.infoPanelDividerPosition = lastDividerPosition
widgetsController.update()
}
}
delay(1_000)
}
}
postsTableView.tableView.selectionModel.selectedItemProperty().onChange {
if (widgetsController.currentSettings.visibleInfoPanel) {
splitpane.items.removeIf { it.id == "postinfo_panel" }
splitpane.add(buildInfoPanel(it))
splitpane.setDividerPositions(widgetsController.currentSettings.infoPanelDividerPosition)
}
}
mainView.root.selectionModel.selectedIndexProperty().onChange {
if (it == 0 && widgetsController.currentSettings.visibleInfoPanel) {
val selectedItem = postsTableView.tableView.selectedItem
splitpane.add(buildInfoPanel(selectedItem))
splitpane.setDividerPositions(widgetsController.currentSettings.infoPanelDividerPosition)
} else {
splitpane.items.removeIf { it.id == "postinfo_panel" }
}
}
widgetsController.currentSettings.visibleInfoPanelProperty.onChange {
if (mainView.root.selectionModel.selectedIndex != 0) return@onChange
if (it) {
val selectedItem = postsTableView.tableView.selectedItem
splitpane.add(buildInfoPanel(selectedItem))
splitpane.setDividerPositions(widgetsController.currentSettings.infoPanelDividerPosition)
} else {
splitpane.items.removeIf { it.id == "postinfo_panel" }
}
}
widgetsController.currentSettings.visibleToolbarPanelProperty.onChange { it ->
if (it) {
root.children.add(1, find<ActionBarFragment>().root)
root.children.add(1, actionBarView.root)
} else {
root.children.removeIf { it.id == "action_toolbar" }
}
}
widgetsController.currentSettings.visibleStatusBarPanelProperty.onChange { it ->
if (it) {
root.children.add(find<StatusBarFragment>().root)
root.children.add(statusBarView.root)
} else {
root.children.removeIf { it.id == "statusbar" }
}
}
widgetsController.currentSettings.darkModeProperty.onChange {
if (it) {
Application.setUserAgentStylesheet(CupertinoDark().userAgentStylesheet)
@@ -122,21 +66,45 @@ class AppView : View() {
Application.setUserAgentStylesheet(CupertinoLight().userAgentStylesheet)
}
}
}
private fun buildInfoPanel(it: PostModel?): PostInfoPanelFragment {
val postInfoPanelFragment = find<PostInfoPanelFragment>()
if (it != null) {
postInfoPanelFragment.setPostId(it.postId)
}
postInfoPanelFragment.root.selectionModel.select(lastSelectedInfoPanelTab)
postInfoPanelFragment.root.selectionModel.selectedIndexProperty().onChange {
lastSelectedInfoPanelTab = it
}
return postInfoPanelFragment
}
coroutineScope.launch {
GuiEventBus.events.collect {
when (it) {
is GuiEventBus.LocalSession -> {
job?.cancel()
GuiStartupLister().run()
}
override fun onUndock() {
coroutineScope.cancel()
is GuiEventBus.RemoteSession -> {
job?.cancel()
job = launch {
while (isActive) {
if (grpcEndpointService.connectionState() != ConnectivityState.READY) {
val loadingView = find<LoadingView>()
runLater {
replaceWith(loadingView)
find<SessionFragment>(mapOf(SessionFragment::component to loadingView)).openModal()
}
cancel()
}
delay(1000)
}
}
}
}
}
}
runLater {
if (widgetsController.currentSettings.localSession) {
runBlocking {
GuiEventBus.publishEvent(GuiEventBus.LocalSession)
}
} else {
runBlocking {
GuiEventBus.publishEvent(GuiEventBus.RemoteSession)
}
}
}
}
}
@@ -0,0 +1,320 @@
package me.vripper.gui.components.views
import atlantafx.base.theme.Styles
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.event.EventHandler
import javafx.geometry.Pos
import javafx.scene.control.*
import javafx.scene.control.cell.TextFieldTableCell
import javafx.scene.input.MouseButton
import javafx.util.Callback
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.javafx.asFlow
import me.vripper.entities.domain.Status
import me.vripper.gui.components.cells.PreviewTableCell
import me.vripper.gui.components.cells.ProgressTableCell
import me.vripper.gui.components.cells.StatusTableCell
import me.vripper.gui.components.fragments.ColumnSelectionFragment
import me.vripper.gui.controller.ImageController
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.model.ImageModel
import me.vripper.gui.utils.Preview
import me.vripper.gui.utils.openLink
import me.vripper.services.IAppEndpointService
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
import kotlin.io.path.Path
class ImagesTableView : View("Photos") {
private val tableView: TableView<ImageModel>
private val imageController: ImageController by inject()
private val widgetsController: WidgetsController by inject()
private val localAppEndpointService: IAppEndpointService by di("localAppEndpointService")
private val remoteAppEndpointService: IAppEndpointService by di("remoteAppEndpointService")
private val coroutineScope = CoroutineScope(SupervisorJob())
private val items: ObservableList<ImageModel> = FXCollections.observableArrayList()
private var preview: Preview? = null
private val jobs = mutableListOf<Job>()
override val root = vbox(alignment = Pos.CENTER_RIGHT) {}
init {
if (widgetsController.currentSettings.localSession) {
imageController.appEndpointService = localAppEndpointService
} else {
imageController.appEndpointService = remoteAppEndpointService
}
coroutineScope.launch {
GuiEventBus.events.filterIsInstance(GuiEventBus.ChangingSession::class).collect {
jobs.forEach { it.cancel() }
items.clear()
}
}
coroutineScope.launch {
GuiEventBus.events.filterIsInstance(GuiEventBus.LocalSession::class).collect {
imageController.appEndpointService = localAppEndpointService
}
}
coroutineScope.launch {
GuiEventBus.events.filterIsInstance(GuiEventBus.RemoteSession::class).collect {
imageController.appEndpointService = remoteAppEndpointService
}
}
with(root) {
tableView = tableview(items) {
addClass(Styles.DENSE)
setRowFactory {
val tableRow = TableRow<ImageModel>()
val urlItem = MenuItem("Open link").apply {
setOnAction {
openLink(tableRow.item.url)
}
graphic = FontIcon.of(Feather.LINK)
}
val contextMenu = ContextMenu()
contextMenu.items.addAll(urlItem)
tableRow.contextMenuProperty().bind(tableRow.emptyProperty()
.map { empty -> if (empty) null else contextMenu })
tableRow
}
contextMenu = ContextMenu()
contextMenu.items.add(MenuItem("Setup columns").apply {
setOnAction {
find<ColumnSelectionFragment>(
mapOf(
ColumnSelectionFragment::map to mapOf(
Pair(
"Preview",
widgetsController.currentSettings.imagesColumnsModel.previewProperty
),
Pair(
"Index",
widgetsController.currentSettings.imagesColumnsModel.indexProperty
),
Pair(
"Link",
widgetsController.currentSettings.imagesColumnsModel.linkProperty
),
Pair(
"Progress",
widgetsController.currentSettings.imagesColumnsModel.progressProperty
),
Pair(
"Filename",
widgetsController.currentSettings.imagesColumnsModel.filenameProperty
),
Pair(
"Status",
widgetsController.currentSettings.imagesColumnsModel.statusProperty
),
Pair(
"Size",
widgetsController.currentSettings.imagesColumnsModel.sizeProperty
),
Pair(
"Downloaded",
widgetsController.currentSettings.imagesColumnsModel.downloadedProperty
),
)
)
).openModal()
}
graphic = FontIcon.of(Feather.COLUMNS)
})
column("Preview", ImageModel::thumbUrlProperty) {
visibleProperty().bind(widgetsController.currentSettings.imagesColumnsModel.previewProperty)
prefWidth = widgetsController.currentSettings.imagesColumnsWidthModel.preview
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.imagesColumnsWidthModel.preview = it as Double
}
}
cellFactory = Callback {
val cell = PreviewTableCell<ImageModel>()
cell.onMouseExited = EventHandler {
preview?.hide()
}
cell.onMouseMoved = EventHandler {
preview?.previewPopup?.apply {
x = it.screenX + 20
y = it.screenY + 10
}
}
cell.onMouseEntered = EventHandler { mouseEvent ->
preview?.hide()
if (cell.tableRow.item != null && cell.tableRow.item.thumbUrl.isNotEmpty()) {
preview = Preview(
currentStage!!,
cell.tableRow.item.thumbUrl,
Path(widgetsController.currentSettings.cachePath)
)
preview?.previewPopup?.apply {
x = mouseEvent.screenX + 20
y = mouseEvent.screenY + 10
}
}
}
cell
}
}
column("Index", ImageModel::indexProperty) {
visibleProperty().bind(widgetsController.currentSettings.imagesColumnsModel.indexProperty)
prefWidth = widgetsController.currentSettings.imagesColumnsWidthModel.index
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.imagesColumnsWidthModel.index = it as Double
}
}
sortOrder.add(this)
cellFactory = Callback {
TextFieldTableCell<ImageModel?, Number?>().apply { alignment = Pos.CENTER_LEFT }
}
}
column("Link", ImageModel::urlProperty) {
visibleProperty().bind(widgetsController.currentSettings.imagesColumnsModel.linkProperty)
prefWidth = widgetsController.currentSettings.imagesColumnsWidthModel.link
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.imagesColumnsWidthModel.link = it as Double
}
}
cellFactory = Callback {
TextFieldTableCell<ImageModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
}
}
column("Progress", ImageModel::progressProperty) {
visibleProperty().bind(widgetsController.currentSettings.imagesColumnsModel.progressProperty)
prefWidth = widgetsController.currentSettings.imagesColumnsWidthModel.progress
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.imagesColumnsWidthModel.progress = it as Double
}
}
cellFactory = Callback {
val cell = ProgressTableCell<ImageModel>()
cell.alignment = Pos.CENTER
cell.setOnMouseClick {
when (it.clickCount) {
1 -> {
this@tableview.requestFocus()
this@tableview.focusModel.focus(cell.tableRow.index)
if (it.button.equals(MouseButton.PRIMARY)) {
this@tableview.selectionModel.clearSelection()
this@tableview.selectionModel.select(cell.tableRow.index)
}
}
}
}
cell as TableCell<ImageModel, Number>
}
}
column("Filename", ImageModel::filenameProperty) {
visibleProperty().bind(widgetsController.currentSettings.imagesColumnsModel.filenameProperty)
prefWidth = widgetsController.currentSettings.imagesColumnsWidthModel.filename
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.imagesColumnsWidthModel.filename = it as Double
}
}
cellFactory = Callback {
TextFieldTableCell<ImageModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
}
}
column("Status", ImageModel::statusProperty) {
visibleProperty().bind(widgetsController.currentSettings.imagesColumnsModel.statusProperty)
prefWidth = widgetsController.currentSettings.imagesColumnsWidthModel.status
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.imagesColumnsWidthModel.status = it as Double
}
}
cellFactory = Callback {
StatusTableCell()
}
}
column("Size", ImageModel::sizeProperty) {
visibleProperty().bind(widgetsController.currentSettings.imagesColumnsModel.sizeProperty)
prefWidth = widgetsController.currentSettings.imagesColumnsWidthModel.size
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.imagesColumnsWidthModel.size = it as Double
}
}
cellFactory = Callback {
TextFieldTableCell<ImageModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
}
}
column("Downloaded", ImageModel::downloadedProperty) {
visibleProperty().bind(widgetsController.currentSettings.imagesColumnsModel.downloadedProperty)
prefWidth = widgetsController.currentSettings.imagesColumnsWidthModel.downloaded
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.imagesColumnsWidthModel.downloaded = it as Double
}
}
cellFactory = Callback {
TextFieldTableCell<ImageModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
}
}
}
}
tableView.prefWidthProperty().bind(root.widthProperty())
tableView.prefHeightProperty().bind(root.heightProperty())
modalStage?.width = 550.0
}
fun setPostId(postId: Long?) {
jobs.forEach { it.cancel() }
jobs.clear()
items.clear()
if (postId == null) {
return
}
coroutineScope.launch {
val list = coroutineScope.async {
imageController.findImages(postId)
}.await()
runLater {
items.addAll(list)
tableView.sort()
tableView.placeholder = Label("No content in table")
}
}
coroutineScope.launch {
imageController.onUpdateImages(postId).collect { image ->
runLater {
val imageModel = items.find { it.id == image.id } ?: return@runLater
imageModel.size = image.size
imageModel.status = image.status.name
imageModel.filename = image.filename
imageModel.downloaded = image.downloaded
imageModel.progress = imageController.progress(
image.size, image.downloaded
)
}
}
}.also { jobs.add(it) }
coroutineScope.launch {
imageController.onStopped().collect {
runLater {
items.forEach { imageModel ->
if (imageModel.status != Status.FINISHED.name) {
imageModel.status = Status.STOPPED.name
}
}
}
}
}.also { jobs.add(it) }
}
}
@@ -1,30 +1,87 @@
package me.vripper.gui.components.views
import io.grpc.ConnectivityState
import javafx.scene.control.ProgressIndicator.INDETERMINATE_PROGRESS
import javafx.scene.effect.DropShadow
import javafx.util.Duration
import me.vripper.gui.event.ApplicationInitialized
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.filterIsInstance
import me.vripper.gui.VripperGuiApplication
import me.vripper.gui.components.fragments.SessionFragment
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.services.DatabaseManager
import me.vripper.gui.services.GrpcEndpointService
import tornadofx.*
class LoadingView : View("VRipper") {
private val appView: AppView by inject()
private val widgetsController: WidgetsController by inject()
private val grpcEndpointService: GrpcEndpointService by di("remoteAppEndpointService")
private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob())
override val root = borderpane {}
init {
subscribe<ApplicationInitialized> {
runLater {
replaceWith(appView, ViewTransition.FadeThrough(Duration.millis(250.0)))
coroutineScope.launch {
GuiEventBus.events.filterIsInstance(GuiEventBus.ApplicationInitialized::class).collect {
runLater {
if (widgetsController.currentSettings.firstRun) {
widgetsController.currentSettings.firstRun = false
val sessionView = find<SessionFragment>(mapOf(SessionFragment::component to this@LoadingView))
sessionView.openModal().also {
it?.setOnCloseRequest {
VripperGuiApplication.APP_INSTANCE.stop()
}
}
} else {
if (widgetsController.currentSettings.localSession) {
DatabaseManager.connect()
replaceWith(find<AppView>())
} else {
grpcEndpointService.connect(
widgetsController.currentSettings.remoteSessionModel.host,
widgetsController.currentSettings.remoteSessionModel.port
)
coroutineScope.launch {
repeat(10) {
if (grpcEndpointService.connectionState() == ConnectivityState.READY) {
runLater {
replaceWith(find<AppView>())
}
cancel()
}
delay(333)
}
runLater {
val sessionView = find<SessionFragment>(
mapOf(SessionFragment::component to this@LoadingView)
)
sessionView.openModal().also {
it?.setOnCloseRequest {
VripperGuiApplication.APP_INSTANCE.stop()
}
}
}
}
}
}
}
}
}
with(root) {
padding = insets(all = 5)
center {
progressindicator {
progress = INDETERMINATE_PROGRESS
}
}
effect = DropShadow()
}
}
override val root = borderpane {
padding = insets(all = 5)
center {
progressindicator {
progress = INDETERMINATE_PROGRESS
}
}
effect = DropShadow()
override fun onUndock() {
coroutineScope.cancel()
}
}
@@ -1,17 +1,19 @@
package me.vripper.gui.components.views
import atlantafx.base.theme.Styles
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.scene.control.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.filterIsInstance
import me.vripper.event.EventBus
import me.vripper.event.LogCreateEvent
import me.vripper.event.LogDeleteEvent
import me.vripper.event.LogUpdateEvent
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.javafx.asFlow
import me.vripper.gui.components.fragments.ColumnSelectionFragment
import me.vripper.gui.components.fragments.LogMessageFragment
import me.vripper.gui.controller.LogController
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.model.LogModel
import me.vripper.services.IAppEndpointService
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
@@ -20,13 +22,140 @@ class LogTableView : View() {
private val logController: LogController by inject()
private val widgetsController: WidgetsController by inject()
private val eventBus: EventBus by di()
private lateinit var tableView: TableView<LogModel>
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
lateinit var items: SortedFilteredList<LogModel>
private val localAppEndpointService: IAppEndpointService by di("localAppEndpointService")
private val remoteAppEndpointService: IAppEndpointService by di("remoteAppEndpointService")
private val tableView: TableView<LogModel>
private val coroutineScope = CoroutineScope(SupervisorJob())
private val items: ObservableList<LogModel> = FXCollections.observableArrayList()
private val jobs = mutableListOf<Job>()
override fun onDock() {
items = SortedFilteredList<LogModel>().bindTo(tableView)
override val root = vbox {}
init {
coroutineScope.launch {
GuiEventBus.events.collect { event ->
when (event) {
is GuiEventBus.LocalSession -> {
logController.appEndpointService = localAppEndpointService
connect()
}
is GuiEventBus.RemoteSession -> {
logController.appEndpointService = remoteAppEndpointService
connect()
}
is GuiEventBus.ChangingSession -> {
jobs.forEach { it.cancel() }
jobs.clear()
}
}
}
}
with(root) {
tableView = tableview(items) {
addClass(Styles.DENSE)
selectionModel.selectionMode = SelectionMode.SINGLE
setRowFactory {
val tableRow = TableRow<LogModel>()
tableRow.setOnMouseClicked {
if (it.clickCount == 2 && tableRow.item != null) {
openLog(tableRow.item)
}
}
tableRow
}
contextMenu = ContextMenu()
contextMenu.items.addAll(MenuItem("Setup columns").apply {
setOnAction {
find<ColumnSelectionFragment>(
mapOf(
ColumnSelectionFragment::map to mapOf(
Pair(
"Time", widgetsController.currentSettings.logsColumnsModel.timeProperty
),
Pair(
"Type", widgetsController.currentSettings.logsColumnsModel.typeProperty
),
Pair(
"Status", widgetsController.currentSettings.logsColumnsModel.statusProperty
),
Pair(
"Message", widgetsController.currentSettings.logsColumnsModel.messageProperty
),
)
)
).openModal()
}
graphic = FontIcon.of(Feather.COLUMNS)
}, SeparatorMenuItem(), MenuItem("Clear", FontIcon.of(Feather.TRASH_2)).apply {
setOnAction {
confirm(
"",
"Are you sure you want to clear the logs",
ButtonType.YES,
ButtonType.NO,
owner = primaryStage,
title = "Clear logs"
) {
coroutineScope.launch {
coroutineScope {
async { logController.clear() }.await()
runLater {
items.clear()
}
}
}
}
}
})
column("Time", LogModel::timeProperty) {
visibleProperty().bind(widgetsController.currentSettings.logsColumnsModel.timeProperty)
id = "time"
prefWidth = widgetsController.currentSettings.logsColumnsWidthModel.time
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.logsColumnsWidthModel.time = it as Double
}
}
sortType = TableColumn.SortType.DESCENDING
}
column("Type", LogModel::typeProperty) {
visibleProperty().bind(widgetsController.currentSettings.logsColumnsModel.typeProperty)
prefWidth = widgetsController.currentSettings.logsColumnsWidthModel.type
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.logsColumnsWidthModel.type = it as Double
}
}
id = "type"
isSortable = false
}
column("Status", LogModel::statusProperty) {
visibleProperty().bind(widgetsController.currentSettings.logsColumnsModel.statusProperty)
prefWidth = widgetsController.currentSettings.logsColumnsWidthModel.status
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.logsColumnsWidthModel.status = it as Double
}
}
id = "status"
isSortable = false
}
column("Message", LogModel::messageProperty) {
visibleProperty().bind(widgetsController.currentSettings.logsColumnsModel.messageProperty)
prefWidth = widgetsController.currentSettings.logsColumnsWidthModel.message
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.logsColumnsWidthModel.message = it as Double
}
}
id = "message"
isSortable = false
}
}
}
titleProperty.bind(items.sizeProperty.map {
if (it.toLong() > 0) {
"Log (${it.toLong()})"
@@ -37,121 +166,50 @@ class LogTableView : View() {
tableView.prefHeightProperty().bind(root.heightProperty())
tableView.placeholder = Label("Loading")
tableView.sortOrder.add(tableView.columns.first { it.id == "time" })
}
private fun connect() {
coroutineScope.launch {
val list = coroutineScope.async {
logController.findAll().sortedByDescending { it.time }
}.await()
runLater {
items.clear()
items.addAll(list)
tableView.placeholder = Label("No content in table")
}
}
coroutineScope.launch {
eventBus.events.filterIsInstance(LogCreateEvent::class).collect {
val logModel = logController.mapper(it.logEntry)
logController.onNewLog().collect {
runLater {
items.add(logModel)
items.add(it)
tableView.sort()
}
}
}
}.also { jobs.add(it) }
coroutineScope.launch {
eventBus.events.filterIsInstance(LogUpdateEvent::class).collect {
val logModel = logController.mapper(it.logEntry)
val find = items.find { threadModel -> threadModel.id == logModel.id }
logController.onUpdateLog().collect {
val find = items.find { threadModel -> threadModel.id == it.id }
if (find != null) {
runLater {
find.apply {
status = logModel.status
message = logModel.message
status = it.status.name
message = it.message
}
}
}
}
}
}.also { jobs.add(it) }
coroutineScope.launch {
eventBus.events.filterIsInstance(LogDeleteEvent::class).collect { deleteEvent ->
logController.onDeleteLogs().collect { deletedId ->
runLater {
items.items.removeIf { it.id in deleteEvent.deleted }
items.removeIf { it.id == deletedId }
}
}
}
}
override val root = vbox {
tableView = tableview {
selectionModel.selectionMode = SelectionMode.SINGLE
setRowFactory {
val tableRow = TableRow<LogModel>()
tableRow.setOnMouseClicked {
if (it.clickCount == 2 && tableRow.item != null) {
openLog(tableRow.item)
}
}
tableRow
}
contextMenu = ContextMenu()
contextMenu.items.addAll(MenuItem("Setup columns").apply {
setOnAction {
find<ColumnSelectionFragment>(
mapOf(
ColumnSelectionFragment::map to mapOf(
Pair(
"Time", widgetsController.currentSettings.logsColumnsModel.timeProperty
),
Pair(
"Type", widgetsController.currentSettings.logsColumnsModel.typeProperty
),
Pair(
"Status", widgetsController.currentSettings.logsColumnsModel.statusProperty
),
Pair(
"Message", widgetsController.currentSettings.logsColumnsModel.messageProperty
),
)
)
).openModal()
}
graphic = FontIcon.of(Feather.COLUMNS)
}, SeparatorMenuItem(), MenuItem("Clear", FontIcon.of(Feather.TRASH_2)).apply {
setOnAction {
confirm(
"",
"Are you sure you want to clear the logs",
ButtonType.YES,
ButtonType.NO,
owner = primaryStage,
title = "Clear logs"
) {
logController.clear()
items.clear()
}
}
})
column("Time", LogModel::timeProperty) {
visibleProperty().bind(widgetsController.currentSettings.logsColumnsModel.timeProperty)
id = "time"
prefWidth = 150.0
sortType = TableColumn.SortType.DESCENDING
}
column("Type", LogModel::typeProperty) {
visibleProperty().bind(widgetsController.currentSettings.logsColumnsModel.typeProperty)
id = "type"
isSortable = false
}
column("Status", LogModel::statusProperty) {
visibleProperty().bind(widgetsController.currentSettings.logsColumnsModel.statusProperty)
id = "status"
isSortable = false
}
column("Message", LogModel::messageProperty) {
visibleProperty().bind(widgetsController.currentSettings.logsColumnsModel.messageProperty)
id = "message"
prefWidth = 300.0
isSortable = false
}
}
}.also { jobs.add(it) }
}
private fun openLog(item: LogModel) {
@@ -1,30 +1,32 @@
package me.vripper.gui.components.views
import javafx.scene.control.TabPane
import javafx.scene.layout.Priority
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.ChangeListener
import tornadofx.View
import tornadofx.tab
import tornadofx.tabpane
import tornadofx.vgrow
class MainView : View("Center view") {
private val threadTableView: ThreadTableView by inject()
private val logTableView: LogTableView by inject()
private val postsTabView: PostsTabView by inject()
override val root = tabpane {
tabClosingPolicy = TabPane.TabClosingPolicy.UNAVAILABLE
selectionModel.selectedItemProperty()
.addListener(ChangeListener { _, _, newValue ->
run {
}
})
tab<PostsTableView> {
vgrow = Priority.ALWAYS
tab(postsTabView) {
graphic = FontIcon(Feather.DOWNLOAD)
id = "download-tab"
}
tab<ThreadTableView> {
tab(threadTableView) {
graphic = FontIcon(Feather.FILE_TEXT)
id = "thread-tab"
}
tab<LogTableView> {
tab(logTableView) {
graphic = FontIcon(Feather.DATABASE)
id = "log-tab"
}
@@ -1,117 +1,155 @@
package me.vripper.gui.components.views
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleIntegerProperty
import javafx.scene.control.ButtonType
import javafx.scene.input.KeyCode
import javafx.scene.input.KeyCodeCombination
import javafx.scene.input.KeyCombination
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import me.vripper.delegate.LoggerDelegate
import me.vripper.gui.VripperGuiApplication
import me.vripper.gui.components.fragments.AboutFragment
import me.vripper.gui.components.fragments.AddLinksFragment
import me.vripper.gui.components.fragments.SessionFragment
import me.vripper.gui.components.fragments.SettingsFragment
import me.vripper.gui.controller.GlobalStateController
import me.vripper.gui.controller.PostController
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.services.GrpcEndpointService
import me.vripper.gui.utils.openLink
import me.vripper.services.AppEndpointService
import me.vripper.services.IAppEndpointService
import me.vripper.utilities.ApplicationProperties
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
class MenuBarView : View() {
private val globalStateController: GlobalStateController by inject()
private val postController: PostController by inject()
private val postsTableView: PostsTableView by inject()
private val logger by LoggerDelegate()
private val coroutineScope = CoroutineScope(SupervisorJob())
private val downloadActiveProperty = SimpleBooleanProperty(false)
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val postsTableView: PostsTableView by inject()
private val widgetsController: WidgetsController by inject()
private val postController: PostController by inject()
private val grpcEndpointService: GrpcEndpointService by di("remoteAppEndpointService")
private val localEndpointService: AppEndpointService by di("localAppEndpointService")
private val running = SimpleIntegerProperty(0)
private val jobs = mutableListOf<Job>()
override val root = menubar {
menu("File") {
item("Add links", KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN)).apply {
graphic = FontIcon.of(Feather.PLUS)
action {
find<AddLinksFragment>().apply {
input.clear()
}.openModal()
override val root = menubar {}
init {
coroutineScope.launch {
GuiEventBus.events.collect { event ->
when (event) {
is GuiEventBus.LocalSession -> {
connect(localEndpointService)
}
is GuiEventBus.RemoteSession -> {
connect(grpcEndpointService)
}
is GuiEventBus.ChangingSession -> {
jobs.forEach { it.cancel() }
jobs.clear()
}
}
}
separator()
item("Start All", KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN)).apply {
graphic = FontIcon.of(Feather.SKIP_FORWARD)
disableWhen(downloadActiveProperty)
action {
postController.startAll()
}
with(root) {
menu("File") {
item("Add links", KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN)).apply {
graphic = FontIcon.of(Feather.PLUS)
action {
find<AddLinksFragment>().apply {
input.clear()
}.openModal()
}
}
}
item("Stop All", KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN)).apply {
graphic = FontIcon.of(Feather.SQUARE)
disableWhen(downloadActiveProperty.not())
action {
postController.stopAll()
}
}
separator()
item("Start", KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN)).apply {
graphic = FontIcon.of(Feather.PLAY)
enableWhen(
postsTableView.tableView.selectionModel.selectedItems.sizeProperty.greaterThan(
0
)
)
action {
postsTableView.startSelected()
}
}
item("Stop", KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN)).apply {
graphic = FontIcon.of(Feather.SQUARE)
enableWhen(
postsTableView.tableView.selectionModel.selectedItems.sizeProperty.greaterThan(
0
)
)
action {
postsTableView.stopSelected()
}
}
item("Rename", KeyCodeCombination(KeyCode.R, KeyCombination.CONTROL_DOWN)).apply {
graphic = FontIcon.of(Feather.EDIT)
enableWhen(
postsTableView.tableView.selectionModel.selectedItems.sizeProperty.greaterThan(
0
)
)
action {
postsTableView.renameSelected()
}
}
item("Delete", KeyCodeCombination(KeyCode.DELETE)).apply {
graphic = FontIcon.of(Feather.TRASH)
enableWhen(
postsTableView.tableView.selectionModel.selectedItems.sizeProperty.greaterThan(
0
)
)
action {
postsTableView.deleteSelected()
}
}
item("Clear", KeyCodeCombination(KeyCode.DELETE, KeyCombination.CONTROL_DOWN)).apply {
graphic = FontIcon.of(Feather.TRASH_2)
action {
confirm(
"",
"Confirm removal of finished posts?",
ButtonType.YES,
ButtonType.NO,
owner = primaryStage,
title = "Clean finished posts"
) {
separator()
item("Start All", KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN)).apply {
graphic = FontIcon.of(Feather.SKIP_FORWARD)
disableWhen(downloadActiveProperty)
action {
coroutineScope.launch {
val clearPosts = postController.clearPosts().await()
postController.startAll()
}
}
}
item("Stop All", KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN)).apply {
graphic = FontIcon.of(Feather.SQUARE)
disableWhen(downloadActiveProperty.not())
action {
coroutineScope.launch {
postController.stopAll()
}
}
}
separator()
item(
"Start", KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN)
).apply {
graphic = FontIcon.of(Feather.PLAY)
enableWhen(
postsTableView.tableView.selectionModel.selectedItems.sizeProperty.greaterThan(
0
)
)
action {
postsTableView.startSelected()
}
}
item(
"Stop", KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN)
).apply {
graphic = FontIcon.of(Feather.SQUARE)
enableWhen(
postsTableView.tableView.selectionModel.selectedItems.sizeProperty.greaterThan(
0
)
)
action {
postsTableView.stopSelected()
}
}
item("Rename", KeyCodeCombination(KeyCode.R, KeyCombination.CONTROL_DOWN)).apply {
graphic = FontIcon.of(Feather.EDIT)
enableWhen(
postsTableView.tableView.selectionModel.selectedItems.sizeProperty.greaterThan(
0
)
)
action {
postsTableView.renameSelected()
}
}
item("Delete", KeyCodeCombination(KeyCode.DELETE)).apply {
graphic = FontIcon.of(Feather.TRASH)
enableWhen(
postsTableView.tableView.selectionModel.selectedItems.sizeProperty.greaterThan(
0
)
)
action {
postsTableView.deleteSelected()
}
}
item("Clear", KeyCodeCombination(KeyCode.DELETE, KeyCombination.CONTROL_DOWN)).apply {
graphic = FontIcon.of(Feather.TRASH_2)
action {
confirm(
"",
"Confirm removal of finished posts?",
ButtonType.YES,
ButtonType.NO,
owner = primaryStage,
title = "Clean finished posts"
) {
coroutineScope.launch {
val clearPosts = async { postController.clearPosts() }.await()
runLater {
postsTableView.tableView.items.removeIf { clearPosts.contains(it.postId) }
}
@@ -119,86 +157,99 @@ class MenuBarView : View() {
}
}
}
}
separator()
item("Settings", KeyCodeCombination(KeyCode.S)).apply {
graphic = FontIcon.of(Feather.SETTINGS)
action {
find<SettingsFragment>().openModal()?.apply {
minWidth = 700.0
minHeight = 400.0
}
}
}
separator()
item("Exit", KeyCodeCombination(KeyCode.X, KeyCombination.CONTROL_DOWN)).apply {
graphic = FontIcon.of(Feather.X_SQUARE)
action {
VripperGuiApplication.APP_INSTANCE.stop()
}
}
}
menu("View") {
checkmenuitem(
"Toolbar", KeyCodeCombination(KeyCode.F6)
).bind(widgetsController.currentSettings.visibleToolbarPanelProperty)
checkmenuitem(
"Info Panel", KeyCodeCombination(KeyCode.F7)
).bind(widgetsController.currentSettings.visibleInfoPanelProperty)
checkmenuitem(
"Status Bar", KeyCodeCombination(KeyCode.F8)
).bind(widgetsController.currentSettings.visibleStatusBarPanelProperty)
checkmenuitem(
"Dark mode"
).bind(widgetsController.currentSettings.darkModeProperty)
}
menu("Help") {
item("Check for updates").apply {
graphic = FontIcon.of(Feather.REFRESH_CCW)
action {
val latestVersion = ApplicationProperties.latestVersion()
val currentVersion = ApplicationProperties.VERSION
if (latestVersion > currentVersion) {
information(
header = "",
content = "A newer version of VRipper is available \nLatest version is $latestVersion\nDo you want to go to the release page?",
title = "VRipper updates",
buttons = arrayOf(ButtonType.YES, ButtonType.NO),
owner = primaryStage,
) {
if (it == ButtonType.YES) {
openLink("https://github.com/death-claw/vripper-project/releases/tag/$latestVersion")
}
separator()
item("Settings", KeyCodeCombination(KeyCode.S)).apply {
graphic = FontIcon.of(Feather.SETTINGS)
action {
find<SettingsFragment>().openModal(owner = primaryStage)?.apply {
minWidth = 700.0
minHeight = 400.0
}
} else {
information(
header = "",
content = "You are running the latest version of VRipper.",
title = "VRipper updates",
owner = primaryStage
)
}
}
separator()
item("Change session", KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN)) {
graphic = FontIcon.of(Feather.LINK_2)
action {
find<SessionFragment>(mapOf(SessionFragment::component to find<AppView>())).openModal()
}
}
separator()
item("Exit", KeyCodeCombination(KeyCode.X, KeyCombination.CONTROL_DOWN)).apply {
graphic = FontIcon.of(Feather.X_SQUARE)
action {
VripperGuiApplication.APP_INSTANCE.stop()
}
}
}
separator()
item("About").apply {
graphic = FontIcon.of(Feather.INFO)
action {
find<AboutFragment>().openModal()?.apply {
this.minWidth = 550.0
this.minHeight = 200.0
menu("View") {
checkmenuitem(
"Toolbar", KeyCodeCombination(KeyCode.F6)
).bind(widgetsController.currentSettings.visibleToolbarPanelProperty)
checkmenuitem(
"Info Panel", KeyCodeCombination(KeyCode.F7)
).bind(widgetsController.currentSettings.visibleInfoPanelProperty)
checkmenuitem(
"Status Bar", KeyCodeCombination(KeyCode.F8)
).bind(widgetsController.currentSettings.visibleStatusBarPanelProperty)
checkmenuitem(
"Dark mode"
).bind(widgetsController.currentSettings.darkModeProperty)
}
menu("Help") {
item("Check for updates").apply {
graphic = FontIcon.of(Feather.REFRESH_CCW)
action {
val latestVersion = ApplicationProperties.latestVersion()
val currentVersion = ApplicationProperties.VERSION
if (latestVersion > currentVersion) {
information(
header = "",
content = "A newer version of VRipper is available \nLatest version is $latestVersion\nDo you want to go to the release page?",
title = "VRipper updates",
buttons = arrayOf(ButtonType.YES, ButtonType.NO),
owner = primaryStage,
) {
if (it == ButtonType.YES) {
openLink("https://github.com/death-claw/vripper-project/releases/tag/$latestVersion")
}
}
} else {
information(
header = "",
content = "You are running the latest version of VRipper.",
title = "VRipper updates",
owner = primaryStage
)
}
}
}
separator()
item("About").apply {
graphic = FontIcon.of(Feather.INFO)
action {
find<AboutFragment>().openModal()?.apply {
this.minWidth = 625.0
this.minHeight = 200.0
}
}
}
}
}
downloadActiveProperty.bind(running.greaterThan(0))
}
override fun onDock() {
downloadActiveProperty.bind(globalStateController.globalState.runningProperty.greaterThan(0))
}
override fun onUndock() {
coroutineScope.cancel()
private fun connect(appEndpointService: IAppEndpointService) {
coroutineScope.launch {
appEndpointService.onQueueStateUpdate().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}.collect {
runLater {
running.set(it.running)
}
}
}.also { jobs.add(it) }
}
}
@@ -0,0 +1,156 @@
package me.vripper.gui.components.views
import javafx.scene.control.TabPane
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.filterIsInstance
import me.vripper.delegate.LoggerDelegate
import me.vripper.gui.controller.PostController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.model.PostModel
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
class PostInfoView : View() {
private val logger by LoggerDelegate()
private val postController: PostController by inject()
private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob())
private val imagesTableView: ImagesTableView by inject()
private val jobs = mutableListOf<Job>()
private val postModel: PostModel = PostModel(
-1, "", 0.0, "", "", 0, 0, "", "", -1, "", "", "", emptyList(), emptyList(), ""
)
override val root = tabpane()
init {
coroutineScope.launch {
GuiEventBus.events.filterIsInstance(GuiEventBus.ChangingSession::class).collect {
jobs.forEach { it.cancel() }
jobs.clear()
}
}
with(root) {
id = "postinfo_panel"
tabClosingPolicy = TabPane.TabClosingPolicy.UNAVAILABLE
tab("General") {
graphic = FontIcon(Feather.INFO)
scrollpane {
form {
fieldset {
field("Post Link:") {
textfield(postModel.urlProperty) {
isEditable = false
visibleWhen(postModel.urlProperty.isNotEmpty)
}
}
field("Posted By:") {
label(postModel.postedByProperty)
}
field("Title:") {
label(postModel.titleProperty)
}
field("More titles:") {
label(postModel.altTitlesProperty.map { it.joinToString(", ") })
}
field("Status:") {
label(postModel.statusProperty.map { it ->
it.lowercase().replaceFirstChar { it.uppercase() }
})
}
field("Path:") {
label(postModel.pathProperty)
}
field("Total:") {
label(postModel.progressCountProperty)
}
field("Hosts:") {
label(postModel.hostsProperty)
}
field("Added On:") {
label(postModel.addedOnProperty)
}
}
}
}
}
tab("Photos") {
graphic = FontIcon(Feather.IMAGE)
add(imagesTableView)
}
}
}
fun setPostId(postId: Long?) {
jobs.forEach { it.cancel() }
jobs.clear()
imagesTableView.setPostId(postId)
if (postId == null) {
postModel.apply {
this.postId = -1
this.title = ""
this.progress = 0.0
this.status = ""
this.url = ""
this.done = 0
this.total = 0
this.hosts = ""
this.addedOn = ""
this.order = -1
this.path = ""
this.folderName = ""
this.progressCount = ""
this.previewList.clear()
this.altTitles.clear()
this.postedBy = ""
}
return
}
coroutineScope.launch {
val model = async { postController.find(postId) }.await()
runLater {
postModel.apply {
this.postId = model.postId
this.title = model.title
this.progress = model.progress
this.status = model.status.lowercase().replaceFirstChar { it.uppercase() }
this.url = model.url
this.done = model.done
this.total = model.total
this.hosts = model.hosts
this.addedOn = model.addedOn
this.order = model.order
this.path = model.path
this.folderName = model.folderName
this.progressCount = model.progressCount
this.previewList = model.previewList
this.altTitles = model.altTitles
this.postedBy = model.postedBy
}
}
}
coroutineScope.launch {
postController.onUpdatePosts().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}.collect { post ->
runLater {
postModel.status = post.status.stringValue.lowercase().replaceFirstChar { it.uppercase() }
postModel.progressCount = postController.progressCount(
post.total, post.done, post.downloaded
)
postModel.order = post.rank + 1
postModel.done = post.done
postModel.progress = postController.progress(
post.total, post.done
)
postModel.path = post.getDownloadFolder()
postModel.folderName = post.folderName
}
}
}.also { jobs.add(it) }
}
}
@@ -0,0 +1,67 @@
package me.vripper.gui.components.views
import javafx.geometry.Orientation
import javafx.scene.control.SplitPane
import javafx.scene.layout.Priority
import kotlinx.coroutines.*
import me.vripper.gui.controller.WidgetsController
import tornadofx.*
class PostsTabView : View() {
override val root: SplitPane = splitpane()
private val postsTableView: PostsTableView by inject()
private val postInfoView: PostInfoView by inject()
private val widgetsController: WidgetsController by inject()
private val coroutineScope = CoroutineScope(SupervisorJob())
init {
with(root) {
add(postsTableView)
if (widgetsController.currentSettings.visibleInfoPanel) {
postInfoView.setPostId(null)
add(postInfoView)
setDividerPositions(widgetsController.currentSettings.infoPanelDividerPosition)
}
orientation = Orientation.VERTICAL
vboxConstraints { vGrow = Priority.ALWAYS }
}
titleProperty.bind(postsTableView.items.sizeProperty.map {
if (it.toLong() > 0) {
"Download (${it.toLong()})"
} else {
"Download"
}
})
postsTableView.tableView.selectionModel.selectedItemProperty().onChange {
if (widgetsController.currentSettings.visibleInfoPanel) {
postInfoView.setPostId(it?.postId)
}
}
widgetsController.currentSettings.visibleInfoPanelProperty.onChange { visible ->
if (visible) {
val selectedItem = postsTableView.tableView.selectedItem
postInfoView.setPostId(selectedItem?.postId)
root.add(postInfoView)
root.setDividerPositions(widgetsController.currentSettings.infoPanelDividerPosition)
} else {
postInfoView.setPostId(null)
root.items.removeIf { it.id == "postinfo_panel" }
}
}
coroutineScope.launch {
var lastDividerPosition = widgetsController.currentSettings.infoPanelDividerPosition
while (isActive) {
if (widgetsController.currentSettings.visibleInfoPanel) {
val dividerPosition = root.dividerPositions.firstOrNull()
if (dividerPosition != null && lastDividerPosition != dividerPosition) {
lastDividerPosition = dividerPosition
widgetsController.currentSettings.infoPanelDividerPosition = lastDividerPosition
widgetsController.update()
}
}
delay(1_000)
}
}
}
}
@@ -1,5 +1,6 @@
package me.vripper.gui.components.views
import atlantafx.base.theme.Styles
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.event.EventHandler
@@ -8,13 +9,9 @@ import javafx.scene.control.*
import javafx.scene.control.cell.TextFieldTableCell
import javafx.scene.input.MouseButton
import javafx.util.Callback
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch
import me.vripper.event.*
import me.vripper.event.EventBus
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.javafx.asFlow
import me.vripper.gui.components.cells.PreviewTableCell
import me.vripper.gui.components.cells.ProgressTableCell
import me.vripper.gui.components.cells.StatusTableCell
@@ -23,324 +20,395 @@ import me.vripper.gui.components.fragments.ColumnSelectionFragment
import me.vripper.gui.components.fragments.RenameFragment
import me.vripper.gui.controller.PostController
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.model.PostModel
import me.vripper.gui.services.ClipboardService
import me.vripper.gui.utils.Preview
import me.vripper.gui.utils.openFileDirectory
import me.vripper.gui.utils.openLink
import me.vripper.services.IAppEndpointService
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
import kotlin.io.path.Path
class PostsTableView : View() {
private val coroutineScope = CoroutineScope(SupervisorJob())
private val postController: PostController by inject()
private val widgetsController: WidgetsController by inject()
private val clipboardService: ClipboardService by di()
private val eventBus: EventBus by di()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val clipboardService: ClipboardService by inject()
private val localAppEndpointService: IAppEndpointService by di("localAppEndpointService")
private val remoteAppEndpointService: IAppEndpointService by di("remoteAppEndpointService")
private val jobs = mutableListOf<Job>()
lateinit var tableView: TableView<PostModel>
private var items: ObservableList<PostModel> = FXCollections.observableArrayList()
val tableView: TableView<PostModel>
var items: ObservableList<PostModel> = FXCollections.observableArrayList()
private var preview: Preview? = null
override val root = vbox {}
init {
titleProperty.bind(items.sizeProperty.map {
if (it.toLong() > 0) {
"Download (${it.toLong()})"
} else {
"Download"
}
})
}
override fun onDock() {
tableView.prefHeightProperty().bind(root.heightProperty())
tableView.placeholder = Label("Loading")
coroutineScope.launch {
val postModelList = postController.findAllPosts().await()
runLater {
items.addAll(postModelList)
tableView.placeholder = Label("No content in table")
clipboardService.init()
}
}
GuiEventBus.events.collect { event ->
when (event) {
is GuiEventBus.LocalSession -> {
postController.appEndpointService = localAppEndpointService
connect()
}
coroutineScope.launch {
eventBus.events.filterIsInstance(PostCreateEvent::class).collect { postEvents ->
val add = postEvents.posts.map { postController.mapper(it.postId) }
runLater {
tableView.items.addAll(add)
}
}
}
is GuiEventBus.RemoteSession -> {
postController.appEndpointService = remoteAppEndpointService
connect()
}
coroutineScope.launch {
eventBus.events.filterIsInstance(PostUpdateEvent::class).collect { postEvents ->
runLater {
for (post in postEvents.posts) {
val postModel = items.find { it.postId == post.postId } ?: continue
postModel.status = post.status.name
postModel.progressCount = postController.progressCount(
post.total, post.done, post.downloaded
)
postModel.order = post.rank + 1
postModel.done = post.done
postModel.progress = postController.progress(
post.total, post.done
)
postModel.path = post.getDownloadFolder()
postModel.folderName = post.folderName
is GuiEventBus.ChangingSession -> {
jobs.forEach { it.cancel() }
jobs.clear()
}
}
}
}
coroutineScope.launch {
eventBus.events.filterIsInstance(PostDeleteEvent::class).collect { postEvents ->
runLater {
postEvents.postIds.forEach {
items.removeIf { p -> p.postId == it }
with(root) {
tableView = tableview(items) {
addClass(Styles.DENSE)
selectionModel.selectionMode = SelectionMode.MULTIPLE
setRowFactory {
val tableRow = TableRow<PostModel>()
val startItem = MenuItem("Start").apply {
setOnAction {
startSelected()
}
graphic = FontIcon.of(Feather.PLAY)
}
val stopItem = MenuItem("Stop").apply {
setOnAction {
stopSelected()
}
graphic = FontIcon.of(Feather.SQUARE)
}
val renameItem = MenuItem("Rename").apply {
setOnAction {
rename(tableRow.item)
}
graphic = FontIcon.of(Feather.EDIT)
}
val deleteItem = MenuItem("Delete").apply {
setOnAction {
deleteSelected()
}
graphic = FontIcon.of(Feather.TRASH)
}
val locationItem = MenuItem("Open containing folder").apply {
visibleWhen(widgetsController.currentSettings.localSessionProperty)
setOnAction {
openFileDirectory(tableRow.item.path)
}
graphic = FontIcon.of(Feather.FOLDER)
}
val urlItem = MenuItem("Open link").apply {
setOnAction {
openLink(tableRow.item.url)
}
graphic = FontIcon.of(Feather.LINK)
}
val contextMenu = ContextMenu()
contextMenu.items.addAll(
startItem, stopItem, renameItem, deleteItem, SeparatorMenuItem(), locationItem, urlItem
)
tableRow.contextMenuProperty()
.bind(tableRow.emptyProperty().map { empty -> if (empty) null else contextMenu })
tableRow
}
}
}
coroutineScope.launch {
eventBus.events.filterIsInstance(MetadataUpdateEvent::class).collect { metadataUpdateEvent ->
runLater {
val postModel = items.find { it.postId == metadataUpdateEvent.metadata.postId } ?: return@runLater
postModel.altTitles =
FXCollections.observableArrayList(metadataUpdateEvent.metadata.data.resolvedNames)
postModel.postedby = metadataUpdateEvent.metadata.data.postedBy
}
}
}
}
override val root = vbox {
tableView = tableview(items) {
selectionModel.selectionMode = SelectionMode.MULTIPLE
setRowFactory {
val tableRow = TableRow<PostModel>()
val startItem = MenuItem("Start").apply {
contextMenu = ContextMenu()
contextMenu.items.addAll(MenuItem("Add links").apply {
graphic = FontIcon.of(Feather.PLUS)
action {
find<AddLinksFragment>().apply {
input.clear()
}.openModal()
}
}, SeparatorMenuItem(), MenuItem("Setup columns").apply {
graphic = FontIcon.of(Feather.COLUMNS)
setOnAction {
startSelected()
}
graphic = FontIcon.of(Feather.PLAY)
}
val stopItem = MenuItem("Stop").apply {
setOnAction {
stopSelected()
}
graphic = FontIcon.of(Feather.SQUARE)
}
val renameItem = MenuItem("Rename").apply {
setOnAction {
rename(tableRow.item)
}
graphic = FontIcon.of(Feather.EDIT)
}
val deleteItem = MenuItem("Delete").apply {
setOnAction {
deleteSelected()
}
graphic = FontIcon.of(Feather.TRASH)
}
val locationItem = MenuItem("Open containing folder").apply {
setOnAction {
openFileDirectory(tableRow.item.path)
}
graphic = FontIcon.of(Feather.FOLDER)
}
val urlItem = MenuItem("Open link").apply {
setOnAction {
openLink(tableRow.item.url)
}
graphic = FontIcon.of(Feather.LINK)
}
val contextMenu = ContextMenu()
contextMenu.items.addAll(
startItem, stopItem, renameItem, deleteItem, SeparatorMenuItem(), locationItem, urlItem
)
tableRow.contextMenuProperty()
.bind(tableRow.emptyProperty().map { empty -> if (empty) null else contextMenu })
tableRow
}
contextMenu = ContextMenu()
contextMenu.items.addAll(MenuItem("Add links").apply {
graphic = FontIcon.of(Feather.PLUS)
action {
find<AddLinksFragment>().apply {
input.clear()
}.openModal()
}
}, SeparatorMenuItem(), MenuItem("Setup columns").apply {
graphic = FontIcon.of(Feather.COLUMNS)
setOnAction {
find<ColumnSelectionFragment>(
mapOf(
ColumnSelectionFragment::map to mapOf(
Pair(
"Preview", widgetsController.currentSettings.postsColumnsModel.previewProperty
),
Pair(
"Title", widgetsController.currentSettings.postsColumnsModel.titleProperty
),
Pair(
"Progress", widgetsController.currentSettings.postsColumnsModel.progressProperty
),
Pair(
"Status", widgetsController.currentSettings.postsColumnsModel.statusProperty
),
Pair(
"Path", widgetsController.currentSettings.postsColumnsModel.pathProperty
),
Pair(
"Total", widgetsController.currentSettings.postsColumnsModel.totalProperty
),
Pair(
"Hosts", widgetsController.currentSettings.postsColumnsModel.hostsProperty
),
Pair(
"Added On", widgetsController.currentSettings.postsColumnsModel.addedOnProperty
),
Pair(
"Order", widgetsController.currentSettings.postsColumnsModel.orderProperty
),
find<ColumnSelectionFragment>(
mapOf(
ColumnSelectionFragment::map to mapOf(
Pair(
"Preview", widgetsController.currentSettings.postsColumnsModel.previewProperty
),
Pair(
"Title", widgetsController.currentSettings.postsColumnsModel.titleProperty
),
Pair(
"Progress", widgetsController.currentSettings.postsColumnsModel.progressProperty
),
Pair(
"Status", widgetsController.currentSettings.postsColumnsModel.statusProperty
),
Pair(
"Path", widgetsController.currentSettings.postsColumnsModel.pathProperty
),
Pair(
"Total", widgetsController.currentSettings.postsColumnsModel.totalProperty
),
Pair(
"Hosts", widgetsController.currentSettings.postsColumnsModel.hostsProperty
),
Pair(
"Added On", widgetsController.currentSettings.postsColumnsModel.addedOnProperty
),
Pair(
"Order", widgetsController.currentSettings.postsColumnsModel.orderProperty
),
)
)
)
).openModal()
}
})
column("Preview", PostModel::previewListProperty) {
visibleProperty().bind(widgetsController.currentSettings.postsColumnsModel.previewProperty)
prefWidth = 100.0
cellFactory = Callback {
val cell = PreviewTableCell<PostModel>()
cell.onMouseExited = EventHandler {
preview?.hide()
).openModal()
}
cell.onMouseMoved = EventHandler {
preview?.previewPopup?.apply {
x = it.screenX + 20
y = it.screenY + 10
})
column("Preview", PostModel::previewListProperty) {
visibleProperty().bind(widgetsController.currentSettings.postsColumnsModel.previewProperty)
prefWidth = widgetsController.currentSettings.postsColumnsWidthModel.preview
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.postsColumnsWidthModel.preview = it as Double
}
}
cell.onMouseEntered = EventHandler { mouseEvent ->
preview?.hide()
if (cell.tableRow.item != null && cell.tableRow.item.previewList.isNotEmpty()) {
preview = Preview(currentStage!!, cell.tableRow.item.previewList)
cellFactory = Callback {
val cell = PreviewTableCell<PostModel>()
cell.onMouseExited = EventHandler {
preview?.hide()
}
cell.onMouseMoved = EventHandler {
preview?.previewPopup?.apply {
x = mouseEvent.screenX + 20
y = mouseEvent.screenY + 10
x = it.screenX + 20
y = it.screenY + 10
}
}
}
cell
}
}
column("Title", PostModel::titleProperty) {
visibleProperty().bind(widgetsController.currentSettings.postsColumnsModel.titleProperty)
prefWidth = 250.0
cellFactory = Callback {
TextFieldTableCell<PostModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
}
}
column("Progress", PostModel::progressProperty) {
visibleProperty().bind(widgetsController.currentSettings.postsColumnsModel.progressProperty)
prefWidth = 100.0
cellFactory = Callback {
val cell = ProgressTableCell<PostModel>()
cell.alignment = Pos.CENTER
cell.setOnMouseClick {
when (it.button) {
MouseButton.SECONDARY -> {
this@tableview.requestFocus()
this@tableview.focusModel.focus(cell.tableRow.index)
if (!this@tableview.selectionModel.isSelected(cell.tableRow.index)) {
this@tableview.selectionModel.clearSelection()
this@tableview.selectionModel.select(cell.tableRow.index)
cell.onMouseEntered = EventHandler { mouseEvent ->
preview?.hide()
if (cell.tableRow.item != null && cell.tableRow.item.previewList.isNotEmpty()) {
preview = Preview(
currentStage!!,
cell.tableRow.item.previewList,
Path(widgetsController.currentSettings.cachePath)
)
preview?.previewPopup?.apply {
x = mouseEvent.screenX + 20
y = mouseEvent.screenY + 10
}
}
}
cell
}
}
column("Title", PostModel::titleProperty) {
visibleProperty().bind(widgetsController.currentSettings.postsColumnsModel.titleProperty)
prefWidth = widgetsController.currentSettings.postsColumnsWidthModel.title
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.postsColumnsWidthModel.title = it as Double
}
}
cellFactory = Callback {
TextFieldTableCell<PostModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
}
}
column("Progress", PostModel::progressProperty) {
visibleProperty().bind(widgetsController.currentSettings.postsColumnsModel.progressProperty)
prefWidth = widgetsController.currentSettings.postsColumnsWidthModel.progress
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.postsColumnsWidthModel.progress = it as Double
}
}
cellFactory = Callback {
val cell = ProgressTableCell<PostModel>()
cell.alignment = Pos.CENTER
cell.setOnMouseClick {
when (it.button) {
MouseButton.SECONDARY -> {
this@tableview.requestFocus()
this@tableview.focusModel.focus(cell.tableRow.index)
if (!this@tableview.selectionModel.isSelected(cell.tableRow.index)) {
this@tableview.selectionModel.clearSelection()
this@tableview.selectionModel.select(cell.tableRow.index)
}
}
MouseButton.PRIMARY -> {
when (it.clickCount) {
1 -> {
this@tableview.requestFocus()
this@tableview.focusModel.focus(cell.tableRow.index)
if (it.isControlDown && it.button.equals(MouseButton.PRIMARY)) {
if (this@tableview.selectionModel.isSelected(cell.tableRow.index)) {
this@tableview.selectionModel.clearSelection(cell.tableRow.index)
} else {
MouseButton.PRIMARY -> {
when (it.clickCount) {
1 -> {
this@tableview.requestFocus()
this@tableview.focusModel.focus(cell.tableRow.index)
if (it.isControlDown && it.button.equals(MouseButton.PRIMARY)) {
if (this@tableview.selectionModel.isSelected(cell.tableRow.index)) {
this@tableview.selectionModel.clearSelection(cell.tableRow.index)
} else {
this@tableview.selectionModel.select(cell.tableRow.index)
}
} else if (it.button.equals(MouseButton.PRIMARY)) {
this@tableview.selectionModel.clearSelection()
this@tableview.selectionModel.select(cell.tableRow.index)
}
} else if (it.button.equals(MouseButton.PRIMARY)) {
this@tableview.selectionModel.clearSelection()
this@tableview.selectionModel.select(cell.tableRow.index)
}
}
}
}
else -> {}
else -> {}
}
}
cell as TableCell<PostModel, Number>
}
}
column("Status", PostModel::statusProperty) {
visibleProperty().bind(widgetsController.currentSettings.postsColumnsModel.statusProperty)
prefWidth = widgetsController.currentSettings.postsColumnsWidthModel.status
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.postsColumnsWidthModel.status = it as Double
}
}
cell as TableCell<PostModel, Number>
cellFactory = Callback {
StatusTableCell<PostModel>()
}
}
}
column("Status", PostModel::statusProperty) {
visibleProperty().bind(widgetsController.currentSettings.postsColumnsModel.statusProperty)
prefWidth = 100.0
cellFactory = Callback {
StatusTableCell<PostModel>()
column("Path", PostModel::pathProperty) {
visibleProperty().bind(widgetsController.currentSettings.postsColumnsModel.pathProperty)
prefWidth = widgetsController.currentSettings.postsColumnsWidthModel.path
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.postsColumnsWidthModel.path = it as Double
}
}
cellFactory = Callback {
TextFieldTableCell<PostModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
}
}
}
column("Path", PostModel::pathProperty) {
visibleProperty().bind(widgetsController.currentSettings.postsColumnsModel.pathProperty)
prefWidth = 250.0
cellFactory = Callback {
TextFieldTableCell<PostModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
column("Total", PostModel::progressCountProperty) {
visibleProperty().bind(widgetsController.currentSettings.postsColumnsModel.totalProperty)
prefWidth = widgetsController.currentSettings.postsColumnsWidthModel.total
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.postsColumnsWidthModel.total = it as Double
}
}
cellFactory = Callback {
TextFieldTableCell<PostModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
}
}
}
column("Total", PostModel::progressCountProperty) {
visibleProperty().bind(widgetsController.currentSettings.postsColumnsModel.totalProperty)
prefWidth = 150.0
cellFactory = Callback {
TextFieldTableCell<PostModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
column("Hosts", PostModel::hostsProperty) {
visibleProperty().bind(widgetsController.currentSettings.postsColumnsModel.hostsProperty)
prefWidth = widgetsController.currentSettings.postsColumnsWidthModel.hosts
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.postsColumnsWidthModel.hosts = it as Double
}
}
cellFactory = Callback {
TextFieldTableCell<PostModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
}
}
}
column("Hosts", PostModel::hostsProperty) {
visibleProperty().bind(widgetsController.currentSettings.postsColumnsModel.hostsProperty)
prefWidth = 100.0
cellFactory = Callback {
TextFieldTableCell<PostModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
column("Added On", PostModel::addedOnProperty) {
visibleProperty().bind(widgetsController.currentSettings.postsColumnsModel.addedOnProperty)
prefWidth = widgetsController.currentSettings.postsColumnsWidthModel.addedOn
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.postsColumnsWidthModel.addedOn = it as Double
}
}
cellFactory = Callback {
TextFieldTableCell<PostModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
}
}
}
column("Added On", PostModel::addedOnProperty) {
visibleProperty().bind(widgetsController.currentSettings.postsColumnsModel.addedOnProperty)
prefWidth = 125.0
cellFactory = Callback {
TextFieldTableCell<PostModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
}
}
column("Order", PostModel::orderProperty) {
visibleProperty().bind(widgetsController.currentSettings.postsColumnsModel.orderProperty)
prefWidth = 100.0
sortOrder.add(this)
cellFactory = Callback {
TextFieldTableCell<PostModel?, Number?>().apply { alignment = Pos.CENTER_LEFT }
column("Order", PostModel::orderProperty) {
visibleProperty().bind(widgetsController.currentSettings.postsColumnsModel.orderProperty)
prefWidth = widgetsController.currentSettings.postsColumnsWidthModel.order
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.postsColumnsWidthModel.order = it as Double
}
}
sortType = TableColumn.SortType.DESCENDING
sortOrder.add(this)
cellFactory = Callback {
TextFieldTableCell<PostModel?, Number?>().apply { alignment = Pos.CENTER_LEFT }
}
}
}
}
tableView.prefHeightProperty().bind(root.heightProperty())
tableView.placeholder = Label("Loading")
}
private fun connect() {
coroutineScope.launch {
val postModelList = async { postController.findAllPosts() }.await()
runLater {
items.clear()
items.addAll(postModelList)
tableView.sort()
tableView.placeholder = Label("No content in table")
clipboardService.init(postController.appEndpointService)
}
}
coroutineScope.launch {
postController.onNewPosts().collect {
runLater {
tableView.items.addAll(it)
tableView.sort()
}
}
}.also { jobs.add(it) }
coroutineScope.launch {
postController.onUpdatePosts().collect { post ->
runLater {
val postModel = items.find { it.postId == post.postId } ?: return@runLater
postModel.status = post.status.name
postModel.progressCount = postController.progressCount(
post.total, post.done, post.downloaded
)
postModel.order = post.rank + 1
postModel.done = post.done
postModel.progress = postController.progress(
post.total, post.done
)
postModel.path = post.getDownloadFolder()
postModel.folderName = post.folderName
}
}
}.also { jobs.add(it) }
coroutineScope.launch {
postController.onDeletePosts().collect {
runLater {
items.removeIf { p -> p.postId == it }
tableView.sort()
}
}
}.also { jobs.add(it) }
coroutineScope.launch {
postController.onUpdateMetadata().collect {
runLater {
val postModel = items.find { it.postId == it.postId } ?: return@runLater
postModel.altTitles = FXCollections.observableArrayList(it.data.resolvedNames)
postModel.postedBy = it.data.postedBy
}
}
}.also { jobs.add(it) }
}
private fun rename(post: PostModel) {
@@ -372,18 +440,26 @@ class PostsTableView : View() {
owner = primaryStage,
title = "Remove posts"
) {
postController.delete(postIdList)
tableView.items.removeIf { postIdList.contains(it.postId) }
coroutineScope.launch {
postController.delete(postIdList)
runLater {
tableView.items.removeIf { postIdList.contains(it.postId) }
}
}
}
}
fun stopSelected() {
val postIdList = tableView.selectionModel.selectedItems.map { it.postId }
postController.stop(postIdList)
coroutineScope.launch {
postController.stop(postIdList)
}
}
fun startSelected() {
val postIdList = tableView.selectionModel.selectedItems.map { it.postId }
postController.start(postIdList)
coroutineScope.launch {
postController.start(postIdList)
}
}
}
@@ -0,0 +1,179 @@
package me.vripper.gui.components.views
import io.grpc.ConnectivityState
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleStringProperty
import javafx.geometry.Orientation
import javafx.geometry.Pos
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import me.vripper.delegate.LoggerDelegate
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.services.GrpcEndpointService
import me.vripper.services.AppEndpointService
import me.vripper.services.IAppEndpointService
import me.vripper.utilities.formatSI
import tornadofx.*
class StatusBarView : View("Status bar") {
private val logger by LoggerDelegate()
private val widgetsController: WidgetsController by inject()
private val coroutineScope = CoroutineScope(SupervisorJob())
private val grpcEndpointService: GrpcEndpointService by di("remoteAppEndpointService")
private val localEndpointService: AppEndpointService by di("localAppEndpointService")
private val remoteText = SimpleStringProperty()
private val loggedUser = SimpleStringProperty()
private val tasksRunning = SimpleBooleanProperty(false)
private val downloadSpeed = SimpleStringProperty(0L.formatSI())
private val running = SimpleIntegerProperty(0)
private val pending = SimpleIntegerProperty(0)
private val error = SimpleIntegerProperty(0)
private val jobs = mutableListOf<Job>()
init {
coroutineScope.launch {
GuiEventBus.events.collect { event ->
when (event) {
is GuiEventBus.LocalSession -> {
connect(localEndpointService)
}
is GuiEventBus.RemoteSession -> {
connect(grpcEndpointService)
}
is GuiEventBus.ChangingSession -> {
jobs.forEach { it.cancel() }
jobs.clear()
}
}
}
}
}
private fun connect(endpointService: IAppEndpointService) {
coroutineScope.launch {
val user = async { endpointService.loggedInUser() }.await()
runLater {
loggedUser.set(user)
}
}
coroutineScope.launch {
endpointService.onVGUserUpdate().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}.collect {
runLater {
loggedUser.set(it)
}
}
}.also { jobs.add(it) }
coroutineScope.launch {
endpointService.onTasksRunning().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}.collect {
runLater {
tasksRunning.set(it)
}
}
}.also { jobs.add(it) }
coroutineScope.launch {
endpointService.onDownloadSpeed().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}.collect {
runLater {
downloadSpeed.set(it.formatSI())
}
}
}.also { jobs.add(it) }
coroutineScope.launch {
endpointService.onQueueStateUpdate().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}.collect {
runLater {
running.set(it.running)
pending.set(it.remaining)
}
}
}.also { jobs.add(it) }
coroutineScope.launch {
endpointService.onErrorCountUpdate().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}.collect {
runLater {
error.set(it)
}
}
}.also { jobs.add(it) }
if (!widgetsController.currentSettings.localSession) {
coroutineScope.launch {
while (isActive) {
if (grpcEndpointService.connectionState() != ConnectivityState.READY) {
runLater {
remoteText.set("Unable to connect to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}")
}
} else {
val version = grpcEndpointService.getVersion()
runLater {
remoteText.set("Connected to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port} v$version")
}
}
delay(5000)
}
}.also { jobs.add(it) }
}
}
override val root = borderpane {
id = "statusbar"
left {
hbox {
label(remoteText) {
visibleWhen { widgetsController.currentSettings.localSessionProperty.not() }
}
separator(Orientation.VERTICAL) {
visibleWhen {
widgetsController.currentSettings.localSessionProperty.not().and(loggedUser.isNotBlank())
}
}
label(loggedUser.map { "Logged in as: $it" }) {
visibleWhen(loggedUser.isNotBlank())
}
}
}
right {
padding = insets(right = 5, left = 5, top = 3, bottom = 3)
hbox {
spacing = 3.0
progressbar {
visibleWhen(tasksRunning)
}
separator(Orientation.VERTICAL) {
visibleWhen(tasksRunning)
}
label(downloadSpeed.map { "$it/s" })
separator(Orientation.VERTICAL)
label("Downloading")
label(running.asString())
separator(Orientation.VERTICAL)
label("Pending")
label(pending.asString())
separator(Orientation.VERTICAL)
label("Error")
label(error.asString())
alignment = Pos.CENTER_RIGHT
}
}
}
}
@@ -1,34 +1,165 @@
package me.vripper.gui.components.views
import atlantafx.base.theme.Styles
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.scene.control.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.filterIsInstance
import me.vripper.event.EventBus
import me.vripper.event.ThreadClearEvent
import me.vripper.event.ThreadCreateEvent
import me.vripper.event.ThreadDeleteEvent
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.javafx.asFlow
import me.vripper.gui.components.fragments.ColumnSelectionFragment
import me.vripper.gui.components.fragments.ThreadSelectionTableFragment
import me.vripper.gui.controller.ThreadController
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.model.ThreadModel
import me.vripper.gui.utils.openLink
import me.vripper.services.IAppEndpointService
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
class ThreadTableView : View() {
private val coroutineScope = CoroutineScope(SupervisorJob())
private val threadController: ThreadController by inject()
private val widgetsController: WidgetsController by inject()
private val eventBus: EventBus by di()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
lateinit var tableView: TableView<ThreadModel>
private var items: ObservableList<ThreadModel> = FXCollections.observableArrayList()
private val localAppEndpointService: IAppEndpointService by di("localAppEndpointService")
private val remoteAppEndpointService: IAppEndpointService by di("remoteAppEndpointService")
private val tableView: TableView<ThreadModel>
private val items: ObservableList<ThreadModel> = FXCollections.observableArrayList()
private val jobs = mutableListOf<Job>()
override val root = vbox {}
init {
coroutineScope.launch {
GuiEventBus.events.collect { event ->
when (event) {
is GuiEventBus.LocalSession -> {
threadController.appEndpointService = localAppEndpointService
connect()
}
is GuiEventBus.RemoteSession -> {
threadController.appEndpointService = remoteAppEndpointService
connect()
}
is GuiEventBus.ChangingSession -> {
jobs.forEach { it.cancel() }
jobs.clear()
}
}
}
}
with(root) {
tableView = tableview(items) {
addClass(Styles.DENSE)
selectionModel.selectionMode = SelectionMode.MULTIPLE
setRowFactory {
val tableRow = TableRow<ThreadModel>()
tableRow.setOnMouseClicked {
if (it.clickCount == 2 && tableRow.item != null) {
selectPosts(tableRow.item.threadId)
}
}
val selectItem = MenuItem("Select posts").apply {
setOnAction {
selectPosts(tableRow.item.threadId)
}
graphic = FontIcon.of(Feather.MENU)
}
val urlItem = MenuItem("Open link").apply {
setOnAction {
openLink(tableRow.item.link)
}
graphic = FontIcon.of(Feather.LINK)
}
val deleteItem = MenuItem("Delete").apply {
setOnAction {
deleteSelected()
}
graphic = FontIcon.of(Feather.TRASH)
}
val contextMenu = ContextMenu()
contextMenu.items.addAll(selectItem, urlItem, SeparatorMenuItem(), deleteItem)
tableRow.contextMenuProperty()
.bind(tableRow.emptyProperty().map { empty -> if (empty) null else contextMenu })
tableRow
}
contextMenu = ContextMenu()
contextMenu.items.addAll(MenuItem("Setup columns").apply {
setOnAction {
find<ColumnSelectionFragment>(
mapOf(
ColumnSelectionFragment::map to mapOf(
Pair(
"Title", widgetsController.currentSettings.threadsColumnsModel.titleProperty
),
Pair(
"URL", widgetsController.currentSettings.threadsColumnsModel.linkProperty
),
Pair(
"Count", widgetsController.currentSettings.threadsColumnsModel.countProperty
),
)
)
).openModal()
}
graphic = FontIcon.of(Feather.COLUMNS)
}, SeparatorMenuItem(), MenuItem("Clear", FontIcon.of(Feather.TRASH_2)).apply {
setOnAction {
confirm(
"",
"Confirm removal of all threads",
ButtonType.YES,
ButtonType.NO,
owner = primaryStage,
title = "Clean threads"
) {
coroutineScope.launch {
threadController.clearAll()
}
}
}
})
column("Title", ThreadModel::titleProperty) {
visibleProperty().bind(widgetsController.currentSettings.threadsColumnsModel.titleProperty)
prefWidth = widgetsController.currentSettings.threadsColumnsWidthModel.title
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.threadsColumnsWidthModel.title = it as Double
}
}
}
column("URL", ThreadModel::linkProperty) {
visibleProperty().bind(widgetsController.currentSettings.threadsColumnsModel.linkProperty)
prefWidth = widgetsController.currentSettings.threadsColumnsWidthModel.link
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.threadsColumnsWidthModel.link = it as Double
}
}
}
column("Count", ThreadModel::totalProperty) {
visibleProperty().bind(widgetsController.currentSettings.threadsColumnsModel.countProperty)
prefWidth = widgetsController.currentSettings.threadsColumnsWidthModel.count
coroutineScope.launch {
widthProperty().asFlow().debounce(200).collect {
widgetsController.currentSettings.threadsColumnsWidthModel.count = it as Double
}
}
}
}
}
titleProperty.bind(items.sizeProperty.map {
if (it.toLong() > 0) {
"Threads (${it.toLong()})"
@@ -36,143 +167,57 @@ class ThreadTableView : View() {
"Threads"
}
})
}
override fun onDock() {
tableView.prefHeightProperty().bind(root.heightProperty())
tableView.placeholder = Label("Loading")
}
fun connect() {
coroutineScope.launch {
val list = coroutineScope.async {
val list = async {
threadController.findAll()
}.await()
runLater {
items.clear()
items.addAll(list)
tableView.placeholder = Label("No content in table")
}
}
coroutineScope.launch {
eventBus.events.filterIsInstance(ThreadCreateEvent::class).collect {
val threadModelMapper = threadController.threadModelMapper(it.thread)
threadController.onNewThread().collect {
runLater {
items.add(threadModelMapper)
items.add(it)
}
}
}
}.also { jobs.add(it) }
coroutineScope.launch {
eventBus.events.filterIsInstance(ThreadCreateEvent::class).collect {
val threadModelMapper = threadController.threadModelMapper(it.thread)
threadController.onUpdateThread().collect { thread ->
runLater {
items.add(threadModelMapper)
val threadModel = items.find { it.threadId == thread.threadId } ?: return@runLater
threadModel.total = thread.total
threadModel.title = thread.title
}
}
}
}.also { jobs.add(it) }
coroutineScope.launch {
eventBus.events.filterIsInstance(ThreadDeleteEvent::class).collect { event ->
threadController.onDeleteThread().collect { threadId ->
runLater {
tableView.items.removeIf { it.threadId == event.threadId }
tableView.items.removeIf { it.threadId == threadId }
}
}
}
}.also { jobs.add(it) }
coroutineScope.launch {
eventBus.events.filterIsInstance(ThreadClearEvent::class).collect {
threadController.onClearThreads().collect {
runLater {
tableView.items.clear()
}
}
}
}.also { jobs.add(it) }
}
override val root = vbox {
tableView = tableview(items) {
selectionModel.selectionMode = SelectionMode.MULTIPLE
setRowFactory {
val tableRow = TableRow<ThreadModel>()
tableRow.setOnMouseClicked {
if (it.clickCount == 2 && tableRow.item != null) {
selectPosts(tableRow.item.threadId)
}
}
val selectItem = MenuItem("Select posts").apply {
setOnAction {
selectPosts(tableRow.item.threadId)
}
graphic = FontIcon.of(Feather.MENU)
}
val urlItem = MenuItem("Open link").apply {
setOnAction {
openLink(tableRow.item.link)
}
graphic = FontIcon.of(Feather.LINK)
}
val deleteItem = MenuItem("Delete").apply {
setOnAction {
deleteSelected()
}
graphic = FontIcon.of(Feather.TRASH)
}
val contextMenu = ContextMenu()
contextMenu.items.addAll(selectItem, urlItem, SeparatorMenuItem(), deleteItem)
tableRow.contextMenuProperty()
.bind(tableRow.emptyProperty().map { empty -> if (empty) null else contextMenu })
tableRow
}
contextMenu = ContextMenu()
contextMenu.items.addAll(MenuItem("Setup columns").apply {
setOnAction {
find<ColumnSelectionFragment>(
mapOf(
ColumnSelectionFragment::map to mapOf(
Pair(
"Title", widgetsController.currentSettings.threadsColumnsModel.titleProperty
),
Pair(
"URL", widgetsController.currentSettings.threadsColumnsModel.linkProperty
),
Pair(
"Count", widgetsController.currentSettings.threadsColumnsModel.countProperty
),
)
)
).openModal()
}
graphic = FontIcon.of(Feather.COLUMNS)
}, SeparatorMenuItem(), MenuItem("Clear", FontIcon.of(Feather.TRASH_2)).apply {
setOnAction {
confirm(
"",
"Confirm removal of all threads",
ButtonType.YES,
ButtonType.NO,
owner = primaryStage,
title = "Clean threads"
) {
threadController.clearAll()
}
}
})
column("Title", ThreadModel::titleProperty) {
visibleProperty().bind(widgetsController.currentSettings.threadsColumnsModel.titleProperty)
prefWidth = 350.0
}
column("URL", ThreadModel::linkProperty) {
visibleProperty().bind(widgetsController.currentSettings.threadsColumnsModel.linkProperty)
prefWidth = 350.0
}
column("Count", ThreadModel::totalProperty) {
visibleProperty().bind(widgetsController.currentSettings.threadsColumnsModel.countProperty)
}
}
}
fun deleteSelected() {
private fun deleteSelected() {
val threadIdList = tableView.selectionModel.selectedItems.map { it.threadId }
confirm(
"",
@@ -182,7 +227,9 @@ class ThreadTableView : View() {
owner = primaryStage,
title = "Clean threads"
) {
threadController.delete(threadIdList)
coroutineScope.launch {
threadController.delete(threadIdList)
}
}
}
@@ -1,66 +0,0 @@
package me.vripper.gui.controller
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.time.sample
import me.vripper.event.*
import me.vripper.event.EventBus
import me.vripper.gui.model.GlobalStateModel
import me.vripper.services.VGAuthService
import me.vripper.utilities.formatSI
import tornadofx.*
import java.time.Duration
class GlobalStateController : Controller() {
private val eventBus: EventBus by di()
private val vgAuthService: VGAuthService by di()
var globalState: GlobalStateModel =
GlobalStateModel(0, 0, 0, vgAuthService.loggedUser, 0L.formatSI(), false)
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
@OptIn(FlowPreview::class)
fun init() {
coroutineScope.launch {
eventBus.events.filterIsInstance(DownloadSpeedEvent::class).collect {
runLater {
globalState.downloadSpeed = it.downloadSpeed.speed.formatSI()
}
}
}
coroutineScope.launch {
eventBus.events.filterIsInstance(VGUserLoginEvent::class).collect {
runLater {
globalState.loggedUser = it.username
}
}
}
coroutineScope.launch {
eventBus.events.filterIsInstance(QueueStateEvent::class).collect {
runLater {
globalState.apply {
running = it.queueState.running
remaining = it.queueState.remaining
}
}
}
}
coroutineScope.launch {
eventBus.events.filterIsInstance(ErrorCountEvent::class).collect {
runLater {
globalState.error = it.errorCount.count
}
}
}
coroutineScope.launch {
eventBus.events.filterIsInstance(LoadingTasks::class).sample(Duration.ofMillis(500))
.collect {
runLater {
globalState.loading = it.loading
}
}
}
}
}
@@ -1,16 +1,19 @@
package me.vripper.gui.controller
import me.vripper.entities.Image
import kotlinx.coroutines.cancel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.catch
import me.vripper.delegate.LoggerDelegate
import me.vripper.gui.model.ImageModel
import me.vripper.services.DataTransaction
import me.vripper.model.Image
import me.vripper.services.IAppEndpointService
import tornadofx.Controller
class ImageController : Controller() {
private val dataTransaction by di<DataTransaction>()
fun findImages(postId: Long): List<ImageModel> {
return dataTransaction.findImagesByPostId(postId).map(::mapper)
private val logger by LoggerDelegate()
lateinit var appEndpointService: IAppEndpointService
suspend fun findImages(postId: Long): List<ImageModel> {
return appEndpointService.findImagesByPostId(postId).map(::mapper)
}
private fun mapper(it: Image): ImageModel {
@@ -34,4 +37,16 @@ class ImageController : Controller() {
downloaded.toDouble() / size
}
}
fun onUpdateImages(postId: Long) =
appEndpointService.onUpdateImages(postId).catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}
fun onStopped() = appEndpointService.onStopped().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}
}
@@ -1,32 +1,47 @@
package me.vripper.gui.controller
import me.vripper.entities.LogEntry
import kotlinx.coroutines.cancel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import me.vripper.delegate.LoggerDelegate
import me.vripper.gui.model.LogModel
import me.vripper.services.AppEndpointService
import me.vripper.services.DataTransaction
import me.vripper.model.LogEntry
import me.vripper.services.IAppEndpointService
import tornadofx.Controller
import java.time.format.DateTimeFormatter
class LogController : Controller() {
private val appEndpointService: AppEndpointService by di()
private val dataTransaction: DataTransaction by di()
private val logger by LoggerDelegate()
lateinit var appEndpointService: IAppEndpointService
private val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
fun findAll(): List<LogModel> {
return dataTransaction.findAllLogs().map(::mapper)
suspend fun findAll(): List<LogModel> {
return appEndpointService.findAllLogs().map(::mapper)
}
fun clear() {
suspend fun clear() {
appEndpointService.logClear()
}
fun mapper(it: LogEntry): LogModel {
private fun mapper(it: LogEntry): LogModel {
return LogModel(
it.id,
it.type.stringValue,
it.status.stringValue,
it.time.format(dateTimeFormatter),
it.message
it.id, it.type.stringValue, it.status.stringValue, it.time.format(dateTimeFormatter), it.message
)
}
fun onNewLog() = appEndpointService.onNewLog().map(::mapper).catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}
fun onUpdateLog() = appEndpointService.onUpdateLog().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}
fun onDeleteLogs() = appEndpointService.onDeleteLogs().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}
}

Some files were not shown because too many files have changed in this diff Show More