Files
cloudron-box/dashboard/src/components/AppstoreView.vue

200 lines
5.5 KiB
Vue

<script setup>
import { ref, computed, useTemplateRef, onMounted } from 'vue';
import { Button, ButtonGroup, TextInput } from 'pankow';
import AppstoreModel from '../models/AppstoreModel.js';
import AppInstallDialog from './AppInstallDialog.vue';
import ApplinkDialog from './ApplinkDialog.vue';
import { PROXY_APP_ID } from '../constants.js';
const API_ORIGIN = import.meta.env.VITE_API_ORIGIN ? import.meta.env.VITE_API_ORIGIN : window.location.origin;
const appstoreModel = AppstoreModel.create(API_ORIGIN, localStorage.token);
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);
}
onMounted(async () => {
apps.value = await appstoreModel.list();
ready.value = true;
const query = window.location.hash.slice('#/appstore/'.length);
if (query) {
const appId = query.split('?')[0];
const version = query.slice(query.indexOf('version=')+'version='.length);
const app = await appstoreModel.get(appId, version);
if (app) {
appInstallDialog.value.open(app);
} else {
console.error('No such version found');
}
} else {
searchInput.value.$el.focus();
}
proxyApp.value = await appstoreModel.get(PROXY_APP_ID);
});
</script>
<template>
<div>
<AppInstallDialog ref="appInstallDialog" @close="onAppInstallDialogClose"/>
<ApplinkDialog ref="applinkDialog" @success="onApplinkDialogSuccess()"/>
<div class="filter-bar">
<div></div>
<TextInput ref="searchInput" @keydown.esc="search = ''" v-model="search" :placeholder="$t('appstore.searchPlaceholder')" style="max-width: 100%; width: 500px;"/>
<ButtonGroup>
<Button outline icon="fas fa-exchange-alt" @click="onInstall(proxyApp)">{{ $t('apps.addAppproxyAction') }}</Button>
<Button outline icon="fas fa-link" @click="onApplinkDialogOpen()">{{ $t('apps.addApplinkAction') }}</Button>
</ButtonGroup>
</div>
<div v-if="!search">
<h4 v-show="filteredPopularApps.length">{{ $t('appstore.category.popular') }}</h4>
<TransitionGroup name="grid-animation" tag="div" class="grid" v-show="ready">
<div class="item" v-for="app in filteredPopularApps" :key="app.id" :ref="'item-' + app.id" @click="onInstall(app)">
<img class="icon" :src="app.iconUrl" />
<div class="description">
<div class="title">{{ app.manifest.title }}</div>
<div class="tagline">{{ app.manifest.tagline }}</div>
</div>
</div>
</TransitionGroup>
<h4 v-show="filteredAllApps.length">{{ $t('appstore.category.all') }}</h4>
<TransitionGroup name="grid-animation" tag="div" class="grid" v-show="ready">
<div class="item" v-for="app in filteredAllApps" :key="app.id" :ref="'item-' + app.id" @click="onInstall(app)">
<img class="icon" :src="app.iconUrl" />
<div class="description">
<div class="title">{{ app.manifest.title }}</div>
<div class="tagline">{{ app.manifest.tagline }}</div>
</div>
</div>
</TransitionGroup>
</div>
<div v-else>
<TransitionGroup name="grid-animation" tag="div" class="grid" v-show="ready">
<div class="item" v-for="app in filteredApps" :key="app.id" :ref="'item-' + app.id" @click="onInstall(app)">
<img class="icon" :src="app.iconUrl" />
<div class="description">
<div class="title">{{ app.manifest.title }}</div>
<div class="tagline">{{ app.manifest.tagline }}</div>
</div>
</div>
</TransitionGroup>
</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;
}
.grid {
position: relative;
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.item {
display: flex;
align-items: center;
width: 300px;
padding: 10px 15px;
border-radius: 10px;
cursor: pointer;
}
.item:hover {
background-color: var(--card-background);
}
.item.active {
width: 100%;
background-color: var(--card-background);
}
.icon {
width: 64px;
height: 64px;
object-fit: contain;
margin-right: 20px;
}
.title {
font-size: 16px;
}
.tagline {
font-size: 12px;
opacity: 0.8;
}
</style>