@@ -2,24 +2,34 @@ import { openUrl } from "@tauri-apps/plugin-opener";
22import { Command } from "cmdk" ;
33import {
44 ArchiveIcon ,
5+ BellIcon ,
6+ ChevronDownIcon ,
7+ DownloadIcon ,
58 FileImageIcon ,
9+ FlaskConicalIcon ,
610 FolderIcon ,
711 FolderPlusIcon ,
812 GalleryThumbnailsIcon ,
13+ HistoryIcon ,
914 Loader2Icon ,
1015 MonitorIcon ,
1116 MoonIcon ,
1217 PlusIcon ,
18+ RefreshCwIcon ,
19+ RotateCcwIcon ,
1320 SearchIcon ,
1421 SettingsIcon ,
1522 SparklesIcon ,
1623 SunIcon ,
1724 TrashIcon ,
1825 TvIcon ,
26+ UploadIcon ,
1927 VideoIcon ,
28+ Volume2Icon ,
2029} from "lucide-react" ;
2130import { useCallback , useEffect , useState } from "react" ;
2231import { sileo } from "sileo" ;
32+ import { checkForUpdate , useUpdateStore } from "@/hooks/use-app-updater" ;
2333import * as sounds from "@/lib/sounds" ;
2434import { searchVideos , type YoutubeVideo } from "@/lib/youtube-api" ;
2535import { getYoutubeApiKey } from "@/lib/youtube-store" ;
@@ -30,6 +40,8 @@ import {
3040} from "@/stores/use-gallery-store" ;
3141import { useTabsStore } from "@/stores/use-tabs-store" ;
3242
43+ const PROJECTS_PAGE_SIZE = 8 ;
44+
3345type Page =
3446 | "gallery"
3547 | "ai-generate"
@@ -59,13 +71,34 @@ export function CommandPalette({
5971} : CommandPaletteProps ) {
6072 const thumbnails = useGalleryStore ( ( s ) => s . thumbnails ) ;
6173 const setTheme = useAppSettingsStore ( ( s ) => s . setTheme ) ;
74+ const autoCheckForUpdates = useAppSettingsStore ( ( s ) => s . autoCheckForUpdates ) ;
75+ const setAutoCheckForUpdates = useAppSettingsStore (
76+ ( s ) => s . setAutoCheckForUpdates
77+ ) ;
78+ const saveSearchHistory = useAppSettingsStore ( ( s ) => s . saveSearchHistory ) ;
79+ const setSaveSearchHistory = useAppSettingsStore (
80+ ( s ) => s . setSaveSearchHistory
81+ ) ;
82+ const experimentalFeaturesEnabled = useAppSettingsStore (
83+ ( s ) => s . experimentalFeaturesEnabled
84+ ) ;
85+ const setExperimentalFeaturesEnabled = useAppSettingsStore (
86+ ( s ) => s . setExperimentalFeaturesEnabled
87+ ) ;
88+ const soundsEnabled = useAppSettingsStore ( ( s ) => s . soundsEnabled ) ;
89+ const setSoundsEnabled = useAppSettingsStore ( ( s ) => s . setSoundsEnabled ) ;
90+ const setOnboardingCompleted = useAppSettingsStore (
91+ ( s ) => s . setOnboardingCompleted
92+ ) ;
6293 const openTab = useTabsStore ( ( s ) => s . openTab ) ;
6394 const setEditorVisible = useTabsStore ( ( s ) => s . setEditorVisible ) ;
95+ const checking = useUpdateStore ( ( s ) => s . checking ) ;
6496
6597 const [ search , setSearch ] = useState ( "" ) ;
6698 const [ ytApiKey , setYtApiKey ] = useState < string | null > ( null ) ;
6799 const [ ytResults , setYtResults ] = useState < YoutubeVideo [ ] > ( [ ] ) ;
68100 const [ ytLoading , setYtLoading ] = useState ( false ) ;
101+ const [ projectPage , setProjectPage ] = useState ( 1 ) ;
69102
70103 useEffect ( ( ) => {
71104 getYoutubeApiKey ( )
@@ -77,9 +110,14 @@ export function CommandPalette({
77110 if ( ! open ) {
78111 setSearch ( "" ) ;
79112 setYtResults ( [ ] ) ;
113+ setProjectPage ( 1 ) ;
80114 }
81115 } , [ open ] ) ;
82116
117+ useEffect ( ( ) => {
118+ setProjectPage ( 1 ) ;
119+ } , [ search ] ) ;
120+
83121 useEffect ( ( ) => {
84122 const handler = ( e : KeyboardEvent ) => {
85123 if ( ( e . ctrlKey || e . metaKey ) && e . key . toLowerCase ( ) === "k" ) {
@@ -147,6 +185,40 @@ export function CommandPalette({
147185 onOpenChange ( false ) ;
148186 } ;
149187
188+ const handleCheckForUpdates = ( ) => {
189+ sounds . click ( ) ;
190+ onOpenChange ( false ) ;
191+ checkForUpdate ( ) ;
192+ } ;
193+
194+ const handleToggle = (
195+ current : boolean ,
196+ setter : ( v : boolean ) => Promise < void > ,
197+ label : string
198+ ) => {
199+ sounds . click ( ) ;
200+ onOpenChange ( false ) ;
201+ setter ( ! current ) . then ( ( ) => {
202+ sileo . success ( { title : `${ label } : ${ current ? "Off" : "On" } ` } ) ;
203+ } ) ;
204+ } ;
205+
206+ const handleResetOnboarding = ( ) => {
207+ sounds . click ( ) ;
208+ onOpenChange ( false ) ;
209+ setOnboardingCompleted ( false ) . then ( ( ) => {
210+ sileo . success ( { title : "Onboarding reset" } ) ;
211+ } ) ;
212+ } ;
213+
214+ const isSearching = search . trim ( ) . length > 0 ;
215+ const visibleThumbnails = isSearching
216+ ? thumbnails
217+ : thumbnails . slice ( 0 , projectPage * PROJECTS_PAGE_SIZE ) ;
218+ const remainingProjects =
219+ thumbnails . length - projectPage * PROJECTS_PAGE_SIZE ;
220+ const hasMoreProjects = ! isSearching && remainingProjects > 0 ;
221+
150222 if ( ! open ) return null ;
151223
152224 return (
@@ -158,7 +230,7 @@ export function CommandPalette({
158230 />
159231
160232 { /* palette */ }
161- < div className = "relative z-10 w-full max-w-[560px ] overflow-hidden rounded-xl border border-border bg-popover shadow-2xl" >
233+ < div className = "relative z-10 w-full max-w-[580px ] overflow-hidden rounded-xl border border-border bg-popover shadow-2xl" >
162234 < Command
163235 className = "flex flex-col"
164236 filter = { commandFilter }
@@ -167,19 +239,19 @@ export function CommandPalette({
167239 } }
168240 >
169241 { /* input */ }
170- < div className = "flex items-center gap-2 border-border border-b px-3 " >
171- < SearchIcon className = "size-4 shrink-0 text-muted-foreground" />
242+ < div className = "flex items-center gap-3 border-border border-b px-4 " >
243+ < SearchIcon className = "size-5 shrink-0 text-muted-foreground" />
172244 < Command . Input
173245 autoFocus
174- className = "h-11 w-full bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground"
246+ className = "h-14 w-full bg-transparent py-3 text-base outline-none placeholder:text-muted-foreground"
175247 onValueChange = { setSearch }
176248 placeholder = "Search commands, projects, YouTube..."
177249 value = { search }
178250 />
179251 </ div >
180252
181253 { /* list */ }
182- < Command . List className = "max-h-[420px ] overflow-y-auto overflow-x-hidden p-1" >
254+ < Command . List className = "max-h-[500px ] overflow-y-auto overflow-x-hidden p-1.5 " >
183255 < Command . Empty className = "py-8 text-center text-muted-foreground text-sm" >
184256 No results found.
185257 </ Command . Empty >
@@ -312,12 +384,140 @@ export function CommandPalette({
312384 </ Item >
313385 </ Group >
314386
387+ < Separator />
388+
389+ { /* System */ }
390+ < Group heading = "System" >
391+ < Item
392+ icon = {
393+ checking ? (
394+ < Loader2Icon className = "animate-spin" />
395+ ) : (
396+ < RefreshCwIcon />
397+ )
398+ }
399+ keywords = { [
400+ "check" ,
401+ "update" ,
402+ "version" ,
403+ "upgrade" ,
404+ "new version" ,
405+ ] }
406+ onSelect = { handleCheckForUpdates }
407+ value = "check-updates"
408+ >
409+ Check for Updates
410+ </ Item >
411+ < Item
412+ icon = { < BellIcon /> }
413+ keywords = { [ "auto" , "automatic" , "updates" , "notify" ] }
414+ onSelect = { ( ) =>
415+ handleToggle (
416+ autoCheckForUpdates ,
417+ setAutoCheckForUpdates ,
418+ "Auto Updates"
419+ )
420+ }
421+ suffix = { < StateBadge enabled = { autoCheckForUpdates } /> }
422+ value = "toggle-auto-updates"
423+ >
424+ Auto Updates
425+ </ Item >
426+ < Item
427+ icon = { < FlaskConicalIcon /> }
428+ keywords = { [
429+ "experimental" ,
430+ "beta" ,
431+ "features" ,
432+ "labs" ,
433+ "preview" ,
434+ ] }
435+ onSelect = { ( ) =>
436+ handleToggle (
437+ experimentalFeaturesEnabled ,
438+ setExperimentalFeaturesEnabled ,
439+ "Experimental Features"
440+ )
441+ }
442+ suffix = { < StateBadge enabled = { experimentalFeaturesEnabled } /> }
443+ value = "toggle-experimental"
444+ >
445+ Experimental Features
446+ </ Item >
447+ < Item
448+ icon = { < HistoryIcon /> }
449+ keywords = { [ "search" , "history" , "save" , "privacy" ] }
450+ onSelect = { ( ) =>
451+ handleToggle (
452+ saveSearchHistory ,
453+ setSaveSearchHistory ,
454+ "Search History"
455+ )
456+ }
457+ suffix = { < StateBadge enabled = { saveSearchHistory } /> }
458+ value = "toggle-search-history"
459+ >
460+ Search History
461+ </ Item >
462+ < Item
463+ icon = { < Volume2Icon /> }
464+ keywords = { [ "sound" , "audio" , "effects" , "mute" , "sfx" ] }
465+ onSelect = { ( ) =>
466+ handleToggle ( soundsEnabled , setSoundsEnabled , "Sound Effects" )
467+ }
468+ suffix = { < StateBadge enabled = { soundsEnabled } /> }
469+ value = "toggle-sounds"
470+ >
471+ Sound Effects
472+ </ Item >
473+ < Item
474+ icon = { < RotateCcwIcon /> }
475+ keywords = { [
476+ "reset" ,
477+ "onboarding" ,
478+ "tutorial" ,
479+ "restart" ,
480+ "walkthrough" ,
481+ ] }
482+ onSelect = { handleResetOnboarding }
483+ value = "reset-onboarding"
484+ >
485+ Reset Onboarding
486+ </ Item >
487+ < Item
488+ icon = { < DownloadIcon /> }
489+ keywords = { [ "export" , "backup" , "save" , "zip" , "storage" ] }
490+ onSelect = { ( ) => run ( ( ) => onPageChange ( "settings" ) ) }
491+ suffix = {
492+ < span className = "shrink-0 text-muted-foreground text-xs" >
493+ Settings → Storage
494+ </ span >
495+ }
496+ value = "export-backup"
497+ >
498+ Export Backup
499+ </ Item >
500+ < Item
501+ icon = { < UploadIcon /> }
502+ keywords = { [ "import" , "backup" , "restore" , "zip" , "storage" ] }
503+ onSelect = { ( ) => run ( ( ) => onPageChange ( "settings" ) ) }
504+ suffix = {
505+ < span className = "shrink-0 text-muted-foreground text-xs" >
506+ Settings → Storage
507+ </ span >
508+ }
509+ value = "import-backup"
510+ >
511+ Import Backup
512+ </ Item >
513+ </ Group >
514+
315515 { /* Projects */ }
316516 { thumbnails . length > 0 && (
317517 < >
318518 < Separator />
319519 < Group heading = "Projects" >
320- { thumbnails . slice ( 0 , 50 ) . map ( ( t ) => (
520+ { visibleThumbnails . map ( ( t ) => (
321521 < Item
322522 icon = { < FileImageIcon /> }
323523 key = { t . id }
@@ -333,6 +533,23 @@ export function CommandPalette({
333533 { t . name }
334534 </ Item >
335535 ) ) }
536+ { hasMoreProjects && (
537+ < Command . Item
538+ className = "mt-0.5 flex cursor-default select-none items-center gap-2 rounded-lg px-2 py-2.5 text-muted-foreground text-sm outline-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground"
539+ onSelect = { ( ) => setProjectPage ( ( p ) => p + 1 ) }
540+ value = "load-more-projects"
541+ >
542+ < ChevronDownIcon className = "size-4 shrink-0" />
543+ < span >
544+ Show { Math . min ( PROJECTS_PAGE_SIZE , remainingProjects ) } { " " }
545+ more projects
546+ </ span >
547+ < span className = "ml-auto text-xs" >
548+ { projectPage * PROJECTS_PAGE_SIZE } of{ " " }
549+ { thumbnails . length }
550+ </ span >
551+ </ Command . Item >
552+ ) }
336553 </ Group >
337554 </ >
338555 ) }
@@ -344,7 +561,7 @@ export function CommandPalette({
344561 < Group heading = "YouTube" >
345562 { ytLoading ? (
346563 < Command . Item
347- className = "flex cursor-default items-center gap-2 rounded-lg px-2 py-1 .5 text-muted-foreground text-sm"
564+ className = "flex cursor-default items-center gap-2 rounded-lg px-2 py-2 .5 text-muted-foreground text-sm"
348565 disabled
349566 value = "yt-loading"
350567 >
@@ -400,6 +617,20 @@ function Separator() {
400617 return < Command . Separator className = "my-1 h-px bg-border" /> ;
401618}
402619
620+ function StateBadge ( { enabled } : { enabled : boolean } ) {
621+ return (
622+ < span
623+ className = { `shrink-0 rounded-full px-1.5 py-0.5 font-medium text-xs ${
624+ enabled
625+ ? "bg-green-500/15 text-green-500"
626+ : "bg-muted text-muted-foreground"
627+ } `}
628+ >
629+ { enabled ? "On" : "Off" }
630+ </ span >
631+ ) ;
632+ }
633+
403634function Item ( {
404635 value,
405636 keywords,
@@ -419,7 +650,7 @@ function Item({
419650} ) {
420651 return (
421652 < Command . Item
422- className = "flex cursor-default select-none items-center gap-2 rounded-lg px-2 py-1 .5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-40 [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground data-[selected=true]:[&_svg]:text-accent-foreground"
653+ className = "flex cursor-default select-none items-center gap-2.5 rounded-lg px-2 py-2 .5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-40 [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground data-[selected=true]:[&_svg]:text-accent-foreground"
423654 keywords = { keywords }
424655 onSelect = { onSelect }
425656 value = { value }
0 commit comments