@@ -2,11 +2,7 @@ import { Button } from "@cap/ui-solid";
22import { RadioGroup as KRadioGroup } from "@kobalte/core/radio-group" ;
33import { debounce } from "@solid-primitives/scheduled" ;
44import { makePersisted } from "@solid-primitives/storage" ;
5- import {
6- createMutation ,
7- createQuery ,
8- keepPreviousData ,
9- } from "@tanstack/solid-query" ;
5+ import { createMutation } from "@tanstack/solid-query" ;
106import { Channel } from "@tauri-apps/api/core" ;
117import { CheckMenuItem , Menu } from "@tauri-apps/api/menu" ;
128import { ask , save as saveDialog } from "@tauri-apps/plugin-dialog" ;
@@ -102,7 +98,7 @@ export const EXPORT_TO_OPTIONS = [
10298 description : "Copy to paste anywhere" ,
10399 } ,
104100 {
105- label : "Link" ,
101+ label : "Shareable Link" ,
106102 value : "link" ,
107103 icon : IconCapLink ,
108104 description : "Share via Cap cloud" ,
@@ -195,11 +191,33 @@ export function ExportPage() {
195191
196192 const [ previewUrl , setPreviewUrl ] = createSignal < string | null > ( null ) ;
197193 const [ previewLoading , setPreviewLoading ] = createSignal ( false ) ;
198-
199- const updateSettings : typeof setSettings = ( ...args ) => {
194+ const [ renderEstimate , setRenderEstimate ] = createSignal < {
195+ frameRenderTimeMs : number ;
196+ totalFrames : number ;
197+ estimatedSizeMb : number ;
198+ } | null > ( null ) ;
199+
200+ type EstimateCacheKey = string ;
201+ const estimateCache = new Map <
202+ EstimateCacheKey ,
203+ { frameRenderTimeMs : number ; totalFrames : number ; estimatedSizeMb : number }
204+ > ( ) ;
205+
206+ const getEstimateCacheKey = (
207+ fps : number ,
208+ width : number ,
209+ height : number ,
210+ bpp : number ,
211+ ) : EstimateCacheKey => `${ fps } -${ width } -${ height } -${ bpp } ` ;
212+
213+ const updateSettings : typeof setSettings = ( (
214+ ...args : Parameters < typeof setSettings >
215+ ) => {
200216 setPreviewLoading ( true ) ;
201- return setSettings ( ...args ) ;
202- } ;
217+ return ( setSettings as ( ...args : Parameters < typeof setSettings > ) => void ) (
218+ ...args ,
219+ ) ;
220+ } ) as typeof setSettings ;
203221 const [ previewDialogOpen , setPreviewDialogOpen ] = createSignal ( false ) ;
204222 const [ compressionBpp , setCompressionBpp ] = createSignal (
205223 COMPRESSION_TO_BPP [ _settings . compression ] ?? 0.15 ,
@@ -223,6 +241,13 @@ export function ExportPage() {
223241 resHeight : number ,
224242 bpp : number ,
225243 ) => {
244+ const cacheKey = getEstimateCacheKey ( fps , resWidth , resHeight , bpp ) ;
245+ const cachedEstimate = estimateCache . get ( cacheKey ) ;
246+
247+ if ( cachedEstimate ) {
248+ setRenderEstimate ( cachedEstimate ) ;
249+ }
250+
226251 try {
227252 const result = await commands . generateExportPreviewFast ( frameTime , {
228253 fps,
@@ -238,6 +263,17 @@ export function ExportPage() {
238263 ) ;
239264 const blob = new Blob ( [ byteArray ] , { type : "image/jpeg" } ) ;
240265 setPreviewUrl ( URL . createObjectURL ( blob ) ) ;
266+
267+ const newEstimate = {
268+ frameRenderTimeMs : result . frame_render_time_ms ,
269+ totalFrames : result . total_frames ,
270+ estimatedSizeMb : result . estimated_size_mb ,
271+ } ;
272+
273+ if ( ! cachedEstimate ) {
274+ estimateCache . set ( cacheKey , newEstimate ) ;
275+ }
276+ setRenderEstimate ( newEstimate ) ;
241277 } catch ( e ) {
242278 console . error ( "Failed to generate preview:" , e ) ;
243279 } finally {
@@ -334,39 +370,6 @@ export function ExportPage() {
334370 }
335371 } ;
336372
337- const exportEstimates = createQuery ( ( ) => ( {
338- placeholderData : keepPreviousData ,
339- queryKey : [
340- "exportEstimates" ,
341- {
342- format : settings . format ,
343- resolution : {
344- x : settings . resolution . width ,
345- y : settings . resolution . height ,
346- } ,
347- fps : settings . fps ,
348- compression : settings . compression ,
349- } ,
350- ] as const ,
351- queryFn : ( { queryKey : [ _ , { format, resolution, fps, compression } ] } ) => {
352- const exportSettings =
353- format === "Mp4"
354- ? {
355- format : "Mp4" as const ,
356- fps,
357- resolution_base : resolution ,
358- compression,
359- }
360- : {
361- format : "Gif" as const ,
362- fps,
363- resolution_base : resolution ,
364- quality : null ,
365- } ;
366- return commands . getExportEstimates ( projectPath , exportSettings ) ;
367- } ,
368- } ) ) ;
369-
370373 const copy = createMutation ( ( ) => ( {
371374 mutationFn : async ( ) => {
372375 setIsCancelled ( false ) ;
@@ -653,33 +656,68 @@ export function ExportPage() {
653656 </ Show >
654657 </ div >
655658
656- < Suspense >
657- < Show when = { exportEstimates . data } >
658- { ( est ) => (
659+ < Show
660+ when = { ! previewLoading ( ) && renderEstimate ( ) }
661+ fallback = {
662+ < div class = "flex items-center justify-center gap-4 mt-4 text-xs text-gray-11" >
663+ < span class = "flex items-center gap-1.5" >
664+ < IconLucideClock class = "size-3.5" />
665+ < span class = "h-3.5 w-8 bg-gray-4 rounded animate-pulse" />
666+ </ span >
667+ < span class = "flex items-center gap-1.5" >
668+ < IconLucideMonitor class = "size-3.5" />
669+ < span class = "h-3.5 w-16 bg-gray-4 rounded animate-pulse" />
670+ </ span >
671+ < span class = "flex items-center gap-1.5" >
672+ < IconLucideHardDrive class = "size-3.5" />
673+ < span class = "h-3.5 w-12 bg-gray-4 rounded animate-pulse" />
674+ </ span >
675+ < span class = "flex items-center gap-1.5" >
676+ < IconLucideZap class = "size-3.5" />
677+ < span class = "h-3.5 w-10 bg-gray-4 rounded animate-pulse" />
678+ </ span >
679+ </ div >
680+ }
681+ >
682+ { ( est ) => {
683+ const data = est ( ) ;
684+ const durationSeconds = data . totalFrames / settings . fps ;
685+
686+ const exportSpeedMultiplier =
687+ settings . format === "Gif" ? 4 : 10 ;
688+ const totalTimeMs =
689+ ( data . frameRenderTimeMs * data . totalFrames ) /
690+ exportSpeedMultiplier ;
691+ const estimatedTimeSeconds = Math . max ( 1 , totalTimeMs / 1000 ) ;
692+
693+ const sizeMultiplier = settings . format === "Gif" ? 0.7 : 0.5 ;
694+ const estimatedSizeMb = data . estimatedSizeMb * sizeMultiplier ;
695+
696+ return (
659697 < div class = "flex items-center justify-center gap-4 mt-4 text-xs text-gray-11" >
660698 < span class = "flex items-center gap-1.5" >
661699 < IconLucideClock class = "size-3.5" />
662- { formatDuration ( Math . round ( est ( ) . duration_seconds ) ) }
700+ { formatDuration ( Math . round ( durationSeconds ) ) }
663701 </ span >
664702 < span class = "flex items-center gap-1.5" >
665703 < IconLucideMonitor class = "size-3.5" />
666704 { settings . resolution . width } ×{ settings . resolution . height }
667705 </ span >
668706 < span class = "flex items-center gap-1.5" >
669707 < IconLucideHardDrive class = "size-3.5" /> ~
670- { est ( ) . estimated_size_mb . toFixed ( 1 ) } MB
708+ { estimatedSizeMb . toFixed ( 1 ) } MB
671709 </ span >
672710 < span class = "flex items-center gap-1.5" >
673711 < IconLucideZap class = "size-3.5" /> ~
674- { formatDuration ( Math . round ( est ( ) . estimated_time_seconds ) ) }
712+ { formatDuration ( Math . round ( estimatedTimeSeconds ) ) }
675713 </ span >
676714 </ div >
677- ) }
678- </ Show >
679- </ Suspense >
715+ ) ;
716+ } }
717+ </ Show >
680718 </ div >
681719
682- < div class = "w-72 border-l border-gray-3 flex flex-col bg-gray-1 dark:bg-gray-2" >
720+ < div class = "w-[340px] border-l border-gray-3 flex flex-col bg-gray-1 dark:bg-gray-2" >
683721 < div class = "flex-1 overflow-y-auto p-4 space-y-5" >
684722 < Field name = "Destination" icon = { < IconCapUpload class = "size-4" /> } >
685723 < KRadioGroup
@@ -1006,15 +1044,17 @@ export function ExportPage() {
10061044 < span >
10071045 { settings . resolution . width } ×{ settings . resolution . height }
10081046 </ span >
1009- < Suspense >
1010- < Show when = { exportEstimates . data } >
1011- { ( est ) => (
1047+ < Show when = { renderEstimate ( ) } >
1048+ { ( est ) => {
1049+ const sizeMultiplier = settings . format === "Gif" ? 0.7 : 0.5 ;
1050+ return (
10121051 < span >
1013- Estimated size: { est ( ) . estimated_size_mb . toFixed ( 1 ) } MB
1052+ Estimated size:{ " " }
1053+ { ( est ( ) . estimatedSizeMb * sizeMultiplier ) . toFixed ( 1 ) } MB
10141054 </ span >
1015- ) }
1016- </ Show >
1017- </ Suspense >
1055+ ) ;
1056+ } }
1057+ </ Show >
10181058 </ div >
10191059 </ div >
10201060 </ Dialog . Root >
0 commit comments