diff --git a/vripper-core/src/main/kotlin/me/vripper/model/DownloadRequest.kt b/vripper-core/src/main/kotlin/me/vripper/model/DownloadRequest.kt new file mode 100644 index 0000000..8ab0d8d --- /dev/null +++ b/vripper-core/src/main/kotlin/me/vripper/model/DownloadRequest.kt @@ -0,0 +1,3 @@ +package me.vripper.model + +data class DownloadRequest(val imageId: Long) diff --git a/vripper-core/src/main/kotlin/me/vripper/model/ImageChunk.kt b/vripper-core/src/main/kotlin/me/vripper/model/ImageChunk.kt new file mode 100644 index 0000000..63f0fbe --- /dev/null +++ b/vripper-core/src/main/kotlin/me/vripper/model/ImageChunk.kt @@ -0,0 +1,33 @@ +package me.vripper.model + +data class ImageChunk( + val missing: Boolean, + val imageId: Long, + val offset: Long, + val data: ByteArray, + val isLast: Boolean +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ImageChunk + + if (missing != other.missing) return false + if (imageId != other.imageId) return false + if (offset != other.offset) return false + if (isLast != other.isLast) return false + if (!data.contentEquals(other.data)) return false + + return true + } + + override fun hashCode(): Int { + var result = missing.hashCode() + result = 31 * result + imageId.hashCode() + result = 31 * result + offset.hashCode() + result = 31 * result + isLast.hashCode() + result = 31 * result + data.contentHashCode() + return result + } +} diff --git a/vripper-core/src/main/kotlin/me/vripper/services/AppEndpointService.kt b/vripper-core/src/main/kotlin/me/vripper/services/AppEndpointService.kt index 6eb8aa5..468f7f1 100644 --- a/vripper-core/src/main/kotlin/me/vripper/services/AppEndpointService.kt +++ b/vripper-core/src/main/kotlin/me/vripper/services/AppEndpointService.kt @@ -17,6 +17,7 @@ import me.vripper.tasks.ThreadLookupTask import me.vripper.utilities.* import me.vripper.utilities.ApplicationProperties.VRIPPER_DIR import org.h2.jdbc.JdbcSQLNonTransientConnectionException +import java.nio.file.Files import java.sql.DriverManager import java.time.Duration import java.util.concurrent.locks.ReentrantLock @@ -25,6 +26,7 @@ import kotlin.concurrent.withLock import kotlin.io.path.Path import kotlin.io.path.exists import kotlin.jvm.optionals.getOrNull +import kotlin.math.min @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) internal class AppEndpointService( @@ -173,11 +175,8 @@ internal class AppEndpointService( override suspend fun renameToFirst(postEntityIds: List) { postEntityIds.forEach { postEntityId -> - dataAccessService - .findMetadataByPostEntityId(postEntityId) - .map { it.data.resolvedNames } - .filter { it.isNotEmpty() } - .getOrNull()?.let { rename(postEntityId, it.first()) } + dataAccessService.findMetadataByPostEntityId(postEntityId).map { it.data.resolvedNames } + .filter { it.isNotEmpty() }.getOrNull()?.let { rename(postEntityId, it.first()) } } } @@ -329,11 +328,55 @@ internal class AppEndpointService( downloadService.move(postEntityId, position) } + override fun downloadImage(downloadRequest: DownloadRequest): Flow { + val imageEntity = dataAccessService.findImageById(downloadRequest.imageId).orElseThrow() + val postEntity = dataAccessService.findPostByEntityId(imageEntity.postEntityId) + val filePath = Path(postEntity.downloadDirectory).resolve(postEntity.folderName).resolve(imageEntity.filename) + return if (filePath.exists()) { + val chunkSize = 256 * 1024 + val bytes = Files.readAllBytes(filePath) + flow { + var offset = 0L + while (offset < bytes.size) { + val start = offset.toInt() + val len = min(chunkSize, bytes.size - start) + val part = bytes.copyOfRange(start, start + len) + + val isLast = (start + len) >= bytes.size + + emit( + ImageChunk( + missing = false, + imageId = downloadRequest.imageId, + offset = offset, + data = part, + isLast = isLast + ) + ) + + offset += len.toLong() + if (isLast) break + } + } + } else { + flow { + emit( + ImageChunk( + missing = true, + imageId = downloadRequest.imageId, + offset = 0, + data = ByteArray(0), + isLast = true + ) + ) + } + } + } + override suspend fun dbMigration(): String { val conn = try { - DriverManager - .getConnection("jdbc:h2:file:$VRIPPER_DIR/vripper;DB_CLOSE_DELAY=-1;IFEXISTS=TRUE") + DriverManager.getConnection("jdbc:h2:file:$VRIPPER_DIR/vripper;DB_CLOSE_DELAY=-1;IFEXISTS=TRUE") } catch (_: JdbcSQLNonTransientConnectionException) { return "Old database not found, nothing to do" } @@ -425,8 +468,7 @@ internal class AppEndpointService( if (set.next()) { val data = Json.decodeFromString(set.getString("DATA")) as MetadataEntity.Data val metadata = MetadataEntity( - postIdRef = savedPost.id, - data = data + postIdRef = savedPost.id, data = data ) dataAccessService.saveMetadata(metadata) } diff --git a/vripper-core/src/main/kotlin/me/vripper/services/IAppEndpointService.kt b/vripper-core/src/main/kotlin/me/vripper/services/IAppEndpointService.kt index ca6f232..27a4a7a 100644 --- a/vripper-core/src/main/kotlin/me/vripper/services/IAppEndpointService.kt +++ b/vripper-core/src/main/kotlin/me/vripper/services/IAppEndpointService.kt @@ -47,5 +47,6 @@ interface IAppEndpointService { suspend fun dbMigration(): String suspend fun initLogger() suspend fun move(postEntityId: Long, position: MovePosition) + fun downloadImage(downloadRequest: DownloadRequest): Flow fun connectionState(): String } \ No newline at end of file diff --git a/vripper-core/src/main/proto/endpoint.service.proto b/vripper-core/src/main/proto/endpoint.service.proto index 62f653f..e612720 100644 --- a/vripper-core/src/main/proto/endpoint.service.proto +++ b/vripper-core/src/main/proto/endpoint.service.proto @@ -94,6 +94,18 @@ message MovePositionMessage { MovePositionEnum position = 2; } +message DownloadRequest { + int64 imageId = 1; +} + +message ImageChunk { + bool missing = 1; + int64 imageId = 2; + int64 offset = 3; + bytes data = 4; + bool isLast = 5; +} + service EndpointService { rpc scanLinks (Links) returns (EmptyResponse) {} rpc onNewPosts (EmptyRequest) returns (stream Post) {} @@ -136,5 +148,6 @@ service EndpointService { rpc getVersion (EmptyRequest) returns (Version) {} rpc dbMigration (EmptyRequest) returns (DBMigrationResponse) {} rpc initLogger (EmptyRequest) returns (EmptyResponse) {} - rpc move (MovePositionMessage) returns (EmptyResponse) {} + rpc move(MovePositionMessage) returns (EmptyResponse) {} + rpc downloadImage(DownloadRequest) returns (stream ImageChunk); } diff --git a/vripper-gui/src/main/kotlin/me/vripper/gui/components/fragments/ImageSource.kt b/vripper-gui/src/main/kotlin/me/vripper/gui/components/fragments/ImageSource.kt new file mode 100644 index 0000000..6e546ca --- /dev/null +++ b/vripper-gui/src/main/kotlin/me/vripper/gui/components/fragments/ImageSource.kt @@ -0,0 +1,36 @@ +package me.vripper.gui.components.fragments + +import kotlinx.coroutines.flow.Flow +import me.vripper.model.ImageChunk +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream + +interface ImageSource { + suspend fun inputStream(): InputStream? + fun fileName(): String +} + +data class BytesImageSource( + val fileName: String, + val downloadFunction: () -> Flow +) : ImageSource { + + override suspend fun inputStream(): InputStream? { + val bos = ByteArrayOutputStream() + var missing = false + downloadFunction().collect { + if (it.missing) { + missing = true + } else { + bos.write(it.data) + } + } + val bytes = bos.toByteArray() + return if (missing) null else ByteArrayInputStream(bytes) + } + + override fun fileName(): String { + return fileName + } +} \ No newline at end of file diff --git a/vripper-gui/src/main/kotlin/me/vripper/gui/components/fragments/PhotoViewerFragment.kt b/vripper-gui/src/main/kotlin/me/vripper/gui/components/fragments/PhotoViewerFragment.kt new file mode 100644 index 0000000..53f0a42 --- /dev/null +++ b/vripper-gui/src/main/kotlin/me/vripper/gui/components/fragments/PhotoViewerFragment.kt @@ -0,0 +1,165 @@ +package me.vripper.gui.components.fragments + +import atlantafx.base.theme.Styles +import javafx.beans.property.SimpleIntegerProperty +import javafx.geometry.Pos +import javafx.scene.control.Button +import javafx.scene.control.Label +import javafx.scene.image.Image +import javafx.scene.image.ImageView +import javafx.scene.layout.BorderPane +import javafx.scene.layout.HBox +import kotlinx.coroutines.* +import org.kordamp.ikonli.feather.Feather +import org.kordamp.ikonli.javafx.FontIcon +import tornadofx.* +import java.io.ByteArrayInputStream +import java.util.* +import kotlin.math.min + +class PhotoViewerFragment : Fragment("Image Viewer") { + + val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + val sources: List by param() + val initialIndex: Int by param() + + private val indexProperty = SimpleIntegerProperty(initialIndex) + + private val imageView = ImageView().apply { + isPreserveRatio = true + isSmooth = true + isCache = true + } + + private val cache = object : LinkedHashMap(8, 0.75f, true) { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean = size > 3 + } + + private var bottomBar: HBox + + override val root = BorderPane().apply { + center = imageView + BorderPane.setAlignment(imageView, Pos.CENTER) + + val prevButton = Button().apply { + graphic = FontIcon.of(Feather.ARROW_LEFT) + addClass(Styles.ACCENT) + setOnAction { + if (sources.isEmpty()) return@setOnAction + coroutineScope.launch { + goTo((indexProperty.get() - 1 + sources.size) % sources.size) + } + } + } + + val nextButton = Button().apply { + graphic = FontIcon.of(Feather.ARROW_RIGHT) + addClass(Styles.ACCENT) + setOnAction { + if (sources.isEmpty()) return@setOnAction + coroutineScope.launch { + goTo((indexProperty.get() + 1) % sources.size) + } + } + } + + val label = Label().apply { + textProperty().bind(indexProperty.plus(1).asString().concat(" / ${sources.size}")) + } + + bottomBar = HBox(10.0, prevButton, label, nextButton).apply { + alignment = Pos.CENTER + paddingBottom = 5.0 + } + bottom = bottomBar + + fun scheduleResize() { + runLater { resizeNoUpscale() } + } + + // Re-evaluate whenever container size or bottom bar size changes + layoutBoundsProperty().addListener { _, _, _ -> scheduleResize() } + bottomBar.layoutBoundsProperty().addListener { _, _, _ -> scheduleResize() } + + // Re-evaluate whenever the image changes (so it works on prev/next) + imageView.imageProperty().addListener { _, _, _ -> scheduleResize() } + } + + init { + coroutineScope.launch { + if (sources.isEmpty()) { + return@launch + } + val image = loadImageFor(indexProperty.value) + runLater { + imageView.image = image + title = sources[indexProperty.value].fileName() + resizeNoUpscale() + } + } + + } + + private suspend fun goTo(newIndex: Int) { + if (sources.isEmpty()) return + + val image = loadImageFor(newIndex) + runLater { + imageView.image = image + indexProperty.set(newIndex) + title = sources[newIndex].fileName() + } + + // preload neighbors only + val prev = (newIndex - 1 + sources.size) % sources.size + val next = (newIndex + 1) % sources.size + preload(prev) + preload(next) + } + + private fun resizeNoUpscale() { + val img = imageView.image ?: return + val ih = img.height + if (ih <= 0.0) return + + val bottomH = bottomBar.layoutBounds.height + val availableH = (root.height - bottomH - 10.0).coerceAtLeast(0.0) + + val scale = min(1.0, availableH / ih) // never upscale + imageView.isPreserveRatio = true + imageView.fitHeight = ih * scale + imageView.fitWidth = 0.0 + } + + private suspend fun preload(i: Int) { + if (i == indexProperty.get()) return + if (cache.containsKey(i)) return + cache[i] = loadImageFor(i) + } + + private suspend fun loadImageFor(i: Int): Image { + cache[i]?.let { return it } + val source = sources[i] + val inputStream = source.inputStream() + val img = if (inputStream == null) { + missingPlaceholder + } else { + Image(inputStream, 0.0, 0.0, true, true) + } + cache[i] = img + return img + } + + private val missingPlaceholder: Image by lazy { + // 1x1 transparent PNG + val transparentPngBase64 = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/xcAAgMBgX7Y1z8AAAAASUVORK5CYII=" + val bytes = Base64.getDecoder().decode(transparentPngBase64) + Image(ByteArrayInputStream(bytes), 0.0, 0.0, true, true) + } + + override fun onUndock() { + coroutineScope.cancel() + } +} \ No newline at end of file diff --git a/vripper-gui/src/main/kotlin/me/vripper/gui/components/views/ImagesTableView.kt b/vripper-gui/src/main/kotlin/me/vripper/gui/components/views/ImagesTableView.kt index b259ec3..199ec97 100644 --- a/vripper-gui/src/main/kotlin/me/vripper/gui/components/views/ImagesTableView.kt +++ b/vripper-gui/src/main/kotlin/me/vripper/gui/components/views/ImagesTableView.kt @@ -6,6 +6,7 @@ import javafx.collections.FXCollections import javafx.collections.ObservableList import javafx.event.EventHandler import javafx.geometry.Pos +import javafx.scene.Cursor import javafx.scene.control.* import javafx.scene.control.cell.TextFieldTableCell import javafx.scene.input.MouseButton @@ -17,6 +18,7 @@ import me.vripper.entities.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.PhotoViewerFragment import me.vripper.gui.controller.ImageController import me.vripper.gui.controller.WidgetsController import me.vripper.gui.model.ImageModel @@ -73,6 +75,7 @@ class ImagesTableView : View("Photos") { cellFactory = Callback { val cell = PreviewTableCell() cell.onMouseExited = EventHandler { + cursor = Cursor.DEFAULT preview.cleanup() } cell.onMouseMoved = EventHandler { @@ -82,6 +85,7 @@ class ImagesTableView : View("Photos") { } } cell.onMouseEntered = EventHandler { mouseEvent -> + cursor = Cursor.HAND preview.cleanup() if (cell.tableRow.item != null && cell.tableRow.item.thumbUrl.isNotEmpty()) { preview.display(cell.tableRow.item.postEntityId, listOf(cell.tableRow.item.thumbUrl)) @@ -91,6 +95,34 @@ class ImagesTableView : View("Photos") { } } } + cell.onLeftClick { + preview.cleanup() + coroutineScope.launch { + val imageSources = imageController.getImageSources(cell.tableRow.item) + if (imageSources.none { it.key.id == cell.tableRow.item.id }) { + return@launch + } + runLater { + find( + mapOf( + PhotoViewerFragment::sources to imageSources.map { it.value }, + PhotoViewerFragment::initialIndex to cell.tableRow.item.index - 1, + ) + ).openModal()?.apply { + val w = 800.0 + val h = 600.0 + minWidth = 100.0 + minHeight = 100.0 + width = w + height = h + val x = owner.x + (owner.width - w) / 2 + val y = owner.y + (owner.height - h) / 2 + this.x = x + this.y = y + } + } + } + } cell } } diff --git a/vripper-gui/src/main/kotlin/me/vripper/gui/components/views/PostsTableView.kt b/vripper-gui/src/main/kotlin/me/vripper/gui/components/views/PostsTableView.kt index f414c97..026d4d5 100644 --- a/vripper-gui/src/main/kotlin/me/vripper/gui/components/views/PostsTableView.kt +++ b/vripper-gui/src/main/kotlin/me/vripper/gui/components/views/PostsTableView.kt @@ -6,6 +6,7 @@ import javafx.collections.FXCollections import javafx.collections.ObservableList import javafx.event.EventHandler import javafx.geometry.Pos +import javafx.scene.Cursor import javafx.scene.control.* import javafx.scene.control.cell.TextFieldTableCell import javafx.scene.input.KeyCode @@ -20,6 +21,7 @@ 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.AddLinksFragment +import me.vripper.gui.components.fragments.PhotoViewerFragment import me.vripper.gui.components.fragments.RenameFragment import me.vripper.gui.controller.PostController import me.vripper.gui.controller.WidgetsController @@ -253,6 +255,7 @@ class PostsTableView : View() { cellFactory = Callback { val cell = PreviewTableCell>() cell.onMouseExited = EventHandler { + cursor = Cursor.DEFAULT preview.cleanup() } cell.onMouseMoved = EventHandler { @@ -262,6 +265,7 @@ class PostsTableView : View() { } } cell.onMouseEntered = EventHandler { mouseEvent -> + cursor = Cursor.HAND preview.cleanup() if (cell.tableRow.item != null && cell.tableRow.item.previewList.isNotEmpty()) { preview.display( @@ -273,6 +277,34 @@ class PostsTableView : View() { } } } + cell.onLeftClick { + preview.cleanup() + coroutineScope.launch { + val imageSources = postController.getImageSources(cell.tableRow.item) + if (imageSources.isEmpty()) { + return@launch + } + runLater { + find( + mapOf( + PhotoViewerFragment::sources to imageSources, + PhotoViewerFragment::initialIndex to 0, + ) + ).openModal()?.apply { + val w = 800.0 + val h = 600.0 + minWidth = 100.0 + minHeight = 100.0 + width = w + height = h + val x = owner.x + (owner.width - w) / 2 + val y = owner.y + (owner.height - h) / 2 + this.x = x + this.y = y + } + } + } + } cell } } diff --git a/vripper-gui/src/main/kotlin/me/vripper/gui/controller/ImageController.kt b/vripper-gui/src/main/kotlin/me/vripper/gui/controller/ImageController.kt index 9b66f6e..8476823 100644 --- a/vripper-gui/src/main/kotlin/me/vripper/gui/controller/ImageController.kt +++ b/vripper-gui/src/main/kotlin/me/vripper/gui/controller/ImageController.kt @@ -1,7 +1,11 @@ package me.vripper.gui.controller +import me.vripper.entities.ImageEntity +import me.vripper.gui.components.fragments.BytesImageSource +import me.vripper.gui.components.fragments.ImageSource import me.vripper.gui.model.ImageModel import me.vripper.gui.utils.AppEndpointManager.currentAppEndpointService +import me.vripper.model.DownloadRequest import me.vripper.model.Image import tornadofx.Controller @@ -42,4 +46,13 @@ class ImageController : Controller() { currentAppEndpointService().onUpdateImagesByPostEntityId(postId) fun onStopped() = currentAppEndpointService().onStopped() + + suspend fun getImageSources(item: ImageModel): Map { + val imageEntities = currentAppEndpointService().findImagesByPostEntityId(item.postEntityId) + return imageEntities.associateWith { imageEntity -> + BytesImageSource(imageEntity.filename) { + currentAppEndpointService().downloadImage(DownloadRequest(imageEntity.id)) + } + } + } } \ No newline at end of file diff --git a/vripper-gui/src/main/kotlin/me/vripper/gui/controller/PostController.kt b/vripper-gui/src/main/kotlin/me/vripper/gui/controller/PostController.kt index 1d818ad..3dc1225 100644 --- a/vripper-gui/src/main/kotlin/me/vripper/gui/controller/PostController.kt +++ b/vripper-gui/src/main/kotlin/me/vripper/gui/controller/PostController.kt @@ -1,11 +1,14 @@ package me.vripper.gui.controller import kotlinx.coroutines.flow.map +import me.vripper.gui.components.fragments.BytesImageSource +import me.vripper.gui.components.fragments.ImageSource 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.model.DownloadRequest import me.vripper.model.Post import me.vripper.model.QueueState import me.vripper.services.download.MovePosition @@ -133,4 +136,13 @@ class PostController : Controller() { fun progress(total: Int, done: Int): Double { return if (done == 0 && total == 0) 0.0 else (done.toDouble() / total) } + + suspend fun getImageSources(item: PostModel): List { + val imageEntities = currentAppEndpointService().findImagesByPostEntityId(item.id) + return imageEntities.map { imageEntity -> + BytesImageSource(imageEntity.filename) { + currentAppEndpointService().downloadImage(DownloadRequest(imageEntity.id)) + } + } + } } \ No newline at end of file diff --git a/vripper-gui/src/main/kotlin/me/vripper/gui/services/GrpcEndpointService.kt b/vripper-gui/src/main/kotlin/me/vripper/gui/services/GrpcEndpointService.kt index 9baefe6..9b242c2 100644 --- a/vripper-gui/src/main/kotlin/me/vripper/gui/services/GrpcEndpointService.kt +++ b/vripper-gui/src/main/kotlin/me/vripper/gui/services/GrpcEndpointService.kt @@ -8,8 +8,10 @@ import kotlinx.coroutines.flow.map import me.vripper.entities.MetadataEntity import me.vripper.entities.Status import me.vripper.model.* +import me.vripper.model.DownloadRequest import me.vripper.model.DownloadSpeed import me.vripper.model.ErrorCount +import me.vripper.model.ImageChunk import me.vripper.model.PostSelection import me.vripper.model.QueueState import me.vripper.model.Rank @@ -252,6 +254,14 @@ internal class GrpcEndpointService : IAppEndpointService { override suspend fun dbMigration(): String = endpointServiceCoroutineStub!!.dbMigration(EmptyRequest.getDefaultInstance()).message + override fun downloadImage(downloadRequest: DownloadRequest): Flow = + endpointServiceCoroutineStub!!.downloadImage( + EndpointServiceOuterClass + .DownloadRequest + .newBuilder() + .setImageId(downloadRequest.imageId).build() + ).map { ImageChunk(it.missing, it.imageId, it.offset, it.data.toByteArray(), it.isLast) } + private fun mapper(queueState: EndpointServiceOuterClass.QueueState) = QueueState(queueState.running, queueState.remaining, queueState.rankList.map { Rank(it.postEntityId, it.rank) diff --git a/vripper-gui/src/main/kotlin/me/vripper/gui/utils/AppEndpointManager.kt b/vripper-gui/src/main/kotlin/me/vripper/gui/utils/AppEndpointManager.kt index 1890cde..1e19714 100644 --- a/vripper-gui/src/main/kotlin/me/vripper/gui/utils/AppEndpointManager.kt +++ b/vripper-gui/src/main/kotlin/me/vripper/gui/utils/AppEndpointManager.kt @@ -29,4 +29,8 @@ object AppEndpointManager : KoinComponent { else -> throw IllegalStateException("Unknown current state: $current") } } + + fun currentAppState(): GUIEvent { + return this.current + } } \ No newline at end of file diff --git a/vripper-web/src/main/kotlin/me/vripper/web/grpc/GrpcServerAppEndpointService.kt b/vripper-web/src/main/kotlin/me/vripper/web/grpc/GrpcServerAppEndpointService.kt index 348a971..642022c 100644 --- a/vripper-web/src/main/kotlin/me/vripper/web/grpc/GrpcServerAppEndpointService.kt +++ b/vripper-web/src/main/kotlin/me/vripper/web/grpc/GrpcServerAppEndpointService.kt @@ -1,5 +1,6 @@ package me.vripper.web.grpc +import com.google.protobuf.ByteString import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import me.vripper.entities.ImageEntity @@ -213,15 +214,13 @@ class GrpcServerAppEndpointService : EndpointServiceGrpcKt.EndpointServiceCorout enableClipboardMonitoring = request.systemSettings.enableClipboardMonitoring, clipboardPollingRate = request.systemSettings.clipboardPollingRate, maxEventLog = request.systemSettings.maxEventLog, - ), - hostSettings = request.hostSettingsMap.entries.associate { hostSettings -> + ), hostSettings = request.hostSettingsMap.entries.associate { hostSettings -> HostName.valueOf(hostSettings.key) to hostSettings.value.settingsMap.entries.associate { HostSettingKey.valueOf( it.key ) to it.value } - } - ) + }) ) return EndpointServiceOuterClass.EmptyResponse.getDefaultInstance() } @@ -239,6 +238,18 @@ class GrpcServerAppEndpointService : EndpointServiceGrpcKt.EndpointServiceCorout override suspend fun dbMigration(request: EndpointServiceOuterClass.EmptyRequest): EndpointServiceOuterClass.DBMigrationResponse = EndpointServiceOuterClass.DBMigrationResponse.newBuilder().setMessage(appEndpointService.dbMigration()).build() + override fun downloadImage(request: EndpointServiceOuterClass.DownloadRequest): Flow { + return appEndpointService.downloadImage(DownloadRequest(request.imageId)).map { + with(EndpointServiceOuterClass.ImageChunk.newBuilder()) { + imageId = it.imageId + offset = it.offset + data = ByteString.copyFrom(it.data) + isLast = it.isLast + build() + } + } + } + private fun mapper(queueState: QueueState): EndpointServiceOuterClass.QueueState { return with( diff --git a/vripper-web/src/main/resources/application.properties b/vripper-web/src/main/resources/application.properties index 439a24d..22e354a 100644 --- a/vripper-web/src/main/resources/application.properties +++ b/vripper-web/src/main/resources/application.properties @@ -1,5 +1,5 @@ spring.liquibase.enabled=false server.port=8080 -grpc.enabled=false -grpc.passphrase= +grpc.enabled=true +grpc.passphrase=123 grpc.port=30000 \ No newline at end of file