Skip to content

Commit ec2059e

Browse files
authored
feat: add dynamic intellisense for parentId/id values in YAML editor (#72)
1 parent 63bfaad commit ec2059e

File tree

1 file changed

+117
-0
lines changed
  • Jellyfin.Plugin.Streamyfin/Pages/YamlEditor

1 file changed

+117
-0
lines changed

Jellyfin.Plugin.Streamyfin/Pages/YamlEditor/index.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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(parentId|id)\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

Comments
 (0)