feat: external iframes, container startup UX, release notes modal
- Add https: to CSP frame-src so external site iframes (BotFights, 484 Kitchen, etc.) load without being blocked by Content-Security-Policy - Show spinner + "Starting..." on marketplace cards for containers that are booting up, preventing users from re-installing running apps - Add spinner to transitional state badges (starting/stopping/installing) on installed app cards in Apps view - Add "What's New" button to Settings version card with release notes modal covering recent highlights in layman-friendly language Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
7278397209
commit
b9c9881e4b
@@ -177,37 +177,49 @@
|
||||
</p>
|
||||
|
||||
<div class="flex gap-2 mt-auto">
|
||||
<span
|
||||
v-if="isInstalled(app.id)"
|
||||
<!-- Installed & starting up (transitional state) -->
|
||||
<span
|
||||
v-if="isInstalled(app.id) && isStartingUp(app.id)"
|
||||
class="flex-1 px-4 py-2 bg-yellow-500/15 border border-yellow-500/30 rounded-lg text-yellow-200 text-sm font-medium text-center cursor-default flex items-center justify-center gap-2"
|
||||
>
|
||||
<svg class="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
{{ getInstalledState(app.id) === 'installing' ? 'Installing...' : 'Starting...' }}
|
||||
</span>
|
||||
<!-- Installed & ready -->
|
||||
<span
|
||||
v-else-if="isInstalled(app.id)"
|
||||
class="flex-1 px-4 py-2 bg-white/20 rounded-lg text-white/60 text-sm font-medium text-center cursor-default"
|
||||
>
|
||||
{{ t('marketplace.alreadyInstalled') }}
|
||||
</span>
|
||||
>
|
||||
{{ t('marketplace.alreadyInstalled') }}
|
||||
</span>
|
||||
<button
|
||||
v-if="isInstalled(app.id)"
|
||||
v-if="isInstalled(app.id) && !isStartingUp(app.id)"
|
||||
@click.stop="launchInstalledApp(app)"
|
||||
class="px-4 py-2 glass-button glass-button-sm rounded-lg text-sm font-medium"
|
||||
>
|
||||
{{ t('common.launch') }}
|
||||
</button>
|
||||
<button
|
||||
v-else-if="app.source === 'local' || app.dockerImage"
|
||||
v-else-if="!isInstalled(app.id) && (app.source === 'local' || app.dockerImage)"
|
||||
data-controller-install-btn
|
||||
@click.stop="app.source === 'local' ? installApp(app) : installCommunityApp(app)"
|
||||
:disabled="installingApps.has(app.id)"
|
||||
:disabled="installingApps.has(app.id)"
|
||||
class="flex-1 px-4 py-2 glass-button glass-button-sm rounded-lg text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span v-if="installingApps.has(app.id)" class="flex items-center justify-center gap-2">
|
||||
<svg class="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
{{ installingApps.get(app.id)?.message || t('common.installing') }}
|
||||
</span>
|
||||
<span v-else>{{ t('common.install') }}</span>
|
||||
>
|
||||
<span v-if="installingApps.has(app.id)" class="flex items-center justify-center gap-2">
|
||||
<svg class="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
{{ installingApps.get(app.id)?.message || t('common.installing') }}
|
||||
</span>
|
||||
<span v-else>{{ t('common.install') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
v-else-if="!isInstalled(app.id)"
|
||||
disabled
|
||||
class="flex-1 px-4 py-2 bg-white/10 rounded-lg text-white/40 text-sm font-medium cursor-not-allowed"
|
||||
>
|
||||
@@ -640,6 +652,26 @@ function isInstalled(appId: string): boolean {
|
||||
return aliases ? aliases.some((a) => a in installedPackages.value) : false
|
||||
}
|
||||
|
||||
/** Get the package state for an installed app (checks aliases too). */
|
||||
function getInstalledState(appId: string): string | null {
|
||||
const pkg = installedPackages.value[appId]
|
||||
if (pkg) return pkg.state
|
||||
const aliases = INSTALLED_ALIASES[appId]
|
||||
if (aliases) {
|
||||
for (const a of aliases) {
|
||||
const aliasPkg = installedPackages.value[a]
|
||||
if (aliasPkg) return aliasPkg.state
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** True if installed and currently in a transitional state (not yet ready). */
|
||||
function isStartingUp(appId: string): boolean {
|
||||
const state = getInstalledState(appId)
|
||||
return state !== null && state !== 'running' && state !== 'stopped' && state !== 'exited'
|
||||
}
|
||||
|
||||
function launchInstalledApp(app: MarketplaceApp) {
|
||||
appLauncher.openSession(app.id)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user