2025-02-21 20:58:43 +01:00
|
|
|
|
<script setup>
|
|
|
|
|
|
|
2025-10-01 14:35:14 +02:00
|
|
|
|
import { ref, onMounted, useTemplateRef, inject } from 'vue';
|
2025-02-21 20:58:43 +01:00
|
|
|
|
import { marked } from 'marked';
|
2025-09-25 15:46:36 +02:00
|
|
|
|
import { Button, Switch, Dialog, Checkbox } from '@cloudron/pankow';
|
2025-07-24 11:35:50 +02:00
|
|
|
|
import { ISTATES } from '../../constants.js';
|
2025-06-25 21:16:59 +02:00
|
|
|
|
import SettingsItem from '../SettingsItem.vue';
|
2025-02-21 20:58:43 +01:00
|
|
|
|
import AppsModel from '../../models/AppsModel.js';
|
|
|
|
|
|
import ProfileModel from '../../models/ProfileModel.js';
|
2025-07-24 11:35:50 +02:00
|
|
|
|
import TasksModel from '../../models/TasksModel.js';
|
2025-02-21 20:58:43 +01:00
|
|
|
|
|
2026-02-20 22:27:30 +01:00
|
|
|
|
const props = defineProps([ 'app', 'refresh-app' ]);
|
2025-02-21 20:58:43 +01:00
|
|
|
|
|
|
|
|
|
|
const appsModel = AppsModel.create();
|
|
|
|
|
|
const profileModel = ProfileModel.create();
|
2025-07-24 11:35:50 +02:00
|
|
|
|
const tasksModel = TasksModel.create();
|
2025-02-21 20:58:43 +01:00
|
|
|
|
|
2025-10-01 14:35:14 +02:00
|
|
|
|
const features = inject('features');
|
|
|
|
|
|
|
2025-02-21 20:58:43 +01:00
|
|
|
|
const dialog = useTemplateRef('dialog');
|
|
|
|
|
|
const profile = ref({});
|
|
|
|
|
|
const busyUpdate = ref(false);
|
|
|
|
|
|
const busyCheck = ref(false);
|
|
|
|
|
|
const skipBackup = ref(false);
|
2025-05-29 11:27:37 +02:00
|
|
|
|
const updateError = ref('');
|
2025-02-21 20:58:43 +01:00
|
|
|
|
const autoUpdatesEnabled = ref(false);
|
2025-05-29 11:27:37 +02:00
|
|
|
|
const autoUpdatesEnabledBusy = ref(false);
|
|
|
|
|
|
|
|
|
|
|
|
async function onAutoUpdatesEnabledChange(value) {
|
|
|
|
|
|
autoUpdatesEnabledBusy.value = true;
|
|
|
|
|
|
|
|
|
|
|
|
const [error] = await appsModel.configure(props.app.id, 'automatic_update', { enable: value });
|
|
|
|
|
|
if (error) {
|
|
|
|
|
|
autoUpdatesEnabled.value = !value;
|
|
|
|
|
|
console.error(error);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
autoUpdatesEnabledBusy.value = false;
|
|
|
|
|
|
}
|
2025-02-21 20:58:43 +01:00
|
|
|
|
|
2025-07-24 11:35:50 +02:00
|
|
|
|
async function waitForTask(id) {
|
|
|
|
|
|
if (!id) return;
|
|
|
|
|
|
|
|
|
|
|
|
const [error, result] = await tasksModel.get(id);
|
2025-12-10 18:04:07 +01:00
|
|
|
|
if (error) return console.error(error);
|
2025-07-24 11:35:50 +02:00
|
|
|
|
|
|
|
|
|
|
// task done, refresh menu
|
|
|
|
|
|
if (!result.active) {
|
|
|
|
|
|
await onCheck();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(waitForTask.bind(null, id), 2000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-21 20:58:43 +01:00
|
|
|
|
async function onCheck() {
|
|
|
|
|
|
busyCheck.value = true;
|
|
|
|
|
|
|
2025-09-17 11:56:08 +02:00
|
|
|
|
const [error] = await appsModel.checkUpdate(props.app.id);
|
2025-12-10 18:04:07 +01:00
|
|
|
|
if (error) return console.error(error);
|
2025-02-21 20:58:43 +01:00
|
|
|
|
|
2026-02-20 22:27:30 +01:00
|
|
|
|
await props.refreshApp();
|
|
|
|
|
|
|
2025-02-21 20:58:43 +01:00
|
|
|
|
busyCheck.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function onUpdate() {
|
|
|
|
|
|
busyUpdate.value = true;
|
2025-05-29 11:27:37 +02:00
|
|
|
|
updateError.value = '';
|
|
|
|
|
|
|
2026-02-05 17:29:00 +01:00
|
|
|
|
let appData = '';
|
2026-02-24 05:31:13 +01:00
|
|
|
|
if (props.app.appStoreId) {
|
|
|
|
|
|
appData = { manifest: props.app.updateInfo.manifest };
|
|
|
|
|
|
} else if (props.app.versionsUrl) {
|
|
|
|
|
|
appData = { versionsUrl: `${props.app.versionsUrl}@${props.app.updateInfo.manifest.version}` };
|
|
|
|
|
|
}
|
2026-02-05 17:29:00 +01:00
|
|
|
|
|
|
|
|
|
|
const [error, result] = await appsModel.update(props.app.id, appData, skipBackup.value);
|
2025-02-21 20:58:43 +01:00
|
|
|
|
if (error) {
|
|
|
|
|
|
busyUpdate.value = false;
|
2026-02-24 05:46:09 +01:00
|
|
|
|
if (error.status !== 202) updateError.value = error.body ? error.body.message : 'Internal error';
|
2025-12-10 18:04:07 +01:00
|
|
|
|
return console.error(error);
|
2025-02-21 20:58:43 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
dialog.value.close();
|
2025-05-29 11:27:37 +02:00
|
|
|
|
|
2025-07-24 11:35:50 +02:00
|
|
|
|
waitForTask(result);
|
2025-02-21 20:58:43 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onAskUpdate() {
|
|
|
|
|
|
busyUpdate.value = false;
|
2026-02-24 05:46:09 +01:00
|
|
|
|
updateError.value = '';
|
|
|
|
|
|
|
2025-02-21 20:58:43 +01:00
|
|
|
|
dialog.value.open();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
busyUpdate.value = false;
|
|
|
|
|
|
busyCheck.value = false;
|
|
|
|
|
|
autoUpdatesEnabled.value = props.app.enableAutomaticUpdate;
|
|
|
|
|
|
|
|
|
|
|
|
const [error, result] = await profileModel.get();
|
2025-12-10 18:04:07 +01:00
|
|
|
|
if (error) return console.error(error);
|
2025-02-21 20:58:43 +01:00
|
|
|
|
|
|
|
|
|
|
profile.value = result;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div>
|
2025-09-17 11:56:08 +02:00
|
|
|
|
<Dialog v-if="app.updateInfo" ref="dialog"
|
2025-03-06 16:54:30 +01:00
|
|
|
|
:title="$t('app.updateDialog.title', { app: app.fqdn })"
|
|
|
|
|
|
:reject-label="$t('main.dialog.cancel')"
|
|
|
|
|
|
reject-style="secondary"
|
|
|
|
|
|
:confirm-label="$t('app.updateDialog.updateAction')"
|
2025-10-01 14:35:14 +02:00
|
|
|
|
:confirm-active="!busyUpdate"
|
2025-03-06 16:54:30 +01:00
|
|
|
|
:confirm-busy="busyUpdate"
|
|
|
|
|
|
@confirm="onUpdate()"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div>
|
2025-10-03 11:22:19 +02:00
|
|
|
|
<div class="unstable-warning text-danger" v-if="app.updateInfo.unstable">{{ $t('app.updateDialog.unstableWarning') }}</div>
|
2025-09-30 14:42:27 +02:00
|
|
|
|
<div>{{ $t('app.updateDialog.changelogHeader', { version: app.updateInfo.manifest.version }) }}</div>
|
|
|
|
|
|
<div class="changelog" v-html="marked.parse(app.updateInfo.manifest.changelog)"></div>
|
2025-10-01 14:35:14 +02:00
|
|
|
|
<Checkbox class="skip-backup" v-model="skipBackup" :label="$t('app.updateDialog.skipBackupCheckbox')" />
|
2026-02-24 05:46:09 +01:00
|
|
|
|
<div class="error-label" style="margin-top: 12px" v-if="updateError">{{ updateError }}</div>
|
2025-03-06 16:54:30 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
|
2025-06-25 21:16:59 +02:00
|
|
|
|
<SettingsItem>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label>{{ $t('app.updates.auto.title') }}</label>
|
2026-02-05 17:29:00 +01:00
|
|
|
|
<div v-if="app.appStoreId || app.versionsUrl" v-html="$t('app.updates.auto.description')"></div>
|
|
|
|
|
|
<div v-else>{{ $t('app.updates.info.customAppUpdateInfo') }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Switch v-if="app.appStoreId || app.versionsUrl" v-model="autoUpdatesEnabled" :disabled="autoUpdatesEnabledBusy" @change="onAutoUpdatesEnabledChange"/>
|
2025-06-25 21:16:59 +02:00
|
|
|
|
</SettingsItem>
|
2025-02-21 20:58:43 +01:00
|
|
|
|
|
2025-09-25 15:02:16 +02:00
|
|
|
|
<hr style="margin-top: 20px"/>
|
|
|
|
|
|
|
2026-02-05 17:29:00 +01:00
|
|
|
|
<div v-if="app.appStoreId || app.versionsUrl">
|
2025-09-25 15:02:16 +02:00
|
|
|
|
<label>{{ $t('app.updatesTabTitle') }}</label>
|
2026-02-25 15:24:10 +01:00
|
|
|
|
<div>{{ $t('app.updates.updates.description') }}</div>
|
2025-09-25 15:02:16 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
<br/>
|
2026-02-05 17:29:00 +01:00
|
|
|
|
<Button v-if="app.appStoreId || app.versionsUrl" @click="onCheck()" :disabled="busyCheck" :loading="busyCheck">{{ $t('settings.updates.checkForUpdatesAction') }}</Button>
|
2025-02-21 20:58:43 +01:00
|
|
|
|
|
2025-09-25 15:02:16 +02:00
|
|
|
|
<hr v-if="app.updateInfo" style="margin-top: 20px"/>
|
2025-06-25 21:49:05 +02:00
|
|
|
|
|
2025-09-25 15:02:16 +02:00
|
|
|
|
<div v-if="app.updateInfo">
|
|
|
|
|
|
<label>{{ $t('settings.updates.updateAvailableAction') }}</label>
|
2025-10-01 13:33:03 +02:00
|
|
|
|
|
|
|
|
|
|
<div>{{ $t('app.updateDialog.changelogHeader', { version: app.updateInfo.manifest.version }) }}</div>
|
|
|
|
|
|
<div class="changelog" v-html="marked.parse(app.updateInfo.manifest.changelog)"></div>
|
|
|
|
|
|
|
2025-10-01 14:35:14 +02:00
|
|
|
|
<div class="error-label" style="margin-top: 12px" v-if="!features.appUpdates">{{ $t('app.updateDialog.subscriptionExpired') }}</div>
|
2025-10-01 13:46:25 +02:00
|
|
|
|
<div class="error-label" style="margin-top: 12px" v-if="app.updateInfo.unstable">{{ $t('app.updateDialog.unstableWarning') }}</div>
|
2025-09-25 15:02:16 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
<br/>
|
2025-10-17 19:45:41 +02:00
|
|
|
|
<Button v-if="app.updateInfo && features.appUpdates" :danger="app.updateInfo.unstable ? true : null" :success="app.updateInfo.unstable ? null : true" @click="onAskUpdate()" :disabled="app.taskId || (app.error && app.error.installationState !== ISTATES.PENDING_UPDATE) || app.runState === 'stopped' || app.installationState === 'pending_update'">{{ $t('app.updateDialog.updateAction') }}</Button>
|
2025-10-01 14:35:14 +02:00
|
|
|
|
<Button v-else-if="app.updateInfo && !features.appUpdates && profile.isAtLeastOwner" success href="/#/cloudron-account">{{ $t('app.updateDialog.setupSubscriptionAction') }}</Button>
|
2025-02-21 20:58:43 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2025-09-30 14:42:27 +02:00
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
|
|
|
|
|
|
.changelog {
|
2025-09-30 14:46:39 +02:00
|
|
|
|
max-height: 20lh;
|
2025-09-30 14:42:27 +02:00
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
padding-right: 0.5rem; /* space so scrollbar doesn’t overlap text */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-03 11:22:19 +02:00
|
|
|
|
.unstable-warning {
|
|
|
|
|
|
padding-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-30 14:46:39 +02:00
|
|
|
|
.skip-backup {
|
|
|
|
|
|
padding-top: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-01 13:46:25 +02:00
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
|
|
|
|
|
|
|
.changelog > ul {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-30 14:42:27 +02:00
|
|
|
|
</style>
|