Remove video preview - JavaFX MOV playback not viable in Compose Desktop
Removed LivePhotoPlayer.kt, JavaFX plugin/deps from build.gradle.kts and libs.versions.toml. PhotoPreviewDialog shows still HEIC only. LIVE badge in dialog uses the same green as in the grid.
This commit is contained in:
@@ -4,7 +4,6 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
alias(libs.plugins.kotlin.serialization)
|
alias(libs.plugins.kotlin.serialization)
|
||||||
alias(libs.plugins.compose.multiplatform)
|
alias(libs.plugins.compose.multiplatform)
|
||||||
alias(libs.plugins.javafx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "com.bolenpad.photophetch"
|
group = "com.bolenpad.photophetch"
|
||||||
@@ -32,13 +31,6 @@ dependencies {
|
|||||||
implementation(libs.serialization.json)
|
implementation(libs.serialization.json)
|
||||||
implementation(libs.slf4j.api)
|
implementation(libs.slf4j.api)
|
||||||
implementation(libs.logback.classic)
|
implementation(libs.logback.classic)
|
||||||
|
|
||||||
// JavaFX media — configured via the javafx plugin block below
|
|
||||||
}
|
|
||||||
|
|
||||||
javafx {
|
|
||||||
version = "21.0.3"
|
|
||||||
modules = listOf("javafx.media", "javafx.swing", "javafx.graphics", "javafx.controls", "javafx.base")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
compose.desktop {
|
compose.desktop {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ coroutines = "1.8.0"
|
|||||||
serialization = "1.6.3"
|
serialization = "1.6.3"
|
||||||
slf4j = "2.0.12"
|
slf4j = "2.0.12"
|
||||||
logback = "1.5.3"
|
logback = "1.5.3"
|
||||||
javafx = "21.0.3"
|
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
|
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
|
||||||
@@ -19,4 +18,3 @@ logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "lo
|
|||||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||||
compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "compose" }
|
compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "compose" }
|
||||||
javafx = { id = "org.openjfx.javafxplugin", version = "0.1.0" }
|
|
||||||
|
|||||||
@@ -365,16 +365,11 @@ private fun PhotoPreviewDialog(
|
|||||||
val hash = (cacheKey + photo.devicePath).hashCode().toString(16)
|
val hash = (cacheKey + photo.devicePath).hashCode().toString(16)
|
||||||
val cachePath = cacheDir.resolve("$hash.jpg")
|
val cachePath = cacheDir.resolve("$hash.jpg")
|
||||||
|
|
||||||
// For Live Photos: pull companion MOV to temp for inline playback
|
|
||||||
var movPath by remember(photo.devicePath) { mutableStateOf<String?>(null) }
|
|
||||||
|
|
||||||
LaunchedEffect(photo.devicePath) {
|
LaunchedEffect(photo.devicePath) {
|
||||||
val bytes = withContext(Dispatchers.IO) {
|
val bytes = withContext(Dispatchers.IO) {
|
||||||
// Always use the cached JPEG (sips-converted) — works for HEIC too
|
|
||||||
if (cachePath.toFile().exists() && cachePath.toFile().length() > 0) {
|
if (cachePath.toFile().exists() && cachePath.toFile().length() > 0) {
|
||||||
java.nio.file.Files.readAllBytes(cachePath)
|
java.nio.file.Files.readAllBytes(cachePath)
|
||||||
} else {
|
} else {
|
||||||
// Pull to temp, convert to JPEG via sips, cache it
|
|
||||||
val tmpDir = Paths.get(System.getProperty("java.io.tmpdir"), "photophetch-preview")
|
val tmpDir = Paths.get(System.getProperty("java.io.tmpdir"), "photophetch-preview")
|
||||||
tmpDir.toFile().mkdirs()
|
tmpDir.toFile().mkdirs()
|
||||||
val tmpFile = tmpDir.resolve("preview-${photo.filename}")
|
val tmpFile = tmpDir.resolve("preview-${photo.filename}")
|
||||||
@@ -391,21 +386,7 @@ private fun PhotoPreviewDialog(
|
|||||||
} else null
|
} else null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (bytes != null) {
|
if (bytes != null) runCatching { bitmap = SkiaImage.makeFromEncoded(bytes).toComposeImageBitmap() }
|
||||||
runCatching { bitmap = SkiaImage.makeFromEncoded(bytes).toComposeImageBitmap() }
|
|
||||||
}
|
|
||||||
|
|
||||||
// For Live Photos: pull companion MOV for inline video playback
|
|
||||||
if (photo.isLivePhotoStill && photo.companionDevicePath != null) {
|
|
||||||
val tmpDir = Paths.get(System.getProperty("java.io.tmpdir"), "photophetch-preview")
|
|
||||||
tmpDir.toFile().mkdirs()
|
|
||||||
val movFilename = photo.companionDevicePath.substringAfterLast("/")
|
|
||||||
val tmpMov = tmpDir.resolve(movFilename)
|
|
||||||
if (!tmpMov.toFile().exists()) {
|
|
||||||
withContext(Dispatchers.IO) { pullFn(photo.companionDevicePath, tmpMov) }
|
|
||||||
}
|
|
||||||
if (tmpMov.toFile().exists()) movPath = tmpMov.toString()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialog(onDismissRequest = onDismiss) {
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
@@ -420,15 +401,14 @@ private fun PhotoPreviewDialog(
|
|||||||
Text(photo.filename, fontWeight = FontWeight.SemiBold,
|
Text(photo.filename, fontWeight = FontWeight.SemiBold,
|
||||||
style = MaterialTheme.typography.bodyMedium)
|
style = MaterialTheme.typography.bodyMedium)
|
||||||
val date = photo.dateTaken.atZone(ZoneId.systemDefault())
|
val date = photo.dateTaken.atZone(ZoneId.systemDefault())
|
||||||
Text(
|
Text("${date.toLocalDate()} ${date.toLocalTime().toString().take(5)}",
|
||||||
"${date.toLocalDate()} ${date.toLocalTime().toString().take(5)}",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (photo.isLivePhotoStill) {
|
if (photo.isLivePhotoStill) {
|
||||||
Badge(containerColor = MaterialTheme.colorScheme.secondary) {
|
Badge(containerColor = Color(0xFF00C853)) {
|
||||||
Text("LIVE", style = MaterialTheme.typography.labelSmall)
|
Text("LIVE", style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = Color.White)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -436,42 +416,13 @@ private fun PhotoPreviewDialog(
|
|||||||
Box(modifier = Modifier.fillMaxWidth().aspectRatio(4f / 3f),
|
Box(modifier = Modifier.fillMaxWidth().aspectRatio(4f / 3f),
|
||||||
contentAlignment = Alignment.Center) {
|
contentAlignment = Alignment.Center) {
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
androidx.compose.foundation.Image(
|
androidx.compose.foundation.Image(bitmap = bitmap!!,
|
||||||
bitmap = bitmap!!,
|
|
||||||
contentDescription = photo.filename,
|
contentDescription = photo.filename,
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit, modifier = Modifier.fillMaxSize())
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
if (photo.isLivePhotoStill) {
|
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
Text("Loading…",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Live Photo motion companion — inline looping video player
|
|
||||||
val mov = movPath
|
|
||||||
if (photo.isLivePhotoStill && mov != null && isJavaFxMediaAvailable()) {
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
Text("▶ Live Photo motion",
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
||||||
Spacer(Modifier.height(2.dp))
|
|
||||||
LivePhotoPlayer(
|
|
||||||
videoPath = mov,
|
|
||||||
modifier = Modifier.fillMaxWidth().height(200.dp),
|
|
||||||
)
|
|
||||||
} else if (photo.isLivePhotoStill && mov == null && movPath == null) {
|
|
||||||
Text("⏳ Loading motion clip…",
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text("${photo.sizeBytes / 1024} KB · ${photo.sourceFolder}",
|
Text("${photo.sizeBytes / 1024} KB · ${photo.sourceFolder}",
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
package com.bolenpad.photophetch.ui
|
|
||||||
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.awt.SwingPanel
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.awt.BorderLayout
|
|
||||||
import java.io.File
|
|
||||||
import javax.swing.JPanel
|
|
||||||
import javax.swing.SwingUtilities
|
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger("LivePhotoPlayer")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Embeds a looping JavaFX MediaPlayer inside a Compose Desktop SwingPanel.
|
|
||||||
*
|
|
||||||
* JavaFX toolkit must be initialized before use. We initialize it lazily
|
|
||||||
* via JFXPanel on first use.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun LivePhotoPlayer(
|
|
||||||
videoPath: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
SwingPanel(
|
|
||||||
modifier = modifier,
|
|
||||||
factory = {
|
|
||||||
val panel = JPanel(BorderLayout())
|
|
||||||
|
|
||||||
SwingUtilities.invokeLater {
|
|
||||||
try {
|
|
||||||
// Initialize JavaFX toolkit via JFXPanel
|
|
||||||
val jfxPanel = javafx.embed.swing.JFXPanel()
|
|
||||||
panel.add(jfxPanel, BorderLayout.CENTER)
|
|
||||||
panel.revalidate()
|
|
||||||
|
|
||||||
javafx.application.Platform.runLater {
|
|
||||||
try {
|
|
||||||
val file = File(videoPath)
|
|
||||||
if (!file.exists()) {
|
|
||||||
log.warn("LivePhotoPlayer: file not found: $videoPath")
|
|
||||||
return@runLater
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("LivePhotoPlayer: loading ${file.name} (${file.length() / 1024}KB)")
|
|
||||||
|
|
||||||
val media = javafx.scene.media.Media(file.toURI().toString())
|
|
||||||
val player = javafx.scene.media.MediaPlayer(media)
|
|
||||||
|
|
||||||
player.setOnError {
|
|
||||||
log.warn("LivePhotoPlayer: MediaPlayer error: ${player.error?.message}")
|
|
||||||
}
|
|
||||||
player.setOnReady {
|
|
||||||
log.info("LivePhotoPlayer: ready, playing")
|
|
||||||
player.cycleCount = javafx.scene.media.MediaPlayer.INDEFINITE
|
|
||||||
player.isMute = true
|
|
||||||
player.play()
|
|
||||||
}
|
|
||||||
|
|
||||||
val mediaView = javafx.scene.media.MediaView(player).apply {
|
|
||||||
isPreserveRatio = true
|
|
||||||
fitWidth = 580.0
|
|
||||||
}
|
|
||||||
|
|
||||||
val root = javafx.scene.layout.StackPane(mediaView)
|
|
||||||
root.style = "-fx-background-color: black;"
|
|
||||||
jfxPanel.scene = javafx.scene.Scene(root)
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.warn("LivePhotoPlayer: Platform.runLater failed: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.warn("LivePhotoPlayer: SwingUtilities init failed: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
panel
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if JavaFX media is available on the classpath.
|
|
||||||
*/
|
|
||||||
fun isJavaFxMediaAvailable(): Boolean {
|
|
||||||
return try {
|
|
||||||
Class.forName("javafx.scene.media.MediaPlayer")
|
|
||||||
true
|
|
||||||
} catch (_: ClassNotFoundException) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user