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 width: number
} }
pageTitle: null pageTitle: null
modalsOpen: Set<string>
notifications: Record<NotificationsKey, number> notifications: Record<NotificationsKey, number>
websocketEventsHandlers: Record<WebSocketEventName, WebSocketHandlers> websocketEventsHandlers: Record<WebSocketEventName, WebSocketHandlers>
@ -85,7 +86,8 @@ const store: Module<State, RootState> = {
'user_request.created': {}, 'user_request.created': {},
Listen: {} Listen: {}
}, },
pageTitle: null pageTitle: null,
modalsOpen: new Set([])
}, },
getters: { getters: {
showInstanceSupportMessage: (state, getters, rootState) => { showInstanceSupportMessage: (state, getters, rootState) => {
@ -149,7 +151,9 @@ const store: Module<State, RootState> = {
} else { } else {
return 'large' return 'large'
} }
} },
modalShown: (state, key) =>
state.modalsOpen.has(key)
}, },
mutations: { mutations: {
addWebsocketEventHandler: (state, { eventName, id, handler }: { eventName: WebSocketEventName, id: string, handler: (event: any) => void}) => { 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) { removeMessage (state, key) {
state.messages.splice(state.messages.findIndex(message => message.key === key), 1) 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 }) { notifications (state, { type, count }: { type: NotificationsKey, count: number }) {
state.notifications[type] = count state.notifications[type] = count
}, },

View File

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

View File

@ -12,26 +12,13 @@ import UserMenu from './UserMenu.vue'
import Button from '~/components/ui/Button.vue' import Button from '~/components/ui/Button.vue'
import Layout from '~/components/ui/Layout.vue' import Layout from '~/components/ui/Layout.vue'
import Spacer from '~/components/ui/layout/Spacer.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 isCollapsed = ref(false)
const route = useRoute() const route = useRoute()
watch(() => route.path, () => ( isCollapsed.value = true )) watch(() => route.path, () => ( isCollapsed.value = true ))
const { openShortcutsModal } = defineProps<{ openShortcutsModal: ()=> void }>()
const searchQuery = ref('') const searchQuery = ref('')
// Hide the fake app when the real one is loaded // Hide the fake app when the real one is loaded
@ -86,7 +73,7 @@ const uploads = useUploadsStore()
</Transition> </Transition>
</Link> </Link>
<UserMenu :showShortcutsModal="openShortcutsModal"/> <UserMenu/>
<Button round ghost icon="bi-list large" class="hide-on-desktop" @click="isCollapsed=!isCollapsed"/> <Button round ghost icon="bi-list large" class="hide-on-desktop" @click="isCollapsed=!isCollapsed"/>
</Layout> </Layout>

View File

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { SUPPORTED_LOCALES, setI18nLanguage } from '~/init/locale'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { computed, ref, watch, watchEffect } from 'vue' import { computed, ref} from 'vue'
import { useStore } from '~/store' import { useStore } from '~/store'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
@ -10,32 +9,19 @@ import useTheme from '~/composables/useTheme'
import Button from '~/components/ui/Button.vue' import Button from '~/components/ui/Button.vue'
import Popover from '~/components/ui/Popover.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 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 PopoverSubmenu from '~/components/ui/popover/PopoverSubmenu.vue'
import Link from '~/components/ui/Link.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 route = useRoute()
const store = useStore() const store = useStore()
const { t, locale } = useI18n() const { t } = useI18n()
const themes = useThemeList() const themes = useThemeList()
const { theme } = useTheme() const { theme } = useTheme()
const openUserMenu = ref(false) const isOpen = ref(false)
const isLanguageModelOpen = ref(false)
const labels = computed(() => ({ const labels = computed(() => ({
profile: t('components.common.UserMenu.link.profile'), profile: t('components.common.UserMenu.link.profile'),
@ -54,23 +40,19 @@ const labels = computed(() => ({
signup: t('components.common.UserMenu.link.signup'), signup: t('components.common.UserMenu.link.signup'),
notifications: t('components.common.UserMenu.link.notifications') notifications: t('components.common.UserMenu.link.notifications')
})) }))
watchEffect(()=> {
isLanguageModelOpen.value = isLanguageModelOpen.value && openUserMenu.value
})
</script> </script>
<template> <template>
<Popover raised v-model:open="openUserMenu"> <Popover raised v-model:open="isOpen">
<Button <Button
@click="openUserMenu = !openUserMenu" @click="isOpen = !isOpen"
round round
default default
raised raised
icon="" icon=""
ghost ghost
class="is-icon-only" class="is-icon-only"
:ariaPressed="openUserMenu ? true : undefined" :ariaPressed="isOpen ? true : undefined"
> >
<img <img
v-if="store.state.auth.authenticated && store.state.auth.profile?.avatar?.urls.medium_square_crop" v-if="store.state.auth.authenticated && store.state.auth.profile?.avatar?.urls.medium_square_crop"
@ -105,26 +87,11 @@ watchEffect(()=> {
</Link> </Link>
</PopoverItem> </PopoverItem>
<hr v-if="store.state.auth.authenticated && store.state.auth.profile?.avatar?.urls.medium_square_crop"/> <hr v-if="store.state.auth.authenticated && store.state.auth.profile?.avatar?.urls.medium_square_crop"/>
<PopoverItem @click="isLanguageModelOpen = true" <PopoverItem @click="store.commit('ui/toggleModal', 'languages')"
:aria-pressed="isLanguageModelOpen" :aria-pressed="store.state.ui.modalsOpen.has('languages')"
class="solid interactive"
> >
<i class="bi bi-translate" /> <i class="bi bi-translate" />
{{ labels.language }}... {{ 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> </PopoverItem>
<PopoverSubmenu> <PopoverSubmenu>
<i class="bi bi-palette-fill" /> <i class="bi bi-palette-fill" />
@ -161,7 +128,9 @@ watchEffect(()=> {
<i class="bi bi-book" /> <i class="bi bi-book" />
{{ labels.docs }} {{ labels.docs }}
</PopoverItem> </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" /> <i class="bi bi-keyboard" />
{{ labels.shortcuts }} {{ labels.shortcuts }}
</PopoverItem> </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"> <script setup lang="ts">
import Modal from '~/components/ui/Modal.vue'
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' 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 Button from '~/components/ui/Button.vue'
import Layout from '~/components/ui/Layout.vue' import Layout from '~/components/ui/Layout.vue'
import Spacer from '~/components/ui/layout/Spacer.vue' import Spacer from '~/components/ui/layout/Spacer.vue'
const model = defineModel<boolean>()
const { t } = useI18n() 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(() => [ const general = computed(() => [
{ {
title: t('components.ShortcutsModal.shortcut.general.label'), title: t('components.ShortcutsModal.shortcut.general.label'),
@ -99,13 +115,11 @@ const player = computed(() => [
</script> </script>
<template> <template>
<Modal <Modal overPopover
overPopover
:title="t('components.ShortcutsModal.header.modal')" :title="t('components.ShortcutsModal.header.modal')"
v-model="model"> v-model="isOpen"
<section class="scrolling content"> >
<Layout columns style="column-gap: 64px; column-rule: none;"> <Layout columns style="column-gap: 64px; column-rule: none;">
<div class="column">
<div <div
v-for="section in player" v-for="section in player"
:key="section.title" :key="section.title"
@ -113,17 +127,17 @@ const player = computed(() => [
> >
<h3 style="margin-top: 0px;">{{ section.title }}</h3> <h3 style="margin-top: 0px;">{{ section.title }}</h3>
<layout stack no-gap> <layout stack no-gap>
<div <template
v-for="shortcut in section.shortcuts" v-for="shortcut in section.shortcuts"
:key="shortcut.summary" :key="shortcut.summary"
> >
<layout flex> <layout flex>
<span :class="$style.description" style="align-self: center;">{{ shortcut.summary }}</span> <span :class="$style.description" style="align-self: center;">{{ shortcut.summary }}</span>
<Spacer grow /> <Spacer grow />
<Button style="pointer-events:none;" class="is-icon-only">{{ shortcut.key }}</Button> <Button style="pointer-events:none;" width="auto">{{ shortcut.key }}</Button>
</layout> </layout>
<hr /> <hr />
</div> </template>
</layout> </layout>
</div> </div>
<div <div
@ -139,22 +153,17 @@ const player = computed(() => [
<layout flex> <layout flex>
<span :class="$style.description" style="align-self: center;">{{ shortcut.summary }}</span> <span :class="$style.description" style="align-self: center;">{{ shortcut.summary }}</span>
<Spacer grow /> <Spacer grow />
<Button style="pointer-events:none;" class="is-icon-only">{{ shortcut.key }}</Button> <Button style="pointer-events:none;" width="auto">{{ shortcut.key }}</Button>
</layout> </layout>
<hr /> <hr />
</div> </div>
</layout> </layout>
</div> </div>
</div>
</Layout> </Layout>
</section>
</Modal> </Modal>
</template> </template>
<style module> <style module>
.withPadding {
padding:8px;
}
.description { .description {
font-size: 0.875em; font-size: 0.875em;
} }