Files
cloudron-box/dashboard/src/views/AppstoreView.vue
2025-05-05 15:27:13 +02:00

173 lines
4.8 KiB
Vue

<script setup>
import { ref, computed, useTemplateRef, onMounted } from 'vue';
import { Button, TextInput, Spinner } from 'pankow';
import AppstoreModel from '../models/AppstoreModel.js';
import AppInstallDialog from '../components/AppInstallDialog.vue';
import ApplinkDialog from '../components/ApplinkDialog.vue';
import AppStoreItem from '../components/AppStoreItem.vue';
import { PROXY_APP_ID } from '../constants.js';
const appstoreModel = AppstoreModel.create();
const ready = ref(false);
const proxyApp = ref();
const apps = ref([]);
const search = ref('');
const filteredApps = computed(() => {
if (!search.value) return apps.value;
const s = search.value.toLowerCase();
return apps.value.filter(a => {
if (a.manifest.title.toLowerCase().indexOf(s) !== -1) return true;
if (a.manifest.tagline.toLowerCase().indexOf(s) !== -1) return true;
if (a.manifest.tags.join().toLowerCase().indexOf(s) !== -1) return true;
return false;
});
});
const filteredAllApps = computed(() => {
return filteredApps.value.filter(a => !a.featured);
});
const filteredPopularApps = computed(() => {
return filteredApps.value.filter(a => a.featured);
});
const appInstallDialog = useTemplateRef('appInstallDialog');
const searchInput = useTemplateRef('searchInput');
const applinkDialog = useTemplateRef('applinkDialog');
function onAppInstallDialogClose() {
window.location.href = '#/appstore';
}
function onApplinkDialogOpen() {
applinkDialog.value.open();
}
function onApplinkDialogSuccess() {
window.location.href = '#/apps';
}
function onInstall(app) {
window.location.href = `#/appstore/${app.manifest.id}?version=${app.manifest.version}`;
appInstallDialog.value.open(app);
}
async function getAppList() {
const [error, result] = await appstoreModel.list();
if (error) return console.error(error);
apps.value = result;
}
async function getApp(id, version = '') {
const [error, result] = await appstoreModel.get(id, version);
if (error) {
console.error(error);
return null;
}
return result;
}
onMounted(async () => {
await getAppList();
ready.value = true;
const query = window.location.hash.slice('#/appstore/'.length);
if (query) {
const appId = query.split('?')[0];
const params = new URLSearchParams(window.location.hash.slice(window.location.hash.indexOf('?')));
const version = params.get('version') || 'latest';
const app = await getApp(appId, version);
if (app) {
appInstallDialog.value.open(app);
} else {
console.error('No such version found');
}
} else {
setTimeout(() => {
try {
searchInput.value.$el.focus();
// eslint-disable-next-line no-unused-vars
} catch(e) {;}
}, 100);
}
proxyApp.value = await getApp(PROXY_APP_ID);
});
</script>
<template>
<div class="content" style="width: 100%;">
<AppInstallDialog ref="appInstallDialog" @close="onAppInstallDialogClose"/>
<ApplinkDialog ref="applinkDialog" @success="onApplinkDialogSuccess()"/>
<div class="filter-bar">
<div></div>
<Spinner v-if="!ready" class="pankow-spinner-large"/>
<TextInput v-show="ready" ref="searchInput" @keydown.esc="search = ''" v-model="search" :placeholder="$t('appstore.searchPlaceholder')" style="max-width: 100%; width: 500px;"/>
<div>
<Button secondary plain icon="fas fa-exchange-alt" @click="onInstall(proxyApp)">{{ $t('apps.addAppproxyAction') }}</Button>
<Button secondary plain icon="fas fa-link" @click="onApplinkDialogOpen()">{{ $t('apps.addApplinkAction') }}</Button>
</div>
</div>
<div v-if="!search && ready">
<h4 v-show="filteredPopularApps.length">{{ $t('appstore.category.popular') }}</h4>
<div class="grid">
<AppStoreItem v-for="app in filteredPopularApps" :app="app" :key="app.id" :ref="'item-' + app.id" @click="onInstall(app)"/>
</div>
<h4 v-show="filteredAllApps.length">{{ $t('appstore.category.all') }}</h4>
<div class="grid">
<AppStoreItem v-for="app in filteredAllApps" :app="app" :key="app.id" :ref="'item-' + app.id" @click="onInstall(app)"/>
</div>
</div>
<div v-else-if="ready">
<div class="grid">
<AppStoreItem v-for="app in filteredApps" :app="app" :key="app.id" :ref="'item-' + app.id" @click="onInstall(app)"/>
</div>
</div>
</div>
</template>
<style scoped>
.grid-animation-move,
.grid-animation-enter-active,
.grid-animation-leave-active {
transition: all 0.2s ease;
}
.grid-animation-enter-from {
opacity: 0;
transform: scale(0);
}
.grid-animation-leave-to {
opacity: 0;
transform: scale(0);
}
.filter-bar {
width: 100%;
display: flex;
justify-content: space-between;
margin-bottom: 30px;
padding-left: 20px;
padding-right: 20px;
}
.grid {
position: relative;
display: flex;
gap: 20px;
flex-wrap: wrap;
}
</style>