@@ -9,25 +9,33 @@ import "@total-typescript/ts-reset/filter-boolean";
99import { CheckMenuItem , Menu , MenuItem } from "@tauri-apps/api/menu" ;
1010import { confirm } from "@tauri-apps/plugin-dialog" ;
1111import { cx } from "cva" ;
12- import { createEffect , createMemo , createResource , For , Show } from "solid-js" ;
12+ import {
13+ createEffect ,
14+ createMemo ,
15+ createResource ,
16+ createSignal ,
17+ For ,
18+ Show ,
19+ } from "solid-js" ;
1320import { createStore , reconcile } from "solid-js/store" ;
1421import themePreviewAuto from "~/assets/theme-previews/auto.jpg" ;
1522import themePreviewDark from "~/assets/theme-previews/dark.jpg" ;
1623import themePreviewLight from "~/assets/theme-previews/light.jpg" ;
1724import { Input } from "~/routes/editor/ui" ;
1825import { authStore , generalSettingsStore } from "~/store" ;
19- import IconLucidePlus from "~icons/lucide/plus" ;
20- import IconLucideX from "~icons/lucide/x" ;
2126import {
2227 type AppTheme ,
23- type CaptureWindowWithThumbnail ,
28+ type CaptureWindow ,
2429 commands ,
30+ events ,
2531 type GeneralSettingsStore ,
26- type WindowExclusion ,
2732 type MainWindowRecordingStartBehaviour ,
2833 type PostDeletionBehaviour ,
2934 type PostStudioRecordingBehaviour ,
35+ type WindowExclusion ,
3036} from "~/utils/tauri" ;
37+ import IconLucidePlus from "~icons/lucide/plus" ;
38+ import IconLucideX from "~icons/lucide/x" ;
3139import { Setting , ToggleSetting } from "./Setting" ;
3240
3341const getExclusionPrimaryLabel = ( entry : WindowExclusion ) =>
@@ -45,7 +53,7 @@ const getExclusionSecondaryLabel = (entry: WindowExclusion) => {
4553 return entry . bundleIdentifier ?? null ;
4654} ;
4755
48- const getWindowOptionLabel = ( window : CaptureWindowWithThumbnail ) => {
56+ const getWindowOptionLabel = ( window : CaptureWindow ) => {
4957 const parts = [ window . owner_name ] ;
5058 if ( window . name && window . name !== window . owner_name ) {
5159 parts . push ( window . name ) ;
@@ -59,9 +67,15 @@ type LegacyWindowExclusion = {
5967 window_title ?: string | null ;
6068} ;
6169
62- const coerceWindowExclusion = ( entry : WindowExclusion | LegacyWindowExclusion ) : WindowExclusion => {
70+ const coerceWindowExclusion = (
71+ entry : WindowExclusion | LegacyWindowExclusion ,
72+ ) : WindowExclusion => {
6373 if ( entry && typeof entry === "object" ) {
64- if ( "bundleIdentifier" in entry || "ownerName" in entry || "windowTitle" in entry ) {
74+ if (
75+ "bundleIdentifier" in entry ||
76+ "ownerName" in entry ||
77+ "windowTitle" in entry
78+ ) {
6579 const current = entry as WindowExclusion ;
6680 return {
6781 bundleIdentifier : current . bundleIdentifier ?? null ,
@@ -97,7 +111,7 @@ const normalizeWindowExclusions = (
97111 return {
98112 bundleIdentifier,
99113 ownerName,
100- windowTitle : hasBundleIdentifier ? null : coerced . windowTitle ?? null ,
114+ windowTitle : hasBundleIdentifier ? null : ( coerced . windowTitle ?? null ) ,
101115 } satisfies WindowExclusion ;
102116 } ) ;
103117
@@ -124,9 +138,9 @@ const deriveInitialSettings = (
124138 excluded_windows ?: LegacyWindowExclusion [ ] ;
125139 } ;
126140
127- const rawExcludedWindows = (
128- store . excludedWindows ?? excluded_windows ?? [ ]
129- ) as ( WindowExclusion | LegacyWindowExclusion ) [ ] ;
141+ const rawExcludedWindows = ( store . excludedWindows ??
142+ excluded_windows ??
143+ [ ] ) as ( WindowExclusion | LegacyWindowExclusion ) [ ] ;
130144
131145 return {
132146 ...defaults ,
@@ -137,7 +151,7 @@ const deriveInitialSettings = (
137151
138152export default function GeneralSettings ( ) {
139153 const [ store ] = createResource ( generalSettingsStore . get , {
140- initialValue : null ,
154+ initialValue : undefined ,
141155 } ) ;
142156
143157 return (
@@ -231,9 +245,16 @@ function Inner(props: {
231245 setSettings ( reconcile ( deriveInitialSettings ( props . initialStore ) ) ) ;
232246 } ) ;
233247
234- const [ windows ] = createResource ( commands . listWindowsWithThumbnails , {
235- initialValue : [ ] as CaptureWindowWithThumbnail [ ] ,
236- } ) ;
248+ const [ windows , { refetch : refetchWindows } ] = createResource (
249+ async ( ) => {
250+ // Fetch windows with a small delay to avoid blocking initial render
251+ await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) ) ;
252+ return commands . listCaptureWindows ( ) ;
253+ } ,
254+ {
255+ initialValue : [ ] as CaptureWindow [ ] ,
256+ } ,
257+ ) ;
237258
238259 const handleChange = async < K extends keyof typeof settings > (
239260 key : K ,
@@ -251,7 +272,10 @@ function Inner(props: {
251272 ( ) => props . isStoreLoading || windows . loading ,
252273 ) ;
253274
254- const matchesExclusion = ( exclusion : WindowExclusion , window : CaptureWindowWithThumbnail ) => {
275+ const matchesExclusion = (
276+ exclusion : WindowExclusion ,
277+ window : CaptureWindow ,
278+ ) => {
255279 const bundleMatch = exclusion . bundleIdentifier
256280 ? window . bundle_identifier === exclusion . bundleIdentifier
257281 : false ;
@@ -276,33 +300,48 @@ function Inner(props: {
276300 return false ;
277301 } ;
278302
279- const isManagedWindowsApp = ( window : CaptureWindowWithThumbnail ) => {
303+ const isManagedWindowsApp = ( window : CaptureWindow ) => {
280304 const bundle = window . bundle_identifier ?. toLowerCase ( ) ?? "" ;
281305 if ( bundle . includes ( "so.cap.desktop" ) ) {
282306 return true ;
283307 }
284308 return window . owner_name . toLowerCase ( ) . includes ( "cap" ) ;
285309 } ;
286310
311+ const isWindowAvailable = ( window : CaptureWindow ) => {
312+ if ( excludedWindows ( ) . some ( ( entry ) => matchesExclusion ( entry , window ) ) ) {
313+ return false ;
314+ }
315+ if ( ostype === "windows" ) {
316+ return isManagedWindowsApp ( window ) ;
317+ }
318+ return true ;
319+ } ;
320+
287321 const availableWindows = createMemo ( ( ) => {
288322 const data = windows ( ) ?? [ ] ;
289- return data . filter ( ( window ) => {
290- if ( excludedWindows ( ) . some ( ( entry ) => matchesExclusion ( entry , window ) ) ) {
291- return false ;
292- }
293- if ( ostype === "windows" ) {
294- return isManagedWindowsApp ( window ) ;
295- }
296- return true ;
297- } ) ;
323+ return data . filter ( isWindowAvailable ) ;
298324 } ) ;
299325
326+ const refreshAvailableWindows = async ( ) : Promise < CaptureWindow [ ] > => {
327+ try {
328+ const refreshed = ( await refetchWindows ( ) ) ?? windows ( ) ?? [ ] ;
329+ return refreshed . filter ( isWindowAvailable ) ;
330+ } catch ( error ) {
331+ console . error ( "Failed to refresh available windows" , error ) ;
332+ return availableWindows ( ) ;
333+ }
334+ } ;
335+
300336 const applyExcludedWindows = async ( next : WindowExclusion [ ] ) => {
301337 const normalized = normalizeWindowExclusions ( next ) ;
302338 setSettings ( "excludedWindows" , normalized ) ;
303339 try {
304340 await generalSettingsStore . set ( { excludedWindows : normalized } ) ;
305341 await commands . refreshWindowContentProtection ( ) ;
342+ if ( ostype === "macos" ) {
343+ await events . requestScreenCapturePrewarm . emit ( { force : true } ) ;
344+ }
306345 } catch ( error ) {
307346 console . error ( "Failed to update excluded windows" , error ) ;
308347 }
@@ -314,7 +353,7 @@ function Inner(props: {
314353 await applyExcludedWindows ( current ) ;
315354 } ;
316355
317- const handleAddWindow = async ( window : CaptureWindowWithThumbnail ) => {
356+ const handleAddWindow = async ( window : CaptureWindow ) => {
318357 const windowTitle = window . bundle_identifier ? null : window . name ;
319358
320359 const next = [
@@ -682,13 +721,14 @@ function Inner(props: {
682721 </ For >
683722 </ div >
684723 </ div >
685- </ Show >
724+ </ Show >
686725 ) }
687726 </ For >
688727
689728 < ExcludedWindowsCard
690729 excludedWindows = { excludedWindows ( ) }
691730 availableWindows = { availableWindows ( ) }
731+ onRequestAvailableWindows = { refreshAvailableWindows }
692732 onRemove = { handleRemoveExclusion }
693733 onAdd = { handleAddWindow }
694734 onReset = { handleResetExclusions }
@@ -757,33 +797,68 @@ function ServerURLSetting(props: {
757797
758798function ExcludedWindowsCard ( props : {
759799 excludedWindows : WindowExclusion [ ] ;
760- availableWindows : CaptureWindowWithThumbnail [ ] ;
800+ availableWindows : CaptureWindow [ ] ;
801+ onRequestAvailableWindows : ( ) => Promise < CaptureWindow [ ] > ;
761802 onRemove : ( index : number ) => Promise < void > ;
762- onAdd : ( window : CaptureWindowWithThumbnail ) => Promise < void > ;
803+ onAdd : ( window : CaptureWindow ) => Promise < void > ;
763804 onReset : ( ) => Promise < void > ;
764805 isLoading : boolean ;
765806 isWindows : boolean ;
766807} ) {
767808 const hasExclusions = ( ) => props . excludedWindows . length > 0 ;
768- const canAdd = ( ) => props . availableWindows . length > 0 && ! props . isLoading ;
809+ const canAdd = ( ) => ! props . isLoading ;
810+
811+ const handleAddClick = async ( event : MouseEvent ) => {
812+ event . preventDefault ( ) ;
813+ event . stopPropagation ( ) ;
769814
770- const handleAddClick = async ( ) => {
771815 if ( ! canAdd ( ) ) return ;
772816
773- const items = await Promise . all (
774- props . availableWindows . map ( ( window ) =>
775- MenuItem . new ( {
776- text : getWindowOptionLabel ( window ) ,
777- action : ( ) => {
778- void props . onAdd ( window ) ;
779- } ,
780- } ) ,
781- ) ,
782- ) ;
817+ // Use available windows if we have them, otherwise fetch
818+ let windows = props . availableWindows ;
783819
784- const menu = await Menu . new ( { items } ) ;
785- await menu . popup ( ) ;
786- await menu . close ( ) ;
820+ // Only refresh if we don't have any windows cached
821+ if ( ! windows . length ) {
822+ try {
823+ windows = await props . onRequestAvailableWindows ( ) ;
824+ } catch ( error ) {
825+ console . error ( "Failed to fetch windows:" , error ) ;
826+ return ;
827+ }
828+ }
829+
830+ if ( ! windows . length ) {
831+ console . log ( "No available windows to exclude" ) ;
832+ return ;
833+ }
834+
835+ try {
836+ const items = await Promise . all (
837+ windows . map ( ( window ) =>
838+ MenuItem . new ( {
839+ text : getWindowOptionLabel ( window ) ,
840+ action : ( ) => {
841+ void props . onAdd ( window ) ;
842+ } ,
843+ } ) ,
844+ ) ,
845+ ) ;
846+
847+ const menu = await Menu . new ( { items } ) ;
848+
849+ // Save scroll position before popup
850+ const scrollPos = window . scrollY ;
851+
852+ await menu . popup ( ) ;
853+ await menu . close ( ) ;
854+
855+ // Restore scroll position after menu closes
856+ requestAnimationFrame ( ( ) => {
857+ window . scrollTo ( 0 , scrollPos ) ;
858+ } ) ;
859+ } catch ( error ) {
860+ console . error ( "Error showing window menu:" , error ) ;
861+ }
787862 } ;
788863
789864 return (
@@ -796,38 +871,37 @@ function ExcludedWindowsCard(props: {
796871 </ p >
797872 < Show when = { props . isWindows } >
798873 < p class = "text-xs text-gray-9" >
799- Only Cap windows can be excluded on Windows.
874+ < span class = "font-medium text-gray-11" > Note:</ span > Only Cap
875+ related windows can be excluded on Windows due to technical
876+ limitations.
800877 </ p >
801878 </ Show >
802879 </ div >
803880 < div class = "flex gap-2" >
804881 < Button
805- variant = "ghost "
882+ variant = "gray "
806883 size = "sm"
807884 disabled = { props . isLoading }
808885 onClick = { ( ) => {
809886 if ( props . isLoading ) return ;
810887 void props . onReset ( ) ;
811888 } }
812889 >
813- Default
890+ Set to Default
814891 </ Button >
815892 < Button
816893 variant = "dark"
817894 size = "sm"
818895 disabled = { ! canAdd ( ) }
819- onClick = { ( ) => void handleAddClick ( ) }
896+ onClick = { ( e ) => void handleAddClick ( e ) }
820897 class = "flex items-center gap-2"
821898 >
822899 < IconLucidePlus class = "size-4" />
823900 Add
824901 </ Button >
825902 </ div >
826903 </ div >
827- < Show
828- when = { ! props . isLoading }
829- fallback = { < ExcludedWindowsSkeleton /> }
830- >
904+ < Show when = { ! props . isLoading } fallback = { < ExcludedWindowsSkeleton /> } >
831905 < Show
832906 when = { hasExclusions ( ) }
833907 fallback = {
0 commit comments