@@ -13,11 +13,19 @@ import OnboardingWizard from './components/OnboardingWizard'
1313import { useTheme } from './hooks/useTheme'
1414import CloseConfirmDialog from './components/CloseConfirmDialog'
1515
16+ const GITHUB_REPO_URL =
17+ 'https://github.com/beautyfree/skiller-skills-desktop-manager'
18+ const STAR_PROMPT_MIN_LAUNCHES = 3
19+ const STAR_PROMPT_MIN_AGE_MS = 24 * 60 * 60 * 1000
20+ const STAR_PROMPT_RESHOW_LAUNCHES = 3
21+ const STAR_PROMPT_RESHOW_MS = 3 * 24 * 60 * 60 * 1000
22+
1623function AppInner ( ) {
1724 const queryClient = useQueryClient ( )
1825 const { i18n } = useTranslation ( )
1926 useTheme ( )
2027 const [ closeDialogOpen , setCloseDialogOpen ] = useState ( false )
28+ const [ showGithubStarPrompt , setShowGithubStarPrompt ] = useState ( false )
2129 // Onboarding shows on very first launch (or when user explicitly replays it
2230 // from Settings). Guarded by a localStorage flag — we respect privacy mode
2331 // by falling back to "not done" if storage throws, which still shows once.
@@ -92,6 +100,104 @@ function AppInner() {
92100 . catch ( ( ) => { } )
93101 } , [ ] ) // eslint-disable-line react-hooks/exhaustive-deps
94102
103+ // Ask for a GitHub star once after meaningful usage cadence.
104+ useEffect ( ( ) => {
105+ let cancelled = false
106+ void invoke ( 'read_settings' )
107+ . then ( async ( settings ) => {
108+ const existing = settings . github_star_prompt ?? { }
109+ const now = Date . now ( )
110+ const nowIso = new Date ( now ) . toISOString ( )
111+ const firstSeenAt = existing . first_seen_at ?? nowIso
112+ const launchCount = Math . max ( 0 , existing . launch_count ?? 0 ) + 1
113+ const hasClicked = Boolean ( existing . cta_clicked_at )
114+ const dismissedAt = existing . dismissed_at
115+ const dismissedLaunchCount = Math . max (
116+ 0 ,
117+ existing . dismissed_launch_count ?? 0 ,
118+ )
119+ const firstSeenMs = Date . parse ( firstSeenAt )
120+ const ageMs = Number . isFinite ( firstSeenMs ) ? now - firstSeenMs : 0
121+ const reachedInitialCadence =
122+ launchCount >= STAR_PROMPT_MIN_LAUNCHES &&
123+ ageMs >= STAR_PROMPT_MIN_AGE_MS
124+
125+ let shouldPrompt = false
126+ if ( ! hasClicked && reachedInitialCadence ) {
127+ if ( ! dismissedAt ) {
128+ shouldPrompt = true
129+ } else {
130+ const dismissedMs = Date . parse ( dismissedAt )
131+ const dismissedAgeMs = Number . isFinite ( dismissedMs )
132+ ? now - dismissedMs
133+ : 0
134+ const launchesSinceDismiss = Math . max (
135+ 0 ,
136+ launchCount - dismissedLaunchCount ,
137+ )
138+ shouldPrompt =
139+ launchesSinceDismiss >= STAR_PROMPT_RESHOW_LAUNCHES ||
140+ dismissedAgeMs >= STAR_PROMPT_RESHOW_MS
141+ }
142+ }
143+
144+ const nextSettings = {
145+ ...settings ,
146+ github_star_prompt : {
147+ ...existing ,
148+ first_seen_at : firstSeenAt ,
149+ launch_count : launchCount ,
150+ } ,
151+ }
152+
153+ await invoke ( 'write_settings' , { settings : nextSettings } )
154+ if ( ! cancelled && shouldPrompt ) setShowGithubStarPrompt ( true )
155+ } )
156+ . catch ( ( ) => { } )
157+ return ( ) => {
158+ cancelled = true
159+ }
160+ } , [ ] )
161+
162+ const handleGithubStarDismiss = ( ) => {
163+ setShowGithubStarPrompt ( false )
164+ void invoke ( 'read_settings' )
165+ . then ( ( settings ) => {
166+ const existing = settings . github_star_prompt ?? { }
167+ const launchCount = Math . max ( 0 , existing . launch_count ?? 0 )
168+ return invoke ( 'write_settings' , {
169+ settings : {
170+ ...settings ,
171+ github_star_prompt : {
172+ ...existing ,
173+ dismissed_at : new Date ( ) . toISOString ( ) ,
174+ dismissed_launch_count : launchCount ,
175+ dismiss_count : Math . max ( 0 , existing . dismiss_count ?? 0 ) + 1 ,
176+ } ,
177+ } ,
178+ } )
179+ } )
180+ . catch ( ( ) => { } )
181+ }
182+
183+ const handleGithubStarClick = ( ) => {
184+ setShowGithubStarPrompt ( false )
185+ void invoke ( 'open_external' , { url : GITHUB_REPO_URL } )
186+ void invoke ( 'read_settings' )
187+ . then ( ( settings ) =>
188+ invoke ( 'write_settings' , {
189+ settings : {
190+ ...settings ,
191+ github_star_prompt : {
192+ ...( settings . github_star_prompt ?? { } ) ,
193+ cta_clicked_at : new Date ( ) . toISOString ( ) ,
194+ } ,
195+ } ,
196+ } ) ,
197+ )
198+ . catch ( ( ) => { } )
199+ }
200+
95201 useEffect ( ( ) => {
96202 let cancelled = false
97203 let unlisten : ( ( ) => void ) | undefined
@@ -142,7 +248,15 @@ function AppInner() {
142248 { /* Electrobun / file URLs often use .../index.html as the pathname; SPA routes are /. */ }
143249 < Route path = "/index.html" element = { < Navigate to = "/" replace /> } />
144250 < Route path = "/main/index.html" element = { < Navigate to = "/" replace /> } />
145- < Route element = { < Layout /> } >
251+ < Route
252+ element = {
253+ < Layout
254+ showGithubStarPrompt = { showGithubStarPrompt }
255+ onDismissGithubStarPrompt = { handleGithubStarDismiss }
256+ onGithubStarPromptCta = { handleGithubStarClick }
257+ />
258+ }
259+ >
146260 < Route index element = { < Dashboard /> } />
147261 < Route path = "skills" element = { < SkillsManager /> } />
148262 < Route path = "marketplace" element = { < Marketplace /> } />
0 commit comments