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:
Kyle Bolen
2026-07-30 06:35:34 +00:00
parent bbb3bb1aae
commit 6912deac14
4 changed files with 9 additions and 161 deletions

View File

@@ -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 {

View File

@@ -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" }

View File

@@ -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,

View File

@@ -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
}
}