mirror of
https://github.com/dev-claw/vripper-project.git
synced 2026-08-19 08:35:41 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
331749ca91 | ||
|
|
e9b4ca24eb | ||
|
|
e5bbbd651f | ||
|
|
e7a2ed6e8b | ||
|
|
2923cc350c | ||
|
|
53cdc8a4a1 | ||
|
|
592a6d44c3 | ||
|
|
dbf6e3436a |
@@ -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,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"
|
||||
|
||||
@@ -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
@@ -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
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -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,
|
||||
+1
-1
@@ -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
-8
@@ -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
|
||||
}
|
||||
+2
-2
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
package me.vripper.model
|
||||
package me.vripper.parser
|
||||
|
||||
import me.vripper.host.Host
|
||||
|
||||
+1
-1
@@ -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
-1
@@ -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,
|
||||
|
||||
+12
-10
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
-19
@@ -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) {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
syntax = "proto3";
|
||||
option java_package = "me.vripper.proto";
|
||||
import "image.proto";
|
||||
|
||||
message Images {
|
||||
repeated Image images = 1;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
${project.version}
|
||||
+43
-16
@@ -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()
|
||||
}
|
||||
}
|
||||
+12
-1
@@ -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()
|
||||
}
|
||||
}
|
||||
+46
-34
@@ -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()
|
||||
}
|
||||
}
|
||||
+78
-59
@@ -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()
|
||||
}
|
||||
}
|
||||
-251
@@ -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()
|
||||
}
|
||||
}
|
||||
-131
@@ -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()
|
||||
}
|
||||
}
|
||||
+23
-12
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+63
-54
@@ -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()
|
||||
}
|
||||
}
|
||||
+46
-10
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+46
-32
@@ -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
Reference in New Issue
Block a user