mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-10-08 14:05:02 -04:00
This commit is contained in:
6 files changed
+305
-34
No files matched your search
@@ -46,7 +46,7 @@ public class AdminUiController : ControllerBase
|
||||
{
|
||||
var activeBackend = _configuration.GetValue<string>("Backend:Type") ?? "Jellyfin";
|
||||
var repositories = _extensionManager.GetConfiguredRepositories();
|
||||
var installedExtensionCount = _extensionManager.GetActiveExtensions().Count;
|
||||
var installedExtensionCount = _extensionManager.GetInstalledExtensions().Count;
|
||||
|
||||
var schema = new AdminUiSchemaResponse
|
||||
{
|
||||
|
||||
@@ -34,7 +34,7 @@ public class ExtensionController : ControllerBase
|
||||
[HttpGet("installed")]
|
||||
public IActionResult GetInstalledExtensions()
|
||||
{
|
||||
var items = _extensionManager.GetActiveExtensions()
|
||||
var items = _extensionManager.GetInstalledExtensions()
|
||||
.Select(e => new
|
||||
{
|
||||
e.Id,
|
||||
@@ -42,7 +42,8 @@ public class ExtensionController : ControllerBase
|
||||
e.DisplayName,
|
||||
e.Description,
|
||||
e.Version,
|
||||
e.Types
|
||||
e.Types,
|
||||
e.Enabled
|
||||
})
|
||||
.ToList();
|
||||
return Ok(items);
|
||||
@@ -89,6 +90,30 @@ public class ExtensionController : ControllerBase
|
||||
return NotFound(new { success = false, message = "Extension not found or failed to delete." });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("disable/{id}")]
|
||||
public IActionResult DisableExtension(string id)
|
||||
{
|
||||
var success = _extensionManager.DisableExtension(id);
|
||||
if (success)
|
||||
{
|
||||
return Ok(new { success = true, message = "Extension disabled successfully." });
|
||||
}
|
||||
|
||||
return NotFound(new { success = false, message = "Extension not found." });
|
||||
}
|
||||
|
||||
[HttpPost("enable/{id}")]
|
||||
public async Task<IActionResult> EnableExtension(string id)
|
||||
{
|
||||
var success = await _extensionManager.EnableExtensionAsync(id);
|
||||
if (success)
|
||||
{
|
||||
return Ok(new { success = true, message = "Extension enabled successfully." });
|
||||
}
|
||||
|
||||
return NotFound(new { success = false, message = "Extension not found or failed to load." });
|
||||
}
|
||||
}
|
||||
|
||||
public class InstallRequest
|
||||
|
||||
@@ -17,6 +17,8 @@ namespace allstarr.Services.Common;
|
||||
|
||||
public class ExtensionManager
|
||||
{
|
||||
private const string DisabledMarkerFile = ".disabled";
|
||||
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ILogger<ExtensionManager> _logger;
|
||||
private readonly IConfiguration _configuration;
|
||||
@@ -48,9 +50,24 @@ public class ExtensionManager
|
||||
|
||||
public IReadOnlyCollection<ExtensionSandbox> GetActiveExtensions() => _activeExtensions.Values.ToList();
|
||||
|
||||
public IReadOnlyCollection<InstalledExtensionInfo> GetInstalledExtensions()
|
||||
{
|
||||
if (!Directory.Exists(_extensionsDir))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return Directory.GetDirectories(_extensionsDir)
|
||||
.Select(ReadInstalledExtensionInfo)
|
||||
.Where(item => item != null)
|
||||
.Select(item => item!)
|
||||
.OrderBy(item => item.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public ExtensionSandbox? GetExtension(string id)
|
||||
{
|
||||
return _activeExtensions.TryGetValue(id.ToLowerInvariant(), out var sandbox) ? sandbox : null;
|
||||
return _activeExtensions.TryGetValue(NormalizeExtensionId(id), out var sandbox) ? sandbox : null;
|
||||
}
|
||||
|
||||
public List<string> GetConfiguredRepositories()
|
||||
@@ -121,7 +138,8 @@ public class ExtensionManager
|
||||
var parsedItems = ParseStoreRegistry(json, repo);
|
||||
foreach (var item in parsedItems)
|
||||
{
|
||||
item.IsInstalled = _activeExtensions.ContainsKey(item.Id.ToLowerInvariant());
|
||||
item.IsInstalled = IsExtensionInstalled(item.Id);
|
||||
item.IsEnabled = _activeExtensions.ContainsKey(item.Id.ToLowerInvariant());
|
||||
catalog.Items.Add(item);
|
||||
}
|
||||
}
|
||||
@@ -205,25 +223,59 @@ public class ExtensionManager
|
||||
public bool UninstallExtension(string id)
|
||||
{
|
||||
var normId = id.ToLowerInvariant();
|
||||
if (_activeExtensions.TryRemove(normId, out _))
|
||||
_activeExtensions.TryRemove(normId, out _);
|
||||
|
||||
var folder = Path.Combine(_extensionsDir, normId);
|
||||
if (Directory.Exists(folder))
|
||||
{
|
||||
var folder = Path.Combine(_extensionsDir, normId);
|
||||
if (Directory.Exists(folder))
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(folder, true);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to delete extension directory {Path}", folder);
|
||||
}
|
||||
Directory.Delete(folder, true);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to delete extension directory {Path}", folder);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool DisableExtension(string id)
|
||||
{
|
||||
var normId = id.ToLowerInvariant();
|
||||
var folder = Path.Combine(_extensionsDir, normId);
|
||||
if (!Directory.Exists(folder))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_activeExtensions.TryRemove(normId, out _);
|
||||
File.WriteAllText(Path.Combine(folder, DisabledMarkerFile), DateTime.UtcNow.ToString("O"));
|
||||
_logger.LogInformation("Disabled extension {ExtensionId}", normId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> EnableExtensionAsync(string id)
|
||||
{
|
||||
var normId = id.ToLowerInvariant();
|
||||
var folder = Path.Combine(_extensionsDir, normId);
|
||||
if (!Directory.Exists(folder))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var disabledMarker = Path.Combine(folder, DisabledMarkerFile);
|
||||
if (File.Exists(disabledMarker))
|
||||
{
|
||||
File.Delete(disabledMarker);
|
||||
}
|
||||
|
||||
await BootExtensionAsync(folder);
|
||||
return _activeExtensions.ContainsKey(normId);
|
||||
}
|
||||
|
||||
private async Task BootInstalledExtensions()
|
||||
{
|
||||
try
|
||||
@@ -231,6 +283,12 @@ public class ExtensionManager
|
||||
var dirs = Directory.GetDirectories(_extensionsDir);
|
||||
foreach (var dir in dirs)
|
||||
{
|
||||
if (IsExtensionDisabled(dir))
|
||||
{
|
||||
_logger.LogInformation("Skipping disabled extension folder {Path}", dir);
|
||||
continue;
|
||||
}
|
||||
|
||||
await BootExtensionAsync(dir);
|
||||
}
|
||||
}
|
||||
@@ -244,6 +302,11 @@ public class ExtensionManager
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsExtensionDisabled(folderPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var manifestPath = Path.Combine(folderPath, "manifest.json");
|
||||
var indexJsPath = Path.Combine(folderPath, "index.js");
|
||||
|
||||
@@ -253,7 +316,7 @@ public class ExtensionManager
|
||||
var indexJs = await File.ReadAllTextAsync(indexJsPath);
|
||||
|
||||
var sandbox = new ExtensionSandbox(folderPath, manifestJson, indexJs, _httpClientFactory, _logger);
|
||||
_activeExtensions[sandbox.Id.ToLowerInvariant()] = sandbox;
|
||||
_activeExtensions[NormalizeExtensionId(sandbox.Id)] = sandbox;
|
||||
_logger.LogInformation("Loaded extension successfully: {DisplayName} ({Id}) v{Version}", sandbox.DisplayName, sandbox.Id, sandbox.Version);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -262,6 +325,62 @@ public class ExtensionManager
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsExtensionInstalled(string id)
|
||||
{
|
||||
var normId = id.ToLowerInvariant();
|
||||
return Directory.Exists(Path.Combine(_extensionsDir, normId));
|
||||
}
|
||||
|
||||
private static bool IsExtensionDisabled(string folderPath)
|
||||
{
|
||||
return File.Exists(Path.Combine(folderPath, DisabledMarkerFile));
|
||||
}
|
||||
|
||||
private InstalledExtensionInfo? ReadInstalledExtensionInfo(string folderPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var manifestPath = Path.Combine(folderPath, "manifest.json");
|
||||
if (!File.Exists(manifestPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var manifestJson = File.ReadAllText(manifestPath);
|
||||
using var doc = JsonDocument.Parse(manifestJson);
|
||||
var root = doc.RootElement;
|
||||
var id = NormalizeExtensionId(ReadString(root, "id", "name"));
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
id = Path.GetFileName(folderPath).ToLowerInvariant();
|
||||
}
|
||||
|
||||
var active = _activeExtensions.TryGetValue(id, out var sandbox);
|
||||
var displayName = sandbox?.DisplayName ?? ReadString(root, "displayName", "display_name", "title", "name");
|
||||
if (string.IsNullOrWhiteSpace(displayName))
|
||||
{
|
||||
displayName = id;
|
||||
}
|
||||
|
||||
var version = sandbox?.Version ?? ReadString(root, "version");
|
||||
return new InstalledExtensionInfo
|
||||
{
|
||||
Id = id,
|
||||
Name = sandbox?.Name ?? ReadString(root, "name", "id"),
|
||||
DisplayName = displayName,
|
||||
Description = sandbox?.Description ?? ReadString(root, "description", "summary"),
|
||||
Version = string.IsNullOrWhiteSpace(version) ? "1.0.0" : version,
|
||||
Types = sandbox?.Types.ToList() ?? ReadStringList(root, "types", "type", "capabilities", "capability"),
|
||||
Enabled = active && !IsExtensionDisabled(folderPath)
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to read installed extension manifest from {Path}", folderPath);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static List<StoreExtensionItem> ParseStoreRegistry(string json, string repoUrl = "")
|
||||
{
|
||||
var items = new List<StoreExtensionItem>();
|
||||
@@ -463,11 +582,23 @@ public class StoreExtensionItem
|
||||
public string DownloadUrl { get; set; } = "";
|
||||
public string Version { get; set; } = "";
|
||||
public bool IsInstalled { get; set; }
|
||||
public bool IsEnabled { get; set; }
|
||||
public string RepoUrl { get; set; } = "";
|
||||
public string HomepageUrl { get; set; } = "";
|
||||
public List<string> Types { get; set; } = [];
|
||||
}
|
||||
|
||||
public class InstalledExtensionInfo
|
||||
{
|
||||
public string Id { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public string DisplayName { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public string Version { get; set; } = "";
|
||||
public bool Enabled { get; set; }
|
||||
public List<string> Types { get; set; } = [];
|
||||
}
|
||||
|
||||
public class ExtensionSandbox
|
||||
{
|
||||
public string Id { get; }
|
||||
|
||||
@@ -194,17 +194,21 @@ a {
|
||||
|
||||
.nav-list {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
min-height: 40px;
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
border-radius: var(--radius);
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
padding: 9px 10px;
|
||||
padding: 11px 14px;
|
||||
font-size: 0.98rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.nav-link.active,
|
||||
@@ -402,6 +406,13 @@ button.chip {
|
||||
border-color: color-mix(in srgb, var(--error), transparent 58%);
|
||||
}
|
||||
|
||||
.status-chip.disabled,
|
||||
.chip.disabled {
|
||||
color: var(--text-muted);
|
||||
background: color-mix(in srgb, var(--text-muted), transparent 90%);
|
||||
border-color: color-mix(in srgb, var(--text-muted), transparent 64%);
|
||||
}
|
||||
|
||||
.chip.muted-chip {
|
||||
opacity: 0.72;
|
||||
}
|
||||
@@ -629,6 +640,36 @@ tr:last-child td {
|
||||
padding: 2px 7px;
|
||||
}
|
||||
|
||||
.provider-token {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-width: 0;
|
||||
font-size: 0.84rem;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.provider-token-logo {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--surface-card);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.62rem;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.provider-token-logo img {
|
||||
display: block;
|
||||
max-width: 16px;
|
||||
max-height: 16px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.activity-list {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--radius: 8px;
|
||||
--rail-width: 264px;
|
||||
--rail-width: 316px;
|
||||
--now-playing-height: 76px;
|
||||
}
|
||||
|
||||
|
||||
@@ -129,6 +129,12 @@ function providerLogoUrl(provider) {
|
||||
return logos.has(id) ? `/images/providers/${id}.svg` : "";
|
||||
}
|
||||
|
||||
function providerDisplayName(providerId, providers = []) {
|
||||
const provider = asArray(providers).find((item) =>
|
||||
String(item?.id || item?.Id || "").toLowerCase() === String(providerId).toLowerCase());
|
||||
return provider?.name || provider?.Name || providerMark({ id: providerId });
|
||||
}
|
||||
|
||||
function isAwaitingApple2fa(value) {
|
||||
const state = String(value?.state || value?.login_state || value?.account?.state || "").toLowerCase();
|
||||
return state === "awaiting_2fa";
|
||||
@@ -260,6 +266,10 @@ const API = {
|
||||
installedExtensions: () => requestJson("/api/admin/extensions/installed", {}, "Failed to load installed extensions"),
|
||||
installExtension: (item) =>
|
||||
requestJson("/api/admin/extensions/install", jsonBody({ id: item.id || item.Id, downloadUrl: item.downloadUrl || item.DownloadUrl || "" }), "Failed to install extension"),
|
||||
enableExtension: (id) =>
|
||||
requestJson(`/api/admin/extensions/enable/${encodeURIComponent(id)}`, { method: "POST" }, "Failed to enable extension"),
|
||||
disableExtension: (id) =>
|
||||
requestJson(`/api/admin/extensions/disable/${encodeURIComponent(id)}`, { method: "POST" }, "Failed to disable extension"),
|
||||
uninstallExtension: (id) =>
|
||||
requestJson(`/api/admin/extensions/uninstall/${encodeURIComponent(id)}`, { method: "DELETE" }, "Failed to uninstall extension"),
|
||||
scrobblingStatus: () => requestJson("/api/admin/scrobbling/status", {}, "Failed to load scrobbling"),
|
||||
@@ -649,7 +659,7 @@ class AllstarrApp extends LitElement {
|
||||
this.extensionActions = { ...this.extensionActions, [key]: "Installing" };
|
||||
try {
|
||||
await API.installExtension(item);
|
||||
await Promise.all([this.loadInstalledExtensions(), this.loadExtensionStore()]);
|
||||
await Promise.all([this.loadInstalledExtensions(), this.loadExtensionStore(), this.loadSchema()]);
|
||||
this.toast("Extension installed");
|
||||
} finally {
|
||||
const nextActions = { ...this.extensionActions };
|
||||
@@ -658,6 +668,46 @@ class AllstarrApp extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
async setExtensionEnabled(item, enabled) {
|
||||
const id = item.id || item.Id;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.extensionActions = { ...this.extensionActions, [id]: enabled ? "Enabling" : "Disabling" };
|
||||
try {
|
||||
if (enabled) {
|
||||
await API.enableExtension(id);
|
||||
} else {
|
||||
await API.disableExtension(id);
|
||||
}
|
||||
await Promise.all([this.loadInstalledExtensions(), this.loadExtensionStore(), this.loadSchema()]);
|
||||
this.toast(`Extension ${enabled ? "enabled" : "disabled"}`);
|
||||
} finally {
|
||||
const nextActions = { ...this.extensionActions };
|
||||
delete nextActions[id];
|
||||
this.extensionActions = nextActions;
|
||||
}
|
||||
}
|
||||
|
||||
async uninstallExtension(item) {
|
||||
const id = item.id || item.Id;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.extensionActions = { ...this.extensionActions, [id]: "Uninstalling" };
|
||||
try {
|
||||
await API.uninstallExtension(id);
|
||||
await Promise.all([this.loadInstalledExtensions(), this.loadExtensionStore(), this.loadSchema()]);
|
||||
this.toast("Extension uninstalled");
|
||||
} finally {
|
||||
const nextActions = { ...this.extensionActions };
|
||||
delete nextActions[id];
|
||||
this.extensionActions = nextActions;
|
||||
}
|
||||
}
|
||||
|
||||
async runServiceAction(key, action) {
|
||||
this.serviceResults = { ...this.serviceResults, [key]: { state: "running", message: "Testing..." } };
|
||||
try {
|
||||
@@ -1585,6 +1635,7 @@ class AllstarrApp extends LitElement {
|
||||
}
|
||||
|
||||
renderPriorityGroups() {
|
||||
const providers = asArray(this.schema?.providers);
|
||||
return html`
|
||||
<div class="panel">
|
||||
<h3>Provider priority</h3>
|
||||
@@ -1595,7 +1646,7 @@ class AllstarrApp extends LitElement {
|
||||
<div class="priority-list">
|
||||
${asArray(group.providers).map((provider, index) => html`
|
||||
<span class="priority-item">
|
||||
<span>${provider}</span>
|
||||
${this.renderProviderToken(provider, providers)}
|
||||
<button ?disabled=${index === 0} @click=${() => this.movePriority(group, index, -1)}>Up</button>
|
||||
<button ?disabled=${index === group.providers.length - 1} @click=${() => this.movePriority(group, index, 1)}>Down</button>
|
||||
</span>
|
||||
@@ -1603,7 +1654,7 @@ class AllstarrApp extends LitElement {
|
||||
</div>
|
||||
${group.enabledEnvKey ? html`
|
||||
<div class="chip-list provider-enabled-list">
|
||||
${this.capabilityProviders(group.id).map((provider) => html`<span class="chip success">${provider}</span>`)}
|
||||
${this.capabilityProviders(group.id).map((provider) => html`<span class="chip success">${this.renderProviderToken(provider, providers)}</span>`)}
|
||||
</div>
|
||||
` : nothing}
|
||||
</div>
|
||||
@@ -1613,6 +1664,19 @@ class AllstarrApp extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
renderProviderToken(providerId, providers = asArray(this.schema?.providers)) {
|
||||
const label = providerDisplayName(providerId, providers);
|
||||
const logoUrl = providerLogoUrl({ id: providerId, name: label });
|
||||
return html`
|
||||
<span class="provider-token">
|
||||
<span class="provider-token-logo provider-${providerId}">
|
||||
${logoUrl ? html`<img src="${logoUrl}" alt="">` : providerMark({ id: providerId, name: label }).slice(0, 2)}
|
||||
</span>
|
||||
<span>${label}</span>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
async movePriority(group, index, direction) {
|
||||
const providers = [...group.providers];
|
||||
const target = index + direction;
|
||||
@@ -1650,17 +1714,25 @@ class AllstarrApp extends LitElement {
|
||||
<div class="panel">
|
||||
<h3>Installed extensions</h3>
|
||||
<div class="activity-list">
|
||||
${installed.length ? installed.map((item) => html`
|
||||
<div class="activity-item">
|
||||
<strong>${item.displayName || item.DisplayName || item.name || item.Name}</strong>
|
||||
<span class="muted">${display(item.description || item.Description)}</span>
|
||||
<div class="row-actions">
|
||||
${asArray(item.types || item.Types).map((type) => html`<span class="chip success">${titleCase(type)}</span>`)}
|
||||
${asArray(item.capabilities || item.Capabilities).map((type) => html`<span class="chip">${titleCase(type)}</span>`)}
|
||||
<button class="danger" @click=${async () => { await API.uninstallExtension(item.id || item.Id); await this.loadInstalledExtensions(); this.toast("Extension uninstalled"); }}>Uninstall</button>
|
||||
${installed.length ? installed.map((item) => {
|
||||
const id = item.id || item.Id;
|
||||
const action = this.extensionActions[id];
|
||||
const enabled = item.enabled ?? item.Enabled ?? true;
|
||||
return html`
|
||||
<div class="activity-item">
|
||||
<strong>${item.displayName || item.DisplayName || item.name || item.Name}</strong>
|
||||
<span class="muted">${display(item.description || item.Description)}</span>
|
||||
<div class="row-actions">
|
||||
<span class="status-chip ${enabled ? "configured" : "disabled"}">${enabled ? "Enabled" : "Disabled"}</span>
|
||||
${asArray(item.types || item.Types).map((type) => html`<span class="chip success">${titleCase(type)}</span>`)}
|
||||
${asArray(item.capabilities || item.Capabilities).map((type) => html`<span class="chip">${titleCase(type)}</span>`)}
|
||||
<button ?disabled=${Boolean(action)} @click=${() => this.setExtensionEnabled(item, !enabled)}>${enabled ? "Disable" : "Enable"}</button>
|
||||
<button class="danger" ?disabled=${Boolean(action)} @click=${() => this.uninstallExtension(item)}>Uninstall</button>
|
||||
</div>
|
||||
${action ? html`<div class="progress indeterminate"><span></span></div>` : nothing}
|
||||
</div>
|
||||
</div>
|
||||
`) : html`<div class="empty">No extensions installed.</div>`}
|
||||
`;
|
||||
}) : html`<div class="empty">No extensions installed.</div>`}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
@@ -1670,12 +1742,14 @@ class AllstarrApp extends LitElement {
|
||||
const key = item.id || item.Id || item.displayName || item.DisplayName;
|
||||
const action = this.extensionActions[key];
|
||||
const installedItem = Boolean(item.isInstalled || item.IsInstalled);
|
||||
const enabled = item.isEnabled ?? item.IsEnabled ?? false;
|
||||
return html`
|
||||
<div class="activity-item">
|
||||
<strong>${item.displayName || item.DisplayName}</strong>
|
||||
<span class="muted">${display(item.description || item.Description)}</span>
|
||||
<div class="row-actions">
|
||||
<span class="chip">${display(item.version || item.Version)}</span>
|
||||
${installedItem ? html`<span class="status-chip ${enabled ? "configured" : "disabled"}">${enabled ? "Enabled" : "Disabled"}</span>` : nothing}
|
||||
<button class="primary" ?disabled=${installedItem || Boolean(action)} @click=${() => this.installExtension(item)}>
|
||||
${installedItem ? "Installed" : action || "Install"}
|
||||
</button>
|
||||
|
||||
Reference in new issue
Block a user