mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-23 16:53:50 +02:00
rename
This commit is contained in:
18
src/plugins/components.js
Normal file
18
src/plugins/components.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { TooltipWrapper } from '../components/ui/tooltip';
|
||||
|
||||
import AvatarInfo from '../components/AvatarInfo.vue';
|
||||
import CountdownTimer from '../components/CountdownTimer.vue';
|
||||
import DisplayName from '../components/DisplayName.vue';
|
||||
import Location from '../components/Location.vue';
|
||||
import LocationWorld from '../components/LocationWorld.vue';
|
||||
import Timer from '../components/Timer.vue';
|
||||
|
||||
export function initComponents(app) {
|
||||
app.component('Location', Location);
|
||||
app.component('Timer', Timer);
|
||||
app.component('CountdownTimer', CountdownTimer);
|
||||
app.component('AvatarInfo', AvatarInfo);
|
||||
app.component('DisplayName', DisplayName);
|
||||
app.component('LocationWorld', LocationWorld);
|
||||
app.component('TooltipWrapper', TooltipWrapper);
|
||||
}
|
||||
18
src/plugins/dayjs.js
Normal file
18
src/plugins/dayjs.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
import dayjs from 'dayjs';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
|
||||
export function initDayjs() {
|
||||
dayjs.extend(duration);
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(isSameOrAfter);
|
||||
dayjs.extend(localizedFormat);
|
||||
dayjs.extend(customParseFormat);
|
||||
dayjs.extend(relativeTime);
|
||||
}
|
||||
31
src/plugins/i18n.js
Normal file
31
src/plugins/i18n.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { createI18n } from 'vue-i18n';
|
||||
|
||||
import { getLocalizedStrings } from '../localization';
|
||||
|
||||
const FALLBACK_LOCALE = 'en';
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: FALLBACK_LOCALE,
|
||||
fallbackLocale: FALLBACK_LOCALE,
|
||||
legacy: false,
|
||||
globalInjection: false,
|
||||
missingWarn: false,
|
||||
warnHtmlMessage: false,
|
||||
fallbackWarn: false
|
||||
});
|
||||
|
||||
async function loadLocalizedStrings(code) {
|
||||
const localesToLoad =
|
||||
code === FALLBACK_LOCALE ? [FALLBACK_LOCALE] : [FALLBACK_LOCALE, code];
|
||||
|
||||
for (const locale of localesToLoad) {
|
||||
const messages = await getLocalizedStrings(locale);
|
||||
i18n.global.setLocaleMessage(locale, messages);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateLocalizedStrings() {
|
||||
await loadLocalizedStrings(i18n.global.locale.value);
|
||||
}
|
||||
|
||||
export { i18n, loadLocalizedStrings, updateLocalizedStrings };
|
||||
24
src/plugins/index.js
Normal file
24
src/plugins/index.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { initDayjs } from './dayjs';
|
||||
import { initInteropApi } from './interopApi';
|
||||
import { initNoty } from './noty';
|
||||
import { initUi } from './ui';
|
||||
|
||||
/**
|
||||
* @param {boolean} isVrOverlay
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function initPlugins(isVrOverlay = false) {
|
||||
await initInteropApi(isVrOverlay);
|
||||
if (!isVrOverlay) {
|
||||
await initUi();
|
||||
}
|
||||
initDayjs();
|
||||
if (isVrOverlay) {
|
||||
initNoty(true);
|
||||
}
|
||||
}
|
||||
|
||||
export * from './i18n';
|
||||
export * from './components';
|
||||
export * from './sentry';
|
||||
export * from './router';
|
||||
42
src/plugins/interopApi.js
Normal file
42
src/plugins/interopApi.js
Normal file
@@ -0,0 +1,42 @@
|
||||
// @ts-nocheck
|
||||
import InteropApi from '../ipc-electron/interopApi.js';
|
||||
import configRepository from '../services/config.js';
|
||||
import vrcxJsonStorage from '../services/jsonStorage.js';
|
||||
|
||||
export async function initInteropApi(isVrOverlay = false) {
|
||||
if (isVrOverlay) {
|
||||
if (WINDOWS) {
|
||||
await CefSharp.BindObjectAsync('AppApiVr');
|
||||
} else {
|
||||
// @ts-ignore
|
||||
window.AppApiVr = InteropApi.AppApiVrElectron;
|
||||
}
|
||||
} else {
|
||||
// #region | Init Cef C# bindings
|
||||
if (WINDOWS) {
|
||||
await CefSharp.BindObjectAsync(
|
||||
'AppApi',
|
||||
'WebApi',
|
||||
'VRCXStorage',
|
||||
'SQLite',
|
||||
'LogWatcher',
|
||||
'Discord',
|
||||
'AssetBundleManager'
|
||||
);
|
||||
} else {
|
||||
window.AppApi = InteropApi.AppApiElectron;
|
||||
window.WebApi = InteropApi.WebApi;
|
||||
window.VRCXStorage = InteropApi.VRCXStorage;
|
||||
window.SQLite = InteropApi.SQLite;
|
||||
window.LogWatcher = InteropApi.LogWatcher;
|
||||
window.Discord = InteropApi.Discord;
|
||||
window.AssetBundleManager = InteropApi.AssetBundleManager;
|
||||
window.AppApiVrElectron = InteropApi.AppApiVrElectron;
|
||||
}
|
||||
|
||||
await configRepository.init();
|
||||
new vrcxJsonStorage(VRCXStorage);
|
||||
|
||||
AppApi.SetUserAgent();
|
||||
}
|
||||
}
|
||||
25
src/plugins/noty.js
Normal file
25
src/plugins/noty.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import Noty from 'noty';
|
||||
|
||||
export function initNoty(isVrOverlay = false) {
|
||||
if (isVrOverlay) {
|
||||
Noty.overrideDefaults({
|
||||
animation: {
|
||||
open: 'animate__animated animate__fadeIn',
|
||||
close: 'animate__animated animate__zoomOut'
|
||||
},
|
||||
layout: 'topCenter',
|
||||
theme: 'relax',
|
||||
timeout: 3000
|
||||
});
|
||||
} else {
|
||||
Noty.overrideDefaults({
|
||||
animation: {
|
||||
open: 'animate__animated animate__bounceInLeft',
|
||||
close: 'animate__animated animate__bounceOutLeft'
|
||||
},
|
||||
layout: 'bottomLeft',
|
||||
theme: 'mint',
|
||||
timeout: 2000
|
||||
});
|
||||
}
|
||||
}
|
||||
135
src/plugins/piniaActionTrail.js
Normal file
135
src/plugins/piniaActionTrail.js
Normal file
@@ -0,0 +1,135 @@
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const STORAGE_KEY = 'vrcx:sentry:piniaActions';
|
||||
const DEFAULT_MAX_ENTRIES = 200;
|
||||
|
||||
function getStorage() {
|
||||
try {
|
||||
return localStorage;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function safeJsonParse(value) {
|
||||
if (!value) return null;
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function safeJsonStringify(value, fallback = '[]') {
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
export function getPiniaActionTrail() {
|
||||
const storage = getStorage();
|
||||
if (!storage) return [];
|
||||
|
||||
const data = safeJsonParse(storage.getItem(STORAGE_KEY));
|
||||
return Array.isArray(data) ? data : [];
|
||||
}
|
||||
|
||||
export function clearPiniaActionTrail() {
|
||||
const storage = getStorage();
|
||||
if (!storage) return;
|
||||
storage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
|
||||
export function appendPiniaActionTrail(entry, options) {
|
||||
const storage = getStorage();
|
||||
if (!storage) return;
|
||||
|
||||
const maxEntries = options?.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
||||
|
||||
const existing = getPiniaActionTrail();
|
||||
existing.push(entry);
|
||||
|
||||
if (existing.length > maxEntries) {
|
||||
existing.splice(0, existing.length - maxEntries);
|
||||
}
|
||||
|
||||
try {
|
||||
storage.setItem(STORAGE_KEY, safeJsonStringify(existing));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export function createPiniaActionTrailPlugin(options) {
|
||||
const maxEntries = options?.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
||||
return ({ store }) => {
|
||||
store.$onAction(({ name }) => {
|
||||
appendPiniaActionTrail(
|
||||
{
|
||||
t: dayjs().format('HH:mm:ss'),
|
||||
a: name
|
||||
},
|
||||
{ maxEntries }
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function readPerformanceMemory() {
|
||||
// @ts-ignore
|
||||
const memory = window?.performance?.memory;
|
||||
if (!memory) return null;
|
||||
const { usedJSHeapSize, jsHeapSizeLimit } = memory;
|
||||
if (
|
||||
typeof usedJSHeapSize !== 'number' ||
|
||||
typeof jsHeapSizeLimit !== 'number'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return { usedJSHeapSize, jsHeapSizeLimit };
|
||||
}
|
||||
|
||||
export function startRendererMemoryThresholdReport(
|
||||
Sentry,
|
||||
{ intervalMs = 10_000, thresholdRatio = 0.8, cooldownMs = 5 * 60_000 } = {}
|
||||
) {
|
||||
const initial = readPerformanceMemory();
|
||||
if (!initial) return null;
|
||||
|
||||
if (!Sentry?.withScope) return null;
|
||||
|
||||
let lastSent = 0;
|
||||
|
||||
return setInterval(() => {
|
||||
const m = readPerformanceMemory();
|
||||
if (!m) return;
|
||||
|
||||
const ratio = m.usedJSHeapSize / m.jsHeapSizeLimit;
|
||||
if (!Number.isFinite(ratio) || ratio < thresholdRatio) return;
|
||||
|
||||
const now = Date.now();
|
||||
if (now - lastSent < cooldownMs) return;
|
||||
lastSent = now;
|
||||
|
||||
const trail = getPiniaActionTrail();
|
||||
const trailText = JSON.stringify(trail);
|
||||
Sentry.withScope((scope) => {
|
||||
scope.setLevel('warning');
|
||||
scope.setTag('reason', 'high-js-heap');
|
||||
scope.setContext('memory', {
|
||||
usedMB: m.usedJSHeapSize / 1024 / 1024,
|
||||
limitMB: m.jsHeapSizeLimit / 1024 / 1024,
|
||||
ratio
|
||||
});
|
||||
scope.setContext('pinia_actions', {
|
||||
trailText,
|
||||
count: trail.length
|
||||
});
|
||||
Sentry.captureMessage(
|
||||
'Memory usage critical: nearing JS heap limit'
|
||||
);
|
||||
});
|
||||
}, intervalMs);
|
||||
}
|
||||
151
src/plugins/router.js
Normal file
151
src/plugins/router.js
Normal file
@@ -0,0 +1,151 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||
|
||||
import { watchState } from '../services/watchState';
|
||||
|
||||
import FavoritesAvatar from './../views/Favorites/FavoritesAvatar.vue';
|
||||
import FavoritesFriend from './../views/Favorites/FavoritesFriend.vue';
|
||||
import FavoritesWorld from './../views/Favorites/FavoritesWorld.vue';
|
||||
import Feed from './../views/Feed/Feed.vue';
|
||||
import FriendList from './../views/FriendList/FriendList.vue';
|
||||
import FriendLog from './../views/FriendLog/FriendLog.vue';
|
||||
import FriendsLocations from './../views/FriendsLocations/FriendsLocations.vue';
|
||||
import Gallery from './../views/Tools/Gallery.vue';
|
||||
import GameLog from './../views/GameLog/GameLog.vue';
|
||||
import Login from './../views/Login/Login.vue';
|
||||
import MainLayout from '../views/Layout/MainLayout.vue';
|
||||
import Moderation from './../views/Moderation/Moderation.vue';
|
||||
import MyAvatars from './../views/MyAvatars/MyAvatars.vue';
|
||||
import Notification from './../views/Notifications/Notification.vue';
|
||||
import PlayerList from './../views/PlayerList/PlayerList.vue';
|
||||
import ScreenshotMetadata from './../views/Tools/ScreenshotMetadata.vue';
|
||||
import Search from './../views/Search/Search.vue';
|
||||
import Settings from './../views/Settings/Settings.vue';
|
||||
import Tools from './../views/Tools/Tools.vue';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: Login,
|
||||
meta: { public: true }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: MainLayout,
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{ path: '', redirect: { name: 'feed' } },
|
||||
{ path: 'feed', name: 'feed', component: Feed },
|
||||
{
|
||||
path: 'friends-locations',
|
||||
name: 'friends-locations',
|
||||
component: FriendsLocations
|
||||
},
|
||||
{ path: 'game-log', name: 'game-log', component: GameLog },
|
||||
{ path: 'player-list', name: 'player-list', component: PlayerList },
|
||||
{ path: 'search', name: 'search', component: Search },
|
||||
{
|
||||
path: 'favorites/friends',
|
||||
name: 'favorite-friends',
|
||||
component: FavoritesFriend
|
||||
},
|
||||
{
|
||||
path: 'favorites/worlds',
|
||||
name: 'favorite-worlds',
|
||||
component: FavoritesWorld
|
||||
},
|
||||
{
|
||||
path: 'favorites/avatars',
|
||||
name: 'favorite-avatars',
|
||||
component: FavoritesAvatar
|
||||
},
|
||||
{
|
||||
path: 'social/friend-log',
|
||||
name: 'friend-log',
|
||||
component: FriendLog
|
||||
},
|
||||
{
|
||||
path: 'social/moderation',
|
||||
name: 'moderation',
|
||||
component: Moderation
|
||||
},
|
||||
{
|
||||
path: 'my-avatars',
|
||||
name: 'my-avatars',
|
||||
component: MyAvatars
|
||||
},
|
||||
{
|
||||
path: 'notification',
|
||||
name: 'notification',
|
||||
component: Notification
|
||||
},
|
||||
{
|
||||
path: 'social/friend-list',
|
||||
name: 'friend-list',
|
||||
component: FriendList
|
||||
},
|
||||
{
|
||||
path: 'charts',
|
||||
name: 'charts',
|
||||
redirect: { name: 'charts-instance' }
|
||||
},
|
||||
{
|
||||
path: 'charts/instance',
|
||||
name: 'charts-instance',
|
||||
component: () =>
|
||||
import('./../views/Charts/components/InstanceActivity.vue')
|
||||
},
|
||||
{
|
||||
path: 'charts/mutual',
|
||||
name: 'charts-mutual',
|
||||
component: () =>
|
||||
import('./../views/Charts/components/MutualFriends.vue')
|
||||
},
|
||||
{ path: 'tools', name: 'tools', component: Tools },
|
||||
{
|
||||
path: 'tools/gallery',
|
||||
name: 'gallery',
|
||||
component: Gallery,
|
||||
meta: { navKey: 'tools' }
|
||||
},
|
||||
{
|
||||
path: 'tools/screenshot-metadata',
|
||||
name: 'screenshot-metadata',
|
||||
component: ScreenshotMetadata,
|
||||
meta: { navKey: 'tools' }
|
||||
},
|
||||
{ path: 'settings', name: 'settings', component: Settings }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
// @ts-ignore
|
||||
routes
|
||||
});
|
||||
|
||||
export function initRouter(app) {
|
||||
app.use(router);
|
||||
}
|
||||
|
||||
router.beforeEach((to) => {
|
||||
if (to.path === '/social') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (to.name === 'login' && watchState.isLoggedIn) {
|
||||
return { name: 'feed' };
|
||||
}
|
||||
|
||||
const requiresAuth = to.matched.some((record) => record.meta?.requiresAuth);
|
||||
if (requiresAuth && !watchState.isLoggedIn) {
|
||||
const redirect = to.fullPath;
|
||||
if (redirect && redirect !== '/feed') {
|
||||
return { name: 'login', query: { redirect } };
|
||||
}
|
||||
return { name: 'login' };
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
110
src/plugins/sentry.js
Normal file
110
src/plugins/sentry.js
Normal file
@@ -0,0 +1,110 @@
|
||||
import { router } from './router';
|
||||
import { startRendererMemoryThresholdReport } from './piniaActionTrail';
|
||||
|
||||
import configRepository from '../services/config';
|
||||
|
||||
export async function isSentryOptedIn() {
|
||||
return NIGHTLY && configRepository.getBool('VRCX_SentryEnabled', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Guarded import, prevents leaking Sentry into non-nightly bundles.
|
||||
*/
|
||||
export function getSentry() {
|
||||
return NIGHTLY ? import('@sentry/vue') : null;
|
||||
}
|
||||
|
||||
export async function initSentry(app) {
|
||||
if (!NIGHTLY) return;
|
||||
|
||||
try {
|
||||
if (!(await isSentryOptedIn())) return;
|
||||
|
||||
const vrcxId = await configRepository.getString('VRCX_id', '');
|
||||
const response = await webApiService.execute({
|
||||
url: 'https://api0.vrcx.app/errorreporting/getdsn',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Referer: 'https://vrcx.app',
|
||||
'VRCX-ID': vrcxId
|
||||
}
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
console.error(
|
||||
'Failed to get Sentry DSN:',
|
||||
response.status,
|
||||
response.data
|
||||
);
|
||||
return;
|
||||
}
|
||||
const dsn = atob(response.data);
|
||||
const Sentry = await getSentry();
|
||||
Sentry.init({
|
||||
app,
|
||||
dsn,
|
||||
environment: 'nightly',
|
||||
release: VERSION,
|
||||
replaysSessionSampleRate: 0,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
tracesSampleRate: 0.0001,
|
||||
beforeSend(event, hint) {
|
||||
const error = hint.originalException;
|
||||
if (error && typeof error.message === 'string') {
|
||||
if (
|
||||
error.message.includes('401') ||
|
||||
error.message.includes('403') ||
|
||||
error.message.includes('404') ||
|
||||
error.message.includes('500') ||
|
||||
error.message.includes('503') ||
|
||||
error.message.includes('No such host is known') ||
|
||||
error.message.includes(
|
||||
'The SSL connection could not be established'
|
||||
) ||
|
||||
error.message.includes('A connection attempt failed') ||
|
||||
error.message.includes(
|
||||
'no data of the requested type was found'
|
||||
) ||
|
||||
error.message.includes(
|
||||
'An error occurred while sending the request'
|
||||
) ||
|
||||
error.message.includes('database or disk is full') ||
|
||||
error.message.includes('disk I/O error') ||
|
||||
error.message.includes(
|
||||
'There is not enough space on the disk.'
|
||||
) ||
|
||||
error.message.includes(
|
||||
'The requested address is not valid in its context.'
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return event;
|
||||
}
|
||||
return event;
|
||||
},
|
||||
|
||||
integrations: [
|
||||
Sentry.replayIntegration({
|
||||
maskAllText: true,
|
||||
blockAllMedia: true
|
||||
}),
|
||||
Sentry.browserTracingIntegration({ router }),
|
||||
Sentry.vueIntegration({
|
||||
tracingOptions: {
|
||||
trackComponents: true
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
console.log('Sentry initialized');
|
||||
|
||||
startRendererMemoryThresholdReport(Sentry, {
|
||||
thresholdRatio: 0.8,
|
||||
intervalMs: 10_000,
|
||||
cooldownMs: 5 * 60_000
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to initialize Sentry:', e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
35
src/plugins/ui.js
Normal file
35
src/plugins/ui.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
// changeAppDarkStyle,
|
||||
changeAppThemeStyle,
|
||||
changeHtmlLangAttribute,
|
||||
getThemeMode,
|
||||
initThemeColor,
|
||||
refreshCustomCss
|
||||
// setLoginContainerStyle
|
||||
} from '../shared/utils/base/ui';
|
||||
import { i18n, loadLocalizedStrings } from './i18n';
|
||||
|
||||
import configRepository from '../services/config';
|
||||
|
||||
export async function initUi() {
|
||||
try {
|
||||
const language = await configRepository.getString(
|
||||
'VRCX_appLanguage',
|
||||
'en'
|
||||
);
|
||||
// @ts-ignore
|
||||
i18n.locale = language;
|
||||
await loadLocalizedStrings(language);
|
||||
changeHtmlLangAttribute(language);
|
||||
|
||||
const { initThemeMode, isDarkMode } =
|
||||
await getThemeMode(configRepository);
|
||||
// setLoginContainerStyle(isDarkMode);
|
||||
changeAppThemeStyle(initThemeMode);
|
||||
await initThemeColor();
|
||||
} catch (error) {
|
||||
console.error('Error initializing locale and theme:', error);
|
||||
}
|
||||
|
||||
refreshCustomCss();
|
||||
}
|
||||
Reference in New Issue
Block a user