Initial appstore view in vue

This commit is contained in:
Johannes Zellner
2025-01-05 22:47:50 +01:00
parent 6da071c88d
commit ec2dd67d89
6 changed files with 287 additions and 2 deletions
+151
View File
@@ -0,0 +1,151 @@
<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>
<TransitionGroup name="grid-animation" tag="div" class="grid">
<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>
<script>
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;
const accessToken = localStorage.token;
const appstoreModel = AppstoreModel.create(API_ORIGIN, accessToken);
export default {
name: 'AppstoreView',
components: {
AppInstallDialog,
TextInput,
},
data() {
return {
API_ORIGIN,
ready: false,
apps: [],
search: '',
activeApp: {
manifest: {}
},
};
},
computed: {
filteredApps() {
if (!this.search) return this.apps;
const search = this.search.toLowerCase();
return this.apps.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;
});
}
},
methods: {
onInstall(app) {
this.$refs.appInstallDialog.open(app);
},
},
async mounted() {
this.apps = await appstoreModel.list();
this.ready = true;
setTimeout(() => this.$refs.searchInput.$el.focus(), 0);
}
};
</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;
object-fit: cover;
margin-right: 20px;
}
.title {
font-size: 16px;
}
.tagline {
font-size: 12px;
opacity: 0.8;
}
</style>