@@ -19,6 +19,118 @@ export default function (view, params) {
1919 const Page = {
2020 editor : null ,
2121 yaml : null ,
22+ parentIdProvider : null ,
23+ parentIdSuggestions : null ,
24+ // Fetch libraries and collections from Jellyfin and map to Monaco suggestions
25+ loadParentIdSuggestions : async function ( ) {
26+ try {
27+ const userId = await window . ApiClient . getCurrentUserId ?. ( ) ?? null ;
28+
29+ // Build URLs using ApiClient to preserve base path and auth
30+ // Prefer user views over raw media folders for broader compatibility
31+ const libsUrl = userId
32+ ? window . ApiClient . getUrl ( `Users/${ userId } /Views` )
33+ : window . ApiClient . getUrl ( 'Library/MediaFolders' ) ;
34+ const collectionsUrl = userId
35+ ? window . ApiClient . getUrl ( `Users/${ userId } /Items` , {
36+ IncludeItemTypes : 'BoxSet' ,
37+ Recursive : true ,
38+ SortBy : 'SortName' ,
39+ SortOrder : 'Ascending'
40+ } )
41+ : null ;
42+
43+ // Fetch in parallel using ApiClient.ajax to include auth
44+ const [ libsRes , colRes ] = await Promise . all ( [
45+ window . ApiClient . ajax ( { type : 'GET' , url : libsUrl , contentType : 'application/json' } ) ,
46+ collectionsUrl
47+ ? window . ApiClient . ajax ( { type : 'GET' , url : collectionsUrl , contentType : 'application/json' } )
48+ : Promise . resolve ( null )
49+ ] ) ;
50+
51+ const libsJson = libsRes ? libsRes : { Items : [ ] } ;
52+ const colsJson = colRes ? colRes : { Items : [ ] } ;
53+
54+ // Normalize arrays (Jellyfin usually returns { Items: [...] })
55+ const libraries = Array . isArray ( libsJson ?. Items ) ? libsJson . Items : ( Array . isArray ( libsJson ) ? libsJson : [ ] ) ;
56+ const collections = Array . isArray ( colsJson ?. Items ) ? colsJson . Items : ( Array . isArray ( colsJson ) ? colsJson : [ ] ) ;
57+
58+ const libSuggestions = libraries
59+ . filter ( i => i ?. Id && i ?. Name )
60+ . map ( i => ( {
61+ label : `${ i . Name } (${ i . Id } )` ,
62+ kind : monaco . languages . CompletionItemKind . Value ,
63+ insertText : i . Id ,
64+ detail : 'Library folder' ,
65+ documentation : i . Path ? `Path: ${ i . Path } ` : undefined
66+ } ) ) ;
67+
68+ const colSuggestions = collections
69+ . filter ( i => i ?. Id && i ?. Name )
70+ . map ( i => ( {
71+ label : `${ i . Name } (${ i . Id } )` ,
72+ kind : monaco . languages . CompletionItemKind . Value ,
73+ insertText : i . Id ,
74+ detail : 'Collection' ,
75+ documentation : i . Overview || undefined
76+ } ) ) ;
77+
78+ Page . parentIdSuggestions = [ ...libSuggestions , ...colSuggestions ] ;
79+ } catch ( e ) {
80+ console . warn ( 'Failed to load parentId suggestions' , e ) ;
81+ Page . parentIdSuggestions = [ ] ;
82+ }
83+ } ,
84+ // Register a YAML completion provider that triggers when value for key 'parentId' is being edited
85+ registerParentIdProvider : function ( ) {
86+ if ( Page . parentIdProvider ) return ; // avoid duplicates
87+
88+ Page . parentIdProvider = monaco . languages . registerCompletionItemProvider ( 'yaml' , {
89+ triggerCharacters : [ ':' , ' ' , '-' , '\n' , '"' , "'" ] ,
90+ provideCompletionItems : async ( model , position ) => {
91+ try {
92+ const line = model . getLineContent ( position . lineNumber ) ;
93+ const beforeCursor = line . substring ( 0 , position . column - 1 ) ;
94+ // Heuristic: we're in a value position for a key named 'parentId'
95+ // Match lines like: "parentId: |" or "id: |" with optional indent or list dash
96+ const isTargetLine = / ( ^ | \s | - ) \b ( p a r e n t I d | i d ) \b \s * : \s * [ ^ # ] * $ / i. test ( beforeCursor ) ;
97+ if ( ! isTargetLine ) {
98+ return { suggestions : [ ] } ;
99+ }
100+
101+ if ( ! Array . isArray ( Page . parentIdSuggestions ) ) {
102+ await Page . loadParentIdSuggestions ( ) ;
103+ }
104+
105+ // Compute replacement range: from word start to cursor
106+ const word = model . getWordUntilPosition ( position ) ;
107+ const startColFromColon = ( ( ) => {
108+ const idx = beforeCursor . lastIndexOf ( ':' ) ;
109+ if ( idx === - 1 ) return word . startColumn ;
110+ let start = idx + 1 ; // first char after colon
111+ // skip spaces
112+ while ( start < beforeCursor . length && beforeCursor . charAt ( start ) === ' ' ) start ++ ;
113+ // skip optional opening quotes
114+ while ( start < beforeCursor . length && ( beforeCursor . charAt ( start ) === '"' || beforeCursor . charAt ( start ) === "'" ) ) start ++ ;
115+ // Monaco columns are 1-based
116+ return start + 1 ;
117+ } ) ( ) ;
118+ const range = new monaco . Range (
119+ position . lineNumber ,
120+ Math . max ( 1 , startColFromColon ) ,
121+ position . lineNumber ,
122+ position . column
123+ ) ;
124+
125+ const suggestions = Page . parentIdSuggestions . map ( s => ( { ...s , range } ) ) ;
126+ return { suggestions } ;
127+ } catch ( err ) {
128+ console . warn ( 'parentId provider error' , err ) ;
129+ return { suggestions : [ ] } ;
130+ }
131+ }
132+ } ) ;
133+ } ,
22134 saveConfig : function ( e ) {
23135 e . preventDefault ( ) ;
24136 shared . setYamlConfig ( Page . editor . getModel ( ) . getValue ( ) )
@@ -76,6 +188,9 @@ export default function (view, params) {
76188 saveBtn ( ) . addEventListener ( "click" , Page . saveConfig ) ;
77189 exampleBtn ( ) . addEventListener ( "click" , Page . resetConfig ) ;
78190
191+ // Register dynamic intellisense for parentId values
192+ Page . registerParentIdProvider ( ) ;
193+
79194 if ( shared . getConfig ( ) && Page . editor == null ) {
80195 Page . loadConfig ( shared . getConfig ( ) ) ;
81196 }
@@ -102,8 +217,10 @@ export default function (view, params) {
102217 console . log ( "Hiding" )
103218 Page ?. editor ?. dispose ( )
104219 Page ?. yaml ?. dispose ( )
220+ Page ?. parentIdProvider ?. dispose ?. ( )
105221 Page . editor = undefined ;
106222 Page . yaml = undefined ;
223+ Page . parentIdProvider = undefined ;
107224 monaco ?. editor ?. getModels ?. ( ) ?. forEach ( model => model . dispose ( ) )
108225 monaco ?. editor ?. getEditors ?. ( ) ?. forEach ( editor => editor . dispose ( ) ) ;
109226 } ) ;
0 commit comments