mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-19 14:53:50 +02:00
Calendar local datetime format and download buttons
This commit is contained in:
@@ -221,5 +221,28 @@ namespace VRCX
|
|||||||
{
|
{
|
||||||
MainForm.Instance.BeginInvoke(new MethodInvoker(() => { MainForm.Instance.SetTrayIconNotification(notify); }));
|
MainForm.Instance.BeginInvoke(new MethodInvoker(() => { MainForm.Instance.SetTrayIconNotification(notify); }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void OpenCalendarFile(string icsContent)
|
||||||
|
{
|
||||||
|
// validate content
|
||||||
|
if (!icsContent.StartsWith("BEGIN:VCALENDAR") ||
|
||||||
|
!icsContent.EndsWith("END:VCALENDAR"))
|
||||||
|
throw new Exception("Invalid calendar file");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tempPath = Path.Combine(Program.AppDataDirectory, "event.ics");
|
||||||
|
File.WriteAllText(tempPath, icsContent);
|
||||||
|
Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = tempPath,
|
||||||
|
UseShellExecute = true
|
||||||
|
})?.Dispose();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error(ex, "Failed to open calendar file");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,7 @@ namespace VRCX
|
|||||||
public abstract void CopyImageToClipboard(string path);
|
public abstract void CopyImageToClipboard(string path);
|
||||||
public abstract void FlashWindow();
|
public abstract void FlashWindow();
|
||||||
public abstract void SetUserAgent();
|
public abstract void SetUserAgent();
|
||||||
|
public abstract void OpenCalendarFile(string icsContent);
|
||||||
|
|
||||||
// Folders
|
// Folders
|
||||||
public abstract string GetVRChatAppDataLocation();
|
public abstract string GetVRChatAppDataLocation();
|
||||||
|
|||||||
@@ -147,5 +147,9 @@ namespace VRCX
|
|||||||
public override void SetTrayIconNotification(bool notify)
|
public override void SetTrayIconNotification(bool notify)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void OpenCalendarFile(string icsContent)
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,8 @@
|
|||||||
:width="50"
|
:width="50"
|
||||||
:stroke-width="3"
|
:stroke-width="3"
|
||||||
:percentage="updateProgress"
|
:percentage="updateProgress"
|
||||||
:format="updateProgressText"></el-progress>
|
:format="updateProgressText"
|
||||||
|
style="padding: 7px"></el-progress>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="pendingVRCXUpdate || pendingVRCXInstall" class="pending-update">
|
<div v-else-if="pendingVRCXUpdate || pendingVRCXInstall" class="pending-update">
|
||||||
<el-button
|
<el-button
|
||||||
|
|||||||
@@ -1721,10 +1721,12 @@
|
|||||||
"search_no_this_month": "No events this month",
|
"search_no_this_month": "No events this month",
|
||||||
"event_card": {
|
"event_card": {
|
||||||
"category": "Category",
|
"category": "Category",
|
||||||
"interested_user": "Interested User",
|
"interested_user": "Users Interested",
|
||||||
"close_time": "Close Instance After End",
|
"close_time": "Close Instance After End",
|
||||||
"created": "Created Date",
|
"created": "Created Date",
|
||||||
"description": "Description"
|
"description": "Description",
|
||||||
|
"export_to_calendar": "Export to Calendar",
|
||||||
|
"download_ics": "Download .ics"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"moderate_group": {
|
"moderate_group": {
|
||||||
|
|||||||
@@ -1,8 +1,64 @@
|
|||||||
import { useAppearanceSettingsStore } from '../../../stores';
|
import { useAppearanceSettingsStore } from '../../../stores';
|
||||||
|
|
||||||
|
function padZero(num) {
|
||||||
|
return String(num).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toIsoLong(date) {
|
||||||
|
const y = date.getFullYear();
|
||||||
|
const m = padZero(date.getMonth() + 1);
|
||||||
|
const d = padZero(date.getDate());
|
||||||
|
const hh = padZero(date.getHours());
|
||||||
|
const mm = padZero(date.getMinutes());
|
||||||
|
const ss = padZero(date.getSeconds());
|
||||||
|
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toLocalShort(date, dateFormat, hour12) {
|
||||||
|
return date
|
||||||
|
.toLocaleDateString(dateFormat, {
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
hourCycle: hour12 ? 'h12' : 'h23'
|
||||||
|
})
|
||||||
|
.replace(' AM', 'am')
|
||||||
|
.replace(' PM', 'pm')
|
||||||
|
.replace(',', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toLocalLong(date, dateFormat, hour12) {
|
||||||
|
return date.toLocaleDateString(dateFormat, {
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
second: 'numeric',
|
||||||
|
hourCycle: hour12 ? 'h12' : 'h23'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toLocalTime(date, dateFormat, hour12) {
|
||||||
|
return date.toLocaleTimeString(dateFormat, {
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
hourCycle: hour12 ? 'h12' : 'h23'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toLocalDate(date, dateFormat) {
|
||||||
|
return date.toLocaleDateString(dateFormat, {
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
year: 'numeric'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} dateStr
|
* @param {string} dateStr
|
||||||
* @param {'long'|'short'} format
|
* @param {'long'|'short'|'time'|'date'} format
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function formatDateFilter(dateStr, format) {
|
function formatDateFilter(dateStr, format) {
|
||||||
@@ -22,60 +78,23 @@ function formatDateFilter(dateStr, format) {
|
|||||||
return '-';
|
return '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
function padZero(num) {
|
|
||||||
return String(num).padStart(2, '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
function toIsoLong(date) {
|
|
||||||
const y = date.getFullYear();
|
|
||||||
const m = padZero(date.getMonth() + 1);
|
|
||||||
const d = padZero(date.getDate());
|
|
||||||
const hh = padZero(date.getHours());
|
|
||||||
const mm = padZero(date.getMinutes());
|
|
||||||
const ss = padZero(date.getSeconds());
|
|
||||||
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let dateFormat = 'en-gb';
|
let dateFormat = 'en-gb';
|
||||||
if (!isoFormat && currentCulture) {
|
if (!isoFormat && currentCulture) {
|
||||||
dateFormat = currentCulture;
|
dateFormat = currentCulture;
|
||||||
}
|
}
|
||||||
function toLocalShort(date) {
|
|
||||||
return date
|
|
||||||
.toLocaleDateString(dateFormat, {
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: 'numeric',
|
|
||||||
hourCycle: hour12 ? 'h12' : 'h23'
|
|
||||||
})
|
|
||||||
.replace(' AM', 'am')
|
|
||||||
.replace(' PM', 'pm')
|
|
||||||
.replace(',', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isoFormat) {
|
if (isoFormat && format === 'long') {
|
||||||
if (format === 'long') {
|
return toIsoLong(dt);
|
||||||
return toIsoLong(dt);
|
} else if (format === 'long') {
|
||||||
}
|
return toLocalLong(dt, dateFormat, hour12);
|
||||||
if (format === 'short') {
|
} else if (format === 'short') {
|
||||||
return toLocalShort(dt);
|
return toLocalShort(dt, dateFormat, hour12);
|
||||||
}
|
} else if (format === 'time') {
|
||||||
|
return toLocalTime(dt, dateFormat, hour12);
|
||||||
|
} else if (format === 'date') {
|
||||||
|
return toLocalDate(dt, dateFormat);
|
||||||
} else {
|
} else {
|
||||||
if (format === 'long') {
|
console.warn(`Unknown date format: ${format}`);
|
||||||
return dt.toLocaleDateString(dateFormat, {
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
year: 'numeric',
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: 'numeric',
|
|
||||||
second: 'numeric',
|
|
||||||
hourCycle: hour12 ? 'h12' : 'h23'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (format === 'short') {
|
|
||||||
return toLocalShort(dt);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return '-';
|
return '-';
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ function timeToText(sec, isNeedSeconds = false) {
|
|||||||
n %= 60;
|
n %= 60;
|
||||||
}
|
}
|
||||||
if (isNeedSeconds || (arr.length === 0 && n < 60)) {
|
if (isNeedSeconds || (arr.length === 0 && n < 60)) {
|
||||||
|
// round to 5 seconds
|
||||||
|
n = Math.floor((n + 2.5) / 5) * 5;
|
||||||
arr.push(`${n}s`);
|
arr.push(`${n}s`);
|
||||||
}
|
}
|
||||||
return arr.join(' ');
|
return arr.join(' ');
|
||||||
|
|||||||
1
src/types/globals.d.ts
vendored
1
src/types/globals.d.ts
vendored
@@ -190,6 +190,7 @@ declare global {
|
|||||||
FlashWindow(): Promise<void>;
|
FlashWindow(): Promise<void>;
|
||||||
SetUserAgent(): Promise<void>;
|
SetUserAgent(): Promise<void>;
|
||||||
SetTrayIconNotification(notify: boolean): Promise<void>;
|
SetTrayIconNotification(notify: boolean): Promise<void>;
|
||||||
|
OpenCalendarFile(icsContent: string): Promise<void>;
|
||||||
|
|
||||||
// Common Functions
|
// Common Functions
|
||||||
GetColourFromUserID(userId: string): Promise<number>;
|
GetColourFromUserID(userId: string): Promise<number>;
|
||||||
|
|||||||
@@ -14,6 +14,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<el-descriptions-item>
|
||||||
|
<el-button type="default" :icon="Calendar" size="small" @click="openCalendarEvent(event)">{{
|
||||||
|
t('dialog.group_calendar.event_card.export_to_calendar')
|
||||||
|
}}</el-button>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item>
|
||||||
|
<el-button type="default" :icon="Download" size="small" @click="downloadEventIcs(event)">{{
|
||||||
|
t('dialog.group_calendar.event_card.download_ics')
|
||||||
|
}}</el-button>
|
||||||
|
</el-descriptions-item>
|
||||||
<el-descriptions-item :label="t('dialog.group_calendar.event_card.category')">
|
<el-descriptions-item :label="t('dialog.group_calendar.event_card.category')">
|
||||||
{{ capitalizeFirst(event.category) }}
|
{{ capitalizeFirst(event.category) }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -24,7 +34,7 @@
|
|||||||
{{ event.closeInstanceAfterEndMinutes + ' min' }}
|
{{ event.closeInstanceAfterEndMinutes + ' min' }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item :label="t('dialog.group_calendar.event_card.created')">
|
<el-descriptions-item :label="t('dialog.group_calendar.event_card.created')">
|
||||||
{{ dayjs(event.createdAt).format('YYYY-MM-DD HH:mm') }}
|
{{ formatDateFilter(event.createdAt, 'long') }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item :label="t('dialog.group_calendar.event_card.description')">
|
<el-descriptions-item :label="t('dialog.group_calendar.event_card.description')">
|
||||||
{{ event.description }}
|
{{ event.description }}
|
||||||
@@ -53,12 +63,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Check } from '@element-plus/icons-vue';
|
import { Calendar, Check, Download } from '@element-plus/icons-vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
import dayjs from 'dayjs';
|
import { AppDebug } from '../../../service/appConfig';
|
||||||
|
import { formatDateFilter } from '../../../shared/utils';
|
||||||
import { useGroupStore } from '../../../stores';
|
import { useGroupStore } from '../../../stores';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -107,12 +118,51 @@
|
|||||||
if (props.mode === 'timeline') {
|
if (props.mode === 'timeline') {
|
||||||
return formatTimeRange(props.event.startsAt, props.event.endsAt);
|
return formatTimeRange(props.event.startsAt, props.event.endsAt);
|
||||||
} else {
|
} else {
|
||||||
return `${dayjs(props.event.startsAt).format('MM-DD ddd HH:mm')} - ${dayjs(props.event.endsAt).format('HH:mm')}`;
|
return `${formatDateFilter(props.event.startsAt, 'short')} - ${formatDateFilter(props.event.endsAt, 'short')}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function openCalendarEvent(event) {
|
||||||
|
const content = await getCalendarIcs(event);
|
||||||
|
if (!content) return;
|
||||||
|
await AppApi.OpenCalendarFile(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCalendarIcs(event) {
|
||||||
|
const url = `${AppDebug.endpointDomain}/calendar/${event.ownerId}/${event.id}.ics`;
|
||||||
|
try {
|
||||||
|
const response = await webApiService.execute({
|
||||||
|
url,
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(`Error: ${response.data}`);
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage({
|
||||||
|
message: `Failed to download .ics file, ${error.message}`,
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
console.error('Failed to download .ics file:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadEventIcs(event) {
|
||||||
|
const content = await getCalendarIcs(event);
|
||||||
|
if (!content) return;
|
||||||
|
const blob = new Blob([content], { type: 'text/calendar' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
link.download = `${event.id}.ics`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(link.href);
|
||||||
|
}
|
||||||
|
|
||||||
const formatTimeRange = (startsAt, endsAt) =>
|
const formatTimeRange = (startsAt, endsAt) =>
|
||||||
`${dayjs(startsAt).format('HH:mm')} - ${dayjs(endsAt).format('HH:mm')}`;
|
`${formatDateFilter(startsAt, 'time')} - ${formatDateFilter(endsAt, 'time')}`;
|
||||||
|
|
||||||
const capitalizeFirst = (str) => str?.charAt(0).toUpperCase() + str?.slice(1);
|
const capitalizeFirst = (str) => str?.charAt(0).toUpperCase() + str?.slice(1);
|
||||||
|
|
||||||
|
|||||||
@@ -122,6 +122,7 @@
|
|||||||
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import { formatDateFilter } from '../../../shared/utils';
|
||||||
import { groupRequest } from '../../../api';
|
import { groupRequest } from '../../../api';
|
||||||
import { replaceBioSymbols } from '../../../shared/utils';
|
import { replaceBioSymbols } from '../../../shared/utils';
|
||||||
import { useGroupStore } from '../../../stores';
|
import { useGroupStore } from '../../../stores';
|
||||||
@@ -297,7 +298,7 @@
|
|||||||
const timeGroups = {};
|
const timeGroups = {};
|
||||||
|
|
||||||
eventsForDay.forEach((event) => {
|
eventsForDay.forEach((event) => {
|
||||||
const startTimeKey = dayjs(event.startsAt).format('HH:mm');
|
const startTimeKey = formatDateFilter(event.startsAt, 'time');
|
||||||
if (!timeGroups[startTimeKey]) {
|
if (!timeGroups[startTimeKey]) {
|
||||||
timeGroups[startTimeKey] = [];
|
timeGroups[startTimeKey] = [];
|
||||||
}
|
}
|
||||||
@@ -314,7 +315,7 @@
|
|||||||
.sort((a, b) => dayjs(a.startsAt).diff(dayjs(b.startsAt)));
|
.sort((a, b) => dayjs(a.startsAt).diff(dayjs(b.startsAt)));
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatDateKey = (date) => dayjs(date).format('YYYY-MM-DD');
|
const formatDateKey = (date) => formatDateFilter(date, 'date');
|
||||||
|
|
||||||
function getGroupName(event) {
|
function getGroupName(event) {
|
||||||
if (!event) return '';
|
if (!event) return '';
|
||||||
|
|||||||
Reference in New Issue
Block a user