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

136 lines
2.9 KiB
Vue
Raw Normal View History

2025-01-05 22:47:50 +01:00
<template>
<div class="content">
<AppInstallDialog ref="appInstallDialog" />
<div class="filter-bar">
<TextInput ref="searchInput" v-model="search" :placeholder="$t('appstore.searchPlaceholder')" style="max-width: 100%; width: 500px;"/>
</div>
2025-01-05 23:35:01 +01:00
<TransitionGroup name="grid-animation" tag="div" class="grid" v-show="ready">
2025-01-05 22:47:50 +01:00
<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>
</template>
2025-01-06 18:52:44 +01:00
<script setup>
2025-01-05 22:47:50 +01:00
2025-01-06 18:52:44 +01:00
import { ref, computed, useTemplateRef, onMounted } from 'vue';
2025-01-05 22:47:50 +01:00
import { TextInput } from 'pankow';
import AppstoreModel from '../models/AppstoreModel.js';
import AppInstallDialog from './AppInstallDialog.vue';
const API_ORIGIN = import.meta.env.VITE_API_ORIGIN ? import.meta.env.VITE_API_ORIGIN : window.location.origin;
2025-01-06 18:52:44 +01:00
const appstoreModel = AppstoreModel.create(API_ORIGIN, localStorage.token);
const ready = ref(false);
const apps = ref([]);
const search = ref('');
const filteredApps = computed(() => {
if (!search.value) return apps.value;
const search = search.value.toLowerCase();
return apps.value.filter(a => {
if (a.manifest.title.toLowerCase().indexOf(search) !== -1) return true;
if (a.manifest.tagline.toLowerCase().indexOf(search) !== -1) return true;
if (a.manifest.tags.join().toLowerCase().indexOf(search) !== -1) return true;
return false;
});
});
const appInstallDialog = useTemplateRef('appInstallDialog');
const searchInput = useTemplateRef('searchInput');
function onInstall(app) {
appInstallDialog.value.open(app);
}
onMounted(async () => {
apps.value = await appstoreModel.list();
ready.value = true;
setTimeout(() => searchInput.value.$el.focus(), 0);
});
2025-01-05 22:47:50 +01:00
</script>
<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);
}
.content {
position: relative;
width: 100%;
}
.filter-bar {
width: 100%;
display: flex;
justify-content: center;
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;
2025-01-05 23:35:01 +01:00
object-fit: contain;
2025-01-05 22:47:50 +01:00
margin-right: 20px;
}
.title {
font-size: 16px;
}
.tagline {
font-size: 12px;
opacity: 0.8;
}
</style>