Skip to content

Commit a528f91

Browse files
xororzclaude
andcommitted
feat: import a local image for UltraFix
Long-pressing the upscale or UltraFix button on the result page opens an import dialog: pick a local image, adjust a scale slider, and use it as the UltraFix source instead of only images generated in-app. The slider spans from "shrink to upscale-eligible" up to a 4096 long-side cap (bounding decode memory and upload size). There is no minimum size: an import too small for the UltraFix window (long side above 1024, short side at least the tile) simply lands on the result page upscale-eligible, so the user upscales it first and then runs UltraFix. Oversized sources are decoded pre-scaled to the cap, EXIF rotation is applied, and output dimensions are aligned to multiples of 8 for the VAE. The imported image is not written to history; only the repaired result is. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent a35a56f commit a528f91

7 files changed

Lines changed: 382 additions & 16 deletions

File tree

app/src/main/java/io/github/xororz/localdream/ui/screens/ModelRunPages.kt

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import androidx.compose.material3.LocalMinimumInteractiveComponentSize
6363
import androidx.compose.material3.MaterialTheme
6464
import androidx.compose.material3.Surface
6565
import androidx.compose.material3.Text
66+
import androidx.compose.material3.minimumInteractiveComponentSize
6667
import androidx.compose.runtime.Composable
6768
import androidx.compose.runtime.CompositionLocalProvider
6869
import androidx.compose.runtime.LaunchedEffect
@@ -74,8 +75,10 @@ import androidx.compose.runtime.withFrameNanos
7475
import androidx.compose.ui.Alignment
7576
import androidx.compose.ui.Modifier
7677
import androidx.compose.ui.draw.alpha
78+
import androidx.compose.ui.draw.clip
7779
import androidx.compose.ui.graphics.Color
7880
import androidx.compose.ui.graphics.graphicsLayer
81+
import androidx.compose.ui.graphics.vector.ImageVector
7982
import androidx.compose.ui.platform.LocalConfiguration
8083
import androidx.compose.ui.platform.LocalContext
8184
import androidx.compose.ui.res.pluralStringResource
@@ -105,6 +108,10 @@ internal fun ModelRunResultPage(
105108
upscaleEnabled: Boolean,
106109
showUltrafixButton: Boolean,
107110
ultrafixEnabled: Boolean,
111+
// Long-pressing either the upscale or the UltraFix button opens the
112+
// local-image import dialog for UltraFix.
113+
onUpscaleLongClick: () -> Unit,
114+
onUltrafixLongClick: () -> Unit,
108115
// null when the displayed image has no history row to mark (hides the
109116
// favorite button).
110117
isFavorite: Boolean?,
@@ -230,27 +237,23 @@ internal fun ModelRunResultPage(
230237
}
231238

232239
if (showUpscaleButton) {
233-
FilledTonalIconButton(
234-
onClick = onUpscaleClick,
240+
LongPressableTonalIconButton(
235241
enabled = upscaleEnabled,
236-
) {
237-
Icon(
238-
imageVector = Icons.Default.AutoFixHigh,
239-
contentDescription = "upscale image",
240-
)
241-
}
242+
onClick = onUpscaleClick,
243+
onLongClick = onUpscaleLongClick,
244+
icon = Icons.Default.AutoFixHigh,
245+
contentDescription = "upscale image",
246+
)
242247
}
243248

244249
if (showUltrafixButton) {
245-
FilledTonalIconButton(
246-
onClick = onUltrafixClick,
250+
LongPressableTonalIconButton(
247251
enabled = ultrafixEnabled,
248-
) {
249-
Icon(
250-
imageVector = Icons.Default.AutoAwesome,
251-
contentDescription = "ultrafix image",
252-
)
253-
}
252+
onClick = onUltrafixClick,
253+
onLongClick = onUltrafixLongClick,
254+
icon = Icons.Default.AutoAwesome,
255+
contentDescription = "ultrafix image",
256+
)
254257
}
255258

256259
FilledTonalIconButton(
@@ -724,3 +727,39 @@ internal fun ModelRunHistoryPage(
724727
}
725728
}
726729
}
730+
731+
// FilledTonalIconButton look-alike with a long-press slot (M3 icon buttons
732+
// expose none). Both result-page image actions use it so a long press on
733+
// either can open the UltraFix local-image import dialog.
734+
@Composable
735+
private fun LongPressableTonalIconButton(
736+
enabled: Boolean,
737+
onClick: () -> Unit,
738+
onLongClick: () -> Unit,
739+
icon: ImageVector,
740+
contentDescription: String,
741+
) {
742+
val colors = IconButtonDefaults.filledTonalIconButtonColors()
743+
Box(
744+
modifier = Modifier
745+
.minimumInteractiveComponentSize()
746+
// Standard M3 icon-button container.
747+
.size(40.dp)
748+
.clip(CircleShape)
749+
.background(
750+
if (enabled) colors.containerColor else colors.disabledContainerColor,
751+
)
752+
.combinedClickable(
753+
enabled = enabled,
754+
onClick = onClick,
755+
onLongClick = onLongClick,
756+
),
757+
contentAlignment = Alignment.Center,
758+
) {
759+
Icon(
760+
imageVector = icon,
761+
contentDescription = contentDescription,
762+
tint = if (enabled) colors.contentColor else colors.disabledContentColor,
763+
)
764+
}
765+
}

app/src/main/java/io/github/xororz/localdream/ui/screens/ModelRunScreen.kt

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,7 @@ fun ModelRunScreen(
518518
// pendingUltrafix marks the in-flight generation as an ultrafix run so the
519519
// completion handler can record the right mode.
520520
var showUltrafixConfirmDialog by remember { mutableStateOf(false) }
521+
var showUltrafixImportDialog by remember { mutableStateOf(false) }
521522
var pendingUltrafix by remember { mutableStateOf(false) }
522523
var isUltrafixPreparing by remember { mutableStateOf(false) }
523524
// UltraFix runs with its own steps/denoise (defaults 10 / 0.4), persisted
@@ -983,6 +984,31 @@ fun ModelRunScreen(
983984
uri?.let { processSelectedImage(it) }
984985
}
985986

987+
// Places an imported (already size-validated and resized) local image
988+
// into the result slot as the UltraFix source. Synthetic params: only the
989+
// dimensions matter (they drive the size gating); everything else marks
990+
// "not generated by this app". The import itself is never written to
991+
// history - only the repaired result is.
992+
fun applyUltrafixImport(bitmap: Bitmap) {
993+
currentBitmap = bitmap
994+
generationParams = GenerationParameters(
995+
steps = 0,
996+
cfg = cfg,
997+
seed = null,
998+
prompt = "",
999+
negativePrompt = "",
1000+
generationTime = null,
1001+
width = bitmap.width,
1002+
height = bitmap.height,
1003+
runOnCpu = false,
1004+
mode = GenerationMode.UNKNOWN,
1005+
)
1006+
generationParamsModelId = modelId
1007+
currentDisplayedHistoryId = null
1008+
stitchableHistoryIds = emptySet()
1009+
imageVersion++
1010+
}
1011+
9861012
val contentPickerLauncher = rememberLauncherForActivityResult(
9871013
ActivityResultContracts.GetContent(),
9881014
) { uri ->
@@ -2495,6 +2521,15 @@ fun ModelRunScreen(
24952521
maxOf(currentWidth, currentHeight, 512)
24962522
} == true,
24972523
ultrafixEnabled = !isRunning && !isUpscaling && !isUltrafixPreparing,
2524+
// The upscale button also exists on models that
2525+
// can't run UltraFix (SD1.5 NPU); its long press
2526+
// only opens the import dialog when the model can.
2527+
onUpscaleLongClick = {
2528+
if (useImg2img && model.isSdxl && cfg == 1f) {
2529+
showUltrafixImportDialog = true
2530+
}
2531+
},
2532+
onUltrafixLongClick = { showUltrafixImportDialog = true },
24982533
isFavorite = if (currentDisplayedHistoryId != null) {
24992534
displayedFavorite
25002535
} else {
@@ -2733,6 +2768,27 @@ fun ModelRunScreen(
27332768
)
27342769
}
27352770

2771+
// Import-and-resize a local image for UltraFix. A confirmed image that
2772+
// already satisfies the UltraFix window goes straight to the parameter
2773+
// dialog; a smaller one just lands on the result page, where the visible
2774+
// upscale button is the natural next step.
2775+
if (showUltrafixImportDialog) {
2776+
UltrafixImportDialog(
2777+
tileSize = maxOf(currentWidth, currentHeight, 512),
2778+
onDismiss = { showUltrafixImportDialog = false },
2779+
onConfirm = { bitmap ->
2780+
applyUltrafixImport(bitmap)
2781+
showUltrafixImportDialog = false
2782+
val tile = maxOf(currentWidth, currentHeight, 512)
2783+
if (maxOf(bitmap.width, bitmap.height) > 1024 &&
2784+
minOf(bitmap.width, bitmap.height) >= tile
2785+
) {
2786+
showUltrafixConfirmDialog = true
2787+
}
2788+
},
2789+
)
2790+
}
2791+
27362792
// Ultrafix parameter confirmation.
27372793
if (showUltrafixConfirmDialog) {
27382794
AlertDialog(
@@ -2755,6 +2811,12 @@ fun ModelRunScreen(
27552811
color = MaterialTheme.colorScheme.onSurfaceVariant,
27562812
)
27572813

2814+
Text(
2815+
stringResource(R.string.ultrafix_longpress_hint),
2816+
style = MaterialTheme.typography.bodySmall,
2817+
color = MaterialTheme.colorScheme.onSurfaceVariant,
2818+
)
2819+
27582820
// Independent UltraFix steps (1..20). Lowering steps also
27592821
// tightens the denoise-step cap below.
27602822
val denoiseStepsMax =

0 commit comments

Comments
 (0)