mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-19 14:53:50 +02:00
change screenshot metadata dialog to route view
This commit is contained in:
@@ -20,6 +20,7 @@ import Search from './../views/Search/Search.vue';
|
|||||||
import Settings from './../views/Settings/Settings.vue';
|
import Settings from './../views/Settings/Settings.vue';
|
||||||
import Tools from './../views/Tools/Tools.vue';
|
import Tools from './../views/Tools/Tools.vue';
|
||||||
import Gallery from './../views/Tools/Gallery.vue';
|
import Gallery from './../views/Tools/Gallery.vue';
|
||||||
|
import ScreenshotMetadata from './../views/Tools/ScreenshotMetadata.vue';
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@@ -90,6 +91,12 @@ const routes = [
|
|||||||
component: Gallery,
|
component: Gallery,
|
||||||
meta: { navKey: 'tools' }
|
meta: { navKey: 'tools' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'tools/screenshot-metadata',
|
||||||
|
name: 'screenshot-metadata',
|
||||||
|
component: ScreenshotMetadata,
|
||||||
|
meta: { navKey: 'tools' }
|
||||||
|
},
|
||||||
{ path: 'settings', name: 'settings', component: Settings }
|
{ path: 'settings', name: 'settings', component: Settings }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<div class="screenshot-metadata-page x-container">
|
||||||
class="x-dialog"
|
<div class="screenshot-metadata-page__header">
|
||||||
:model-value="isScreenshotMetadataDialogVisible"
|
<Button variant="ghost" class="screenshot-metadata-page__back" @click="goBack">
|
||||||
:title="t('dialog.screenshot_metadata.header')"
|
{{ t('nav_tooltip.tools') }}
|
||||||
width="1050px"
|
</Button>
|
||||||
@close="closeDialog">
|
<span class="header">{{ t('dialog.screenshot_metadata.header') }}</span>
|
||||||
<div
|
</div>
|
||||||
v-loading="screenshotMetadataDialog.loading"
|
<div v-loading="screenshotMetadataDialog.loading" @dragover.prevent @dragenter.prevent @drop="handleDrop">
|
||||||
style="-webkit-app-region: drag"
|
|
||||||
@dragover.prevent
|
|
||||||
@dragenter.prevent
|
|
||||||
@drop="handleDrop">
|
|
||||||
<span style="margin-left: 5px; color: var(--el-text-color-secondary); font-family: monospace">{{
|
<span style="margin-left: 5px; color: var(--el-text-color-secondary); font-family: monospace">{{
|
||||||
t('dialog.screenshot_metadata.drag')
|
t('dialog.screenshot_metadata.drag')
|
||||||
}}</span>
|
}}</span>
|
||||||
@@ -55,13 +51,11 @@
|
|||||||
<br />
|
<br />
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<!-- Search bar input -->
|
|
||||||
<InputGroupSearch
|
<InputGroupSearch
|
||||||
v-model="screenshotMetadataDialog.search"
|
v-model="screenshotMetadataDialog.search"
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
style="width: 200px"
|
style="width: 200px"
|
||||||
@input="screenshotMetadataSearch" />
|
@input="screenshotMetadataSearch" />
|
||||||
<!-- Search type dropdown -->
|
|
||||||
<Select :model-value="screenshotMetadataDialog.searchType" @update:modelValue="handleSearchTypeChange">
|
<Select :model-value="screenshotMetadataDialog.searchType" @update:modelValue="handleSearchTypeChange">
|
||||||
<SelectTrigger size="sm" style="width: 150px; margin-left: 10px">
|
<SelectTrigger size="sm" style="width: 150px; margin-left: 10px">
|
||||||
<SelectValue placeholder="Search Type" />
|
<SelectValue placeholder="Search Type" />
|
||||||
@@ -75,7 +69,6 @@
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<!-- Search index/total label -->
|
|
||||||
<template v-if="screenshotMetadataDialog.searchIndex !== null">
|
<template v-if="screenshotMetadataDialog.searchIndex !== null">
|
||||||
<span style="white-space: pre-wrap; font-size: 12px; margin-left: 10px">{{
|
<span style="white-space: pre-wrap; font-size: 12px; margin-left: 10px">{{
|
||||||
screenshotMetadataDialog.searchIndex + 1 + '/' + screenshotMetadataDialog.searchResults.length
|
screenshotMetadataDialog.searchIndex + 1 + '/' + screenshotMetadataDialog.searchResults.length
|
||||||
@@ -110,7 +103,7 @@
|
|||||||
:hint="screenshotMetadataDialog.metadata.author.displayName"
|
:hint="screenshotMetadataDialog.metadata.author.displayName"
|
||||||
style="color: var(--el-text-color-secondary); font-family: monospace" />
|
style="color: var(--el-text-color-secondary); font-family: monospace" />
|
||||||
<br />
|
<br />
|
||||||
<div class="my-2 w-[95%] ml-6.5">
|
<div class="my-2 w-[90%] ml-17">
|
||||||
<Carousel :opts="{ loop: false }" @init-api="handleScreenshotMetadataCarouselInit">
|
<Carousel :opts="{ loop: false }" @init-api="handleScreenshotMetadataCarouselInit">
|
||||||
<CarouselContent class="h-150">
|
<CarouselContent class="h-150">
|
||||||
<CarouselItem>
|
<CarouselItem>
|
||||||
@@ -158,60 +151,36 @@
|
|||||||
<br />
|
<br />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/components/ui/carousel';
|
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/components/ui/carousel';
|
||||||
import { reactive, ref, watch } from 'vue';
|
import { onBeforeUnmount, onMounted, reactive, ref } from 'vue';
|
||||||
|
import { useGalleryStore, useUserStore, useVrcxStore } from '@/stores';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { InputGroupSearch } from '@/components/ui/input-group';
|
import { InputGroupSearch } from '@/components/ui/input-group';
|
||||||
|
import { formatDateFilter } from '@/shared/utils';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { vrcPlusImageRequest } from '@/api';
|
||||||
|
|
||||||
import { useGalleryStore, useUserStore, useVrcxStore } from '../../../stores';
|
const router = useRouter();
|
||||||
import { Badge } from '../../../components/ui/badge';
|
const { t } = useI18n();
|
||||||
import { formatDateFilter } from '../../../shared/utils';
|
|
||||||
import { vrcPlusImageRequest } from '../../../api';
|
|
||||||
|
|
||||||
const { showFullscreenImageDialog, handleGalleryImageAdd } = useGalleryStore();
|
const { showFullscreenImageDialog, handleGalleryImageAdd } = useGalleryStore();
|
||||||
const { currentlyDroppingFile } = storeToRefs(useVrcxStore());
|
const { currentlyDroppingFile } = storeToRefs(useVrcxStore());
|
||||||
const { isLocalUserVrcPlusSupporter } = storeToRefs(useUserStore());
|
const { isLocalUserVrcPlusSupporter } = storeToRefs(useUserStore());
|
||||||
|
const { fullscreenImageDialog } = storeToRefs(useGalleryStore());
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const { lookupUser } = userStore;
|
const { lookupUser } = userStore;
|
||||||
|
|
||||||
const { fullscreenImageDialog } = storeToRefs(useGalleryStore());
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
isScreenshotMetadataDialogVisible: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['close']);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.isScreenshotMetadataDialogVisible,
|
|
||||||
(newVal) => {
|
|
||||||
if (newVal) {
|
|
||||||
if (!screenshotMetadataDialog.metadata.filePath) {
|
|
||||||
getAndDisplayLastScreenshot();
|
|
||||||
}
|
|
||||||
window.addEventListener('keyup', handleComponentKeyup);
|
|
||||||
} else {
|
|
||||||
window.removeEventListener('keyup', handleComponentKeyup);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const screenshotMetadataDialog = reactive({
|
const screenshotMetadataDialog = reactive({
|
||||||
// visible: false,
|
|
||||||
loading: false,
|
loading: false,
|
||||||
search: '',
|
search: '',
|
||||||
searchType: 'Player Name',
|
searchType: 'Player Name',
|
||||||
@@ -226,9 +195,20 @@
|
|||||||
const screenshotMetadataCarouselApi = ref(null);
|
const screenshotMetadataCarouselApi = ref(null);
|
||||||
const ignoreCarouselSelect = ref(false);
|
const ignoreCarouselSelect = ref(false);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!screenshotMetadataDialog.metadata.filePath) {
|
||||||
|
getAndDisplayLastScreenshot();
|
||||||
|
}
|
||||||
|
window.addEventListener('keyup', handleComponentKeyup);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('keyup', handleComponentKeyup);
|
||||||
|
});
|
||||||
|
|
||||||
const handleComponentKeyup = (event) => {
|
const handleComponentKeyup = (event) => {
|
||||||
const carouselNavigation = { ArrowLeft: 0, ArrowRight: 2 }[event.key];
|
const carouselNavigation = { ArrowLeft: 0, ArrowRight: 2 }[event.key];
|
||||||
if (typeof carouselNavigation !== 'undefined' && props.isScreenshotMetadataDialogVisible) {
|
if (typeof carouselNavigation !== 'undefined') {
|
||||||
if (screenshotMetadataCarouselApi.value) {
|
if (screenshotMetadataCarouselApi.value) {
|
||||||
if (event.key === 'ArrowLeft') {
|
if (event.key === 'ArrowLeft') {
|
||||||
screenshotMetadataCarouselApi.value.scrollPrev();
|
screenshotMetadataCarouselApi.value.scrollPrev();
|
||||||
@@ -241,8 +221,8 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function closeDialog() {
|
function goBack() {
|
||||||
emit('close');
|
router.push({ name: 'tools' });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDrop(event) {
|
function handleDrop(event) {
|
||||||
@@ -261,7 +241,7 @@
|
|||||||
let filePath = '';
|
let filePath = '';
|
||||||
|
|
||||||
if (LINUX) {
|
if (LINUX) {
|
||||||
filePath = await window.electron.openFileDialog(); // PNG filter is applied in main.js
|
filePath = await window.electron.openFileDialog();
|
||||||
} else {
|
} else {
|
||||||
filePath = await AppApi.OpenFileSelectorDialog(
|
filePath = await AppApi.OpenFileSelectorDialog(
|
||||||
await AppApi.GetVRChatPhotosLocation(),
|
await AppApi.GetVRChatPhotosLocation(),
|
||||||
@@ -347,7 +327,6 @@
|
|||||||
function screenshotMetadataSearch() {
|
function screenshotMetadataSearch() {
|
||||||
const D = screenshotMetadataDialog;
|
const D = screenshotMetadataDialog;
|
||||||
|
|
||||||
// Don't search if user is still typing
|
|
||||||
screenshotMetadataSearchInputs.value++;
|
screenshotMetadataSearchInputs.value++;
|
||||||
let current = screenshotMetadataSearchInputs.value;
|
let current = screenshotMetadataSearchInputs.value;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -359,13 +338,12 @@
|
|||||||
if (D.search === '') {
|
if (D.search === '') {
|
||||||
screenshotMetadataResetSearch();
|
screenshotMetadataResetSearch();
|
||||||
if (D.metadata.filePath !== null) {
|
if (D.metadata.filePath !== null) {
|
||||||
// Re-retrieve the current screenshot metadata and get previous/next files for regular carousel directory navigation
|
|
||||||
getAndDisplayScreenshot(D.metadata.filePath, true);
|
getAndDisplayScreenshot(D.metadata.filePath, true);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchType = D.searchTypes.indexOf(D.searchType); // Matches the search type enum in .NET
|
const searchType = D.searchTypes.indexOf(D.searchType);
|
||||||
D.loading = true;
|
D.loading = true;
|
||||||
AppApi.FindScreenshotsBySearch(D.search, searchType)
|
AppApi.FindScreenshotsBySearch(D.search, searchType)
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
@@ -383,7 +361,6 @@
|
|||||||
D.searchIndex = 0;
|
D.searchIndex = 0;
|
||||||
D.searchResults = results;
|
D.searchResults = results;
|
||||||
|
|
||||||
// console.log("Search results", results)
|
|
||||||
getAndDisplayScreenshot(results[0], false);
|
getAndDisplayScreenshot(results[0], false);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@@ -530,15 +507,12 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get extra data for display dialog like resolution, file size, etc
|
|
||||||
D.loading = true;
|
D.loading = true;
|
||||||
const extraData = await AppApi.GetExtraScreenshotData(metadata.sourceFile, needsCarouselFiles);
|
const extraData = await AppApi.GetExtraScreenshotData(metadata.sourceFile, needsCarouselFiles);
|
||||||
D.loading = false;
|
D.loading = false;
|
||||||
const extraDataObj = JSON.parse(extraData);
|
const extraDataObj = JSON.parse(extraData);
|
||||||
Object.assign(metadata, extraDataObj);
|
Object.assign(metadata, extraDataObj);
|
||||||
|
|
||||||
// console.log("Displaying screenshot metadata", json, "extra data", extraDataObj, "path", json.filePath)
|
|
||||||
|
|
||||||
D.metadata = metadata;
|
D.metadata = metadata;
|
||||||
|
|
||||||
const regex = metadata.fileName?.match(
|
const regex = metadata.fileName?.match(
|
||||||
@@ -546,19 +520,13 @@
|
|||||||
);
|
);
|
||||||
if (regex) {
|
if (regex) {
|
||||||
if (typeof regex[2] !== 'undefined' && regex[4].length === 4) {
|
if (typeof regex[2] !== 'undefined' && regex[4].length === 4) {
|
||||||
// old format
|
|
||||||
// VRChat_3840x2160_2022-02-02_03-21-39.771
|
|
||||||
date = `${regex[4]}-${regex[5]}-${regex[6]}`;
|
date = `${regex[4]}-${regex[5]}-${regex[6]}`;
|
||||||
time = `${regex[7]}:${regex[8]}:${regex[9]}`;
|
time = `${regex[7]}:${regex[8]}:${regex[9]}`;
|
||||||
D.metadata.dateTime = Date.parse(`${date} ${time}`);
|
D.metadata.dateTime = Date.parse(`${date} ${time}`);
|
||||||
// D.metadata.resolution = `${regex[2]}x${regex[3]}`;
|
|
||||||
} else if (typeof regex[11] !== 'undefined' && regex[11].length === 4) {
|
} else if (typeof regex[11] !== 'undefined' && regex[11].length === 4) {
|
||||||
// new format
|
|
||||||
// VRChat_2023-02-16_10-39-25.274_3840x2160
|
|
||||||
date = `${regex[11]}-${regex[12]}-${regex[13]}`;
|
date = `${regex[11]}-${regex[12]}-${regex[13]}`;
|
||||||
time = `${regex[14]}:${regex[15]}:${regex[16]}`;
|
time = `${regex[14]}:${regex[15]}:${regex[16]}`;
|
||||||
D.metadata.dateTime = Date.parse(`${date} ${time}`);
|
D.metadata.dateTime = Date.parse(`${date} ${time}`);
|
||||||
// D.metadata.resolution = `${regex[18]}x${regex[19]}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (metadata.timestamp) {
|
if (metadata.timestamp) {
|
||||||
@@ -573,3 +541,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.screenshot-metadata-page__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="tools-grid" v-show="!categoryCollapsed['image']">
|
<div class="tools-grid" v-show="!categoryCollapsed['image']">
|
||||||
<el-card :body-style="{ padding: '0px' }" class="tool-card">
|
<el-card :body-style="{ padding: '0px' }" class="tool-card">
|
||||||
<div class="tool-content" @click="showScreenshotMetadataDialog">
|
<div class="tool-content" @click="showScreenshotMetadataPage">
|
||||||
<div class="tool-icon">
|
<div class="tool-icon">
|
||||||
<i class="ri-screenshot-2-line"></i>
|
<i class="ri-screenshot-2-line"></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -185,9 +185,6 @@
|
|||||||
<GroupCalendarDialog
|
<GroupCalendarDialog
|
||||||
:visible="isGroupCalendarDialogVisible"
|
:visible="isGroupCalendarDialogVisible"
|
||||||
@close="isGroupCalendarDialogVisible = false" />
|
@close="isGroupCalendarDialogVisible = false" />
|
||||||
<ScreenshotMetadataDialog
|
|
||||||
:isScreenshotMetadataDialogVisible="isScreenshotMetadataDialogVisible"
|
|
||||||
@close="isScreenshotMetadataDialogVisible = false" />
|
|
||||||
<NoteExportDialog
|
<NoteExportDialog
|
||||||
:isNoteExportDialogVisible="isNoteExportDialogVisible"
|
:isNoteExportDialogVisible="isNoteExportDialogVisible"
|
||||||
@close="isNoteExportDialogVisible = false" />
|
@close="isNoteExportDialogVisible = false" />
|
||||||
@@ -207,16 +204,15 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, defineAsyncComponent, ref } from 'vue';
|
import { computed, defineAsyncComponent, ref } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { ArrowRight } from '@element-plus/icons-vue';
|
import { ArrowRight } from '@element-plus/icons-vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
|
|
||||||
import { useFriendStore, useGalleryStore } from '../../stores';
|
import { useFriendStore, useGalleryStore } from '../../stores';
|
||||||
|
|
||||||
const GroupCalendarDialog = defineAsyncComponent(() => import('./dialogs/GroupCalendarDialog.vue'));
|
const GroupCalendarDialog = defineAsyncComponent(() => import('./dialogs/GroupCalendarDialog.vue'));
|
||||||
const ScreenshotMetadataDialog = defineAsyncComponent(() => import('./dialogs/ScreenshotMetadataDialog.vue'));
|
|
||||||
const NoteExportDialog = defineAsyncComponent(() => import('./dialogs/NoteExportDialog.vue'));
|
const NoteExportDialog = defineAsyncComponent(() => import('./dialogs/NoteExportDialog.vue'));
|
||||||
const EditInviteMessageDialog = defineAsyncComponent(() => import('./dialogs/EditInviteMessagesDialog.vue'));
|
const EditInviteMessageDialog = defineAsyncComponent(() => import('./dialogs/EditInviteMessagesDialog.vue'));
|
||||||
const ExportDiscordNamesDialog = defineAsyncComponent(() => import('./dialogs/ExportDiscordNamesDialog.vue'));
|
const ExportDiscordNamesDialog = defineAsyncComponent(() => import('./dialogs/ExportDiscordNamesDialog.vue'));
|
||||||
@@ -224,6 +220,7 @@
|
|||||||
const ExportAvatarsListDialog = defineAsyncComponent(() => import('./dialogs/ExportAvatarsListDialog.vue'));
|
const ExportAvatarsListDialog = defineAsyncComponent(() => import('./dialogs/ExportAvatarsListDialog.vue'));
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const { showGalleryPage } = useGalleryStore();
|
const { showGalleryPage } = useGalleryStore();
|
||||||
const { friends } = storeToRefs(useFriendStore());
|
const { friends } = storeToRefs(useFriendStore());
|
||||||
@@ -236,7 +233,6 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isGroupCalendarDialogVisible = ref(false);
|
const isGroupCalendarDialogVisible = ref(false);
|
||||||
const isScreenshotMetadataDialogVisible = ref(false);
|
|
||||||
const isNoteExportDialogVisible = ref(false);
|
const isNoteExportDialogVisible = ref(false);
|
||||||
const isExportDiscordNamesDialogVisible = ref(false);
|
const isExportDiscordNamesDialogVisible = ref(false);
|
||||||
const isExportFriendsListDialogVisible = ref(false);
|
const isExportFriendsListDialogVisible = ref(false);
|
||||||
@@ -252,8 +248,8 @@
|
|||||||
isGroupCalendarDialogVisible.value = true;
|
isGroupCalendarDialogVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const showScreenshotMetadataDialog = () => {
|
const showScreenshotMetadataPage = () => {
|
||||||
isScreenshotMetadataDialogVisible.value = true;
|
router.push({ name: 'screenshot-metadata' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const showNoteExportDialog = () => {
|
const showNoteExportDialog = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user