fix(modals): operate global modals through central ui state (vuex)
This commit is contained in:
parent
3955c9ebcd
commit
9253e8e408
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue