1+ /**
2+ * AutoShare Manager - Handles automatic sharing with debounced saves
3+ */
4+ class AutoShareManager {
5+ constructor ( ) {
6+ this . state = 'synced' ; // hidden, synced, syncing, error
7+ this . dirty = false ;
8+ this . currentShareUrl = null ;
9+ this . currentShareId = null ;
10+ this . debounceTimer = null ;
11+ this . pendingSync = false ;
12+ this . lastPayloadHash = null ;
13+
14+ // UI elements
15+ this . panel = null ;
16+ this . linkInput = null ;
17+ this . copyButton = null ;
18+
19+ this . init ( ) ;
20+ }
21+
22+ init ( ) {
23+ this . panel = document . getElementById ( 'auto-share-panel' ) ;
24+ this . linkInput = document . getElementById ( 'share-link-input' ) ;
25+ this . copyButton = document . getElementById ( 'copy-share-link' ) ;
26+
27+ if ( this . copyButton ) {
28+ this . copyButton . addEventListener ( 'click' , ( ) => this . copyLink ( ) ) ;
29+ }
30+
31+ // Listen for workspace changes
32+ this . attachListeners ( ) ;
33+ }
34+
35+ attachListeners ( ) {
36+ // Listen for file content changes
37+ window . addEventListener ( 'workspace-content-changed' , ( ) => {
38+ this . markDirty ( ) ;
39+ } ) ;
40+
41+ // Listen for file additions/deletions
42+ window . addEventListener ( 'workspace-file-added' , ( ) => {
43+ this . markDirty ( ) ;
44+ } ) ;
45+
46+ window . addEventListener ( 'workspace-file-deleted' , ( ) => {
47+ this . markDirty ( ) ;
48+ } ) ;
49+ }
50+
51+ markDirty ( ) {
52+ this . dirty = true ;
53+
54+ // Clear existing timer
55+ if ( this . debounceTimer ) {
56+ clearTimeout ( this . debounceTimer ) ;
57+ }
58+
59+ // Set new timer for 500ms
60+ this . debounceTimer = setTimeout ( ( ) => {
61+ this . sync ( ) ;
62+ } , 500 ) ;
63+ }
64+
65+ async sync ( ) {
66+ // Skip if already syncing
67+ if ( this . state === 'syncing' ) {
68+ this . pendingSync = true ;
69+ return ;
70+ }
71+
72+ // Collect current workspace data
73+ const payload = this . collectWorkspaceData ( ) ;
74+
75+ // Check if payload has changed
76+ const payloadHash = this . hashPayload ( payload ) ;
77+ if ( payloadHash === this . lastPayloadHash ) {
78+ this . dirty = false ;
79+ return ;
80+ }
81+
82+ // Update UI to syncing state
83+ this . setState ( 'syncing' ) ;
84+
85+ try {
86+ // Send to API
87+ const response = await fetch ( '/api/install' , {
88+ method : 'POST' ,
89+ headers : {
90+ 'Content-Type' : 'application/json' ,
91+ } ,
92+ body : JSON . stringify ( { files : payload } )
93+ } ) ;
94+
95+ if ( ! response . ok ) {
96+ throw new Error ( 'Failed to sync' ) ;
97+ }
98+
99+ const data = await response . json ( ) ;
100+
101+ // Update state with new share URL
102+ this . currentShareId = data . hash ;
103+ this . currentShareUrl = `sh -c "$(curl -fsSL ${ window . location . origin } /api/install/${ data . hash } .sh)"` ;
104+ this . lastPayloadHash = payloadHash ;
105+ this . dirty = false ;
106+
107+ // Update UI to synced state
108+ this . setState ( 'synced' ) ;
109+
110+ // Check if we need another sync
111+ if ( this . pendingSync || this . dirty ) {
112+ this . pendingSync = false ;
113+ setTimeout ( ( ) => this . sync ( ) , 100 ) ;
114+ }
115+
116+ } catch ( error ) {
117+ console . error ( 'Auto-share sync failed:' , error ) ;
118+ this . setState ( 'error' ) ;
119+
120+ // Retry after a delay if still dirty
121+ if ( this . dirty ) {
122+ setTimeout ( ( ) => this . sync ( ) , 2000 ) ;
123+ }
124+ }
125+ }
126+
127+ collectWorkspaceData ( ) {
128+ const allFiles = { } ;
129+
130+ // Helper function to collect files from tree structure
131+ function collectFilesFromTree ( nodes , collected ) {
132+ nodes . forEach ( node => {
133+ if ( node . type === 'file' ) {
134+ const state = window . workspaceManager ?. getState ( ) ;
135+ if ( state ?. files [ node . path ] ) {
136+ collected [ node . path ] = state . files [ node . path ] ;
137+ }
138+ } else if ( node . type === 'folder' && node . children ) {
139+ collectFilesFromTree ( node . children , collected ) ;
140+ }
141+ } ) ;
142+ }
143+
144+ // Collect files from dynamic tree
145+ if ( window . generateFileTreeData ) {
146+ const fileTreeData = window . generateFileTreeData ( ) ;
147+ collectFilesFromTree ( fileTreeData , allFiles ) ;
148+ }
149+
150+ return allFiles ;
151+ }
152+
153+ hashPayload ( payload ) {
154+ // Simple hash for change detection
155+ return JSON . stringify ( payload ) ;
156+ }
157+
158+ setState ( newState ) {
159+ this . state = newState ;
160+
161+ switch ( newState ) {
162+ case 'hidden' :
163+ if ( this . panel ) this . panel . style . display = 'none' ;
164+ break ;
165+
166+ case 'synced' :
167+ if ( this . panel ) this . panel . style . display = 'flex' ;
168+ if ( this . linkInput ) {
169+ this . linkInput . value = this . currentShareUrl || 'No share link yet' ;
170+ this . linkInput . classList . remove ( 'opacity-50' ) ;
171+ this . linkInput . disabled = false ;
172+ }
173+ if ( this . copyButton ) {
174+ this . copyButton . disabled = false ;
175+ this . copyButton . classList . remove ( 'opacity-50' ) ;
176+ }
177+ break ;
178+
179+ case 'syncing' :
180+ if ( this . panel ) this . panel . style . display = 'flex' ;
181+ if ( this . linkInput ) {
182+ this . linkInput . classList . add ( 'opacity-50' ) ;
183+ this . linkInput . disabled = true ;
184+ }
185+ if ( this . copyButton ) {
186+ this . copyButton . disabled = true ;
187+ this . copyButton . classList . add ( 'opacity-50' ) ;
188+ }
189+ break ;
190+
191+ case 'error' :
192+ if ( this . panel ) this . panel . style . display = 'flex' ;
193+ // Keep last good link visible but indicate error somehow
194+ if ( this . linkInput && this . currentShareUrl ) {
195+ this . linkInput . value = this . currentShareUrl ;
196+ this . linkInput . classList . remove ( 'opacity-50' ) ;
197+ this . linkInput . disabled = false ;
198+ }
199+ if ( this . copyButton && this . currentShareUrl ) {
200+ this . copyButton . disabled = false ;
201+ this . copyButton . classList . remove ( 'opacity-50' ) ;
202+ }
203+ break ;
204+ }
205+ }
206+
207+ async copyLink ( ) {
208+ if ( ! this . currentShareUrl ) return ;
209+
210+ try {
211+ await navigator . clipboard . writeText ( this . currentShareUrl ) ;
212+
213+ // Show feedback
214+ const originalText = this . copyButton . textContent ;
215+ this . copyButton . textContent = 'Copied!' ;
216+ this . copyButton . classList . remove ( 'bg-cyan-400' ) ;
217+ this . copyButton . classList . add ( 'bg-green-400' ) ;
218+
219+ setTimeout ( ( ) => {
220+ this . copyButton . textContent = originalText ;
221+ this . copyButton . classList . remove ( 'bg-green-400' ) ;
222+ this . copyButton . classList . add ( 'bg-cyan-400' ) ;
223+ } , 2000 ) ;
224+ } catch ( error ) {
225+ console . error ( 'Failed to copy:' , error ) ;
226+ }
227+ }
228+ }
229+
230+ // Initialize when DOM is ready
231+ document . addEventListener ( 'DOMContentLoaded' , function ( ) {
232+ // Wait a bit for workspace to be ready
233+ setTimeout ( ( ) => {
234+ window . autoShareManager = new AutoShareManager ( ) ;
235+ } , 200 ) ;
236+ } ) ;
0 commit comments