mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-19 06:43:51 +02:00
feat: custom nav menu
This commit is contained in:
File diff suppressed because it is too large
Load Diff
666
src/components/dialogs/CustomNavDialog.vue
Normal file
666
src/components/dialogs/CustomNavDialog.vue
Normal file
@@ -0,0 +1,666 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
class="custom-nav-dialog"
|
||||
:title="t('nav_menu.custom_nav.dialog_title')"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
destroy-on-close>
|
||||
<div class="custom-nav-dialog__list" v-if="localLayout.length">
|
||||
<div
|
||||
v-for="(entry, index) in localLayout"
|
||||
:key="entry.key || entry.id"
|
||||
:class="['custom-nav-entry', `custom-nav-entry--${entry.type}`]">
|
||||
<template v-if="entry.type === 'item'">
|
||||
<div class="custom-nav-entry__info">
|
||||
<i :class="definitionsMap.get(entry.key)?.icon"></i>
|
||||
<span>{{ t(definitionsMap.get(entry.key)?.labelKey || entry.key) }}</span>
|
||||
</div>
|
||||
<div class="custom-nav-entry__controls">
|
||||
<div class="custom-nav-entry__move">
|
||||
<el-button circle size="small" :disabled="index === 0" @click="handleMoveEntry(index, -1)">
|
||||
<i class="ri-arrow-up-line"></i>
|
||||
</el-button>
|
||||
<el-button
|
||||
circle
|
||||
size="small"
|
||||
:disabled="index === localLayout.length - 1"
|
||||
@click="handleMoveEntry(index, 1)">
|
||||
<i class="ri-arrow-down-line"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="custom-nav-entry__folder-header">
|
||||
<div class="custom-nav-entry__info">
|
||||
<i :class="entry.icon || defaultFolderIcon"></i>
|
||||
<span>{{ entry.name?.trim() || t('nav_menu.custom_nav.folder_name_placeholder') }}</span>
|
||||
</div>
|
||||
<div class="custom-nav-entry__actions">
|
||||
<el-button size="small" plain @click="openFolderEditor(index)">
|
||||
<i class="ri-edit-box-line"></i>
|
||||
{{ t('nav_menu.custom_nav.edit_folder') }}
|
||||
</el-button>
|
||||
<div class="custom-nav-entry__move">
|
||||
<el-button
|
||||
circle
|
||||
size="small"
|
||||
:disabled="index === 0"
|
||||
@click="handleMoveEntry(index, -1)">
|
||||
<i class="ri-arrow-up-line"></i>
|
||||
</el-button>
|
||||
<el-button
|
||||
circle
|
||||
size="small"
|
||||
:disabled="index === localLayout.length - 1"
|
||||
@click="handleMoveEntry(index, 1)">
|
||||
<i class="ri-arrow-down-line"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="custom-nav-entry__folder-items">
|
||||
<template v-if="entry.items?.length">
|
||||
<el-tag
|
||||
v-for="key in entry.items"
|
||||
:key="`${entry.id}-${key}`"
|
||||
size="small"
|
||||
class="custom-nav-entry__folder-tag">
|
||||
{{ t(definitionsMap.get(key)?.labelKey || key) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<span v-else class="custom-nav-entry__folder-empty">
|
||||
{{ t('nav_menu.custom_nav.folder_empty') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<el-alert
|
||||
v-if="invalidFolders.length"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
:title="t('nav_menu.custom_nav.invalid_folder')" />
|
||||
<template #footer>
|
||||
<div class="custom-nav-dialog__footer">
|
||||
<div class="custom-nav-dialog__footer-left">
|
||||
<el-button size="small" type="primary" plain @click="openFolderEditor()">
|
||||
{{ t('nav_menu.custom_nav.add_folder') }}
|
||||
</el-button>
|
||||
<el-button size="small" type="warning" plain @click="handleReset">
|
||||
{{ t('nav_menu.custom_nav.restore_default') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="custom-nav-dialog__footer-right">
|
||||
<el-button @click="handleClose">
|
||||
{{ t('nav_menu.custom_nav.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" :disabled="isSaveDisabled" @click="handleSave">
|
||||
{{ t('nav_menu.custom_nav.save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="folderEditor.visible"
|
||||
class="folder-editor-dialog"
|
||||
:title="folderEditor.isEditing ? t('nav_menu.custom_nav.edit_folder') : t('nav_menu.custom_nav.add_folder')"
|
||||
width="900px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close>
|
||||
<div class="folder-editor">
|
||||
<div class="folder-editor__form">
|
||||
<el-input
|
||||
v-model="folderEditor.data.name"
|
||||
:placeholder="t('nav_menu.custom_nav.folder_name_placeholder')" />
|
||||
<el-input
|
||||
v-model="folderEditor.data.icon"
|
||||
:placeholder="t('nav_menu.custom_nav.folder_icon_placeholder')" />
|
||||
</div>
|
||||
<div class="folder-editor__lists">
|
||||
<div class="folder-editor__column">
|
||||
<div class="folder-editor__column-title">
|
||||
{{ t('nav_menu.custom_nav.folder_available') }}
|
||||
</div>
|
||||
<div v-if="!folderEditorAvailableItems.length" class="folder-editor__empty">
|
||||
{{ t('nav_menu.custom_nav.folder_empty') }}
|
||||
</div>
|
||||
<el-scrollbar v-else always class="folder-editor__scroll">
|
||||
<div v-for="item in folderEditorAvailableItems" :key="item.key" class="folder-editor__option">
|
||||
<el-checkbox
|
||||
:model-value="folderEditor.data.items.includes(item.key)"
|
||||
@change="(val) => toggleFolderItem(item.key, val)">
|
||||
<span class="folder-editor__option-label">
|
||||
<i :class="item.icon"></i>
|
||||
{{ t(item.labelKey) }}
|
||||
</span>
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<div class="folder-editor__column folder-editor__column--selected">
|
||||
<div class="folder-editor__column-title">
|
||||
{{ t('nav_menu.custom_nav.folder_selected') }}
|
||||
</div>
|
||||
<div v-if="!folderEditor.data.items.length" class="folder-editor__empty">
|
||||
{{ t('nav_menu.custom_nav.folder_selected_empty') }}
|
||||
</div>
|
||||
<div
|
||||
v-for="(key, index) in folderEditor.data.items"
|
||||
:key="`selected-${key}`"
|
||||
class="folder-editor__selected-item">
|
||||
<div class="folder-editor__selected-label">
|
||||
<i :class="definitionsMap.get(key)?.icon"></i>
|
||||
<span>{{ t(definitionsMap.get(key)?.labelKey || key) }}</span>
|
||||
</div>
|
||||
<div class="folder-editor__selected-actions">
|
||||
<div class="custom-nav-entry__move">
|
||||
<el-button
|
||||
circle
|
||||
size="small"
|
||||
:disabled="index === 0"
|
||||
@click="handleFolderItemMove(index, -1)">
|
||||
<i class="ri-arrow-up-line"></i>
|
||||
</el-button>
|
||||
<el-button
|
||||
circle
|
||||
size="small"
|
||||
:disabled="index === folderEditor.data.items.length - 1"
|
||||
@click="handleFolderItemMove(index, 1)">
|
||||
<i class="ri-arrow-down-line"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button size="small" text @click="toggleFolderItem(key, false)">
|
||||
{{ t('nav_menu.custom_nav.remove_from_folder') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="folder-editor__footer">
|
||||
<el-button
|
||||
v-if="folderEditor.isEditing"
|
||||
type="danger"
|
||||
:disabled="!canDeleteFolder"
|
||||
@click="handleFolderEditorDelete">
|
||||
{{ t('nav_menu.custom_nav.delete_folder') }}
|
||||
</el-button>
|
||||
<div class="folder-editor__footer-spacer"></div>
|
||||
<el-button @click="closeFolderEditor">
|
||||
{{ t('nav_menu.custom_nav.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" :disabled="folderEditorSaveDisabled" @click="handleFolderEditorSave">
|
||||
{{ t('nav_menu.custom_nav.save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { navDefinitions } from '../../shared/constants/ui.js';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
layout: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
defaultFolderIcon: {
|
||||
type: String,
|
||||
default: 'ri-menu-fold-line'
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:visible', 'save', 'reset']);
|
||||
const { t } = useI18n();
|
||||
|
||||
const cloneLayout = (source) => {
|
||||
if (!Array.isArray(source)) {
|
||||
return [];
|
||||
}
|
||||
return source.map((entry) => {
|
||||
if (entry?.type === 'folder') {
|
||||
return {
|
||||
type: 'folder',
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
icon: entry.icon || props.defaultFolderIcon,
|
||||
items: Array.isArray(entry.items) ? [...entry.items] : []
|
||||
};
|
||||
}
|
||||
return { type: 'item', key: entry.key };
|
||||
});
|
||||
};
|
||||
|
||||
const localLayout = ref(cloneLayout(props.layout));
|
||||
|
||||
const folderEditor = reactive({
|
||||
visible: false,
|
||||
isEditing: false,
|
||||
index: -1,
|
||||
data: {
|
||||
id: '',
|
||||
name: '',
|
||||
icon: '',
|
||||
items: []
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
localLayout.value = cloneLayout(props.layout);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const definitionsMap = computed(() => {
|
||||
const map = new Map();
|
||||
navDefinitions.forEach((definition) => {
|
||||
if (definition?.key) {
|
||||
map.set(definition.key, definition);
|
||||
}
|
||||
});
|
||||
return map;
|
||||
});
|
||||
|
||||
const folderEntries = computed(() => localLayout.value.filter((entry) => entry.type === 'folder'));
|
||||
|
||||
const invalidFolders = computed(() =>
|
||||
folderEntries.value.filter((entry) => !entry.name?.trim() || entry.items?.length < 2)
|
||||
);
|
||||
|
||||
const isSaveDisabled = computed(() => invalidFolders.value.length > 0);
|
||||
|
||||
const handleMoveEntry = (index, direction) => {
|
||||
const targetIndex = index + direction;
|
||||
if (targetIndex < 0 || targetIndex >= localLayout.value.length) {
|
||||
return;
|
||||
}
|
||||
const entries = [...localLayout.value];
|
||||
const [entry] = entries.splice(index, 1);
|
||||
entries.splice(targetIndex, 0, entry);
|
||||
localLayout.value = entries;
|
||||
};
|
||||
|
||||
const folderEditorSaveDisabled = computed(() => {
|
||||
const nameInvalid = !folderEditor.data.name?.trim();
|
||||
const insufficientItems = folderEditor.data.items.length < 2;
|
||||
return nameInvalid || insufficientItems;
|
||||
});
|
||||
|
||||
const canDeleteFolder = computed(() => {
|
||||
return localLayout.value[folderEditor.index]?.type === 'folder';
|
||||
});
|
||||
|
||||
const usedKeysExcludingEditor = computed(() => {
|
||||
const set = new Set();
|
||||
localLayout.value.forEach((entry) => {
|
||||
if (entry.type !== 'folder') {
|
||||
return;
|
||||
}
|
||||
if (folderEditor.isEditing && entry.id === folderEditor.data.id) {
|
||||
return;
|
||||
}
|
||||
entry.items?.forEach((key) => set.add(key));
|
||||
});
|
||||
return set;
|
||||
});
|
||||
|
||||
const folderEditorAvailableItems = computed(() =>
|
||||
navDefinitions.filter(
|
||||
(definition) =>
|
||||
!usedKeysExcludingEditor.value.has(definition.key) || folderEditor.data.items.includes(definition.key)
|
||||
)
|
||||
);
|
||||
|
||||
const openFolderEditor = (index) => {
|
||||
folderEditor.isEditing = !!index;
|
||||
folderEditor.index = folderEditor.isEditing ? index : -1;
|
||||
if (folderEditor.isEditing) {
|
||||
const entry = localLayout.value[index];
|
||||
folderEditor.data = {
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
icon: entry.icon || props.defaultFolderIcon,
|
||||
items: Array.isArray(entry.items) ? [...entry.items] : []
|
||||
};
|
||||
} else {
|
||||
folderEditor.data = {
|
||||
id: `custom-folder-${dayjs().toISOString()}-${Math.random().toString().slice(2, 7)}`,
|
||||
name: '',
|
||||
icon: props.defaultFolderIcon,
|
||||
items: []
|
||||
};
|
||||
}
|
||||
folderEditor.visible = true;
|
||||
};
|
||||
|
||||
const closeFolderEditor = () => {
|
||||
folderEditor.visible = false;
|
||||
};
|
||||
|
||||
const toggleFolderItem = (key, enabled) => {
|
||||
if (enabled) {
|
||||
if (!folderEditor.data.items.includes(key)) {
|
||||
folderEditor.data.items.push(key);
|
||||
}
|
||||
} else {
|
||||
folderEditor.data.items = folderEditor.data.items.filter((item) => item !== key);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFolderItemMove = (index, direction) => {
|
||||
const targetIndex = index + direction;
|
||||
if (targetIndex < 0 || targetIndex >= folderEditor.data.items.length) {
|
||||
return;
|
||||
}
|
||||
const items = [...folderEditor.data.items];
|
||||
const [item] = items.splice(index, 1);
|
||||
items.splice(targetIndex, 0, item);
|
||||
folderEditor.data.items = items;
|
||||
};
|
||||
|
||||
const applyFolderChanges = () => {
|
||||
const sanitizedItems = folderEditor.data.items.filter((key) => definitionsMap.value.has(key));
|
||||
const entries = [...localLayout.value];
|
||||
|
||||
if (folderEditor.isEditing) {
|
||||
const targetIndex = folderEditor.index;
|
||||
if (targetIndex < 0) {
|
||||
return;
|
||||
}
|
||||
const existing = entries[targetIndex];
|
||||
if (!existing || existing.type !== 'folder') {
|
||||
return;
|
||||
}
|
||||
const removedItems = existing.items.filter((key) => !sanitizedItems.includes(key));
|
||||
entries.splice(targetIndex, 1);
|
||||
|
||||
const filteredEntries = entries.filter(
|
||||
(entry) => !(entry.type === 'item' && sanitizedItems.includes(entry.key))
|
||||
);
|
||||
|
||||
filteredEntries.splice(targetIndex, 0, {
|
||||
type: 'folder',
|
||||
id: folderEditor.data.id,
|
||||
name: folderEditor.data.name.trim(),
|
||||
icon: folderEditor.data.icon || props.defaultFolderIcon,
|
||||
items: sanitizedItems
|
||||
});
|
||||
|
||||
if (removedItems.length) {
|
||||
const insertItems = removedItems.map((key) => ({ type: 'item', key }));
|
||||
filteredEntries.splice(targetIndex + 1, 0, ...insertItems);
|
||||
}
|
||||
|
||||
localLayout.value = filteredEntries;
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredEntries = entries.filter(
|
||||
(entry) => !(entry.type === 'item' && sanitizedItems.includes(entry.key))
|
||||
);
|
||||
|
||||
filteredEntries.push({
|
||||
type: 'folder',
|
||||
id: folderEditor.data.id,
|
||||
name: folderEditor.data.name.trim(),
|
||||
icon: folderEditor.data.icon || props.defaultFolderIcon,
|
||||
items: sanitizedItems
|
||||
});
|
||||
|
||||
localLayout.value = filteredEntries;
|
||||
};
|
||||
|
||||
const handleFolderEditorSave = () => {
|
||||
if (folderEditorSaveDisabled.value) {
|
||||
return;
|
||||
}
|
||||
applyFolderChanges();
|
||||
closeFolderEditor();
|
||||
};
|
||||
|
||||
const handleFolderEditorDelete = () => {
|
||||
if (!folderEditor.isEditing) {
|
||||
return;
|
||||
}
|
||||
const entries = [...localLayout.value];
|
||||
const targetIndex = folderEditor.index;
|
||||
if (targetIndex < 0) {
|
||||
return;
|
||||
}
|
||||
const folder = entries[targetIndex];
|
||||
if (!folder || folder.type !== 'folder') {
|
||||
return;
|
||||
}
|
||||
entries.splice(targetIndex, 1);
|
||||
const restoredItems = (folder.items || []).map((key) => ({ type: 'item', key }));
|
||||
entries.splice(targetIndex, 0, ...restoredItems);
|
||||
localLayout.value = entries;
|
||||
closeFolderEditor();
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (isSaveDisabled.value) {
|
||||
return;
|
||||
}
|
||||
emit('save', cloneLayout(localLayout.value));
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
emit('reset');
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
emit('update:visible', false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.custom-nav-dialog__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
max-height: 430px;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.custom-nav-entry {
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.custom-nav-entry__info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.custom-nav-entry__info i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.custom-nav-entry__controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.custom-nav-entry__move {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.custom-nav-entry__folder-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.custom-nav-entry__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.custom-nav-entry__folder-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.custom-nav-entry__folder-tag {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.custom-nav-entry__folder-empty {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.custom-nav-dialog__footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.custom-nav-dialog__footer-left {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.custom-nav-dialog__footer-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.folder-editor__form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.folder-editor__lists {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 0.9fr) minmax(260px, 1.1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.folder-editor__column {
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
min-height: 220px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--el-fill-color-blank);
|
||||
}
|
||||
|
||||
.folder-editor__column-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.folder-editor__empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.folder-editor__scroll {
|
||||
max-height: 240px;
|
||||
}
|
||||
|
||||
.folder-editor__option {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.folder-editor__option-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.folder-editor__option-label i {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.folder-editor__selected-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
}
|
||||
|
||||
.folder-editor__selected-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.folder-editor__selected-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.folder-editor__selected-label i {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.folder-editor__selected-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.folder-editor__column--selected {
|
||||
min-height: 260px;
|
||||
}
|
||||
|
||||
.folder-editor__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.folder-editor__footer-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -8,6 +8,9 @@
|
||||
"player_list": "Player List",
|
||||
"search": "Search",
|
||||
"favorites": "Favorites",
|
||||
"favorite_friends": "Favorite Friends",
|
||||
"favorite_worlds": "Favorite Worlds",
|
||||
"favorite_avatars": "Favorite Avatars",
|
||||
"social": "Social",
|
||||
"friend_log": "Friend Log",
|
||||
"moderation": "Moderation",
|
||||
@@ -26,7 +29,27 @@
|
||||
"get_help": "GET HELP",
|
||||
"github": "VRCX on GitHub",
|
||||
"discord": "Join our Discord",
|
||||
"whats_new": "What's New?"
|
||||
"whats_new": "What's New?",
|
||||
"custom_nav": {
|
||||
"header": "Custom Navigation Menu",
|
||||
"dialog_title": "Customize Navigation",
|
||||
"add_folder": "Add Folder",
|
||||
"folder_name_placeholder": "Folder name",
|
||||
"folder_icon_placeholder": "Icon class (e.g. ri-menu-fold-line)",
|
||||
"edit_folder": "Edit Folder",
|
||||
"folder_available": "Available items",
|
||||
"folder_selected": "Folder items",
|
||||
"folder_selected_empty": "No items selected",
|
||||
"delete_folder": "Delete Folder",
|
||||
"assign_folder": "Add to folder",
|
||||
"remove_from_folder": "Remove from folder",
|
||||
"folder_empty": "No items in this folder",
|
||||
"invalid_folder": "Folder must have a name and contain at least two items.",
|
||||
"restore_default": "Restore Default",
|
||||
"restore_default_confirm": "Restore navigation to its default order?",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
},
|
||||
"view": {
|
||||
"login": {
|
||||
|
||||
Reference in New Issue
Block a user