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

View File

@@ -0,0 +1,99 @@
<template>
<Dialog ref="dialog"
:reject-label="$t('main.dialog.cancel')"
:confirm-label="'Install'"
confirm-style="success"
>
<div>
<div class="header">
<img class="icon" :src="app.iconUrl" />
<div class="right">
<div class="title">{{ manifest.title }}</div>
<div class="lastUpdated">{{ $t('appstore.installDialog.lastUpdated', { date: prettyDate(app.creationDate) }) }}</div>
<div class="memoryRequirement">{{ $t('appstore.installDialog.memoryRequirement', { size: prettyFileSize(manifest.memoryLimit) }) }}</div>
<div class="author"><a :href="manifest.website" target="_blank">Website</a></div>
</div>
</div>
<div v-html="description"></div>
</div>
</Dialog>
</template>
<script>
import { Dialog } from 'pankow';
import { prettyDate, prettyFileSize } from 'pankow/utils';
import { marked } from 'marked';
const API_ORIGIN = import.meta.env.VITE_API_ORIGIN ? import.meta.env.VITE_API_ORIGIN : window.location.origin;
const accessToken = localStorage.token;
export default {
name: 'AppInstallDialog',
components: {
Dialog,
},
data() {
return {
API_ORIGIN,
app: {},
manifest: {},
busy: false,
error: {},
};
},
computed: {
description() {
return marked.parse(this.manifest.description || '');
}
},
methods: {
prettyDate,
prettyFileSize,
async open(app) {
this.app = app;
this.manifest = app.manifest;
this.$refs.dialog.open();
},
onProceed() {
}
},
};
</script>
<style scoped>
.header {
display: flex;
align-items: center;
}
.right {
display: flex;
flex-direction: column;
font-size: 12px;
}
.title {
font-size: 26px;
margin-bottom: 10px;
}
.icon {
width: 96px;
height: 96px;
object-fit: cover;
margin-right: 20px;
}
.description {
display: flex;
flex-direction: column;
align-items: left;
justify-content: center;
}
</style>

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>

View File

@@ -2,6 +2,7 @@
<div>
<Notification />
<AppsView v-if="view === VIEWS.APPS" />
<AppstoreView v-if="view === VIEWS.APPSTORE" />
<SupportView v-if="view === VIEWS.SUPPORT" />
<VolumesView v-if="view === VIEWS.VOLUMES" />
</div>
@@ -12,6 +13,7 @@
import { Notification } from 'pankow';
import AppsView from './AppsView.vue';
import AppstoreView from './AppstoreView.vue';
import SupportView from './SupportView.vue';
import VolumesView from './VolumesView.vue';
@@ -21,6 +23,7 @@ const API_ORIGIN = import.meta.env.VITE_API_ORIGIN ? import.meta.env.VITE_API_OR
const VIEWS = {
APPS: 'apps',
APPSTORE: 'appstore',
SUPPORT: 'support',
VOLUMES: 'volumes',
};
@@ -29,6 +32,7 @@ export default {
name: 'Index',
components: {
AppsView,
AppstoreView,
Notification,
SupportView,
VolumesView,
@@ -58,6 +62,8 @@ export default {
if (view === VIEWS.APPS) {
that.view = VIEWS.APPS;
} else if (view === VIEWS.APPSTORE) {
that.view = VIEWS.APPSTORE;
} else if (view === VIEWS.SUPPORT) {
that.view = VIEWS.SUPPORT;
} else if (view === VIEWS.VOLUMES) {