fix(modals): operate global modals through central ui state (vuex)

This commit is contained in:
upsiflu 2024-12-19 20:14:58 +01:00
parent 3955c9ebcd
commit 9253e8e408
6 changed files with 154 additions and 115 deletions

View File

@ -47,6 +47,7 @@ export interface State {
width: number
}
pageTitle: null
modalsOpen: Set<string>
notifications: Record<NotificationsKey, number>
websocketEventsHandlers: Record<WebSocketEventName, WebSocketHandlers>
@ -85,7 +86,8 @@ const store: Module<State, RootState> = {
'user_request.created': {},
Listen: {}
},
pageTitle: null
pageTitle: null,
modalsOpen: new Set([])
},
getters: {
showInstanceSupportMessage: (state, getters, rootState) => {
@ -149,7 +151,9 @@ const store: Module<State, RootState> = {
} else {
return 'large'
}
}
},
modalShown: (state, key) =>
state.modalsOpen.has(key)
},
mutations: {
addWebsocketEventHandler: (state, { eventName, id, handler }: { eventName: WebSocketEventName, id: string, handler: (event: any) => void}) => {
@ -190,6 +194,25 @@ const store: Module<State, RootState> = {
removeMessage (state, key) {
state.messages.splice(state.messages.findIndex(message => message.key === key), 1)
},
addModal (state, key) {
state.modalsOpen.add(key)
console.log("Added", key, "->", state.modalsOpen)
},
removeModal (state, key) {
state.modalsOpen.delete(key)
console.log("Removed", key, "->", state.modalsOpen)
},
toggleModal (state, key) {
state.modalsOpen.has(key) ? state.modalsOpen.delete(key) : state.modalsOpen.add(key)
console.log("Toggled", key, "->", state.modalsOpen)
},
setModal (state, [key, isOpen]:[string, boolean]) {
console.log("Set", key, "was:", state.modalsOpen.has(key) )
isOpen ? state.modalsOpen.add(key) : state.modalsOpen.delete(key)
console.log("Set", key, isOpen, "->", state.modalsOpen)
},
notifications (state, { type, count }: { type: NotificationsKey, count: number }) {
state.notifications[type] = count
},

View File

@ -1,27 +1,27 @@
<script setup lang="ts">
import { onMounted, nextTick, ref, defineAsyncComponent } from 'vue'
import onKeyboardShortcut from '~/composables/onKeyboardShortcut';
import Sidebar from '~/ui/components/Sidebar.vue'
import ShortcutsModal from '~/components/ShortcutsModal.vue'
import { onMounted, nextTick } from 'vue'
import { color } from '~/composables/colors.ts';
import Sidebar from '~/ui/components/Sidebar.vue'
import ShortcutsModal from './modals/Shortcuts.vue'
import LanguagesModal from './modals/Languages.vue'
// Fake content
onMounted(async () => {
await nextTick()
document.getElementById('fake-app')?.remove()
})
const isShortcutsModalOpen = ref(false)
onKeyboardShortcut('h', () => isShortcutsModalOpen.value = !isShortcutsModalOpen.value)
</script>
<template>
<div class="funkwhale grid">
<Sidebar :openShortcutsModal = "() => isShortcutsModalOpen=true" />
<Sidebar/>
<main v-bind="color('default solid')">
<RouterView />
</main>
<ShortcutsModal v-model="isShortcutsModalOpen" />
<LanguagesModal />
<ShortcutsModal />
</div>
</template>

View File

@ -12,26 +12,13 @@ import UserMenu from './UserMenu.vue'
import Button from '~/components/ui/Button.vue'
import Layout from '~/components/ui/Layout.vue'
import Spacer from '~/components/ui/layout/Spacer.vue'
import { useRoute, useRouter } from 'vue-router'
import { useRoute } from 'vue-router'
/*
Sidebar on slim screen
- While screen is slim, sidebar is drawn on top of screen
- While screen is slim, When new route is loaded, top-sidebar is auto-collapsed: `watch(() => route.path, () => (isCollapsed.value = true))`
- While screen is slim, top-sidebar can be expanded and collapsed with additional hamburger button on top-right
*/
const isCollapsed = ref(false)
const route = useRoute()
watch(() => route.path, () => ( isCollapsed.value = true ))
const { openShortcutsModal } = defineProps<{ openShortcutsModal: ()=> void }>()
const searchQuery = ref('')
// Hide the fake app when the real one is loaded
@ -86,7 +73,7 @@ const uploads = useUploadsStore()
</Transition>
</Link>
<UserMenu :showShortcutsModal="openShortcutsModal"/>
<UserMenu/>
<Button round ghost icon="bi-list large" class="hide-on-desktop" @click="isCollapsed=!isCollapsed"/>
</Layout>

View File

@ -1,7 +1,6 @@
<script setup lang="ts">
import { SUPPORTED_LOCALES, setI18nLanguage } from '~/init/locale'
import { useI18n } from 'vue-i18n'
import { computed, ref, watch, watchEffect } from 'vue'
import { computed, ref} from 'vue'
import { useStore } from '~/store'
import { useRoute } from 'vue-router'
@ -10,32 +9,19 @@ import useTheme from '~/composables/useTheme'
import Button from '~/components/ui/Button.vue'
import Popover from '~/components/ui/Popover.vue'
import PopoverCheckbox from '~/components/ui/popover/PopoverCheckbox.vue'
import PopoverItem from '~/components/ui/popover/PopoverItem.vue'
import PopoverRadio from '~/components/ui/popover/PopoverRadio.vue'
import PopoverRadioItem from '~/components/ui/popover/PopoverRadioItem.vue'
import PopoverSubmenu from '~/components/ui/popover/PopoverSubmenu.vue'
import Link from '~/components/ui/Link.vue'
import Modal from '~/components/ui/Modal.vue'
import Layout from '~/components/ui/Layout.vue'
const { showShortcutsModal } = defineProps<{
showShortcutsModal : () => void
}>()
const route = useRoute()
const store = useStore()
const { t, locale } = useI18n()
const { t } = useI18n()
const themes = useThemeList()
const { theme } = useTheme()
const openUserMenu = ref(false)
const isLanguageModelOpen = ref(false)
const isOpen = ref(false)
const labels = computed(() => ({
profile: t('components.common.UserMenu.link.profile'),
@ -54,23 +40,19 @@ const labels = computed(() => ({
signup: t('components.common.UserMenu.link.signup'),
notifications: t('components.common.UserMenu.link.notifications')
}))
watchEffect(()=> {
isLanguageModelOpen.value = isLanguageModelOpen.value && openUserMenu.value
})
</script>
<template>
<Popover raised v-model:open="openUserMenu">
<Popover raised v-model:open="isOpen">
<Button
@click="openUserMenu = !openUserMenu"
@click="isOpen = !isOpen"
round
default
raised
icon=""
ghost
class="is-icon-only"
:ariaPressed="openUserMenu ? true : undefined"
:ariaPressed="isOpen ? true : undefined"
>
<img
v-if="store.state.auth.authenticated && store.state.auth.profile?.avatar?.urls.medium_square_crop"
@ -105,26 +87,11 @@ watchEffect(()=> {
</Link>
</PopoverItem>
<hr v-if="store.state.auth.authenticated && store.state.auth.profile?.avatar?.urls.medium_square_crop"/>
<PopoverItem @click="isLanguageModelOpen = true"
:aria-pressed="isLanguageModelOpen"
class="solid interactive"
<PopoverItem @click="store.commit('ui/toggleModal', 'languages')"
:aria-pressed="store.state.ui.modalsOpen.has('languages')"
>
<i class="bi bi-translate" />
{{ labels.language }}...
<Modal v-model="isLanguageModelOpen" :title="labels.language" overPopover>
<Layout columns :column-width="140">
<Button ghost v-for="(language, key) in SUPPORTED_LOCALES"
thin
width="full"
align-text="left"
:aria-pressed="key===locale"
:key="key"
@click="setI18nLanguage(key)"
>
{{ language }}
</Button>
</Layout>
</Modal>
</PopoverItem>
<PopoverSubmenu>
<i class="bi bi-palette-fill" />
@ -161,7 +128,9 @@ watchEffect(()=> {
<i class="bi bi-book" />
{{ labels.docs }}
</PopoverItem>
<PopoverItem @click.prevent="showShortcutsModal ()">
<PopoverItem @click="store.commit('ui/toggleModal', 'shortcuts')"
:aria-pressed="store.state.ui.modalsOpen.has('shortcuts')"
>
<i class="bi bi-keyboard" />
{{ labels.shortcuts }}
</PopoverItem>

View File

@ -0,0 +1,51 @@
<script setup lang="ts">
import { SUPPORTED_LOCALES, setI18nLanguage } from '~/init/locale'
import { useI18n } from 'vue-i18n'
import { useStore } from '~/store'
import Modal from '~/components/ui/Modal.vue'
import Button from '~/components/ui/Button.vue'
import Layout from '~/components/ui/Layout.vue'
import { computed } from 'vue'
const { t, locale } = useI18n()
const store = useStore()
const modalName = 'languages'
const isOpen = computed({
get() {
return store.state.ui.modalsOpen.has(modalName);
},
set(value) {
store.commit('ui/setModal', [modalName, value]);
}
})
</script>
<template>
<Modal overPopover
:title="t('components.common.UserMenu.label.language')"
v-model="isOpen"
>
<Layout columns :column-width="140">
<Button ghost thin
v-for="(language, key) in SUPPORTED_LOCALES"
width="full"
align-text="left"
:aria-pressed="key===locale"
:key="key"
@click="setI18nLanguage(key)"
>
{{ language }}
</Button>
</Layout>
</Modal>
</template>
<style module>
.description {
font-size: 0.875em;
}
</style>

View File

@ -1,14 +1,30 @@
<script setup lang="ts">
import Modal from '~/components/ui/Modal.vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useStore } from '~/store'
import onKeyboardShortcut from '~/composables/onKeyboardShortcut';
import Modal from '~/components/ui/Modal.vue'
import Button from '~/components/ui/Button.vue'
import Layout from '~/components/ui/Layout.vue'
import Spacer from '~/components/ui/layout/Spacer.vue'
const model = defineModel<boolean>()
const { t } = useI18n()
const store = useStore()
const modalName = 'shortcuts'
const isOpen = computed({
get() {
return store.state.ui.modalsOpen.has(modalName);
},
set(value) {
store.commit('ui/setModal', [modalName, value]);
}
})
onKeyboardShortcut('h', () => store.commit('ui/toggleModal', modalName))
const general = computed(() => [
{
title: t('components.ShortcutsModal.shortcut.general.label'),
@ -99,62 +115,55 @@ const player = computed(() => [
</script>
<template>
<Modal
overPopover
<Modal overPopover
:title="t('components.ShortcutsModal.header.modal')"
v-model="model">
<section class="scrolling content">
<Layout columns style="column-gap: 64px; column-rule: none;">
<div class="column">
<div
v-for="section in player"
:key="section.title"
style="break-inside: avoid;"
v-model="isOpen"
>
<Layout columns style="column-gap: 64px; column-rule: none;">
<div
v-for="section in player"
:key="section.title"
style="break-inside: avoid;"
>
<h3 style="margin-top: 0px;">{{ section.title }}</h3>
<layout stack no-gap>
<template
v-for="shortcut in section.shortcuts"
:key="shortcut.summary"
>
<h3 style="margin-top: 0px;">{{ section.title }}</h3>
<layout stack no-gap>
<div
v-for="shortcut in section.shortcuts"
:key="shortcut.summary"
>
<layout flex>
<span :class="$style.description" style="align-self: center;">{{ shortcut.summary }}</span>
<Spacer grow />
<Button style="pointer-events:none;" class="is-icon-only">{{ shortcut.key }}</Button>
</layout>
<hr />
</div>
<layout flex>
<span :class="$style.description" style="align-self: center;">{{ shortcut.summary }}</span>
<Spacer grow />
<Button style="pointer-events:none;" width="auto">{{ shortcut.key }}</Button>
</layout>
</div>
<hr />
</template>
</layout>
</div>
<div
v-for="section in general"
:key="section.title"
>
<h3>{{ section.title }}</h3>
<layout stack no-gap>
<div
v-for="section in general"
:key="section.title"
v-for="shortcut in section.shortcuts"
:key="shortcut.summary"
>
<h3>{{ section.title }}</h3>
<layout stack no-gap>
<div
v-for="shortcut in section.shortcuts"
:key="shortcut.summary"
>
<layout flex>
<span :class="$style.description" style="align-self: center;">{{ shortcut.summary }}</span>
<Spacer grow />
<Button style="pointer-events:none;" class="is-icon-only">{{ shortcut.key }}</Button>
</layout>
<hr />
</div>
<layout flex>
<span :class="$style.description" style="align-self: center;">{{ shortcut.summary }}</span>
<Spacer grow />
<Button style="pointer-events:none;" width="auto">{{ shortcut.key }}</Button>
</layout>
<hr />
</div>
</div>
</Layout>
</section>
</layout>
</div>
</Layout>
</Modal>
</template>
<style module>
.withPadding {
padding:8px;
}
.description {
font-size: 0.875em;
}