Compare commits

...
Author SHA1 Message Date
dev-claw dca4499654 fixes #231 2025-04-15 00:04:16 +01:00
40 changed files with 712 additions and 1103 deletions
+3 -2
View File
@@ -22,9 +22,10 @@ Direct access to `vipergirls.to` domain.
<img src="https://github.com/stashapp/stash/raw/develop/docs/readme_assets/windows_logo.svg" width="100%" height="75"> Windows | <img src="https://github.com/stashapp/stash/raw/develop/docs/readme_assets/mac_logo.svg" width="100%" height="75"> macOS (Intel) | <img src="https://github.com/stashapp/stash/raw/develop/docs/readme_assets/mac_logo.svg" width="100%" height="75"> macOS (Apple silicon) | <img src="https://github.com/stashapp/stash/raw/develop/docs/readme_assets/linux_logo.svg" width="100%" height="75"> Linux | <img src="https://images.vexels.com/media/users/3/166401/isolated/preview/b82aa7ac3f736dd78570dd3fa3fa9e24-java-programming-language-icon-by-vexels.png" width="100%" height="75"> Java
:---:|:---:|:---:|:---:|:---:
[Installer (EXE)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-windows-installer-6.5.3.exe) <br /> [Installer (MSI)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-windows-installer-6.5.3.msi) <br /> [Portable (ZIP)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-windows-portable-6.5.3.zip) | [Installer (DMG)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-macos-6.5.3.x86_64.dmg) <br /> [Installer (PKG)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-macos-6.5.3.x86_64.pkg) <br /> [Portable (ZIP)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-macos-portable-6.5.3.x86_64.zip) | [Installer (DMG)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-macos-6.5.3.arm64.dmg) <br /> [Installer (PKG)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-macos-6.5.3.arm64.pkg) <br /> [Portable (ZIP)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-macos-portable-6.5.3.arm64.zip) | [Linux (amd64) (DEB)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-linux-6.5.3_amd64.deb) <br /> [Linux (x86_64) (RPM)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-linux-6.5.3.x86_64.rpm) <br /> [Portable (ZIP)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-linux-portable-6.5.3.zip) | [Java GUI (noarch)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-noarch-gui-6.5.3.jar) <br /> [Java Web (noarch)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-noarch-web-6.5.3.jar)
[Installer (EXE)](https://github.com/dev-claw/vripper-project/releases/download/6.5.3/vripper-windows-installer-6.5.3.exe) <br /> [Installer (MSI)](https://github.com/dev-claw/vripper-project/releases/download/6.5.3/vripper-windows-installer-6.5.3.msi) <br /> [Portable (ZIP)](https://github.com/dev-claw/vripper-project/releases/download/6.5.3/vripper-windows-portable-6.5.3.zip) | [Installer (DMG)](https://github.com/dev-claw/vripper-project/releases/download/6.5.3/vripper-macos-6.5.3.x86_64.dmg) <br /> [Installer (PKG)](https://github.com/dev-claw/vripper-project/releases/download/6.5.3/vripper-macos-6.5.3.x86_64.pkg) <br /> [Portable (ZIP)](https://github.com/dev-claw/vripper-project/releases/download/6.5.3/vripper-macos-portable-6.5.3.x86_64.zip) | [Installer (DMG)](https://github.com/dev-claw/vripper-project/releases/download/6.5.3/vripper-macos-6.5.3.arm64.dmg) <br /> [Installer (PKG)](https://github.com/dev-claw/vripper-project/releases/download/6.5.3/vripper-macos-6.5.3.arm64.pkg) <br /> [Portable (ZIP)](https://github.com/dev-claw/vripper-project/releases/download/6.5.3/vripper-macos-portable-6.5.3.arm64.zip) | [Linux (amd64) (DEB)](https://github.com/dev-claw/vripper-project/releases/download/6.5.3/vripper-linux-6.5.3_amd64.deb) <br /> [Linux (x86_64) (RPM)](https://github.com/dev-claw/vripper-project/releases/download/6.5.3/vripper-linux-6.5.3.x86_64.rpm) <br /> [Portable (ZIP)](https://github.com/dev-claw/vripper-project/releases/download/6.5.3/vripper-linux-portable-6.5.3.zip) | [Java GUI (noarch)](https://github.com/dev-claw/vripper-project/releases/download/6.5.3/vripper-noarch-gui-6.5.3.jar) <br /> [Java Web (noarch)](https://github.com/dev-claw/vripper-project/releases/download/6.5.3/vripper-noarch-web-6.5.3.jar)
Source code and previous versions are available on the [Releases page](https://github.com/death-claw/vripper-project/releases).
Source code and previous versions are available on
the [Releases page](https://github.com/dev-claw/vripper-project/releases).
Application data (application logs, settings and persisted data) is stored in:
* Windows --> `C:\USERS\<your Windows username>\vripper`
+1 -1
View File
@@ -5,4 +5,4 @@
--win-per-user-install
--win-shortcut
--win-shortcut-prompt
--win-update-url https://github.com/death-claw/vripper-project/releases
--win-update-url https://github.com/dev-claw/vripper-project/releases
+1 -1
View File
@@ -5,4 +5,4 @@
--name VRipper
--description "Image ripper tool for vipergirls"
--dest dist
--vendor "death-claw"
--vendor "dev-claw"
@@ -5,7 +5,6 @@ import kotlinx.coroutines.flow.filterIsInstance
import me.vripper.event.EventBus
import me.vripper.event.SettingsUpdateEvent
import me.vripper.services.*
import me.vripper.utilities.DatabaseManager
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -38,7 +37,6 @@ object AppManager : KoinComponent {
threadCacheService.invalidate()
}
}
DatabaseManager.connect()
dataTransaction.setDownloadingToStopped()
dataTransaction.stopImagesByPostIdAndIsNotCompleted()
settingsService.init()
@@ -49,11 +47,8 @@ object AppManager : KoinComponent {
fun stop() {
job?.cancel()
if (DatabaseManager.isConnected()) {
downloadService.halt()
downloadService.stop()
downloadSpeedService.halt()
DatabaseManager.disconnect()
}
downloadService.halt()
downloadService.stop()
downloadSpeedService.halt()
}
}
@@ -451,4 +451,8 @@ internal class AppEndpointService(
"Successfully imported $postsCount posts and $threadCount threads"
}
}
override fun connectionState(): String {
return ""
}
}
@@ -206,6 +206,7 @@ internal class DownloadService(
fun init() {
downloadMonitorThread?.interrupt()
downloadMonitorThread = Thread.ofVirtual().name("Download Monitor").unstarted(Runnable {
log.info("Scheduler have been initialized")
val accepted: MutableList<ImageDownloadRunnable> = mutableListOf()
val candidates: MutableList<ImageDownloadRunnable> = mutableListOf()
while (!Thread.currentThread().isInterrupted) {
@@ -231,6 +232,7 @@ internal class DownloadService(
}
}
}
log.info("Scheduler have been shutdown")
})
downloadMonitorThread?.start()
}
@@ -44,4 +44,5 @@ interface IAppEndpointService {
suspend fun renameToFirst(postIds: List<Long>)
suspend fun dbMigration(): String
suspend fun initLogger()
fun connectionState(): String
}
@@ -22,6 +22,7 @@ internal class AddPostTask(private val items: List<ThreadPostId>) : KoinComponen
val toProcess = mutableListOf<PostItem>()
for ((threadId, postId) in items) {
if (dataTransaction.exists(postId)) {
log.info("Post $postId already loaded")
continue
}
@@ -56,6 +57,10 @@ internal class AddPostTask(private val items: List<ThreadPostId>) : KoinComponen
toProcess.add(postItem)
}
if (toProcess.isEmpty()) {
return
}
val posts = try {
dataTransaction.newPosts(toProcess.toList())
} catch (e: Exception) {
@@ -7,34 +7,15 @@ import liquibase.database.DatabaseFactory
import liquibase.database.jvm.JdbcConnection
import liquibase.resource.ClassLoaderResourceAccessor
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.transactions.TransactionManager
import java.sql.Connection
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
object DatabaseManager {
private var database: Database? = null
private var connected: Boolean = false
private var lock = ReentrantLock()
fun connect() {
lock.withLock {
database =
Database.connect("jdbc:sqlite:${ApplicationProperties.VRIPPER_DIR}/vripper.db")
.also { update(it.connector.invoke().connection as Connection) }
connected = true
}
}
fun disconnect() {
lock.withLock {
database?.let { TransactionManager.closeAndUnregister(it) }
connected = false
}
}
fun isConnected(): Boolean {
return lock.withLock { connected }
database =
Database.connect("jdbc:sqlite:${ApplicationProperties.VRIPPER_DIR}/vripper.db")
.also { update(it.connector.invoke().connection as Connection) }
}
private fun update(connection: Connection) {
@@ -39,7 +39,7 @@ class VripperGuiApplication : App(
override fun start(stage: Stage) {
Thread.setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler { t, e ->
log.error("Thread $t threw an exception", e)
log.error("Thread $t threw an exception: ${e.message}", e)
})
if (widgetsController.currentSettings.darkMode) {
setUserAgentStylesheet(CupertinoDark().userAgentStylesheet)
@@ -17,7 +17,7 @@ class ConnectionSettingsFragment : Fragment("Connection Settings") {
init {
coroutineScope.launch {
async { connectionSettings = settingsController.findConnectionSettings() }.await()
connectionSettings = settingsController.findConnectionSettings()
runLater {
with(root) {
form {
@@ -21,16 +21,15 @@ class DownloadSettingsFragment : Fragment("Download Settings") {
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()
downloadSettings = settingsController.findDownloadSettings() ?: DownloadSettings()
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
runLater {
with(root) {
form {
@@ -7,18 +7,17 @@ import javafx.scene.control.RadioButton
import javafx.scene.control.Spinner
import javafx.scene.control.ToggleGroup
import javafx.scene.layout.VBox
import kotlinx.coroutines.*
import kotlinx.coroutines.runBlocking
import me.vripper.gui.VripperGuiApplication
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.services.GrpcEndpointService
import me.vripper.gui.utils.ActiveUICoroutines
import me.vripper.gui.utils.AppEndpointManager
import me.vripper.listeners.AppManager
import tornadofx.*
class SessionFragment : Fragment("Change Session") {
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()
@@ -67,57 +66,42 @@ class SessionFragment : Fragment("Change Session") {
isDefaultButton = true
action {
val selectedToggle = toggleGroup.selectedToggle
if (selectedToggle == null) {
VripperGuiApplication.APP_INSTANCE.stop()
runBlocking {
GuiEventBus.publishEvent(GuiEventBus.ChangingSession)
AppManager.stop()
grpcEndpointService.disconnect()
}
when ((selectedToggle as RadioButton).id) {
"localSession" -> {
coroutineScope.launch {
AppManager.stop()
runBlocking {
widgetsController.currentSettings.localSession = true
GuiEventBus.publishEvent(GuiEventBus.ChangingSession)
while (ActiveUICoroutines.all().isNotEmpty()) {
delay(200)
}
grpcEndpointService.disconnect()
AppEndpointManager.set(GuiEventBus.LocalSession)
AppManager.start()
GuiEventBus.publishEvent(GuiEventBus.LocalSession)
runLater {
close()
}
}
}
"remoteSession" -> {
coroutineScope.launch {
AppManager.stop()
runBlocking {
widgetsController.currentSettings.localSession = false
GuiEventBus.publishEvent(GuiEventBus.ChangingSession)
while (ActiveUICoroutines.all().isNotEmpty()) {
delay(200)
}
AppEndpointManager.set(GuiEventBus.RemoteSession)
grpcEndpointService.connect(
widgetsController.currentSettings.remoteSessionModel.host,
widgetsController.currentSettings.remoteSessionModel.port,
widgetsController.currentSettings.remoteSessionModel.passcode,
)
GuiEventBus.publishEvent(GuiEventBus.RemoteSession)
runLater {
close()
}
}
}
else -> VripperGuiApplication.APP_INSTANCE.stop()
}
runLater {
close()
}
}
}
}
}
}
}
override fun onUndock() {
coroutineScope.cancel()
}
}
@@ -21,10 +21,8 @@ class SystemSettingsFragment : Fragment("System Settings") {
init {
coroutineScope.launch {
async {
systemSettings = settingsController.findSystemSettings()
systemSettingsModel.tempPath = systemSettings.tempPath
}.await()
systemSettings = settingsController.findSystemSettings()
systemSettingsModel.tempPath = systemSettings.tempPath
runLater {
with(root) {
form {
@@ -20,16 +20,14 @@ class ViperSettingsFragment : Fragment("Viper Settings") {
init {
coroutineScope.launch {
async {
viperGirlsSettings = settingsController.findViperGirlsSettings()
viperSettingsModel.username = viperGirlsSettings.username
viperSettingsModel.password = viperGirlsSettings.password
viperSettingsModel.thanks = viperGirlsSettings.thanks
viperSettingsModel.host = viperGirlsSettings.host
viperSettingsModel.requestLimit = viperGirlsSettings.requestLimit
viperSettingsModel.fetchMetadata = viperGirlsSettings.fetchMetadata
proxies.addAll(settingsController.getProxies())
}.await()
viperGirlsSettings = settingsController.findViperGirlsSettings()
viperSettingsModel.username = viperGirlsSettings.username
viperSettingsModel.password = viperGirlsSettings.password
viperSettingsModel.thanks = viperGirlsSettings.thanks
viperSettingsModel.host = viperGirlsSettings.host
viperSettingsModel.requestLimit = viperGirlsSettings.requestLimit
viperSettingsModel.fetchMetadata = viperGirlsSettings.fetchMetadata
proxies.addAll(settingsController.getProxies())
runLater {
with(root) {
form {
@@ -1,6 +1,5 @@
package me.vripper.gui.components.views
import io.grpc.StatusException
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleIntegerProperty
import javafx.geometry.Orientation
@@ -8,14 +7,11 @@ import javafx.scene.control.ButtonType
import javafx.scene.control.ContentDisplay
import javafx.scene.layout.Priority
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import me.vripper.gui.components.Shared
import me.vripper.gui.components.fragments.AddLinksFragment
import me.vripper.gui.components.fragments.SettingsFragment
import me.vripper.gui.controller.ActionBarController
import me.vripper.gui.controller.PostController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.utils.ActiveUICoroutines
import me.vripper.services.IAppEndpointService
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
@@ -24,31 +20,13 @@ class ActionBarView : View() {
private val downloadActiveProperty = SimpleBooleanProperty(true)
private val postController: PostController by inject()
private val postsTableView: PostsTableView by inject()
private val actionBarController: ActionBarController by inject()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val grpcEndpointService: IAppEndpointService by di("remoteAppEndpointService")
private val localEndpointService: IAppEndpointService by di("localAppEndpointService")
private val running = SimpleIntegerProperty(0)
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 -> {
ActiveUICoroutines.cancelActionBar()
}
}
}
}
with(root) {
id = "action_toolbar"
padding = insets(all = 5)
@@ -125,25 +103,13 @@ class ActionBarView : View() {
}
}
downloadActiveProperty.bind(running.greaterThan(0))
}
private fun connect(appEndpointService: IAppEndpointService) {
coroutineScope.launch {
appEndpointService.onQueueStateUpdate().catch {
ActiveUICoroutines.removeFromActionBar(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connect(appEndpointService)
}
}
}.collect {
actionBarController.onQueueStateUpdate.collect {
runLater {
running.set(it.running)
}
}
}.also { runBlocking { ActiveUICoroutines.addToActionBar(it) } }
}
}
}
@@ -11,7 +11,6 @@ import javafx.scene.control.cell.TextFieldTableCell
import javafx.scene.input.MouseButton
import javafx.util.Callback
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.javafx.asFlow
import me.vripper.entities.Status
@@ -20,12 +19,9 @@ 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.event.GuiEventBus
import me.vripper.gui.model.ImageModel
import me.vripper.gui.utils.ActiveUICoroutines
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.*
@@ -36,39 +32,14 @@ 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() + Dispatchers.IO)
private val items: ObservableList<ImageModel> = FXCollections.observableArrayList()
private var preview: Preview? = null
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.collect { event ->
when (event) {
is GuiEventBus.LocalSession -> {
imageController.appEndpointService = localAppEndpointService
}
is GuiEventBus.RemoteSession -> {
imageController.appEndpointService = remoteAppEndpointService
}
is GuiEventBus.ChangingSession -> {
ActiveUICoroutines.cancelImages()
}
}
}
}
with(root) {
tableView = tableview(items) {
isTableMenuButtonVisible = true
@@ -271,7 +242,10 @@ class ImagesTableView : View("Photos") {
}
fun setPostId(postId: Long?) {
runBlocking { ActiveUICoroutines.cancelImages() }
runBlocking {
jobs.forEach { it.cancelAndJoin() }
jobs.clear()
}
runLater {
items.clear()
}
@@ -279,19 +253,16 @@ class ImagesTableView : View("Photos") {
return
}
coroutineScope.launch {
val list = coroutineScope.async {
imageController.findImages(postId)
}.await()
val list = imageController.findImages(postId)
runLater {
items.addAll(list)
tableView.sort()
tableView.placeholder = Label("No content in table")
}
}
coroutineScope.launch {
imageController.onUpdateImages(postId).catch {
ActiveUICoroutines.removeFromImages(currentCoroutineContext().job)
}.collect { image ->
imageController.onUpdateImages(postId).collect { image ->
runLater {
val imageModel = items.find { it.id == image.id } ?: return@runLater
@@ -304,12 +275,10 @@ class ImagesTableView : View("Photos") {
)
}
}
}.also { runBlocking { ActiveUICoroutines.addToImages(it) } }
}.also { jobs.add(it) }
coroutineScope.launch {
imageController.onStopped().catch {
ActiveUICoroutines.removeFromImages(currentCoroutineContext().job)
}.collect {
imageController.onStopped().collect {
runLater {
items.forEach { imageModel ->
if (imageModel.status != Status.FINISHED.name) {
@@ -318,6 +287,6 @@ class ImagesTableView : View("Photos") {
}
}
}
}.also { runBlocking { ActiveUICoroutines.addToImages(it) } }
}.also { jobs.add(it) }
}
}
@@ -7,8 +7,11 @@ import kotlinx.coroutines.flow.filterIsInstance
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.services.GrpcEndpointService
import me.vripper.gui.utils.AppEndpointManager
import me.vripper.gui.utils.ClipboardManager
import me.vripper.gui.utils.Watcher
import me.vripper.listeners.AppManager
import me.vripper.utilities.DatabaseManager
import tornadofx.*
class LoadingView : View("VRipper") {
@@ -20,11 +23,15 @@ class LoadingView : View("VRipper") {
override val root = borderpane {}
init {
DatabaseManager.connect()
ClipboardManager.init()
coroutineScope.launch {
GuiEventBus.events.filterIsInstance(GuiEventBus.ApplicationInitialized::class).collect {
if (widgetsController.currentSettings.localSession) {
AppEndpointManager.set(GuiEventBus.LocalSession)
AppManager.start()
} else {
AppEndpointManager.set(GuiEventBus.RemoteSession)
grpcEndpointService.connect(
widgetsController.currentSettings.remoteSessionModel.host,
widgetsController.currentSettings.remoteSessionModel.port,
@@ -2,12 +2,10 @@ package me.vripper.gui.components.views
import atlantafx.base.theme.Styles
import atlantafx.base.theme.Tweaks
import io.grpc.StatusException
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.scene.control.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.javafx.asFlow
import me.vripper.gui.components.fragments.LogMessageFragment
@@ -15,8 +13,6 @@ 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.gui.utils.ActiveUICoroutines
import me.vripper.services.IAppEndpointService
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
@@ -25,12 +21,10 @@ class LogTableView : View() {
private val logController: LogController by inject()
private val widgetsController: WidgetsController by inject()
private val localAppEndpointService: IAppEndpointService by di("localAppEndpointService")
private val remoteAppEndpointService: IAppEndpointService by di("remoteAppEndpointService")
private val tableView: TableView<LogModel>
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val items: ObservableList<LogModel> = FXCollections.observableArrayList()
private var maxLogEvent = 0
private var maxLogEvent = 100
override val root = vbox {}
@@ -158,98 +152,59 @@ class LogTableView : View() {
tableView.prefHeightProperty().bind(root.heightProperty())
tableView.placeholder = Label("Loading")
tableView.sortOrder.add(tableView.columns.first { it.id == "time" })
}
override fun onDock() {
coroutineScope.launch {
GuiEventBus.events.collect { event ->
when (event) {
is GuiEventBus.LocalSession -> {
runLater {
tableView.placeholder = Label("No Content in table")
launch {
GuiEventBus.events.collect {
when (it) {
GuiEventBus.LocalSession, GuiEventBus.RemoteSession -> {
while (isActive) {
val result = runCatching { logController.getMaxEventLog() }
if (result.isSuccess) {
maxLogEvent = result.getOrNull()!!
break
}
}
while (isActive) {
val result = runCatching { logController.initLogger() }
if (result.isSuccess) {
break
}
}
}
logController.appEndpointService = localAppEndpointService
connect()
}
is GuiEventBus.RemoteSession -> {
runLater {
tableView.placeholder = Label("No Content in table")
}
logController.appEndpointService = remoteAppEndpointService
connect()
}
is GuiEventBus.ChangingSession -> {
ActiveUICoroutines.cancelLog()
runLater {
tableView.placeholder = Label("Loading")
GuiEventBus.ChangingSession -> runLater {
items.clear()
tableView.placeholder = Label("Loading")
}
else -> {}
}
}
}
launch {
logController.newLogs.collect {
runLater {
items.sortWith(Comparator.comparing { it.sequence })
while (items.isNotEmpty() && (items.size >= maxLogEvent)) {
items.removeFirst()
}
items.add(it)
tableView.sort()
}
}
}
launch {
logController.updateSettings.collect {
maxLogEvent = it.systemSettings.maxEventLog
}
}
}
}
private fun connect() {
runBlocking {
maxLogEvent = logController.appEndpointService.getSettings().systemSettings.maxEventLog
}
connectToNewLogs()
connectToSettingsUpdate()
runLater {
runBlocking {
logController.initLogger()
}
}
}
private fun connectToSettingsUpdate() {
coroutineScope.launch {
logController.onUpdateSettings().catch {
ActiveUICoroutines.removeFromLog(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToSettingsUpdate()
}
}
}.collect {
maxLogEvent = it.systemSettings.maxEventLog
}
}.also { runBlocking { ActiveUICoroutines.addToLog(it) } }
}
private fun connectToNewLogs() {
coroutineScope.launch {
logController.onNewLog().catch {
ActiveUICoroutines.removeFromLog(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToNewLogs()
}
}
}.collect {
runLater {
items.sortWith(Comparator.comparing { it.sequence })
while (items.isNotEmpty() && (items.size >= maxLogEvent)) {
items.removeFirst()
}
items.add(it)
tableView.sort()
}
}
}.also { runBlocking { ActiveUICoroutines.addToLog(it) } }
}
private fun openLog(item: LogModel) {
find<LogMessageFragment>(mapOf(LogMessageFragment::logModel to item)).openModal()?.apply {
minWidth = 100.0
@@ -1,6 +1,5 @@
package me.vripper.gui.components.views
import io.grpc.StatusException
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleIntegerProperty
import javafx.scene.control.ButtonType
@@ -8,16 +7,14 @@ 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.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.ActionBarController
import me.vripper.gui.controller.PostController
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.utils.ActiveUICoroutines
import me.vripper.gui.utils.openLink
import me.vripper.services.IAppEndpointService
import me.vripper.utilities.ApplicationProperties
@@ -31,33 +28,13 @@ class MenuBarView : View() {
private val postsTableView: PostsTableView by inject()
private val widgetsController: WidgetsController by inject()
private val postController: PostController by inject()
private val grpcEndpointService: IAppEndpointService by di("remoteAppEndpointService")
private val localEndpointService: IAppEndpointService by di("localAppEndpointService")
private val actionBarController: ActionBarController by inject()
private lateinit var appEndpointService: IAppEndpointService
private val running = SimpleIntegerProperty(0)
override val root = menubar {}
init {
coroutineScope.launch {
GuiEventBus.events.collect { event ->
when (event) {
is GuiEventBus.LocalSession -> {
appEndpointService = localEndpointService
connect(localEndpointService)
}
is GuiEventBus.RemoteSession -> {
appEndpointService = grpcEndpointService
connect(grpcEndpointService)
}
is GuiEventBus.ChangingSession -> {
ActiveUICoroutines.cancelMenuBar()
}
}
}
}
with(root) {
menu("File") {
item("Add links", KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN)).apply {
@@ -165,7 +142,8 @@ class MenuBarView : View() {
title = "Database Migration"
) {
coroutineScope.launch {
val message = appEndpointService.dbMigration()
val message =
runCatching { appEndpointService.dbMigration() }.getOrDefault("Migration failed")
runLater {
information(
header = "",
@@ -221,25 +199,13 @@ class MenuBarView : View() {
}
}
downloadActiveProperty.bind(running.greaterThan(0))
}
private fun connect(appEndpointService: IAppEndpointService) {
coroutineScope.launch {
appEndpointService.onQueueStateUpdate().catch {
ActiveUICoroutines.removeFromMenuBar(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connect(appEndpointService)
}
}
}.collect {
actionBarController.onQueueStateUpdate.collect {
runLater {
running.set(it.running)
}
}
}.also { runBlocking { ActiveUICoroutines.addToMenuBar(it) } }
}
}
}
@@ -2,14 +2,13 @@ package me.vripper.gui.components.views
import javafx.collections.FXCollections
import javafx.scene.control.TabPane
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch
import me.vripper.gui.controller.PostController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.model.PostModel
import me.vripper.gui.utils.ActiveUICoroutines
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
@@ -25,11 +24,6 @@ class PostInfoView : View() {
override val root = tabpane()
init {
coroutineScope.launch {
GuiEventBus.events.filterIsInstance(GuiEventBus.ChangingSession::class).collect {
ActiveUICoroutines.cancelPostInfo()
}
}
with(root) {
id = "postinfo_panel"
tabClosingPolicy = TabPane.TabClosingPolicy.UNAVAILABLE
@@ -84,7 +78,6 @@ class PostInfoView : View() {
}
fun setPostId(postId: Long?) {
runBlocking { ActiveUICoroutines.cancelPostInfo() }
imagesTableView.setPostId(postId)
if (postId == null) {
postModel.apply {
@@ -108,13 +101,7 @@ class PostInfoView : View() {
return
}
coroutineScope.launch {
val model: PostModel? = async {
try {
postController.find(postId)
} catch (e: Exception) {
null
}
}.await()
val model: PostModel? = postController.find(postId)
if (model == null) {
return@launch
}
@@ -140,9 +127,7 @@ class PostInfoView : View() {
}
}
coroutineScope.launch {
postController.onUpdatePosts().catch {
ActiveUICoroutines.removeFromPostInfo(currentCoroutineContext().job)
}.filter {
postController.updatePostsFlow.filter {
it.postId == postModel.postId
}.collect { post ->
runLater {
@@ -159,12 +144,10 @@ class PostInfoView : View() {
postModel.folderName = post.folderName
}
}
}.also { runBlocking { ActiveUICoroutines.addToPostInfo(it) } }
}
coroutineScope.launch {
postController.onUpdateMetadata().catch {
ActiveUICoroutines.removeFromPostInfo(currentCoroutineContext().job)
}.filter {
postController.updateMetadataFlow.filter {
it.postId == postModel.postId
}.collect {
runLater {
@@ -172,6 +155,6 @@ class PostInfoView : View() {
postModel.postedBy = it.data.postedBy
}
}
}.also { runBlocking { ActiveUICoroutines.addToPostInfo(it) } }
}
}
}
@@ -2,7 +2,6 @@ package me.vripper.gui.components.views
import atlantafx.base.theme.Styles
import atlantafx.base.theme.Tweaks
import io.grpc.StatusException
import javafx.collections.FXCollections
import javafx.event.EventHandler
import javafx.geometry.Pos
@@ -12,10 +11,13 @@ import javafx.scene.input.KeyCode
import javafx.scene.input.KeyEvent
import javafx.scene.input.MouseButton
import javafx.util.Callback
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.javafx.asFlow
import kotlinx.coroutines.launch
import me.vripper.gui.components.Shared
import me.vripper.gui.components.cells.PreviewTableCell
import me.vripper.gui.components.cells.ProgressTableCell
@@ -26,13 +28,9 @@ 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.services.GrpcEndpointService
import me.vripper.gui.utils.ActiveUICoroutines
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.*
@@ -42,10 +40,7 @@ class PostsTableView : View() {
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val postController: PostController by inject()
private val widgetsController: WidgetsController by inject()
private val clipboardService: ClipboardService by inject()
private val mainView: MainView by inject()
private val localAppEndpointService: IAppEndpointService by di("localAppEndpointService")
private val remoteAppEndpointService: GrpcEndpointService by di("remoteAppEndpointService")
val tableView: TableView<PostModel>
var items: SortedFilteredList<PostModel> = SortedFilteredList()
@@ -62,28 +57,6 @@ class PostsTableView : View() {
|| item.status.contains(query, ignoreCase = true)
|| item.path.contains(query, ignoreCase = true)
}
coroutineScope.launch {
GuiEventBus.events.collect { event ->
when (event) {
is GuiEventBus.LocalSession -> {
postController.appEndpointService = localAppEndpointService
connect()
}
is GuiEventBus.RemoteSession -> {
postController.appEndpointService = remoteAppEndpointService
connect()
}
is GuiEventBus.ChangingSession -> {
ActiveUICoroutines.cancelPosts()
runLater {
tableView.placeholder = Label("Loading")
}
}
}
}
}
with(root) {
tableView = tableview(items) {
@@ -391,126 +364,83 @@ class PostsTableView : View() {
}
tableView.prefHeightProperty().bind(root.heightProperty())
tableView.placeholder = Label("Loading")
coroutineScope.launch {
launch {
GuiEventBus.events.collect {
when (it) {
GuiEventBus.LocalSession, GuiEventBus.RemoteSession -> {
val postModelList = postController.findAllPosts().toList()
runLater {
items.addAll(postModelList)
tableView.sort()
tableView.placeholder = Label("No content in table")
}
}
GuiEventBus.ChangingSession -> runLater {
tableView.placeholder = Label("Loading")
items.clear()
}
else -> {}
}
}
}
launch {
postController.updateMetadataFlow.collect {
runLater {
val postModel = items.find { it.postId == it.postId } ?: return@runLater
postModel.altTitles = FXCollections.observableArrayList(it.data.resolvedNames)
postModel.postedBy = it.data.postedBy
}
}
}
launch {
postController.deletedPostsFlow.collect {
runLater {
items.items.removeIf { p -> p.postId == it }
tableView.sort()
}
}
}
launch {
postController.updatePostsFlow.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
}
}
}
launch {
postController.newPostsFlow.collect {
runLater {
items.addAll(it)
tableView.sort()
}
}
}
}
}
private fun isCurrentTab(): Boolean = mainView.root.selectionModel.selectedItem.id == "download-tab"
private fun connect() {
coroutineScope.launch {
connectToOnNewPosts()
connectToOnPostUpdated()
connectToOnDeletedPost()
connectToOnMetadataUpdated()
}
}
private fun connectToOnMetadataUpdated() {
coroutineScope.launch {
postController.onUpdateMetadata().catch {
ActiveUICoroutines.removeFromPosts(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToOnMetadataUpdated()
}
}
}.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 { runBlocking { ActiveUICoroutines.addToPosts(it) } }
}
private fun connectToOnDeletedPost() {
coroutineScope.launch {
postController.onDeletePosts().catch {
ActiveUICoroutines.removeFromPosts(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToOnDeletedPost()
}
}
}.collect {
runLater {
items.items.removeIf { p -> p.postId == it }
tableView.sort()
}
}
}.also { runBlocking { ActiveUICoroutines.addToPosts(it) } }
}
private fun connectToOnPostUpdated() {
coroutineScope.launch {
postController.onUpdatePosts().catch {
ActiveUICoroutines.removeFromPosts(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToOnPostUpdated()
}
}
}.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 { runBlocking { ActiveUICoroutines.addToPosts(it) } }
}
private fun connectToOnNewPosts() {
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().catch {
ActiveUICoroutines.removeFromPosts(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToOnNewPosts()
}
}
}.collect {
runLater {
items.addAll(it)
tableView.sort()
}
}
}.also { runBlocking { ActiveUICoroutines.addToPosts(it) } }
}
private fun rename(post: PostModel) {
find<RenameFragment>(
mapOf(
@@ -1,27 +1,21 @@
package me.vripper.gui.components.views
import io.grpc.ConnectivityState
import io.grpc.StatusException
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.gui.controller.StatusBarController
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.services.GrpcEndpointService
import me.vripper.gui.utils.ActiveUICoroutines
import me.vripper.services.IAppEndpointService
import me.vripper.utilities.formatSI
import tornadofx.*
class StatusBarView : View("Status bar") {
private val widgetsController: WidgetsController by inject()
private val statusBarController: StatusBarController by inject()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val grpcEndpointService: GrpcEndpointService by di("remoteAppEndpointService")
private val localEndpointService: IAppEndpointService by di("localAppEndpointService")
private val remoteText = SimpleStringProperty()
private val loggedUser = SimpleStringProperty()
private val tasksRunning = SimpleBooleanProperty(false)
@@ -32,171 +26,84 @@ class StatusBarView : View("Status bar") {
init {
coroutineScope.launch {
GuiEventBus.events.collect { event ->
runLater {
tasksRunning.set(false)
downloadSpeed.set(0L.formatSI())
running.set(0)
pending.set(0)
error.set(0)
}
when (event) {
is GuiEventBus.LocalSession -> {
connect(localEndpointService)
}
is GuiEventBus.RemoteSession -> {
connect(grpcEndpointService)
}
is GuiEventBus.ChangingSession -> {
ActiveUICoroutines.cancelStatusBar()
runLater {
remoteText.set("Connecting to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}")
GuiEventBus.events.collect {
when (it) {
GuiEventBus.LocalSession, GuiEventBus.RemoteSession -> {
while (isActive) {
val result = runCatching { statusBarController.loggedInUser() }
if (result.isSuccess) {
runLater {
loggedUser.set(result.getOrNull())
}
break
}
delay(1000)
}
}
else -> {}
}
}
}
}
private fun connect(endpointService: IAppEndpointService) {
coroutineScope.launch {
val user = async { endpointService.loggedInUser() }.await()
runLater {
loggedUser.set(user)
}
}
connectToVGUserUpdate(endpointService)
connectToTasksRunning(endpointService)
connectToDownloadSpeed(endpointService)
connectToQueueStateUpdate(endpointService)
connectToErrorCountUpdate(endpointService)
if (widgetsController.currentSettings.localSession) {
runLater {
remoteText.set("")
}
} else {
coroutineScope.launch {
while (isActive) {
val text = when (grpcEndpointService.connectionState()) {
ConnectivityState.CONNECTING -> "Connecting to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}"
ConnectivityState.READY -> "Connected to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port} ${grpcEndpointService.getVersion()}"
ConnectivityState.TRANSIENT_FAILURE -> "Failing to connect to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}"
ConnectivityState.IDLE -> "Idle connection to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}"
ConnectivityState.SHUTDOWN -> "Connection shutdown to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}"
}
runLater {
remoteText.set(text)
}
delay(1000)
}
}
}
}
private fun connectToErrorCountUpdate(endpointService: IAppEndpointService) {
coroutineScope.launch {
endpointService.onErrorCountUpdate().catch {
ActiveUICoroutines.removeFromStatusBar(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToErrorCountUpdate(endpointService)
}
}
}.collect {
statusBarController.vgUserUpdate.collect {
runLater {
error.set(it.count)
loggedUser.set(it)
}
}
}.also { runBlocking { ActiveUICoroutines.addToStatusBar(it) } }
}
}
private fun connectToQueueStateUpdate(endpointService: IAppEndpointService) {
coroutineScope.launch {
endpointService.onQueueStateUpdate().catch {
ActiveUICoroutines.removeFromStatusBar(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToQueueStateUpdate(endpointService)
}
statusBarController.tasksRunning.collect {
runLater {
tasksRunning.set(it)
}
}.collect {
}
}
coroutineScope.launch {
statusBarController.downloadSpeed.collect {
runLater {
downloadSpeed.set(it.speed.formatSI())
}
}
}
coroutineScope.launch {
statusBarController.queueStateUpdate.collect {
runLater {
running.set(it.running)
pending.set(it.remaining)
}
}
}.also { runBlocking { ActiveUICoroutines.addToStatusBar(it) } }
}
}
private fun connectToDownloadSpeed(endpointService: IAppEndpointService) {
coroutineScope.launch {
endpointService.onDownloadSpeed().catch {
ActiveUICoroutines.removeFromStatusBar(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToDownloadSpeed(endpointService)
}
}
}.collect {
statusBarController.errorCountUpdate.collect {
runLater {
downloadSpeed.set(it.speed.formatSI())
error.set(it.count)
}
}
}.also { runBlocking { ActiveUICoroutines.addToStatusBar(it) } }
}
}
private fun connectToTasksRunning(endpointService: IAppEndpointService) {
coroutineScope.launch {
endpointService.onTasksRunning().catch {
ActiveUICoroutines.removeFromStatusBar(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToTasksRunning(endpointService)
}
while (isActive) {
val text = when (statusBarController.connectionState()) {
"CONNECTING" -> "Connecting to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}"
"READY" -> "Connected to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port} ${statusBarController.getVersion()}"
"TRANSIENT_FAILURE" -> "Failing to connect to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}"
"IDLE" -> "Idle connection to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}"
"SHUTDOWN" -> "Connection shutdown to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}"
else -> ""
}
}.collect {
runLater {
tasksRunning.set(it)
remoteText.set(text)
}
delay(1000)
}
}.also { runBlocking { ActiveUICoroutines.addToStatusBar(it) } }
}
private fun connectToVGUserUpdate(endpointService: IAppEndpointService) {
coroutineScope.launch {
endpointService.onVGUserUpdate().catch {
ActiveUICoroutines.removeFromStatusBar(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToVGUserUpdate(endpointService)
}
}
}.collect {
runLater {
loggedUser.set(it)
}
}
}.also { runBlocking { ActiveUICoroutines.addToStatusBar(it) } }
}
}
override val root = borderpane {
@@ -2,24 +2,24 @@ package me.vripper.gui.components.views
import atlantafx.base.theme.Styles
import atlantafx.base.theme.Tweaks
import io.grpc.StatusException
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.scene.control.*
import javafx.scene.input.KeyCode
import javafx.scene.input.KeyEvent
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.javafx.asFlow
import kotlinx.coroutines.launch
import me.vripper.gui.components.fragments.ThreadSelectionTableFragment
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.ActiveUICoroutines
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.*
@@ -30,37 +30,12 @@ class ThreadTableView : View() {
private val threadController: ThreadController by inject()
private val widgetsController: WidgetsController by inject()
private val mainView: MainView by inject()
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()
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 -> {
ActiveUICoroutines.cancelThreads()
runLater {
tableView.placeholder = Label("Loading")
}
}
}
}
}
with(root) {
tableView = tableview(items) {
isTableMenuButtonVisible = true
@@ -180,105 +155,65 @@ class ThreadTableView : View() {
})
tableView.prefHeightProperty().bind(root.heightProperty())
tableView.placeholder = Label("Loading")
}
fun connect() {
connectToNewThread()
connectToUpdateThread()
connectToDeleteThread()
connectToClearThread()
}
private fun connectToClearThread() {
coroutineScope.launch {
threadController.onClearThreads().catch {
ActiveUICoroutines.removeFromThreads(currentCoroutineContext().job)
launch {
GuiEventBus.events.collect {
when (it) {
GuiEventBus.LocalSession, GuiEventBus.RemoteSession -> {
val list = threadController.findAll().toList()
runLater {
items.clear()
items.addAll(list)
tableView.placeholder = Label("No content in table")
}
}
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToClearThread()
GuiEventBus.ChangingSession -> runLater {
tableView.placeholder = Label("Loading")
items.clear()
}
else -> {}
}
}
}.collect {
runLater {
tableView.items.clear()
}
}
}.also { runBlocking { ActiveUICoroutines.addToThreads(it) } }
}
private fun connectToDeleteThread() {
coroutineScope.launch {
threadController.onDeleteThread().catch {
ActiveUICoroutines.removeFromThreads(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToDeleteThread()
launch {
threadController.newThread.collect {
runLater {
items.add(it)
}
}
}.collect { threadId ->
runLater {
tableView.items.removeIf { it.threadId == threadId }
}
}
}.also { runBlocking { ActiveUICoroutines.addToThreads(it) } }
}
private fun connectToUpdateThread() {
coroutineScope.launch {
threadController.onUpdateThread().catch {
ActiveUICoroutines.removeFromThreads(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToUpdateThread()
launch {
threadController.updateThread.collect { thread ->
runLater {
val threadModel = items.find { it.threadId == thread.threadId } ?: return@runLater
threadModel.total = thread.total
threadModel.title = thread.title
}
}
}.collect { thread ->
runLater {
val threadModel = items.find { it.threadId == thread.threadId } ?: return@runLater
threadModel.total = thread.total
threadModel.title = thread.title
}
launch {
threadController.deleteThread.collect { threadId ->
runLater {
tableView.items.removeIf { it.threadId == threadId }
}
}
}
}.also { runBlocking { ActiveUICoroutines.addToThreads(it) } }
}
private fun connectToNewThread() {
coroutineScope.launch {
val list = async {
threadController.findAll()
}.await()
runLater {
items.clear()
items.addAll(list)
tableView.placeholder = Label("No content in table")
launch {
threadController.clearThreads.collect {
runLater {
tableView.items.clear()
}
}
}
}
coroutineScope.launch {
threadController.onNewThread().catch {
ActiveUICoroutines.removeFromThreads(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToNewThread()
}
}
}.collect {
runLater {
items.add(it)
}
}
}.also { runBlocking { ActiveUICoroutines.addToThreads(it) } }
}
private fun isCurrentTab(): Boolean = mainView.root.selectionModel.selectedItem.id == "thread-tab"
@@ -0,0 +1,14 @@
package me.vripper.gui.controller
import me.vripper.gui.utils.AppEndpointManager.localAppEndpointService
import me.vripper.gui.utils.AppEndpointManager.remoteAppEndpointService
import me.vripper.gui.utils.ChannelFlowBuilder
import tornadofx.Controller
class ActionBarController : Controller() {
val onQueueStateUpdate = ChannelFlowBuilder.build(
localAppEndpointService::onQueueStateUpdate,
remoteAppEndpointService::onQueueStateUpdate
)
}
@@ -1,14 +1,18 @@
package me.vripper.gui.controller
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.retryWhen
import me.vripper.gui.model.ImageModel
import me.vripper.gui.utils.AppEndpointManager.currentAppEndpointService
import me.vripper.model.Image
import me.vripper.services.IAppEndpointService
import tornadofx.Controller
class ImageController : Controller() {
lateinit var appEndpointService: IAppEndpointService
suspend fun findImages(postId: Long): List<ImageModel> {
return appEndpointService.findImagesByPostId(postId).map(::mapper)
return runCatching { currentAppEndpointService().findImagesByPostId(postId).map(::mapper) }.getOrDefault(
emptyList()
)
}
private fun mapper(it: Image): ImageModel {
@@ -34,8 +38,7 @@ class ImageController : Controller() {
}
fun onUpdateImages(postId: Long) =
appEndpointService.onUpdateImagesByPostId(postId)
fun onStopped() = appEndpointService.onStopped()
currentAppEndpointService().onUpdateImagesByPostId(postId).retryWhen { _, _ -> delay(1000);true }
fun onStopped() = currentAppEndpointService().onStopped().retryWhen { _, _ -> delay(1000);true }
}
@@ -2,12 +2,25 @@ package me.vripper.gui.controller
import kotlinx.coroutines.flow.map
import me.vripper.gui.model.LogModel
import me.vripper.gui.utils.AppEndpointManager.currentAppEndpointService
import me.vripper.gui.utils.AppEndpointManager.localAppEndpointService
import me.vripper.gui.utils.AppEndpointManager.remoteAppEndpointService
import me.vripper.gui.utils.ChannelFlowBuilder
import me.vripper.model.LogEntry
import me.vripper.services.IAppEndpointService
import tornadofx.Controller
class LogController : Controller() {
lateinit var appEndpointService: IAppEndpointService
val newLogs = ChannelFlowBuilder.build(
{ localAppEndpointService.onNewLog().map(::mapper) },
{ remoteAppEndpointService.onNewLog().map(::mapper) }
)
val updateSettings =
ChannelFlowBuilder.build(
localAppEndpointService::onUpdateSettings,
remoteAppEndpointService::onUpdateSettings
)
private fun mapper(it: LogEntry): LogModel {
return LogModel(
@@ -21,13 +34,11 @@ class LogController : Controller() {
)
}
fun onNewLog() = appEndpointService.onNewLog().map(::mapper)
suspend fun initLogger() {
appEndpointService.initLogger()
currentAppEndpointService().initLogger()
}
fun onUpdateSettings() =
appEndpointService.onUpdateSettings()
suspend fun getMaxEventLog(): Int {
return currentAppEndpointService().getSettings().systemSettings.maxEventLog
}
}
@@ -1,9 +1,14 @@
package me.vripper.gui.controller
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import me.vripper.gui.model.PostModel
import me.vripper.gui.utils.AppEndpointManager.currentAppEndpointService
import me.vripper.gui.utils.AppEndpointManager.localAppEndpointService
import me.vripper.gui.utils.AppEndpointManager.remoteAppEndpointService
import me.vripper.gui.utils.ChannelFlowBuilder
import me.vripper.gui.utils.ChannelFlowBuilder.toFlow
import me.vripper.model.Post
import me.vripper.services.IAppEndpointService
import me.vripper.utilities.formatSI
import tornadofx.Controller
import java.time.format.DateTimeFormatter
@@ -12,42 +17,77 @@ class PostController : Controller() {
private val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss")
lateinit var appEndpointService: IAppEndpointService
val updatePostsFlow =
ChannelFlowBuilder.build(
localAppEndpointService::onUpdatePosts,
remoteAppEndpointService::onUpdatePosts
)
val newPostsFlow = ChannelFlowBuilder.build(
{
localAppEndpointService.onNewPosts().map { post ->
mapper(post)
}
}, {
remoteAppEndpointService.onNewPosts().map { post ->
mapper(post)
}
}
)
val deletedPostsFlow =
ChannelFlowBuilder.build(
localAppEndpointService::onDeletePosts,
remoteAppEndpointService::onDeletePosts
)
val updateMetadataFlow = ChannelFlowBuilder.build(
localAppEndpointService::onUpdateMetadata,
remoteAppEndpointService::onUpdateMetadata,
)
suspend fun scan(postLinks: String) {
appEndpointService.scanLinks(postLinks)
runCatching { currentAppEndpointService().scanLinks(postLinks) }
}
suspend fun start(postIdList: List<Long>) {
appEndpointService.restartAll(postIdList)
runCatching { currentAppEndpointService().restartAll(postIdList) }
}
suspend fun startAll() {
appEndpointService.restartAll()
runCatching { currentAppEndpointService().restartAll() }
}
suspend fun delete(postIdList: List<Long>) {
appEndpointService.remove(postIdList)
runCatching { currentAppEndpointService().remove(postIdList) }
}
suspend fun stop(postIdList: List<Long>) {
appEndpointService.stopAll(postIdList)
runCatching { currentAppEndpointService().stopAll(postIdList) }
}
suspend fun clearPosts(): List<Long> {
return appEndpointService.clearCompleted()
return runCatching { return currentAppEndpointService().clearCompleted() }.getOrDefault(emptyList())
}
suspend fun stopAll() {
appEndpointService.stopAll()
runCatching { currentAppEndpointService().stopAll() }
}
suspend fun find(postId: Long): PostModel {
return mapper(appEndpointService.findPost(postId))
suspend fun find(postId: Long): PostModel? {
return runCatching { mapper(currentAppEndpointService().findPost(postId)) }.getOrNull()
}
suspend fun findAllPosts(): List<PostModel> {
return appEndpointService.findAllPosts().map(::mapper)
fun findAllPosts(): Flow<PostModel> {
return toFlow { currentAppEndpointService().findAllPosts().map(::mapper) }
}
suspend fun rename(postId: Long, value: String) {
runCatching { currentAppEndpointService().rename(postId, value) }
}
suspend fun renameToFirst(postIds: List<Long>) {
runCatching { currentAppEndpointService().renameToFirst(postIds) }
}
private fun mapper(post: Post): PostModel {
@@ -79,26 +119,4 @@ class PostController : Controller() {
fun progress(total: Int, done: Int): Double {
return if (done == 0 && total == 0) 0.0 else (done.toDouble() / total)
}
suspend fun rename(postId: Long, value: String) {
appEndpointService.rename(postId, value)
}
suspend fun renameToFirst(postIds: List<Long>) {
appEndpointService.renameToFirst(postIds)
}
fun onNewPosts() =
appEndpointService.onNewPosts().map { post ->
mapper(post)
}
fun onUpdatePosts() =
appEndpointService.onUpdatePosts()
fun onDeletePosts() =
appEndpointService.onDeletePosts()
fun onUpdateMetadata() =
appEndpointService.onUpdateMetadata()
}
@@ -4,29 +4,36 @@ import me.vripper.gui.model.settings.ConnectionSettingsModel
import me.vripper.gui.model.settings.DownloadSettingsModel
import me.vripper.gui.model.settings.SystemSettingsModel
import me.vripper.gui.model.settings.ViperSettingsModel
import me.vripper.gui.utils.AppEndpointManager
import me.vripper.model.*
import me.vripper.services.IAppEndpointService
import tornadofx.Controller
class SettingsController : Controller() {
private val widgetsController: WidgetsController by inject()
private val appEndpointService: IAppEndpointService by di(if (widgetsController.currentSettings.localSession) "localAppEndpointService" else "remoteAppEndpointService")
suspend fun findDownloadSettings(): DownloadSettings {
return appEndpointService.getSettings().downloadSettings
return runCatching {
AppEndpointManager.currentAppEndpointService().getSettings().downloadSettings
}.getOrDefault(
DownloadSettings()
)
}
suspend fun findConnectionSettings(): ConnectionSettings {
return appEndpointService.getSettings().connectionSettings
return runCatching {
AppEndpointManager.currentAppEndpointService().getSettings().connectionSettings
}.getOrDefault(ConnectionSettings())
}
suspend fun findViperGirlsSettings(): ViperSettings {
return appEndpointService.getSettings().viperSettings
return runCatching { AppEndpointManager.currentAppEndpointService().getSettings().viperSettings }.getOrDefault(
ViperSettings()
)
}
suspend fun findSystemSettings(): SystemSettings {
return appEndpointService.getSettings().systemSettings
return runCatching { AppEndpointManager.currentAppEndpointService().getSettings().systemSettings }.getOrDefault(
SystemSettings()
)
}
suspend fun saveNewSettings(
@@ -35,45 +42,47 @@ class SettingsController : Controller() {
viperSettingsModel: ViperSettingsModel,
systemSettingsModel: SystemSettingsModel
) {
appEndpointService.saveSettings(
Settings(
downloadSettings = DownloadSettings(
downloadSettingsModel.downloadPath,
downloadSettingsModel.autoStart,
downloadSettingsModel.autoQueueThreshold,
downloadSettingsModel.forceOrder,
downloadSettingsModel.forumSubfolder,
downloadSettingsModel.threadSubLocation,
downloadSettingsModel.clearCompleted,
downloadSettingsModel.appendPostId
),
connectionSettings = ConnectionSettings(
connectionSettingsModel.maxThreads,
connectionSettingsModel.maxTotalThreads,
connectionSettingsModel.timeout,
connectionSettingsModel.maxAttempts,
),
viperSettings = ViperSettings(
viperSettingsModel.login,
viperSettingsModel.username,
viperSettingsModel.password,
viperSettingsModel.thanks,
viperSettingsModel.host,
viperSettingsModel.requestLimit,
viperSettingsModel.fetchMetadata,
),
systemSettings =
SystemSettings(
systemSettingsModel.tempPath,
systemSettingsModel.enable,
systemSettingsModel.pollingRate,
systemSettingsModel.logEntries
)
runCatching {
AppEndpointManager.currentAppEndpointService().saveSettings(
Settings(
downloadSettings = DownloadSettings(
downloadSettingsModel.downloadPath,
downloadSettingsModel.autoStart,
downloadSettingsModel.autoQueueThreshold,
downloadSettingsModel.forceOrder,
downloadSettingsModel.forumSubfolder,
downloadSettingsModel.threadSubLocation,
downloadSettingsModel.clearCompleted,
downloadSettingsModel.appendPostId
),
connectionSettings = ConnectionSettings(
connectionSettingsModel.maxThreads,
connectionSettingsModel.maxTotalThreads,
connectionSettingsModel.timeout,
connectionSettingsModel.maxAttempts,
),
viperSettings = ViperSettings(
viperSettingsModel.login,
viperSettingsModel.username,
viperSettingsModel.password,
viperSettingsModel.thanks,
viperSettingsModel.host,
viperSettingsModel.requestLimit,
viperSettingsModel.fetchMetadata,
),
systemSettings =
SystemSettings(
systemSettingsModel.tempPath,
systemSettingsModel.enable,
systemSettingsModel.pollingRate,
systemSettingsModel.logEntries
)
)
)
)
}
}
suspend fun getProxies(): List<String> {
return appEndpointService.getProxies()
return runCatching { AppEndpointManager.currentAppEndpointService().getProxies() }.getOrDefault(emptyList())
}
}
@@ -0,0 +1,46 @@
package me.vripper.gui.controller
import me.vripper.gui.utils.AppEndpointManager.currentAppEndpointService
import me.vripper.gui.utils.AppEndpointManager.localAppEndpointService
import me.vripper.gui.utils.AppEndpointManager.remoteAppEndpointService
import me.vripper.gui.utils.ChannelFlowBuilder
import tornadofx.Controller
class StatusBarController : Controller() {
suspend fun loggedInUser(): String {
return currentAppEndpointService().loggedInUser()
}
fun connectionState(): String {
return currentAppEndpointService().connectionState()
}
suspend fun getVersion(): String {
return currentAppEndpointService().getVersion()
}
val vgUserUpdate = ChannelFlowBuilder.build(
localAppEndpointService::onVGUserUpdate,
remoteAppEndpointService::onVGUserUpdate,
)
val tasksRunning = ChannelFlowBuilder.build(
localAppEndpointService::onTasksRunning,
remoteAppEndpointService::onTasksRunning,
)
val downloadSpeed = ChannelFlowBuilder.build(
localAppEndpointService::onDownloadSpeed,
remoteAppEndpointService::onDownloadSpeed,
)
val queueStateUpdate = ChannelFlowBuilder.build(
localAppEndpointService::onQueueStateUpdate,
remoteAppEndpointService::onQueueStateUpdate,
)
val errorCountUpdate = ChannelFlowBuilder.build(
localAppEndpointService::onErrorCountUpdate,
remoteAppEndpointService::onErrorCountUpdate
)
}
@@ -1,19 +1,47 @@
package me.vripper.gui.controller
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import me.vripper.entities.ThreadEntity
import me.vripper.gui.model.ThreadModel
import me.vripper.gui.model.ThreadSelectionModel
import me.vripper.gui.utils.AppEndpointManager.currentAppEndpointService
import me.vripper.gui.utils.AppEndpointManager.localAppEndpointService
import me.vripper.gui.utils.AppEndpointManager.remoteAppEndpointService
import me.vripper.gui.utils.ChannelFlowBuilder
import me.vripper.gui.utils.ChannelFlowBuilder.toFlow
import me.vripper.model.ThreadPostId
import me.vripper.services.IAppEndpointService
import org.koin.core.component.KoinComponent
import tornadofx.Controller
class ThreadController : KoinComponent, Controller() {
lateinit var appEndpointService: IAppEndpointService
suspend fun findAll(): List<ThreadModel> {
return appEndpointService.findAllThreads().map(::threadModelMapper)
val newThread = ChannelFlowBuilder.build(
{
localAppEndpointService.onNewThread().map(::threadModelMapper)
},
{
remoteAppEndpointService.onNewThread().map(::threadModelMapper)
},
)
val updateThread = ChannelFlowBuilder.build(
localAppEndpointService::onUpdateThread,
remoteAppEndpointService::onUpdateThread,
)
val deleteThread = ChannelFlowBuilder.build(
localAppEndpointService::onDeleteThread,
remoteAppEndpointService::onDeleteThread,
)
val clearThreads = ChannelFlowBuilder.build(
localAppEndpointService::onClearThreads,
remoteAppEndpointService::onClearThreads,
)
fun findAll(): Flow<ThreadModel> {
return toFlow { currentAppEndpointService().findAllThreads().map(::threadModelMapper) }
}
private fun threadModelMapper(it: ThreadEntity): ThreadModel {
@@ -26,15 +54,15 @@ class ThreadController : KoinComponent, Controller() {
}
suspend fun delete(threadIdList: List<Long>) {
appEndpointService.threadRemove(threadIdList)
currentAppEndpointService().threadRemove(threadIdList)
}
suspend fun clearAll() {
appEndpointService.threadClear()
currentAppEndpointService().threadClear()
}
suspend fun grab(threadId: Long): List<ThreadSelectionModel> =
appEndpointService.grab(threadId).map { postItem ->
currentAppEndpointService().grab(threadId).map { postItem ->
ThreadSelectionModel(
postItem.number,
postItem.title,
@@ -46,21 +74,12 @@ class ThreadController : KoinComponent, Controller() {
)
}
suspend fun download(selectedItems: List<ThreadSelectionModel>) {
appEndpointService.download(selectedItems.map {
currentAppEndpointService().download(selectedItems.map {
ThreadPostId(
it.threadId,
it.postId
)
})
}
fun onNewThread() = appEndpointService.onNewThread().map(::threadModelMapper)
fun onUpdateThread() = appEndpointService.onUpdateThread()
fun onDeleteThread() = appEndpointService.onDeleteThread()
fun onClearThreads() = appEndpointService.onClearThreads()
}
@@ -1,19 +1,22 @@
package me.vripper.gui.event
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
object GuiEventBus {
private val _events = MutableSharedFlow<Any>()
private val _events = MutableSharedFlow<GUIEvent>(0, Int.MAX_VALUE, BufferOverflow.DROP_OLDEST)
val events = _events.asSharedFlow()
suspend fun publishEvent(event: Any) {
suspend fun publishEvent(event: GUIEvent) {
_events.emit(event)
}
data class ApplicationInitialized(val args: List<String>)
object ChangingSession
object LocalSession
object RemoteSession
data class ApplicationInitialized(val args: List<String>) : GUIEvent
object ChangingSession : GUIEvent
object LocalSession : GUIEvent
object RemoteSession : GUIEvent
}
sealed interface GUIEvent
@@ -1,66 +0,0 @@
package me.vripper.gui.services
import io.grpc.StatusException
import javafx.scene.input.Clipboard
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.filterIsInstance
import me.vripper.gui.event.GuiEventBus
import me.vripper.model.Settings
import me.vripper.services.IAppEndpointService
import me.vripper.utilities.LoggerDelegate
import tornadofx.Controller
import tornadofx.runLater
class ClipboardService : Controller() {
private val logger by LoggerDelegate()
private var current: String? = null
private var coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var pollJob: Job? = null
private var subscribeJob: Job? = null
fun init(appEndpointService: IAppEndpointService) {
subscribeJob?.cancel()
subscribeJob = coroutineScope.launch {
appEndpointService.onUpdateSettings().catch {
if (it !is StatusException) {
logger.error("gRPC error", it)
}
}.collect {
run(it, appEndpointService)
}
}
coroutineScope.launch {
GuiEventBus.events.filterIsInstance(GuiEventBus.ChangingSession::class).collect {
subscribeJob?.cancel()
}
}
runBlocking {
run(appEndpointService.getSettings(), appEndpointService)
}
}
private fun run(settings: Settings, appEndpointService: IAppEndpointService) {
pollJob?.cancel()
if (settings.systemSettings.enableClipboardMonitoring) {
pollJob = coroutineScope.launch {
var value: String? = null
while (isActive) {
runLater {
val clipboard = Clipboard.getSystemClipboard()
if (clipboard.hasString()) {
value = clipboard.string
}
}
if (!value.isNullOrBlank() && value != current) {
current = value
appEndpointService.scanLinks(value)
}
delay(settings.systemSettings.clipboardPollingRate.toLong())
}
}
} else {
current = null
}
}
}
@@ -48,11 +48,9 @@ internal class GrpcEndpointService : IAppEndpointService {
override suspend fun findPost(postId: Long): Post =
mapper(endpointServiceCoroutineStub!!.findPost(Id.newBuilder().setId(postId).build()))
override suspend fun findAllPosts(): List<Post> =
endpointServiceCoroutineStub!!.findAllPosts(EmptyRequest.getDefaultInstance()).postsList.map(::mapper)
override suspend fun rename(postId: Long, newName: String) {
endpointServiceCoroutineStub!!.rename(
Rename.newBuilder().setPostId(postId).setName(newName).build()
@@ -375,5 +373,5 @@ internal class GrpcEndpointService : IAppEndpointService {
}
}
fun connectionState(): ConnectivityState = channel?.getState(false) ?: ConnectivityState.SHUTDOWN
override fun connectionState(): String = channel?.getState(true)?.name ?: ConnectivityState.SHUTDOWN.name
}
@@ -1,192 +0,0 @@
package me.vripper.gui.utils
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
object ActiveUICoroutines {
private val mutex = Mutex()
private val posts: MutableList<Job> = mutableListOf()
private val actionBar: MutableList<Job> = mutableListOf()
private val images: MutableList<Job> = mutableListOf()
private val logs: MutableList<Job> = mutableListOf()
private val menuBar: MutableList<Job> = mutableListOf()
private val postInfo: MutableList<Job> = mutableListOf()
private val statusBar: MutableList<Job> = mutableListOf()
private val threads: MutableList<Job> = mutableListOf()
suspend fun cancelPosts() {
mutex.withLock {
posts.forEach { it.cancelAndJoin() }
posts.clear()
}
}
suspend fun addToPosts(job: Job) {
mutex.withLock {
posts.add(job)
}
}
suspend fun removeFromPosts(job: Job) {
mutex.withLock {
job.cancel()
posts.remove(job)
}
}
suspend fun cancelActionBar() {
mutex.withLock {
actionBar.forEach { it.cancelAndJoin() }
actionBar.clear()
}
}
suspend fun addToActionBar(job: Job) {
mutex.withLock {
actionBar.add(job)
}
}
suspend fun removeFromActionBar(job: Job) {
mutex.withLock {
job.cancel()
actionBar.remove(job)
}
}
suspend fun cancelImages() {
mutex.withLock {
images.forEach { it.cancelAndJoin() }
images.clear()
}
}
suspend fun addToImages(job: Job) {
mutex.withLock {
images.add(job)
}
}
suspend fun removeFromImages(job: Job) {
mutex.withLock {
job.cancel()
images.remove(job)
}
}
suspend fun cancelLog() {
mutex.withLock {
logs.forEach { it.cancelAndJoin() }
logs.clear()
}
}
suspend fun addToLog(job: Job) {
mutex.withLock {
logs.add(job)
}
}
suspend fun removeFromLog(job: Job) {
mutex.withLock {
job.cancel()
logs.remove(job)
}
}
suspend fun cancelMenuBar() {
mutex.withLock {
menuBar.forEach { it.cancelAndJoin() }
menuBar.clear()
}
}
suspend fun addToMenuBar(job: Job) {
mutex.withLock {
menuBar.add(job)
}
}
suspend fun removeFromMenuBar(job: Job) {
mutex.withLock {
job.cancel()
menuBar.remove(job)
}
}
suspend fun cancelPostInfo() {
mutex.withLock {
postInfo.forEach { it.cancelAndJoin() }
postInfo.clear()
}
}
suspend fun addToPostInfo(job: Job) {
mutex.withLock {
postInfo.add(job)
}
}
suspend fun removeFromPostInfo(job: Job) {
mutex.withLock {
job.cancel()
postInfo.remove(job)
}
}
suspend fun cancelStatusBar() {
mutex.withLock {
statusBar.forEach { it.cancelAndJoin() }
statusBar.clear()
}
}
suspend fun addToStatusBar(job: Job) {
mutex.withLock {
statusBar.add(job)
}
}
suspend fun removeFromStatusBar(job: Job) {
mutex.withLock {
job.cancel()
statusBar.remove(job)
}
}
suspend fun cancelThreads() {
mutex.withLock {
threads.forEach { it.cancelAndJoin() }
threads.clear()
}
}
suspend fun addToThreads(job: Job) {
mutex.withLock {
threads.add(job)
}
}
suspend fun removeFromThreads(job: Job) {
mutex.withLock {
job.cancel()
threads.remove(job)
}
}
suspend fun all() = mutex.withLock {
listOf(
posts,
actionBar,
logs,
menuBar,
postInfo,
statusBar,
threads
).flatten()
}
}
@@ -0,0 +1,32 @@
package me.vripper.gui.utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import me.vripper.gui.event.GUIEvent
import me.vripper.gui.event.GuiEventBus
import me.vripper.services.IAppEndpointService
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.qualifier.named
object AppEndpointManager : KoinComponent {
val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
val localAppEndpointService: IAppEndpointService by inject(named("localAppEndpointService"))
val remoteAppEndpointService: IAppEndpointService by inject(named("remoteAppEndpointService"))
private lateinit var current: GUIEvent
fun set(event: GUIEvent) {
this.current = event
}
fun currentAppEndpointService(): IAppEndpointService {
return when (current) {
GuiEventBus.LocalSession -> localAppEndpointService
GuiEventBus.RemoteSession -> remoteAppEndpointService
else -> throw IllegalStateException("Unknown current state: $current")
}
}
}
@@ -0,0 +1,53 @@
package me.vripper.gui.utils
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.retryWhen
import me.vripper.gui.event.GuiEventBus
object ChannelFlowBuilder {
fun <T> build(localFlow: () -> Flow<T>, remoteFlow: () -> Flow<T>): Flow<T> {
return channelFlow {
var job: Job? = null
fun collect(localSession: Boolean) {
job = if (localSession) {
launch {
localFlow().cancellable().collect { if (isActive) send(it) }
}
} else {
launch {
remoteFlow().cancellable().retryWhen { _, _ ->
delay(1000)
true
}.collect { if (isActive) send(it) }
}
}
}
launch {
GuiEventBus
.events
.collect {
if (job != null && job.isActive) {
job.cancelAndJoin()
}
when (it) {
GuiEventBus.LocalSession -> collect(true)
GuiEventBus.RemoteSession -> collect(false)
else -> {}
}
}
}
}
}
fun <T> toFlow(source: suspend () -> List<T>): Flow<T> {
return channelFlow {
source().forEach { if (isActive) send(it) }
}.retryWhen { _, _ -> delay(1000); true }
}
}
@@ -0,0 +1,73 @@
package me.vripper.gui.utils
import javafx.scene.input.Clipboard
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.retryWhen
import me.vripper.gui.event.GuiEventBus
import me.vripper.model.Settings
import me.vripper.utilities.LoggerDelegate
import org.koin.core.component.KoinComponent
import tornadofx.runLater
object ClipboardManager : KoinComponent {
private val logger by LoggerDelegate()
private var current: String? = null
private var coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var pollJob: Job? = null
private var settingsUpdateJob: Job? = null
fun init() {
coroutineScope.launch {
GuiEventBus.events.collect {
when (it) {
GuiEventBus.LocalSession, GuiEventBus.RemoteSession -> {
logger.info("Clipboard manager initialized")
while (isActive) {
val result = runCatching { AppEndpointManager.currentAppEndpointService().getSettings() }
if (result.isSuccess) {
update(result.getOrNull()!!)
break
}
delay(1000)
}
settingsUpdateJob?.cancelAndJoin()
settingsUpdateJob = launch {
AppEndpointManager.currentAppEndpointService().onUpdateSettings()
.retryWhen { _, _ -> delay(1000); true }.collect {
update(it)
}
}
}
else -> {}
}
}
}
}
private fun update(settings: Settings) {
pollJob?.cancel()
if (settings.systemSettings.enableClipboardMonitoring) {
logger.info("Polling clipboard every ${settings.systemSettings.clipboardPollingRate}ms")
pollJob = coroutineScope.launch {
var value: String? = null
while (isActive) {
runLater {
val clipboard = Clipboard.getSystemClipboard()
if (clipboard.hasString()) {
value = clipboard.string
}
}
if (!value.isNullOrBlank() && value != current) {
current = value
runCatching { AppEndpointManager.currentAppEndpointService().scanLinks(value) }
}
delay(settings.systemSettings.clipboardPollingRate.toLong())
}
}
} else {
logger.info("Clipboard monitoring deactivated")
current = null
}
}
}
@@ -61,11 +61,11 @@ object Watcher : KoinComponent {
log.info("Handling thread ${it.context()}")
if (WidgetSettings.loadSettings().localSession) {
val link = localAppEndpointService.getSettings().viperSettings.host + "/threads/${it.context()}"
localAppEndpointService.scanLinks(link)
runCatching { localAppEndpointService.scanLinks(link) }
} else {
val link =
remoteAppEndpointService.getSettings().viperSettings.host + "/threads/${it.context()}"
remoteAppEndpointService.scanLinks(link)
runCatching { remoteAppEndpointService.scanLinks(link) }
}
path.resolve(it.context().toString()).deleteIfExists()
}
@@ -1,6 +1,7 @@
package me.vripper
import me.vripper.listeners.AppManager
import me.vripper.utilities.DatabaseManager
import org.koin.core.context.startKoin
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.builder.SpringApplicationBuilder
@@ -12,6 +13,7 @@ fun main(args: Array<String>) {
startKoin {
modules(coreModule)
}
DatabaseManager.connect()
AppManager.start()
SpringApplicationBuilder(VripperWebApplication::class.java).listeners(AppListener()).run(*args)
}