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>
|