main
html 473 lines 20.3 KB
Raw
1 <html>
2 <head>
3 <title>Plugins</title>
4 <script type="module">
5 import { store } from "/components/plugins/list/pluginListStore.js";
6 </script>
7 </head>
8
9 <body>
10 <div x-data>
11 <template x-if="$store.pluginListStore">
12 <div x-init="$store.pluginListStore.init && $store.pluginListStore.init()">
13
14 <ul class="nav nav-tabs" id="plugin-list-tabs" role="tablist">
15 <li class="nav-item" role="presentation">
16 <button class="nav-link"
17 :class="{ active: $store.pluginListStore.activeTab === 'custom' }"
18 id="plugins-custom-tab"
19 type="button"
20 role="tab"
21 aria-controls="plugins-custom"
22 :aria-selected="$store.pluginListStore.activeTab === 'custom'"
23 @click="$store.pluginListStore.setTab('custom')">
24 Custom
25 </button>
26 </li>
27 <li class="nav-item" role="presentation">
28 <button class="nav-link"
29 :class="{ active: $store.pluginListStore.activeTab === 'builtin' }"
30 id="plugins-builtin-tab"
31 type="button"
32 role="tab"
33 aria-controls="plugins-builtin"
34 :aria-selected="$store.pluginListStore.activeTab === 'builtin'"
35 @click="$store.pluginListStore.setTab('builtin')">
36 Builtin
37 </button>
38 </li>
39 <li class="nav-item" role="presentation">
40 <button class="nav-link"
41 :class="{ active: $store.pluginListStore.activeTab === 'pluginHub' }"
42 id="plugins-plugin-hub-tab"
43 type="button"
44 role="tab"
45 aria-controls="plugins-plugin-hub"
46 :aria-selected="$store.pluginListStore.activeTab === 'pluginHub'"
47 @click="$store.pluginListStore.setTab('pluginHub')">
48 <x-icon class="pi-tab-icon" name="store"></x-icon> Browse
49 </button>
50 </li>
51 </ul>
52
53 <template x-if="$store.pluginListStore.activeTab !== 'pluginHub'">
54 <div>
55 <div class="plugins-toolbar">
56 <div class="plugins-toolbar-search">
57 <input type="text"
58 class="plugins-search-input"
59 placeholder="Search plugins by name or description..."
60 x-model="$store.pluginListStore.searchQuery">
61 </div>
62 <div class="plugins-toolbar-actions">
63 <button type="button"
64 class="button"
65 title="Refresh"
66 @click="$store.pluginListStore.refresh()"
67 :disabled="$store.pluginListStore.loading">
68 <x-icon class="icon" name="refresh"></x-icon> Refresh
69 </button>
70
71 <x-extension id="plugins-list-header-buttons"></x-extension>
72 </div>
73 </div>
74
75 <div x-show="$store.pluginListStore.loading" class="loading">
76 <span>Loading plugins...</span>
77 </div>
78
79 <div class="plugins-list">
80 <template x-for="plugin in (($store.pluginListStore.plugins || []).filter((plugin) => {
81 const q = (($store.pluginListStore.searchQuery || '').trim().toLowerCase());
82 if (!q) return true;
83 const name = (plugin.display_name || plugin.name || '').toLowerCase();
84 const description = (plugin.description || '').toLowerCase();
85 return name.includes(q) || description.includes(q);
86 }))" :key="plugin.path || plugin.name">
87 <div class="plugin-card">
88 <div class="plugin-header">
89 <template x-if="plugin.thumbnail_url">
90 <img class="plugin-thumb"
91 :src="plugin.thumbnail_url"
92 :alt="plugin.display_name || plugin.name"
93 loading="lazy">
94 </template>
95 <div class="plugin-heading">
96 <div class="plugin-title" x-text="plugin.display_name || plugin.name || '(unnamed plugin)'"></div>
97 <code class="plugin-path" x-text="plugin.path"></code>
98 </div>
99 <div class="plugin-actions" x-data="{ actionsOpen: false }">
100 <template x-if="plugin.has_main_screen">
101 <button type="button"
102 class="button"
103 title="Open"
104 @click="$store.pluginListStore.openPlugin(plugin)">
105 <x-icon class="icon" name="dashboard"></x-icon> Open
106 </button>
107 </template>
108 <template x-if="$store.pluginListStore.canOpenPluginConfig(plugin)">
109 <button type="button"
110 class="button"
111 title="Config"
112 @click="$store.pluginListStore.openPluginConfig(plugin)">
113 <x-icon class="icon" name="settings"></x-icon> Config
114 </button>
115 </template>
116 <template x-if="plugin.has_execute_script">
117 <button type="button"
118 class="button"
119 title="Execute"
120 @click="$store.pluginListStore.openPluginExecute(plugin)">
121 <x-icon class="icon" name="terminal"></x-icon> Execute
122 </button>
123 </template>
124 <button type="button"
125 class="button icon-button"
126 title="Info"
127 aria-label="Info"
128 @click="$store.pluginListStore.openPluginInfo(plugin)">
129 <x-icon class="icon" name="info"></x-icon>
130 </button>
131 <div class="dropdown plugin-actions-dropdown"
132 @click.outside="actionsOpen = false"
133 @keydown.escape.window="if (actionsOpen) actionsOpen = false">
134 <button type="button"
135 class="btn-icon-action dropdown-trigger"
136 title="More actions"
137 aria-label="More actions"
138 @click.stop="actionsOpen = !actionsOpen"
139 :aria-expanded="actionsOpen.toString()">
140 <x-icon name="more_vert"></x-icon>
141 </button>
142 <div class="dropdown-menu"
143 x-show="actionsOpen"
144 x-transition
145 style="display: none;">
146 <x-extension id="plugins-list-dropdown-start"></x-extension>
147 <template x-if="plugin.has_readme">
148 <button type="button"
149 class="dropdown-item"
150 title="README"
151 @click="$store.pluginListStore.openPluginDoc(plugin, 'readme'); actionsOpen = false">
152 <x-icon name="description"></x-icon>
153 <span>README</span>
154 </button>
155 </template>
156 <template x-if="plugin.has_license">
157 <button type="button"
158 class="dropdown-item"
159 title="License"
160 @click="$store.pluginListStore.openPluginDoc(plugin, 'license'); actionsOpen = false">
161 <x-icon name="gavel"></x-icon>
162 <span>License</span>
163 </button>
164 </template>
165 <x-extension id="plugins-list-dropdown-end"></x-extension>
166 <template x-if="plugin.is_custom">
167 <button type="button"
168 class="dropdown-item plugin-dropdown-delete"
169 title="Delete"
170 @click="$confirmClick($event, () => { $store.pluginListStore.deletePlugin(plugin); actionsOpen = false; })">
171 <x-icon name="delete"></x-icon>
172 <span>Delete</span>
173 </button>
174 </template>
175 </div>
176 </div>
177 </div>
178 </div>
179
180 <div class="plugin-footer-row">
181 <div class="plugin-description" x-text="plugin.description || 'No description provided.'"></div>
182 <div class="plugin-status-group">
183 <label class="toggle plugin-status-toggle"
184 :class="{ 'disabled-appearance': plugin.always_enabled || $store.pluginListStore.loading }"
185 :title="plugin.always_enabled ? 'Always enabled' : ($store.pluginListStore.isPluginEnabled(plugin) ? 'Disable plugin' : 'Enable plugin')">
186 <input type="checkbox"
187 :checked="$store.pluginListStore.isPluginEnabled(plugin)"
188 :disabled="plugin.always_enabled || $store.pluginListStore.loading"
189 @change="$store.pluginListStore.updateToggle(plugin, $event.target.checked)"
190 @click.stop>
191 <span class="toggler"></span>
192 </label>
193 <span class="plugin-status-text" x-text="$store.pluginListStore.toggleStatusLabel(plugin)"></span>
194 </div>
195 </div>
196 </div>
197 </template>
198 </div>
199 </div>
200 </template>
201
202 <template x-if="$store.pluginListStore.activeTab === 'pluginHub'">
203 <x-component path="/plugins/_plugin_installer/webui/install-index.html"></x-component>
204 </template>
205 </div>
206 </template>
207 </div>
208
209 <style>
210 .nav {
211 display: flex;
212 padding-left: 0;
213 margin: 0 0 1rem 0;
214 list-style: none;
215 border-bottom: 1px solid var(--color-border);
216 gap: 0.25rem;
217 }
218
219 .nav-link {
220 font-family: "Rubik", Arial, Helvetica, sans-serif;
221 border: 1px solid transparent;
222 border-top-left-radius: 4px;
223 border-top-right-radius: 4px;
224 padding: 0.4rem 0.7rem;
225 background: transparent;
226 color: var(--color-text-secondary);
227 cursor: pointer;
228 display: inline-flex;
229 align-items: center;
230 gap: 0.3rem;
231 }
232
233 .nav-link.active {
234 color: var(--color-text-primary);
235 border-color: var(--color-border);
236 border-bottom-color: transparent;
237 background: var(--color-background);
238 }
239
240 .pi-tab-icon {
241 font-size: 1.1rem;
242 }
243
244 .plugins-toolbar {
245 display: flex;
246 align-items: center;
247 gap: 0.75rem;
248 flex-wrap: wrap;
249 margin-top: 0.5rem;
250 margin-bottom: 0.75rem;
251 }
252
253 .plugins-toolbar-search {
254 flex: 1 1 22rem;
255 min-width: 14rem;
256 }
257
258 .plugins-search-input {
259 width: 100%;
260 padding: 0.4rem 0.7rem;
261 border: 1px solid var(--color-border);
262 border-radius: 4px;
263 background: var(--color-background);
264 color: var(--color-text-primary);
265 font-family: "Rubik", Arial, Helvetica, sans-serif;
266 font-size: 0.95rem;
267 }
268
269 .plugins-search-input:focus {
270 outline: none;
271 border-color: var(--color-primary);
272 }
273
274 .plugins-toolbar-actions {
275 margin-left: auto;
276 display: flex;
277 flex: 0 0 auto;
278 gap: var(--spacing-sm);
279 }
280
281 .plugins-list {
282 margin-top: 0.25rem;
283 }
284
285 .plugin-card {
286 border: 1px solid var(--color-border);
287 border-radius: 10px;
288 padding: 0.75rem;
289 margin-top: 0.75rem;
290 background: var(--color-background);
291 }
292
293 .plugin-header {
294 display: flex;
295 align-items: flex-start;
296 justify-content: space-between;
297 gap: 1rem;
298 flex-wrap: nowrap;
299 min-width: 0;
300 }
301
302 .plugin-heading {
303 display: flex;
304 flex-direction: column;
305 min-width: 0;
306 flex: 1 1 auto;
307 }
308
309 .plugin-thumb {
310 width: 44px;
311 height: 44px;
312 border-radius: 0.5rem;
313 object-fit: contain;
314 flex-shrink: 0;
315 background: var(--color-panel);
316 border: 1px solid var(--color-border);
317 }
318
319 .plugin-title {
320 font-weight: 600;
321 font-size: 1rem;
322 min-width: 0;
323 flex: 1 1 auto;
324 }
325
326 .plugin-actions {
327 display: flex;
328 align-items: center;
329 gap: 0.5rem;
330 padding-bottom: var(--spacing-sm);
331 flex-wrap: nowrap;
332 min-width: 0;
333 flex: 0 0 auto;
334 max-width: 100%;
335 justify-content: flex-end;
336 }
337
338 .plugin-actions .button {
339 padding: 0.3rem 0.6rem;
340 font-size: 0.9rem;
341 }
342
343 .plugin-actions .button .icon {
344 font-size: 1.1rem;
345 }
346
347 .plugin-actions-dropdown {
348 flex: 0 0 auto;
349 }
350
351 .plugin-actions-dropdown:not(:has(.dropdown-menu .dropdown-item)) {
352 display: none;
353 }
354
355 .plugin-actions-dropdown .dropdown-menu {
356 margin-top: 0;
357 top: calc(100% - 1px);
358 }
359
360 .plugin-actions-dropdown .plugin-dropdown-delete,
361 .plugin-actions-dropdown .plugin-dropdown-delete .material-symbols-outlined {
362 color: var(--color-accent);
363 opacity: 1;
364 }
365
366 .plugin-actions-dropdown .plugin-dropdown-delete:hover {
367 background-color: color-mix(in srgb, var(--color-accent) 10%, transparent);
368 }
369
370 .plugin-footer-row {
371 display: flex;
372 align-items: center;
373 justify-content: space-between;
374 gap: 1rem;
375 margin-top: 0.5rem;
376 }
377
378 .plugin-status-group {
379 display: flex;
380 align-items: center;
381 gap: 0.5rem;
382 flex: 0 0 auto;
383 }
384
385 .plugin-status-toggle {
386 width: 48px;
387 height: 28px;
388 flex: 0 0 48px;
389 }
390
391 .plugin-status-toggle .toggler {
392 border-radius: 999px;
393 }
394
395 .plugin-status-toggle .toggler:before {
396 width: 20px;
397 height: 20px;
398 left: 4px;
399 bottom: 4px;
400 }
401
402 .plugin-status-toggle input:checked + .toggler:before {
403 transform: translateX(20px);
404 }
405
406 .plugin-status-toggle input:disabled + .toggler {
407 cursor: default;
408 }
409
410 .plugin-status-toggle.disabled-appearance {
411 opacity: 0.6;
412 }
413
414 .plugin-status-text {
415 color: var(--color-text-primary);
416 font-size: 0.9rem;
417 font-weight: 600;
418 min-width: 2rem;
419 }
420
421 .plugin-description {
422 color: var(--color-text-secondary);
423 font-size: var(--font-size-small);
424 flex: 1 1 auto;
425 min-width: 0;
426 }
427
428 .plugin-path {
429 margin-top: 0.35rem;
430 font-size: 0.85rem;
431 color: var(--color-text-muted);
432 min-width: 0;
433 max-width: 100%;
434 white-space: pre-wrap;
435 word-break: break-word;
436 overflow-wrap: anywhere;
437 }
438
439 .loading {
440 width: 100%;
441 text-align: center;
442 margin-top: 0.75rem;
443 margin-bottom: 0.75rem;
444 color: var(--color-secondary);
445 }
446
447 @media (max-width: 768px) {
448 .plugin-header {
449 flex-direction: column;
450 align-items: stretch;
451 }
452
453 .plugin-heading {
454 width: 100%;
455 flex: 0 0 auto;
456 }
457
458 .plugin-actions {
459 width: 100%;
460 justify-content: flex-start;
461 padding-bottom: 0;
462 flex-wrap: wrap;
463 }
464
465 .plugin-path {
466 white-space: normal;
467 word-break: break-word;
468 }
469 }
470 </style>
471 </body>
472
473 </html>