Initial appstore view in vue
This commit is contained in:
99
dashboard/src/components/AppInstallDialog.vue
Normal file
99
dashboard/src/components/AppInstallDialog.vue
Normal 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>
|
||||
151
dashboard/src/components/AppstoreView.vue
Normal file
151
dashboard/src/components/AppstoreView.vue
Normal 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>
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user