1- /* global chrome */
21
32import {
43 UIManager ,
@@ -30,6 +29,9 @@ class ScriptEditor {
3029 lintingEnabled : localStorage . getItem ( "lintingEnabled" ) === "true" ,
3130 isAutosaveEnabled : localStorage . getItem ( "autosaveEnabled" ) === "true" ,
3231 autosaveTimeout : null ,
32+ headerSyncTimeout : null ,
33+ sidebarSyncTimeout : null ,
34+ isUpdatingFromSidebar : false ,
3335 hasUserInteraction : false ,
3436 codeEditor : null ,
3537 } ;
@@ -125,34 +127,156 @@ class ScriptEditor {
125127 ) ;
126128 }
127129
128- _debouncedSave ( force = false ) {
129- // Clear existing timeout
130+ _debouncedSave ( ) {
130131 if ( this . state . autosaveTimeout ) {
131132 clearTimeout ( this . state . autosaveTimeout ) ;
132- this . state . autosaveTimeout = null ;
133133 }
134134
135- if ( this . state . isAutosaveEnabled || force ) {
136- this . state . autosaveTimeout = setTimeout ( async ( ) => {
137- if ( this . state . hasUnsavedChanges && this . state . codeEditor ) {
135+ this . state . autosaveTimeout = setTimeout ( async ( ) => {
136+ if ( this . state . hasUnsavedChanges ) {
137+ if ( this . state . isEditMode ) {
138138 try {
139139 await this . saveScript ( true ) ;
140-
141- if ( ! force ) {
142- setTimeout ( ( ) => {
143- if ( ! this . state . hasUnsavedChanges ) {
144- this . ui . clearStatusMessage ( ) ;
145- }
146- } , 2000 ) ;
147- }
148140 } catch ( error ) {
149- console . error ( "Error during autosave :" , error ) ;
150- if ( ! force ) {
141+ console . error ( "Autosave failed :" , error ) ;
142+ if ( this . ui && this . ui . showStatusMessage ) {
151143 this . ui . showStatusMessage ( "Autosave failed" , "error" ) ;
152144 }
153145 }
154146 }
155- } , this . config . AUTOSAVE_DELAY ) ;
147+ }
148+ } , this . config . AUTOSAVE_DELAY ) ;
149+ }
150+
151+ _debouncedHeaderSync ( ) {
152+ if ( this . state . headerSyncTimeout ) {
153+ clearTimeout ( this . state . headerSyncTimeout ) ;
154+ }
155+
156+ this . state . headerSyncTimeout = setTimeout ( ( ) => {
157+ // Skip if we're currently updating from sidebar to prevent loops
158+ if ( ! this . state . isUpdatingFromSidebar ) {
159+ this . syncHeaderToSidebar ( ) ;
160+ }
161+ } , 500 ) ; // 500ms debounce
162+ }
163+
164+ _debouncedSidebarSync ( ) {
165+ if ( this . state . sidebarSyncTimeout ) {
166+ clearTimeout ( this . state . sidebarSyncTimeout ) ;
167+ }
168+
169+ this . state . sidebarSyncTimeout = setTimeout ( ( ) => {
170+ this . syncSidebarToHeader ( ) ;
171+ } , 500 ) ; // 500ms debounce
172+ }
173+
174+ syncHeaderToSidebar ( ) {
175+ try {
176+ const currentCode = this . codeEditorManager . getValue ( ) ;
177+ const headerMatch = currentCode . match ( / \/ \/ = = U s e r S c r i p t = = [ \s \S ] * ?\/ \/ = = \/ U s e r S c r i p t = = / ) ;
178+
179+ if ( ! headerMatch ) return ; // No header found
180+
181+ const metadata = parseUserScriptMetadata ( headerMatch [ 0 ] ) ;
182+
183+ // Overwrite sidebar fields with header metadata
184+ if ( metadata . name ) this . elements . scriptName . value = metadata . name ;
185+ if ( metadata . author ) this . elements . scriptAuthor . value = metadata . author ;
186+ if ( metadata . version ) this . elements . scriptVersion . value = metadata . version ;
187+ if ( metadata . description ) this . elements . scriptDescription . value = metadata . description ;
188+ if ( metadata . license ) this . elements . scriptLicense . value = metadata . license ;
189+ if ( metadata . icon ) this . elements . scriptIcon . value = metadata . icon ;
190+ if ( metadata . runAt ) this . elements . runAt . value = metadata . runAt . replace ( / - / g, '_' ) ;
191+
192+ // Update target URLs (replace list with header values)
193+ if ( this . elements . urlList ) {
194+ this . elements . urlList . innerHTML = '' ;
195+ }
196+ if ( Array . isArray ( metadata . matches ) && metadata . matches . length > 0 ) {
197+ metadata . matches . forEach ( match => {
198+ this . ui . addUrlToList ( match ) ;
199+ } ) ;
200+ }
201+
202+ // Reset and update GM API checkboxes to reflect @grant
203+ if ( this . gmApiDefinitions ) {
204+ Object . values ( this . gmApiDefinitions ) . forEach ( def => {
205+ const el = this . elements [ def . el ] ;
206+ if ( el ) el . checked = false ;
207+ } ) ;
208+ }
209+ if ( metadata . gmApis ) {
210+ Object . keys ( metadata . gmApis ) . forEach ( apiFlag => {
211+ const element = this . elements [ apiFlag ] ;
212+ if ( element ) {
213+ element . checked = ! ! metadata . gmApis [ apiFlag ] ;
214+ }
215+ } ) ;
216+ }
217+ this . updateApiCount ( ) ;
218+ // Update dependent sections visibility after grants change
219+ this . toggleResourcesSection (
220+ this . elements . gmGetResourceText ?. checked || this . elements . gmGetResourceURL ?. checked
221+ ) ;
222+ this . toggleRequiredScriptsSection ( ) ;
223+
224+ // Update resources (replace list with header values)
225+ if ( this . elements . resourceList ) {
226+ this . elements . resourceList . innerHTML = '' ;
227+ }
228+ if ( Array . isArray ( metadata . resources ) && metadata . resources . length > 0 ) {
229+ metadata . resources . forEach ( resource => {
230+ this . ui . addResourceToList ( resource . name , resource . url ) ;
231+ } ) ;
232+ }
233+
234+ // Update required scripts from @require (replace list)
235+ if ( this . elements . requireList ) {
236+ this . elements . requireList . innerHTML = '' ;
237+ }
238+ if ( Array . isArray ( metadata . requires ) && metadata . requires . length > 0 ) {
239+ metadata . requires . forEach ( url => this . ui . addRequireToList ( url ) ) ;
240+ }
241+
242+ } catch {
243+ // Silently fail - don't spam console during normal editing
244+ }
245+ }
246+
247+ syncSidebarToHeader ( ) {
248+ try {
249+ const currentCode = this . codeEditorManager . getValue ( ) ;
250+ const headerMatch = currentCode . match ( / \/ \/ = = U s e r S c r i p t = = [ \s \S ] * ?\/ \/ = = \/ U s e r S c r i p t = = / ) ;
251+
252+ // Generate new header from current sidebar data
253+ const scriptData = this . gatherScriptData ( ) ;
254+ const newMetadata = buildTampermonkeyMetadata ( scriptData ) ;
255+
256+ let newCode ;
257+ if ( headerMatch ) {
258+ // Replace existing header
259+ newCode = currentCode . replace ( headerMatch [ 0 ] , newMetadata ) ;
260+ } else {
261+ // Insert header at the beginning
262+ newCode = newMetadata + '\n\n' + currentCode ;
263+ }
264+
265+ // Only update if the code actually changed to avoid infinite loops
266+ if ( newCode !== currentCode ) {
267+ // Set flag to prevent header sync during this update
268+ this . state . isUpdatingFromSidebar = true ;
269+
270+ this . codeEditorManager . setValue ( newCode ) ;
271+
272+ // Reset flag after a short delay to allow future header syncing
273+ setTimeout ( ( ) => {
274+ this . state . isUpdatingFromSidebar = false ;
275+ } , 100 ) ;
276+ }
277+
278+ } catch {
279+ // Silently fail - don't spam console during normal editing
156280 }
157281 }
158282
@@ -218,7 +342,8 @@ class ScriptEditor {
218342 "addRequireBtn" ,
219343 "requireURL" ,
220344 "requireList" ,
221- "helpButton"
345+ "helpButton" ,
346+ "generateHeaderBtn"
222347 ] ;
223348
224349 const elements = { } ;
@@ -260,6 +385,8 @@ class ScriptEditor {
260385 if ( this . state . isAutosaveEnabled ) {
261386 this . _debouncedSave ( ) ;
262387 }
388+ // Check for header changes and update sidebar
389+ this . _debouncedHeaderSync ( ) ;
263390 } ) ;
264391 this . codeEditorManager . setImportCallback ( ( importData ) => this . handleScriptImport ( importData ) ) ;
265392 this . codeEditorManager . setStatusCallback ( ( message , type ) => {
@@ -491,6 +618,12 @@ class ScriptEditor {
491618 e . preventDefault ( ) ;
492619 this . saveScript ( ) ;
493620 } ) ;
621+
622+ // Add click listener to generate header button
623+ this . elements . generateHeaderBtn ?. addEventListener ( 'click' , ( e ) => {
624+ e . preventDefault ( ) ;
625+ this . generateTampermonkeyHeader ( ) ;
626+ } ) ;
494627
495628 // Setup UI callbacks - UIManager initializes everything in constructor, no init() method needed
496629 const callbacks = {
@@ -514,15 +647,28 @@ class ScriptEditor {
514647
515648 // Setup additional UI components that need callbacks
516649 this . ui . setupSettingsModal ( callbacks ) ;
517- this . ui . setupUrlManagement ( { markAsUnsaved : ( ) => this . markAsUnsaved ( ) } ) ;
518- this . ui . setupResourceManagement ( callbacks ) ;
650+ this . ui . setupUrlManagement ( {
651+ markAsUnsaved : ( ) => {
652+ this . markAsUnsaved ( ) ;
653+ this . _debouncedSidebarSync ( ) ;
654+ }
655+ } ) ;
656+ this . ui . setupResourceManagement ( {
657+ ...callbacks ,
658+ markAsUnsaved : ( ) => {
659+ this . markAsUnsaved ( ) ;
660+ this . _debouncedSidebarSync ( ) ;
661+ }
662+ } ) ;
519663
520664 // Add both change and input events for better responsiveness
521665 const handleChange = ( ) => {
522666 this . markAsDirty ( ) ;
523667 if ( this . state . isAutosaveEnabled ) {
524668 this . _debouncedSave ( ) ;
525669 }
670+ // Sync sidebar changes to header
671+ this . _debouncedSidebarSync ( ) ;
526672 } ;
527673
528674 // Form inputs that should trigger change detection
@@ -586,12 +732,17 @@ class ScriptEditor {
586732 sidebarIconBtns . forEach ( btn => btn . classList . remove ( 'active' ) ) ;
587733 this . elements . sidebarPanels . forEach ( panel => panel . classList . remove ( 'active' ) ) ;
588734 this . elements . sidebarContentArea . style . display = 'none' ;
589- this . elements . sidebarContentArea . style . width = '0' ;
590735
591736 sidebarIconBtns . forEach ( btn => {
592737 btn . addEventListener ( 'click' , ( ) => {
593- const section = btn . dataset . section ;
738+ const section = btn . getAttribute ( 'data-section' ) ;
739+
740+ // Skip if this is the generate header button (no data-section)
741+ if ( ! section ) return ;
742+
594743 const panel = document . getElementById ( `${ section } -panel` ) ;
744+ if ( ! panel ) return ;
745+
595746 const isCurrentlyActive = btn . classList . contains ( 'active' ) ;
596747
597748 if ( isCurrentlyActive ) {
@@ -969,6 +1120,43 @@ class ScriptEditor {
9691120 this . ui . showStatusMessage ( 'Export failed' , 'error' ) ;
9701121 }
9711122 }
1123+
1124+ /**
1125+ * Generate Tampermonkey-style header and insert at top of code editor
1126+ */
1127+ generateTampermonkeyHeader ( ) {
1128+ try {
1129+ const scriptData = this . gatherScriptData ( ) ;
1130+ const metadata = buildTampermonkeyMetadata ( scriptData ) ;
1131+
1132+ // Get current code content
1133+ const currentCode = this . codeEditorManager . getValue ( ) ;
1134+
1135+ // Check if there's already a userscript header
1136+ const existingHeaderMatch = currentCode . match ( / \/ \/ = = U s e r S c r i p t = = [ \s \S ] * ?\/ \/ = = \/ U s e r S c r i p t = = / ) ;
1137+
1138+ let newCode ;
1139+ if ( existingHeaderMatch ) {
1140+ // Replace existing header
1141+ newCode = currentCode . replace ( existingHeaderMatch [ 0 ] , metadata ) ;
1142+ this . ui . showStatusMessage ( 'Metadata updated' , 'success' ) ;
1143+ } else {
1144+ // Insert header at the beginning
1145+ newCode = metadata + '\n\n' + currentCode ;
1146+ this . ui . showStatusMessage ( 'Metadata generated' , 'success' ) ;
1147+ }
1148+
1149+ // Set the new code content
1150+ this . codeEditorManager . setValue ( newCode ) ;
1151+
1152+ // Mark as dirty to indicate changes
1153+ this . markAsDirty ( ) ;
1154+
1155+ } catch ( err ) {
1156+ console . error ( 'Generate header failed:' , err ) ;
1157+ this . ui . showStatusMessage ( 'Failed to generate header' , 'error' ) ;
1158+ }
1159+ }
9721160}
9731161
9741162// Main init for editor
0 commit comments