frontend: add About dialog and unify global dialog backdrop
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
<SettingsModal />
|
||||
<UserManagementModal />
|
||||
<UserTokensModal />
|
||||
<AboutModal />
|
||||
<AccessDeniedModal />
|
||||
<header>
|
||||
<HeaderMain ref="headerMain" :path="path.pathList" :query="path.query" />
|
||||
@@ -28,11 +29,12 @@ import type HeaderMain from '@/components/HeaderMain.vue'
|
||||
import { loadSession, watchConnect, watchDisconnect } from '@/repositories/WS'
|
||||
import { useMainStore } from '@/stores/main'
|
||||
import type { ComputedRef } from 'vue'
|
||||
import { onMounted, onUnmounted, ref, watchEffect } from 'vue'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { RouterView } from 'vue-router'
|
||||
|
||||
import Router from '@/router/index'
|
||||
import { computed } from 'vue'
|
||||
import AboutModal from './components/AboutModal.vue'
|
||||
import AccessDeniedModal from './components/AccessDeniedModal.vue'
|
||||
import SelectionToolbar from './components/SelectionToolbar.vue'
|
||||
import type SettingsModalVue from './components/SettingsModal.vue'
|
||||
@@ -56,12 +58,16 @@ const path: ComputedRef<Path> = computed(() => {
|
||||
query
|
||||
}
|
||||
})
|
||||
watchEffect(() => {
|
||||
watch(
|
||||
() => path.value.path,
|
||||
() => {
|
||||
document.title =
|
||||
path.value.path.replace(/\/$/, '').split('/').pop() ||
|
||||
store.server.name ||
|
||||
'Cista Storage'
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
onMounted(loadSession)
|
||||
onMounted(watchConnect)
|
||||
onUnmounted(watchDisconnect)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><rect width="512" height="512" fill="#f80"/><path fill="#fff" d="M381 298h-84V167h-66L339 35l108 132h-66zm-168-84h-84v131H63l108 132 108-132h-66z"/></svg>
|
||||
|
After Width: | Height: | Size: 242 B |
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<ModalDialog name="about" title="">
|
||||
<div class="about-content">
|
||||
<div class="about-logo-pane">
|
||||
<img :src="logoUrl" alt="Cista Storage logo" class="about-logo" />
|
||||
</div>
|
||||
<div class="about-details">
|
||||
<h3 class="about-name">Cista {{ softwareVersion }}</h3>
|
||||
<p class="about-link">
|
||||
<a :href="projectUrl" target="_blank" rel="noopener noreferrer">{{ displayProjectUrl }}</a>
|
||||
</p>
|
||||
<div class="dialog-buttons about-actions">
|
||||
<div class="spacer"></div>
|
||||
<input id="close" type="reset" value="Close" class="button" @click="close" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import logoUrl from '@/assets/logo-square.svg?url'
|
||||
import ModalDialog from '@/components/ModalDialog.vue'
|
||||
import { useMainStore } from '@/stores/main'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const store = useMainStore()
|
||||
|
||||
const softwareVersion = computed(() => store.server.version || 'unknown')
|
||||
const projectUrl = 'https://git.zi.fi/Vasanko/cista-storage'
|
||||
const displayProjectUrl = projectUrl.replace(/^https?:\/\//, '')
|
||||
|
||||
const close = () => {
|
||||
store.dialog = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(#about.modal-dialog) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.about-content {
|
||||
display: grid;
|
||||
grid-template-columns: 11rem minmax(0, 1fr);
|
||||
align-items: stretch;
|
||||
width: min(35rem, 92vw);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
margin: -1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.about-logo-pane {
|
||||
display: block;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.about-logo {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 1 / 1;
|
||||
margin: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.about-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.about-name {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.about-link {
|
||||
margin: 0.65rem 0 1rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.about-actions {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
.about-content {
|
||||
grid-template-columns: 1fr;
|
||||
width: min(24rem, 90vw);
|
||||
}
|
||||
|
||||
.about-logo-pane {
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.about-logo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.about-details {
|
||||
padding: 0.85rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,32 +1,19 @@
|
||||
<template>
|
||||
<div v-if="store.dialog === 'accessdenied'" class="modal-overlay">
|
||||
<div class="modal-dialog" id="accessdenied">
|
||||
<div class="modal-content access-denied">
|
||||
<ModalDialog name="accessdenied" title="">
|
||||
<div class="access-denied">
|
||||
<p class="icon">⛔</p>
|
||||
<p class="message">Access Denied</p>
|
||||
<button @click="reload" class="button">Reload</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMainStore } from '@/stores/main'
|
||||
import { holdGlobalBackdrop } from 'paskia'
|
||||
import { watchEffect } from 'vue'
|
||||
|
||||
const store = useMainStore()
|
||||
import ModalDialog from '@/components/ModalDialog.vue'
|
||||
|
||||
const reload = () => {
|
||||
location.reload()
|
||||
}
|
||||
|
||||
// Keep backdrop active when this dialog shows
|
||||
watchEffect(() => {
|
||||
if (store.dialog === 'accessdenied') {
|
||||
holdGlobalBackdrop()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -164,6 +164,14 @@ const settingsMenu = (e: Event) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
items.push({
|
||||
label: 'ℹ️ About Cista...',
|
||||
onClick: () => {
|
||||
store.dialog = 'about'
|
||||
}
|
||||
})
|
||||
|
||||
ContextMenu.showContextMenu({
|
||||
// @ts-ignore
|
||||
x: e.target.getBoundingClientRect().right,
|
||||
|
||||
@@ -15,15 +15,55 @@
|
||||
<script setup lang="ts">
|
||||
import { useMainStore } from '@/stores/main'
|
||||
import { holdGlobalBackdrop, releaseGlobalBackdrop } from 'paskia'
|
||||
import { nextTick, ref, watchEffect } from 'vue'
|
||||
import { nextTick, onBeforeUnmount, ref, watch, watchEffect } from 'vue'
|
||||
|
||||
const overlay = ref<HTMLDivElement | null>(null)
|
||||
const dialog = ref<HTMLDivElement | null>(null)
|
||||
const store = useMainStore()
|
||||
let backdropHeld = false
|
||||
|
||||
const ensureGlobalBackdropStyles = () => {
|
||||
if (typeof document === 'undefined') return
|
||||
if (document.getElementById('paskia-dialog')) return
|
||||
const style = document.createElement('style')
|
||||
style.id = 'paskia-dialog'
|
||||
style.textContent = `body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1099;
|
||||
background: transparent;
|
||||
backdrop-filter: blur(0) brightness(1);
|
||||
-webkit-backdrop-filter: blur(0) brightness(1);
|
||||
pointer-events: none;
|
||||
visibility: hidden;
|
||||
transition: all 0.2s ease-out;
|
||||
}
|
||||
body.paskia-backdrop::before {
|
||||
-webkit-backdrop-filter: blur(.2rem) brightness(0.5);
|
||||
backdrop-filter: blur(.2rem) brightness(0.5);
|
||||
visibility: visible;
|
||||
}
|
||||
body.paskia-backdrop {
|
||||
overflow: auto;
|
||||
}
|
||||
#paskia-iframe {
|
||||
border: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 9999;
|
||||
color-scheme: auto;
|
||||
background: transparent;
|
||||
}
|
||||
`
|
||||
document.head.insertBefore(style, document.head.firstChild)
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
store.dialog = ''
|
||||
releaseGlobalBackdrop()
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -33,7 +73,6 @@ const props = defineProps<{
|
||||
|
||||
const show = () => {
|
||||
store.dialog = props.name
|
||||
holdGlobalBackdrop()
|
||||
nextTick(() => {
|
||||
overlay.value?.focus()
|
||||
const input = dialog.value?.querySelector('input')
|
||||
@@ -41,6 +80,29 @@ const show = () => {
|
||||
})
|
||||
}
|
||||
defineExpose({ show, close })
|
||||
|
||||
watch(
|
||||
() => store.dialog === props.name,
|
||||
isOpen => {
|
||||
if (isOpen && !backdropHeld) {
|
||||
ensureGlobalBackdropStyles()
|
||||
holdGlobalBackdrop()
|
||||
backdropHeld = true
|
||||
} else if (!isOpen && backdropHeld) {
|
||||
releaseGlobalBackdrop()
|
||||
backdropHeld = false
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (backdropHeld) {
|
||||
releaseGlobalBackdrop()
|
||||
backdropHeld = false
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (overlay.value) {
|
||||
overlay.value.focus()
|
||||
|
||||
@@ -84,7 +84,7 @@ export const useMainStore = defineStore('main', {
|
||||
paskia?: boolean
|
||||
office_previews?: boolean
|
||||
},
|
||||
dialog: '' as '' | 'settings' | 'usermgmt' | 'accessdenied' | 'tokens',
|
||||
dialog: '' as '' | 'settings' | 'usermgmt' | 'accessdenied' | 'tokens' | 'about',
|
||||
uprogress: {} as any,
|
||||
dprogress: {} as any,
|
||||
prefs: {
|
||||
|
||||
Reference in New Issue
Block a user