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
|
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
|
||||||
},
|
},
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
<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,62 +115,55 @@ 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"
|
style="break-inside: avoid;"
|
||||||
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 flex>
|
||||||
<layout stack no-gap>
|
<span :class="$style.description" style="align-self: center;">{{ shortcut.summary }}</span>
|
||||||
<div
|
<Spacer grow />
|
||||||
v-for="shortcut in section.shortcuts"
|
<Button style="pointer-events:none;" width="auto">{{ shortcut.key }}</Button>
|
||||||
: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>
|
</layout>
|
||||||
</div>
|
<hr />
|
||||||
|
</template>
|
||||||
|
</layout>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="section in general"
|
||||||
|
:key="section.title"
|
||||||
|
>
|
||||||
|
<h3>{{ section.title }}</h3>
|
||||||
|
<layout stack no-gap>
|
||||||
<div
|
<div
|
||||||
v-for="section in general"
|
v-for="shortcut in section.shortcuts"
|
||||||
:key="section.title"
|
:key="shortcut.summary"
|
||||||
>
|
>
|
||||||
<h3>{{ section.title }}</h3>
|
<layout flex>
|
||||||
<layout stack no-gap>
|
<span :class="$style.description" style="align-self: center;">{{ shortcut.summary }}</span>
|
||||||
<div
|
<Spacer grow />
|
||||||
v-for="shortcut in section.shortcuts"
|
<Button style="pointer-events:none;" width="auto">{{ shortcut.key }}</Button>
|
||||||
: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>
|
</layout>
|
||||||
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</layout>
|
||||||
</Layout>
|
</div>
|
||||||
</section>
|
</Layout>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style module>
|
<style module>
|
||||||
.withPadding {
|
|
||||||
padding:8px;
|
|
||||||
}
|
|
||||||
.description {
|
.description {
|
||||||
font-size: 0.875em;
|
font-size: 0.875em;
|
||||||
}
|
}
|
Loading…
Reference in New Issue