replace el-dialog

This commit is contained in:
pa
2026-01-15 15:33:20 +09:00
committed by Natsumi
parent fc13dca0a4
commit 3b47d3a0eb
67 changed files with 7034 additions and 6769 deletions

View File

@@ -44,101 +44,105 @@
<span>{{ t('view.charts.mutual_friend.progress.no_relationships_discovered') }}</span>
</div>
<el-dialog
v-model="isForceDialogVisible"
:title="t('view.charts.mutual_friend.force_dialog.title')"
width="440px">
<p class="mutual-graph__force-description">
{{ t('view.charts.mutual_friend.force_dialog.description') }}
</p>
<FieldGroup class="mutual-graph__force-form">
<Field>
<FieldLabel>{{ t('view.charts.mutual_friend.force_dialog.repulsion') }}</FieldLabel>
<FieldContent>
<NumberField
v-model="forceForm.repulsion"
:step="1"
:format-options="{ maximumFractionDigits: 0 }"
class="mutual-graph__number-input">
<NumberFieldContent>
<NumberFieldInput />
</NumberFieldContent>
</NumberField>
<FieldDescription class="mutual-graph__helper">
{{ t('view.charts.mutual_friend.force_dialog.repulsion_help') }}
</FieldDescription>
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('view.charts.mutual_friend.force_dialog.edge_length_min') }}</FieldLabel>
<FieldContent>
<NumberField
v-model="forceForm.edgeLengthMin"
:step="1"
:format-options="{ maximumFractionDigits: 0 }"
class="mutual-graph__number-input">
<NumberFieldContent>
<NumberFieldInput />
</NumberFieldContent>
</NumberField>
<FieldDescription class="mutual-graph__helper">
{{ t('view.charts.mutual_friend.force_dialog.edge_length_min_help') }}
</FieldDescription>
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('view.charts.mutual_friend.force_dialog.edge_length_max') }}</FieldLabel>
<FieldContent>
<NumberField
v-model="forceForm.edgeLengthMax"
:step="1"
:format-options="{ maximumFractionDigits: 0 }"
class="mutual-graph__number-input">
<NumberFieldContent>
<NumberFieldInput />
</NumberFieldContent>
</NumberField>
<FieldDescription class="mutual-graph__helper">
{{ t('view.charts.mutual_friend.force_dialog.edge_length_max_help') }}
</FieldDescription>
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('view.charts.mutual_friend.force_dialog.gravity') }}</FieldLabel>
<FieldContent>
<NumberField
v-model="forceForm.gravity"
:max="1"
:step="0.1"
:format-options="{ maximumFractionDigits: 1 }"
class="mutual-graph__number-input">
<NumberFieldContent>
<NumberFieldInput />
</NumberFieldContent>
</NumberField>
<FieldDescription class="mutual-graph__helper">
{{ t('view.charts.mutual_friend.force_dialog.gravity_help') }}
</FieldDescription>
</FieldContent>
</Field>
</FieldGroup>
<Dialog v-model:open="isForceDialogVisible">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('view.charts.mutual_friend.force_dialog.title') }}</DialogTitle>
</DialogHeader>
<template #footer>
<div class="mutual-graph__dialog-footer">
<Button variant="secondary" class="mr-2" @click="resetForceSettings">{{
t('view.charts.mutual_friend.force_dialog.reset')
}}</Button>
<Button :disabled="!graphReady" @click="applyForceSettings">
{{ t('view.charts.mutual_friend.force_dialog.apply') }}
</Button>
</div>
</template>
</el-dialog>
<p class="mutual-graph__force-description">
{{ t('view.charts.mutual_friend.force_dialog.description') }}
</p>
<FieldGroup class="mutual-graph__force-form">
<Field>
<FieldLabel>{{ t('view.charts.mutual_friend.force_dialog.repulsion') }}</FieldLabel>
<FieldContent>
<NumberField
v-model="forceForm.repulsion"
:step="1"
:format-options="{ maximumFractionDigits: 0 }"
class="mutual-graph__number-input">
<NumberFieldContent>
<NumberFieldInput />
</NumberFieldContent>
</NumberField>
<FieldDescription class="mutual-graph__helper">
{{ t('view.charts.mutual_friend.force_dialog.repulsion_help') }}
</FieldDescription>
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('view.charts.mutual_friend.force_dialog.edge_length_min') }}</FieldLabel>
<FieldContent>
<NumberField
v-model="forceForm.edgeLengthMin"
:step="1"
:format-options="{ maximumFractionDigits: 0 }"
class="mutual-graph__number-input">
<NumberFieldContent>
<NumberFieldInput />
</NumberFieldContent>
</NumberField>
<FieldDescription class="mutual-graph__helper">
{{ t('view.charts.mutual_friend.force_dialog.edge_length_min_help') }}
</FieldDescription>
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('view.charts.mutual_friend.force_dialog.edge_length_max') }}</FieldLabel>
<FieldContent>
<NumberField
v-model="forceForm.edgeLengthMax"
:step="1"
:format-options="{ maximumFractionDigits: 0 }"
class="mutual-graph__number-input">
<NumberFieldContent>
<NumberFieldInput />
</NumberFieldContent>
</NumberField>
<FieldDescription class="mutual-graph__helper">
{{ t('view.charts.mutual_friend.force_dialog.edge_length_max_help') }}
</FieldDescription>
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('view.charts.mutual_friend.force_dialog.gravity') }}</FieldLabel>
<FieldContent>
<NumberField
v-model="forceForm.gravity"
:max="1"
:step="0.1"
:format-options="{ maximumFractionDigits: 1 }"
class="mutual-graph__number-input">
<NumberFieldContent>
<NumberFieldInput />
</NumberFieldContent>
</NumberField>
<FieldDescription class="mutual-graph__helper">
{{ t('view.charts.mutual_friend.force_dialog.gravity_help') }}
</FieldDescription>
</FieldContent>
</Field>
</FieldGroup>
<DialogFooter>
<div class="mutual-graph__dialog-footer">
<Button variant="secondary" class="mr-2" @click="resetForceSettings">{{
t('view.charts.mutual_friend.force_dialog.reset')
}}</Button>
<Button :disabled="!graphReady" @click="applyForceSettings">
{{ t('view.charts.mutual_friend.force_dialog.apply') }}
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</template>
<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Field, FieldContent, FieldDescription, FieldGroup, FieldLabel } from '@/components/ui/field';
import { NumberField, NumberFieldContent, NumberFieldInput } from '@/components/ui/number-field';
import { Button } from '@/components/ui/button';

View File

@@ -1,64 +1,71 @@
<template>
<el-dialog v-model="isDialogVisible" :title="t('dialog.avatar_export.header')" width="650px">
<div style="margin-bottom: 10px" class="flex flex-col gap-2">
<label v-for="option in exportSelectOptions" :key="option.value" class="inline-flex items-center gap-2">
<Checkbox
:model-value="exportSelectedOptions.includes(option.label)"
@update:modelValue="(val) => toggleAvatarExportOption(option.label, val)" />
<span>{{ option.label }}</span>
</label>
</div>
<Dialog v-model:open="isDialogVisible">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('dialog.avatar_export.header') }}</DialogTitle>
</DialogHeader>
<div class="flex items-center gap-2">
<Select
:model-value="avatarExportFavoriteGroupSelection"
@update:modelValue="handleAvatarExportFavoriteGroupSelect">
<SelectTrigger size="sm">
<SelectValue placeholder="All Favorites" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem :value="AVATAR_EXPORT_ALL_VALUE">All Favorites</SelectItem>
<SelectItem
v-for="groupAPI in favoriteAvatarGroups"
:key="groupAPI.name"
:value="groupAPI.name">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<div style="margin-bottom: 10px" class="flex flex-col gap-2">
<label v-for="option in exportSelectOptions" :key="option.value" class="inline-flex items-center gap-2">
<Checkbox
:model-value="exportSelectedOptions.includes(option.label)"
@update:modelValue="(val) => toggleAvatarExportOption(option.label, val)" />
<span>{{ option.label }}</span>
</label>
</div>
<Select
:model-value="avatarExportLocalFavoriteGroupSelection"
@update:modelValue="handleAvatarExportLocalFavoriteGroupSelect"
style="margin-left: 10px">
<SelectTrigger size="sm">
<SelectValue placeholder="Select Group" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem :value="AVATAR_EXPORT_NONE_VALUE">None</SelectItem>
<SelectItem v-for="group in localAvatarFavoriteGroups" :key="group" :value="group">
{{ group }} ({{ localAvatarFavGroupLength(group) }})
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<br />
<InputGroupTextareaField
v-model="avatarExportContent"
:rows="15"
readonly
style="margin-top: 15px"
input-class="resize-none"
@click="handleCopyAvatarExportData" />
</el-dialog>
<div class="flex items-center gap-2">
<Select
:model-value="avatarExportFavoriteGroupSelection"
@update:modelValue="handleAvatarExportFavoriteGroupSelect">
<SelectTrigger size="sm">
<SelectValue placeholder="All Favorites" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem :value="AVATAR_EXPORT_ALL_VALUE">All Favorites</SelectItem>
<SelectItem
v-for="groupAPI in favoriteAvatarGroups"
:key="groupAPI.name"
:value="groupAPI.name">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Select
:model-value="avatarExportLocalFavoriteGroupSelection"
@update:modelValue="handleAvatarExportLocalFavoriteGroupSelect"
style="margin-left: 10px">
<SelectTrigger size="sm">
<SelectValue placeholder="Select Group" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem :value="AVATAR_EXPORT_NONE_VALUE">None</SelectItem>
<SelectItem v-for="group in localAvatarFavoriteGroups" :key="group" :value="group">
{{ group }} ({{ localAvatarFavGroupLength(group) }})
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<br />
<InputGroupTextareaField
v-model="avatarExportContent"
:rows="15"
readonly
style="margin-top: 15px"
input-class="resize-none"
@click="handleCopyAvatarExportData" />
</DialogContent>
</Dialog>
</template>
<script setup>
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, ref, watch } from 'vue';
import { Checkbox } from '@/components/ui/checkbox';
import { InputGroupTextareaField } from '@/components/ui/input-group';

View File

@@ -1,120 +1,122 @@
<template>
<el-dialog
:z-index="avatarImportDialogIndex"
v-model="isVisible"
:title="t('dialog.avatar_import.header')"
width="650px">
<div style="display: flex; align-items: center; justify-content: space-between">
<div style="font-size: 12px">{{ t('dialog.avatar_import.description') }}</div>
<div style="display: flex; align-items: center">
<div v-if="avatarImportDialog.progress">
{{ t('dialog.avatar_import.process_progress') }} {{ avatarImportDialog.progress }} /
{{ avatarImportDialog.progressTotal }}
<Loader2 style="margin: 0 5px" />
<Dialog v-model:open="isVisible">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('dialog.avatar_import.header') }}</DialogTitle>
</DialogHeader>
<div style="display: flex; align-items: center; justify-content: space-between">
<div style="font-size: 12px">{{ t('dialog.avatar_import.description') }}</div>
<div style="display: flex; align-items: center">
<div v-if="avatarImportDialog.progress">
{{ t('dialog.avatar_import.process_progress') }} {{ avatarImportDialog.progress }} /
{{ avatarImportDialog.progressTotal }}
<Loader2 style="margin: 0 5px" />
</div>
<Button v-if="avatarImportDialog.loading" size="sm" variant="secondary" @click="cancelAvatarImport">
{{ t('dialog.avatar_import.cancel') }}
</Button>
<Button size="sm" v-else :disabled="!avatarImportDialog.input" @click="processAvatarImportList">
{{ t('dialog.avatar_import.process_list') }}
</Button>
</div>
<Button v-if="avatarImportDialog.loading" size="sm" variant="secondary" @click="cancelAvatarImport">
{{ t('dialog.avatar_import.cancel') }}
</Button>
<Button size="sm" v-else :disabled="!avatarImportDialog.input" @click="processAvatarImportList">
{{ t('dialog.avatar_import.process_list') }}
</Button>
</div>
</div>
<InputGroupTextareaField
v-model="avatarImportDialog.input"
:rows="10"
style="margin-top: 10px"
input-class="resize-none" />
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 5px">
<div>
<div class="flex items-center gap-2">
<Select
:model-value="avatarImportFavoriteGroupSelection"
@update:modelValue="handleAvatarImportGroupSelect"
style="margin-right: 5px">
<SelectTrigger size="sm">
<SelectValue :placeholder="t('dialog.avatar_import.select_group_placeholder')" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem
v-for="groupAPI in favoriteAvatarGroups"
:key="groupAPI.name"
:value="groupAPI.name"
:disabled="groupAPI.count >= groupAPI.capacity">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<InputGroupTextareaField
v-model="avatarImportDialog.input"
:rows="10"
style="margin-top: 10px"
input-class="resize-none" />
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 5px">
<div>
<div class="flex items-center gap-2">
<Select
:model-value="avatarImportFavoriteGroupSelection"
@update:modelValue="handleAvatarImportGroupSelect"
style="margin-right: 5px">
<SelectTrigger size="sm">
<SelectValue :placeholder="t('dialog.avatar_import.select_group_placeholder')" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem
v-for="groupAPI in favoriteAvatarGroups"
:key="groupAPI.name"
:value="groupAPI.name"
:disabled="groupAPI.count >= groupAPI.capacity">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Select
:model-value="avatarImportLocalFavoriteGroupSelection"
@update:modelValue="handleAvatarImportLocalGroupSelect"
style="margin-left: 10px">
<SelectTrigger size="sm">
<SelectValue :placeholder="t('dialog.avatar_import.select_group_placeholder')" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem v-for="group in localAvatarFavoriteGroups" :key="group" :value="group">
{{ group }} ({{ localAvatarFavGroupLength(group) }})
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Select
:model-value="avatarImportLocalFavoriteGroupSelection"
@update:modelValue="handleAvatarImportLocalGroupSelect"
style="margin-left: 10px">
<SelectTrigger size="sm">
<SelectValue :placeholder="t('dialog.avatar_import.select_group_placeholder')" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem v-for="group in localAvatarFavoriteGroups" :key="group" :value="group">
{{ group }} ({{ localAvatarFavGroupLength(group) }})
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<span v-if="avatarImportDialog.avatarImportFavoriteGroup" style="margin-left: 5px">
{{ avatarImportTable.data.length }} /
{{
avatarImportDialog.avatarImportFavoriteGroup.capacity -
avatarImportDialog.avatarImportFavoriteGroup.count
}}
</span>
</div>
<div>
<Button size="sm" variant="secondary" class="mr-2" @click="clearAvatarImportTable">
{{ t('dialog.avatar_import.clear_table') }}
</Button>
<Button
size="sm"
:disabled="
avatarImportTable.data.length === 0 ||
(!avatarImportDialog.avatarImportFavoriteGroup &&
!avatarImportDialog.avatarImportLocalFavoriteGroup)
"
@click="importAvatarImportTable">
{{ t('dialog.avatar_import.import') }}
</Button>
</div>
<span v-if="avatarImportDialog.avatarImportFavoriteGroup" style="margin-left: 5px">
{{ avatarImportTable.data.length }} /
{{
avatarImportDialog.avatarImportFavoriteGroup.capacity -
avatarImportDialog.avatarImportFavoriteGroup.count
}}
</span>
</div>
<div>
<Button size="sm" variant="secondary" class="mr-2" @click="clearAvatarImportTable">
{{ t('dialog.avatar_import.clear_table') }}
<span v-if="avatarImportDialog.importProgress" style="margin: 10px">
<Loader2 style="margin-right: 5px" />
{{ t('dialog.avatar_import.import_progress') }}
{{ avatarImportDialog.importProgress }}/{{ avatarImportDialog.importProgressTotal }}
</span>
<br />
<template v-if="avatarImportDialog.errors">
<Button size="sm" variant="secondary" @click="avatarImportDialog.errors = ''">
{{ t('dialog.avatar_import.clear_errors') }}
</Button>
<Button
size="sm"
:disabled="
avatarImportTable.data.length === 0 ||
(!avatarImportDialog.avatarImportFavoriteGroup &&
!avatarImportDialog.avatarImportLocalFavoriteGroup)
"
@click="importAvatarImportTable">
{{ t('dialog.avatar_import.import') }}
</Button>
</div>
</div>
<span v-if="avatarImportDialog.importProgress" style="margin: 10px">
<Loader2 style="margin-right: 5px" />
{{ t('dialog.avatar_import.import_progress') }}
{{ avatarImportDialog.importProgress }}/{{ avatarImportDialog.importProgressTotal }}
</span>
<br />
<template v-if="avatarImportDialog.errors">
<Button size="sm" variant="secondary" @click="avatarImportDialog.errors = ''">
{{ t('dialog.avatar_import.clear_errors') }}
</Button>
<h2 style="font-weight: bold; margin: 5px 0">
{{ t('dialog.avatar_import.errors') }}
</h2>
<pre style="white-space: pre-wrap; font-size: 12px" v-text="avatarImportDialog.errors"></pre>
</template>
<DataTableLayout
class="min-w-0 w-full"
:table="table"
:loading="avatarImportDialog.loading"
:table-style="tableStyle"
:show-pagination="false"
style="margin-top: 10px" />
</el-dialog>
<h2 style="font-weight: bold; margin: 5px 0">
{{ t('dialog.avatar_import.errors') }}
</h2>
<pre style="white-space: pre-wrap; font-size: 12px" v-text="avatarImportDialog.errors"></pre>
</template>
<DataTableLayout
class="min-w-0 w-full"
:table="table"
:loading="avatarImportDialog.loading"
:table-style="tableStyle"
:show-pagination="false"
style="margin-top: 10px" />
</DialogContent>
</Dialog>
</template>
<script setup>
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { DataTableLayout } from '@/components/ui/data-table';
@@ -127,7 +129,6 @@
import { useAvatarStore, useFavoriteStore, useGalleryStore, useUserStore } from '../../../stores';
import { avatarRequest, favoriteRequest } from '../../../api';
import { createColumns } from './avatarImportColumns.jsx';
import { getNextDialogIndex } from '../../../shared/utils/base/ui';
import { removeFromArray } from '../../../shared/utils';
import { useVrcxVueTable } from '../../../lib/table/useVrcxVueTable';
@@ -189,8 +190,6 @@
enableSorting: false
});
const avatarImportDialogIndex = ref(2000);
const isVisible = computed({
get() {
return avatarImportDialogVisible.value;
@@ -204,7 +203,6 @@
() => avatarImportDialogVisible.value,
(value) => {
if (value) {
avatarImportDialogIndex.value = getNextDialogIndex();
clearAvatarImportTable();
resetAvatarImport();
if (avatarImportDialogInput.value) {

View File

@@ -1,41 +1,47 @@
<template>
<el-dialog
v-model="isDialogVisible"
class="x-dialog"
:title="t('dialog.friend_export.header')"
width="650px"
destroy-on-close>
<Select :model-value="friendExportFavoriteGroupSelection" @update:modelValue="handleFriendExportGroupSelect">
<SelectTrigger size="sm">
<SelectValue placeholder="All Favorites" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem :value="FRIEND_EXPORT_ALL_VALUE">All Favorites</SelectItem>
<SelectItem v-for="groupAPI in favoriteFriendGroups" :key="groupAPI.name" :value="groupAPI.name">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Dialog v-model:open="isDialogVisible">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('dialog.friend_export.header') }}</DialogTitle>
</DialogHeader>
<Select
:model-value="friendExportFavoriteGroupSelection"
@update:modelValue="handleFriendExportGroupSelect">
<SelectTrigger size="sm">
<SelectValue placeholder="All Favorites" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem :value="FRIEND_EXPORT_ALL_VALUE">All Favorites</SelectItem>
<SelectItem
v-for="groupAPI in favoriteFriendGroups"
:key="groupAPI.name"
:value="groupAPI.name">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<br />
<br />
<InputGroupTextareaField
v-model="friendExportContent"
:rows="15"
readonly
style="margin-top: 15px"
input-class="resize-none"
@click="handleCopyFriendExportData" />
</el-dialog>
<InputGroupTextareaField
v-model="friendExportContent"
:rows="15"
readonly
style="margin-top: 15px"
input-class="resize-none"
@click="handleCopyFriendExportData" />
</DialogContent>
</Dialog>
</template>
<script setup>
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { InputGroupTextareaField } from '@/components/ui/input-group';
import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';

View File

@@ -1,101 +1,103 @@
<template>
<el-dialog
:z-index="friendImportDialogIndex"
v-model="isVisible"
:title="t('dialog.friend_import.header')"
width="650px">
<div style="display: flex; align-items: center; justify-content: space-between">
<div style="font-size: 12px">{{ t('dialog.friend_import.description') }}</div>
<div style="display: flex; align-items: center">
<div v-if="friendImportDialog.progress">
{{ t('dialog.friend_import.process_progress') }} {{ friendImportDialog.progress }} /
{{ friendImportDialog.progressTotal }}
<Loader2 style="margin: 0 5px" />
<Dialog v-model:open="isVisible">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('dialog.friend_import.header') }}</DialogTitle>
</DialogHeader>
<div style="display: flex; align-items: center; justify-content: space-between">
<div style="font-size: 12px">{{ t('dialog.friend_import.description') }}</div>
<div style="display: flex; align-items: center">
<div v-if="friendImportDialog.progress">
{{ t('dialog.friend_import.process_progress') }} {{ friendImportDialog.progress }} /
{{ friendImportDialog.progressTotal }}
<Loader2 style="margin: 0 5px" />
</div>
<Button v-if="friendImportDialog.loading" size="sm" variant="secondary" @click="cancelFriendImport">
{{ t('dialog.friend_import.cancel') }}
</Button>
<Button size="sm" v-else :disabled="!friendImportDialog.input" @click="processFriendImportList">
{{ t('dialog.friend_import.process_list') }}
</Button>
</div>
<Button v-if="friendImportDialog.loading" size="sm" variant="secondary" @click="cancelFriendImport">
{{ t('dialog.friend_import.cancel') }}
</Button>
<Button size="sm" v-else :disabled="!friendImportDialog.input" @click="processFriendImportList">
{{ t('dialog.friend_import.process_list') }}
</Button>
</div>
</div>
<InputGroupTextareaField
v-model="friendImportDialog.input"
:rows="10"
style="margin-top: 10px"
input-class="resize-none" />
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 5px">
<div>
<Select
:model-value="friendImportFavoriteGroupSelection"
@update:modelValue="handleFriendImportGroupSelect">
<SelectTrigger size="sm">
<SelectValue :placeholder="t('dialog.friend_import.select_group_placeholder')" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem
v-for="groupAPI in favoriteFriendGroups"
:key="groupAPI.name"
:value="groupAPI.name"
:disabled="groupAPI.count >= groupAPI.capacity">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<span v-if="friendImportDialog.friendImportFavoriteGroup" style="margin-left: 5px">
{{ friendImportTable.data.length }} /
{{
friendImportDialog.friendImportFavoriteGroup.capacity -
friendImportDialog.friendImportFavoriteGroup.count
}}
</span>
<InputGroupTextareaField
v-model="friendImportDialog.input"
:rows="10"
style="margin-top: 10px"
input-class="resize-none" />
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 5px">
<div>
<Select
:model-value="friendImportFavoriteGroupSelection"
@update:modelValue="handleFriendImportGroupSelect">
<SelectTrigger size="sm">
<SelectValue :placeholder="t('dialog.friend_import.select_group_placeholder')" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem
v-for="groupAPI in favoriteFriendGroups"
:key="groupAPI.name"
:value="groupAPI.name"
:disabled="groupAPI.count >= groupAPI.capacity">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<span v-if="friendImportDialog.friendImportFavoriteGroup" style="margin-left: 5px">
{{ friendImportTable.data.length }} /
{{
friendImportDialog.friendImportFavoriteGroup.capacity -
friendImportDialog.friendImportFavoriteGroup.count
}}
</span>
</div>
<div>
<Button
size="sm"
class="mr-2"
variant="secondary"
:disabled="friendImportTable.data.length === 0"
@click="clearFriendImportTable">
{{ t('dialog.friend_import.clear_table') }}
</Button>
<Button
size="sm"
:disabled="friendImportTable.data.length === 0 || !friendImportDialog.friendImportFavoriteGroup"
@click="importFriendImportTable">
{{ t('dialog.friend_import.import') }}
</Button>
</div>
</div>
<div>
<Button
size="sm"
class="mr-2"
variant="secondary"
:disabled="friendImportTable.data.length === 0"
@click="clearFriendImportTable">
{{ t('dialog.friend_import.clear_table') }}
<span v-if="friendImportDialog.importProgress" style="margin: 10px">
<Loader2 style="margin-right: 5px" />
{{ t('dialog.friend_import.import_progress') }} {{ friendImportDialog.importProgress }}/{{
friendImportDialog.importProgressTotal
}}
</span>
<br />
<template v-if="friendImportDialog.errors">
<Button size="sm" variant="secondary" @click="friendImportDialog.errors = ''">
{{ t('dialog.friend_import.clear_errors') }}
</Button>
<Button
size="sm"
:disabled="friendImportTable.data.length === 0 || !friendImportDialog.friendImportFavoriteGroup"
@click="importFriendImportTable">
{{ t('dialog.friend_import.import') }}
</Button>
</div>
</div>
<span v-if="friendImportDialog.importProgress" style="margin: 10px">
<Loader2 style="margin-right: 5px" />
{{ t('dialog.friend_import.import_progress') }} {{ friendImportDialog.importProgress }}/{{
friendImportDialog.importProgressTotal
}}
</span>
<br />
<template v-if="friendImportDialog.errors">
<Button size="sm" variant="secondary" @click="friendImportDialog.errors = ''">
{{ t('dialog.friend_import.clear_errors') }}
</Button>
<h2 style="font-weight: bold; margin: 5px 0">{{ t('dialog.friend_import.errors') }}</h2>
<pre style="white-space: pre-wrap; font-size: 12px" v-text="friendImportDialog.errors"></pre>
</template>
<DataTableLayout
class="min-w-0 w-full"
:table="table"
:loading="friendImportDialog.loading"
:table-style="tableStyle"
:show-pagination="false"
style="margin-top: 10px" />
</el-dialog>
<h2 style="font-weight: bold; margin: 5px 0">{{ t('dialog.friend_import.errors') }}</h2>
<pre style="white-space: pre-wrap; font-size: 12px" v-text="friendImportDialog.errors"></pre>
</template>
<DataTableLayout
class="min-w-0 w-full"
:table="table"
:loading="friendImportDialog.loading"
:table-style="tableStyle"
:show-pagination="false"
style="margin-top: 10px" />
</DialogContent>
</Dialog>
</template>
<script setup>
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { DataTableLayout } from '@/components/ui/data-table';
@@ -109,7 +111,6 @@
import { useFavoriteStore, useGalleryStore, useUserStore } from '../../../stores';
import { favoriteRequest, userRequest } from '../../../api';
import { createColumns } from './friendImportColumns.jsx';
import { getNextDialogIndex } from '../../../shared/utils/base/ui';
import { useVrcxVueTable } from '../../../lib/table/useVrcxVueTable';
const { t } = useI18n();
@@ -169,8 +170,6 @@
enableSorting: false
});
const friendImportDialogIndex = ref(2000);
const isVisible = computed({
get() {
return friendImportDialogVisible.value;
@@ -184,7 +183,6 @@
() => friendImportDialogVisible.value,
(value) => {
if (value) {
friendImportDialogIndex.value = getNextDialogIndex();
clearFriendImportTable();
resetFriendImport();
friendImportFavoriteGroupSelection.value =

View File

@@ -1,61 +1,73 @@
<template>
<el-dialog v-model="isDialogVisible" :title="t('dialog.world_export.header')" width="650px">
<div style="margin-bottom: 10px" class="flex flex-col gap-2">
<label v-for="option in exportSelectOptions" :key="option.value" class="inline-flex items-center gap-2">
<Checkbox
:model-value="exportSelectedOptions.includes(option.label)"
@update:modelValue="(val) => toggleWorldExportOption(option.label, val)" />
<span>{{ option.label }}</span>
</label>
</div>
<Dialog v-model:open="isDialogVisible">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('dialog.world_export.header') }}</DialogTitle>
</DialogHeader>
<div class="flex items-center gap-2">
<Select :model-value="worldExportFavoriteGroupSelection" @update:modelValue="handleWorldExportGroupSelect">
<SelectTrigger size="sm">
<SelectValue placeholder="All Favorites" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem :value="WORLD_EXPORT_ALL_VALUE">None</SelectItem>
<SelectItem v-for="groupAPI in favoriteWorldGroups" :key="groupAPI.name" :value="groupAPI.name">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<div style="margin-bottom: 10px" class="flex flex-col gap-2">
<label v-for="option in exportSelectOptions" :key="option.value" class="inline-flex items-center gap-2">
<Checkbox
:model-value="exportSelectedOptions.includes(option.label)"
@update:modelValue="(val) => toggleWorldExportOption(option.label, val)" />
<span>{{ option.label }}</span>
</label>
</div>
<Select
:model-value="worldExportLocalFavoriteGroupSelection"
@update:modelValue="handleWorldExportLocalGroupSelect"
style="margin-left: 10px">
<SelectTrigger size="sm">
<SelectValue placeholder="Select Group" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem :value="WORLD_EXPORT_NONE_VALUE">None</SelectItem>
<SelectItem v-for="group in localWorldFavoriteGroups" :key="group" :value="group">
{{ group }} ({{ localWorldFavorites[group].length }})
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<div class="flex items-center gap-2">
<Select
:model-value="worldExportFavoriteGroupSelection"
@update:modelValue="handleWorldExportGroupSelect">
<SelectTrigger size="sm">
<SelectValue placeholder="All Favorites" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem :value="WORLD_EXPORT_ALL_VALUE">None</SelectItem>
<SelectItem
v-for="groupAPI in favoriteWorldGroups"
:key="groupAPI.name"
:value="groupAPI.name">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<br />
<Select
:model-value="worldExportLocalFavoriteGroupSelection"
@update:modelValue="handleWorldExportLocalGroupSelect"
style="margin-left: 10px">
<SelectTrigger size="sm">
<SelectValue placeholder="Select Group" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem :value="WORLD_EXPORT_NONE_VALUE">None</SelectItem>
<SelectItem v-for="group in localWorldFavoriteGroups" :key="group" :value="group">
{{ group }} ({{ localWorldFavorites[group].length }})
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<InputGroupTextareaField
v-model="worldExportContent"
:rows="15"
readonly
style="margin-top: 15px"
input-class="resize-none"
@click="handleCopyWorldExportData" />
</el-dialog>
<br />
<InputGroupTextareaField
v-model="worldExportContent"
:rows="15"
readonly
style="margin-top: 15px"
input-class="resize-none"
@click="handleCopyWorldExportData" />
</DialogContent>
</Dialog>
</template>
<script setup>
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, ref, watch } from 'vue';
import { Checkbox } from '@/components/ui/checkbox';
import { InputGroupTextareaField } from '@/components/ui/input-group';

View File

@@ -1,125 +1,126 @@
<template>
<el-dialog
:z-index="worldImportDialogIndex"
v-model="isVisible"
:title="t('dialog.world_import.header')"
width="650px"
class="x-dialog">
<div style="display: flex; align-items: center; justify-content: space-between">
<div style="font-size: 12px">{{ t('dialog.world_import.description') }}</div>
<div style="display: flex; align-items: center">
<div v-if="worldImportDialog.progress">
{{ t('dialog.world_import.process_progress') }}
{{ worldImportDialog.progress }} / {{ worldImportDialog.progressTotal }}
<Loader2 style="margin: 0 5px" />
<Dialog v-model:open="isVisible">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('dialog.world_import.header') }}</DialogTitle>
</DialogHeader>
<div style="display: flex; align-items: center; justify-content: space-between">
<div style="font-size: 12px">{{ t('dialog.world_import.description') }}</div>
<div style="display: flex; align-items: center">
<div v-if="worldImportDialog.progress">
{{ t('dialog.world_import.process_progress') }}
{{ worldImportDialog.progress }} / {{ worldImportDialog.progressTotal }}
<Loader2 style="margin: 0 5px" />
</div>
<Button v-if="worldImportDialog.loading" size="sm" variant="outline" @click="cancelWorldImport">
{{ t('dialog.world_import.cancel') }}
</Button>
<Button size="sm" v-else :disabled="!worldImportDialog.input" @click="processWorldImportList">
{{ t('dialog.world_import.process_list') }}
</Button>
</div>
<Button v-if="worldImportDialog.loading" size="sm" variant="outline" @click="cancelWorldImport">
{{ t('dialog.world_import.cancel') }}
</Button>
<Button size="sm" v-else :disabled="!worldImportDialog.input" @click="processWorldImportList">
{{ t('dialog.world_import.process_list') }}
</Button>
</div>
</div>
<InputGroupTextareaField
v-model="worldImportDialog.input"
:rows="10"
style="margin-top: 10px"
input-class="resize-none" />
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 5px">
<div>
<div class="flex items-center gap-2">
<Select
:model-value="worldImportFavoriteGroupSelection"
@update:modelValue="handleWorldImportGroupSelect">
<SelectTrigger size="sm">
<SelectValue :placeholder="t('dialog.world_import.select_vrchat_group_placeholder')" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem
v-for="groupAPI in favoriteWorldGroups"
:key="groupAPI.name"
:value="groupAPI.name"
:disabled="groupAPI.count >= groupAPI.capacity">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<InputGroupTextareaField
v-model="worldImportDialog.input"
:rows="10"
style="margin-top: 10px"
input-class="resize-none" />
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 5px">
<div>
<div class="flex items-center gap-2">
<Select
:model-value="worldImportFavoriteGroupSelection"
@update:modelValue="handleWorldImportGroupSelect">
<SelectTrigger size="sm">
<SelectValue :placeholder="t('dialog.world_import.select_vrchat_group_placeholder')" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem
v-for="groupAPI in favoriteWorldGroups"
:key="groupAPI.name"
:value="groupAPI.name"
:disabled="groupAPI.count >= groupAPI.capacity">
{{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }})
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Select
:model-value="worldImportLocalFavoriteGroupSelection"
@update:modelValue="handleWorldImportLocalGroupSelect"
style="margin-left: 10px">
<SelectTrigger size="sm">
<SelectValue :placeholder="t('dialog.world_import.select_local_group_placeholder')" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem v-for="group in localWorldFavoriteGroups" :key="group" :value="group">
{{ group }} ({{ localWorldFavGroupLength(group) }})
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Select
:model-value="worldImportLocalFavoriteGroupSelection"
@update:modelValue="handleWorldImportLocalGroupSelect"
style="margin-left: 10px">
<SelectTrigger size="sm">
<SelectValue :placeholder="t('dialog.world_import.select_local_group_placeholder')" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem v-for="group in localWorldFavoriteGroups" :key="group" :value="group">
{{ group }} ({{ localWorldFavGroupLength(group) }})
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<span v-if="worldImportDialog.worldImportFavoriteGroup" style="margin-left: 5px">
{{ worldImportTable.data.length }} /
{{
worldImportDialog.worldImportFavoriteGroup.capacity -
worldImportDialog.worldImportFavoriteGroup.count
}}
</span>
</div>
<div>
<Button
size="sm"
variant="secondary"
class="mr-2"
:disabled="worldImportTable.data.length === 0"
@click="clearWorldImportTable">
{{ t('dialog.world_import.clear_table') }}
</Button>
<Button
size="sm"
:disabled="
worldImportTable.data.length === 0 ||
(!worldImportDialog.worldImportFavoriteGroup &&
!worldImportDialog.worldImportLocalFavoriteGroup)
"
@click="importWorldImportTable">
{{ t('dialog.world_import.import') }}
</Button>
</div>
<span v-if="worldImportDialog.worldImportFavoriteGroup" style="margin-left: 5px">
{{ worldImportTable.data.length }} /
{{
worldImportDialog.worldImportFavoriteGroup.capacity -
worldImportDialog.worldImportFavoriteGroup.count
}}
</span>
</div>
<div>
<Button
size="sm"
variant="secondary"
class="mr-2"
:disabled="worldImportTable.data.length === 0"
@click="clearWorldImportTable">
{{ t('dialog.world_import.clear_table') }}
<span v-if="worldImportDialog.importProgress" style="margin: 10px">
<Loader2 style="margin-right: 5px" />
{{ t('dialog.world_import.import_progress') }}
{{ worldImportDialog.importProgress }}/{{ worldImportDialog.importProgressTotal }}
</span>
<br />
<template v-if="worldImportDialog.errors">
<Button size="sm" variant="secondary" @click="worldImportDialog.errors = ''">
{{ t('dialog.world_import.clear_errors') }}
</Button>
<Button
size="sm"
:disabled="
worldImportTable.data.length === 0 ||
(!worldImportDialog.worldImportFavoriteGroup &&
!worldImportDialog.worldImportLocalFavoriteGroup)
"
@click="importWorldImportTable">
{{ t('dialog.world_import.import') }}
</Button>
</div>
</div>
<span v-if="worldImportDialog.importProgress" style="margin: 10px">
<Loader2 style="margin-right: 5px" />
{{ t('dialog.world_import.import_progress') }}
{{ worldImportDialog.importProgress }}/{{ worldImportDialog.importProgressTotal }}
</span>
<br />
<template v-if="worldImportDialog.errors">
<Button size="sm" variant="secondary" @click="worldImportDialog.errors = ''">
{{ t('dialog.world_import.clear_errors') }}
</Button>
<h2 style="font-weight: bold; margin: 5px 0">
{{ t('dialog.world_import.errors') }}
</h2>
<pre style="white-space: pre-wrap; font-size: 12px" v-text="worldImportDialog.errors"></pre>
</template>
<DataTableLayout
class="min-w-0 w-full"
:table="table"
:loading="worldImportDialog.loading"
:table-style="tableStyle"
:show-pagination="false"
style="margin-top: 10px" />
</el-dialog>
<h2 style="font-weight: bold; margin: 5px 0">
{{ t('dialog.world_import.errors') }}
</h2>
<pre style="white-space: pre-wrap; font-size: 12px" v-text="worldImportDialog.errors"></pre>
</template>
<DataTableLayout
class="min-w-0 w-full"
:table="table"
:loading="worldImportDialog.loading"
:table-style="tableStyle"
:show-pagination="false"
style="margin-top: 10px" />
</DialogContent>
</Dialog>
</template>
<script setup>
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { DataTableLayout } from '@/components/ui/data-table';
@@ -132,7 +133,6 @@
import { useFavoriteStore, useGalleryStore, useUserStore, useWorldStore } from '../../../stores';
import { favoriteRequest, worldRequest } from '../../../api';
import { createColumns } from './worldImportColumns.jsx';
import { getNextDialogIndex } from '../../../shared/utils/base/ui';
import { removeFromArray } from '../../../shared/utils';
import { useVrcxVueTable } from '../../../lib/table/useVrcxVueTable';
@@ -147,8 +147,6 @@
const { t } = useI18n();
const worldImportDialogIndex = ref(2000);
const worldImportDialog = ref({
loading: false,
progress: 0,
@@ -211,7 +209,6 @@
() => worldImportDialogVisible.value,
(visible) => {
if (visible) {
worldImportDialogIndex.value = getNextDialogIndex();
clearWorldImportTable();
resetWorldImport();
if (worldImportDialogInput.value) {

View File

@@ -78,34 +78,37 @@
</div>
</template>
</DataTableLayout>
<el-dialog
v-model="friendsListLoadDialogVisible"
:title="t('view.friend_list.load_dialog_title')"
width="420px"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false"
align-center>
<div style="margin-bottom: 10px" v-text="t('view.friend_list.load_dialog_message')"></div>
<div class="flex items-center gap-2">
<Progress :model-value="friendsListLoadingPercent" class="h-4 w-full" />
<span class="text-xs w-10 text-right">{{ friendsListLoadingPercent }}%</span>
</div>
<div style="margin-top: 10px; text-align: right">
<span>{{ friendsListLoadingCurrent }} / {{ friendsListLoadingTotal }}</span>
</div>
<template #footer>
<Button variant="secondary" @click="cancelFriendsListLoad">
{{ t('view.friend_list.load_cancel') }}
</Button>
</template>
</el-dialog>
<Dialog v-model:open="friendsListLoadDialogVisible">
<DialogContent
:show-close-button="false"
@interact-outside.prevent
@escape-key-down.prevent
class="sm:max-w-[420px]">
<DialogHeader>
<DialogTitle>{{ t('view.friend_list.load_dialog_title') }}</DialogTitle>
</DialogHeader>
<div style="margin-bottom: 10px" v-text="t('view.friend_list.load_dialog_message')"></div>
<div class="flex items-center gap-2">
<Progress :model-value="friendsListLoadingPercent" class="h-4 w-full" />
<span class="text-xs w-10 text-right">{{ friendsListLoadingPercent }}%</span>
</div>
<div style="margin-top: 10px; text-align: right">
<span>{{ friendsListLoadingCurrent }} / {{ friendsListLoadingTotal }}</span>
</div>
<DialogFooter>
<Button variant="secondary" @click="cancelFriendsListLoad">
{{ t('view.friend_list.load_cancel') }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
</template>
<script setup>
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, nextTick, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { InputGroupField } from '@/components/ui/input-group';

View File

@@ -1,33 +1,37 @@
<template>
<el-dialog
class="x-dialog"
:model-value="editAndSendInviteResponseDialog.visible"
@close="cancelEditAndSendInviteResponse"
:title="t('dialog.edit_send_invite_response_message.header')"
width="400px"
append-to-body>
<div style="font-size: 12px">
<span>{{ t('dialog.edit_send_invite_response_message.description') }}</span>
</div>
<InputGroupTextareaField
v-model="editAndSendInviteResponseDialog.newMessage"
:maxlength="64"
:rows="2"
class="mt-2.5"
placeholder=""
show-count />
<template #footer>
<Button variant="secondary" class="mr-2" @click="cancelEditAndSendInviteResponse">{{
t('dialog.edit_send_invite_response_message.cancel')
}}</Button>
<Button @click="saveEditAndSendInviteResponse" :disabled="!editAndSendInviteResponseDialog.newMessage">{{
t('dialog.edit_send_invite_response_message.send')
}}</Button>
</template>
</el-dialog>
<Dialog
:open="editAndSendInviteResponseDialog.visible"
@update:open="(open) => (open ? null : cancelEditAndSendInviteResponse())">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('dialog.edit_send_invite_response_message.header') }}</DialogTitle>
</DialogHeader>
<div style="font-size: 12px">
<span>{{ t('dialog.edit_send_invite_response_message.description') }}</span>
</div>
<InputGroupTextareaField
v-model="editAndSendInviteResponseDialog.newMessage"
:maxlength="64"
:rows="2"
class="mt-2.5"
placeholder=""
show-count />
<DialogFooter>
<Button variant="secondary" class="mr-2" @click="cancelEditAndSendInviteResponse">{{
t('dialog.edit_send_invite_response_message.cancel')
}}</Button>
<Button
@click="saveEditAndSendInviteResponse"
:disabled="!editAndSendInviteResponseDialog.newMessage"
>{{ t('dialog.edit_send_invite_response_message.send') }}</Button
>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<script setup>
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { InputGroupTextareaField } from '@/components/ui/input-group';
import { storeToRefs } from 'pinia';

View File

@@ -1,44 +1,46 @@
<template>
<el-dialog
class="x-dialog"
:model-value="sendInviteRequestResponseDialogVisible"
:title="t('dialog.invite_request_response_message.header')"
width="800px"
append-to-body
@close="cancelSendInviteRequestResponse">
<template v-if="isLocalUserVrcPlusSupporter">
<input class="inviteImageUploadButton" type="file" accept="image/*" @change="inviteImageUpload" />
</template>
<Dialog
:open="sendInviteRequestResponseDialogVisible"
@update:open="(open) => (open ? null : cancelSendInviteRequestResponse())">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('dialog.invite_request_response_message.header') }}</DialogTitle>
</DialogHeader>
<template v-if="isLocalUserVrcPlusSupporter">
<input class="inviteImageUploadButton" type="file" accept="image/*" @change="inviteImageUpload" />
</template>
<DataTableLayout
style="margin-top: 10px"
:table="inviteRequestResponseTable"
:loading="false"
:show-pagination="false"
:on-row-click="handleInviteRequestResponseRowClick" />
<DataTableLayout
style="margin-top: 10px"
:table="inviteRequestResponseTable"
:loading="false"
:show-pagination="false"
:on-row-click="handleInviteRequestResponseRowClick" />
<template #footer>
<Button variant="secondary" class="mr-2" @click="cancelSendInviteRequestResponse">
{{ t('dialog.invite_request_response_message.cancel') }}
</Button>
<Button @click="refreshInviteMessageTableData('requestResponse')">
{{ t('dialog.invite_request_response_message.refresh') }}
</Button>
</template>
<EditAndSendInviteResponseDialog
:edit-and-send-invite-response-dialog="editAndSendInviteResponseDialog"
:send-invite-response-dialog="sendInviteResponseDialog"
@closeInviteDialog="closeInviteDialog"
@closeResponseConfirmDialog="closeResponseConfirmDialog" />
<SendInviteResponseConfirmDialog
:send-invite-response-dialog="sendInviteResponseDialog"
:send-invite-response-confirm-dialog="sendInviteResponseConfirmDialog"
@closeInviteDialog="closeInviteDialog"
@closeResponseConfirmDialog="closeResponseConfirmDialog" />
</el-dialog>
<DialogFooter>
<Button variant="secondary" class="mr-2" @click="cancelSendInviteRequestResponse">
{{ t('dialog.invite_request_response_message.cancel') }}
</Button>
<Button @click="refreshInviteMessageTableData('requestResponse')">
{{ t('dialog.invite_request_response_message.refresh') }}
</Button>
</DialogFooter>
<EditAndSendInviteResponseDialog
:edit-and-send-invite-response-dialog="editAndSendInviteResponseDialog"
:send-invite-response-dialog="sendInviteResponseDialog"
@closeInviteDialog="closeInviteDialog"
@closeResponseConfirmDialog="closeResponseConfirmDialog" />
<SendInviteResponseConfirmDialog
:send-invite-response-dialog="sendInviteResponseDialog"
:send-invite-response-confirm-dialog="sendInviteResponseConfirmDialog"
@closeInviteDialog="closeInviteDialog"
@closeResponseConfirmDialog="closeResponseConfirmDialog" />
</DialogContent>
</Dialog>
</template>
<script setup>
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, ref } from 'vue';
import { Button } from '@/components/ui/button';
import { DataTableLayout } from '@/components/ui/data-table';

View File

@@ -1,25 +1,27 @@
<template>
<el-dialog
class="x-dialog"
:model-value="sendInviteResponseConfirmDialog.visible"
:title="t('dialog.invite_response_message.header')"
width="400px"
append-to-body
@close="cancelInviteResponseConfirm">
<div style="font-size: 12px">
<span>{{ t('dialog.invite_response_message.confirmation') }}</span>
</div>
<Dialog
:open="sendInviteResponseConfirmDialog.visible"
@update:open="(open) => (open ? null : cancelInviteResponseConfirm())">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('dialog.invite_response_message.header') }}</DialogTitle>
</DialogHeader>
<div style="font-size: 12px">
<span>{{ t('dialog.invite_response_message.confirmation') }}</span>
</div>
<template #footer>
<Button variant="secondary" class="mr-2" @click="cancelInviteResponseConfirm">{{
t('dialog.invite_response_message.cancel')
}}</Button>
<Button @click="sendInviteResponseConfirm">{{ t('dialog.invite_response_message.confirm') }}</Button>
</template>
</el-dialog>
<DialogFooter>
<Button variant="secondary" class="mr-2" @click="cancelInviteResponseConfirm">{{
t('dialog.invite_response_message.cancel')
}}</Button>
<Button @click="sendInviteResponseConfirm">{{ t('dialog.invite_response_message.confirm') }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<script setup>
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';

View File

@@ -1,44 +1,44 @@
<template>
<el-dialog
class="x-dialog"
:model-value="sendInviteResponseDialogVisible"
:title="t('dialog.invite_response_message.header')"
width="800px"
append-to-body
@close="cancelSendInviteResponse">
<template v-if="isLocalUserVrcPlusSupporter">
<input class="inviteImageUploadButton" type="file" accept="image/*" @change="inviteImageUpload" />
</template>
<Dialog :open="sendInviteResponseDialogVisible" @update:open="(open) => (open ? null : cancelSendInviteResponse())">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('dialog.invite_response_message.header') }}</DialogTitle>
</DialogHeader>
<template v-if="isLocalUserVrcPlusSupporter">
<input class="inviteImageUploadButton" type="file" accept="image/*" @change="inviteImageUpload" />
</template>
<DataTableLayout
style="margin-top: 10px"
:table="inviteResponseTable"
:loading="false"
:show-pagination="false"
:on-row-click="handleInviteResponseRowClick" />
<DataTableLayout
style="margin-top: 10px"
:table="inviteResponseTable"
:loading="false"
:show-pagination="false"
:on-row-click="handleInviteResponseRowClick" />
<template #footer>
<Button variant="secondary" class="mr-2" @click="cancelSendInviteResponse">{{
t('dialog.invite_response_message.cancel')
}}</Button>
<Button @click="refreshInviteMessageTableData('response')">{{
t('dialog.invite_response_message.refresh')
}}</Button>
</template>
<EditAndSendInviteResponseDialog
:edit-and-send-invite-response-dialog="editAndSendInviteResponseDialog"
:send-invite-response-dialog="sendInviteResponseDialog"
@closeResponseConfirmDialog="closeResponseConfirmDialog"
@closeInviteDialog="closeInviteDialog" />
<SendInviteResponseConfirmDialog
:send-invite-response-dialog="sendInviteResponseDialog"
:send-invite-response-confirm-dialog="sendInviteResponseConfirmDialog"
@closeResponseConfirmDialog="closeResponseConfirmDialog"
@closeInviteDialog="closeInviteDialog" />
</el-dialog>
<DialogFooter>
<Button variant="secondary" class="mr-2" @click="cancelSendInviteResponse">{{
t('dialog.invite_response_message.cancel')
}}</Button>
<Button @click="refreshInviteMessageTableData('response')">{{
t('dialog.invite_response_message.refresh')
}}</Button>
</DialogFooter>
<EditAndSendInviteResponseDialog
:edit-and-send-invite-response-dialog="editAndSendInviteResponseDialog"
:send-invite-response-dialog="sendInviteResponseDialog"
@closeResponseConfirmDialog="closeResponseConfirmDialog"
@closeInviteDialog="closeInviteDialog" />
<SendInviteResponseConfirmDialog
:send-invite-response-dialog="sendInviteResponseDialog"
:send-invite-response-confirm-dialog="sendInviteResponseConfirmDialog"
@closeResponseConfirmDialog="closeResponseConfirmDialog"
@closeInviteDialog="closeInviteDialog" />
</DialogContent>
</Dialog>
</template>
<script setup>
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, ref } from 'vue';
import { Button } from '@/components/ui/button';
import { DataTableLayout } from '@/components/ui/data-table';

View File

@@ -1,60 +1,62 @@
<template>
<el-dialog
class="x-dialog"
v-model="chatboxBlacklistDialog.visible"
:title="t('dialog.chatbox_blacklist.header')"
width="600px">
<div v-if="chatboxBlacklistDialog.visible" v-loading="chatboxBlacklistDialog.loading">
<h2>{{ t('dialog.chatbox_blacklist.keyword_blacklist') }}</h2>
<InputGroupAction
v-for="(item, index) in chatboxBlacklist"
:key="index"
v-model="chatboxBlacklist[index]"
size="sm"
style="margin-top: 5px"
@change="saveChatboxBlacklist">
<template #actions>
<Button
variant="outline"
@click="
chatboxBlacklist.splice(index, 1);
saveChatboxBlacklist();
">
</Button>
</template>
</InputGroupAction>
<Button size="sm" variant="outline" style="margin-top: 5px" @click="chatboxBlacklist.push('')">
{{ t('dialog.chatbox_blacklist.add_item') }}
</Button>
<br />
<h2>{{ t('dialog.chatbox_blacklist.user_blacklist') }}</h2>
<Badge
v-for="user in chatboxUserBlacklist"
:key="user[0]"
variant="outline"
style="margin-right: 5px; margin-top: 5px">
<span>{{ user[1] }}</span>
<button
type="button"
style="
margin-left: 6px;
border: none;
background: transparent;
padding: 0;
display: inline-flex;
align-items: center;
color: inherit;
cursor: pointer;
"
@click="deleteChatboxUserBlacklist(user[0])">
<X class="h-3 w-3" style="line-height: 1" />
</button>
</Badge>
</div>
</el-dialog>
<Dialog v-model:open="chatboxBlacklistDialog.visible">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('dialog.chatbox_blacklist.header') }}</DialogTitle>
</DialogHeader>
<div v-if="chatboxBlacklistDialog.visible" v-loading="chatboxBlacklistDialog.loading">
<h2>{{ t('dialog.chatbox_blacklist.keyword_blacklist') }}</h2>
<InputGroupAction
v-for="(item, index) in chatboxBlacklist"
:key="index"
v-model="chatboxBlacklist[index]"
size="sm"
style="margin-top: 5px"
@change="saveChatboxBlacklist">
<template #actions>
<Button
variant="outline"
@click="
chatboxBlacklist.splice(index, 1);
saveChatboxBlacklist();
">
</Button>
</template>
</InputGroupAction>
<Button size="sm" variant="outline" style="margin-top: 5px" @click="chatboxBlacklist.push('')">
{{ t('dialog.chatbox_blacklist.add_item') }}
</Button>
<br />
<h2>{{ t('dialog.chatbox_blacklist.user_blacklist') }}</h2>
<Badge
v-for="user in chatboxUserBlacklist"
:key="user[0]"
variant="outline"
style="margin-right: 5px; margin-top: 5px">
<span>{{ user[1] }}</span>
<button
type="button"
style="
margin-left: 6px;
border: none;
background: transparent;
padding: 0;
display: inline-flex;
align-items: center;
color: inherit;
cursor: pointer;
"
@click="deleteChatboxUserBlacklist(user[0])">
<X class="h-3 w-3" style="line-height: 1" />
</button>
</Badge>
</div>
</DialogContent>
</Dialog>
</template>
<script setup>
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { InputGroupAction } from '@/components/ui/input-group';
import { X } from 'lucide-vue-next';

View File

@@ -1,33 +1,34 @@
<template>
<el-dialog
class="x-dialog"
:model-value="isAvatarProviderDialogVisible"
:title="t('dialog.avatar_database_provider.header')"
width="600px"
@close="closeDialog">
<div>
<InputGroupAction
v-for="(provider, index) in avatarRemoteDatabaseProviderList"
:key="index"
v-model="avatarRemoteDatabaseProviderList[index]"
size="sm"
style="margin-top: 5px"
@change="saveAvatarProviderList">
<template #actions>
<Button variant="outline" size="icon" @click="removeAvatarProvider(provider)">
<Trash2 />
</Button>
</template>
</InputGroupAction>
<Dialog :open="isAvatarProviderDialogVisible" @update:open="(open) => (open ? null : closeDialog())">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('dialog.avatar_database_provider.header') }}</DialogTitle>
</DialogHeader>
<div>
<InputGroupAction
v-for="(provider, index) in avatarRemoteDatabaseProviderList"
:key="index"
v-model="avatarRemoteDatabaseProviderList[index]"
size="sm"
style="margin-top: 5px"
@change="saveAvatarProviderList">
<template #actions>
<Button variant="outline" size="icon" @click="removeAvatarProvider(provider)">
<Trash2 />
</Button>
</template>
</InputGroupAction>
<Button size="sm" style="margin-top: 5px" @click="avatarRemoteDatabaseProviderList.push('')">
{{ t('dialog.avatar_database_provider.add_provider') }}
</Button>
</div>
</el-dialog>
<Button size="sm" style="margin-top: 5px" @click="avatarRemoteDatabaseProviderList.push('')">
{{ t('dialog.avatar_database_provider.add_provider') }}
</Button>
</div>
</DialogContent>
</Dialog>
</template>
<script setup>
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { InputGroupAction } from '@/components/ui/input-group';
import { Trash2 } from 'lucide-vue-next';

View File

@@ -1,43 +1,43 @@
<template>
<el-dialog
class="x-dialog"
:model-value="changeLogDialog.visible"
:title="t('dialog.change_log.header')"
width="800px"
append-to-body
@close="closeDialog">
<div v-loading="!changeLogDialog.changeLog" class="changelog-dialog">
<h2 v-text="changeLogDialog.buildName"></h2>
<span v-show="changeLogDialog.buildName">
{{ t('dialog.change_log.description') }}
<a class="x-link" @click="openExternalLink('https://www.patreon.com/Natsumi_VRCX')">Patreon</a>,
<a class="x-link" @click="openExternalLink('https://ko-fi.com/natsumi_sama')">Ko-fi</a>.
</span>
<VueShowdown
:markdown="changeLogDialog.changeLog"
flavor="github"
:options="showdownOptions"
@click="handleLinkClick"
style="height: 62vh; overflow-y: auto; margin-top: 10px" />
</div>
<template #footer>
<Button
variant="ghost"
class="mr-2"
@click="openExternalLink('https://github.com/vrcx-team/VRCX/releases')">
{{ t('dialog.change_log.github') }}
</Button>
<Button variant="outline" class="mr-2" @click="openExternalLink('https://patreon.com/Natsumi_VRCX')">
{{ t('dialog.change_log.donate') }}
</Button>
<Button @click="closeDialog">
{{ t('dialog.change_log.close') }}
</Button>
</template>
</el-dialog>
<Dialog v-model:open="changeLogDialog.visible">
<DialogContent class="sm:max-w-4xl">
<DialogHeader>
<DialogTitle>{{ t('dialog.change_log.header') }}</DialogTitle>
</DialogHeader>
<div v-loading="!changeLogDialog.changeLog" class="changelog-dialog">
<h2 v-text="changeLogDialog.buildName"></h2>
<span v-show="changeLogDialog.buildName">
{{ t('dialog.change_log.description') }}
<a class="x-link" @click="openExternalLink('https://www.patreon.com/Natsumi_VRCX')">Patreon</a>,
<a class="x-link" @click="openExternalLink('https://ko-fi.com/natsumi_sama')">Ko-fi</a>.
</span>
<VueShowdown
:markdown="changeLogDialog.changeLog"
flavor="github"
:options="showdownOptions"
@click="handleLinkClick"
style="height: 62vh; overflow-y: auto; margin-top: 10px" />
</div>
<DialogFooter>
<Button
variant="ghost"
class="mr-2"
@click="openExternalLink('https://github.com/vrcx-team/VRCX/releases')">
{{ t('dialog.change_log.github') }}
</Button>
<Button variant="outline" class="mr-2" @click="openExternalLink('https://patreon.com/Natsumi_VRCX')">
{{ t('dialog.change_log.donate') }}
</Button>
<Button @click="closeDialog">
{{ t('dialog.change_log.close') }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<script setup>
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { defineAsyncComponent } from 'vue';
import { storeToRefs } from 'pinia';

View File

@@ -1,49 +1,23 @@
<template>
<el-dialog
:model-value="!!feedFiltersDialogMode"
:title="dialogTitle"
width="600px"
destroy-on-close
@close="handleDialogClose">
<div class="toggle-list" style="height: 75vh; overflow-y: auto">
<div v-for="setting in currentOptions" :key="setting.key" class="toggle-item">
<span class="toggle-name"
>{{ setting.name
}}<TooltipWrapper
v-if="setting.tooltip"
side="top"
style="margin-left: 5px"
:content="setting.tooltip">
<AlertTriangle v-if="setting.tooltipWarning" />
<Info v-else />
</TooltipWrapper>
</span>
<Dialog :open="!!feedFiltersDialogMode" @update:open="(open) => !open && handleDialogClose()">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ dialogTitle }}</DialogTitle>
</DialogHeader>
<div class="toggle-list" style="height: 75vh; overflow-y: auto">
<div v-for="setting in currentOptions" :key="setting.key" class="toggle-item">
<span class="toggle-name"
>{{ setting.name
}}<TooltipWrapper
v-if="setting.tooltip"
side="top"
style="margin-left: 5px"
:content="setting.tooltip">
<AlertTriangle v-if="setting.tooltipWarning" />
<Info v-else />
</TooltipWrapper>
</span>
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="currentSharedFeedFilters[setting.key]"
@update:model-value="
(value) => {
currentSharedFeedFilters[setting.key] = value;
saveSharedFeedFilters();
}
">
<ToggleGroupItem v-for="option in setting.options" :key="option.label" :value="option.label">
{{ t(option.textKey) }}
</ToggleGroupItem>
</ToggleGroup>
</div>
<template v-if="photonLoggingEnabled">
<br />
<div class="toggle-item">
<span class="toggle-name">Photon Event Logging</span>
</div>
<div v-for="setting in photonFeedFiltersOptions" :key="setting.key" class="toggle-item">
<span class="toggle-name">{{ setting.name }}</span>
<ToggleGroup
type="single"
required
@@ -61,22 +35,52 @@
</ToggleGroupItem>
</ToggleGroup>
</div>
</template>
</div>
<template #footer>
<Button variant="secondary" @click="currentResetFunction">{{
t('dialog.shared_feed_filters.reset')
}}</Button>
<Button style="margin-left: 10px" @click="handleDialogClose">{{
t('dialog.shared_feed_filters.close')
}}</Button>
</template>
</el-dialog>
<template v-if="photonLoggingEnabled">
<br />
<div class="toggle-item">
<span class="toggle-name">Photon Event Logging</span>
</div>
<div v-for="setting in photonFeedFiltersOptions" :key="setting.key" class="toggle-item">
<span class="toggle-name">{{ setting.name }}</span>
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="currentSharedFeedFilters[setting.key]"
@update:model-value="
(value) => {
currentSharedFeedFilters[setting.key] = value;
saveSharedFeedFilters();
}
">
<ToggleGroupItem
v-for="option in setting.options"
:key="option.label"
:value="option.label">
{{ t(option.textKey) }}
</ToggleGroupItem>
</ToggleGroup>
</div>
</template>
</div>
<DialogFooter>
<Button variant="secondary" @click="currentResetFunction">{{
t('dialog.shared_feed_filters.reset')
}}</Button>
<Button style="margin-left: 10px" @click="handleDialogClose">{{
t('dialog.shared_feed_filters.close')
}}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<script setup>
import { Info, AlertTriangle } from 'lucide-vue-next';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { AlertTriangle, Info } from 'lucide-vue-next';
import { Button } from '@/components/ui/button';
import { computed } from 'vue';
import { storeToRefs } from 'pinia';

View File

@@ -1,63 +1,64 @@
<template>
<el-dialog
class="x-dialog"
:model-value="isLaunchOptionsDialogVisible"
:title="t('dialog.launch_options.header')"
width="600px"
@close="closeDialog">
<div style="font-size: 12px">
{{ t('dialog.launch_options.description') }} <br />
{{ t('dialog.launch_options.example') }}
<Badge variant="outline"
>--fps=144 --enable-debug-gui --enable-sdk-log-levels --enable-udon-debug-logging
</Badge>
</div>
<InputGroupTextareaField
v-model="launchOptionsDialog.launchArguments"
:autosize="{ minRows: 2, maxRows: 5 }"
:rows="2"
placeholder=""
style="margin-top: 10px"
input-class="resize-none" />
<template v-if="!isLinux">
<div style="font-size: 12px; margin-top: 10px">
{{ t('dialog.launch_options.path_override') }}
<Dialog :open="isLaunchOptionsDialogVisible" @update:open="(open) => (open ? null : closeDialog())">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('dialog.launch_options.header') }}</DialogTitle>
</DialogHeader>
<div style="font-size: 12px">
{{ t('dialog.launch_options.description') }} <br />
{{ t('dialog.launch_options.example') }}
<Badge variant="outline"
>--fps=144 --enable-debug-gui --enable-sdk-log-levels --enable-udon-debug-logging
</Badge>
</div>
<InputGroupTextareaField
v-model="launchOptionsDialog.vrcLaunchPathOverride"
placeholder="C:\Program Files (x86)\Steam\steamapps\common\VRChat"
:rows="1"
style="display: block; margin-top: 10px"
input-class="resize-none min-h-0" />
</template>
v-model="launchOptionsDialog.launchArguments"
:autosize="{ minRows: 2, maxRows: 5 }"
:rows="2"
placeholder=""
style="margin-top: 10px"
input-class="resize-none" />
<template #footer>
<div class="flex items-center justify-between">
<div>
<Button
variant="outline"
class="mr-2"
@click="openExternalLink('https://docs.vrchat.com/docs/launch-options')">
{{ t('dialog.launch_options.vrchat_docs') }}
</Button>
<Button
variant="outline"
@click="openExternalLink('https://docs.unity3d.com/Manual/CommandLineArguments.html')">
{{ t('dialog.launch_options.unity_manual') }}
<template v-if="!isLinux">
<div style="font-size: 12px; margin-top: 10px">
{{ t('dialog.launch_options.path_override') }}
</div>
<InputGroupTextareaField
v-model="launchOptionsDialog.vrcLaunchPathOverride"
placeholder="C:\Program Files (x86)\Steam\steamapps\common\VRChat"
:rows="1"
style="display: block; margin-top: 10px"
input-class="resize-none min-h-0" />
</template>
<DialogFooter>
<div class="flex items-center justify-between">
<div>
<Button
variant="outline"
class="mr-2"
@click="openExternalLink('https://docs.vrchat.com/docs/launch-options')">
{{ t('dialog.launch_options.vrchat_docs') }}
</Button>
<Button
variant="outline"
@click="openExternalLink('https://docs.unity3d.com/Manual/CommandLineArguments.html')">
{{ t('dialog.launch_options.unity_manual') }}
</Button>
</div>
<Button @click="updateLaunchOptions">
{{ t('dialog.launch_options.save') }}
</Button>
</div>
<Button @click="updateLaunchOptions">
{{ t('dialog.launch_options.save') }}
</Button>
</div>
</template>
</el-dialog>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<script setup>
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, ref } from 'vue';
import { Button } from '@/components/ui/button';
import { InputGroupTextareaField } from '@/components/ui/input-group';

View File

@@ -1,92 +1,93 @@
<template>
<el-dialog
class="x-dialog"
:model-value="isNotificationPositionDialogVisible"
:title="t('dialog.notification_position.header')"
width="400px"
@close="closeDialog">
<div style="font-size: 12px">
{{ t('dialog.notification_position.description') }}
</div>
<div class="relative mx-auto mt-4 size-75">
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="80 80 80 100"
style="margin-top: 24px"
xml:space="preserve"
class="absolute inset-0 size-full">
<path
style="fill: black"
d="M291.89,5A3.11,3.11,0,0,1,295,8.11V160.64a3.11,3.11,0,0,1-3.11,3.11H8.11A3.11,3.11,0,0,1,5,160.64V8.11A3.11,3.11,0,0,1,8.11,5H291.89m0-5H8.11A8.11,8.11,0,0,0,0,8.11V160.64a8.11,8.11,0,0,0,8.11,8.11H291.89a8.11,8.11,0,0,0,8.11-8.11V8.11A8.11,8.11,0,0,0,291.89,0Z" />
<rect style="fill: #c4c4c4" x="5" y="5" width="290" height="158.75" rx="2.5" />
</svg>
<RadioGroup
:model-value="notificationPosition"
class="absolute inset-0"
@update:modelValue="changeNotificationPosition">
<RadioGroupItem
id="notificationPosition-topLeft"
aria-label="topLeft"
value="topLeft"
class="absolute top-[20%] left-[10%] -translate-x-1/2 -translate-y-1/2" />
<RadioGroupItem
id="notificationPosition-top"
aria-label="top"
value="top"
class="absolute top-[20%] left-1/2 -translate-x-1/2 -translate-y-1/2" />
<RadioGroupItem
id="notificationPosition-topRight"
aria-label="topRight"
value="topRight"
class="absolute top-[20%] left-[90%] -translate-x-1/2 -translate-y-1/2" />
<RadioGroupItem
id="notificationPosition-centerLeft"
aria-label="centerLeft"
value="centerLeft"
class="absolute top-1/2 left-[10%] -translate-x-1/2 -translate-y-1/2" />
<RadioGroupItem
id="notificationPosition-center"
aria-label="center"
value="center"
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" />
<RadioGroupItem
id="notificationPosition-centerRight"
aria-label="centerRight"
value="centerRight"
class="absolute top-1/2 left-[90%] -translate-x-1/2 -translate-y-1/2" />
<RadioGroupItem
id="notificationPosition-bottomLeft"
aria-label="bottomLeft"
value="bottomLeft"
class="absolute top-[80%] left-[10%] -translate-x-1/2 -translate-y-1/2" />
<RadioGroupItem
id="notificationPosition-bottom"
aria-label="bottom"
value="bottom"
class="absolute top-[80%] left-1/2 -translate-x-1/2 -translate-y-1/2" />
<RadioGroupItem
id="notificationPosition-bottomRight"
aria-label="bottomRight"
value="bottomRight"
class="absolute top-[80%] left-[90%] -translate-x-1/2 -translate-y-1/2" />
</RadioGroup>
</div>
<template #footer>
<div style="display: flex">
<Button @click="closeDialog">
{{ t('dialog.notification_position.ok') }}
</Button>
<Dialog :open="isNotificationPositionDialogVisible" @update:open="(open) => (open ? null : closeDialog())">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('dialog.notification_position.header') }}</DialogTitle>
</DialogHeader>
<div style="font-size: 12px">
{{ t('dialog.notification_position.description') }}
</div>
</template>
</el-dialog>
<div class="relative mx-auto mt-4 size-75">
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="80 80 80 100"
style="margin-top: 24px"
xml:space="preserve"
class="absolute inset-0 size-full">
<path
style="fill: black"
d="M291.89,5A3.11,3.11,0,0,1,295,8.11V160.64a3.11,3.11,0,0,1-3.11,3.11H8.11A3.11,3.11,0,0,1,5,160.64V8.11A3.11,3.11,0,0,1,8.11,5H291.89m0-5H8.11A8.11,8.11,0,0,0,0,8.11V160.64a8.11,8.11,0,0,0,8.11,8.11H291.89a8.11,8.11,0,0,0,8.11-8.11V8.11A8.11,8.11,0,0,0,291.89,0Z" />
<rect style="fill: #c4c4c4" x="5" y="5" width="290" height="158.75" rx="2.5" />
</svg>
<RadioGroup
:model-value="notificationPosition"
class="absolute inset-0"
@update:modelValue="changeNotificationPosition">
<RadioGroupItem
id="notificationPosition-topLeft"
aria-label="topLeft"
value="topLeft"
class="absolute top-[20%] left-[10%] -translate-x-1/2 -translate-y-1/2" />
<RadioGroupItem
id="notificationPosition-top"
aria-label="top"
value="top"
class="absolute top-[20%] left-1/2 -translate-x-1/2 -translate-y-1/2" />
<RadioGroupItem
id="notificationPosition-topRight"
aria-label="topRight"
value="topRight"
class="absolute top-[20%] left-[90%] -translate-x-1/2 -translate-y-1/2" />
<RadioGroupItem
id="notificationPosition-centerLeft"
aria-label="centerLeft"
value="centerLeft"
class="absolute top-1/2 left-[10%] -translate-x-1/2 -translate-y-1/2" />
<RadioGroupItem
id="notificationPosition-center"
aria-label="center"
value="center"
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" />
<RadioGroupItem
id="notificationPosition-centerRight"
aria-label="centerRight"
value="centerRight"
class="absolute top-1/2 left-[90%] -translate-x-1/2 -translate-y-1/2" />
<RadioGroupItem
id="notificationPosition-bottomLeft"
aria-label="bottomLeft"
value="bottomLeft"
class="absolute top-[80%] left-[10%] -translate-x-1/2 -translate-y-1/2" />
<RadioGroupItem
id="notificationPosition-bottom"
aria-label="bottom"
value="bottom"
class="absolute top-[80%] left-1/2 -translate-x-1/2 -translate-y-1/2" />
<RadioGroupItem
id="notificationPosition-bottomRight"
aria-label="bottomRight"
value="bottomRight"
class="absolute top-[80%] left-[90%] -translate-x-1/2 -translate-y-1/2" />
</RadioGroup>
</div>
<DialogFooter>
<div style="display: flex">
<Button @click="closeDialog">
{{ t('dialog.notification_position.ok') }}
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<script setup>
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';

View File

@@ -1,25 +1,25 @@
<template>
<el-dialog
class="x-dialog"
:model-value="ossDialog"
:title="t('dialog.open_source.header')"
width="650px"
@close="closeDialog"
destroy-on-close>
<div v-once style="height: 350px; overflow: hidden scroll; word-break: break-all">
<div>
<span>{{ t('dialog.open_source.description') }}</span>
</div>
<Dialog :open="ossDialog" @update:open="(open) => !open && closeDialog()">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('dialog.open_source.header') }}</DialogTitle>
</DialogHeader>
<div v-once style="height: 350px; overflow: hidden scroll; word-break: break-all">
<div>
<span>{{ t('dialog.open_source.description') }}</span>
</div>
<div v-for="lib in openSourceSoftwareLicenses" :key="lib.name" style="margin-top: 15px">
<p style="font-weight: bold">{{ lib.name }}</p>
<pre style="font-size: 12px; white-space: pre-line">{{ lib.licenseText }}</pre>
<div v-for="lib in openSourceSoftwareLicenses" :key="lib.name" style="margin-top: 15px">
<p style="font-weight: bold">{{ lib.name }}</p>
<pre style="font-size: 12px; white-space: pre-line">{{ lib.licenseText }}</pre>
</div>
</div>
</div>
</el-dialog>
</DialogContent>
</Dialog>
</template>
<script setup>
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { useI18n } from 'vue-i18n';
import { openSourceSoftwareLicenses } from '../../../shared/constants/ossLicenses';

View File

@@ -1,41 +1,41 @@
<template>
<el-dialog
class="x-dialog"
v-model="enablePrimaryPasswordDialog.visible"
:before-close="enablePrimaryPasswordDialog.beforeClose"
:close-on-click-modal="false"
:title="t('dialog.primary_password.header')"
width="400px">
<InputGroupField
v-model="enablePrimaryPasswordDialog.password"
:placeholder="t('dialog.primary_password.password_placeholder')"
type="password"
size="sm"
maxlength="32"
show-password
autofocus />
<InputGroupField
v-model="enablePrimaryPasswordDialog.rePassword"
:placeholder="t('dialog.primary_password.re_input_placeholder')"
type="password"
style="margin-top: 5px"
size="sm"
maxlength="32"
show-password />
<template #footer>
<Button
:disabled="
enablePrimaryPasswordDialog.password.length === 0 ||
enablePrimaryPasswordDialog.password !== enablePrimaryPasswordDialog.rePassword
"
@click="handleSetPrimaryPassword()">
{{ t('dialog.primary_password.ok') }}
</Button>
</template>
</el-dialog>
<Dialog v-model:open="enablePrimaryPasswordDialog.visible">
<DialogContent @interact-outside.prevent>
<DialogHeader>
<DialogTitle>{{ t('dialog.primary_password.header') }}</DialogTitle>
</DialogHeader>
<InputGroupField
v-model="enablePrimaryPasswordDialog.password"
:placeholder="t('dialog.primary_password.password_placeholder')"
type="password"
size="sm"
maxlength="32"
show-password
autofocus />
<InputGroupField
v-model="enablePrimaryPasswordDialog.rePassword"
:placeholder="t('dialog.primary_password.re_input_placeholder')"
type="password"
style="margin-top: 5px"
size="sm"
maxlength="32"
show-password />
<DialogFooter>
<Button
:disabled="
enablePrimaryPasswordDialog.password.length === 0 ||
enablePrimaryPasswordDialog.password !== enablePrimaryPasswordDialog.rePassword
"
@click="handleSetPrimaryPassword()">
{{ t('dialog.primary_password.ok') }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<script setup>
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { InputGroupField } from '@/components/ui/input-group';
import { storeToRefs } from 'pinia';

View File

@@ -1,52 +1,54 @@
<template>
<el-dialog
class="x-dialog"
:model-value="isRegistryBackupDialogVisible"
:title="t('dialog.registry_backup.header')"
width="600px"
@close="closeDialog"
@closed="clearVrcRegistryDialog">
<div style="margin-top: 10px">
<div style="display: flex; align-items: center; justify-content: space-between; font-size: 12px">
<span class="name" style="margin-right: 24px">{{ t('dialog.registry_backup.auto_backup') }}</span>
<Switch :model-value="vrcRegistryAutoBackup" @update:modelValue="setVrcRegistryAutoBackup" />
</div>
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
margin-top: 5px;
">
<span class="name" style="margin-right: 24px">{{ t('dialog.registry_backup.ask_to_restore') }}</span>
<Switch :model-value="vrcRegistryAskRestore" @update:modelValue="setVrcRegistryAskRestore" />
</div>
<DataTableLayout
class="min-w-0 w-full"
:table="table"
:loading="false"
:table-style="tableStyle"
:show-pagination="false"
style="margin-top: 10px" />
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 10px">
<Button size="sm" variant="destructive" @click="deleteVrcRegistry">{{
t('dialog.registry_backup.reset')
}}</Button>
<div class="flex gap-2">
<Button size="sm" variant="outline" @click="promptVrcRegistryBackupName">{{
t('dialog.registry_backup.backup')
}}</Button>
<Button size="sm" variant="outline" @click="restoreVrcRegistryFromFile">{{
t('dialog.registry_backup.restore_from_file')
<Dialog :open="isRegistryBackupDialogVisible" @update:open="(open) => !open && closeAndClearDialog()">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('dialog.registry_backup.header') }}</DialogTitle>
</DialogHeader>
<div style="margin-top: 10px">
<div style="display: flex; align-items: center; justify-content: space-between; font-size: 12px">
<span class="name" style="margin-right: 24px">{{ t('dialog.registry_backup.auto_backup') }}</span>
<Switch :model-value="vrcRegistryAutoBackup" @update:modelValue="setVrcRegistryAutoBackup" />
</div>
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
margin-top: 5px;
">
<span class="name" style="margin-right: 24px">{{
t('dialog.registry_backup.ask_to_restore')
}}</span>
<Switch :model-value="vrcRegistryAskRestore" @update:modelValue="setVrcRegistryAskRestore" />
</div>
<DataTableLayout
class="min-w-0 w-full"
:table="table"
:loading="false"
:table-style="tableStyle"
:show-pagination="false"
style="margin-top: 10px" />
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 10px">
<Button size="sm" variant="destructive" @click="deleteVrcRegistry">{{
t('dialog.registry_backup.reset')
}}</Button>
<div class="flex gap-2">
<Button size="sm" variant="outline" @click="promptVrcRegistryBackupName">{{
t('dialog.registry_backup.backup')
}}</Button>
<Button size="sm" variant="outline" @click="restoreVrcRegistryFromFile">{{
t('dialog.registry_backup.restore_from_file')
}}</Button>
</div>
</div>
</div>
</div>
</el-dialog>
</DialogContent>
</Dialog>
</template>
<script setup>
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { DataTableLayout } from '@/components/ui/data-table';
@@ -201,7 +203,8 @@
document.body.appendChild(fileInput);
fileInput.onchange = function (event) {
const file = event.target.files[0];
const target = /** @type {HTMLInputElement | null} */ (event.target);
const file = target?.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = function () {
@@ -263,6 +266,13 @@
registryBackupTable.value.data = [];
}
function closeAndClearDialog() {
closeDialog();
// TODO: Element Plus had a distinct @closed event after animation.
// If you ever need exact timing, wrap DialogContent with a Transition and call clear on after-leave.
clearVrcRegistryDialog();
}
function closeDialog() {
isRegistryBackupDialogVisible.value = false;
}

View File

@@ -1,136 +1,139 @@
<template>
<el-dialog
class="x-dialog"
:model-value="isTranslationApiDialogVisible"
:title="t('dialog.translation_api.header')"
width="450px"
@close="closeDialog">
<div class="options-container-item">
<span class="name">{{ t('view.settings.appearance.appearance.bio_language') }}</span>
<Select :model-value="bioLanguage" @update:modelValue="setBioLanguage">
<SelectTrigger size="sm" style="float: right">
<SelectValue :placeholder="String(getLanguageName(bioLanguage) || bioLanguage || '')" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem v-for="language in languageCodes" :key="language" :value="language">
{{ getLanguageName(language) }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<br />
<FieldGroup class="mb-3">
<Field>
<FieldLabel>{{ t('dialog.translation_api.mode') }}</FieldLabel>
<FieldContent>
<Select :model-value="form.translationApiType" @update:modelValue="handleTranslationApiTypeChange">
<SelectTrigger size="sm" style="width: 100%">
<SelectValue :placeholder="t('dialog.translation_api.mode')" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="google" :text-value="t('dialog.translation_api.mode_google')">
{{ t('dialog.translation_api.mode_google') }}
</SelectItem>
<SelectItem value="openai" :text-value="t('dialog.translation_api.mode_openai')">
{{ t('dialog.translation_api.mode_openai') }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FieldContent>
</Field>
</FieldGroup>
<template v-if="form.translationApiType === 'google'">
<FieldGroup>
<Field>
<FieldLabel>{{ t('dialog.translation_api.description') }}</FieldLabel>
<FieldContent>
<InputGroupField
v-model="form.translationApiKey"
type="password"
show-password
placeholder="AIzaSy..."
clearable />
</FieldContent>
</Field>
</FieldGroup>
</template>
<template v-if="form.translationApiType === 'openai'">
<FieldGroup>
<Field>
<FieldLabel>{{ t('dialog.translation_api.openai.endpoint') }}</FieldLabel>
<FieldContent>
<InputGroupField
v-model="form.translationApiEndpoint"
placeholder="https://api.openai.com/v1/chat/completions"
clearable />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.translation_api.openai.api_key') }}</FieldLabel>
<FieldContent>
<InputGroupField
v-model="form.translationApiKey"
type="password"
show-password
placeholder="sk-..."
clearable />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.translation_api.openai.model') }}</FieldLabel>
<FieldContent>
<InputGroupField v-model="form.translationApiModel" clearable />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.translation_api.openai.prompt_optional') }}</FieldLabel>
<FieldContent>
<InputGroupTextareaField v-model="form.translationApiPrompt" :rows="3" clearable />
</FieldContent>
</Field>
</FieldGroup>
</template>
<template #footer>
<div class="flex items-center justify-between">
<Button
variant="outline"
v-if="form.translationApiType === 'google'"
@click="
openExternalLink(
'https://translatepress.com/docs/automatic-translation/generate-google-api-key/'
)
">
{{ t('dialog.translation_api.guide') }}
</Button>
<Button
variant="outline"
class="mr-2"
v-if="form.translationApiType === 'openai'"
@click="testOpenAiTranslation">
{{ t('dialog.translation_api.test') }}
</Button>
<div>
<Button style="margin-left: auto" @click="saveTranslationApiConfig">
{{ t('dialog.translation_api.save') }}
</Button>
</div>
<Dialog :open="isTranslationApiDialogVisible" @update:open="(open) => (open ? null : closeDialog())">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('dialog.translation_api.header') }}</DialogTitle>
</DialogHeader>
<div class="options-container-item">
<span class="name">{{ t('view.settings.appearance.appearance.bio_language') }}</span>
<Select :model-value="bioLanguage" @update:modelValue="setBioLanguage">
<SelectTrigger size="sm" style="float: right">
<SelectValue :placeholder="String(getLanguageName(bioLanguage) || bioLanguage || '')" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem v-for="language in languageCodes" :key="language" :value="language">
{{ getLanguageName(language) }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</template>
</el-dialog>
<br />
<FieldGroup class="mb-3">
<Field>
<FieldLabel>{{ t('dialog.translation_api.mode') }}</FieldLabel>
<FieldContent>
<Select
:model-value="form.translationApiType"
@update:modelValue="handleTranslationApiTypeChange">
<SelectTrigger size="sm" style="width: 100%">
<SelectValue :placeholder="t('dialog.translation_api.mode')" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="google" :text-value="t('dialog.translation_api.mode_google')">
{{ t('dialog.translation_api.mode_google') }}
</SelectItem>
<SelectItem value="openai" :text-value="t('dialog.translation_api.mode_openai')">
{{ t('dialog.translation_api.mode_openai') }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FieldContent>
</Field>
</FieldGroup>
<template v-if="form.translationApiType === 'google'">
<FieldGroup>
<Field>
<FieldLabel>{{ t('dialog.translation_api.description') }}</FieldLabel>
<FieldContent>
<InputGroupField
v-model="form.translationApiKey"
type="password"
show-password
placeholder="AIzaSy..."
clearable />
</FieldContent>
</Field>
</FieldGroup>
</template>
<template v-if="form.translationApiType === 'openai'">
<FieldGroup>
<Field>
<FieldLabel>{{ t('dialog.translation_api.openai.endpoint') }}</FieldLabel>
<FieldContent>
<InputGroupField
v-model="form.translationApiEndpoint"
placeholder="https://api.openai.com/v1/chat/completions"
clearable />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.translation_api.openai.api_key') }}</FieldLabel>
<FieldContent>
<InputGroupField
v-model="form.translationApiKey"
type="password"
show-password
placeholder="sk-..."
clearable />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.translation_api.openai.model') }}</FieldLabel>
<FieldContent>
<InputGroupField v-model="form.translationApiModel" clearable />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.translation_api.openai.prompt_optional') }}</FieldLabel>
<FieldContent>
<InputGroupTextareaField v-model="form.translationApiPrompt" :rows="3" clearable />
</FieldContent>
</Field>
</FieldGroup>
</template>
<DialogFooter>
<div class="flex items-center justify-between">
<Button
variant="outline"
v-if="form.translationApiType === 'google'"
@click="
openExternalLink(
'https://translatepress.com/docs/automatic-translation/generate-google-api-key/'
)
">
{{ t('dialog.translation_api.guide') }}
</Button>
<Button
variant="outline"
class="mr-2"
v-if="form.translationApiType === 'openai'"
@click="testOpenAiTranslation">
{{ t('dialog.translation_api.test') }}
</Button>
<div>
<Button style="margin-left: auto" @click="saveTranslationApiConfig">
{{ t('dialog.translation_api.save') }}
</Button>
</div>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<script setup>
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Field, FieldContent, FieldGroup, FieldLabel } from '@/components/ui/field';
import { InputGroupField, InputGroupTextareaField } from '@/components/ui/input-group';
import { reactive, watch } from 'vue';

View File

@@ -1,176 +1,181 @@
<template>
<el-dialog
class="x-dialog"
:model-value="isVRChatConfigDialogVisible"
:title="t('dialog.config_json.header')"
width="420px"
@close="closeDialog">
<div v-loading="loading">
<div style="font-size: 12px; word-break: keep-all">
{{ t('dialog.config_json.description1') }} <br />
{{ t('dialog.config_json.description2') }}
</div>
<br />
<span style="margin-right: 5px">{{ t('dialog.config_json.cache_size') }}</span>
<span v-text="VRChatUsedCacheSize"></span>
<span>/</span>
<span v-text="totalCacheSize"></span>
<span>GB</span>
<TooltipWrapper side="top" :content="t('dialog.config_json.refresh')">
<Button
class="rounded-full"
variant="outline"
size="icon-sm"
:disabled="VRChatCacheSizeLoading"
style="margin-left: 5px"
@click="getVRChatCacheSize">
<Spinner v-if="VRChatCacheSizeLoading" />
<RefreshCw v-else />
</Button>
</TooltipWrapper>
<div style="margin-top: 10px">
<span style="margin-right: 5px">{{ t('dialog.config_json.delete_all_cache') }}</span>
<Button size="sm" variant="outline" style="margin-left: 5px" @click="showDeleteAllVRChatCacheConfirm">{{
t('dialog.config_json.delete_cache')
}}</Button>
</div>
<div style="margin-top: 10px">
<span style="margin-right: 5px">{{ t('dialog.config_json.delete_old_cache') }}</span>
<Button size="sm" variant="outline" style="margin-left: 5px" @click="sweepVRChatCache">{{
t('dialog.config_json.sweep_cache')
}}</Button>
</div>
<div v-for="(item, value) in VRChatConfigList" :key="value" style="display: block; margin-top: 10px">
<span style="word-break: keep-all">{{ item.name }}:</span>
<div style="display: flex">
<InputGroupAction
v-model="VRChatConfigFile[value]"
:placeholder="item.default"
size="sm"
:type="item.type ? item.type : 'text'"
:min="item.min"
:max="item.max"
@input="refreshDialogValues"
style="flex: 1; margin-top: 5px">
<template #actions>
<Button
size="sm"
variant="outline"
v-if="item.folderBrowser"
@click="openConfigFolderBrowser(value)">
</Button>
</template>
</InputGroupAction>
<Dialog :open="isVRChatConfigDialogVisible" @update:open="(open) => (open ? null : closeDialog())">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('dialog.config_json.header') }}</DialogTitle>
</DialogHeader>
<div v-loading="loading">
<div style="font-size: 12px; word-break: keep-all">
{{ t('dialog.config_json.description1') }} <br />
{{ t('dialog.config_json.description2') }}
</div>
</div>
<div style="display: inline-block; margin-top: 10px">
<span>{{ t('dialog.config_json.camera_resolution') }}</span>
<br />
<Select
:model-value="vrchatCameraResolutionKey"
@update:modelValue="(v) => (vrchatCameraResolutionKey = v)">
<SelectTrigger size="sm" style="margin-top: 5px">
<SelectValue :placeholder="getVRChatCameraResolution()" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem
v-for="row in VRChatCameraResolutions"
:key="row.name"
:value="getVRChatResolutionKey(row)">
{{ row.name }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<br />
<div style="display: inline-block; margin-top: 10px">
<span>{{ t('dialog.config_json.spout_resolution') }}</span>
<br />
<Select
:model-value="vrchatSpoutResolutionKey"
@update:modelValue="(v) => (vrchatSpoutResolutionKey = v)">
<SelectTrigger size="sm" style="margin-top: 5px">
<SelectValue :placeholder="getVRChatSpoutResolution()" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem
v-for="row in VRChatScreenshotResolutions"
:key="row.name"
:value="getVRChatResolutionKey(row)">
{{ row.name }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<br />
<div style="display: inline-block; margin-top: 10px">
<span>{{ t('dialog.config_json.screenshot_resolution') }}</span>
<br />
<Select
:model-value="vrchatScreenshotResolutionKey"
@update:modelValue="(v) => (vrchatScreenshotResolutionKey = v)">
<SelectTrigger size="sm" style="margin-top: 5px">
<SelectValue :placeholder="getVRChatScreenshotResolution()" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem
v-for="row in VRChatScreenshotResolutions"
:key="row.name"
:value="getVRChatResolutionKey(row)">
{{ row.name }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<br />
<label class="inline-flex items-center gap-2" style="margin-top: 5px; display: block">
<Checkbox
v-model="VRChatConfigFile.picture_output_split_by_date"
@update:modelValue="refreshDialogValues" />
<span>{{ t('dialog.config_json.picture_sort_by_date') }}</span>
</label>
<label class="inline-flex items-center gap-2" style="margin-top: 5px; display: block">
<Checkbox v-model="VRChatConfigFile.disableRichPresence" @update:modelValue="refreshDialogValues" />
<span>{{ t('dialog.config_json.disable_discord_presence') }}</span>
</label>
</div>
<template #footer>
<div style="display: flex; align-items: center; justify-content: space-between">
<div>
<span style="margin-right: 5px">{{ t('dialog.config_json.cache_size') }}</span>
<span v-text="VRChatUsedCacheSize"></span>
<span>/</span>
<span v-text="totalCacheSize"></span>
<span>GB</span>
<TooltipWrapper side="top" :content="t('dialog.config_json.refresh')">
<Button
variant="ghost"
@click="openExternalLink('https://docs.vrchat.com/docs/configuration-file')"
>{{ t('dialog.config_json.vrchat_docs') }}</Button
class="rounded-full"
variant="outline"
size="icon-sm"
:disabled="VRChatCacheSizeLoading"
style="margin-left: 5px"
@click="getVRChatCacheSize">
<Spinner v-if="VRChatCacheSizeLoading" />
<RefreshCw v-else />
</Button>
</TooltipWrapper>
<div style="margin-top: 10px">
<span style="margin-right: 5px">{{ t('dialog.config_json.delete_all_cache') }}</span>
<Button
size="sm"
variant="outline"
style="margin-left: 5px"
@click="showDeleteAllVRChatCacheConfirm"
>{{ t('dialog.config_json.delete_cache') }}</Button
>
</div>
<div>
<Button variant="secondary" class="mr-2" @click="closeDialog">{{
t('dialog.config_json.cancel')
}}</Button>
<Button :disabled="loading" @click="saveVRChatConfigFile">{{
t('dialog.config_json.save')
<div style="margin-top: 10px">
<span style="margin-right: 5px">{{ t('dialog.config_json.delete_old_cache') }}</span>
<Button size="sm" variant="outline" style="margin-left: 5px" @click="sweepVRChatCache">{{
t('dialog.config_json.sweep_cache')
}}</Button>
</div>
<div v-for="(item, value) in VRChatConfigList" :key="value" style="display: block; margin-top: 10px">
<span style="word-break: keep-all">{{ item.name }}:</span>
<div style="display: flex">
<InputGroupAction
v-model="VRChatConfigFile[value]"
:placeholder="item.default"
size="sm"
:type="item.type ? item.type : 'text'"
:min="item.min"
:max="item.max"
@input="refreshDialogValues"
style="flex: 1; margin-top: 5px">
<template #actions>
<Button
size="sm"
variant="outline"
v-if="item.folderBrowser"
@click="openConfigFolderBrowser(value)">
</Button>
</template>
</InputGroupAction>
</div>
</div>
<div style="display: inline-block; margin-top: 10px">
<span>{{ t('dialog.config_json.camera_resolution') }}</span>
<br />
<Select
:model-value="vrchatCameraResolutionKey"
@update:modelValue="(v) => (vrchatCameraResolutionKey = v)">
<SelectTrigger size="sm" style="margin-top: 5px">
<SelectValue :placeholder="getVRChatCameraResolution()" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem
v-for="row in VRChatCameraResolutions"
:key="row.name"
:value="getVRChatResolutionKey(row)">
{{ row.name }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<br />
<div style="display: inline-block; margin-top: 10px">
<span>{{ t('dialog.config_json.spout_resolution') }}</span>
<br />
<Select
:model-value="vrchatSpoutResolutionKey"
@update:modelValue="(v) => (vrchatSpoutResolutionKey = v)">
<SelectTrigger size="sm" style="margin-top: 5px">
<SelectValue :placeholder="getVRChatSpoutResolution()" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem
v-for="row in VRChatScreenshotResolutions"
:key="row.name"
:value="getVRChatResolutionKey(row)">
{{ row.name }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<br />
<div style="display: inline-block; margin-top: 10px">
<span>{{ t('dialog.config_json.screenshot_resolution') }}</span>
<br />
<Select
:model-value="vrchatScreenshotResolutionKey"
@update:modelValue="(v) => (vrchatScreenshotResolutionKey = v)">
<SelectTrigger size="sm" style="margin-top: 5px">
<SelectValue :placeholder="getVRChatScreenshotResolution()" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem
v-for="row in VRChatScreenshotResolutions"
:key="row.name"
:value="getVRChatResolutionKey(row)">
{{ row.name }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<br />
<label class="inline-flex items-center gap-2" style="margin-top: 5px; display: block">
<Checkbox
v-model="VRChatConfigFile.picture_output_split_by_date"
@update:modelValue="refreshDialogValues" />
<span>{{ t('dialog.config_json.picture_sort_by_date') }}</span>
</label>
<label class="inline-flex items-center gap-2" style="margin-top: 5px; display: block">
<Checkbox v-model="VRChatConfigFile.disableRichPresence" @update:modelValue="refreshDialogValues" />
<span>{{ t('dialog.config_json.disable_discord_presence') }}</span>
</label>
</div>
</template>
</el-dialog>
<DialogFooter>
<div style="display: flex; align-items: center; justify-content: space-between">
<div>
<Button
variant="ghost"
@click="openExternalLink('https://docs.vrchat.com/docs/configuration-file')"
>{{ t('dialog.config_json.vrchat_docs') }}</Button
>
</div>
<div>
<Button variant="secondary" class="mr-2" @click="closeDialog">{{
t('dialog.config_json.cancel')
}}</Button>
<Button :disabled="loading" @click="saveVRChatConfigFile">{{
t('dialog.config_json.save')
}}</Button>
</div>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<script setup>
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';

View File

@@ -1,34 +1,37 @@
<template>
<el-dialog
class="x-dialog"
:model-value="isYouTubeApiDialogVisible"
:title="t('dialog.youtube_api.header')"
width="450px"
@close="closeDialog">
<div style="font-size: 12px">{{ t('dialog.youtube_api.description') }} <br /></div>
<Dialog :open="isYouTubeApiDialogVisible" @update:open="(open) => (open ? null : closeDialog())">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('dialog.youtube_api.header') }}</DialogTitle>
</DialogHeader>
<div style="font-size: 12px">{{ t('dialog.youtube_api.description') }} <br /></div>
<InputGroupTextareaField
v-model="youTubeApiKey"
:placeholder="t('dialog.youtube_api.placeholder')"
:maxlength="39"
:rows="2"
class="mt-2.5"
show-count />
<InputGroupTextareaField
v-model="youTubeApiKey"
:placeholder="t('dialog.youtube_api.placeholder')"
:maxlength="39"
:rows="2"
class="mt-2.5"
show-count />
<template #footer>
<div class="flex items-center justify-between">
<Button variant="outline" @click="openExternalLink('https://smashballoon.com/doc/youtube-api-key/')">
{{ t('dialog.youtube_api.guide') }}
</Button>
<Button style="margin-left: auto" @click="testYouTubeApiKey">
{{ t('dialog.youtube_api.save') }}
</Button>
</div>
</template>
</el-dialog>
<DialogFooter>
<div class="flex items-center justify-between">
<Button
variant="outline"
@click="openExternalLink('https://smashballoon.com/doc/youtube-api-key/')">
{{ t('dialog.youtube_api.guide') }}
</Button>
<Button style="margin-left: auto" @click="testYouTubeApiKey">
{{ t('dialog.youtube_api.save') }}
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<script setup>
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { InputGroupTextareaField } from '@/components/ui/input-group';
import { storeToRefs } from 'pinia';

View File

@@ -1,30 +1,31 @@
<template>
<el-dialog
class="x-dialog"
:model-value="isEditInviteMessageDialogVisible"
:title="t('dialog.edit_invite_message.header')"
width="400px"
@close="closeDialog">
<div style="font-size: 12px">
<span>{{ t('dialog.edit_invite_message.description') }}</span>
<InputGroupTextareaField
v-model="message"
:maxlength="64"
:rows="2"
class="mt-2.5"
placeholder=""
show-count />
</div>
<template #footer>
<Button variant="secondary" class="mr-2" @click="closeDialog">{{
t('dialog.edit_invite_message.cancel')
}}</Button>
<Button @click="saveEditInviteMessage">{{ t('dialog.edit_invite_message.save') }}</Button>
</template>
</el-dialog>
<Dialog :open="isEditInviteMessageDialogVisible" @update:open="(open) => !open && closeDialog()">
<DialogContent class="sm:max-w-sm">
<DialogHeader>
<DialogTitle>{{ t('dialog.edit_invite_message.header') }}</DialogTitle>
</DialogHeader>
<div style="font-size: 12px">
<span>{{ t('dialog.edit_invite_message.description') }}</span>
<InputGroupTextareaField
v-model="message"
:maxlength="64"
:rows="2"
class="mt-2.5"
placeholder=""
show-count />
</div>
<DialogFooter>
<Button variant="secondary" class="mr-2" @click="closeDialog">{{
t('dialog.edit_invite_message.cancel')
}}</Button>
<Button @click="saveEditInviteMessage">{{ t('dialog.edit_invite_message.save') }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<script setup>
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { InputGroupTextareaField } from '@/components/ui/input-group';

View File

@@ -1,45 +1,45 @@
<template>
<el-dialog
class="x-dialog"
:model-value="isEditInviteMessagesDialogVisible"
:title="t('dialog.edit_invite_messages.header')"
width="1000px"
@close="closeDialog">
<TabsUnderline v-model="activeTab" :items="editInviteTabs" :unmount-on-hide="false" class="mt-2.5">
<template #message>
<DataTableLayout
style="margin-top: 10px; cursor: pointer"
:table="inviteMessageTanstackTable"
:loading="false"
:show-pagination="false"
:on-row-click="handleEditInviteMessageRowClick" />
</template>
<template #request>
<DataTableLayout
style="margin-top: 10px; cursor: pointer"
:table="inviteRequestTanstackTable"
:loading="false"
:show-pagination="false"
:on-row-click="handleEditInviteMessageRowClick" />
</template>
<template #requestResponse>
<DataTableLayout
style="margin-top: 10px; cursor: pointer"
:table="inviteRequestResponseTanstackTable"
:loading="false"
:show-pagination="false"
:on-row-click="handleEditInviteMessageRowClick" />
</template>
<template #response>
<DataTableLayout
style="margin-top: 10px; cursor: pointer"
:table="inviteResponseTanstackTable"
:loading="false"
:show-pagination="false"
:on-row-click="handleEditInviteMessageRowClick" />
</template>
</TabsUnderline>
</el-dialog>
<Dialog :open="isEditInviteMessagesDialogVisible" @update:open="(open) => !open && closeDialog()">
<DialogContent class="sm:max-w-5xl">
<DialogHeader>
<DialogTitle>{{ t('dialog.edit_invite_messages.header') }}</DialogTitle>
</DialogHeader>
<TabsUnderline v-model="activeTab" :items="editInviteTabs" :unmount-on-hide="false" class="mt-2.5">
<template #message>
<DataTableLayout
style="margin-top: 10px; cursor: pointer"
:table="inviteMessageTanstackTable"
:loading="false"
:show-pagination="false"
:on-row-click="handleEditInviteMessageRowClick" />
</template>
<template #request>
<DataTableLayout
style="margin-top: 10px; cursor: pointer"
:table="inviteRequestTanstackTable"
:loading="false"
:show-pagination="false"
:on-row-click="handleEditInviteMessageRowClick" />
</template>
<template #requestResponse>
<DataTableLayout
style="margin-top: 10px; cursor: pointer"
:table="inviteRequestResponseTanstackTable"
:loading="false"
:show-pagination="false"
:on-row-click="handleEditInviteMessageRowClick" />
</template>
<template #response>
<DataTableLayout
style="margin-top: 10px; cursor: pointer"
:table="inviteResponseTanstackTable"
:loading="false"
:show-pagination="false"
:on-row-click="handleEditInviteMessageRowClick" />
</template>
</TabsUnderline>
</DialogContent>
</Dialog>
<template v-if="isEditInviteMessagesDialogVisible">
<EditInviteMessageDialog
v-model:isEditInviteMessageDialogVisible="isEditInviteMessageDialogVisible"
@@ -50,6 +50,7 @@
</template>
<script setup>
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, ref, watch } from 'vue';
import { DataTableLayout } from '@/components/ui/data-table';
import { TabsUnderline } from '@/components/ui/tabs';

View File

@@ -1,20 +1,26 @@
<template>
<el-dialog v-model="isVisible" :title="t('dialog.export_own_avatars.header')" width="650px">
<InputGroupTextareaField
v-model="exportAvatarsListCsv"
v-loading="loading"
:rows="15"
readonly
style="margin-top: 15px"
input-class="resize-none"
@click="$event.target.tagName === 'TEXTAREA' && $event.target.select()" />
</el-dialog>
<Dialog v-model:open="isVisible">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('dialog.export_own_avatars.header') }}</DialogTitle>
</DialogHeader>
<InputGroupTextareaField
v-model="exportAvatarsListCsv"
v-loading="loading"
:rows="15"
readonly
style="margin-top: 15px"
input-class="resize-none"
@click="$event.target.tagName === 'TEXTAREA' && $event.target.select()" />
</DialogContent>
</Dialog>
</template>
<script setup>
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { InputGroupTextareaField } from '@/components/ui/input-group';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useAvatarStore, useUserStore } from '../../../stores';

View File

@@ -1,26 +1,27 @@
<template>
<el-dialog
class="x-dialog"
:model-value="discordNamesDialogVisible"
:title="t('dialog.discord_names.header')"
width="650px"
@close="closeDialog">
<div style="font-size: 12px">
{{ t('dialog.discord_names.description') }}
</div>
<InputGroupTextareaField
v-model="discordNamesContent"
:rows="15"
readonly
style="margin-top: 15px"
input-class="resize-none" />
</el-dialog>
<Dialog :open="discordNamesDialogVisible" @update:open="(open) => !open && closeDialog()">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('dialog.discord_names.header') }}</DialogTitle>
</DialogHeader>
<div style="font-size: 12px">
{{ t('dialog.discord_names.description') }}
</div>
<InputGroupTextareaField
v-model="discordNamesContent"
:rows="15"
readonly
style="margin-top: 15px"
input-class="resize-none" />
</DialogContent>
</Dialog>
</template>
<script setup>
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { InputGroupTextareaField } from '@/components/ui/input-group';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useUserStore } from '../../../stores';

View File

@@ -1,29 +1,35 @@
<template>
<el-dialog :title="t('dialog.export_friends_list.header')" v-model="isVisible" width="650px">
<TabsUnderline default-value="csv" :items="exportFriendsTabs" :unmount-on-hide="false" class="mt-2.5">
<template #csv>
<InputGroupTextareaField
v-model="exportFriendsListCsv"
:rows="15"
readonly
style="margin-top: 15px"
input-class="resize-none"
@click="$event.target.tagName === 'TEXTAREA' && $event.target.select()" />
</template>
<template #json>
<InputGroupTextareaField
v-model="exportFriendsListJson"
:rows="15"
readonly
style="margin-top: 15px"
input-class="resize-none"
@click="$event.target.tagName === 'TEXTAREA' && $event.target.select()" />
</template>
</TabsUnderline>
</el-dialog>
<Dialog v-model:open="isVisible">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('dialog.export_friends_list.header') }}</DialogTitle>
</DialogHeader>
<TabsUnderline default-value="csv" :items="exportFriendsTabs" :unmount-on-hide="false" class="mt-2.5">
<template #csv>
<InputGroupTextareaField
v-model="exportFriendsListCsv"
:rows="15"
readonly
style="margin-top: 15px"
input-class="resize-none"
@click="$event.target.tagName === 'TEXTAREA' && $event.target.select()" />
</template>
<template #json>
<InputGroupTextareaField
v-model="exportFriendsListJson"
:rows="15"
readonly
style="margin-top: 15px"
input-class="resize-none"
@click="$event.target.tagName === 'TEXTAREA' && $event.target.select()" />
</template>
</TabsUnderline>
</DialogContent>
</Dialog>
</template>
<script setup>
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, ref, watch } from 'vue';
import { InputGroupTextareaField } from '@/components/ui/input-group';
import { TabsUnderline } from '@/components/ui/tabs';

View File

@@ -1,119 +1,124 @@
<template>
<el-dialog
class="x-dialog"
:model-value="visible"
:title="t('dialog.group_calendar.header')"
width="90vw"
height="80vh"
@close="closeDialog">
<template #header>
<div class="dialog-title-container">
<span>{{ t('dialog.group_calendar.header') }}</span>
<Button size="sm" variant="outline" @click="toggleViewMode" class="view-toggle-btn">
{{
viewMode === 'timeline'
? t('dialog.group_calendar.list_view')
: t('dialog.group_calendar.calendar_view')
}}
</Button>
</div>
<div class="featured-switch">
<span class="featured-switch-text">{{ t('dialog.group_calendar.featured_events') }}</span>
<Switch v-model="showFeaturedEvents" @update:modelValue="toggleFeaturedEvents" />
</div>
</template>
<div class="top-content">
<transition name="el-fade-in-linear" mode="out-in">
<div v-if="viewMode === 'timeline'" key="timeline" class="timeline-view">
<div class="timeline-container">
<el-timeline v-if="groupedTimelineEvents.length">
<el-timeline-item
v-for="(timeGroup, key) of groupedTimelineEvents"
:key="key"
:timestamp="dayjs(timeGroup.startsAt).format('MM-DD ddd') + ' ' + timeGroup.startTime"
placement="top">
<div class="time-group-container">
<GroupCalendarEventCard
v-for="value in timeGroup.events"
:key="value.id"
:event="value"
mode="timeline"
:is-following="isEventFollowing(value.id)"
:card-class="{ 'grouped-card': timeGroup.events.length > 1 }"
@update-following-calendar-data="updateFollowingCalendarData"
@click-action="showGroupDialog(value.ownerId)" />
</div>
</el-timeline-item>
</el-timeline>
<div v-else>{{ t('dialog.group_calendar.no_events') }}</div>
</div>
<Dialog :open="visible" @update:open="(open) => (open ? null : closeDialog())">
<DialogContent class="x-dialog w-[90vw] max-w-[90vw] h-[80vh] overflow-hidden">
<DialogHeader>
<div class="dialog-title-container">
<DialogTitle>{{ t('dialog.group_calendar.header') }}</DialogTitle>
<Button size="sm" variant="outline" @click="toggleViewMode" class="view-toggle-btn">
{{
viewMode === 'timeline'
? t('dialog.group_calendar.list_view')
: t('dialog.group_calendar.calendar_view')
}}
</Button>
</div>
<div class="featured-switch">
<span class="featured-switch-text">{{ t('dialog.group_calendar.featured_events') }}</span>
<Switch v-model="showFeaturedEvents" @update:modelValue="toggleFeaturedEvents" />
</div>
</DialogHeader>
<div class="top-content">
<transition name="el-fade-in-linear" mode="out-in">
<div v-if="viewMode === 'timeline'" key="timeline" class="timeline-view">
<div class="timeline-container">
<el-timeline v-if="groupedTimelineEvents.length">
<el-timeline-item
v-for="(timeGroup, key) of groupedTimelineEvents"
:key="key"
:timestamp="
dayjs(timeGroup.startsAt).format('MM-DD ddd') + ' ' + timeGroup.startTime
"
placement="top">
<div class="time-group-container">
<GroupCalendarEventCard
v-for="value in timeGroup.events"
:key="value.id"
:event="value"
mode="timeline"
:is-following="isEventFollowing(value.id)"
:card-class="{ 'grouped-card': timeGroup.events.length > 1 }"
@update-following-calendar-data="updateFollowingCalendarData"
@click-action="showGroupDialog(value.ownerId)" />
</div>
</el-timeline-item>
</el-timeline>
<div v-else>{{ t('dialog.group_calendar.no_events') }}</div>
</div>
<div class="calendar-container">
<el-calendar v-model="selectedDay" v-loading="isLoading">
<template #date-cell="{ data }">
<div class="date">
<div
class="calendar-date-content"
:class="{ 'has-events': filteredCalendar[formatDateKey(data.date)]?.length }">
{{ dayjs(data.date).format('D') }}
<div class="calendar-container">
<el-calendar v-model="selectedDay" v-loading="isLoading">
<template #date-cell="{ data }">
<div class="date">
<div
v-if="filteredCalendar[formatDateKey(data.date)]?.length"
class="calendar-event-badge"
:class="followingCalendarDate[formatDateKey(data.date)] ? 'has-following' : 'no-following'">
{{ filteredCalendar[formatDateKey(data.date)]?.length }}
class="calendar-date-content"
:class="{
'has-events': filteredCalendar[formatDateKey(data.date)]?.length
}">
{{ dayjs(data.date).format('D') }}
<div
v-if="filteredCalendar[formatDateKey(data.date)]?.length"
class="calendar-event-badge"
:class="
followingCalendarDate[formatDateKey(data.date)]
? 'has-following'
: 'no-following'
">
{{ filteredCalendar[formatDateKey(data.date)]?.length }}
</div>
</div>
</div>
</div>
</template>
</el-calendar>
</div>
</div>
<div v-else key="grid" class="grid-view">
<div class="search-container">
<InputGroupSearch
v-model="searchQuery"
size="sm"
:placeholder="t('dialog.group_calendar.search_placeholder')"
class="search-input" />
</template>
</el-calendar>
</div>
</div>
<div v-else key="grid" class="grid-view">
<div class="search-container">
<InputGroupSearch
v-model="searchQuery"
size="sm"
:placeholder="t('dialog.group_calendar.search_placeholder')"
class="search-input" />
</div>
<div class="groups-grid" v-loading="isLoading">
<div v-if="filteredGroupEvents.length" class="groups-container">
<div v-for="group in filteredGroupEvents" :key="group.groupId" class="group-row">
<div class="group-header" @click="toggleGroup(group.groupId)">
<ArrowRight
class="rotation-transition"
:class="{ rotate: !groupCollapsed[group.groupId] }" />
{{ group.groupName }}
</div>
<div class="events-row" v-show="!groupCollapsed[group.groupId]">
<GroupCalendarEventCard
v-for="event in group.events"
:key="event.id"
:event="event"
mode="grid"
:is-following="isEventFollowing(event.id)"
@update-following-calendar-data="updateFollowingCalendarData"
@click-action="showGroupDialog(event.ownerId)"
card-class="grid-card" />
<div class="groups-grid" v-loading="isLoading">
<div v-if="filteredGroupEvents.length" class="groups-container">
<div v-for="group in filteredGroupEvents" :key="group.groupId" class="group-row">
<div class="group-header" @click="toggleGroup(group.groupId)">
<ArrowRight
class="rotation-transition"
:class="{ rotate: !groupCollapsed[group.groupId] }" />
{{ group.groupName }}
</div>
<div class="events-row" v-show="!groupCollapsed[group.groupId]">
<GroupCalendarEventCard
v-for="event in group.events"
:key="event.id"
:event="event"
mode="grid"
:is-following="isEventFollowing(event.id)"
@update-following-calendar-data="updateFollowingCalendarData"
@click-action="showGroupDialog(event.ownerId)"
card-class="grid-card" />
</div>
</div>
</div>
</div>
<div v-else class="no-events">
{{
searchQuery
? t('dialog.group_calendar.search_no_matching')
: t('dialog.group_calendar.search_no_this_month')
}}
<div v-else class="no-events">
{{
searchQuery
? t('dialog.group_calendar.search_no_matching')
: t('dialog.group_calendar.search_no_this_month')
}}
</div>
</div>
</div>
</div>
</transition>
</div>
</el-dialog>
</transition>
</div>
</DialogContent>
</Dialog>
</template>
<script setup>
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, onMounted, ref, watch } from 'vue';
import { ArrowRight } from 'lucide-vue-next';
import { Button } from '@/components/ui/button';

View File

@@ -1,68 +1,69 @@
<template>
<el-dialog
class="x-dialog"
:model-value="isNoteExportDialogVisible"
:title="t('dialog.note_export.header')"
width="1000px"
@close="closeDialog">
<div style="font-size: 12px">
{{ t('dialog.note_export.description1') }} <br />
{{ t('dialog.note_export.description2') }} <br />
{{ t('dialog.note_export.description3') }} <br />
{{ t('dialog.note_export.description4') }} <br />
{{ t('dialog.note_export.description5') }} <br />
{{ t('dialog.note_export.description6') }} <br />
{{ t('dialog.note_export.description7') }} <br />
{{ t('dialog.note_export.description8') }} <br />
</div>
<Dialog :open="isNoteExportDialogVisible" @update:open="(open) => !open && closeDialog()">
<DialogContent class="sm:max-w-5xl">
<DialogHeader>
<DialogTitle>{{ t('dialog.note_export.header') }}</DialogTitle>
</DialogHeader>
<div style="font-size: 12px">
{{ t('dialog.note_export.description1') }} <br />
{{ t('dialog.note_export.description2') }} <br />
{{ t('dialog.note_export.description3') }} <br />
{{ t('dialog.note_export.description4') }} <br />
{{ t('dialog.note_export.description5') }} <br />
{{ t('dialog.note_export.description6') }} <br />
{{ t('dialog.note_export.description7') }} <br />
{{ t('dialog.note_export.description8') }} <br />
</div>
<Button
size="sm"
class="mr-2"
variant="outline"
:disabled="loading"
style="margin-top: 10px"
@click="updateNoteExportDialog">
{{ t('dialog.note_export.refresh') }}
</Button>
<Button
size="sm"
class="mr-2"
variant="outline"
:disabled="loading"
style="margin-top: 10px"
@click="exportNoteExport">
{{ t('dialog.note_export.export') }}
</Button>
<Button v-if="loading" size="sm" variant="outline" style="margin-top: 10px" @click="cancelNoteExport">
{{ t('dialog.note_export.cancel') }}
</Button>
<span v-if="loading" style="margin: 10px">
<Loader2 style="margin-right: 5px" />
{{ t('dialog.note_export.progress') }} {{ progress }}/{{ progressTotal }}
</span>
<template v-if="errors">
<Button size="sm" variant="outline" @click="errors = ''">
{{ t('dialog.note_export.clear_errors') }}
<Button
size="sm"
class="mr-2"
variant="outline"
:disabled="loading"
style="margin-top: 10px"
@click="updateNoteExportDialog">
{{ t('dialog.note_export.refresh') }}
</Button>
<h2 style="font-weight: bold; margin: 0">
{{ t('dialog.note_export.errors') }}
</h2>
<pre style="white-space: pre-wrap; font-size: 12px" v-text="errors"></pre>
</template>
<Button
size="sm"
class="mr-2"
variant="outline"
:disabled="loading"
style="margin-top: 10px"
@click="exportNoteExport">
{{ t('dialog.note_export.export') }}
</Button>
<Button v-if="loading" size="sm" variant="outline" style="margin-top: 10px" @click="cancelNoteExport">
{{ t('dialog.note_export.cancel') }}
</Button>
<span v-if="loading" style="margin: 10px">
<Loader2 style="margin-right: 5px" />
{{ t('dialog.note_export.progress') }} {{ progress }}/{{ progressTotal }}
</span>
<DataTableLayout
class="min-w-0 w-full"
:table="table"
:loading="loading"
:table-style="tableStyle"
:show-pagination="false"
style="margin-top: 10px" />
</el-dialog>
<template v-if="errors">
<Button size="sm" variant="outline" @click="errors = ''">
{{ t('dialog.note_export.clear_errors') }}
</Button>
<h2 style="font-weight: bold; margin: 0">
{{ t('dialog.note_export.errors') }}
</h2>
<pre style="white-space: pre-wrap; font-size: 12px" v-text="errors"></pre>
</template>
<DataTableLayout
class="min-w-0 w-full"
:table="table"
:loading="loading"
:table-style="tableStyle"
:show-pagination="false"
style="margin-top: 10px" />
</DialogContent>
</Dialog>
</template>
<script setup>
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { DataTableLayout } from '@/components/ui/data-table';