refactor: app.js (#1291)

* refactor: frontend

* Fix avatar gallery sort

* Update .NET dependencies

* Update npm dependencies

electron v37.1.0

* bulkRefreshFriends

* fix dark theme

* Remove crowdin

* Fix config.json dialog not updating

* VRCX log file fixes & add Cef log

* Remove SharedVariable, fix startup

* Revert init theme change

* Logging date not working? Fix WinformThemer designer error

* Add Cef request hander, no more escaping main page

* clean

* fix

* fix

* clean

* uh

* Apply thememode at startup, fixes random user colours

* Split database into files

* Instance info remove empty lines

* Open external VRC links with VRCX

* Electron fixes

* fix userdialog style

* ohhhh

* fix store

* fix store

* fix: load all group members after kicking a user

* fix: world dialog favorite button style

* fix: Clear VRCX Cache Timer input value

* clean

* Fix VR overlay

* Fix VR overlay 2

* Fix Discord discord rich presence for RPC worlds

* Clean up age verified user tags

* Fix playerList being occupied after program reload

* no `this`

* Fix login stuck loading

* writable: false

* Hide dialogs on logout

* add flush sync option

* rm LOGIN event

* rm LOGOUT event

* remove duplicate event listeners

* remove duplicate event listeners

* clean

* remove duplicate event listeners

* clean

* fix theme style

* fix t

* clearable

* clean

* fix ipcEvent

* Small changes

* Popcorn Palace support

* Remove checkActiveFriends

* Clean up

* Fix dragEnterCef

* Block API requests when not logged in

* Clear state on login & logout

* Fix worldDialog instances not updating

* use <script setup>

* Fix avatar change event, CheckGameRunning at startup

* Fix image dragging

* fix

* Remove PWI

* fix updateLoop

* add webpack-dev-server to dev environment

* rm unnecessary chunks

* use <script setup>

* webpack-dev-server changes

* use <script setup>

* use <script setup>

* Fix UGC text size

* Split login event

* t

* use <script setup>

* fix

* Update .gitignore and enable checkJs in jsconfig

* fix i18n t

* use <script setup>

* use <script setup>

* clean

* global types

* fix

* use checkJs for debugging

* Add watchState for login watchers

* fix .vue template

* type fixes

* rm Vue.filter

* Cef v138.0.170, VC++ 2022

* Settings fixes

* Remove 'USER:CURRENT'

* clean up 2FA callbacks

* remove userApply

* rm i18n import

* notification handling to use notification store methods

* refactor favorite handling to use favorite store methods and clean up event emissions

* refactor moderation handling to use dedicated functions for player moderation events

* refactor friend handling to use dedicated functions for friend events

* Fix program startup, move lang init

* Fix friend state

* Fix status change error

* Fix user notes diff

* fix

* rm group event

* rm auth event

* rm avatar event

* clean

* clean

* getUser

* getFriends

* getFavoriteWorlds, getFavoriteAvatars

* AvatarGalleryUpload btn style & package.json update

* Fix friend requests

* Apply user

* Apply world

* Fix note diff

* Fix VR overlay

* Fixes

* Update build scripts

* Apply avatar

* Apply instance

* Apply group

* update hidden VRC+ badge

* Fix sameInstance "private"

* fix 502/504 API errors

* fix 502/504 API errors

* clean

* Fix friend in same instance on orange showing twice in friends list

* Add back in broken friend state repair methods

* add types

---------

Co-authored-by: Natsumi <cmcooper123@hotmail.com>
This commit is contained in:
pa
2025-07-14 12:00:08 +09:00
committed by GitHub
parent 952fd77ed5
commit f4f78bb5ec
323 changed files with 47745 additions and 43326 deletions
+882
View File
@@ -0,0 +1,882 @@
import Noty from 'noty';
import { defineStore } from 'pinia';
import { computed, reactive, watch } from 'vue';
import { authRequest } from '../api';
import { $app } from '../app';
import { useI18n } from 'vue-i18n-bridge';
import configRepository from '../service/config';
import { database } from '../service/database';
import { AppGlobal } from '../service/appConfig';
import { request } from '../service/request';
import security from '../service/security';
import webApiService from '../service/webapi';
import { closeWebSocket, initWebsocket } from '../service/websocket';
import { watchState } from '../service/watchState';
import { escapeTag } from '../shared/utils';
import { useNotificationStore } from './notification';
import { useAdvancedSettingsStore } from './settings/advanced';
import { useUpdateLoopStore } from './updateLoop';
import { useUserStore } from './user';
export const useAuthStore = defineStore('Auth', () => {
const advancedSettingsStore = useAdvancedSettingsStore();
const notificationStore = useNotificationStore();
const userStore = useUserStore();
const updateLoopStore = useUpdateLoopStore();
const { t } = useI18n();
const state = reactive({
attemptingAutoLogin: false,
autoLoginAttempts: new Set(),
loginForm: {
loading: false,
username: '',
password: '',
endpoint: '',
websocket: '',
saveCredentials: false,
savedCredentials: {},
lastUserLoggedIn: '',
rules: {
username: [
{
required: true,
trigger: 'blur'
}
],
password: [
{
required: true,
trigger: 'blur'
}
]
}
},
enablePrimaryPasswordDialog: {
visible: false,
password: '',
rePassword: '',
beforeClose(done) {
$app._data.enablePrimaryPassword = false;
done();
}
},
saveCredentials: null,
// it's a flag
twoFactorAuthDialogVisible: false,
enableCustomEndpoint: false,
cachedConfig: {}
});
async function init() {
const [savedCredentials, lastUserLoggedIn, enableCustomEndpoint] =
await Promise.all([
configRepository.getString('savedCredentials'),
configRepository.getString('lastUserLoggedIn'),
configRepository.getBool('VRCX_enableCustomEndpoint', false)
]);
state.loginForm = {
...state.loginForm,
savedCredentials: savedCredentials
? JSON.parse(savedCredentials)
: {},
lastUserLoggedIn
};
state.enableCustomEndpoint = enableCustomEndpoint;
}
init();
const loginForm = computed({
get: () => state.loginForm,
set: (value) => {
state.loginForm = value;
}
});
const enablePrimaryPasswordDialog = computed({
get: () => state.enablePrimaryPasswordDialog,
set: (value) => {
state.enablePrimaryPasswordDialog = value;
}
});
const saveCredentials = computed({
get: () => state.saveCredentials,
set: (value) => {
state.saveCredentials = value;
}
});
const twoFactorAuthDialogVisible = computed({
get: () => state.twoFactorAuthDialogVisible,
set: (value) => {
state.twoFactorAuthDialogVisible = value;
}
});
const cachedConfig = computed({
get: () => state.cachedConfig,
set: (value) => {
state.cachedConfig = value;
}
});
const enableCustomEndpoint = computed({
get: () => state.enableCustomEndpoint,
set: (value) => {
state.enableCustomEndpoint = value;
}
});
watch(
[() => watchState.isLoggedIn, () => userStore.currentUser],
([isLoggedIn, currentUser]) => {
state.twoFactorAuthDialogVisible = false;
if (isLoggedIn) {
updateStoredUser(currentUser);
new Noty({
type: 'success',
text: `Hello there, <strong>${escapeTag(
currentUser.displayName
)}</strong>!`
}).show();
}
},
{ flush: 'sync' }
);
watch(
() => watchState.isFriendsLoaded,
(isFriendsLoaded) => {
if (isFriendsLoaded) {
initWebsocket();
AppApi.IPCAnnounceStart();
}
},
{ flush: 'sync' }
);
async function handleLogoutEvent() {
if (watchState.isLoggedIn) {
new Noty({
type: 'success',
text: `See you again, <strong>${escapeTag(
userStore.currentUser.displayName
)}</strong>!`
}).show();
}
watchState.isLoggedIn = false;
watchState.isFriendsLoaded = false;
notificationStore.notificationInitStatus = false;
await updateStoredUser(userStore.currentUser);
webApiService.clearCookies();
state.loginForm.lastUserLoggedIn = '';
await configRepository.remove('lastUserLoggedIn');
// workerTimers.setTimeout(() => location.reload(), 500);
state.attemptingAutoLogin = false;
state.autoLoginAttempts.clear();
closeWebSocket();
}
/**
* Automatically logs in the last user after the app is mounted.
* @returns {Promise<void>}
*/
async function autoLoginAfterMounted() {
if (
!advancedSettingsStore.enablePrimaryPassword &&
(await configRepository.getString('lastUserLoggedIn')) !== null
) {
const user =
state.loginForm.savedCredentials[
state.loginForm.lastUserLoggedIn
];
if (user?.loginParmas?.endpoint) {
AppGlobal.endpointDomain = user.loginParmas.endpoint;
AppGlobal.websocketDomain = user.loginParmas.websocket;
}
// login at startup
state.loginForm.loading = true;
authRequest
.getConfig()
.catch((err) => {
state.loginForm.loading = false;
throw err;
})
.then(() => {
userStore
.getCurrentUser()
.finally(() => {
state.loginForm.loading = false;
})
.catch((err) => {
updateLoopStore.nextCurrentUserRefresh = 60; // 1min
console.error(err);
});
});
}
}
async function clearCookiesTryLogin() {
await webApiService.clearCookies();
if (state.loginForm.lastUserLoggedIn) {
const user =
state.loginForm.savedCredentials[
state.loginForm.lastUserLoggedIn
];
if (typeof user !== 'undefined') {
delete user.cookies;
await relogin(user);
}
}
}
async function resendEmail2fa() {
if (state.loginForm.lastUserLoggedIn) {
const user =
state.loginForm.savedCredentials[
state.loginForm.lastUserLoggedIn
];
if (typeof user !== 'undefined') {
await webApiService.clearCookies();
delete user.cookies;
relogin(user).then(() => {
new Noty({
type: 'success',
text: 'Email 2FA resent.'
}).show();
});
return;
}
}
new Noty({
type: 'error',
text: 'Cannot send 2FA email without saved credentials. Please login again.'
}).show();
}
function enablePrimaryPasswordChange() {
advancedSettingsStore.enablePrimaryPassword =
!advancedSettingsStore.enablePrimaryPassword;
state.enablePrimaryPasswordDialog.password = '';
state.enablePrimaryPasswordDialog.rePassword = '';
if (advancedSettingsStore.enablePrimaryPassword) {
state.enablePrimaryPasswordDialog.visible = true;
} else {
$app.$prompt(
t('prompt.primary_password.description'),
t('prompt.primary_password.header'),
{
inputType: 'password',
inputPattern: /[\s\S]{1,32}/
}
)
.then(({ value }) => {
for (const userId in state.loginForm.savedCredentials) {
security
.decrypt(
state.loginForm.savedCredentials[userId]
.loginParmas.password,
value
)
.then(async (pt) => {
state.saveCredentials = {
username:
state.loginForm.savedCredentials[userId]
.loginParmas.username,
password: pt
};
await updateStoredUser(
state.loginForm.savedCredentials[userId]
.user
);
await configRepository.setBool(
'enablePrimaryPassword',
false
);
})
.catch(async () => {
advancedSettingsStore.enablePrimaryPassword = true;
advancedSettingsStore.setEnablePrimaryPasswordConfigRepository(
true
);
});
}
})
.catch(async () => {
advancedSettingsStore.enablePrimaryPassword = true;
advancedSettingsStore.setEnablePrimaryPasswordConfigRepository(
true
);
});
}
}
async function setPrimaryPassword() {
await configRepository.setBool(
'enablePrimaryPassword',
advancedSettingsStore.enablePrimaryPassword
);
state.enablePrimaryPasswordDialog.visible = false;
if (advancedSettingsStore.enablePrimaryPassword) {
const key = state.enablePrimaryPasswordDialog.password;
for (const userId in state.loginForm.savedCredentials) {
security
.encrypt(
state.loginForm.savedCredentials[userId].loginParmas
.password,
key
)
.then((ct) => {
state.saveCredentials = {
username:
state.loginForm.savedCredentials[userId]
.loginParmas.username,
password: ct
};
updateStoredUser(
state.loginForm.savedCredentials[userId].user
);
});
}
}
}
async function updateStoredUser(user) {
let savedCredentials = {};
if ((await configRepository.getString('savedCredentials')) !== null) {
savedCredentials = JSON.parse(
await configRepository.getString('savedCredentials')
);
}
if (state.saveCredentials) {
const credentialsToSave = {
user,
loginParmas: state.saveCredentials
};
savedCredentials[user.id] = credentialsToSave;
state.saveCredentials = null;
} else if (typeof savedCredentials[user.id] !== 'undefined') {
savedCredentials[user.id].user = user;
savedCredentials[user.id].cookies =
await webApiService.getCookies();
}
state.loginForm.savedCredentials = savedCredentials;
const jsonCredentialsArray = JSON.stringify(savedCredentials);
await configRepository.setString(
'savedCredentials',
jsonCredentialsArray
);
state.loginForm.lastUserLoggedIn = user.id;
await configRepository.setString('lastUserLoggedIn', user.id);
}
async function migrateStoredUsers() {
let savedCredentials = {};
if ((await configRepository.getString('savedCredentials')) !== null) {
savedCredentials = JSON.parse(
await configRepository.getString('savedCredentials')
);
}
for (const name in savedCredentials) {
const userId = savedCredentials[name]?.user?.id;
if (userId && userId !== name) {
savedCredentials[userId] = savedCredentials[name];
delete savedCredentials[name];
}
}
await configRepository.setString(
'savedCredentials',
JSON.stringify(savedCredentials)
);
}
function checkPrimaryPassword(args) {
return new Promise((resolve, reject) => {
if (!advancedSettingsStore.enablePrimaryPassword) {
resolve(args.password);
}
$app.$prompt(
t('prompt.primary_password.description'),
t('prompt.primary_password.header'),
{
inputType: 'password',
inputPattern: /[\s\S]{1,32}/
}
)
.then(({ value }) => {
security
.decrypt(args.password, value)
.then(resolve)
.catch(reject);
})
.catch(reject);
});
}
async function toggleCustomEndpoint() {
await configRepository.setBool(
'VRCX_enableCustomEndpoint',
state.enableCustomEndpoint
);
state.loginForm.endpoint = '';
state.loginForm.websocket = '';
}
function logout() {
$app.$confirm('Continue? Logout', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
if (action === 'confirm') {
handleLogoutEvent();
}
}
});
}
async function relogin(user) {
const { loginParmas } = user;
if (user.cookies) {
await webApiService.setCookies(user.cookies);
}
state.loginForm.lastUserLoggedIn = user.user.id; // for resend email 2fa
if (loginParmas.endpoint) {
AppGlobal.endpointDomain = loginParmas.endpoint;
AppGlobal.websocketDomain = loginParmas.websocket;
} else {
AppGlobal.endpointDomain = AppGlobal.endpointDomainVrchat;
AppGlobal.websocketDomain = AppGlobal.websocketDomainVrchat;
}
return new Promise((resolve, reject) => {
state.loginForm.loading = true;
if (advancedSettingsStore.enablePrimaryPassword) {
checkPrimaryPassword(loginParmas)
.then((pwd) => {
return authRequest
.getConfig()
.catch((err) => {
reject(err);
})
.then(() => {
authLogin({
username: loginParmas.username,
password: pwd,
cipher: loginParmas.password,
endpoint: loginParmas.endpoint,
websocket: loginParmas.websocket
})
.catch((err2) => {
reject(err2);
})
.then(() => {
resolve();
});
});
})
.catch((_) => {
$app.$message({
message: 'Incorrect primary password',
type: 'error'
});
reject(_);
});
} else {
authRequest
.getConfig()
.catch((err) => {
reject(err);
})
.then(() => {
authLogin({
username: loginParmas.username,
password: loginParmas.password,
endpoint: loginParmas.endpoint,
websocket: loginParmas.websocket
})
.catch((err2) => {
handleLogoutEvent();
reject(err2);
})
.then(() => {
resolve();
});
});
}
}).finally(() => (state.loginForm.loading = false));
}
async function deleteSavedLogin(userId) {
const savedCredentials = JSON.parse(
await configRepository.getString('savedCredentials')
);
delete savedCredentials[userId];
// Disable primary password when no account is available.
if (Object.keys(savedCredentials).length === 0) {
advancedSettingsStore.enablePrimaryPassword = false;
advancedSettingsStore.setEnablePrimaryPasswordConfigRepository(
false
);
}
state.loginForm.savedCredentials = savedCredentials;
const jsonCredentials = JSON.stringify(savedCredentials);
await configRepository.setString('savedCredentials', jsonCredentials);
new Noty({
type: 'success',
text: 'Account removed.'
}).show();
}
async function login() {
// TODO: remove/refactor saveCredentials & primaryPassword (security)
await webApiService.clearCookies();
if (!state.loginForm.loading) {
state.loginForm.loading = true;
if (state.loginForm.endpoint) {
AppGlobal.endpointDomain = state.loginForm.endpoint;
AppGlobal.websocketDomain = state.loginForm.websocket;
} else {
AppGlobal.endpointDomain = AppGlobal.endpointDomainVrchat;
AppGlobal.websocketDomain = AppGlobal.websocketDomainVrchat;
}
authRequest
.getConfig()
.catch((err) => {
state.loginForm.loading = false;
throw err;
})
.then((args) => {
if (
state.loginForm.saveCredentials &&
advancedSettingsStore.enablePrimaryPassword
) {
$app.$prompt(
t('prompt.primary_password.description'),
t('prompt.primary_password.header'),
{
inputType: 'password',
inputPattern: /[\s\S]{1,32}/
}
)
.then(({ value }) => {
const saveCredential =
state.loginForm.savedCredentials[
Object.keys(
state.loginForm.savedCredentials
)[0]
];
security
.decrypt(
saveCredential.loginParmas.password,
value
)
.then(() => {
security
.encrypt(
state.loginForm.password,
value
)
.then((pwd) => {
authLogin({
username:
state.loginForm
.username,
password:
state.loginForm
.password,
endpoint:
state.loginForm
.endpoint,
websocket:
state.loginForm
.websocket,
saveCredentials:
state.loginForm
.saveCredentials,
cipher: pwd
});
});
});
})
.finally(() => {
state.loginForm.loading = false;
});
return args;
}
authLogin({
username: state.loginForm.username,
password: state.loginForm.password,
endpoint: state.loginForm.endpoint,
websocket: state.loginForm.websocket,
saveCredentials: state.loginForm.saveCredentials
}).finally(() => {
state.loginForm.loading = false;
});
return args;
});
}
}
function promptTOTP() {
if (state.twoFactorAuthDialogVisible) {
return;
}
AppApi.FlashWindow();
state.twoFactorAuthDialogVisible = true;
$app.$prompt(t('prompt.totp.description'), t('prompt.totp.header'), {
distinguishCancelAndClose: true,
cancelButtonText: t('prompt.totp.use_otp'),
confirmButtonText: t('prompt.totp.verify'),
inputPlaceholder: t('prompt.totp.input_placeholder'),
inputPattern: /^[0-9]{6}$/,
inputErrorMessage: t('prompt.totp.input_error'),
callback: (action, instance) => {
if (action === 'confirm') {
authRequest
.verifyTOTP({
code: instance.inputValue.trim()
})
.catch((err) => {
clearCookiesTryLogin();
throw err;
})
.then((args) => {
userStore.getCurrentUser();
return args;
});
} else if (action === 'cancel') {
promptOTP();
}
},
beforeClose: (action, instance, done) => {
state.twoFactorAuthDialogVisible = false;
done();
}
});
}
function promptOTP() {
if (state.twoFactorAuthDialogVisible) {
return;
}
state.twoFactorAuthDialogVisible = true;
$app.$prompt(t('prompt.otp.description'), t('prompt.otp.header'), {
distinguishCancelAndClose: true,
cancelButtonText: t('prompt.otp.use_totp'),
confirmButtonText: t('prompt.otp.verify'),
inputPlaceholder: t('prompt.otp.input_placeholder'),
inputPattern: /^[a-z0-9]{4}-[a-z0-9]{4}$/,
inputErrorMessage: t('prompt.otp.input_error'),
callback: (action, instance) => {
if (action === 'confirm') {
authRequest
.verifyOTP({
code: instance.inputValue.trim()
})
.catch((err) => {
clearCookiesTryLogin();
throw err;
})
.then((args) => {
userStore.getCurrentUser();
return args;
});
} else if (action === 'cancel') {
promptTOTP();
}
},
beforeClose: (action, instance, done) => {
state.twoFactorAuthDialogVisible = false;
done();
}
});
}
function promptEmailOTP() {
if (state.twoFactorAuthDialogVisible) {
return;
}
AppApi.FlashWindow();
state.twoFactorAuthDialogVisible = true;
$app.$prompt(
t('prompt.email_otp.description'),
t('prompt.email_otp.header'),
{
distinguishCancelAndClose: true,
cancelButtonText: t('prompt.email_otp.resend'),
confirmButtonText: t('prompt.email_otp.verify'),
inputPlaceholder: t('prompt.email_otp.input_placeholder'),
inputPattern: /^[0-9]{6}$/,
inputErrorMessage: t('prompt.email_otp.input_error'),
callback: (action, instance) => {
if (action === 'confirm') {
authRequest
.verifyEmailOTP({
code: instance.inputValue.trim()
})
.catch((err) => {
promptEmailOTP();
throw err;
})
.then((args) => {
userStore.getCurrentUser();
return args;
});
} else if (action === 'cancel') {
resendEmail2fa();
}
},
beforeClose: (action, instance, done) => {
state.twoFactorAuthDialogVisible = false;
done();
}
}
);
}
/**
* @param {{ username: string, password: string, saveCredentials: any, cipher: string }} params credential to login
* @returns {Promise<{origin: boolean, json: any}>}
*/
function authLogin(params) {
let { username, password, saveCredentials, cipher } = params;
username = encodeURIComponent(username);
password = encodeURIComponent(password);
const auth = btoa(`${username}:${password}`);
if (saveCredentials) {
delete params.saveCredentials;
if (cipher) {
params.password = cipher;
delete params.cipher;
}
state.saveCredentials = params;
}
return request('auth/user', {
method: 'GET',
headers: {
Authorization: `Basic ${auth}`
}
}).then((json) => {
const args = {
json,
origin: true
};
handleCurrentUserUpdate(json);
return args;
});
}
function handleCurrentUserUpdate(json) {
if (
json.requiresTwoFactorAuth &&
json.requiresTwoFactorAuth.includes('emailOtp')
) {
promptEmailOTP();
} else if (json.requiresTwoFactorAuth) {
promptTOTP();
} else {
updateLoopStore.nextCurrentUserRefresh = 420; // 7mins
userStore.applyCurrentUser(json);
initWebsocket();
}
}
function handleAutoLogin() {
if (state.attemptingAutoLogin) {
return;
}
state.attemptingAutoLogin = true;
const user =
state.loginForm.savedCredentials[state.loginForm.lastUserLoggedIn];
if (typeof user === 'undefined') {
state.attemptingAutoLogin = false;
return;
}
if (advancedSettingsStore.enablePrimaryPassword) {
console.error(
'Primary password is enabled, this disables auto login.'
);
state.attemptingAutoLogin = false;
logout();
return;
}
const attemptsInLastHour = Array.from(state.autoLoginAttempts).filter(
(timestamp) => timestamp > new Date().getTime() - 3600000
).length;
if (attemptsInLastHour >= 3) {
console.error(
'More than 3 auto login attempts within the past hour, logging out instead of attempting auto login.'
);
state.attemptingAutoLogin = false;
logout();
return;
}
state.autoLoginAttempts.add(new Date().getTime());
relogin(user)
.then(() => {
if (AppGlobal.errorNoty) {
AppGlobal.errorNoty.close();
}
AppGlobal.errorNoty = new Noty({
type: 'success',
text: 'Automatically logged in.'
}).show();
console.log('Automatically logged in.');
})
.catch((err) => {
if (AppGlobal.errorNoty) {
AppGlobal.errorNoty.close();
}
AppGlobal.errorNoty = new Noty({
type: 'error',
text: 'Failed to login automatically.'
}).show();
console.error('Failed to login automatically.', err);
})
.finally(() => {
if (!navigator.onLine) {
AppGlobal.errorNoty = new Noty({
type: 'error',
text: `You're offline.`
}).show();
console.error(`You're offline.`);
}
});
}
async function loginComplete() {
await database.initUserTables(userStore.currentUser.id);
watchState.isLoggedIn = true;
AppApi.CheckGameRunning(); // restore state from hot-reload
}
return {
state,
loginForm,
enablePrimaryPasswordDialog,
saveCredentials,
twoFactorAuthDialogVisible,
cachedConfig,
enableCustomEndpoint,
clearCookiesTryLogin,
resendEmail2fa,
enablePrimaryPasswordChange,
setPrimaryPassword,
updateStoredUser,
migrateStoredUsers,
checkPrimaryPassword,
autoLoginAfterMounted,
toggleCustomEndpoint,
logout,
relogin,
deleteSavedLogin,
login,
handleAutoLogin,
handleLogoutEvent,
handleCurrentUserUpdate,
loginComplete
};
});
+720
View File
@@ -0,0 +1,720 @@
import Noty from 'noty';
import { defineStore } from 'pinia';
import { computed, reactive, watch } from 'vue';
import { avatarRequest, imageRequest } from '../api';
import { $app } from '../app';
import { database } from '../service/database';
import { AppGlobal } from '../service/appConfig';
import webApiService from '../service/webapi';
import { watchState } from '../service/watchState';
import {
checkVRChatCache,
extractFileId,
getAvailablePlatforms,
getBundleDateSize,
getPlatformInfo,
replaceBioSymbols,
storeAvatarImage
} from '../shared/utils';
import { useAvatarProviderStore } from './avatarProvider';
import { useFavoriteStore } from './favorite';
import { useAdvancedSettingsStore } from './settings/advanced';
import { useUserStore } from './user';
import { useVRCXUpdaterStore } from './vrcxUpdater';
export const useAvatarStore = defineStore('Avatar', () => {
const favoriteStore = useFavoriteStore();
const avatarProviderStore = useAvatarProviderStore();
const vrcxUpdaterStore = useVRCXUpdaterStore();
const advancedSettingsStore = useAdvancedSettingsStore();
const userStore = useUserStore();
const state = reactive({
avatarDialog: {
visible: false,
loading: false,
id: '',
memo: '',
ref: {},
isFavorite: false,
isBlocked: false,
isQuestFallback: false,
hasImposter: false,
imposterVersion: '',
isPC: false,
isQuest: false,
isIos: false,
bundleSizes: [],
platformInfo: {},
galleryImages: [],
galleryLoading: false,
lastUpdated: '',
inCache: false,
cacheSize: 0,
cacheLocked: false,
cachePath: ''
},
cachedAvatarModerations: new Map(),
avatarHistory: new Set(),
avatarHistoryArray: [],
cachedAvatars: new Map(),
cachedAvatarNames: new Map()
});
const avatarDialog = computed({
get: () => state.avatarDialog,
set: (value) => {
state.avatarDialog = value;
}
});
const avatarHistory = state.avatarHistory;
const avatarHistoryArray = computed({
get: () => state.avatarHistoryArray,
set: (value) => {
state.avatarHistoryArray = value;
}
});
const cachedAvatarModerations = computed({
get: () => state.cachedAvatarModerations,
set: (value) => {
state.cachedAvatarModerations = value;
}
});
const cachedAvatars = computed({
get: () => state.cachedAvatars,
set: (value) => {
state.cachedAvatars = value;
}
});
const cachedAvatarNames = computed({
get: () => state.cachedAvatarNames,
set: (value) => {
state.cachedAvatarNames = value;
}
});
watch(
() => watchState.isLoggedIn,
(isLoggedIn) => {
state.avatarDialog.visible = false;
state.cachedAvatars.clear();
state.cachedAvatarNames.clear();
state.cachedAvatarModerations.clear();
state.avatarHistory.clear();
state.avatarHistoryArray = [];
if (isLoggedIn) {
getAvatarHistory();
}
},
{ flush: 'sync' }
);
/**
/ * @param {object} json
/ * @returns {object} ref
*/
function applyAvatar(json) {
json.name = replaceBioSymbols(json.name);
json.description = replaceBioSymbols(json.description);
let ref = state.cachedAvatars.get(json.id);
if (typeof ref === 'undefined') {
ref = {
acknowledgements: '',
authorId: '',
authorName: '',
created_at: '',
description: '',
featured: false,
highestPrice: null,
id: '',
imageUrl: '',
lock: false,
lowestPrice: null,
name: '',
productId: null,
publishedListings: [],
releaseStatus: '',
searchable: false,
styles: [],
tags: [],
thumbnailImageUrl: '',
unityPackageUrl: '',
unityPackageUrlObject: {},
unityPackages: [],
updated_at: '',
version: 0,
...json
};
state.cachedAvatars.set(ref.id, ref);
} else {
const { unityPackages } = ref;
Object.assign(ref, json);
if (
json.unityPackages?.length > 0 &&
unityPackages.length > 0 &&
!json.unityPackages[0].assetUrl
) {
ref.unityPackages = unityPackages;
}
}
for (const listing of ref.publishedListings) {
listing.displayName = replaceBioSymbols(listing.displayName);
listing.description = replaceBioSymbols(listing.description);
}
favoriteStore.applyFavorite('avatar', ref.id);
if (favoriteStore.localAvatarFavoritesList.includes(ref.id)) {
for (
let i = 0;
i < favoriteStore.localAvatarFavoriteGroups.length;
++i
) {
const groupName = favoriteStore.localAvatarFavoriteGroups[i];
if (!favoriteStore.localAvatarFavorites[groupName]) {
continue;
}
for (
let j = 0;
j < favoriteStore.localAvatarFavorites[groupName].length;
++j
) {
const ref =
favoriteStore.localAvatarFavorites[groupName][j];
if (ref.id === ref.id) {
favoriteStore.localAvatarFavorites[groupName][j] = ref;
}
}
}
// update db cache
database.addAvatarToCache(ref);
}
return ref;
}
/**
*
* @param {string} avatarId
* @returns
*/
function showAvatarDialog(avatarId) {
const D = state.avatarDialog;
D.visible = true;
D.loading = true;
D.id = avatarId;
D.inCache = false;
D.cacheSize = 0;
D.cacheLocked = false;
D.cachePath = '';
D.isQuestFallback = false;
D.isPC = false;
D.isQuest = false;
D.isIos = false;
D.hasImposter = false;
D.imposterVersion = '';
D.lastUpdated = '';
D.bundleSizes = [];
D.platformInfo = {};
D.galleryImages = [];
D.galleryLoading = true;
D.isFavorite =
favoriteStore.cachedFavoritesByObjectId.has(avatarId) ||
(userStore.currentUser.$isVRCPlus &&
favoriteStore.localAvatarFavoritesList.includes(avatarId));
D.isBlocked = state.cachedAvatarModerations.has(avatarId);
const ref2 = state.cachedAvatars.get(avatarId);
if (typeof ref2 !== 'undefined') {
D.ref = ref2;
updateVRChatAvatarCache();
if (
ref2.releaseStatus !== 'public' &&
ref2.authorId !== userStore.currentUser.id
) {
D.loading = false;
return;
}
}
avatarRequest
.getAvatar({ avatarId })
.then((args) => {
const ref = applyAvatar(args.json);
D.ref = ref;
getAvatarGallery(avatarId);
updateVRChatAvatarCache();
if (/quest/.test(ref.tags)) {
D.isQuestFallback = true;
}
const { isPC, isQuest, isIos } = getAvailablePlatforms(
ref.unityPackages
);
D.isPC = isPC;
D.isQuest = isQuest;
D.isIos = isIos;
D.platformInfo = getPlatformInfo(ref.unityPackages);
for (let i = ref.unityPackages.length - 1; i > -1; i--) {
const unityPackage = ref.unityPackages[i];
if (unityPackage.variant === 'impostor') {
D.hasImposter = true;
D.imposterVersion = unityPackage.impostorizerVersion;
break;
}
}
if (D.bundleSizes.length === 0) {
getBundleDateSize(ref).then((bundleSizes) => {
D.bundleSizes = bundleSizes;
});
}
})
.catch((err) => {
D.visible = false;
throw err;
})
.finally(() => {
$app.$nextTick(() => (D.loading = false));
});
}
/**
* aka: `$app.methods.getAvatarGallery`
* @param {string} avatarId
* @returns {Promise<string[]>}
*/
async function getAvatarGallery(avatarId) {
const D = state.avatarDialog;
const args = await avatarRequest
.getAvatarGallery(avatarId)
.finally(() => {
D.galleryLoading = false;
});
if (args.params.galleryId !== D.id) {
return;
}
D.galleryImages = [];
// wtf is this? why is order sorting only needed if it's your own avatar?
const sortedGallery = args.json.sort((a, b) => {
if (!a.order && !b.order) {
return 0;
}
return a.order - b.order;
});
for (const file of sortedGallery) {
const url = file.versions[file.versions.length - 1].file.url;
D.galleryImages.push(url);
}
// for JSON tab treeData
D.ref.gallery = args.json;
return D.galleryImages;
}
/**
*
* @param {object} json
* @returns {object} ref
*/
function applyAvatarModeration(json) {
// fix inconsistent Unix time response
if (typeof json.created === 'number') {
json.created = new Date(json.created).toJSON();
}
let ref = state.cachedAvatarModerations.get(json.targetAvatarId);
if (typeof ref === 'undefined') {
ref = {
avatarModerationType: '',
created: '',
targetAvatarId: '',
...json
};
state.cachedAvatarModerations.set(ref.targetAvatarId, ref);
} else {
Object.assign(ref, json);
}
// update avatar dialog
const D = state.avatarDialog;
if (
D.visible &&
ref.avatarModerationType === 'block' &&
D.id === ref.targetAvatarId
) {
D.isBlocked = true;
}
return ref;
}
function updateVRChatAvatarCache() {
const D = state.avatarDialog;
if (D.visible) {
D.inCache = false;
D.cacheSize = 0;
D.cacheLocked = false;
D.cachePath = '';
checkVRChatCache(D.ref).then((cacheInfo) => {
if (cacheInfo.Item1 > 0) {
D.inCache = true;
D.cacheSize = `${(cacheInfo.Item1 / 1048576).toFixed(2)} MB`;
D.cachePath = cacheInfo.Item3;
}
D.cacheLocked = cacheInfo.Item2;
});
}
}
/**
* aka: `$app.methods.getAvatarHistory`
* @returns {Promise<void>}
*/
async function getAvatarHistory() {
state.avatarHistory = new Set();
const historyArray = await database.getAvatarHistory(
userStore.currentUser.id
);
for (let i = 0; i < historyArray.length; i++) {
const avatar = historyArray[i];
if (avatar.authorId === userStore.currentUser.id) {
continue;
}
applyAvatar(avatar);
state.avatarHistory.add(avatar.id);
}
state.avatarHistoryArray = historyArray;
}
/**
* aka: `$app.methods.addAvatarToHistory`
* @param {string} avatarId
*/
function addAvatarToHistory(avatarId) {
avatarRequest.getAvatar({ avatarId }).then((args) => {
const ref = applyAvatar(args.json);
database.addAvatarToCache(ref);
database.addAvatarToHistory(ref.id);
if (ref.authorId === userStore.currentUser.id) {
return;
}
const historyArray = state.avatarHistoryArray;
for (let i = 0; i < historyArray.length; ++i) {
if (historyArray[i].id === ref.id) {
historyArray.splice(i, 1);
}
}
state.avatarHistoryArray.unshift(ref);
state.avatarHistory.delete(ref.id);
state.avatarHistory.add(ref.id);
});
}
function clearAvatarHistory() {
state.avatarHistory = new Set();
state.avatarHistoryArray = [];
database.clearAvatarHistory();
}
function promptClearAvatarHistory() {
$app.$confirm('Continue? Clear Avatar History', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
if (action === 'confirm') {
clearAvatarHistory();
}
}
});
}
/**
*
* @param {string} imageUrl
* @returns {Promise<object>}
*/
async function getAvatarName(imageUrl) {
const fileId = extractFileId(imageUrl);
if (!fileId) {
return {
ownerId: '',
avatarName: '-'
};
}
if (state.cachedAvatarNames.has(fileId)) {
return state.cachedAvatarNames.get(fileId);
}
const args = await imageRequest.getAvatarImages({ fileId });
return storeAvatarImage(args, state.cachedAvatarNames);
}
async function lookupAvatars(type, search) {
const avatars = new Map();
if (type === 'search') {
try {
const response = await webApiService.execute({
url: `${
avatarProviderStore.avatarRemoteDatabaseProvider
}?${type}=${encodeURIComponent(search)}&n=5000`,
method: 'GET',
headers: {
Referer: 'https://vrcx.app',
'VRCX-ID': vrcxUpdaterStore.vrcxId
}
});
const json = JSON.parse(response.data);
if (AppGlobal.debugWebRequests) {
console.log(json, response);
}
if (response.status === 200 && typeof json === 'object') {
json.forEach((avatar) => {
if (!avatars.has(avatar.Id)) {
const ref = {
authorId: '',
authorName: '',
name: '',
description: '',
id: '',
imageUrl: '',
thumbnailImageUrl: '',
created_at: '0001-01-01T00:00:00.0000000Z',
updated_at: '0001-01-01T00:00:00.0000000Z',
releaseStatus: 'public',
...avatar
};
avatars.set(ref.id, ref);
}
});
} else {
throw new Error(`Error: ${response.data}`);
}
} catch (err) {
const msg = `Avatar search failed for ${search} with ${avatarProviderStore.avatarRemoteDatabaseProvider}\n${err}`;
console.error(msg);
$app.$message({
message: msg,
type: 'error'
});
}
} else if (type === 'authorId') {
const length =
avatarProviderStore.avatarRemoteDatabaseProviderList.length;
for (let i = 0; i < length; ++i) {
const url =
avatarProviderStore.avatarRemoteDatabaseProviderList[i];
const avatarArray = await lookupAvatarsByAuthor(url, search);
avatarArray.forEach((avatar) => {
if (!avatars.has(avatar.id)) {
avatars.set(avatar.id, avatar);
}
});
}
}
return avatars;
}
async function lookupAvatarByImageFileId(authorId, fileId) {
const length =
avatarProviderStore.avatarRemoteDatabaseProviderList.length;
for (let i = 0; i < length; ++i) {
const url = avatarProviderStore.avatarRemoteDatabaseProviderList[i];
const avatarArray = await lookupAvatarsByAuthor(url, authorId);
for (const avatar of avatarArray) {
if (extractFileId(avatar.imageUrl) === fileId) {
return avatar.id;
}
}
}
return null;
}
async function lookupAvatarsByAuthor(url, authorId) {
const avatars = [];
if (!url) {
return avatars;
}
try {
const response = await webApiService.execute({
url: `${url}?authorId=${encodeURIComponent(authorId)}`,
method: 'GET',
headers: {
Referer: 'https://vrcx.app',
'VRCX-ID': vrcxUpdaterStore.vrcxId
}
});
const json = JSON.parse(response.data);
if (AppGlobal.debugWebRequests) {
console.log(json, response);
}
if (response.status === 200 && typeof json === 'object') {
json.forEach((avatar) => {
const ref = {
authorId: '',
authorName: '',
name: '',
description: '',
id: '',
imageUrl: '',
thumbnailImageUrl: '',
created_at: '0001-01-01T00:00:00.0000000Z',
updated_at: '0001-01-01T00:00:00.0000000Z',
releaseStatus: 'public',
...avatar
};
avatars.push(ref);
});
} else {
throw new Error(`Error: ${response.data}`);
}
} catch (err) {
const msg = `Avatar lookup failed for ${authorId} with ${url}\n${err}`;
console.error(msg);
$app.$message({
message: msg,
type: 'error'
});
}
return avatars;
}
function selectAvatarWithConfirmation(id) {
$app.$confirm(`Continue? Select Avatar`, 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
if (action !== 'confirm') {
return;
}
selectAvatarWithoutConfirmation(id);
}
});
}
function selectAvatarWithoutConfirmation(id) {
if (userStore.currentUser.currentAvatar === id) {
$app.$message({
message: 'Avatar already selected',
type: 'info'
});
return;
}
avatarRequest
.selectAvatar({
avatarId: id
})
.then((args) => {
new Noty({
type: 'success',
text: 'Avatar changed via launch command'
}).show();
return args;
});
}
function checkAvatarCache(fileId) {
let avatarId = '';
for (let ref of state.cachedAvatars.values()) {
if (extractFileId(ref.imageUrl) === fileId) {
avatarId = ref.id;
}
}
return avatarId;
}
async function checkAvatarCacheRemote(fileId, ownerUserId) {
if (advancedSettingsStore.avatarRemoteDatabase) {
const avatarId = await lookupAvatarByImageFileId(
ownerUserId,
fileId
);
return avatarId;
}
return null;
}
async function showAvatarAuthorDialog(
refUserId,
ownerUserId,
currentAvatarImageUrl
) {
const fileId = extractFileId(currentAvatarImageUrl);
if (!fileId) {
$app.$message({
message: 'Sorry, the author is unknown',
type: 'error'
});
} else if (refUserId === userStore.currentUser.id) {
showAvatarDialog(userStore.currentUser.currentAvatar);
} else {
let avatarId = checkAvatarCache(fileId);
let avatarInfo;
if (!avatarId) {
avatarInfo = await getAvatarName(currentAvatarImageUrl);
if (avatarInfo.ownerId === userStore.currentUser.id) {
userStore.refreshUserDialogAvatars(fileId);
}
}
if (!avatarId) {
avatarId = await checkAvatarCacheRemote(
fileId,
avatarInfo.ownerId
);
}
if (!avatarId) {
if (avatarInfo.ownerId === refUserId) {
$app.$message({
message:
"It's personal (own) avatar or not found in avatar database",
type: 'warning'
});
} else {
$app.$message({
message: 'Avatar not found in avatar database',
type: 'warning'
});
userStore.showUserDialog(avatarInfo.ownerId);
}
}
if (avatarId) {
showAvatarDialog(avatarId);
}
}
}
function addAvatarWearTime(avatarId) {
if (!userStore.currentUser.$previousAvatarSwapTime || !avatarId) {
return;
}
const timeSpent =
Date.now() - userStore.currentUser.$previousAvatarSwapTime;
database.addAvatarTimeSpent(avatarId, timeSpent);
}
return {
state,
avatarDialog,
avatarHistory,
avatarHistoryArray,
cachedAvatarModerations,
cachedAvatars,
cachedAvatarNames,
showAvatarDialog,
applyAvatarModeration,
getAvatarGallery,
updateVRChatAvatarCache,
getAvatarHistory,
addAvatarToHistory,
applyAvatar,
promptClearAvatarHistory,
getAvatarName,
lookupAvatars,
selectAvatarWithConfirmation,
showAvatarAuthorDialog,
addAvatarWearTime
};
});
+180
View File
@@ -0,0 +1,180 @@
import { defineStore } from 'pinia';
import { computed, reactive, watch } from 'vue';
import configRepository from '../service/config';
import { watchState } from '../service/watchState';
import { useAdvancedSettingsStore } from './settings/advanced';
export const useAvatarProviderStore = defineStore('AvatarProvider', () => {
const advancedSettingsStore = useAdvancedSettingsStore();
const state = reactive({
isAvatarProviderDialogVisible: false,
avatarRemoteDatabaseProvider: '',
avatarRemoteDatabaseProviderList: [
'https://api.avtrdb.com/v2/avatar/search/vrcx',
'https://avtr.just-h.party/vrcx_search.php'
]
});
async function initAvatarProviderState() {
state.avatarRemoteDatabaseProviderList = JSON.parse(
await configRepository.getString(
'VRCX_avatarRemoteDatabaseProviderList',
'[ "https://api.avtrdb.com/v2/avatar/search/vrcx", "https://avtr.just-h.party/vrcx_search.php" ]'
)
);
if (
state.avatarRemoteDatabaseProviderList.length === 1 &&
state.avatarRemoteDatabaseProviderList[0] ===
'https://avtr.just-h.party/vrcx_search.php'
) {
state.avatarRemoteDatabaseProviderList.unshift(
'https://api.avtrdb.com/v2/avatar/search/vrcx'
);
await configRepository.setString(
'VRCX_avatarRemoteDatabaseProviderList',
JSON.stringify(state.avatarRemoteDatabaseProviderList)
);
}
if (
await configRepository.getString(
'VRCX_avatarRemoteDatabaseProvider'
)
) {
// move existing provider to new list
const avatarRemoteDatabaseProvider =
await configRepository.getString(
'VRCX_avatarRemoteDatabaseProvider'
);
if (
!state.avatarRemoteDatabaseProviderList.includes(
avatarRemoteDatabaseProvider
)
) {
state.avatarRemoteDatabaseProviderList.push(
avatarRemoteDatabaseProvider
);
}
await configRepository.remove('VRCX_avatarRemoteDatabaseProvider');
await configRepository.setString(
'VRCX_avatarRemoteDatabaseProviderList',
JSON.stringify(state.avatarRemoteDatabaseProviderList)
);
}
if (state.avatarRemoteDatabaseProviderList.length > 0) {
state.avatarRemoteDatabaseProvider =
state.avatarRemoteDatabaseProviderList[0];
}
}
const isAvatarProviderDialogVisible = computed({
get() {
return state.isAvatarProviderDialogVisible;
},
set(value) {
state.isAvatarProviderDialogVisible = value;
}
});
const avatarRemoteDatabaseProvider = computed({
get() {
return state.avatarRemoteDatabaseProvider;
},
set(value) {
state.avatarRemoteDatabaseProvider = value;
}
});
const avatarRemoteDatabaseProviderList = computed({
get() {
return state.avatarRemoteDatabaseProviderList;
},
set(value) {
state.avatarRemoteDatabaseProviderList = value;
}
});
watch(
() => watchState.isLoggedIn,
() => {
state.isAvatarProviderDialogVisible = false;
},
{ flush: 'sync' }
);
/**
* @param {string} url
*/
function addAvatarProvider(url) {
if (!url) {
return;
}
showAvatarProviderDialog();
if (!state.avatarRemoteDatabaseProviderList.includes(url)) {
state.avatarRemoteDatabaseProviderList.push(url);
}
saveAvatarProviderList();
}
/**
* @param {string} url
*/
function removeAvatarProvider(url) {
const length = state.avatarRemoteDatabaseProviderList.length;
for (let i = 0; i < length; ++i) {
if (state.avatarRemoteDatabaseProviderList[i] === url) {
state.avatarRemoteDatabaseProviderList.splice(i, 1);
}
}
saveAvatarProviderList();
}
async function saveAvatarProviderList() {
const length = state.avatarRemoteDatabaseProviderList.length;
for (let i = 0; i < length; ++i) {
if (!state.avatarRemoteDatabaseProviderList[i]) {
state.avatarRemoteDatabaseProviderList.splice(i, 1);
}
}
await configRepository.setString(
'VRCX_avatarRemoteDatabaseProviderList',
JSON.stringify(state.avatarRemoteDatabaseProviderList)
);
if (state.avatarRemoteDatabaseProviderList.length > 0) {
state.avatarRemoteDatabaseProvider =
state.avatarRemoteDatabaseProviderList[0];
advancedSettingsStore.setAvatarRemoteDatabase(true);
} else {
state.avatarRemoteDatabaseProvider = '';
advancedSettingsStore.setAvatarRemoteDatabase(false);
}
}
function showAvatarProviderDialog() {
state.isAvatarProviderDialogVisible = true;
}
/**
* @param {string} provider
*/
function setAvatarProvider(provider) {
state.avatarRemoteDatabaseProvider = provider;
}
initAvatarProviderState();
return {
state,
isAvatarProviderDialogVisible,
avatarRemoteDatabaseProvider,
avatarRemoteDatabaseProviderList,
addAvatarProvider,
removeAvatarProvider,
saveAvatarProviderList,
showAvatarProviderDialog,
setAvatarProvider
};
});
File diff suppressed because it is too large Load Diff
+238
View File
@@ -0,0 +1,238 @@
import { defineStore } from 'pinia';
import { computed, reactive, watch } from 'vue';
import configRepository from '../service/config';
import { database } from '../service/database';
import { watchState } from '../service/watchState';
import { useFriendStore } from './friend';
import { useNotificationStore } from './notification';
import { useSharedFeedStore } from './sharedFeed';
import { useUiStore } from './ui';
import { useVrcxStore } from './vrcx';
export const useFeedStore = defineStore('Feed', () => {
const friendStore = useFriendStore();
const notificationStore = useNotificationStore();
const UiStore = useUiStore();
const vrcxStore = useVrcxStore();
const sharedFeedStore = useSharedFeedStore();
const state = reactive({
feedTable: {
data: [],
search: '',
vip: false,
loading: false,
filter: [],
tableProps: {
stripe: true,
size: 'mini',
defaultSort: {
prop: 'created_at',
order: 'descending'
}
},
pageSize: 15,
paginationProps: {
small: true,
layout: 'sizes,prev,pager,next,total',
pageSizes: [10, 15, 20, 25, 50, 100]
}
},
feedSessionTable: []
});
async function init() {
state.feedTable.filter = JSON.parse(
await configRepository.getString('VRCX_feedTableFilters', '[]')
);
state.feedTable.vip = await configRepository.getBool(
'VRCX_feedTableVIPFilter',
false
);
}
init();
const feedTable = computed({
get: () => state.feedTable,
set: (value) => {
state.feedTable = value;
}
});
const feedSessionTable = computed({
get: () => state.feedSessionTable,
set: (value) => {
state.feedSessionTable = value;
}
});
watch(
() => watchState.isLoggedIn,
(isLoggedIn) => {
state.feedTable.data = [];
state.feedSessionTable = [];
if (isLoggedIn) {
initFeedTable();
}
},
{ flush: 'sync' }
);
function feedSearch(row) {
const value = state.feedTable.search.toUpperCase();
if (!value) {
return true;
}
if (
(value.startsWith('wrld_') || value.startsWith('grp_')) &&
String(row.location).toUpperCase().includes(value)
) {
return true;
}
switch (row.type) {
case 'GPS':
if (String(row.displayName).toUpperCase().includes(value)) {
return true;
}
if (String(row.worldName).toUpperCase().includes(value)) {
return true;
}
return false;
case 'Online':
if (String(row.displayName).toUpperCase().includes(value)) {
return true;
}
if (String(row.worldName).toUpperCase().includes(value)) {
return true;
}
return false;
case 'Offline':
if (String(row.displayName).toUpperCase().includes(value)) {
return true;
}
if (String(row.worldName).toUpperCase().includes(value)) {
return true;
}
return false;
case 'Status':
if (String(row.displayName).toUpperCase().includes(value)) {
return true;
}
if (String(row.status).toUpperCase().includes(value)) {
return true;
}
if (
String(row.statusDescription).toUpperCase().includes(value)
) {
return true;
}
return false;
case 'Avatar':
if (String(row.displayName).toUpperCase().includes(value)) {
return true;
}
if (String(row.avatarName).toUpperCase().includes(value)) {
return true;
}
return false;
case 'Bio':
if (String(row.displayName).toUpperCase().includes(value)) {
return true;
}
if (String(row.bio).toUpperCase().includes(value)) {
return true;
}
if (String(row.previousBio).toUpperCase().includes(value)) {
return true;
}
return false;
}
return true;
}
async function feedTableLookup() {
await configRepository.setString(
'VRCX_feedTableFilters',
JSON.stringify(state.feedTable.filter)
);
await configRepository.setBool(
'VRCX_feedTableVIPFilter',
state.feedTable.vip
);
state.feedTable.loading = true;
let vipList = [];
if (state.feedTable.vip) {
vipList = Array.from(friendStore.localFavoriteFriends.values());
}
state.feedTable.data = await database.lookupFeedDatabase(
state.feedTable.search,
state.feedTable.filter,
vipList
);
state.feedTable.loading = false;
}
function addFeed(feed) {
notificationStore.queueFeedNoty(feed);
state.feedSessionTable.push(feed);
sharedFeedStore.updateSharedFeed(false);
if (
state.feedTable.filter.length > 0 &&
!state.feedTable.filter.includes(feed.type)
) {
return;
}
if (
state.feedTable.vip &&
!friendStore.localFavoriteFriends.has(feed.userId)
) {
return;
}
if (!feedSearch(feed)) {
return;
}
state.feedTable.data.push(feed);
sweepFeed();
UiStore.notifyMenu('feed');
}
function sweepFeed() {
let limit;
const { data } = state.feedTable;
const j = data.length;
if (j > vrcxStore.maxTableSize) {
data.splice(0, j - vrcxStore.maxTableSize);
}
const date = new Date();
date.setDate(date.getDate() - 1); // 24 hour limit
limit = date.toJSON();
let i = 0;
const k = state.feedSessionTable.length;
while (i < k && state.feedSessionTable[i].created_at < limit) {
++i;
}
if (i === k) {
state.feedSessionTable = [];
} else if (i) {
state.feedSessionTable.splice(0, i);
}
}
async function initFeedTable() {
state.feedTable.loading = true;
feedTableLookup();
state.feedSessionTable = await database.getFeedDatabase();
}
return {
state,
feedTable,
feedSessionTable,
initFeedTable,
feedTableLookup,
addFeed
};
});
+1782
View File
File diff suppressed because it is too large Load Diff
+686
View File
@@ -0,0 +1,686 @@
import Noty from 'noty';
import { defineStore } from 'pinia';
import { computed, reactive, watch } from 'vue';
import * as workerTimers from 'worker-timers';
import {
inventoryRequest,
userRequest,
vrcPlusIconRequest,
vrcPlusImageRequest
} from '../api';
import { $app } from '../app';
import { AppGlobal } from '../service/appConfig';
import { watchState } from '../service/watchState';
import {
getEmojiFileName,
getPrintFileName,
getPrintLocalDate
} from '../shared/utils';
import { useAdvancedSettingsStore } from './settings/advanced';
import { useI18n } from 'vue-i18n-bridge';
export const useGalleryStore = defineStore('Gallery', () => {
const advancedSettingsStore = useAdvancedSettingsStore();
const { t } = useI18n();
const state = reactive({
galleryTable: [],
// galleryDialog: {},
galleryDialogVisible: false,
galleryDialogGalleryLoading: false,
galleryDialogIconsLoading: false,
galleryDialogEmojisLoading: false,
galleryDialogStickersLoading: false,
galleryDialogPrintsLoading: false,
galleryDialogInventoryLoading: false,
uploadImage: '',
VRCPlusIconsTable: [],
printUploadNote: '',
printCropBorder: true,
printCache: [],
printQueue: [],
printQueueWorker: null,
stickerTable: [],
instanceStickersCache: [],
printTable: [],
emojiTable: [],
inventoryTable: [],
previousImagesDialogVisible: false,
previousImagesTable: [],
fullscreenImageDialog: {
visible: false,
imageUrl: '',
fileName: ''
},
instanceInventoryCache: [],
instanceInventoryQueue: [],
instanceInventoryQueueWorker: null
});
const galleryTable = computed({
get: () => state.galleryTable,
set: (value) => {
state.galleryTable = value;
}
});
const galleryDialogVisible = computed({
get: () => state.galleryDialogVisible,
set: (value) => {
state.galleryDialogVisible = value;
}
});
const galleryDialogGalleryLoading = computed({
get: () => state.galleryDialogGalleryLoading,
set: (value) => {
state.galleryDialogGalleryLoading = value;
}
});
const galleryDialogIconsLoading = computed({
get: () => state.galleryDialogIconsLoading,
set: (value) => {
state.galleryDialogIconsLoading = value;
}
});
const galleryDialogEmojisLoading = computed({
get: () => state.galleryDialogEmojisLoading,
set: (value) => {
state.galleryDialogEmojisLoading = value;
}
});
const galleryDialogStickersLoading = computed({
get: () => state.galleryDialogStickersLoading,
set: (value) => {
state.galleryDialogStickersLoading = value;
}
});
const galleryDialogPrintsLoading = computed({
get: () => state.galleryDialogPrintsLoading,
set: (value) => {
state.galleryDialogPrintsLoading = value;
}
});
const galleryDialogInventoryLoading = computed({
get: () => state.galleryDialogInventoryLoading,
set: (value) => {
state.galleryDialogInventoryLoading = value;
}
});
const uploadImage = computed({
get: () => state.uploadImage,
set: (value) => {
state.uploadImage = value;
}
});
const VRCPlusIconsTable = computed({
get: () => state.VRCPlusIconsTable,
set: (value) => {
state.VRCPlusIconsTable = value;
}
});
const printUploadNote = computed({
get: () => state.printUploadNote,
set: (value) => {
state.printUploadNote = value;
}
});
const printCropBorder = computed({
get: () => state.printCropBorder,
set: (value) => {
state.printCropBorder = value;
}
});
const stickerTable = computed({
get: () => state.stickerTable,
set: (value) => {
state.stickerTable = value;
}
});
const instanceStickersCache = computed({
get: () => state.instanceStickersCache,
set: (value) => {
state.instanceStickersCache = value;
}
});
const printTable = computed({
get: () => state.printTable,
set: (value) => {
state.printTable = value;
}
});
const emojiTable = computed({
get: () => state.emojiTable,
set: (value) => {
state.emojiTable = value;
}
});
const inventoryTable = computed({
get: () => state.inventoryTable,
set: (value) => {
state.inventoryTable = value;
}
});
const previousImagesDialogVisible = computed({
get: () => state.previousImagesDialogVisible,
set: (value) => {
state.previousImagesDialogVisible = value;
}
});
const previousImagesTable = computed({
get: () => state.previousImagesTable,
set: (value) => {
state.previousImagesTable = value;
}
});
const fullscreenImageDialog = computed({
get: () => state.fullscreenImageDialog,
set: (value) => {
state.fullscreenImageDialog = value;
}
});
watch(
() => watchState.isLoggedIn,
(isLoggedIn) => {
state.previousImagesTable = [];
state.galleryTable = [];
state.VRCPlusIconsTable = [];
state.stickerTable = [];
state.printTable = [];
state.emojiTable = [];
state.galleryDialogVisible = false;
state.previousImagesDialogVisible = false;
state.fullscreenImageDialog.visible = false;
if (isLoggedIn) {
tryDeleteOldPrints();
}
},
{ flush: 'sync' }
);
function handleFilesList(args) {
if (args.params.tag === 'gallery') {
state.galleryTable = args.json.reverse();
}
if (args.params.tag === 'icon') {
state.VRCPlusIconsTable = args.json.reverse();
}
if (args.params.tag === 'sticker') {
state.stickerTable = args.json.reverse();
state.galleryDialogStickersLoading = false;
}
if (args.params.tag === 'emoji') {
state.emojiTable = args.json.reverse();
state.galleryDialogEmojisLoading = false;
}
}
function handleGalleryImageAdd(args) {
if (Object.keys(state.galleryTable).length !== 0) {
state.galleryTable.unshift(args.json);
}
}
function showGalleryDialog() {
state.galleryDialogVisible = true;
refreshGalleryTable();
refreshVRCPlusIconsTable();
refreshEmojiTable();
refreshStickerTable();
refreshPrintTable();
getInventory();
}
function refreshGalleryTable() {
state.galleryDialogGalleryLoading = true;
const params = {
n: 100,
tag: 'gallery'
};
vrcPlusIconRequest
.getFileList(params)
.then((args) => handleFilesList(args))
.finally(() => {
state.galleryDialogGalleryLoading = false;
});
}
function refreshVRCPlusIconsTable() {
state.galleryDialogIconsLoading = true;
const params = {
n: 100,
tag: 'icon'
};
vrcPlusIconRequest
.getFileList(params)
.then((args) => handleFilesList(args))
.finally(() => {
state.galleryDialogIconsLoading = false;
});
}
function inviteImageUpload(e) {
const files = e.target.files || e.dataTransfer.files;
if (!files.length) {
return;
}
if (files[0].size >= 100000000) {
// 100MB
$app.$message({
message: t('message.file.too_large'),
type: 'error'
});
clearInviteImageUpload();
return;
}
if (!files[0].type.match(/image.*/)) {
$app.$message({
message: t('message.file.not_image'),
type: 'error'
});
clearInviteImageUpload();
return;
}
const r = new FileReader();
r.onload = function () {
state.uploadImage = btoa(r.result);
};
r.readAsBinaryString(files[0]);
}
function clearInviteImageUpload() {
const buttonList = document.querySelectorAll(
'.inviteImageUploadButton'
);
buttonList.forEach((button) => (button.value = ''));
state.uploadImage = '';
}
function refreshStickerTable() {
state.galleryDialogStickersLoading = true;
const params = {
n: 100,
tag: 'sticker'
};
vrcPlusIconRequest
.getFileList(params)
.then((args) => handleFilesList(args))
.finally(() => {
state.galleryDialogStickersLoading = false;
});
}
function handleStickerAdd(args) {
if (Object.keys(state.stickerTable).length !== 0) {
state.stickerTable.unshift(args.json);
}
}
async function trySaveStickerToFile(displayName, userId, inventoryId) {
if (state.instanceStickersCache.includes(inventoryId)) {
return;
}
state.instanceStickersCache.push(inventoryId);
if (state.instanceStickersCache.size > 100) {
state.instanceStickersCache.shift();
}
const args = await inventoryRequest.getUserInventoryItem({
inventoryId,
userId
});
if (
args.json.itemType !== 'sticker' ||
!args.json.flags.includes('ugc')
) {
// Not a sticker or ugc, skipping
return;
}
const imageUrl = args.json.metadata?.imageUrl ?? args.json.imageUrl;
const createdAt = args.json.created_at;
const monthFolder = createdAt.slice(0, 7);
const fileNameDate = createdAt
.replace(/:/g, '-')
.replace(/T/g, '_')
.replace(/Z/g, '');
const fileName = `${displayName}_${fileNameDate}_${inventoryId}.png`;
const filePath = await AppApi.SaveStickerToFile(
imageUrl,
advancedSettingsStore.ugcFolderPath,
monthFolder,
fileName
);
if (filePath) {
console.log(`Sticker saved to file: ${monthFolder}\\${fileName}`);
}
}
async function refreshPrintTable() {
state.galleryDialogPrintsLoading = true;
const params = {
n: 100
};
try {
const args = await vrcPlusImageRequest.getPrints(params);
args.json.sort((a, b) => {
return new Date(b.timestamp) - new Date(a.timestamp);
});
state.printTable = args.json;
} catch (error) {
console.error('Error fetching prints:', error);
} finally {
state.galleryDialogPrintsLoading = false;
}
}
function queueSavePrintToFile(printId) {
if (state.printCache.includes(printId)) {
return;
}
state.printCache.push(printId);
if (state.printCache.length > 100) {
state.printCache.shift();
}
state.printQueue.push(printId);
if (!state.printQueueWorker) {
state.printQueueWorker = workerTimers.setInterval(() => {
const printId = state.printQueue.shift();
if (printId) {
trySavePrintToFile(printId);
}
}, 2_500);
}
}
async function trySavePrintToFile(printId) {
const args = await vrcPlusImageRequest.getPrint({ printId });
const imageUrl = args.json?.files?.image;
if (!imageUrl) {
console.error('Print image URL is missing', args);
return;
}
const print = args.json;
const createdAt = getPrintLocalDate(print);
try {
const owner = await userRequest.getCachedUser({
userId: print.ownerId
});
console.log(
`Print spawned by ${owner?.json?.displayName} id:${print.id} note:${print.note} authorName:${print.authorName} at:${new Date().toISOString()}`
);
} catch (err) {
console.error(err);
}
const monthFolder = createdAt.toISOString().slice(0, 7);
const fileName = getPrintFileName(print);
const filePath = await AppApi.SavePrintToFile(
imageUrl,
advancedSettingsStore.ugcFolderPath,
monthFolder,
fileName
);
if (filePath) {
console.log(`Print saved to file: ${monthFolder}\\${fileName}`);
if (advancedSettingsStore.cropInstancePrints) {
if (!(await AppApi.CropPrintImage(filePath))) {
console.error('Failed to crop print image');
}
}
}
if (state.printQueue.length === 0) {
workerTimers.clearInterval(state.printQueueWorker);
state.printQueueWorker = null;
}
}
// #endregion
// #region | Emoji
function refreshEmojiTable() {
state.galleryDialogEmojisLoading = true;
const params = {
n: 100,
tag: 'emoji'
};
vrcPlusIconRequest
.getFileList(params)
.then((args) => handleFilesList(args))
.finally(() => {
state.galleryDialogEmojisLoading = false;
});
}
async function getInventory() {
state.inventoryTable = [];
advancedSettingsStore.currentUserInventory.clear();
const params = {
n: 100,
offset: 0,
order: 'newest'
};
state.galleryDialogInventoryLoading = true;
try {
for (let i = 0; i < 100; i++) {
params.offset = i * params.n;
const args = await inventoryRequest.getInventoryItems(params);
for (const item of args.json.data) {
advancedSettingsStore.currentUserInventory.set(
item.id,
item
);
if (!item.flags.includes('ugc')) {
state.inventoryTable.push(item);
}
}
if (args.json.data.length === 0) {
break;
}
}
} catch (error) {
console.error('Error fetching inventory items:', error);
} finally {
state.galleryDialogInventoryLoading = false;
}
}
async function tryDeleteOldPrints() {
if (!advancedSettingsStore.deleteOldPrints) {
return;
}
await refreshPrintTable();
const printLimit = 64 - 2; // 2 reserved for new prints
const printCount = state.printTable.length;
if (printCount <= printLimit) {
return;
}
const deleteCount = printCount - printLimit;
if (deleteCount <= 0) {
return;
}
const idList = [];
for (let i = 0; i < deleteCount; i++) {
const print = state.printTable[printCount - 1 - i];
idList.push(print.id);
}
console.log(`Deleting ${deleteCount} old prints`, idList);
try {
for (const printId of idList) {
await vrcPlusImageRequest.deletePrint(printId);
const text = `Old print automatically deleted: ${printId}`;
if (AppGlobal.errorNoty) {
AppGlobal.errorNoty.close();
}
AppGlobal.errorNoty = new Noty({
type: 'info',
text
}).show();
}
} catch (err) {
console.error('Failed to delete old print:', err);
}
await refreshPrintTable();
}
async function checkPreviousImageAvailable(images) {
state.previousImagesTable = [];
for (const image of images) {
if (image.file && image.file.url) {
const response = await fetch(image.file.url, {
method: 'HEAD',
redirect: 'follow'
}).catch((error) => {
console.log(error);
});
if (response.status === 200) {
state.previousImagesTable.push(image);
}
}
}
}
function showFullscreenImageDialog(imageUrl, fileName) {
if (!imageUrl) {
return;
}
const D = state.fullscreenImageDialog;
D.imageUrl = imageUrl;
D.fileName = fileName;
D.visible = true;
}
function queueCheckInstanceInventory(inventoryId, userId) {
if (
state.instanceInventoryCache.includes(inventoryId) ||
state.instanceStickersCache.includes(inventoryId)
) {
return;
}
state.instanceInventoryCache.push(inventoryId);
if (state.instanceInventoryCache.length > 100) {
state.instanceInventoryCache.shift();
}
state.instanceInventoryQueue.push({ inventoryId, userId });
if (!state.instanceInventoryQueueWorker) {
state.instanceInventoryQueueWorker = workerTimers.setInterval(
() => {
const item = state.instanceInventoryQueue.shift();
if (item?.inventoryId) {
trySaveEmojiToFile(item.inventoryId, item.userId);
}
},
2_500
);
}
}
async function trySaveEmojiToFile(inventoryId, userId) {
const args = await inventoryRequest.getUserInventoryItem({
inventoryId,
userId
});
if (
args.json.itemType !== 'emoji' ||
!args.json.flags.includes('ugc')
) {
// Not an emoji or ugc, skipping
return;
}
const userArgs = await userRequest.getCachedUser({
userId: args.json.holderId
});
const displayName = userArgs.json?.displayName ?? '';
const emoji = args.json.metadata;
emoji.name = `${displayName}_${inventoryId}`;
const emojiFileName = getEmojiFileName(emoji);
const imageUrl = args.json.metadata?.imageUrl ?? args.json.imageUrl;
const createdAt = args.json.created_at;
const monthFolder = createdAt.slice(0, 7);
const filePath = await AppApi.SaveEmojiToFile(
imageUrl,
advancedSettingsStore.ugcFolderPath,
monthFolder,
emojiFileName
);
if (filePath) {
console.log(
`Emoji saved to file: ${monthFolder}\\${emojiFileName}`
);
}
if (state.instanceInventoryQueue.length === 0) {
workerTimers.clearInterval(state.instanceInventoryQueueWorker);
state.instanceInventoryQueueWorker = null;
}
}
return {
state,
galleryTable,
galleryDialogVisible,
galleryDialogGalleryLoading,
galleryDialogIconsLoading,
galleryDialogEmojisLoading,
galleryDialogStickersLoading,
galleryDialogPrintsLoading,
galleryDialogInventoryLoading,
uploadImage,
VRCPlusIconsTable,
printUploadNote,
printCropBorder,
stickerTable,
instanceStickersCache,
printTable,
emojiTable,
inventoryTable,
previousImagesDialogVisible,
previousImagesTable,
fullscreenImageDialog,
showGalleryDialog,
refreshGalleryTable,
refreshVRCPlusIconsTable,
inviteImageUpload,
clearInviteImageUpload,
refreshStickerTable,
trySaveStickerToFile,
refreshPrintTable,
queueSavePrintToFile,
refreshEmojiTable,
getInventory,
tryDeleteOldPrints,
checkPreviousImageAvailable,
showFullscreenImageDialog,
handleStickerAdd,
handleGalleryImageAdd,
handleFilesList,
queueCheckInstanceInventory
};
});
+299
View File
@@ -0,0 +1,299 @@
import { defineStore } from 'pinia';
import { computed, reactive } from 'vue';
import * as workerTimers from 'worker-timers';
import { $app } from '../app';
import configRepository from '../service/config.js';
import { database } from '../service/database';
import {
deleteVRChatCache as _deleteVRChatCache,
isRealInstance
} from '../shared/utils';
import { useAvatarStore } from './avatar';
import { useGameLogStore } from './gameLog';
import { useInstanceStore } from './instance';
import { useLaunchStore } from './launch';
import { useLocationStore } from './location';
import { useNotificationStore } from './notification';
import { useAdvancedSettingsStore } from './settings/advanced';
import { useUpdateLoopStore } from './updateLoop';
import { useUserStore } from './user';
import { useVrStore } from './vr';
import { useWorldStore } from './world';
export const useGameStore = defineStore('Game', () => {
const advancedSettingsStore = useAdvancedSettingsStore();
const locationStore = useLocationStore();
const notificationStore = useNotificationStore();
const avatarStore = useAvatarStore();
const launchStore = useLaunchStore();
const worldStore = useWorldStore();
const instanceStore = useInstanceStore();
const gameLogStore = useGameLogStore();
const vrStore = useVrStore();
const userStore = useUserStore();
const updateLoopStore = useUpdateLoopStore();
const state = reactive({
lastCrashedTime: null,
VRChatUsedCacheSize: '',
VRChatTotalCacheSize: '',
VRChatCacheSizeLoading: false,
isGameRunning: false,
isGameNoVR: true,
isSteamVRRunning: false,
isHmdAfk: false
});
async function init() {
state.isGameNoVR = await configRepository.getBool('isGameNoVR');
}
init();
const VRChatUsedCacheSize = computed({
get: () => state.VRChatUsedCacheSize,
set: (value) => {
state.VRChatUsedCacheSize = value;
}
});
const VRChatTotalCacheSize = computed({
get: () => state.VRChatTotalCacheSize,
set: (value) => {
state.VRChatTotalCacheSize = value;
}
});
const VRChatCacheSizeLoading = computed({
get: () => state.VRChatCacheSizeLoading,
set: (value) => {
state.VRChatCacheSizeLoading = value;
}
});
const isGameRunning = computed({
get: () => state.isGameRunning,
set: (value) => {
state.isGameRunning = value;
}
});
const isGameNoVR = computed({
get: () => state.isGameNoVR,
set: (value) => {
state.isGameNoVR = value;
}
});
const isSteamVRRunning = computed({
get: () => state.isSteamVRRunning,
set: (value) => {
state.isSteamVRRunning = value;
}
});
const isHmdAfk = computed({
get: () => state.isHmdAfk,
set: (value) => {
state.isHmdAfk = value;
}
});
async function deleteVRChatCache(ref) {
await _deleteVRChatCache(ref);
getVRChatCacheSize();
worldStore.updateVRChatWorldCache();
avatarStore.updateVRChatAvatarCache();
}
function autoVRChatCacheManagement() {
if (advancedSettingsStore.autoSweepVRChatCache) {
sweepVRChatCache();
}
}
async function sweepVRChatCache() {
const output = await AssetBundleManager.SweepCache();
console.log('SweepCache', output);
if (advancedSettingsStore.isVRChatConfigDialogVisible) {
getVRChatCacheSize();
}
}
function checkIfGameCrashed() {
if (!advancedSettingsStore.relaunchVRChatAfterCrash) {
return;
}
const { location } = locationStore.lastLocation;
AppApi.VrcClosedGracefully().then((result) => {
if (result || !isRealInstance(location)) {
return;
}
// check if relaunched less than 2mins ago (prvent crash loop)
if (
state.lastCrashedTime &&
new Date() - state.lastCrashedTime < 120_000
) {
console.log('VRChat was recently crashed, not relaunching');
return;
}
state.lastCrashedTime = new Date();
// wait a bit for SteamVR to potentially close before deciding to relaunch
let restartDelay = 8000;
if (state.isGameNoVR) {
// wait for game to close before relaunching
restartDelay = 2000;
}
workerTimers.setTimeout(
() => restartCrashedGame(location),
restartDelay
);
});
}
function restartCrashedGame(location) {
if (!state.isGameNoVR && !state.isSteamVRRunning) {
console.log("SteamVR isn't running, not relaunching VRChat");
return;
}
AppApi.FocusWindow();
const message = 'VRChat crashed, attempting to rejoin last instance';
$app.$message({
message,
type: 'info'
});
const entry = {
created_at: new Date().toJSON(),
type: 'Event',
data: message
};
database.addGamelogEventToDatabase(entry);
notificationStore.queueGameLogNoty(entry);
gameLogStore.addGameLog(entry);
launchStore.launchGame(location, '', state.isGameNoVR);
}
async function getVRChatCacheSize() {
state.VRChatCacheSizeLoading = true;
const totalCacheSize = 30;
state.VRChatTotalCacheSize = totalCacheSize;
const usedCacheSize = await AssetBundleManager.GetCacheSize();
state.VRChatUsedCacheSize = (usedCacheSize / 1073741824).toFixed(2);
state.VRChatCacheSizeLoading = false;
}
// use in C#
async function updateIsGameRunning(
isGameRunning,
isSteamVRRunning,
isHmdAfk
) {
const avatarStore = useAvatarStore();
if (advancedSettingsStore.gameLogDisabled) {
return;
}
if (isGameRunning !== state.isGameRunning) {
state.isGameRunning = isGameRunning;
if (isGameRunning) {
userStore.currentUser.$online_for = Date.now();
userStore.currentUser.$offline_for = '';
userStore.currentUser.$previousAvatarSwapTime = Date.now();
} else {
await configRepository.setBool('isGameNoVR', state.isGameNoVR);
userStore.currentUser.$online_for = '';
userStore.currentUser.$offline_for = Date.now();
instanceStore.removeAllQueuedInstances();
autoVRChatCacheManagement();
checkIfGameCrashed();
updateLoopStore.ipcTimeout = 0;
avatarStore.addAvatarWearTime(
userStore.currentUser.currentAvatar
);
userStore.currentUser.$previousAvatarSwapTime = '';
}
locationStore.lastLocationReset();
gameLogStore.clearNowPlaying();
vrStore.updateVRLastLocation();
workerTimers.setTimeout(() => checkVRChatDebugLogging(), 60000);
updateLoopStore.nextDiscordUpdate = 0;
console.log(new Date(), 'isGameRunning', isGameRunning);
}
if (isSteamVRRunning !== state.isSteamVRRunning) {
state.isSteamVRRunning = isSteamVRRunning;
console.log('isSteamVRRunning:', isSteamVRRunning);
}
if (isHmdAfk !== state.isHmdAfk) {
state.isHmdAfk = isHmdAfk;
console.log('isHmdAfk:', isHmdAfk);
}
vrStore.updateOpenVR();
}
async function checkVRChatDebugLogging() {
if (advancedSettingsStore.gameLogDisabled) {
return;
}
try {
const loggingEnabled =
await getVRChatRegistryKey('LOGGING_ENABLED');
if (
loggingEnabled === null ||
typeof loggingEnabled === 'undefined'
) {
// key not found
return;
}
if (parseInt(loggingEnabled, 10) === 1) {
// already enabled
return;
}
const result = await AppApi.SetVRChatRegistryKey(
'LOGGING_ENABLED',
'1',
4
);
if (!result) {
// failed to set key
$app.$alert(
'VRCX has noticed VRChat debug logging is disabled. VRCX requires debug logging in order to function correctly. Please enable debug logging in VRChat quick menu settings > debug > enable debug logging, then rejoin the instance or restart VRChat.',
'Enable debug logging'
);
console.error('Failed to enable debug logging', result);
return;
}
$app.$alert(
'VRCX has noticed VRChat debug logging is disabled and automatically re-enabled it. VRCX requires debug logging in order to function correctly.',
'Enabled debug logging'
);
console.log('Enabled debug logging');
} catch (e) {
console.error(e);
}
}
async function getVRChatRegistryKey(key) {
if (LINUX) {
return AppApi.GetVRChatRegistryKeyString(key);
}
return AppApi.GetVRChatRegistryKey(key);
}
return {
state,
VRChatUsedCacheSize,
VRChatTotalCacheSize,
VRChatCacheSizeLoading,
isGameRunning,
isGameNoVR,
isSteamVRRunning,
isHmdAfk,
deleteVRChatCache,
sweepVRChatCache,
getVRChatCacheSize,
updateIsGameRunning,
getVRChatRegistryKey,
checkVRChatDebugLogging
};
});
File diff suppressed because it is too large Load Diff
+1049
View File
File diff suppressed because it is too large Load Diff
+107
View File
@@ -0,0 +1,107 @@
import { createPinia } from 'pinia';
import { useAuthStore } from './auth';
import { useAvatarStore } from './avatar';
import { useAvatarProviderStore } from './avatarProvider';
import { useFavoriteStore } from './favorite';
import { useFeedStore } from './feed';
import { useFriendStore } from './friend';
import { useGalleryStore } from './gallery';
import { useGameStore } from './game';
import { useGameLogStore } from './gameLog';
import { useGroupStore } from './group';
import { useInstanceStore } from './instance';
import { useInviteStore } from './invite';
import { useLaunchStore } from './launch';
import { useLocationStore } from './location';
import { useModerationStore } from './moderation';
import { useNotificationStore } from './notification';
import { usePhotonStore } from './photon';
import { useSearchStore } from './search';
import { useAdvancedSettingsStore } from './settings/advanced';
import { useAppearanceSettingsStore } from './settings/appearance';
import { useDiscordPresenceSettingsStore } from './settings/discordPresence';
import { useGeneralSettingsStore } from './settings/general';
import { useNotificationsSettingsStore } from './settings/notifications';
import { useWristOverlaySettingsStore } from './settings/wristOverlay';
import { useSharedFeedStore } from './sharedFeed';
import { useUiStore } from './ui';
import { useUpdateLoopStore } from './updateLoop';
import { useUserStore } from './user';
import { useVrStore } from './vr';
import { useVrcxStore } from './vrcx';
import { useVRCXUpdaterStore } from './vrcxUpdater';
import { useWorldStore } from './world';
export const pinia = createPinia();
export function createGlobalStores() {
return {
advancedSettings: useAdvancedSettingsStore(),
appearanceSettings: useAppearanceSettingsStore(),
discordPresenceSettings: useDiscordPresenceSettingsStore(),
generalSettings: useGeneralSettingsStore(),
notificationsSettings: useNotificationsSettingsStore(),
wristOverlaySettings: useWristOverlaySettingsStore(),
avatarProvider: useAvatarProviderStore(),
favorite: useFavoriteStore(),
friend: useFriendStore(),
photon: usePhotonStore(),
user: useUserStore(),
vrcxUpdater: useVRCXUpdaterStore(),
avatar: useAvatarStore(),
world: useWorldStore(),
group: useGroupStore(),
location: useLocationStore(),
instance: useInstanceStore(),
moderation: useModerationStore(),
invite: useInviteStore(),
gallery: useGalleryStore(),
notification: useNotificationStore(),
feed: useFeedStore(),
ui: useUiStore(),
gameLog: useGameLogStore(),
search: useSearchStore(),
game: useGameStore(),
launch: useLaunchStore(),
vr: useVrStore(),
vrcx: useVrcxStore(),
sharedFeed: useSharedFeedStore(),
updateLoop: useUpdateLoopStore(),
auth: useAuthStore()
};
}
export {
useAuthStore,
useAvatarStore,
useAvatarProviderStore,
useFavoriteStore,
useFeedStore,
useFriendStore,
useGalleryStore,
useGameStore,
useGameLogStore,
useGroupStore,
useInstanceStore,
useInviteStore,
useLaunchStore,
useLocationStore,
useModerationStore,
useNotificationStore,
usePhotonStore,
useSearchStore,
useAdvancedSettingsStore,
useAppearanceSettingsStore,
useDiscordPresenceSettingsStore,
useGeneralSettingsStore,
useNotificationsSettingsStore,
useWristOverlaySettingsStore,
useUiStore,
useUserStore,
useVrStore,
useVrcxStore,
useVRCXUpdaterStore,
useWorldStore,
useSharedFeedStore,
useUpdateLoopStore
};
File diff suppressed because it is too large Load Diff
+189
View File
@@ -0,0 +1,189 @@
import { defineStore } from 'pinia';
import { computed, reactive, watch } from 'vue';
import { instanceRequest, inviteMessagesRequest } from '../api';
import { $app } from '../app';
import { watchState } from '../service/watchState';
import { parseLocation } from '../shared/utils';
import { useInstanceStore } from './instance';
export const useInviteStore = defineStore('Invite', () => {
const instanceStore = useInstanceStore();
const state = reactive({
editInviteMessageDialog: {
visible: false,
inviteMessage: {},
messageType: '',
newMessage: ''
},
inviteMessageTable: {
data: [],
tableProps: {
stripe: true,
size: 'mini'
},
layout: 'table',
visible: false
},
inviteResponseMessageTable: {
data: [],
tableProps: {
stripe: true,
size: 'mini'
},
layout: 'table',
visible: false
},
inviteRequestMessageTable: {
data: [],
tableProps: {
stripe: true,
size: 'mini'
},
layout: 'table',
visible: false
},
inviteRequestResponseMessageTable: {
data: [],
tableProps: {
stripe: true,
size: 'mini'
},
layout: 'table',
visible: false
}
});
watch(
() => watchState.isLoggedIn,
() => {
state.inviteMessageTable.data = [];
state.inviteResponseMessageTable.data = [];
state.inviteRequestMessageTable.data = [];
state.inviteRequestResponseMessageTable.data = [];
state.editInviteMessageDialog.visible = false;
state.inviteMessageTable.visible = false;
state.inviteResponseMessageTable.visible = false;
state.inviteRequestMessageTable.visible = false;
state.inviteRequestResponseMessageTable.visible = false;
},
{ flush: 'sync' }
);
const editInviteMessageDialog = computed({
get: () => state.editInviteMessageDialog,
set: (value) => {
state.editInviteMessageDialog = value;
}
});
const inviteMessageTable = computed({
get: () => state.inviteMessageTable,
set: (value) => {
state.inviteMessageTable = value;
}
});
const inviteResponseMessageTable = computed({
get: () => state.inviteResponseMessageTable,
set: (value) => {
state.inviteResponseMessageTable = value;
}
});
const inviteRequestMessageTable = computed({
get: () => state.inviteRequestMessageTable,
set: (value) => {
state.inviteRequestMessageTable = value;
}
});
const inviteRequestResponseMessageTable = computed({
get: () => state.inviteRequestResponseMessageTable,
set: (value) => {
state.inviteRequestResponseMessageTable = value;
}
});
/**
*
* @param {string} messageType
* @param {string} inviteMessage
*/
function showEditInviteMessageDialog(messageType, inviteMessage) {
const D = state.editInviteMessageDialog;
D.newMessage = inviteMessage.message;
D.visible = true;
D.inviteMessage = inviteMessage;
D.messageType = messageType;
}
/**
*
* @param {'message' | 'request' | 'response' | 'requestResponse'} mode
*/
function refreshInviteMessageTableData(mode) {
inviteMessagesRequest
.refreshInviteMessageTableData(mode)
.then(({ json }) => {
switch (mode) {
case 'message':
state.inviteMessageTable.data = json;
break;
case 'response':
state.inviteResponseMessageTable.data = json;
break;
case 'request':
state.inviteRequestMessageTable.data = json;
break;
case 'requestResponse':
state.inviteRequestResponseMessageTable.data = json;
break;
}
})
.catch((err) => {
console.error('refreshInviteMessageTableData Failed', err);
});
}
function newInstanceSelfInvite(worldId) {
instanceStore.createNewInstance(worldId).then((args) => {
const location = args?.json?.location;
if (!location) {
$app.$message({
message: 'Failed to create instance',
type: 'error'
});
return;
}
// self invite
const L = parseLocation(location);
if (!L.isRealInstance) {
return;
}
instanceRequest
.selfInvite({
instanceId: L.instanceId,
worldId: L.worldId
})
.then((args) => {
$app.$message({
message: 'Self invite sent',
type: 'success'
});
return args;
});
});
}
return {
state,
editInviteMessageDialog,
inviteMessageTable,
inviteResponseMessageTable,
inviteRequestMessageTable,
inviteRequestResponseMessageTable,
showEditInviteMessageDialog,
refreshInviteMessageTableData,
newInstanceSelfInvite
};
});
+162
View File
@@ -0,0 +1,162 @@
import { defineStore } from 'pinia';
import { computed, reactive, watch } from 'vue';
import { instanceRequest } from '../api';
import { $app } from '../app';
import configRepository from '../service/config';
import { watchState } from '../service/watchState';
import { parseLocation } from '../shared/utils';
export const useLaunchStore = defineStore('Launch', () => {
const state = reactive({
isLaunchOptionsDialogVisible: false,
launchDialogData: {
visible: false,
loading: false,
tag: '',
shortName: ''
}
});
const isLaunchOptionsDialogVisible = computed({
get: () => state.isLaunchOptionsDialogVisible,
set: (value) => {
state.isLaunchOptionsDialogVisible = value;
}
});
const launchDialogData = computed({
get: () => state.launchDialogData,
set: (value) => {
state.launchDialogData = value;
}
});
watch(
() => watchState.isLoggedIn,
() => {
state.isLaunchOptionsDialogVisible = false;
},
{ flush: 'sync' }
);
function showLaunchOptions() {
state.isLaunchOptionsDialogVisible = true;
}
/**
*
* @param {string} tag
* @param {string} shortName
* @returns {Promise<void>}
*/
async function showLaunchDialog(tag, shortName = null) {
state.launchDialogData = {
visible: true,
// flag, use for trigger adjustDialogZ
loading: true,
tag,
shortName
};
$app.$nextTick(() => (state.launchDialogData.loading = false));
}
/**
*
* @param {string} location
* @param {string} shortName
* @param {boolean} desktopMode
* @returns {Promise<void>}
*/
async function launchGame(location, shortName, desktopMode) {
const L = parseLocation(location);
const args = [];
if (
shortName &&
L.instanceType !== 'public' &&
L.groupAccessType !== 'public'
) {
args.push(
`vrchat://launch?ref=vrcx.app&id=${location}&shortName=${shortName}`
);
} else {
// fetch shortName
let newShortName = '';
const response = await instanceRequest.getInstanceShortName({
worldId: L.worldId,
instanceId: L.instanceId
});
if (response.json) {
if (response.json.shortName) {
newShortName = response.json.shortName;
} else {
newShortName = response.json.secureName;
}
}
if (newShortName) {
args.push(
`vrchat://launch?ref=vrcx.app&id=${location}&shortName=${newShortName}`
);
} else {
args.push(`vrchat://launch?ref=vrcx.app&id=${location}`);
}
}
const launchArguments =
await configRepository.getString('launchArguments');
const vrcLaunchPathOverride = await configRepository.getString(
'vrcLaunchPathOverride'
);
if (launchArguments) {
args.push(launchArguments);
}
if (desktopMode) {
args.push('--no-vr');
}
if (vrcLaunchPathOverride && !LINUX) {
AppApi.StartGameFromPath(
vrcLaunchPathOverride,
args.join(' ')
).then((result) => {
if (!result) {
$app.$message({
message:
'Failed to launch VRChat, invalid custom path set',
type: 'error'
});
} else {
$app.$message({
message: 'VRChat launched',
type: 'success'
});
}
});
} else {
AppApi.StartGame(args.join(' ')).then((result) => {
if (!result) {
$app.$message({
message:
'Failed to find VRChat, set a custom path in launch options',
type: 'error'
});
} else {
$app.$message({
message: 'VRChat launched',
type: 'success'
});
}
});
}
console.log('Launch Game', args.join(' '), desktopMode);
}
return {
state,
isLaunchOptionsDialogVisible,
launchDialogData,
showLaunchOptions,
showLaunchDialog,
launchGame
};
});
+258
View File
@@ -0,0 +1,258 @@
import { defineStore } from 'pinia';
import { computed, reactive } from 'vue';
import { database } from '../service/database';
import {
getGroupName,
getWorldName,
isRealInstance,
parseLocation
} from '../shared/utils';
import { useGameStore } from './game';
import { useGameLogStore } from './gameLog';
import { useInstanceStore } from './instance';
import { useNotificationStore } from './notification';
import { usePhotonStore } from './photon';
import { useAdvancedSettingsStore } from './settings/advanced';
import { useUserStore } from './user';
import { useVrStore } from './vr';
export const useLocationStore = defineStore('Location', () => {
const advancedSettingsStore = useAdvancedSettingsStore();
const userStore = useUserStore();
const instanceStore = useInstanceStore();
const notificationStore = useNotificationStore();
const gameStore = useGameStore();
const vrStore = useVrStore();
const photonStore = usePhotonStore();
const gameLogStore = useGameLogStore();
const state = reactive({
lastLocation: {
date: 0,
location: '',
name: '',
playerList: new Map(),
friendList: new Map()
},
lastLocation$: {
tag: '',
instanceId: '',
accessType: '',
worldName: '',
worldCapacity: 0,
joinUrl: '',
statusName: '',
statusImage: ''
},
lastLocationDestination: '',
lastLocationDestinationTime: 0
});
const lastLocation = computed({
get: () => state.lastLocation,
set: (value) => {
state.lastLocation = value;
}
});
const lastLocation$ = computed({
get: () => state.lastLocation$,
set: (value) => {
state.lastLocation$ = value;
}
});
const lastLocationDestination = computed({
get: () => state.lastLocationDestination,
set: (value) => {
state.lastLocationDestination = value;
}
});
const lastLocationDestinationTime = computed({
get: () => state.lastLocationDestinationTime,
set: (value) => {
state.lastLocationDestinationTime = value;
}
});
function updateCurrentUserLocation() {
const ref = userStore.cachedUsers.get(userStore.currentUser.id);
if (typeof ref === 'undefined') {
return;
}
// update cached user with both gameLog and API locations
let currentLocation = userStore.currentUser.$locationTag;
const L = parseLocation(currentLocation);
if (L.isTraveling) {
currentLocation = userStore.currentUser.$travelingToLocation;
}
ref.location = userStore.currentUser.$locationTag;
ref.travelingToLocation = userStore.currentUser.$travelingToLocation;
if (
gameStore.isGameRunning &&
!advancedSettingsStore.gameLogDisabled &&
state.lastLocation.location !== ''
) {
// use gameLog instead of API when game is running
currentLocation = state.lastLocation.location;
if (state.lastLocation.location === 'traveling') {
currentLocation = state.lastLocationDestination;
}
ref.location = state.lastLocation.location;
ref.travelingToLocation = state.lastLocationDestination;
}
ref.$online_for = userStore.currentUser.$online_for;
ref.$offline_for = userStore.currentUser.$offline_for;
ref.$location = parseLocation(currentLocation);
if (!gameStore.isGameRunning || advancedSettingsStore.gameLogDisabled) {
ref.$location_at = userStore.currentUser.$location_at;
ref.$travelingToTime = userStore.currentUser.$travelingToTime;
userStore.applyUserDialogLocation();
instanceStore.applyWorldDialogInstances();
instanceStore.applyGroupDialogInstances();
} else {
ref.$location_at = state.lastLocation.date;
ref.$travelingToTime = state.lastLocationDestinationTime;
userStore.currentUser.$travelingToTime =
state.lastLocationDestinationTime;
}
}
async function setCurrentUserLocation(location, travelingToLocation) {
userStore.currentUser.$location_at = Date.now();
userStore.currentUser.$travelingToTime = Date.now();
userStore.currentUser.$locationTag = location;
userStore.currentUser.$travelingToLocation = travelingToLocation;
updateCurrentUserLocation();
// janky gameLog support for Quest
if (gameStore.isGameRunning) {
// with the current state of things, lets not run this if we don't need to
return;
}
let lastLocation = '';
for (let i = gameLogStore.gameLogSessionTable.length - 1; i > -1; i--) {
const item = gameLogStore.gameLogSessionTable[i];
if (item.type === 'Location') {
lastLocation = item.location;
break;
}
}
if (lastLocation === location) {
return;
}
state.lastLocationDestination = '';
state.lastLocationDestinationTime = 0;
if (isRealInstance(location)) {
const dt = new Date().toJSON();
const L = parseLocation(location);
state.lastLocation.location = location;
state.lastLocation.date = dt;
const entry = {
created_at: dt,
type: 'Location',
location,
worldId: L.worldId,
worldName: await getWorldName(L.worldId),
groupName: await getGroupName(L.groupId),
time: 0
};
database.addGamelogLocationToDatabase(entry);
notificationStore.queueGameLogNoty(entry);
gameLogStore.addGameLog(entry);
instanceStore.addInstanceJoinHistory(location, dt);
userStore.applyUserDialogLocation();
instanceStore.applyWorldDialogInstances();
instanceStore.applyGroupDialogInstances();
} else {
state.lastLocation.location = '';
state.lastLocation.date = '';
}
}
function lastLocationReset(gameLogDate) {
let dateTime = gameLogDate;
if (!gameLogDate) {
dateTime = new Date().toJSON();
}
const dateTimeStamp = Date.parse(dateTime);
photonStore.photonLobby = new Map();
photonStore.photonLobbyCurrent = new Map();
photonStore.photonLobbyMaster = 0;
photonStore.photonLobbyCurrentUser = 0;
photonStore.photonLobbyUserData = new Map();
photonStore.photonLobbyWatcherLoopStop();
photonStore.photonLobbyAvatars = new Map();
photonStore.photonLobbyLastModeration = new Map();
photonStore.photonLobbyJointime = new Map();
photonStore.photonLobbyActivePortals = new Map();
photonStore.photonEvent7List = new Map();
photonStore.photonLastEvent7List = '';
photonStore.photonLastChatBoxMsg = new Map();
photonStore.moderationEventQueue = new Map();
if (photonStore.photonEventTable.data.length > 0) {
photonStore.photonEventTablePrevious.data =
photonStore.photonEventTable.data;
photonStore.photonEventTable.data = [];
}
const playerList = Array.from(state.lastLocation.playerList.values());
const dataBaseEntries = [];
for (const ref of playerList) {
const entry = {
created_at: dateTime,
type: 'OnPlayerLeft',
displayName: ref.displayName,
location: state.lastLocation.location,
userId: ref.userId,
time: dateTimeStamp - ref.joinTime
};
dataBaseEntries.unshift(entry);
gameLogStore.addGameLog(entry);
}
database.addGamelogJoinLeaveBulk(dataBaseEntries);
if (state.lastLocation.date !== 0) {
const update = {
time: dateTimeStamp - state.lastLocation.date,
created_at: new Date(state.lastLocation.date).toJSON()
};
database.updateGamelogLocationTimeToDatabase(update);
}
state.lastLocationDestination = '';
state.lastLocationDestinationTime = 0;
state.lastLocation = {
date: 0,
location: '',
name: '',
playerList: new Map(),
friendList: new Map()
};
updateCurrentUserLocation();
instanceStore.updateCurrentInstanceWorld();
vrStore.updateVRLastLocation();
instanceStore.getCurrentInstanceUserList();
gameLogStore.lastVideoUrl = '';
gameLogStore.lastResourceloadUrl = '';
userStore.applyUserDialogLocation();
instanceStore.applyWorldDialogInstances();
instanceStore.applyGroupDialogInstances();
}
return {
state,
lastLocation,
lastLocation$,
lastLocationDestination,
lastLocationDestinationTime,
updateCurrentUserLocation,
setCurrentUserLocation,
lastLocationReset
};
});
+265
View File
@@ -0,0 +1,265 @@
import { defineStore } from 'pinia';
import Vue, { computed, reactive, watch } from 'vue';
import { avatarModerationRequest, playerModerationRequest } from '../api';
import { $app } from '../app';
import { watchState } from '../service/watchState';
import { useAvatarStore } from './avatar';
import { useUserStore } from './user';
import { useI18n } from 'vue-i18n-bridge';
export const useModerationStore = defineStore('Moderation', () => {
const avatarStore = useAvatarStore();
const userStore = useUserStore();
const { t } = useI18n();
const state = reactive({
cachedPlayerModerations: new Map(),
cachedPlayerModerationsUserIds: new Set(),
isPlayerModerationsLoading: false,
playerModerationTable: {
data: [],
pageSize: 15
}
});
const cachedPlayerModerations = computed({
get: () => state.cachedPlayerModerations,
set: (value) => {
state.cachedPlayerModerations = value;
}
});
const cachedPlayerModerationsUserIds = computed({
get: () => state.cachedPlayerModerationsUserIds,
set: (value) => {
state.cachedPlayerModerationsUserIds = value;
}
});
const isPlayerModerationsLoading = computed({
get: () => state.isPlayerModerationsLoading,
set: (value) => {
state.isPlayerModerationsLoading = value;
}
});
const playerModerationTable = computed({
get: () => state.playerModerationTable,
set: (value) => {
state.playerModerationTable = value;
}
});
watch(
() => watchState.isLoggedIn,
(isLoggedIn) => {
state.cachedPlayerModerations.clear();
state.cachedPlayerModerationsUserIds.clear();
state.isPlayerModerationsLoading = false;
state.playerModerationTable.data = [];
if (isLoggedIn) {
refreshPlayerModerations();
}
},
{ flush: 'sync' }
);
function handlePlayerModeration(args) {
args.ref = applyPlayerModeration(args.json);
const { ref } = args;
const array = state.playerModerationTable.data;
const { length } = array;
for (let i = 0; i < length; ++i) {
if (array[i].id === ref.id) {
Vue.set(array, i, ref);
return;
}
}
state.playerModerationTable.data.push(ref);
}
/**
*
* @param args
*/
function handlePlayerModerationAtSend(args) {
const { ref } = args;
const D = userStore.userDialog;
if (
D.visible === false ||
(ref.targetUserId !== D.id &&
ref.sourceUserId !== userStore.currentUser.id)
) {
return;
}
if (ref.type === 'block') {
D.isBlock = true;
} else if (ref.type === 'mute') {
D.isMute = true;
} else if (ref.type === 'hideAvatar') {
D.isHideAvatar = true;
} else if (ref.type === 'interactOff') {
D.isInteractOff = true;
} else if (ref.type === 'muteChat') {
D.isMuteChat = true;
}
$app.$message({
message: t('message.user.moderated'),
type: 'success'
});
}
function handlePlayerModerationAtDelete(args) {
const { ref } = args;
const D = userStore.userDialog;
if (
D.visible === false ||
ref.targetUserId !== D.id ||
ref.sourceUserId !== userStore.currentUser.id
) {
return;
}
if (ref.type === 'block') {
D.isBlock = false;
} else if (ref.type === 'mute') {
D.isMute = false;
} else if (ref.type === 'hideAvatar') {
D.isHideAvatar = false;
} else if (ref.type === 'interactOff') {
D.isInteractOff = false;
} else if (ref.type === 'muteChat') {
D.isMuteChat = false;
}
const array = state.playerModerationTable.data;
const { length } = array;
for (let i = 0; i < length; ++i) {
if (array[i].id === ref.id) {
array.splice(i, 1);
return;
}
}
}
function handlePlayerModerationDelete(args) {
let { type, moderated } = args.params;
const userId = userStore.currentUser.id;
for (let ref of state.cachedPlayerModerations.values()) {
if (
ref.type === type &&
ref.targetUserId === moderated &&
ref.sourceUserId === userId
) {
state.cachedPlayerModerations.delete(ref.id);
handlePlayerModerationAtDelete({
ref,
params: {
playerModerationId: ref.id
}
});
}
}
state.cachedPlayerModerationsUserIds.delete(moderated);
}
/**
*
* @param {object} json
* @returns {object}
*/
function applyPlayerModeration(json) {
let ref = state.cachedPlayerModerations.get(json.id);
if (typeof ref === 'undefined') {
ref = {
id: '',
type: '',
sourceUserId: '',
sourceDisplayName: '',
targetUserId: '',
targetDisplayName: '',
created: '',
// VRCX
$isExpired: false,
//
...json
};
state.cachedPlayerModerations.set(ref.id, ref);
} else {
Object.assign(ref, json);
ref.$isExpired = false;
}
if (json.targetUserId) {
state.cachedPlayerModerationsUserIds.add(json.targetUserId);
}
return ref;
}
function expirePlayerModerations() {
state.cachedPlayerModerationsUserIds.clear();
for (let ref of state.cachedPlayerModerations.values()) {
ref.$isExpired = true;
}
}
function deleteExpiredPlayerModerations() {
for (let ref of state.cachedPlayerModerations.values()) {
if (!ref.$isExpired) {
continue;
}
handlePlayerModerationAtDelete({
ref,
params: {
playerModerationId: ref.id
}
});
}
}
async function refreshPlayerModerations() {
if (state.isPlayerModerationsLoading) {
return;
}
state.isPlayerModerationsLoading = true;
expirePlayerModerations();
Promise.all([
playerModerationRequest.getPlayerModerations(),
avatarModerationRequest.getAvatarModerations()
])
.finally(() => {
state.isPlayerModerationsLoading = false;
})
.then((res) => {
// TODO: compare with cachedAvatarModerations
avatarStore.cachedAvatarModerations = new Map();
if (res[1]?.json) {
for (const json of res[1].json) {
avatarStore.applyAvatarModeration(json);
}
}
if (res[0]?.json) {
for (let json of res[0].json) {
handlePlayerModeration({
json,
params: {
playerModerationId: json.id
}
});
}
}
deleteExpiredPlayerModerations();
});
}
return {
state,
cachedPlayerModerations,
cachedPlayerModerationsUserIds,
isPlayerModerationsLoading,
playerModerationTable,
refreshPlayerModerations,
handlePlayerModerationAtSend,
handlePlayerModeration,
handlePlayerModerationDelete
};
});
File diff suppressed because it is too large Load Diff
+1938
View File
File diff suppressed because it is too large Load Diff
+423
View File
@@ -0,0 +1,423 @@
import { defineStore } from 'pinia';
import { computed, reactive, watch } from 'vue';
import { instanceRequest, userRequest } from '../api';
import { groupRequest } from '../api/';
import { $app } from '../app';
import removeConfusables, { removeWhitespace } from '../service/confusables';
import { watchState } from '../service/watchState';
import { compareByName, localeIncludes } from '../shared/utils';
import { useAvatarStore } from './avatar';
import { useFriendStore } from './friend';
import { useGroupStore } from './group';
import { useAppearanceSettingsStore } from './settings/appearance';
import { useUiStore } from './ui';
import { useUserStore } from './user';
import { useWorldStore } from './world';
import { useI18n } from 'vue-i18n-bridge';
export const useSearchStore = defineStore('Search', () => {
const userStore = useUserStore();
const uiStore = useUiStore();
const appearanceSettingsStore = useAppearanceSettingsStore();
const friendStore = useFriendStore();
const worldStore = useWorldStore();
const avatarStore = useAvatarStore();
const groupStore = useGroupStore();
const { t } = useI18n();
const state = reactive({
searchText: '',
searchUserResults: [],
quickSearchItems: [],
friendsListSearch: ''
});
const searchText = computed({
get: () => state.searchText,
set: (value) => {
state.searchText = value;
}
});
const searchUserResults = computed({
get: () => state.searchUserResults,
set: (value) => {
state.searchUserResults = value;
}
});
const quickSearchItems = computed({
get: () => state.quickSearchItems,
set: (value) => {
state.quickSearchItems = value;
}
});
const stringComparer = computed(() =>
Intl.Collator(appearanceSettingsStore.appLanguage.replace('_', '-'), {
usage: 'search',
sensitivity: 'base'
})
);
const friendsListSearch = computed({
get: () => state.friendsListSearch,
set: (value) => {
state.friendsListSearch = value;
}
});
watch(
() => watchState.isLoggedIn,
() => {
state.searchText = '';
state.searchUserResults = [];
},
{ flush: 'sync' }
);
function clearSearch() {
state.searchText = '';
state.searchUserResults = [];
}
async function searchUserByDisplayName(displayName) {
const params = {
n: 10,
offset: 0,
fuzzy: false,
search: displayName
};
await moreSearchUser(null, params);
}
async function moreSearchUser(go, params) {
if (go) {
params.offset += params.n * go;
if (params.offset < 0) {
params.offset = 0;
}
}
await userRequest.getUsers(params).then((args) => {
for (const json of args.json) {
if (!json.displayName) {
console.error('getUsers gave us garbage', json);
continue;
}
userStore.applyUser(json);
}
const map = new Map();
for (const json of args.json) {
const ref = userStore.cachedUsers.get(json.id);
if (typeof ref !== 'undefined') {
map.set(ref.id, ref);
}
}
state.searchUserResults = Array.from(map.values());
return args;
});
}
function quickSearchRemoteMethod(query) {
if (!query) {
state.quickSearchItems = quickSearchUserHistory();
return;
}
const results = [];
const cleanQuery = removeWhitespace(query);
for (const ctx of friendStore.friends.values()) {
if (typeof ctx.ref === 'undefined') {
continue;
}
const cleanName = removeConfusables(ctx.name);
let match = localeIncludes(
cleanName,
cleanQuery,
stringComparer.value
);
if (!match) {
// Also check regular name in case search is with special characters
match = localeIncludes(
ctx.name,
cleanQuery,
stringComparer.value
);
}
// Use query with whitespace for notes and memos as people are more
// likely to include spaces in memos and notes
if (!match && ctx.memo) {
match = localeIncludes(ctx.memo, query, stringComparer.value);
}
if (!match && ctx.ref.note) {
match = localeIncludes(
ctx.ref.note,
query,
stringComparer.value
);
}
if (match) {
results.push({
value: ctx.id,
label: ctx.name,
ref: ctx.ref,
name: ctx.name
});
}
}
results.sort(function (a, b) {
const A =
stringComparer.value.compare(
a.name.substring(0, cleanQuery.length),
cleanQuery
) === 0;
const B =
stringComparer.value.compare(
b.name.substring(0, cleanQuery.length),
cleanQuery
) === 0;
if (A && !B) {
return -1;
} else if (B && !A) {
return 1;
}
return compareByName(a, b);
});
if (results.length > 4) {
results.length = 4;
}
results.push({
value: `search:${query}`,
label: query
});
state.quickSearchItems = results;
}
function quickSearchChange(value) {
if (value) {
if (value.startsWith('search:')) {
const searchText = value.substr(7);
if (state.quickSearchItems.length > 1 && searchText.length) {
state.friendsListSearch = searchText;
uiStore.menuActiveIndex = 'friendList';
} else {
uiStore.menuActiveIndex = 'search';
state.searchText = searchText;
userStore.lookupUser({ displayName: searchText });
}
} else {
userStore.showUserDialog(value);
}
}
}
function quickSearchUserHistory() {
const userHistory = Array.from(userStore.showUserDialogHistory.values())
.reverse()
.slice(0, 5);
const results = [];
userHistory.forEach((userId) => {
const ref = userStore.cachedUsers.get(userId);
if (typeof ref !== 'undefined') {
results.push({
value: ref.id,
label: ref.name,
ref
});
}
});
return results;
}
function directAccessPaste() {
AppApi.GetClipboard().then((clipboard) => {
if (!directAccessParse(clipboard.trim())) {
promptOmniDirectDialog();
}
});
}
function directAccessParse(input) {
if (!input) {
return false;
}
if (directAccessWorld(input)) {
return true;
}
if (input.startsWith('https://vrchat.')) {
const url = new URL(input);
const urlPath = url.pathname;
const urlPathSplit = urlPath.split('/');
if (urlPathSplit.length < 4) {
return false;
}
const type = urlPathSplit[2];
if (type === 'user') {
const userId = urlPathSplit[3];
userStore.showUserDialog(userId);
return true;
} else if (type === 'avatar') {
const avatarId = urlPathSplit[3];
avatarStore.showAvatarDialog(avatarId);
return true;
} else if (type === 'group') {
const groupId = urlPathSplit[3];
groupStore.showGroupDialog(groupId);
return true;
}
} else if (input.startsWith('https://vrc.group/')) {
const shortCode = input.substring(18);
showGroupDialogShortCode(shortCode);
return true;
} else if (/^[A-Za-z0-9]{3,6}\.[0-9]{4}$/g.test(input)) {
showGroupDialogShortCode(input);
return true;
} else if (
input.substring(0, 4) === 'usr_' ||
/^[A-Za-z0-9]{10}$/g.test(input)
) {
userStore.showUserDialog(input);
return true;
} else if (input.substring(0, 5) === 'avtr_') {
avatarStore.showAvatarDialog(input);
return true;
} else if (input.substring(0, 4) === 'grp_') {
groupStore.showGroupDialog(input);
return true;
}
return false;
}
function directAccessWorld(textBoxInput) {
let worldId;
let shortName;
let input = textBoxInput;
if (input.startsWith('/home/')) {
input = `https://vrchat.com${input}`;
}
if (input.length === 8) {
return verifyShortName('', input);
} else if (input.startsWith('https://vrch.at/')) {
shortName = input.substring(16, 24);
return verifyShortName('', shortName);
} else if (
input.startsWith('https://vrchat.') ||
input.startsWith('/home/')
) {
const url = new URL(input);
const urlPath = url.pathname;
const urlPathSplit = urlPath.split('/');
if (urlPathSplit.length >= 4 && urlPathSplit[2] === 'world') {
worldId = urlPathSplit[3];
worldStore.showWorldDialog(worldId);
return true;
} else if (urlPath.substring(5, 12) === '/launch') {
const urlParams = new URLSearchParams(url.search);
worldId = urlParams.get('worldId');
const instanceId = urlParams.get('instanceId');
if (instanceId) {
shortName = urlParams.get('shortName');
const location = `${worldId}:${instanceId}`;
if (shortName) {
return verifyShortName(location, shortName);
}
worldStore.showWorldDialog(location);
return true;
} else if (worldId) {
worldStore.showWorldDialog(worldId);
return true;
}
}
} else if (input.substring(0, 5) === 'wrld_') {
// a bit hacky, but supports weird malformed inputs cut out from url, why not
if (input.indexOf('&instanceId=') >= 0) {
input = `https://vrchat.com/home/launch?worldId=${input}`;
return directAccessWorld(input);
}
worldStore.showWorldDialog(input.trim());
return true;
}
return false;
}
function promptOmniDirectDialog() {
$app.$prompt(
t('prompt.direct_access_omni.description'),
t('prompt.direct_access_omni.header'),
{
distinguishCancelAndClose: true,
confirmButtonText: t('prompt.direct_access_omni.ok'),
cancelButtonText: t('prompt.direct_access_omni.cancel'),
inputPattern: /\S+/,
inputErrorMessage: t('prompt.direct_access_omni.input_error'),
callback: (action, instance) => {
if (action === 'confirm' && instance.inputValue) {
const input = instance.inputValue.trim();
if (!directAccessParse(input)) {
$app.$message({
message: t(
'prompt.direct_access_omni.message.error'
),
type: 'error'
});
}
}
}
}
);
}
function showGroupDialogShortCode(shortCode) {
groupRequest.groupStrictsearch({ query: shortCode }).then((args) => {
for (const group of args.json) {
if (`${group.shortCode}.${group.discriminator}` === shortCode) {
groupStore.showGroupDialog(group.id);
break;
}
}
return args;
});
}
function verifyShortName(location, shortName) {
return instanceRequest
.getInstanceFromShortName({ shortName })
.then((args) => {
const newLocation = args.json.location;
const newShortName = args.json.shortName;
if (newShortName) {
worldStore.showWorldDialog(newLocation, newShortName);
} else if (newLocation) {
worldStore.showWorldDialog(newLocation);
} else {
worldStore.showWorldDialog(location);
}
return args;
});
}
return {
state,
searchText,
searchUserResults,
stringComparer,
quickSearchItems,
friendsListSearch,
clearSearch,
searchUserByDisplayName,
moreSearchUser,
quickSearchUserHistory,
quickSearchRemoteMethod,
quickSearchChange,
directAccessParse,
directAccessPaste,
directAccessWorld,
verifyShortName
};
});
+652
View File
@@ -0,0 +1,652 @@
import { defineStore } from 'pinia';
import { computed, reactive, watch } from 'vue';
import { $app } from '../../app';
import { t } from '../../plugin';
import configRepository from '../../service/config';
import { database } from '../../service/database';
import webApiService from '../../service/webapi';
import { watchState } from '../../service/watchState';
import { useGameStore } from '../game';
import { useVrcxStore } from '../vrcx';
import { AppGlobal } from '../../service/appConfig';
export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
const gameStore = useGameStore();
const vrcxStore = useVrcxStore();
const state = reactive({
enablePrimaryPassword: false,
relaunchVRChatAfterCrash: false,
vrcQuitFix: true,
autoSweepVRChatCache: false,
saveInstancePrints: false,
cropInstancePrints: false,
saveInstanceStickers: false,
avatarRemoteDatabase: true,
enableAppLauncher: true,
enableAppLauncherAutoClose: true,
screenshotHelper: true,
screenshotHelperModifyFilename: false,
screenshotHelperCopyToClipboard: false,
youTubeApi: false,
youTubeApiKey: '',
progressPie: false,
progressPieFilter: true,
showConfirmationOnSwitchAvatar: false,
gameLogDisabled: false,
sqliteTableSizes: {},
ugcFolderPath: '',
currentUserInventory: new Map(),
autoDeleteOldPrints: false,
notificationOpacity: 100,
folderSelectorDialogVisible: false,
isVRChatConfigDialogVisible: false,
saveInstanceEmoji: false,
vrcRegistryAutoBackup: true
});
async function initAdvancedSettings() {
const [
enablePrimaryPassword,
relaunchVRChatAfterCrash,
vrcQuitFix,
autoSweepVRChatCache,
saveInstancePrints,
cropInstancePrints,
saveInstanceStickers,
avatarRemoteDatabase,
enableAppLauncher,
enableAppLauncherAutoClose,
screenshotHelper,
screenshotHelperModifyFilename,
screenshotHelperCopyToClipboard,
youTubeApi,
youTubeApiKey,
progressPie,
progressPieFilter,
showConfirmationOnSwitchAvatar,
gameLogDisabled,
ugcFolderPath,
autoDeleteOldPrints,
notificationOpacity,
saveInstanceEmoji,
vrcRegistryAutoBackup
] = await Promise.all([
configRepository.getBool('enablePrimaryPassword', false),
configRepository.getBool('VRCX_relaunchVRChatAfterCrash', false),
configRepository.getBool('VRCX_vrcQuitFix', true),
configRepository.getBool('VRCX_autoSweepVRChatCache', false),
configRepository.getBool('VRCX_saveInstancePrints', false),
configRepository.getBool('VRCX_cropInstancePrints', false),
configRepository.getBool('VRCX_saveInstanceStickers', false),
configRepository.getBool('VRCX_avatarRemoteDatabase', true),
configRepository.getBool('VRCX_enableAppLauncher', true),
configRepository.getBool('VRCX_enableAppLauncherAutoClose', true),
configRepository.getBool('VRCX_screenshotHelper', true),
configRepository.getBool(
'VRCX_screenshotHelperModifyFilename',
false
),
configRepository.getBool(
'VRCX_screenshotHelperCopyToClipboard',
false
),
configRepository.getBool('VRCX_youtubeAPI', false),
configRepository.getString('VRCX_youtubeAPIKey', ''),
configRepository.getBool('VRCX_progressPie', false),
configRepository.getBool('VRCX_progressPieFilter', true),
configRepository.getBool(
'VRCX_showConfirmationOnSwitchAvatar',
false
),
configRepository.getBool('VRCX_gameLogDisabled', false),
configRepository.getString('VRCX_userGeneratedContentPath', ''),
configRepository.getBool('VRCX_autoDeleteOldPrints', false),
configRepository.getFloat('VRCX_notificationOpacity', 100),
configRepository.getBool('VRCX_saveInstanceEmoji', false),
configRepository.getBool('VRCX_vrcRegistryAutoBackup', true)
]);
state.enablePrimaryPassword = enablePrimaryPassword;
state.relaunchVRChatAfterCrash = relaunchVRChatAfterCrash;
state.vrcQuitFix = vrcQuitFix;
state.autoSweepVRChatCache = autoSweepVRChatCache;
state.saveInstancePrints = saveInstancePrints;
state.cropInstancePrints = cropInstancePrints;
state.saveInstanceStickers = saveInstanceStickers;
state.avatarRemoteDatabase = avatarRemoteDatabase;
state.enableAppLauncher = enableAppLauncher;
state.enableAppLauncherAutoClose = enableAppLauncherAutoClose;
state.screenshotHelper = screenshotHelper;
state.screenshotHelperModifyFilename = screenshotHelperModifyFilename;
state.screenshotHelperCopyToClipboard = screenshotHelperCopyToClipboard;
state.youTubeApi = youTubeApi;
state.youTubeApiKey = youTubeApiKey;
state.progressPie = progressPie;
state.progressPieFilter = progressPieFilter;
state.showConfirmationOnSwitchAvatar = showConfirmationOnSwitchAvatar;
state.gameLogDisabled = gameLogDisabled === 'true';
state.ugcFolderPath = ugcFolderPath;
state.autoDeleteOldPrints = autoDeleteOldPrints;
state.notificationOpacity = notificationOpacity;
state.saveInstanceEmoji = saveInstanceEmoji;
state.vrcRegistryAutoBackup = vrcRegistryAutoBackup;
handleSetAppLauncherSettings();
}
initAdvancedSettings();
watch(
() => watchState.isLoggedIn,
() => {
state.currentUserInventory.clear();
state.isVRChatConfigDialogVisible = false;
},
{ flush: 'sync' }
);
const enablePrimaryPassword = computed({
get: () => state.enablePrimaryPassword,
set: (value) => (state.enablePrimaryPassword = value)
});
const relaunchVRChatAfterCrash = computed(
() => state.relaunchVRChatAfterCrash
);
const vrcQuitFix = computed(() => state.vrcQuitFix);
const autoSweepVRChatCache = computed(() => state.autoSweepVRChatCache);
const saveInstancePrints = computed(() => state.saveInstancePrints);
const cropInstancePrints = computed(() => state.cropInstancePrints);
const saveInstanceStickers = computed(() => state.saveInstanceStickers);
const avatarRemoteDatabase = computed(() => state.avatarRemoteDatabase);
const enableAppLauncher = computed(() => state.enableAppLauncher);
const enableAppLauncherAutoClose = computed(
() => state.enableAppLauncherAutoClose
);
const screenshotHelper = computed(() => state.screenshotHelper);
``;
const screenshotHelperModifyFilename = computed(
() => state.screenshotHelperModifyFilename
);
const screenshotHelperCopyToClipboard = computed(
() => state.screenshotHelperCopyToClipboard
);
const youTubeApi = computed(() => state.youTubeApi);
const youTubeApiKey = computed({
get: () => state.youTubeApiKey,
set: (value) => (state.youTubeApiKey = value)
});
const progressPie = computed(() => state.progressPie);
const progressPieFilter = computed(() => state.progressPieFilter);
const showConfirmationOnSwitchAvatar = computed(
() => state.showConfirmationOnSwitchAvatar
);
const gameLogDisabled = computed(() => state.gameLogDisabled);
const sqliteTableSizes = computed(() => state.sqliteTableSizes);
const ugcFolderPath = computed(() => state.ugcFolderPath);
const autoDeleteOldPrints = computed(() => state.autoDeleteOldPrints);
const notificationOpacity = computed(() => state.notificationOpacity);
const currentUserInventory = computed({
get: () => state.currentUserInventory,
set: (value) => {
state.currentUserInventory = value;
}
});
const isVRChatConfigDialogVisible = computed({
get: () => state.isVRChatConfigDialogVisible,
set: (value) => (state.isVRChatConfigDialogVisible = value)
});
const saveInstanceEmoji = computed({
get: () => state.saveInstanceEmoji,
set: (value) => (state.saveInstanceEmoji = value)
});
const vrcRegistryAutoBackup = computed(() => state.vrcRegistryAutoBackup);
/**
* @param {boolean} value
*/
function setEnablePrimaryPasswordConfigRepository(value) {
configRepository.setBool('enablePrimaryPassword', value);
}
function setRelaunchVRChatAfterCrash() {
state.relaunchVRChatAfterCrash = !state.relaunchVRChatAfterCrash;
configRepository.setBool(
'VRCX_relaunchVRChatAfterCrash',
state.relaunchVRChatAfterCrash
);
}
function setVrcQuitFix() {
state.vrcQuitFix = !state.vrcQuitFix;
configRepository.setBool('VRCX_vrcQuitFix', state.vrcQuitFix);
}
function setAutoSweepVRChatCache() {
state.autoSweepVRChatCache = !state.autoSweepVRChatCache;
configRepository.setBool(
'VRCX_autoSweepVRChatCache',
state.autoSweepVRChatCache
);
}
function setSaveInstancePrints() {
state.saveInstancePrints = !state.saveInstancePrints;
configRepository.setBool(
'VRCX_saveInstancePrints',
state.saveInstancePrints
);
}
function setCropInstancePrints() {
state.cropInstancePrints = !state.cropInstancePrints;
configRepository.setBool(
'VRCX_cropInstancePrints',
state.cropInstancePrints
);
}
function setSaveInstanceStickers() {
state.saveInstanceStickers = !state.saveInstanceStickers;
configRepository.setBool(
'VRCX_saveInstanceStickers',
state.saveInstanceStickers
);
}
/**
* @param {boolean} value
*/
function setAvatarRemoteDatabase(value) {
state.avatarRemoteDatabase = value;
configRepository.setBool(
'VRCX_avatarRemoteDatabase',
state.avatarRemoteDatabase
);
}
async function setEnableAppLauncher() {
state.enableAppLauncher = !state.enableAppLauncher;
await configRepository.setBool(
'VRCX_enableAppLauncher',
state.enableAppLauncher
);
handleSetAppLauncherSettings();
}
async function setEnableAppLauncherAutoClose() {
state.enableAppLauncherAutoClose = !state.enableAppLauncherAutoClose;
await configRepository.setBool(
'VRCX_enableAppLauncherAutoClose',
state.enableAppLauncherAutoClose
);
handleSetAppLauncherSettings();
}
async function setScreenshotHelper() {
state.screenshotHelper = !state.screenshotHelper;
await configRepository.setBool(
'VRCX_screenshotHelper',
state.screenshotHelper
);
}
async function setScreenshotHelperModifyFilename() {
state.screenshotHelperModifyFilename =
!state.screenshotHelperModifyFilename;
await configRepository.setBool(
'VRCX_screenshotHelperModifyFilename',
state.screenshotHelperModifyFilename
);
}
async function setScreenshotHelperCopyToClipboard() {
state.screenshotHelperCopyToClipboard =
!state.screenshotHelperCopyToClipboard;
await configRepository.setBool(
'VRCX_screenshotHelperCopyToClipboard',
state.screenshotHelperCopyToClipboard
);
}
async function setYouTubeApi() {
state.youTubeApi = !state.youTubeApi;
await configRepository.setBool('VRCX_youtubeAPI', state.youTubeApi);
}
/**
* @param {string} value
*/
async function setYouTubeApiKey(value) {
state.youTubeApiKey = value;
await configRepository.setString(
'VRCX_youtubeAPIKey',
state.youTubeApiKey
);
}
async function setProgressPie() {
state.progressPie = !state.progressPie;
await configRepository.setBool('VRCX_progressPie', state.progressPie);
}
async function setProgressPieFilter() {
state.progressPieFilter = !state.progressPieFilter;
await configRepository.setBool(
'VRCX_progressPieFilter',
state.progressPieFilter
);
}
async function setShowConfirmationOnSwitchAvatar() {
state.showConfirmationOnSwitchAvatar =
!state.showConfirmationOnSwitchAvatar;
await configRepository.setBool(
'VRCX_showConfirmationOnSwitchAvatar',
state.showConfirmationOnSwitchAvatar
);
}
async function setGameLogDisabled() {
state.gameLogDisabled = !state.gameLogDisabled;
await configRepository.setBool(
'VRCX_gameLogDisabled',
state.gameLogDisabled
);
}
async function setSaveInstanceEmoji() {
state.saveInstanceEmoji = !state.saveInstanceEmoji;
await configRepository.setBool(
'VRCX_saveInstanceEmoji',
state.saveInstanceEmoji
);
}
async function setUGCFolderPath(path) {
if (typeof path !== 'string') {
path = '';
}
state.ugcFolderPath = path;
await configRepository.setString('VRCX_userGeneratedContentPath', path);
}
async function setAutoDeleteOldPrints() {
state.autoDeleteOldPrints = !state.autoDeleteOldPrints;
await configRepository.setBool(
'VRCX_autoDeleteOldPrints',
state.autoDeleteOldPrints
);
}
async function setNotificationOpacity(value) {
state.notificationOpacity = value;
await configRepository.setInt('VRCX_notificationOpacity', value);
}
async function setVrcRegistryAutoBackup() {
state.vrcRegistryAutoBackup = !state.vrcRegistryAutoBackup;
await configRepository.setBool(
'VRCX_vrcRegistryAutoBackup',
state.vrcRegistryAutoBackup
);
}
async function getSqliteTableSizes() {
const [
gps,
status,
bio,
avatar,
onlineOffline,
friendLogHistory,
notification,
location,
joinLeave,
portalSpawn,
videoPlay,
event,
external
] = await Promise.all([
database.getGpsTableSize(),
database.getStatusTableSize(),
database.getBioTableSize(),
database.getAvatarTableSize(),
database.getOnlineOfflineTableSize(),
database.getFriendLogHistoryTableSize(),
database.getNotificationTableSize(),
database.getLocationTableSize(),
database.getJoinLeaveTableSize(),
database.getPortalSpawnTableSize(),
database.getVideoPlayTableSize(),
database.getEventTableSize(),
database.getExternalTableSize()
]);
state.sqliteTableSizes = {
gps,
status,
bio,
avatar,
onlineOffline,
friendLogHistory,
notification,
location,
joinLeave,
portalSpawn,
videoPlay,
event,
external
};
}
function handleSetAppLauncherSettings() {
AppApi.SetAppLauncherSettings(
state.enableAppLauncher,
state.enableAppLauncherAutoClose
);
}
/**
* @param {string} videoId
*/
async function lookupYouTubeVideo(videoId) {
if (!state.youTubeApi) {
console.warn('no Youtube API key configured');
return null;
}
let data = null;
let apiKey = '';
if (state.youTubeApiKey) {
apiKey = state.youTubeApiKey;
}
try {
const response = await webApiService.execute({
url: `https://www.googleapis.com/youtube/v3/videos?id=${encodeURIComponent(
videoId
)}&part=snippet,contentDetails&key=${apiKey}`,
method: 'GET',
headers: {
Referer: 'https://vrcx.app'
}
});
const json = JSON.parse(response.data);
if (AppGlobal.debugWebRequests) {
console.log(json, response);
}
if (response.status === 200) {
data = json;
} else {
throw new Error(`Error: ${response.data}`);
}
} catch {
console.error(`YouTube video lookup failed for ${videoId}`);
}
return data;
}
function cropPrintsChanged() {
if (!state.cropInstancePrints) return;
$app.$confirm(
t(
'view.settings.advanced.advanced.save_instance_prints_to_file.crop_convert_old'
),
{
confirmButtonText: t(
'view.settings.advanced.advanced.save_instance_prints_to_file.crop_convert_old_confirm'
),
cancelButtonText: t(
'view.settings.advanced.advanced.save_instance_prints_to_file.crop_convert_old_cancel'
),
type: 'info',
showInput: false,
callback: async (action) => {
if (action === 'confirm') {
const msgBox = $app.$message({
message: 'Batch print cropping in progress...',
type: 'warning',
duration: 0
});
try {
await AppApi.CropAllPrints(state.ugcFolderPath);
$app.$message({
message: 'Batch print cropping complete',
type: 'success'
});
} catch (err) {
console.error(err);
$app.$message({
message: `Batch print cropping failed: ${err}`,
type: 'error'
});
} finally {
msgBox.close();
}
}
}
}
);
}
function resetUGCFolder() {
setUGCFolderPath('');
}
async function openUGCFolder() {
if (LINUX && state.ugcFolderPath == null) {
resetUGCFolder();
}
await AppApi.OpenUGCPhotosFolder(state.ugcFolderPath);
}
async function folderSelectorDialog(oldPath) {
if (state.folderSelectorDialogVisible) return;
if (!oldPath) {
oldPath = '';
}
state.folderSelectorDialogVisible = true;
let newFolder = '';
if (LINUX) {
newFolder = await window.electron.openDirectoryDialog();
} else {
newFolder = await AppApi.OpenFolderSelectorDialog(oldPath);
}
state.folderSelectorDialogVisible = false;
return newFolder;
}
async function openUGCFolderSelector() {
const path = await folderSelectorDialog(state.ugcFolderPath);
await setUGCFolderPath(path);
}
async function showVRChatConfig() {
state.isVRChatConfigDialogVisible = true;
if (!gameStore.VRChatUsedCacheSize) {
gameStore.getVRChatCacheSize();
}
}
function promptAutoClearVRCXCacheFrequency() {
$app.$prompt(
t('prompt.auto_clear_cache.description'),
t('prompt.auto_clear_cache.header'),
{
distinguishCancelAndClose: true,
confirmButtonText: t('prompt.auto_clear_cache.ok'),
cancelButtonText: t('prompt.auto_clear_cache.cancel'),
inputValue: vrcxStore.clearVRCXCacheFrequency / 3600 / 2,
inputPattern: /\d+$/,
inputErrorMessage: t('prompt.auto_clear_cache.input_error'),
callback: async (action, instance) => {
if (
action === 'confirm' &&
instance.inputValue &&
!isNaN(instance.inputValue)
) {
vrcxStore.clearVRCXCacheFrequency = Math.trunc(
Number(instance.inputValue) * 3600 * 2
);
await configRepository.setString(
'VRCX_clearVRCXCacheFrequency',
vrcxStore.clearVRCXCacheFrequency
);
}
}
}
);
}
return {
state,
enablePrimaryPassword,
relaunchVRChatAfterCrash,
vrcQuitFix,
autoSweepVRChatCache,
saveInstancePrints,
cropInstancePrints,
saveInstanceStickers,
avatarRemoteDatabase,
enableAppLauncher,
enableAppLauncherAutoClose,
screenshotHelper,
screenshotHelperModifyFilename,
screenshotHelperCopyToClipboard,
youTubeApi,
youTubeApiKey,
progressPie,
progressPieFilter,
showConfirmationOnSwitchAvatar,
gameLogDisabled,
sqliteTableSizes,
ugcFolderPath,
currentUserInventory,
autoDeleteOldPrints,
notificationOpacity,
isVRChatConfigDialogVisible,
saveInstanceEmoji,
vrcRegistryAutoBackup,
setEnablePrimaryPasswordConfigRepository,
setRelaunchVRChatAfterCrash,
setVrcQuitFix,
setAutoSweepVRChatCache,
setSaveInstancePrints,
setCropInstancePrints,
setSaveInstanceStickers,
setAvatarRemoteDatabase,
setEnableAppLauncher,
setEnableAppLauncherAutoClose,
setScreenshotHelper,
setScreenshotHelperModifyFilename,
setScreenshotHelperCopyToClipboard,
setYouTubeApi,
setYouTubeApiKey,
setProgressPie,
setProgressPieFilter,
setShowConfirmationOnSwitchAvatar,
setGameLogDisabled,
setUGCFolderPath,
cropPrintsChanged,
setAutoDeleteOldPrints,
setNotificationOpacity,
getSqliteTableSizes,
handleSetAppLauncherSettings,
lookupYouTubeVideo,
resetUGCFolder,
openUGCFolder,
openUGCFolderSelector,
folderSelectorDialog,
showVRChatConfig,
promptAutoClearVRCXCacheFrequency,
setSaveInstanceEmoji,
setVrcRegistryAutoBackup
};
});
+780
View File
@@ -0,0 +1,780 @@
import { defineStore } from 'pinia';
import { computed, reactive, watch } from 'vue';
import { $app } from '../../app';
import { i18n, t } from '../../plugin';
import configRepository from '../../service/config';
import { database } from '../../service/database';
import { watchState } from '../../service/watchState';
import {
changeAppDarkStyle,
changeAppThemeStyle,
changeCJKFontsOrder,
getNameColour,
HueToHex,
systemIsDarkMode,
updateTrustColorClasses
} from '../../shared/utils';
import { useFeedStore } from '../feed';
import { useFriendStore } from '../friend';
import { useGameLogStore } from '../gameLog';
import { useModerationStore } from '../moderation';
import { useNotificationStore } from '../notification';
import { useUserStore } from '../user';
import { useVrStore } from '../vr';
import { useVrcxStore } from '../vrcx';
export const useAppearanceSettingsStore = defineStore(
'AppearanceSettings',
() => {
const friendStore = useFriendStore();
const vrStore = useVrStore();
const notificationStore = useNotificationStore();
const feedStore = useFeedStore();
const moderationStore = useModerationStore();
const gameLogStore = useGameLogStore();
const vrcxStore = useVrcxStore();
const userStore = useUserStore();
const state = reactive({
appLanguage: 'en',
themeMode: '',
isDarkMode: false,
displayVRCPlusIconsAsAvatar: false,
hideNicknames: false,
hideTooltips: false,
isAgeGatedInstancesVisible: false,
sortFavorites: true,
instanceUsersSortAlphabetical: false,
tablePageSize: 15,
dtHour12: false,
dtIsoFormat: false,
sidebarSortMethod1: 'Sort Private to Bottom',
sidebarSortMethod2: 'Sort by Time in Instance',
sidebarSortMethod3: 'Sort by Last Active',
sidebarSortMethods: [
'Sort Private to Bottom',
'Sort by Time in Instance',
'Sort by Last Active'
],
asideWidth: 300,
isSidebarGroupByInstance: true,
isHideFriendsInSameInstance: false,
isSidebarDivideByFriendGroup: false,
hideUserNotes: false,
hideUserMemos: false,
hideUnfriends: false,
randomUserColours: false,
trustColor: {
untrusted: '#CCCCCC',
basic: '#1778FF',
known: '#2BCF5C',
trusted: '#FF7B42',
veteran: '#B18FFF',
vip: '#FF2626',
troll: '#782F2F'
},
currentCulture: ''
});
async function initAppearanceSettings() {
const [
appLanguage,
themeMode,
displayVRCPlusIconsAsAvatar,
hideNicknames,
hideTooltips,
isAgeGatedInstancesVisible,
sortFavorites,
instanceUsersSortAlphabetical,
tablePageSize,
dtHour12,
dtIsoFormat,
sidebarSortMethods,
asideWidth,
isSidebarGroupByInstance,
isHideFriendsInSameInstance,
isSidebarDivideByFriendGroup,
hideUserNotes,
hideUserMemos,
hideUnfriends,
randomUserColours,
trustColor
] = await Promise.all([
configRepository.getString('VRCX_appLanguage'),
configRepository.getString('VRCX_ThemeMode', 'system'),
configRepository.getBool('displayVRCPlusIconsAsAvatar', true),
configRepository.getBool('VRCX_hideNicknames', false),
configRepository.getBool('VRCX_hideTooltips', false),
configRepository.getBool(
'VRCX_isAgeGatedInstancesVisible',
true
),
configRepository.getBool('VRCX_sortFavorites', true),
configRepository.getBool(
'VRCX_instanceUsersSortAlphabetical',
false
),
configRepository.getInt('VRCX_tablePageSize', 15),
configRepository.getBool('VRCX_dtHour12', false),
configRepository.getBool('VRCX_dtIsoFormat', false),
configRepository.getString(
'VRCX_sidebarSortMethods',
JSON.stringify([
'Sort Private to Bottom',
'Sort by Time in Instance',
'Sort by Last Active'
])
),
configRepository.getInt('VRCX_sidePanelWidth', 300),
configRepository.getBool('VRCX_sidebarGroupByInstance', true),
configRepository.getBool(
'VRCX_hideFriendsInSameInstance',
false
),
configRepository.getBool(
'VRCX_sidebarDivideByFriendGroup',
true
),
configRepository.getBool('VRCX_hideUserNotes', false),
configRepository.getBool('VRCX_hideUserMemos', false),
configRepository.getBool('VRCX_hideUnfriends', false),
configRepository.getBool('VRCX_randomUserColours', false),
configRepository.getString(
'VRCX_trustColor',
JSON.stringify({
untrusted: '#CCCCCC',
basic: '#1778FF',
known: '#2BCF5C',
trusted: '#FF7B42',
veteran: '#B18FFF',
vip: '#FF2626',
troll: '#782F2F'
})
)
]);
if (!appLanguage) {
const result = await AppApi.CurrentLanguage();
const lang = result.split('-')[0];
i18n.availableLocales.forEach((ref) => {
const refLang = ref.split('_')[0];
if (refLang === lang) {
changeAppLanguage(ref);
}
});
} else {
state.appLanguage = appLanguage;
}
changeCJKFontsOrder(state.appLanguage);
state.themeMode = themeMode;
applyThemeMode(themeMode);
state.displayVRCPlusIconsAsAvatar = displayVRCPlusIconsAsAvatar;
state.hideNicknames = hideNicknames;
state.hideTooltips = hideTooltips;
state.isAgeGatedInstancesVisible = isAgeGatedInstancesVisible;
state.sortFavorites = sortFavorites;
state.instanceUsersSortAlphabetical = instanceUsersSortAlphabetical;
setTablePageSize(tablePageSize);
handleSetTablePageSize(state.tablePageSize);
state.dtHour12 = dtHour12;
state.dtIsoFormat = dtIsoFormat;
state.currentCulture = await AppApi.CurrentCulture();
state.sidebarSortMethods = JSON.parse(sidebarSortMethods);
if (state.sidebarSortMethods?.length === 3) {
state.sidebarSortMethod1 = state.sidebarSortMethods[0];
state.sidebarSortMethod2 = state.sidebarSortMethods[1];
state.sidebarSortMethod3 = state.sidebarSortMethods[2];
}
state.trustColor = JSON.parse(trustColor);
state.asideWidth = asideWidth;
state.isSidebarGroupByInstance = isSidebarGroupByInstance;
state.isHideFriendsInSameInstance = isHideFriendsInSameInstance;
state.isSidebarDivideByFriendGroup = isSidebarDivideByFriendGroup;
state.hideUserNotes = hideUserNotes;
state.hideUserMemos = hideUserMemos;
state.hideUnfriends = hideUnfriends;
state.randomUserColours = randomUserColours;
// Migrate old settings
// Assume all exist if one does
await mergeOldSortMethodsSettings();
updateTrustColorClasses(state.trustColor);
vrStore.updateVRConfigVars();
}
initAppearanceSettings();
const appLanguage = computed(() => state.appLanguage);
const themeMode = computed(() => state.themeMode);
const isDarkMode = computed(() => state.isDarkMode);
const displayVRCPlusIconsAsAvatar = computed(
() => state.displayVRCPlusIconsAsAvatar
);
const hideNicknames = computed(() => state.hideNicknames);
const hideTooltips = computed(() => state.hideTooltips);
const isAgeGatedInstancesVisible = computed(
() => state.isAgeGatedInstancesVisible
);
const sortFavorites = computed(() => state.sortFavorites);
const instanceUsersSortAlphabetical = computed(
() => state.instanceUsersSortAlphabetical
);
const tablePageSize = computed(() => state.tablePageSize);
const dtHour12 = computed(() => state.dtHour12);
const dtIsoFormat = computed(() => state.dtIsoFormat);
const sidebarSortMethod1 = computed(() => state.sidebarSortMethod1);
const sidebarSortMethod2 = computed(() => state.sidebarSortMethod2);
const sidebarSortMethod3 = computed(() => state.sidebarSortMethod3);
const sidebarSortMethods = computed(() => state.sidebarSortMethods);
const asideWidth = computed(() => state.asideWidth);
const isSidebarGroupByInstance = computed(
() => state.isSidebarGroupByInstance
);
const isHideFriendsInSameInstance = computed(
() => state.isHideFriendsInSameInstance
);
const isSidebarDivideByFriendGroup = computed(
() => state.isSidebarDivideByFriendGroup
);
const hideUserNotes = computed(() => state.hideUserNotes);
const hideUserMemos = computed(() => state.hideUserMemos);
const hideUnfriends = computed(() => state.hideUnfriends);
const randomUserColours = computed(() => state.randomUserColours);
const trustColor = computed(() => state.trustColor);
const currentCulture = computed(() => state.currentCulture);
watch(
() => watchState.isFriendsLoaded,
(isFriendsLoaded) => {
if (isFriendsLoaded) {
tryInitUserColours();
}
},
{ flush: 'sync' }
);
/**
*
* @param {string} language
*/
function changeAppLanguage(language) {
setAppLanguage(language);
vrStore.updateVRConfigVars();
}
/**
* @param {string} language
*/
function setAppLanguage(language) {
console.log('Language changed:', language);
state.appLanguage = language;
configRepository.setString('VRCX_appLanguage', language);
changeCJKFontsOrder(state.appLanguage);
i18n.locale = state.appLanguage;
}
/**
* @param {string} newThemeMode
* @returns {Promise<void>}
*/
async function saveThemeMode(newThemeMode) {
setThemeMode(newThemeMode);
await changeThemeMode();
}
async function changeThemeMode() {
await changeAppThemeStyle(state.themeMode);
vrStore.updateVRConfigVars();
await updateTrustColor();
}
/**
*
* @param {string} field
* @param {string} color
* @param {boolean} setRandomColor
* @returns {Promise<void>}
*/
async function updateTrustColor(field, color, setRandomColor = false) {
if (setRandomColor) {
setRandomUserColours();
}
if (typeof userStore.currentUser?.id === 'undefined') {
return;
}
if (field && color) {
setTrustColor({
...state.trustColor,
[field]: color
});
}
if (state.randomUserColours) {
const colour = await getNameColour(userStore.currentUser.id);
userStore.currentUser.$userColour = colour;
userColourInit();
} else {
applyUserTrustLevel(userStore.currentUser);
userStore.cachedUsers.forEach((ref) => {
applyUserTrustLevel(ref);
});
}
updateTrustColorClasses(state.trustColor);
}
async function userColourInit() {
let dictObject = await AppApi.GetColourBulk(
Array.from(userStore.cachedUsers.keys())
);
if (LINUX) {
dictObject = Object.fromEntries(dictObject);
}
for (const [userId, hue] of Object.entries(dictObject)) {
const ref = userStore.cachedUsers.get(userId);
if (typeof ref !== 'undefined') {
ref.$userColour = HueToHex(hue);
}
}
}
/**
*
* @param {object} ref
*/
function applyUserTrustLevel(ref) {
ref.$isModerator =
ref.developerType && ref.developerType !== 'none';
ref.$isTroll = false;
ref.$isProbableTroll = false;
let trustColor = '';
const { tags } = ref;
if (tags.includes('admin_moderator')) {
ref.$isModerator = true;
}
if (tags.includes('system_troll')) {
ref.$isTroll = true;
}
if (tags.includes('system_probable_troll') && !ref.$isTroll) {
ref.$isProbableTroll = true;
}
if (tags.includes('system_trust_veteran')) {
ref.$trustLevel = 'Trusted User';
ref.$trustClass = 'x-tag-veteran';
trustColor = 'veteran';
ref.$trustSortNum = 5;
} else if (tags.includes('system_trust_trusted')) {
ref.$trustLevel = 'Known User';
ref.$trustClass = 'x-tag-trusted';
trustColor = 'trusted';
ref.$trustSortNum = 4;
} else if (tags.includes('system_trust_known')) {
ref.$trustLevel = 'User';
ref.$trustClass = 'x-tag-known';
trustColor = 'known';
ref.$trustSortNum = 3;
} else if (tags.includes('system_trust_basic')) {
ref.$trustLevel = 'New User';
ref.$trustClass = 'x-tag-basic';
trustColor = 'basic';
ref.$trustSortNum = 2;
} else {
ref.$trustLevel = 'Visitor';
ref.$trustClass = 'x-tag-untrusted';
trustColor = 'untrusted';
ref.$trustSortNum = 1;
}
if (ref.$isTroll || ref.$isProbableTroll) {
trustColor = 'troll';
ref.$trustSortNum += 0.1;
}
if (ref.$isModerator) {
trustColor = 'vip';
ref.$trustSortNum += 0.3;
}
if (state.randomUserColours && watchState.isFriendsLoaded) {
if (!ref.$userColour) {
getNameColour(ref.id).then((colour) => {
ref.$userColour = colour;
});
}
} else {
ref.$userColour = state.trustColor[trustColor];
}
}
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', async () => {
if (state.themeMode === 'system') {
await changeThemeMode();
}
});
/**
* @param {string} mode
*/
function setThemeMode(mode) {
state.themeMode = mode;
configRepository.setString('VRCX_ThemeMode', mode);
applyThemeMode();
}
function applyThemeMode() {
if (state.themeMode === 'light') {
setIsDarkMode(false);
} else if (state.themeMode === 'system') {
setIsDarkMode(systemIsDarkMode());
} else {
setIsDarkMode(true);
}
}
/**
* @param {boolean} isDark
*/
function setIsDarkMode(isDark) {
state.isDarkMode = isDark;
changeAppDarkStyle(isDark);
}
function setDisplayVRCPlusIconsAsAvatar() {
state.displayVRCPlusIconsAsAvatar =
!state.displayVRCPlusIconsAsAvatar;
configRepository.setBool(
'displayVRCPlusIconsAsAvatar',
state.displayVRCPlusIconsAsAvatar
);
}
function setHideNicknames() {
state.hideNicknames = !state.hideNicknames;
configRepository.setBool('VRCX_hideNicknames', state.hideNicknames);
}
function setHideTooltips() {
state.hideTooltips = !state.hideTooltips;
configRepository.setBool('VRCX_hideTooltips', state.hideTooltips);
}
function setIsAgeGatedInstancesVisible() {
state.isAgeGatedInstancesVisible =
!state.isAgeGatedInstancesVisible;
configRepository.setBool(
'VRCX_isAgeGatedInstancesVisible',
state.isAgeGatedInstancesVisible
);
}
function setSortFavorites() {
state.sortFavorites = !state.sortFavorites;
configRepository.setBool('VRCX_sortFavorites', state.sortFavorites);
}
function setInstanceUsersSortAlphabetical() {
state.instanceUsersSortAlphabetical =
!state.instanceUsersSortAlphabetical;
configRepository.setBool(
'VRCX_instanceUsersSortAlphabetical',
state.instanceUsersSortAlphabetical
);
}
/**
* @param {number} size
*/
function setTablePageSize(size) {
state.tablePageSize = size;
configRepository.setInt('VRCX_tablePageSize', size);
}
function setDtHour12() {
state.dtHour12 = !state.dtHour12;
configRepository.setBool('VRCX_dtHour12', state.dtHour12);
}
function setDtIsoFormat() {
state.dtIsoFormat = !state.dtIsoFormat;
configRepository.setBool('VRCX_dtIsoFormat', state.dtIsoFormat);
}
/**
* @param {string} method
*/
function setSidebarSortMethod1(method) {
state.sidebarSortMethod1 = method;
handleSaveSidebarSortOrder();
}
/**
* @param {string} method
*/
function setSidebarSortMethod2(method) {
state.sidebarSortMethod2 = method;
handleSaveSidebarSortOrder();
}
/**
* @param {string} method
*/
function setSidebarSortMethod3(method) {
state.sidebarSortMethod3 = method;
handleSaveSidebarSortOrder();
}
/**
* @param {Array<string>} methods
*/
function setSidebarSortMethods(methods) {
state.sidebarSortMethods = methods;
configRepository.setString(
'VRCX_sidebarSortMethods',
JSON.stringify(methods)
);
}
/**
* @param {number} width
*/
function setAsideWidth(width) {
requestAnimationFrame(() => {
state.asideWidth = width;
configRepository.setInt('VRCX_sidePanelWidth', width);
});
}
function setIsSidebarGroupByInstance() {
state.isSidebarGroupByInstance = !state.isSidebarGroupByInstance;
configRepository.setBool(
'VRCX_sidebarGroupByInstance',
state.isSidebarGroupByInstance
);
}
function setIsHideFriendsInSameInstance() {
state.isHideFriendsInSameInstance =
!state.isHideFriendsInSameInstance;
configRepository.setBool(
'VRCX_hideFriendsInSameInstance',
state.isHideFriendsInSameInstance
);
}
function setIsSidebarDivideByFriendGroup() {
state.isSidebarDivideByFriendGroup =
!state.isSidebarDivideByFriendGroup;
configRepository.setBool(
'VRCX_sidebarDivideByFriendGroup',
state.isSidebarDivideByFriendGroup
);
}
function setHideUserNotes() {
state.hideUserNotes = !state.hideUserNotes;
configRepository.setBool('VRCX_hideUserNotes', state.hideUserNotes);
}
function setHideUserMemos() {
state.hideUserMemos = !state.hideUserMemos;
configRepository.setBool('VRCX_hideUserMemos', state.hideUserMemos);
}
function setHideUnfriends() {
state.hideUnfriends = !state.hideUnfriends;
configRepository.setBool('VRCX_hideUnfriends', state.hideUnfriends);
}
function setRandomUserColours() {
state.randomUserColours = !state.randomUserColours;
configRepository.setBool(
'VRCX_randomUserColours',
state.randomUserColours
);
}
/**
* @param {object} color
*/
function setTrustColor(color) {
state.trustColor = color;
configRepository.setString(
'VRCX_trustColor',
JSON.stringify(color)
);
}
function handleSaveSidebarSortOrder() {
if (state.sidebarSortMethod1 === state.sidebarSortMethod2) {
state.sidebarSortMethod2 = '';
}
if (state.sidebarSortMethod1 === state.sidebarSortMethod3) {
state.sidebarSortMethod3 = '';
}
if (state.sidebarSortMethod2 === state.sidebarSortMethod3) {
state.sidebarSortMethod3 = '';
}
if (!state.sidebarSortMethod1) {
state.sidebarSortMethod2 = '';
}
if (!state.sidebarSortMethod2) {
state.sidebarSortMethod3 = '';
}
const sidebarSortMethods = [
state.sidebarSortMethod1,
state.sidebarSortMethod2,
state.sidebarSortMethod3
];
setSidebarSortMethods(sidebarSortMethods);
}
async function mergeOldSortMethodsSettings() {
const orderFriendsGroupPrivate = await configRepository.getBool(
'orderFriendGroupPrivate'
);
if (orderFriendsGroupPrivate !== null) {
await configRepository.remove('orderFriendGroupPrivate');
const orderFriendsGroupStatus = await configRepository.getBool(
'orderFriendsGroupStatus'
);
await configRepository.remove('orderFriendsGroupStatus');
const orderFriendsGroupGPS = await configRepository.getBool(
'orderFriendGroupGPS'
);
await configRepository.remove('orderFriendGroupGPS');
const orderOnlineFor =
await configRepository.getBool('orderFriendGroup0');
await configRepository.remove('orderFriendGroup0');
await configRepository.remove('orderFriendGroup1');
await configRepository.remove('orderFriendGroup2');
await configRepository.remove('orderFriendGroup3');
const sortOrder = [];
if (orderFriendsGroupPrivate) {
sortOrder.push('Sort Private to Bottom');
}
if (orderFriendsGroupStatus) {
sortOrder.push('Sort by Status');
}
if (orderOnlineFor && orderFriendsGroupGPS) {
sortOrder.push('Sort by Time in Instance');
}
if (!orderOnlineFor) {
sortOrder.push('Sort Alphabetically');
}
if (sortOrder.length > 0) {
while (sortOrder.length < 3) {
sortOrder.push('');
}
state.sidebarSortMethods = sortOrder;
state.sidebarSortMethod1 = sortOrder[0];
state.sidebarSortMethod2 = sortOrder[1];
state.sidebarSortMethod3 = sortOrder[2];
}
setSidebarSortMethods(sortOrder);
}
}
async function handleSetTablePageSize(pageSize) {
feedStore.feedTable.pageSize = pageSize;
gameLogStore.gameLogTable.pageSize = pageSize;
friendStore.friendLogTable.pageSize = pageSize;
moderationStore.playerModerationTable.pageSize = pageSize;
notificationStore.notificationTable.pageSize = pageSize;
setTablePageSize(pageSize);
}
function promptMaxTableSizeDialog() {
$app.$prompt(
t('prompt.change_table_size.description'),
t('prompt.change_table_size.header'),
{
distinguishCancelAndClose: true,
confirmButtonText: t('prompt.change_table_size.save'),
cancelButtonText: t('prompt.change_table_size.cancel'),
inputValue: vrcxStore.maxTableSize,
inputPattern: /\d+$/,
inputErrorMessage: t(
'prompt.change_table_size.input_error'
),
callback: async (action, instance) => {
if (action === 'confirm' && instance.inputValue) {
if (instance.inputValue > 10000) {
instance.inputValue = 10000;
}
vrcxStore.maxTableSize = instance.inputValue;
await configRepository.setString(
'VRCX_maxTableSize',
vrcxStore.maxTableSize
);
database.setMaxTableSize(vrcxStore.maxTableSize);
feedStore.feedTableLookup();
gameLogStore.gameLogTableLookup();
}
}
}
);
}
async function tryInitUserColours() {
if (!state.randomUserColours) {
return;
}
const colour = await getNameColour(userStore.currentUser.id);
userStore.currentUser.$userColour = colour;
await userColourInit();
}
return {
state,
appLanguage,
themeMode,
isDarkMode,
displayVRCPlusIconsAsAvatar,
hideNicknames,
hideTooltips,
isAgeGatedInstancesVisible,
sortFavorites,
instanceUsersSortAlphabetical,
tablePageSize,
dtHour12,
dtIsoFormat,
sidebarSortMethod1,
sidebarSortMethod2,
sidebarSortMethod3,
sidebarSortMethods,
asideWidth,
isSidebarGroupByInstance,
isHideFriendsInSameInstance,
isSidebarDivideByFriendGroup,
hideUserNotes,
hideUserMemos,
hideUnfriends,
randomUserColours,
trustColor,
currentCulture,
setAppLanguage,
setDisplayVRCPlusIconsAsAvatar,
setHideNicknames,
setHideTooltips,
setIsAgeGatedInstancesVisible,
setSortFavorites,
setInstanceUsersSortAlphabetical,
setTablePageSize,
setDtHour12,
setDtIsoFormat,
setSidebarSortMethod1,
setSidebarSortMethod2,
setSidebarSortMethod3,
setSidebarSortMethods,
setAsideWidth,
setIsSidebarGroupByInstance,
setIsHideFriendsInSameInstance,
setIsSidebarDivideByFriendGroup,
setHideUserNotes,
setHideUserMemos,
setHideUnfriends,
setRandomUserColours,
setTrustColor,
saveThemeMode,
tryInitUserColours,
updateTrustColor,
changeThemeMode,
userColourInit,
applyUserTrustLevel,
changeAppLanguage,
handleSetTablePageSize,
promptMaxTableSizeDialog
};
}
);
+358
View File
@@ -0,0 +1,358 @@
import { defineStore } from 'pinia';
import { computed, reactive } from 'vue';
import { userRequest, worldRequest } from '../../api';
import configRepository from '../../service/config';
import {
getGroupName,
getLaunchURL,
isRealInstance,
isRpcWorld,
parseLocation
} from '../../shared/utils';
import { useGameStore } from '../game';
import { useGameLogStore } from '../gameLog';
import { useLocationStore } from '../location';
import { useUpdateLoopStore } from '../updateLoop';
import { useUserStore } from '../user';
import { useWorldStore } from '../world';
import { useAdvancedSettingsStore } from './advanced';
import { ActivityType } from '../../shared/constants/discord';
export const useDiscordPresenceSettingsStore = defineStore(
'DiscordPresenceSettings',
() => {
const locationStore = useLocationStore();
const gameStore = useGameStore();
const advancedSettingsStore = useAdvancedSettingsStore();
const worldStore = useWorldStore();
const gameLogStore = useGameLogStore();
const userStore = useUserStore();
const updateLoopStore = useUpdateLoopStore();
const state = reactive({
discordActive: false,
discordInstance: true,
discordHideInvite: true,
discordJoinButton: false,
discordHideImage: false,
isDiscordActive: false
});
async function initDiscordPresenceSettings() {
const [
discordActive,
discordInstance,
discordHideInvite,
discordJoinButton,
discordHideImage
] = await Promise.all([
configRepository.getBool('discordActive', false),
configRepository.getBool('discordInstance', true),
configRepository.getBool('discordHideInvite', true),
configRepository.getBool('discordJoinButton', false),
configRepository.getBool('discordHideImage', false)
]);
state.discordActive = discordActive;
state.discordInstance = discordInstance;
state.discordHideInvite = discordHideInvite;
state.discordJoinButton = discordJoinButton;
state.discordHideImage = discordHideImage;
}
const discordActive = computed(() => state.discordActive);
const discordInstance = computed(() => state.discordInstance);
const discordHideInvite = computed(() => state.discordHideInvite);
const discordJoinButton = computed(() => state.discordJoinButton);
const discordHideImage = computed(() => state.discordHideImage);
function setDiscordActive() {
state.discordActive = !state.discordActive;
configRepository.setBool('discordActive', state.discordActive);
}
function setDiscordInstance() {
state.discordInstance = !state.discordInstance;
configRepository.setBool('discordInstance', state.discordInstance);
}
function setDiscordHideInvite() {
state.discordHideInvite = !state.discordHideInvite;
configRepository.setBool(
'discordHideInvite',
state.discordHideInvite
);
}
function setDiscordJoinButton() {
state.discordJoinButton = !state.discordJoinButton;
configRepository.setBool(
'discordJoinButton',
state.discordJoinButton
);
}
function setDiscordHideImage() {
state.discordHideImage = !state.discordHideImage;
configRepository.setBool(
'discordHideImage',
state.discordHideImage
);
}
initDiscordPresenceSettings();
function updateDiscord() {
let platform;
let currentLocation = locationStore.lastLocation.location;
let timeStamp = locationStore.lastLocation.date;
if (locationStore.lastLocation.location === 'traveling') {
currentLocation = locationStore.lastLocationDestination;
timeStamp = locationStore.lastLocationDestinationTime;
}
if (
!state.discordActive ||
(!gameStore.isGameRunning &&
!advancedSettingsStore.gameLogDisabled) ||
(!currentLocation && !locationStore.lastLocation$.tag)
) {
setIsDiscordActive(false);
return;
}
setIsDiscordActive(true);
let L = locationStore.lastLocation$;
if (currentLocation !== locationStore.lastLocation$.tag) {
Discord.SetTimestamps(timeStamp, 0);
L = parseLocation(currentLocation);
L.worldName = '';
L.thumbnailImageUrl = '';
L.worldCapacity = 0;
L.joinUrl = '';
L.accessName = '';
if (L.worldId) {
const ref = worldStore.cachedWorlds.get(L.worldId);
if (ref) {
L.worldName = ref.name;
L.thumbnailImageUrl = ref.thumbnailImageUrl;
L.worldCapacity = ref.capacity;
} else {
worldRequest
.getWorld({
worldId: L.worldId
})
.then((args) => {
L.worldName = args.ref.name;
L.thumbnailImageUrl =
args.ref.thumbnailImageUrl;
L.worldCapacity = args.ref.capacity;
return args;
});
}
if (gameStore.isGameNoVR) {
platform = 'Desktop';
} else {
platform = 'VR';
}
let groupAccessType = '';
if (L.groupAccessType) {
if (L.groupAccessType === 'public') {
groupAccessType = 'Public';
} else if (L.groupAccessType === 'plus') {
groupAccessType = 'Plus';
}
}
switch (L.accessType) {
case 'public':
L.joinUrl = getLaunchURL(L);
L.accessName = `Public #${L.instanceName} (${platform})`;
break;
case 'invite+':
L.accessName = `Invite+ #${L.instanceName} (${platform})`;
break;
case 'invite':
L.accessName = `Invite #${L.instanceName} (${platform})`;
break;
case 'friends':
L.accessName = `Friends #${L.instanceName} (${platform})`;
break;
case 'friends+':
L.accessName = `Friends+ #${L.instanceName} (${platform})`;
break;
case 'group':
L.accessName = `Group #${L.instanceName} (${platform})`;
getGroupName(L.groupId).then((groupName) => {
if (groupName) {
L.accessName = `Group${groupAccessType}(${groupName}) #${L.instanceName} (${platform})`;
}
});
break;
}
}
locationStore.lastLocation$ = L;
}
let hidePrivate = false;
if (
state.discordHideInvite &&
(L.accessType === 'invite' ||
L.accessType === 'invite+' ||
L.groupAccessType === 'members')
) {
hidePrivate = true;
}
switch (userStore.currentUser.status) {
case 'active':
L.statusName = 'Online';
L.statusImage = 'active';
break;
case 'join me':
L.statusName = 'Join Me';
L.statusImage = 'joinme';
break;
case 'ask me':
L.statusName = 'Ask Me';
L.statusImage = 'askme';
if (state.discordHideInvite) {
hidePrivate = true;
}
break;
case 'busy':
L.statusName = 'Do Not Disturb';
L.statusImage = 'busy';
hidePrivate = true;
break;
}
let activityType = ActivityType.Playing;
let appId = '883308884863901717';
let bigIcon = 'vrchat';
let partyId = `${L.worldId}:${L.instanceName}`;
let partySize = locationStore.lastLocation.playerList.size;
let partyMaxSize = L.worldCapacity;
if (partySize > partyMaxSize) {
partyMaxSize = partySize;
}
let buttonText = 'Join';
let buttonUrl = L.joinUrl;
if (!state.discordJoinButton) {
buttonText = '';
buttonUrl = '';
}
if (!state.discordInstance) {
partySize = 0;
partyMaxSize = 0;
}
if (hidePrivate) {
partyId = '';
partySize = 0;
partyMaxSize = 0;
buttonText = '';
buttonUrl = '';
} else if (isRpcWorld(L.tag)) {
// custom world rpc
if (
L.worldId === 'wrld_f20326da-f1ac-45fc-a062-609723b097b1' ||
L.worldId === 'wrld_10e5e467-fc65-42ed-8957-f02cace1398c' ||
L.worldId === 'wrld_04899f23-e182-4a8d-b2c7-2c74c7c15534'
) {
activityType = ActivityType.Listening;
appId = '784094509008551956';
bigIcon = 'pypy';
} else if (
L.worldId === 'wrld_42377cf1-c54f-45ed-8996-5875b0573a83' ||
L.worldId === 'wrld_dd6d2888-dbdc-47c2-bc98-3d631b2acd7c'
) {
activityType = ActivityType.Listening;
appId = '846232616054030376';
bigIcon = 'vr_dancing';
} else if (
L.worldId === 'wrld_52bdcdab-11cd-4325-9655-0fb120846945' ||
L.worldId === 'wrld_2d40da63-8f1f-4011-8a9e-414eb8530acd'
) {
activityType = ActivityType.Listening;
appId = '939473404808007731';
bigIcon = 'zuwa_zuwa_dance';
} else if (
L.worldId === 'wrld_74970324-58e8-4239-a17b-2c59dfdf00db' ||
L.worldId === 'wrld_db9d878f-6e76-4776-8bf2-15bcdd7fc445' ||
L.worldId === 'wrld_435bbf25-f34f-4b8b-82c6-cd809057eb8e' ||
L.worldId === 'wrld_f767d1c8-b249-4ecc-a56f-614e433682c8'
) {
activityType = ActivityType.Watching;
appId = '968292722391785512';
bigIcon = 'ls_media';
} else if (
L.worldId === 'wrld_266523e8-9161-40da-acd0-6bd82e075833' ||
L.worldId === 'wrld_27c7e6b2-d938-447e-a270-3d1a873e2cf3'
) {
activityType = ActivityType.Watching;
appId = '1095440531821170820';
bigIcon = 'popcorn_palace';
}
if (gameLogStore.nowPlaying.name) {
L.worldName = gameLogStore.nowPlaying.name;
}
if (gameLogStore.nowPlaying.playing) {
Discord.SetTimestamps(
gameLogStore.nowPlaying.startTime * 1000,
(gameLogStore.nowPlaying.startTime +
gameLogStore.nowPlaying.length) *
1000
);
}
} else if (!state.discordHideImage && L.thumbnailImageUrl) {
bigIcon = L.thumbnailImageUrl;
}
Discord.SetAssets(
bigIcon, // big icon
'Powered by VRCX', // big icon hover text
L.statusImage, // small icon
L.statusName, // small icon hover text
partyId, // party id
partySize, // party size
partyMaxSize, // party max size
buttonText, // button text
buttonUrl, // button url
appId, // app id
activityType // activity type
);
// NOTE
// 글자 수가 짧으면 업데이트가 안된다..
if (L.worldName.length < 2) {
L.worldName += '\uFFA0'.repeat(2 - L.worldName.length);
}
if (hidePrivate) {
Discord.SetText('Private', '');
Discord.SetTimestamps(0, 0);
} else if (state.discordInstance) {
Discord.SetText(L.worldName, L.accessName);
} else {
Discord.SetText(L.worldName, '');
}
}
async function setIsDiscordActive(active) {
if (active !== state.isDiscordActive) {
state.isDiscordActive = await Discord.SetActive(active);
}
}
async function saveDiscordOption(configLabel = '') {
locationStore.lastLocation$.tag = '';
updateLoopStore.nextDiscordUpdate = 3;
updateDiscord();
}
return {
state,
discordActive,
discordInstance,
discordHideInvite,
discordJoinButton,
discordHideImage,
setDiscordActive,
setDiscordInstance,
setDiscordHideInvite,
setDiscordJoinButton,
setDiscordHideImage,
updateDiscord,
saveDiscordOption
};
}
);
+321
View File
@@ -0,0 +1,321 @@
import { defineStore } from 'pinia';
import { computed, reactive } from 'vue';
import * as workerTimers from 'worker-timers';
import { $app } from '../../app';
import { t } from '../../plugin';
import configRepository from '../../service/config';
import { useVrcxStore } from '../vrcx';
import { useVRCXUpdaterStore } from '../vrcxUpdater';
export const useGeneralSettingsStore = defineStore('GeneralSettings', () => {
const vrcxStore = useVrcxStore();
const VRCXUpdaterStore = useVRCXUpdaterStore();
const state = reactive({
isStartAtWindowsStartup: false,
isStartAsMinimizedState: false,
isCloseToTray: false,
disableGpuAcceleration: false,
disableVrOverlayGpuAcceleration: false,
localFavoriteFriendsGroups: [],
udonExceptionLogging: false,
logResourceLoad: false,
logEmptyAvatars: false,
autoStateChangeEnabled: false,
autoStateChangeAloneStatus: 'join me',
autoStateChangeCompanyStatus: 'busy',
autoStateChangeInstanceTypes: [],
autoStateChangeNoFriends: false,
autoAcceptInviteRequests: 'Off'
});
async function initGeneralSettings() {
const [
isStartAtWindowsStartup,
isStartAsMinimizedState,
isCloseToTray,
isCloseToTrayConfigBool,
disableGpuAccelerationStr,
disableVrOverlayGpuAccelerationStr,
localFavoriteFriendsGroupsStr,
udonExceptionLogging,
logResourceLoad,
logEmptyAvatars,
autoStateChangeEnabled,
autoStateChangeAloneStatus,
autoStateChangeCompanyStatus,
autoStateChangeInstanceTypesStr,
autoAcceptInviteRequests
] = await Promise.all([
configRepository.getBool('VRCX_StartAtWindowsStartup', false),
VRCXStorage.Get('VRCX_StartAsMinimizedState'),
VRCXStorage.Get('VRCX_CloseToTray'),
configRepository.getBool('VRCX_CloseToTray'),
VRCXStorage.Get('VRCX_DisableGpuAcceleration'),
VRCXStorage.Get('VRCX_DisableVrOverlayGpuAcceleration'),
configRepository.getString('VRCX_localFavoriteFriendsGroups', '[]'),
configRepository.getBool('VRCX_udonExceptionLogging', false),
configRepository.getBool('VRCX_logResourceLoad', false),
configRepository.getBool('VRCX_logEmptyAvatars', false),
configRepository.getBool('VRCX_autoStateChangeEnabled', false),
configRepository.getString(
'VRCX_autoStateChangeAloneStatus',
'join me'
),
configRepository.getString(
'VRCX_autoStateChangeCompanyStatus',
'busy'
),
configRepository.getString(
'VRCX_autoStateChangeInstanceTypes',
'[]'
),
configRepository.getString('VRCX_autoAcceptInviteRequests', 'Off')
]);
state.isStartAtWindowsStartup = isStartAtWindowsStartup;
state.isStartAsMinimizedState = isStartAsMinimizedState === 'true';
if (isCloseToTrayConfigBool) {
state.isCloseToTray = isCloseToTrayConfigBool;
await VRCXStorage.Set(
'VRCX_CloseToTray',
state.isCloseToTray.toString()
);
await configRepository.remove('VRCX_CloseToTray');
} else {
state.isCloseToTray = isCloseToTray === 'true';
}
state.disableGpuAcceleration = disableGpuAccelerationStr === 'true';
state.disableVrOverlayGpuAcceleration =
disableVrOverlayGpuAccelerationStr === 'true';
state.localFavoriteFriendsGroups = JSON.parse(
localFavoriteFriendsGroupsStr
);
state.udonExceptionLogging = udonExceptionLogging;
state.logResourceLoad = logResourceLoad;
state.logEmptyAvatars = logEmptyAvatars;
state.autoStateChangeEnabled = autoStateChangeEnabled;
state.autoStateChangeAloneStatus = autoStateChangeAloneStatus;
state.autoStateChangeCompanyStatus = autoStateChangeCompanyStatus;
state.autoStateChangeInstanceTypes = JSON.parse(
autoStateChangeInstanceTypesStr
);
state.autoAcceptInviteRequests = autoAcceptInviteRequests;
}
initGeneralSettings();
const isStartAtWindowsStartup = computed(
() => state.isStartAtWindowsStartup
);
const isStartAsMinimizedState = computed(
() => state.isStartAsMinimizedState
);
const disableGpuAcceleration = computed(() => state.disableGpuAcceleration);
const isCloseToTray = computed(() => state.isCloseToTray);
const disableVrOverlayGpuAcceleration = computed(
() => state.disableVrOverlayGpuAcceleration
);
const localFavoriteFriendsGroups = computed(
() => state.localFavoriteFriendsGroups
);
const udonExceptionLogging = computed(() => state.udonExceptionLogging);
const logResourceLoad = computed(() => state.logResourceLoad);
const logEmptyAvatars = computed(() => state.logEmptyAvatars);
const autoStateChangeEnabled = computed(() => state.autoStateChangeEnabled);
const autoStateChangeAloneStatus = computed(
() => state.autoStateChangeAloneStatus
);
const autoStateChangeCompanyStatus = computed(
() => state.autoStateChangeCompanyStatus
);
const autoStateChangeInstanceTypes = computed(
() => state.autoStateChangeInstanceTypes
);
const autoStateChangeNoFriends = computed(
() => state.autoStateChangeNoFriends
);
const autoAcceptInviteRequests = computed(
() => state.autoAcceptInviteRequests
);
function setIsStartAtWindowsStartup() {
state.isStartAtWindowsStartup = !state.isStartAtWindowsStartup;
configRepository.setBool(
'VRCX_StartAtWindowsStartup',
state.isStartAtWindowsStartup
);
AppApi.SetStartup(state.isStartAtWindowsStartup);
}
function setIsStartAsMinimizedState() {
state.isStartAsMinimizedState = !state.isStartAsMinimizedState;
VRCXStorage.Set(
'VRCX_StartAsMinimizedState',
state.isStartAsMinimizedState.toString()
);
}
function setIsCloseToTray() {
state.isCloseToTray = !state.isCloseToTray;
VRCXStorage.Set('VRCX_CloseToTray', state.isCloseToTray.toString());
}
function setDisableGpuAcceleration() {
state.disableGpuAcceleration = !state.disableGpuAcceleration;
VRCXStorage.Set(
'VRCX_DisableGpuAcceleration',
state.disableGpuAcceleration.toString()
);
}
function setDisableVrOverlayGpuAcceleration() {
state.disableVrOverlayGpuAcceleration =
!state.disableVrOverlayGpuAcceleration;
VRCXStorage.Set(
'VRCX_DisableVrOverlayGpuAcceleration',
state.disableVrOverlayGpuAcceleration.toString()
);
}
/**
* @param {string[]} value
*/
function setLocalFavoriteFriendsGroups(value) {
state.localFavoriteFriendsGroups = value;
configRepository.setString(
'VRCX_localFavoriteFriendsGroups',
JSON.stringify(value)
);
}
function setUdonExceptionLogging() {
state.udonExceptionLogging = !state.udonExceptionLogging;
configRepository.setBool(
'VRCX_udonExceptionLogging',
state.udonExceptionLogging
);
}
function setLogResourceLoad() {
state.logResourceLoad = !state.logResourceLoad;
configRepository.setBool('VRCX_logResourceLoad', state.logResourceLoad);
}
function setLogEmptyAvatars() {
state.logEmptyAvatars = !state.logEmptyAvatars;
configRepository.setBool('VRCX_logEmptyAvatars', state.logEmptyAvatars);
}
function setAutoStateChangeEnabled() {
state.autoStateChangeEnabled = !state.autoStateChangeEnabled;
configRepository.setBool(
'VRCX_autoStateChangeEnabled',
state.autoStateChangeEnabled
);
}
/**
* @param {string} value
*/
function setAutoStateChangeAloneStatus(value) {
state.autoStateChangeAloneStatus = value;
configRepository.setString(
'VRCX_autoStateChangeAloneStatus',
state.autoStateChangeAloneStatus
);
}
/**
* @param {string} value
*/
function setAutoStateChangeCompanyStatus(value) {
state.autoStateChangeCompanyStatus = value;
configRepository.setString(
'VRCX_autoStateChangeCompanyStatus',
state.autoStateChangeCompanyStatus
);
}
function setAutoStateChangeInstanceTypes(value) {
state.autoStateChangeInstanceTypes = value;
configRepository.setString(
'VRCX_autoStateChangeInstanceTypes',
JSON.stringify(state.autoStateChangeInstanceTypes)
);
}
function setAutoStateChangeNoFriends() {
state.autoStateChangeNoFriends = !state.autoStateChangeNoFriends;
configRepository.setBool(
'VRCX_autoStateChangeNoFriends',
state.autoStateChangeNoFriends
);
}
/**
* @param {string} value
*/
function setAutoAcceptInviteRequests(value) {
state.autoAcceptInviteRequests = value;
configRepository.setString(
'VRCX_autoAcceptInviteRequests',
state.autoAcceptInviteRequests
);
}
function promptProxySettings() {
$app.$prompt(
t('prompt.proxy_settings.description'),
t('prompt.proxy_settings.header'),
{
distinguishCancelAndClose: true,
confirmButtonText: t('prompt.proxy_settings.restart'),
cancelButtonText: t('prompt.proxy_settings.close'),
inputValue: vrcxStore.proxyServer,
inputPlaceholder: t('prompt.proxy_settings.placeholder'),
callback: async (action, instance) => {
vrcxStore.proxyServer = instance.inputValue;
await VRCXStorage.Set(
'VRCX_ProxyServer',
vrcxStore.proxyServer
);
await VRCXStorage.Flush();
await new Promise((resolve) => {
workerTimers.setTimeout(resolve, 100);
});
if (action === 'confirm') {
const { restartVRCX } = VRCXUpdaterStore;
const isUpgrade = false;
restartVRCX(isUpgrade);
}
}
}
);
}
return {
state,
isStartAtWindowsStartup,
isStartAsMinimizedState,
isCloseToTray,
disableGpuAcceleration,
disableVrOverlayGpuAcceleration,
localFavoriteFriendsGroups,
udonExceptionLogging,
logResourceLoad,
logEmptyAvatars,
autoStateChangeEnabled,
autoStateChangeAloneStatus,
autoStateChangeCompanyStatus,
autoStateChangeInstanceTypes,
autoStateChangeNoFriends,
autoAcceptInviteRequests,
setIsStartAtWindowsStartup,
setIsStartAsMinimizedState,
setIsCloseToTray,
setDisableGpuAcceleration,
setDisableVrOverlayGpuAcceleration,
setLocalFavoriteFriendsGroups,
setUdonExceptionLogging,
setLogResourceLoad,
setLogEmptyAvatars,
setAutoStateChangeEnabled,
setAutoStateChangeAloneStatus,
setAutoStateChangeCompanyStatus,
setAutoStateChangeInstanceTypes,
setAutoStateChangeNoFriends,
setAutoAcceptInviteRequests,
promptProxySettings
};
});
+518
View File
@@ -0,0 +1,518 @@
import { defineStore } from 'pinia';
import { computed, reactive } from 'vue';
import { $app } from '../../app';
import { t } from '../../plugin';
import configRepository from '../../service/config';
import { sharedFeedFiltersDefaults } from '../../shared/constants';
import { useVrStore } from '../vr';
export const useNotificationsSettingsStore = defineStore(
'NotificationsSettings',
() => {
const vrStore = useVrStore();
const state = reactive({
overlayToast: true,
openVR: false,
overlayNotifications: true,
xsNotifications: true,
ovrtHudNotifications: true,
ovrtWristNotifications: false,
imageNotifications: true,
desktopToast: 'Never',
afkDesktopToast: false,
notificationTTS: 'Never',
notificationTTSNickName: false,
sharedFeedFilters: {
noty: {
Location: 'Off',
OnPlayerJoined: 'VIP',
OnPlayerLeft: 'VIP',
OnPlayerJoining: 'VIP',
Online: 'VIP',
Offline: 'VIP',
GPS: 'Off',
Status: 'Off',
invite: 'Friends',
requestInvite: 'Friends',
inviteResponse: 'Friends',
requestInviteResponse: 'Friends',
friendRequest: 'On',
Friend: 'On',
Unfriend: 'On',
DisplayName: 'VIP',
TrustLevel: 'VIP',
boop: 'Off',
groupChange: 'On',
'group.announcement': 'On',
'group.informative': 'On',
'group.invite': 'On',
'group.joinRequest': 'Off',
'group.transfer': 'On',
'group.queueReady': 'On',
'instance.closed': 'On',
PortalSpawn: 'Everyone',
Event: 'On',
External: 'On',
VideoPlay: 'Off',
BlockedOnPlayerJoined: 'Off',
BlockedOnPlayerLeft: 'Off',
MutedOnPlayerJoined: 'Off',
MutedOnPlayerLeft: 'Off',
AvatarChange: 'Off',
ChatBoxMessage: 'Off',
Blocked: 'Off',
Unblocked: 'Off',
Muted: 'Off',
Unmuted: 'Off'
},
wrist: {
Location: 'On',
OnPlayerJoined: 'Everyone',
OnPlayerLeft: 'Everyone',
OnPlayerJoining: 'Friends',
Online: 'Friends',
Offline: 'Friends',
GPS: 'Friends',
Status: 'Friends',
invite: 'Friends',
requestInvite: 'Friends',
inviteResponse: 'Friends',
requestInviteResponse: 'Friends',
friendRequest: 'On',
Friend: 'On',
Unfriend: 'On',
DisplayName: 'Friends',
TrustLevel: 'Friends',
boop: 'On',
groupChange: 'On',
'group.announcement': 'On',
'group.informative': 'On',
'group.invite': 'On',
'group.joinRequest': 'On',
'group.transfer': 'On',
'group.queueReady': 'On',
'instance.closed': 'On',
PortalSpawn: 'Everyone',
Event: 'On',
External: 'On',
VideoPlay: 'On',
BlockedOnPlayerJoined: 'Off',
BlockedOnPlayerLeft: 'Off',
MutedOnPlayerJoined: 'Off',
MutedOnPlayerLeft: 'Off',
AvatarChange: 'Everyone',
ChatBoxMessage: 'Off',
Blocked: 'On',
Unblocked: 'On',
Muted: 'On',
Unmuted: 'On'
}
},
isTestTTSVisible: false,
notificationTTSVoice: 0,
notificationTTSTest: '',
TTSvoices: [],
notificationPosition: 'topCenter',
notificationTimeout: 3000
});
async function initNotificationsSettings() {
const [
overlayToast,
overlayNotifications,
openVR,
xsNotifications,
ovrtHudNotifications,
ovrtWristNotifications,
imageNotifications,
desktopToast,
afkDesktopToast,
notificationTTS,
notificationTTSNickName,
sharedFeedFilters,
notificationTTSVoice,
notificationPosition,
notificationTimeout
] = await Promise.all([
configRepository.getString('VRCX_overlayToast', 'Game Running'),
configRepository.getBool('VRCX_overlayNotifications', true),
configRepository.getBool('openVR'),
configRepository.getBool('VRCX_xsNotifications', true),
configRepository.getBool('VRCX_ovrtHudNotifications', true),
configRepository.getBool('VRCX_ovrtWristNotifications', false),
configRepository.getBool('VRCX_imageNotifications', true),
configRepository.getString('VRCX_desktopToast', 'Never'),
configRepository.getBool('VRCX_afkDesktopToast', false),
configRepository.getString('VRCX_notificationTTS', 'Never'),
configRepository.getBool('VRCX_notificationTTSNickName', false),
configRepository.getString(
'sharedFeedFilters',
JSON.stringify(sharedFeedFiltersDefaults)
),
configRepository.getString('VRCX_notificationTTSVoice', '0'),
configRepository.getString(
'VRCX_notificationPosition',
'topCenter'
),
configRepository.getString('VRCX_notificationTimeout', '3000')
]);
state.overlayToast = overlayToast;
state.openVR = openVR;
state.overlayNotifications = overlayNotifications;
state.xsNotifications = xsNotifications;
state.ovrtHudNotifications = ovrtHudNotifications;
state.ovrtWristNotifications = ovrtWristNotifications;
state.imageNotifications = imageNotifications;
state.desktopToast = desktopToast;
state.afkDesktopToast = afkDesktopToast;
state.notificationTTS = notificationTTS;
state.notificationTTSNickName = notificationTTSNickName;
state.sharedFeedFilters = JSON.parse(sharedFeedFilters);
state.notificationTTSVoice = notificationTTSVoice;
state.TTSvoices = speechSynthesis.getVoices();
state.notificationPosition = notificationPosition;
state.notificationTimeout = notificationTimeout;
initSharedFeedFilters();
if (LINUX) {
setTimeout(() => {
updateTTSVoices();
}, 5000);
}
}
initNotificationsSettings();
const overlayToast = computed(() => state.overlayToast);
const openVR = computed(() => state.openVR);
const overlayNotifications = computed(() => state.overlayNotifications);
const xsNotifications = computed(() => state.xsNotifications);
const ovrtHudNotifications = computed(() => state.ovrtHudNotifications);
const ovrtWristNotifications = computed(
() => state.ovrtWristNotifications
);
const imageNotifications = computed(() => state.imageNotifications);
const desktopToast = computed(() => state.desktopToast);
const afkDesktopToast = computed(() => state.afkDesktopToast);
const notificationTTS = computed(() => state.notificationTTS);
const notificationTTSNickName = computed(
() => state.notificationTTSNickName
);
const sharedFeedFilters = computed({
get: () => state.sharedFeedFilters,
set: (value) => (state.sharedFeedFilters = value)
});
const isTestTTSVisible = computed({
get: () => state.isTestTTSVisible,
set: (value) => (state.isTestTTSVisible = value)
});
const notificationTTSVoice = computed({
get: () => state.notificationTTSVoice,
set: (value) => (state.notificationTTSVoice = value)
});
const TTSvoices = computed({
get: () => state.TTSvoices,
set: (value) => (state.TTSvoices = value)
});
const notificationTTSTest = computed({
get: () => state.notificationTTSTest,
set: (value) => (state.notificationTTSTest = value)
});
const notificationPosition = computed(() => state.notificationPosition);
const notificationTimeout = computed({
get: () => state.notificationTimeout,
set: (value) => (state.notificationTimeout = value)
});
function setOverlayToast(value) {
state.overlayToast = value;
configRepository.setString('VRCX_overlayToast', value);
}
function setOverlayNotifications() {
state.overlayNotifications = !state.overlayNotifications;
configRepository.setBool(
'VRCX_overlayNotifications',
state.overlayNotifications
);
}
function setOpenVR() {
state.openVR = !state.openVR;
configRepository.setBool('openVR', state.openVR);
}
function setXsNotifications() {
state.xsNotifications = !state.xsNotifications;
configRepository.setBool(
'VRCX_xsNotifications',
state.xsNotifications
);
}
function setOvrtHudNotifications() {
state.ovrtHudNotifications = !state.ovrtHudNotifications;
configRepository.setBool(
'VRCX_ovrtHudNotifications',
state.ovrtHudNotifications
);
}
function setOvrtWristNotifications() {
state.ovrtWristNotifications = !state.ovrtWristNotifications;
configRepository.setBool(
'VRCX_ovrtWristNotifications',
state.ovrtWristNotifications
);
}
function setImageNotifications() {
state.imageNotifications = !state.imageNotifications;
configRepository.setBool(
'VRCX_imageNotifications',
state.imageNotifications
);
}
function changeNotificationPosition(value) {
state.notificationPosition = value;
configRepository.setString(
'VRCX_notificationPosition',
state.notificationPosition
);
vrStore.updateVRConfigVars();
}
/**
* @param {string} value
*/
function setDesktopToast(value) {
state.desktopToast = value;
configRepository.setString('VRCX_desktopToast', value);
}
function setAfkDesktopToast() {
state.afkDesktopToast = !state.afkDesktopToast;
configRepository.setBool(
'VRCX_afkDesktopToast',
state.afkDesktopToast
);
}
/**
* @param {string} value
*/
function setNotificationTTS(value) {
state.notificationTTS = value;
configRepository.setString('VRCX_notificationTTS', value);
}
function setNotificationTTSNickName() {
state.notificationTTSNickName = !state.notificationTTSNickName;
configRepository.setBool(
'VRCX_notificationTTSNickName',
state.notificationTTSNickName
);
}
function initSharedFeedFilters() {
if (!state.sharedFeedFilters.noty.Blocked) {
state.sharedFeedFilters.noty.Blocked = 'Off';
state.sharedFeedFilters.noty.Unblocked = 'Off';
state.sharedFeedFilters.noty.Muted = 'Off';
state.sharedFeedFilters.noty.Unmuted = 'Off';
state.sharedFeedFilters.wrist.Blocked = 'On';
state.sharedFeedFilters.wrist.Unblocked = 'On';
state.sharedFeedFilters.wrist.Muted = 'On';
state.sharedFeedFilters.wrist.Unmuted = 'On';
}
if (!state.sharedFeedFilters.noty['group.announcement']) {
state.sharedFeedFilters.noty['group.announcement'] = 'On';
state.sharedFeedFilters.noty['group.informative'] = 'On';
state.sharedFeedFilters.noty['group.invite'] = 'On';
state.sharedFeedFilters.noty['group.joinRequest'] = 'Off';
state.sharedFeedFilters.wrist['group.announcement'] = 'On';
state.sharedFeedFilters.wrist['group.informative'] = 'On';
state.sharedFeedFilters.wrist['group.invite'] = 'On';
state.sharedFeedFilters.wrist['group.joinRequest'] = 'On';
}
if (!state.sharedFeedFilters.noty['group.queueReady']) {
state.sharedFeedFilters.noty['group.queueReady'] = 'On';
state.sharedFeedFilters.wrist['group.queueReady'] = 'On';
}
if (!state.sharedFeedFilters.noty['instance.closed']) {
state.sharedFeedFilters.noty['instance.closed'] = 'On';
state.sharedFeedFilters.wrist['instance.closed'] = 'On';
}
if (!state.sharedFeedFilters.noty.External) {
state.sharedFeedFilters.noty.External = 'On';
state.sharedFeedFilters.wrist.External = 'On';
}
if (!state.sharedFeedFilters.noty.groupChange) {
state.sharedFeedFilters.noty.groupChange = 'On';
state.sharedFeedFilters.wrist.groupChange = 'On';
}
if (!state.sharedFeedFilters.noty['group.transfer']) {
state.sharedFeedFilters.noty['group.transfer'] = 'On';
state.sharedFeedFilters.wrist['group.transfer'] = 'On';
}
if (!state.sharedFeedFilters.noty.boop) {
state.sharedFeedFilters.noty.boop = 'Off';
state.sharedFeedFilters.wrist.boop = 'On';
}
}
function setNotificationTTSVoice(index) {
state.notificationTTSVoice = index;
configRepository.setString(
'VRCX_notificationTTSVoice',
state.notificationTTSVoice
);
}
function getTTSVoiceName() {
let voices;
if (LINUX) {
voices = state.TTSvoices;
} else {
voices = speechSynthesis.getVoices();
}
if (voices.length === 0) {
return '';
}
if (state.notificationTTSVoice >= voices.length) {
setNotificationTTSVoice(0);
}
return voices[state.notificationTTSVoice].name;
}
async function changeTTSVoice(index) {
setNotificationTTSVoice(index);
let voices;
if (LINUX) {
voices = state.TTSvoices;
} else {
voices = speechSynthesis.getVoices();
}
if (voices.length === 0) {
return;
}
const voiceName = voices[index].name;
speechSynthesis.cancel();
speak(voiceName);
}
function updateTTSVoices() {
state.TTSvoices = speechSynthesis.getVoices();
if (LINUX) {
const voices = speechSynthesis.getVoices();
let uniqueVoices = [];
voices.forEach((voice) => {
if (!uniqueVoices.some((v) => v.lang === voice.lang)) {
uniqueVoices.push(voice);
}
});
uniqueVoices = uniqueVoices.filter((v) =>
v.lang.startsWith('en')
);
state.TTSvoices = uniqueVoices;
}
}
async function saveNotificationTTS(value) {
speechSynthesis.cancel();
if (
(await configRepository.getString('VRCX_notificationTTS')) ===
'Never' &&
value !== 'Never'
) {
speak('Notification text-to-speech enabled');
}
setNotificationTTS(value);
}
function testNotificationTTS() {
speechSynthesis.cancel();
speak(state.notificationTTSTest);
}
function speak(text) {
const tts = new SpeechSynthesisUtterance();
const voices = speechSynthesis.getVoices();
if (voices.length === 0) {
return;
}
let index = 0;
if (state.notificationTTSVoice < voices.length) {
index = state.notificationTTSVoice;
}
tts.voice = voices[index];
tts.text = text;
speechSynthesis.speak(tts);
}
function promptNotificationTimeout() {
$app.$prompt(
t('prompt.notification_timeout.description'),
t('prompt.notification_timeout.header'),
{
distinguishCancelAndClose: true,
confirmButtonText: t('prompt.notification_timeout.ok'),
cancelButtonText: t('prompt.notification_timeout.cancel'),
inputValue: state.notificationTimeout / 1000,
inputPattern: /\d+$/,
inputErrorMessage: t(
'prompt.notification_timeout.input_error'
),
callback: async (action, instance) => {
if (
action === 'confirm' &&
instance.inputValue &&
!isNaN(instance.inputValue)
) {
state.notificationTimeout = Math.trunc(
Number(instance.inputValue) * 1000
);
await configRepository.setString(
'VRCX_notificationTimeout',
state.notificationTimeout
);
vrStore.updateVRConfigVars();
}
}
}
);
}
return {
state,
overlayToast,
openVR,
overlayNotifications,
xsNotifications,
ovrtHudNotifications,
ovrtWristNotifications,
imageNotifications,
desktopToast,
afkDesktopToast,
notificationTTS,
notificationTTSNickName,
sharedFeedFilters,
isTestTTSVisible,
notificationTTSVoice,
TTSvoices,
notificationTTSTest,
notificationPosition,
notificationTimeout,
setOverlayToast,
setOpenVR,
setOverlayNotifications,
setXsNotifications,
setOvrtHudNotifications,
setOvrtWristNotifications,
setImageNotifications,
setDesktopToast,
setAfkDesktopToast,
setNotificationTTS,
setNotificationTTSNickName,
getTTSVoiceName,
changeTTSVoice,
saveNotificationTTS,
testNotificationTTS,
speak,
changeNotificationPosition,
promptNotificationTimeout
};
}
);
+173
View File
@@ -0,0 +1,173 @@
import { defineStore } from 'pinia';
import { computed, reactive } from 'vue';
import configRepository from '../../service/config';
export const useWristOverlaySettingsStore = defineStore(
'WristOverlaySettings',
() => {
const state = reactive({
overlayWrist: true,
hidePrivateFromFeed: false,
openVRAlways: false,
overlaybutton: false,
overlayHand: 0,
vrBackgroundEnabled: false,
minimalFeed: false,
hideDevicesFromFeed: false,
vrOverlayCpuUsage: false,
hideUptimeFromFeed: false,
pcUptimeOnFeed: false
});
async function initWristOverlaySettings() {
const [
overlayWrist,
hidePrivateFromFeed,
openVRAlways,
overlaybutton,
overlayHand,
vrBackgroundEnabled,
minimalFeed,
hideDevicesFromFeed,
vrOverlayCpuUsage,
hideUptimeFromFeed,
pcUptimeOnFeed
] = await Promise.all([
configRepository.getBool('VRCX_overlayWrist', false),
configRepository.getBool('VRCX_hidePrivateFromFeed', false),
configRepository.getBool('openVRAlways', false),
configRepository.getBool('VRCX_overlaybutton', false),
configRepository.getInt('VRCX_overlayHand', 0),
configRepository.getBool('VRCX_vrBackgroundEnabled', false),
configRepository.getBool('VRCX_minimalFeed', false),
configRepository.getBool('VRCX_hideDevicesFromFeed', false),
configRepository.getBool('VRCX_vrOverlayCpuUsage', false),
configRepository.getBool('VRCX_hideUptimeFromFeed', false),
configRepository.getBool('VRCX_pcUptimeOnFeed', false)
]);
state.overlayWrist = overlayWrist;
state.hidePrivateFromFeed = hidePrivateFromFeed;
state.openVRAlways = openVRAlways;
state.overlaybutton = overlaybutton;
state.overlayHand = overlayHand;
state.vrBackgroundEnabled = vrBackgroundEnabled;
state.minimalFeed = minimalFeed;
state.hideDevicesFromFeed = hideDevicesFromFeed;
state.vrOverlayCpuUsage = vrOverlayCpuUsage;
state.hideUptimeFromFeed = hideUptimeFromFeed;
state.pcUptimeOnFeed = pcUptimeOnFeed;
}
const overlayWrist = computed(() => state.overlayWrist);
const hidePrivateFromFeed = computed(() => state.hidePrivateFromFeed);
const openVRAlways = computed(() => state.openVRAlways);
const overlaybutton = computed(() => state.overlaybutton);
const overlayHand = computed(() => state.overlayHand);
const vrBackgroundEnabled = computed(() => state.vrBackgroundEnabled);
const minimalFeed = computed(() => state.minimalFeed);
const hideDevicesFromFeed = computed(() => state.hideDevicesFromFeed);
const vrOverlayCpuUsage = computed(() => state.vrOverlayCpuUsage);
const hideUptimeFromFeed = computed(() => state.hideUptimeFromFeed);
const pcUptimeOnFeed = computed(() => state.pcUptimeOnFeed);
function setOverlayWrist() {
state.overlayWrist = !state.overlayWrist;
configRepository.setBool('VRCX_overlayWrist', state.overlayWrist);
}
function setHidePrivateFromFeed() {
state.hidePrivateFromFeed = !state.hidePrivateFromFeed;
configRepository.setBool(
'VRCX_hidePrivateFromFeed',
state.hidePrivateFromFeed
);
}
function setOpenVRAlways() {
state.openVRAlways = !state.openVRAlways;
configRepository.setBool('openVRAlways', state.openVRAlways);
}
function setOverlaybutton() {
state.overlaybutton = !state.overlaybutton;
configRepository.setBool('VRCX_overlaybutton', state.overlaybutton);
}
/**
* @param {string} value
*/
function setOverlayHand(value) {
state.overlayHand = parseInt(value, 10);
if (isNaN(state.overlayHand)) {
state.overlayHand = 0;
}
configRepository.setInt('VRCX_overlayHand', value);
}
function setVrBackgroundEnabled() {
state.vrBackgroundEnabled = !state.vrBackgroundEnabled;
configRepository.setBool(
'VRCX_vrBackgroundEnabled',
state.vrBackgroundEnabled
);
}
function setMinimalFeed() {
state.minimalFeed = !state.minimalFeed;
configRepository.setBool('VRCX_minimalFeed', state.minimalFeed);
}
function setHideDevicesFromFeed() {
state.hideDevicesFromFeed = !state.hideDevicesFromFeed;
configRepository.setBool(
'VRCX_hideDevicesFromFeed',
state.hideDevicesFromFeed
);
}
function setVrOverlayCpuUsage() {
state.vrOverlayCpuUsage = !state.vrOverlayCpuUsage;
configRepository.setBool(
'VRCX_vrOverlayCpuUsage',
state.vrOverlayCpuUsage
);
}
function setHideUptimeFromFeed() {
state.hideUptimeFromFeed = !state.hideUptimeFromFeed;
configRepository.setBool(
'VRCX_hideUptimeFromFeed',
state.hideUptimeFromFeed
);
}
function setPcUptimeOnFeed() {
state.pcUptimeOnFeed = !state.pcUptimeOnFeed;
configRepository.setBool(
'VRCX_pcUptimeOnFeed',
state.pcUptimeOnFeed
);
}
initWristOverlaySettings();
return {
state,
overlayWrist,
hidePrivateFromFeed,
openVRAlways,
overlaybutton,
overlayHand,
vrBackgroundEnabled,
minimalFeed,
hideDevicesFromFeed,
vrOverlayCpuUsage,
hideUptimeFromFeed,
pcUptimeOnFeed,
setOverlayWrist,
setHidePrivateFromFeed,
setOpenVRAlways,
setOverlaybutton,
setOverlayHand,
setVrBackgroundEnabled,
setMinimalFeed,
setHideDevicesFromFeed,
setVrOverlayCpuUsage,
setHideUptimeFromFeed,
setPcUptimeOnFeed
};
}
);
+609
View File
@@ -0,0 +1,609 @@
import { defineStore } from 'pinia';
import { computed, reactive } from 'vue';
import * as workerTimers from 'worker-timers';
import { groupRequest, worldRequest } from '../api';
import { watchState } from '../service/watchState';
import { useFeedStore } from './feed';
import { useFriendStore } from './friend';
import { useGameLogStore } from './gameLog';
import { useGroupStore } from './group';
import { useInstanceStore } from './instance';
import { useLocationStore } from './location';
import { useModerationStore } from './moderation';
import { useNotificationStore } from './notification';
import { usePhotonStore } from './photon';
import { useNotificationsSettingsStore } from './settings/notifications';
import { useWristOverlaySettingsStore } from './settings/wristOverlay';
import { useUserStore } from './user';
import { useWorldStore } from './world';
import { useAuthStore } from './auth';
export const useSharedFeedStore = defineStore('SharedFeed', () => {
const friendStore = useFriendStore();
const notificationsSettingsStore = useNotificationsSettingsStore();
const locationStore = useLocationStore();
const groupStore = useGroupStore();
const userStore = useUserStore();
const wristOverlaySettingsStore = useWristOverlaySettingsStore();
const instanceStore = useInstanceStore();
const gameLogStore = useGameLogStore();
const moderationStore = useModerationStore();
const notificationStore = useNotificationStore();
const feedStore = useFeedStore();
const worldStore = useWorldStore();
const photonStore = usePhotonStore();
const authStore = useAuthStore();
const state = reactive({
sharedFeed: {
gameLog: {
wrist: [],
lastEntryDate: ''
},
feedTable: {
wrist: [],
lastEntryDate: ''
},
notificationTable: {
wrist: [],
lastEntryDate: ''
},
friendLogTable: {
wrist: [],
lastEntryDate: ''
},
moderationAgainstTable: {
wrist: [],
lastEntryDate: ''
},
pendingUpdate: false
},
updateSharedFeedTimer: null,
updateSharedFeedPending: false,
updateSharedFeedPendingForceUpdate: false
});
const sharedFeed = computed({
get: () => state.sharedFeed,
set: (value) => {
state.sharedFeed = value;
}
});
function updateSharedFeed(forceUpdate) {
if (!watchState.isFriendsLoaded) {
return;
}
if (state.updateSharedFeedTimer) {
if (forceUpdate) {
state.updateSharedFeedPendingForceUpdate = true;
}
state.updateSharedFeedPending = true;
} else {
updateSharedExecute(forceUpdate);
state.updateSharedFeedTimer = setTimeout(() => {
if (state.updateSharedFeedPending) {
updateSharedExecute(
state.updateSharedFeedPendingForceUpdate
);
}
state.updateSharedFeedTimer = null;
}, 150);
}
}
function updateSharedExecute(forceUpdate) {
try {
updateSharedFeedDebounce(forceUpdate);
} catch (err) {
console.error(err);
}
state.updateSharedFeedTimer = null;
state.updateSharedFeedPending = false;
state.updateSharedFeedPendingForceUpdate = false;
}
function updateSharedFeedDebounce(forceUpdate) {
updateSharedFeedGameLog(forceUpdate);
updateSharedFeedFeedTable(forceUpdate);
updateSharedFeedNotificationTable(forceUpdate);
updateSharedFeedFriendLogTable(forceUpdate);
updateSharedFeedModerationAgainstTable(forceUpdate);
const feeds = state.sharedFeed;
if (!feeds.pendingUpdate) {
return;
}
let wristFeed = [];
wristFeed = wristFeed.concat(
feeds.gameLog.wrist,
feeds.feedTable.wrist,
feeds.notificationTable.wrist,
feeds.friendLogTable.wrist,
feeds.moderationAgainstTable.wrist
);
// OnPlayerJoining/Traveling
userStore.currentTravelers.forEach((ref) => {
const isFavorite = friendStore.localFavoriteFriends.has(ref.id);
if (
(notificationsSettingsStore.sharedFeedFilters.wrist
.OnPlayerJoining === 'Friends' ||
(notificationsSettingsStore.sharedFeedFilters.wrist
.OnPlayerJoining === 'VIP' &&
isFavorite)) &&
!locationStore.lastLocation.playerList.has(ref.id)
) {
if (ref.$location.tag === locationStore.lastLocation.location) {
var feedEntry = {
...ref,
isFavorite,
isFriend: true,
type: 'OnPlayerJoining'
};
wristFeed.unshift(feedEntry);
} else {
const worldRef = worldStore.cachedWorlds.get(
ref.$location.worldId
);
let groupName = '';
if (ref.$location.groupId) {
const groupRef = groupStore.cachedGroups.get(
ref.$location.groupId
);
if (typeof groupRef !== 'undefined') {
groupName = groupRef.name;
} else {
// no group cache, fetch group and try again
groupRequest
.getGroup({
groupId: ref.$location.groupId
})
.then((args) => {
args.ref = groupStore.applyGroup(args.json);
workerTimers.setTimeout(() => {
// delay to allow for group cache to update
state.sharedFeed.pendingUpdate = true;
updateSharedFeed(false);
}, 100);
return args;
})
.catch((err) => {
console.error(err);
});
}
}
if (typeof worldRef !== 'undefined') {
var feedEntry = {
created_at: ref.created_at,
type: 'GPS',
userId: ref.id,
displayName: ref.displayName,
location: ref.$location.tag,
worldName: worldRef.name,
groupName,
previousLocation: '',
isFavorite,
time: 0,
isFriend: true,
isTraveling: true
};
wristFeed.unshift(feedEntry);
} else {
// no world cache, fetch world and try again
worldRequest
.getWorld({
worldId: ref.$location.worldId
})
.then((args) => {
workerTimers.setTimeout(() => {
// delay to allow for world cache to update
state.sharedFeed.pendingUpdate = true;
updateSharedFeed(false);
}, 100);
return args;
})
.catch((err) => {
console.error(err);
});
}
}
}
});
wristFeed.sort(function (a, b) {
if (a.created_at < b.created_at) {
return 1;
}
if (a.created_at > b.created_at) {
return -1;
}
return 0;
});
wristFeed.splice(16);
AppApi.ExecuteVrFeedFunction(
'wristFeedUpdate',
JSON.stringify(wristFeed)
);
userStore.applyUserDialogLocation();
instanceStore.applyWorldDialogInstances();
instanceStore.applyGroupDialogInstances();
feeds.pendingUpdate = false;
}
function updateSharedFeedGameLog(forceUpdate) {
// Location, OnPlayerJoined, OnPlayerLeft
const sessionTable = gameLogStore.gameLogSessionTable;
let i = sessionTable.length;
if (i > 0) {
if (
sessionTable[i - 1].created_at ===
state.sharedFeed.gameLog.lastEntryDate &&
forceUpdate === false
) {
return;
}
state.sharedFeed.gameLog.lastEntryDate =
sessionTable[i - 1].created_at;
} else {
return;
}
const bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours
const wristArr = [];
let w = 0;
const wristFilter = notificationsSettingsStore.sharedFeedFilters.wrist;
let currentUserLeaveTime = 0;
let locationJoinTime = 0;
for (i = sessionTable.length - 1; i > -1; i--) {
const ctx = sessionTable[i];
if (ctx.created_at < bias) {
break;
}
if (ctx.type === 'Notification') {
continue;
}
// on Location change remove OnPlayerLeft
if (ctx.type === 'LocationDestination') {
currentUserLeaveTime = Date.parse(ctx.created_at);
const currentUserLeaveTimeOffset =
currentUserLeaveTime + 5 * 1000;
for (var k = w - 1; k > -1; k--) {
var feedItem = wristArr[k];
if (
(feedItem.type === 'OnPlayerLeft' ||
feedItem.type === 'BlockedOnPlayerLeft' ||
feedItem.type === 'MutedOnPlayerLeft') &&
Date.parse(feedItem.created_at) >=
currentUserLeaveTime &&
Date.parse(feedItem.created_at) <=
currentUserLeaveTimeOffset
) {
wristArr.splice(k, 1);
w--;
}
}
}
// on Location change remove OnPlayerJoined
if (ctx.type === 'Location') {
locationJoinTime = Date.parse(ctx.created_at);
const locationJoinTimeOffset = locationJoinTime + 20 * 1000;
for (var k = w - 1; k > -1; k--) {
var feedItem = wristArr[k];
if (
(feedItem.type === 'OnPlayerJoined' ||
feedItem.type === 'BlockedOnPlayerJoined' ||
feedItem.type === 'MutedOnPlayerJoined') &&
Date.parse(feedItem.created_at) >= locationJoinTime &&
Date.parse(feedItem.created_at) <=
locationJoinTimeOffset
) {
wristArr.splice(k, 1);
w--;
}
}
}
// remove current user
if (
(ctx.type === 'OnPlayerJoined' ||
ctx.type === 'OnPlayerLeft' ||
ctx.type === 'PortalSpawn') &&
ctx.displayName === userStore.currentUser.displayName
) {
continue;
}
let isFriend = false;
let isFavorite = false;
if (ctx.userId) {
isFriend = friendStore.friends.has(ctx.userId);
isFavorite = friendStore.localFavoriteFriends.has(ctx.userId);
} else if (ctx.displayName) {
for (var ref of userStore.cachedUsers.values()) {
if (ref.displayName === ctx.displayName) {
isFriend = friendStore.friends.has(ref.id);
isFavorite = friendStore.localFavoriteFriends.has(
ref.id
);
break;
}
}
}
// add tag colour
let tagColour = '';
if (ctx.userId) {
const tagRef = userStore.customUserTags.get(ctx.userId);
if (typeof tagRef !== 'undefined') {
tagColour = tagRef.colour;
}
}
// BlockedOnPlayerJoined, BlockedOnPlayerLeft, MutedOnPlayerJoined, MutedOnPlayerLeft
if (ctx.type === 'OnPlayerJoined' || ctx.type === 'OnPlayerLeft') {
for (var ref of moderationStore.cachedPlayerModerations.values()) {
if (
ref.targetDisplayName !== ctx.displayName &&
ref.sourceUserId !== ctx.userId
) {
continue;
}
if (ref.type === 'block') {
var type = `Blocked${ctx.type}`;
} else if (ref.type === 'mute') {
var type = `Muted${ctx.type}`;
} else {
continue;
}
const entry = {
created_at: ctx.created_at,
type,
displayName: ref.targetDisplayName,
userId: ref.targetUserId,
isFriend,
isFavorite
};
if (
wristFilter[type] &&
(wristFilter[type] === 'Everyone' ||
(wristFilter[type] === 'Friends' && isFriend) ||
(wristFilter[type] === 'VIP' && isFavorite))
) {
wristArr.unshift(entry);
}
notificationStore.queueGameLogNoty(entry);
}
}
// when too many user joins happen at once when switching instances
// the "w" counter maxes out and wont add any more entries
// until the onJoins are cleared by "Location"
// e.g. if a "VideoPlay" occurs between "OnPlayerJoined" and "Location" it wont be added
if (
w < 50 &&
wristFilter[ctx.type] &&
(wristFilter[ctx.type] === 'On' ||
wristFilter[ctx.type] === 'Everyone' ||
(wristFilter[ctx.type] === 'Friends' && isFriend) ||
(wristFilter[ctx.type] === 'VIP' && isFavorite))
) {
wristArr.push({
...ctx,
tagColour,
isFriend,
isFavorite
});
++w;
}
}
state.sharedFeed.gameLog.wrist = wristArr;
state.sharedFeed.pendingUpdate = true;
}
function updateSharedFeedFeedTable(forceUpdate) {
// GPS, Online, Offline, Status, Avatar
const feedSession = feedStore.feedSessionTable;
var i = feedSession.length;
if (i > 0) {
if (
feedSession[i - 1].created_at ===
state.sharedFeed.feedTable.lastEntryDate &&
forceUpdate === false
) {
return;
}
state.sharedFeed.feedTable.lastEntryDate =
feedSession[i - 1].created_at;
} else {
return;
}
const bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours
const wristArr = [];
let w = 0;
const wristFilter = notificationsSettingsStore.sharedFeedFilters.wrist;
for (var i = feedSession.length - 1; i > -1; i--) {
const ctx = feedSession[i];
if (ctx.created_at < bias) {
break;
}
if (ctx.type === 'Avatar') {
continue;
}
// hide private worlds from feed
if (
wristOverlaySettingsStore.hidePrivateFromFeed &&
ctx.type === 'GPS' &&
ctx.location === 'private'
) {
continue;
}
const isFriend = friendStore.friends.has(ctx.userId);
const isFavorite = friendStore.localFavoriteFriends.has(ctx.userId);
if (
w < 20 &&
wristFilter[ctx.type] &&
(wristFilter[ctx.type] === 'Friends' ||
(wristFilter[ctx.type] === 'VIP' && isFavorite))
) {
wristArr.push({
...ctx,
isFriend,
isFavorite
});
++w;
}
}
state.sharedFeed.feedTable.wrist = wristArr;
state.sharedFeed.pendingUpdate = true;
}
function updateSharedFeedNotificationTable(forceUpdate) {
// invite, requestInvite, requestInviteResponse, inviteResponse, friendRequest
const notificationTable = notificationStore.notificationTable.data;
let i = notificationTable.length;
if (i > 0) {
if (
notificationTable[i - 1].created_at ===
state.sharedFeed.notificationTable.lastEntryDate &&
forceUpdate === false
) {
return;
}
state.sharedFeed.notificationTable.lastEntryDate =
notificationTable[i - 1].created_at;
} else {
return;
}
const bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours
const wristArr = [];
let w = 0;
const wristFilter = notificationsSettingsStore.sharedFeedFilters.wrist;
for (i = notificationTable.length - 1; i > -1; i--) {
const ctx = notificationTable[i];
if (ctx.created_at < bias) {
break;
}
if (ctx.senderUserId === userStore.currentUser.id) {
continue;
}
const isFriend = friendStore.friends.has(ctx.senderUserId);
const isFavorite = friendStore.localFavoriteFriends.has(
ctx.senderUserId
);
if (
w < 20 &&
wristFilter[ctx.type] &&
(wristFilter[ctx.type] === 'On' ||
wristFilter[ctx.type] === 'Friends' ||
(wristFilter[ctx.type] === 'VIP' && isFavorite))
) {
wristArr.push({
...ctx,
isFriend,
isFavorite
});
++w;
}
}
state.sharedFeed.notificationTable.wrist = wristArr;
state.sharedFeed.pendingUpdate = true;
}
function updateSharedFeedFriendLogTable(forceUpdate) {
// TrustLevel, Friend, FriendRequest, Unfriend, DisplayName
const friendLog = friendStore.friendLogTable.data;
var i = friendLog.length;
if (i > 0) {
if (
friendLog[i - 1].created_at ===
state.sharedFeed.friendLogTable.lastEntryDate &&
forceUpdate === false
) {
return;
}
state.sharedFeed.friendLogTable.lastEntryDate =
friendLog[i - 1].created_at;
} else {
return;
}
const bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours
const wristArr = [];
let w = 0;
const wristFilter = notificationsSettingsStore.sharedFeedFilters.wrist;
for (var i = friendLog.length - 1; i > -1; i--) {
const ctx = friendLog[i];
if (ctx.created_at < bias) {
break;
}
if (ctx.type === 'FriendRequest') {
continue;
}
const isFriend = friendStore.friends.has(ctx.userId);
const isFavorite = friendStore.localFavoriteFriends.has(ctx.userId);
if (
w < 20 &&
wristFilter[ctx.type] &&
(wristFilter[ctx.type] === 'On' ||
wristFilter[ctx.type] === 'Friends' ||
(wristFilter[ctx.type] === 'VIP' && isFavorite))
) {
wristArr.push({
...ctx,
isFriend,
isFavorite
});
++w;
}
}
state.sharedFeed.friendLogTable.wrist = wristArr;
state.sharedFeed.pendingUpdate = true;
}
function updateSharedFeedModerationAgainstTable(forceUpdate) {
// Unblocked, Blocked, Muted, Unmuted
const moderationAgainst = photonStore.moderationAgainstTable;
var i = moderationAgainst.length;
if (i > 0) {
if (
moderationAgainst[i - 1].created_at ===
state.sharedFeed.moderationAgainstTable.lastEntryDate &&
forceUpdate === false
) {
return;
}
state.sharedFeed.moderationAgainstTable.lastEntryDate =
moderationAgainst[i - 1].created_at;
} else {
return;
}
const bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours
const wristArr = [];
let w = 0;
const wristFilter = notificationsSettingsStore.sharedFeedFilters.wrist;
for (var i = moderationAgainst.length - 1; i > -1; i--) {
const ctx = moderationAgainst[i];
if (ctx.created_at < bias) {
break;
}
const isFriend = friendStore.friends.has(ctx.userId);
const isFavorite = friendStore.localFavoriteFriends.has(ctx.userId);
// add tag colour
let tagColour = '';
const tagRef = userStore.customUserTags.get(ctx.userId);
if (typeof tagRef !== 'undefined') {
tagColour = tagRef.colour;
}
if (
w < 20 &&
wristFilter[ctx.type] &&
wristFilter[ctx.type] === 'On'
) {
wristArr.push({
...ctx,
isFriend,
isFavorite,
tagColour
});
++w;
}
}
state.sharedFeed.moderationAgainstTable.wrist = wristArr;
state.sharedFeed.pendingUpdate = true;
}
return { state, sharedFeed, updateSharedFeed };
});
+82
View File
@@ -0,0 +1,82 @@
import { defineStore } from 'pinia';
import { computed, reactive, watch } from 'vue';
import { watchState } from '../service/watchState';
import { useNotificationStore } from './notification';
export const useUiStore = defineStore('Ui', () => {
const notificationStore = useNotificationStore();
const state = reactive({
menuActiveIndex: 'feed',
notifiedMenus: [],
shiftHeld: false
});
document.addEventListener('keydown', function (e) {
if (e.shiftKey) {
state.shiftHeld = true;
}
});
document.addEventListener('keyup', function (e) {
if (!e.shiftKey) {
state.shiftHeld = false;
}
});
const shiftHeld = computed(() => state.shiftHeld);
const menuActiveIndex = computed({
get: () => state.menuActiveIndex,
set: (value) => {
state.menuActiveIndex = value;
}
});
const notifiedMenus = computed({
get: () => state.notifiedMenus,
set: (value) => {
state.notifiedMenus = value;
}
});
watch(
() => watchState.isLoggedIn,
(isLoggedIn) => {
if (isLoggedIn) {
state.menuActiveIndex = 'feed';
}
},
{ flush: 'sync' }
);
function notifyMenu(index) {
if (
index !== state.menuActiveIndex &&
!state.notifiedMenus.includes(index)
) {
state.notifiedMenus.push(index);
}
}
function selectMenu(index) {
state.menuActiveIndex = index;
removeNotify(index);
if (index === 'notification') {
notificationStore.unseenNotifications = [];
}
}
function removeNotify(index) {
state.notifiedMenus = state.notifiedMenus.filter((i) => i !== index);
}
return {
state,
menuActiveIndex,
notifiedMenus,
shiftHeld,
notifyMenu,
selectMenu,
removeNotify
};
});
+166
View File
@@ -0,0 +1,166 @@
import { defineStore } from 'pinia';
import { computed, reactive, watch } from 'vue';
import * as workerTimers from 'worker-timers';
import { groupRequest } from '../api';
import { database } from '../service/database';
import { watchState } from '../service/watchState';
import { useAuthStore } from './auth';
import { useFriendStore } from './friend';
import { useGameStore } from './game';
import { useGameLogStore } from './gameLog';
import { useModerationStore } from './moderation';
import { useDiscordPresenceSettingsStore } from './settings/discordPresence';
import { useUiStore } from './ui';
import { useUserStore } from './user';
import { useVrcxStore } from './vrcx';
import { useVRCXUpdaterStore } from './vrcxUpdater';
import { useGroupStore } from './group';
export const useUpdateLoopStore = defineStore('UpdateLoop', () => {
const state = reactive({
nextCurrentUserRefresh: 300,
nextFriendsRefresh: 3600,
nextGroupInstanceRefresh: 0,
nextAppUpdateCheck: 3600,
ipcTimeout: 0,
nextClearVRCXCacheCheck: 0,
nextDiscordUpdate: 0,
nextAutoStateChange: 0,
nextGetLogCheck: 0,
nextGameRunningCheck: 0,
nextDatabaseOptimize: 3600
});
watch(
() => watchState.isLoggedIn,
() => {
state.nextCurrentUserRefresh = 300;
state.nextFriendsRefresh = 3600;
state.nextGroupInstanceRefresh = 0;
},
{ flush: 'sync' }
);
const nextGroupInstanceRefresh = computed({
get: () => state.nextGroupInstanceRefresh,
set: (value) => {
state.nextGroupInstanceRefresh = value;
}
});
const nextCurrentUserRefresh = computed({
get: () => state.nextCurrentUserRefresh,
set: (value) => {
state.nextCurrentUserRefresh = value;
}
});
async function updateLoop() {
const authStore = useAuthStore();
const userStore = useUserStore();
const friendStore = useFriendStore();
const gameStore = useGameStore();
const moderationStore = useModerationStore();
const vrcxStore = useVrcxStore();
const discordPresenceSettingsStore = useDiscordPresenceSettingsStore();
const gameLogStore = useGameLogStore();
const vrcxUpdaterStore = useVRCXUpdaterStore();
const uiStore = useUiStore();
const groupStore = useGroupStore();
try {
if (watchState.isLoggedIn) {
if (--state.nextCurrentUserRefresh <= 0) {
state.nextCurrentUserRefresh = 300; // 5min
userStore.getCurrentUser();
}
if (--state.nextFriendsRefresh <= 0) {
state.nextFriendsRefresh = 3600; // 1hour
friendStore.refreshFriendsList();
authStore.updateStoredUser(userStore.currentUser);
if (gameStore.isGameRunning) {
moderationStore.refreshPlayerModerations();
}
}
if (--state.nextGroupInstanceRefresh <= 0) {
if (watchState.isFriendsLoaded) {
state.nextGroupInstanceRefresh = 300; // 5min
const args =
await groupRequest.getUsersGroupInstances();
groupStore.handleGroupUserInstances(args);
}
AppApi.CheckGameRunning();
}
if (--state.nextAppUpdateCheck <= 0) {
state.nextAppUpdateCheck = 3600; // 1hour
if (vrcxUpdaterStore.autoUpdateVRCX !== 'Off') {
vrcxUpdaterStore.checkForVRCXUpdate(uiStore.notifyMenu);
}
}
if (--state.ipcTimeout <= 0) {
vrcxStore.ipcEnabled = false;
}
if (
--state.nextClearVRCXCacheCheck <= 0 &&
vrcxStore.clearVRCXCacheFrequency > 0
) {
state.nextClearVRCXCacheCheck =
vrcxStore.clearVRCXCacheFrequency / 2;
vrcxStore.clearVRCXCache();
}
if (--state.nextDiscordUpdate <= 0) {
state.nextDiscordUpdate = 3;
if (discordPresenceSettingsStore.discordActive) {
discordPresenceSettingsStore.updateDiscord();
}
}
if (--state.nextAutoStateChange <= 0) {
state.nextAutoStateChange = 3;
userStore.updateAutoStateChange();
}
if (
(vrcxStore.isRunningUnderWine || LINUX) &&
--state.nextGetLogCheck <= 0
) {
state.nextGetLogCheck = 0.5;
const logLines = await LogWatcher.GetLogLines();
if (logLines) {
logLines.forEach((logLine) => {
gameLogStore.addGameLogEvent(logLine);
});
}
}
if (
(vrcxStore.isRunningUnderWine || LINUX) &&
--state.nextGameRunningCheck <= 0
) {
if (LINUX) {
state.nextGameRunningCheck = 1;
gameStore.updateIsGameRunning(
await AppApi.IsGameRunning(),
await AppApi.IsSteamVRRunning(),
false
);
} else {
state.nextGameRunningCheck = 3;
AppApi.CheckGameRunning();
}
}
if (--state.nextDatabaseOptimize <= 0) {
state.nextDatabaseOptimize = 86400; // 1 day
database.optimize();
}
}
} catch (err) {
friendStore.isRefreshFriendsLoading = false;
console.error(err);
}
workerTimers.setTimeout(() => updateLoop(), 1000);
}
return {
state,
nextGroupInstanceRefresh,
nextCurrentUserRefresh,
updateLoop
};
});
+1997
View File
File diff suppressed because it is too large Load Diff
+165
View File
@@ -0,0 +1,165 @@
import { defineStore } from 'pinia';
import { reactive, watch } from 'vue';
import { isRpcWorld } from '../shared/utils';
import { watchState } from '../service/watchState';
import { useFriendStore } from './friend';
import { useGameStore } from './game';
import { useGameLogStore } from './gameLog';
import { useLocationStore } from './location';
import { usePhotonStore } from './photon';
import { useAdvancedSettingsStore } from './settings/advanced';
import { useAppearanceSettingsStore } from './settings/appearance';
import { useNotificationsSettingsStore } from './settings/notifications';
import { useWristOverlaySettingsStore } from './settings/wristOverlay';
import { useSharedFeedStore } from './sharedFeed';
import { useUserStore } from './user';
export const useVrStore = defineStore('Vr', () => {
const friendStore = useFriendStore();
const advancedSettingsStore = useAdvancedSettingsStore();
const wristOverlaySettingsStore = useWristOverlaySettingsStore();
const locationStore = useLocationStore();
const notificationsSettingsStore = useNotificationsSettingsStore();
const photonStore = usePhotonStore();
const appearanceSettingsStore = useAppearanceSettingsStore();
const gameStore = useGameStore();
const gameLogStore = useGameLogStore();
const userStore = useUserStore();
const sharedFeedStore = useSharedFeedStore();
const state = reactive({});
watch(
() => watchState.isFriendsLoaded,
(isFriendsLoaded) => {
if (isFriendsLoaded) {
vrInit();
}
},
{ flush: 'sync' }
);
// also runs from CEF C# on overlay browser startup
function vrInit() {
updateVRConfigVars();
updateVRLastLocation();
updateVrNowPlaying();
// run these methods again to send data to the overlay
sharedFeedStore.updateSharedFeed(true);
friendStore.onlineFriendCount = 0; // force an update
friendStore.updateOnlineFriendCoutner();
}
async function saveOpenVROption() {
sharedFeedStore.updateSharedFeed(true);
updateVRConfigVars();
updateVRLastLocation();
AppApi.ExecuteVrOverlayFunction('notyClear', '');
updateOpenVR();
}
function updateVrNowPlaying() {
const json = JSON.stringify(gameLogStore.nowPlaying);
AppApi.ExecuteVrFeedFunction('nowPlayingUpdate', json);
AppApi.ExecuteVrOverlayFunction('nowPlayingUpdate', json);
}
function updateVRLastLocation() {
let progressPie = false;
if (advancedSettingsStore.progressPie) {
progressPie = true;
if (advancedSettingsStore.progressPieFilter) {
if (!isRpcWorld(locationStore.lastLocation.location)) {
progressPie = false;
}
}
}
let onlineFor = '';
if (!wristOverlaySettingsStore.hideUptimeFromFeed) {
onlineFor = userStore.currentUser.$online_for;
}
const lastLocation = {
date: locationStore.lastLocation.date,
location: locationStore.lastLocation.location,
name: locationStore.lastLocation.name,
playerList: Array.from(
locationStore.lastLocation.playerList.values()
),
friendList: Array.from(
locationStore.lastLocation.friendList.values()
),
progressPie,
onlineFor
};
const json = JSON.stringify(lastLocation);
AppApi.ExecuteVrFeedFunction('lastLocationUpdate', json);
AppApi.ExecuteVrOverlayFunction('lastLocationUpdate', json);
}
function updateVRConfigVars() {
let notificationTheme = 'relax';
if (appearanceSettingsStore.isDarkMode) {
notificationTheme = 'sunset';
}
const VRConfigVars = {
overlayNotifications:
notificationsSettingsStore.overlayNotifications,
hideDevicesFromFeed: wristOverlaySettingsStore.hideDevicesFromFeed,
vrOverlayCpuUsage: wristOverlaySettingsStore.vrOverlayCpuUsage,
minimalFeed: wristOverlaySettingsStore.minimalFeed,
notificationPosition:
notificationsSettingsStore.notificationPosition,
notificationTimeout: notificationsSettingsStore.notificationTimeout,
photonOverlayMessageTimeout:
photonStore.photonOverlayMessageTimeout,
notificationTheme,
backgroundEnabled: wristOverlaySettingsStore.vrBackgroundEnabled,
dtHour12: appearanceSettingsStore.dtHour12,
pcUptimeOnFeed: wristOverlaySettingsStore.pcUptimeOnFeed,
appLanguage: appearanceSettingsStore.appLanguage,
notificationOpacity: advancedSettingsStore.notificationOpacity
};
const json = JSON.stringify(VRConfigVars);
AppApi.ExecuteVrFeedFunction('configUpdate', json);
AppApi.ExecuteVrOverlayFunction('configUpdate', json);
}
function updateOpenVR() {
if (
notificationsSettingsStore.openVR &&
gameStore.isSteamVRRunning &&
((gameStore.isGameRunning && !gameStore.isGameNoVR) ||
wristOverlaySettingsStore.openVRAlways)
) {
let hmdOverlay = false;
if (
notificationsSettingsStore.overlayNotifications ||
advancedSettingsStore.progressPie ||
photonStore.photonEventOverlay ||
photonStore.timeoutHudOverlay
) {
hmdOverlay = true;
}
// active, hmdOverlay, wristOverlay, menuButton, overlayHand
AppApi.SetVR(
true,
hmdOverlay,
wristOverlaySettingsStore.overlayWrist,
wristOverlaySettingsStore.overlaybutton,
wristOverlaySettingsStore.overlayHand
);
} else {
AppApi.SetVR(false, false, false, false, 0);
}
}
return {
state,
vrInit,
saveOpenVROption,
updateVrNowPlaying,
updateVRLastLocation,
updateVRConfigVars,
updateOpenVR
};
});
+773
View File
@@ -0,0 +1,773 @@
import { defineStore } from 'pinia';
import { computed, reactive, watch } from 'vue';
import { worldRequest } from '../api';
import { $app } from '../app';
import configRepository from '../service/config';
import { database } from '../service/database';
import { AppGlobal } from '../service/appConfig';
import { failedGetRequests } from '../service/request';
import { watchState } from '../service/watchState';
import {
debounce,
parseLocation,
refreshCustomCss,
removeFromArray
} from '../shared/utils';
import { useAvatarStore } from './avatar';
import { useAvatarProviderStore } from './avatarProvider';
import { useFavoriteStore } from './favorite';
import { useFriendStore } from './friend';
import { useGameStore } from './game';
import { useGameLogStore } from './gameLog';
import { useGroupStore } from './group';
import { useInstanceStore } from './instance';
import { useLocationStore } from './location';
import { useNotificationStore } from './notification';
import { usePhotonStore } from './photon';
import { useSearchStore } from './search';
import { useAdvancedSettingsStore } from './settings/advanced';
import { useUpdateLoopStore } from './updateLoop';
import { useUserStore } from './user';
import { useWorldStore } from './world';
import { useI18n } from 'vue-i18n-bridge';
export const useVrcxStore = defineStore('Vrcx', () => {
const gameStore = useGameStore();
const locationStore = useLocationStore();
const notificationStore = useNotificationStore();
const avatarStore = useAvatarStore();
const worldStore = useWorldStore();
const instanceStore = useInstanceStore();
const friendStore = useFriendStore();
const favoriteStore = useFavoriteStore();
const groupStore = useGroupStore();
const userStore = useUserStore();
const photonStore = usePhotonStore();
const advancedSettingsStore = useAdvancedSettingsStore();
const searchStore = useSearchStore();
const avatarProviderStore = useAvatarProviderStore();
const gameLogStore = useGameLogStore();
const updateLoopStore = useUpdateLoopStore();
const { t } = useI18n();
const state = reactive({
isRunningUnderWine: false,
databaseVersion: 0,
clearVRCXCacheFrequency: 172800,
proxyServer: '',
locationX: 0,
locationY: 0,
sizeWidth: 800,
sizeHeight: 600,
windowState: '',
maxTableSize: 1000,
ipcEnabled: false,
externalNotifierVersion: 0,
currentlyDroppingFile: null,
isRegistryBackupDialogVisible: false
});
async function init() {
if (LINUX) {
window.electron.onWindowPositionChanged((event, position) => {
state.locationX = position.x;
state.locationY = position.y;
debounce(saveVRCXWindowOption(), 300);
});
window.electron.onWindowSizeChanged((event, size) => {
state.sizeWidth = size.width;
state.sizeHeight = size.height;
debounce(saveVRCXWindowOption(), 300);
});
window.electron.onWindowStateChange((event, state) => {
state.windowState = state;
debounce(saveVRCXWindowOption(), 300);
});
// window.electron.onWindowClosed((event) => {
// window.$app.saveVRCXWindowOption();
// });
}
state.databaseVersion = await configRepository.getInt(
'VRCX_databaseVersion',
0
);
updateDatabaseVersion();
state.clearVRCXCacheFrequency = await configRepository.getInt(
'VRCX_clearVRCXCacheFrequency',
172800
);
if (!(await VRCXStorage.Get('VRCX_DatabaseLocation'))) {
await VRCXStorage.Set('VRCX_DatabaseLocation', '');
}
if (!(await VRCXStorage.Get('VRCX_ProxyServer'))) {
await VRCXStorage.Set('VRCX_ProxyServer', '');
}
if ((await VRCXStorage.Get('VRCX_DisableGpuAcceleration')) === '') {
await VRCXStorage.Set('VRCX_DisableGpuAcceleration', 'false');
}
if (
(await VRCXStorage.Get('VRCX_DisableVrOverlayGpuAcceleration')) ===
''
) {
await VRCXStorage.Set(
'VRCX_DisableVrOverlayGpuAcceleration',
'false'
);
}
state.proxyServer = await VRCXStorage.Get('VRCX_ProxyServer');
state.locationX = await VRCXStorage.Get('VRCX_LocationX');
state.locationY = await VRCXStorage.Get('VRCX_LocationY');
state.sizeWidth = await VRCXStorage.Get('VRCX_SizeWidth');
state.sizeHeight = await VRCXStorage.Get('VRCX_SizeHeight');
state.windowState = await VRCXStorage.Get('VRCX_WindowState');
state.maxTableSize = await configRepository.getInt(
'VRCX_maxTableSize',
1000
);
if (state.maxTableSize > 10000) {
state.maxTableSize = 1000;
}
database.setMaxTableSize(state.maxTableSize);
}
init();
const currentlyDroppingFile = computed({
get: () => state.currentlyDroppingFile,
set: (value) => {
state.currentlyDroppingFile = value;
}
});
const isRegistryBackupDialogVisible = computed({
get: () => state.isRegistryBackupDialogVisible,
set: (value) => {
state.isRegistryBackupDialogVisible = value;
}
});
const ipcEnabled = computed({
get: () => state.ipcEnabled,
set: (value) => {
state.ipcEnabled = value;
}
});
const clearVRCXCacheFrequency = computed({
get: () => state.clearVRCXCacheFrequency,
set: (value) => {
state.clearVRCXCacheFrequency = value;
}
});
const maxTableSize = computed({
get: () => state.maxTableSize,
set: (value) => {
state.maxTableSize = value;
}
});
// Make sure file drops outside of the screenshot manager don't navigate to the file path dropped.
// This issue persists on prompts created with prompt(), unfortunately. Not sure how to fix that.
document.body.addEventListener('drop', function (e) {
e.preventDefault();
});
document.addEventListener('keyup', function (e) {
if (e.ctrlKey) {
if (e.key === 'I') {
showConsole();
} else if (e.key === 'r') {
location.reload();
}
} else if (e.altKey && e.key === 'R') {
refreshCustomCss();
}
});
const isRunningUnderWine = computed({
get: () => state.isRunningUnderWine,
set: (value) => {
state.isRunningUnderWine = value;
}
});
async function applyWineEmojis() {
if (document.contains(document.getElementById('app-emoji-font'))) {
document.getElementById('app-emoji-font').remove();
}
if (gameStore.isRunningUnderWine) {
const $appEmojiFont = document.createElement('link');
$appEmojiFont.setAttribute('id', 'app-emoji-font');
$appEmojiFont.rel = 'stylesheet';
$appEmojiFont.href = 'emoji.font.css';
document.head.appendChild($appEmojiFont);
}
}
function showConsole() {
AppApi.ShowDevTools();
if (
AppGlobal.debug ||
AppGlobal.debugWebRequests ||
AppGlobal.debugWebSocket ||
AppGlobal.debugUserDiff
) {
return;
}
console.log(
'%cCareful! This might not do what you think.',
'background-color: red; color: yellow; font-size: 32px; font-weight: bold'
);
console.log(
'%cIf someone told you to copy-paste something here, it can give them access to your account.',
'font-size: 20px;'
);
}
async function updateDatabaseVersion() {
const databaseVersion = 12;
let msgBox;
if (state.databaseVersion < databaseVersion) {
if (state.databaseVersion) {
msgBox = $app.$message({
message:
'DO NOT CLOSE VRCX, database upgrade in progress...',
type: 'warning',
duration: 0
});
}
console.log(
`Updating database from ${state.databaseVersion} to ${databaseVersion}...`
);
try {
await database.cleanLegendFromFriendLog(); // fix friendLog spammed with crap
await database.fixGameLogTraveling(); // fix bug with gameLog location being set as traveling
await database.fixNegativeGPS(); // fix GPS being a negative value due to VRCX bug with traveling
await database.fixBrokenLeaveEntries(); // fix user instance timer being higher than current user location timer
await database.fixBrokenGroupInvites(); // fix notification v2 in wrong table
await database.fixBrokenNotifications(); // fix notifications being null
await database.fixBrokenGroupChange(); // fix spam group left & name change
await database.fixCancelFriendRequestTypo(); // fix CancelFriendRequst typo
await database.fixBrokenGameLogDisplayNames(); // fix gameLog display names "DisplayName (userId)"
await database.upgradeDatabaseVersion(); // update database version
await database.vacuum(); // succ
await database.optimize();
await configRepository.setInt(
'VRCX_databaseVersion',
databaseVersion
);
console.log('Database update complete.');
msgBox?.close();
if (state.databaseVersion) {
// only display when database exists
$app.$message({
message: 'Database upgrade complete',
type: 'success'
});
}
state.databaseVersion = databaseVersion;
} catch (err) {
console.error(err);
msgBox?.close();
$app.$message({
message:
'Database upgrade failed, check console for details',
type: 'error',
duration: 120000
});
AppApi.ShowDevTools();
}
}
}
function clearVRCXCache() {
failedGetRequests.clear();
userStore.cachedUsers.forEach((ref, id) => {
if (
!friendStore.friends.has(id) &&
!locationStore.lastLocation.playerList.has(ref.id) &&
id !== userStore.currentUser.id
) {
userStore.cachedUsers.delete(id);
}
});
worldStore.cachedWorlds.forEach((ref, id) => {
if (
!favoriteStore.cachedFavoritesByObjectId.has(id) &&
ref.authorId !== userStore.currentUser.id &&
!favoriteStore.localWorldFavoritesList.includes(id)
) {
worldStore.cachedWorlds.delete(id);
}
});
avatarStore.cachedAvatars.forEach((ref, id) => {
if (
!favoriteStore.cachedFavoritesByObjectId.has(id) &&
ref.authorId !== userStore.currentUser.id &&
!favoriteStore.localAvatarFavoritesList.includes(id) &&
!avatarStore.avatarHistory.has(id)
) {
avatarStore.cachedAvatars.delete(id);
}
});
groupStore.cachedGroups.forEach((ref, id) => {
if (!groupStore.currentUserGroups.has(id)) {
groupStore.cachedGroups.delete(id);
}
});
instanceStore.cachedInstances.forEach((ref, id) => {
// delete instances over an hour old
if (Date.parse(ref.$fetchedAt) < Date.now() - 3600000) {
instanceStore.cachedInstances.delete(id);
}
});
avatarStore.cachedAvatarNames = new Map();
userStore.customUserTags = new Map();
}
function eventVrcxMessage(data) {
let entry;
switch (data.MsgType) {
case 'CustomTag':
userStore.addCustomTag(data);
break;
case 'ClearCustomTags':
userStore.customUserTags.forEach((value, key) => {
userStore.customUserTags.delete(key);
const ref = userStore.cachedUsers.get(key);
if (typeof ref !== 'undefined') {
ref.$customTag = '';
ref.$customTagColour = '';
}
});
break;
case 'Noty':
if (
photonStore.photonLoggingEnabled ||
(state.externalNotifierVersion &&
state.externalNotifierVersion > 21)
) {
return;
}
entry = {
created_at: new Date().toJSON(),
type: 'Event',
data: data.Data
};
database.addGamelogEventToDatabase(entry);
notificationStore.queueGameLogNoty(entry);
gameLogStore.addGameLog(entry);
break;
case 'External': {
const displayName = data.DisplayName ?? '';
entry = {
created_at: new Date().toJSON(),
type: 'External',
message: data.Data,
displayName,
userId: data.UserId,
location: locationStore.lastLocation.location
};
database.addGamelogExternalToDatabase(entry);
notificationStore.queueGameLogNoty(entry);
gameLogStore.addGameLog(entry);
break;
}
default:
console.log('VRCXMessage:', data);
break;
}
}
async function saveVRCXWindowOption() {
if (LINUX) {
VRCXStorage.Set('VRCX_LocationX', state.locationX);
VRCXStorage.Set('VRCX_LocationY', state.locationY);
VRCXStorage.Set('VRCX_SizeWidth', state.sizeWidth);
VRCXStorage.Set('VRCX_SizeHeight', state.sizeHeight);
VRCXStorage.Set('VRCX_WindowState', state.windowState);
VRCXStorage.Flush();
}
}
async function processScreenshot(path) {
let newPath = path;
if (advancedSettingsStore.screenshotHelper) {
const location = parseLocation(locationStore.lastLocation.location);
const metadata = {
application: 'VRCX',
version: 1,
author: {
id: userStore.currentUser.id,
displayName: userStore.currentUser.displayName
},
world: {
name: locationStore.lastLocation.name,
id: location.worldId,
instanceId: locationStore.lastLocation.location
},
players: []
};
for (const user of locationStore.lastLocation.playerList.values()) {
metadata.players.push({
id: user.userId,
displayName: user.displayName
});
}
newPath = await AppApi.AddScreenshotMetadata(
path,
JSON.stringify(metadata),
location.worldId,
advancedSettingsStore.screenshotHelperModifyFilename
);
console.log('Screenshot metadata added', newPath);
}
if (advancedSettingsStore.screenshotHelperCopyToClipboard) {
await AppApi.CopyImageToClipboard(newPath);
console.log('Screenshot copied to clipboard', newPath);
}
}
// use in C# side
// eslint-disable-next-line no-unused-vars
function ipcEvent(json) {
if (!watchState.isLoggedIn) {
return;
}
let data;
try {
data = JSON.parse(json);
} catch {
console.log(`IPC invalid JSON, ${json}`);
return;
}
switch (data.type) {
case 'OnEvent':
if (!gameStore.isGameRunning) {
console.log('Game closed, skipped event', data);
return;
}
if (AppGlobal.debugPhotonLogging) {
console.log(
'OnEvent',
data.OnEventData.Code,
data.OnEventData
);
}
photonStore.parsePhotonEvent(data.OnEventData, data.dt);
photonStore.photonEventPulse();
break;
case 'OnOperationResponse':
if (!gameStore.isGameRunning) {
console.log('Game closed, skipped event', data);
return;
}
if (AppGlobal.debugPhotonLogging) {
console.log(
'OnOperationResponse',
data.OnOperationResponseData.OperationCode,
data.OnOperationResponseData
);
}
photonStore.parseOperationResponse(
data.OnOperationResponseData,
data.dt
);
photonStore.photonEventPulse();
break;
case 'OnOperationRequest':
if (!gameStore.isGameRunning) {
console.log('Game closed, skipped event', data);
return;
}
if (AppGlobal.debugPhotonLogging) {
console.log(
'OnOperationRequest',
data.OnOperationRequestData.OperationCode,
data.OnOperationRequestData
);
}
break;
case 'VRCEvent':
if (!gameStore.isGameRunning) {
console.log('Game closed, skipped event', data);
return;
}
photonStore.parseVRCEvent(data);
photonStore.photonEventPulse();
break;
case 'Event7List':
photonStore.photonEvent7List.clear();
for (const [id, dt] of Object.entries(data.Event7List)) {
photonStore.photonEvent7List.set(parseInt(id, 10), dt);
}
photonStore.photonLastEvent7List = Date.parse(data.dt);
break;
case 'VrcxMessage':
if (AppGlobal.debugPhotonLogging) {
console.log('VrcxMessage:', data);
}
eventVrcxMessage(data);
break;
case 'Ping':
if (!photonStore.photonLoggingEnabled) {
photonStore.setPhotonLoggingEnabled();
}
state.ipcEnabled = true;
updateLoopStore.ipcTimeout = 60; // 30secs
break;
case 'MsgPing':
state.externalNotifierVersion = data.version;
break;
case 'LaunchCommand':
eventLaunchCommand(data.command);
break;
case 'VRCXLaunch':
console.log('VRCXLaunch:', data);
break;
default:
console.log('IPC:', data);
}
}
/**
* This function is called by .NET(CefCustomDragHandler#CefCustomDragHandler) when a file is dragged over a drop zone in the app window.
* @param {string} filePath - The full path to the file being dragged into the window
*/
// eslint-disable-next-line no-unused-vars
function dragEnterCef(filePath) {
state.currentlyDroppingFile = filePath;
}
watch(
() => watchState.isLoggedIn,
(isLoggedIn) => {
state.isRegistryBackupDialogVisible = false;
if (isLoggedIn) {
startupLaunchCommand();
}
},
{ flush: 'sync' }
);
async function startupLaunchCommand() {
const command = await AppApi.GetLaunchCommand();
if (command) {
eventLaunchCommand(command);
}
}
function eventLaunchCommand(input) {
if (!watchState.isLoggedIn) {
return;
}
console.log('LaunchCommand:', input);
const args = input.split('/');
const command = args[0];
const commandArg = args[1]?.trim();
let shouldFocusWindow = true;
switch (command) {
case 'world':
searchStore.directAccessWorld(input.replace('world/', ''));
break;
case 'avatar':
avatarStore.showAvatarDialog(commandArg);
break;
case 'user':
userStore.showUserDialog(commandArg);
break;
case 'group':
groupStore.showGroupDialog(commandArg);
break;
case 'local-favorite-world':
console.log('local-favorite-world', commandArg);
const [id, group] = commandArg.split(':');
worldRequest.getCachedWorld({ worldId: id }).then((args1) => {
searchStore.directAccessWorld(id);
favoriteStore.addLocalWorldFavorite(id, group);
return args1;
});
break;
case 'addavatardb':
avatarProviderStore.addAvatarProvider(
input.replace('addavatardb/', '')
);
break;
case 'switchavatar':
const avatarId = commandArg;
const regexAvatarId =
/avtr_[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}/g;
if (!avatarId.match(regexAvatarId) || avatarId.length !== 41) {
$app.$message({
message: 'Invalid Avatar ID',
type: 'error'
});
break;
}
if (advancedSettingsStore.showConfirmationOnSwitchAvatar) {
avatarStore.selectAvatarWithConfirmation(avatarId);
// Makes sure the window is focused
shouldFocusWindow = true;
} else {
$app.selectAvatarWithoutConfirmation(avatarId);
shouldFocusWindow = false;
}
break;
case 'import':
const type = args[1];
if (!type) break;
const data = input.replace(`import/${type}/`, '');
if (type === 'avatar') {
favoriteStore.avatarImportDialogInput = data;
favoriteStore.showAvatarImportDialog();
} else if (type === 'world') {
favoriteStore.worldImportDialogInput = data;
favoriteStore.showWorldImportDialog();
} else if (type === 'friend') {
favoriteStore.friendImportDialogInput = data;
favoriteStore.showFriendImportDialog();
}
break;
}
if (shouldFocusWindow) {
AppApi.FocusWindow();
}
}
async function backupVrcRegistry(name) {
let regJson;
if (LINUX) {
regJson = await AppApi.GetVRChatRegistryJson();
regJson = JSON.parse(regJson);
} else {
regJson = await AppApi.GetVRChatRegistry();
}
const newBackup = {
name,
date: new Date().toJSON(),
data: regJson
};
let backupsJson = await configRepository.getString(
'VRCX_VRChatRegistryBackups'
);
if (!backupsJson) {
backupsJson = JSON.stringify([]);
}
const backups = JSON.parse(backupsJson);
backups.push(newBackup);
await configRepository.setString(
'VRCX_VRChatRegistryBackups',
JSON.stringify(backups)
);
// await this.updateRegistryBackupDialog();
}
async function checkAutoBackupRestoreVrcRegistry() {
if (!advancedSettingsStore.vrcRegistryAutoBackup) {
return;
}
// check for auto restore
const hasVRChatRegistryFolder = await AppApi.HasVRChatRegistryFolder();
if (!hasVRChatRegistryFolder) {
const lastBackupDate = await configRepository.getString(
'VRCX_VRChatRegistryLastBackupDate'
);
const lastRestoreCheck = await configRepository.getString(
'VRCX_VRChatRegistryLastRestoreCheck'
);
if (
!lastBackupDate ||
(lastRestoreCheck &&
lastBackupDate &&
lastRestoreCheck === lastBackupDate)
) {
// only ask to restore once and when backup is present
return;
}
// popup message about auto restore
$app.$alert(
t('dialog.registry_backup.restore_prompt'),
t('dialog.registry_backup.header')
);
showRegistryBackupDialog();
await AppApi.FocusWindow();
await configRepository.setString(
'VRCX_VRChatRegistryLastRestoreCheck',
lastBackupDate
);
} else {
await autoBackupVrcRegistry();
}
}
function showRegistryBackupDialog() {
state.isRegistryBackupDialogVisible = true;
}
async function autoBackupVrcRegistry() {
const date = new Date();
const lastBackupDate = await configRepository.getString(
'VRCX_VRChatRegistryLastBackupDate'
);
if (lastBackupDate) {
const lastBackup = new Date(lastBackupDate);
const diff = date.getTime() - lastBackup.getTime();
const diffDays = Math.floor(diff / (1000 * 60 * 60 * 24));
if (diffDays < 7) {
return;
}
}
let backupsJson = await configRepository.getString(
'VRCX_VRChatRegistryBackups'
);
if (!backupsJson) {
backupsJson = JSON.stringify([]);
}
const backups = JSON.parse(backupsJson);
backups.forEach((backup) => {
if (backup.name === 'Auto Backup') {
// remove old auto backup
removeFromArray(backups, backup);
}
});
await configRepository.setString(
'VRCX_VRChatRegistryBackups',
JSON.stringify(backups)
);
backupVrcRegistry('Auto Backup');
await configRepository.setString(
'VRCX_VRChatRegistryLastBackupDate',
date.toJSON()
);
}
return {
state,
isRunningUnderWine,
currentlyDroppingFile,
isRegistryBackupDialogVisible,
ipcEnabled,
clearVRCXCacheFrequency,
maxTableSize,
showConsole,
applyWineEmojis,
clearVRCXCache,
startupLaunchCommand,
eventVrcxMessage,
showRegistryBackupDialog,
checkAutoBackupRestoreVrcRegistry,
processScreenshot,
ipcEvent,
dragEnterCef,
backupVrcRegistry
};
});
+497
View File
@@ -0,0 +1,497 @@
import { defineStore } from 'pinia';
import { computed, reactive } from 'vue';
import * as workerTimers from 'worker-timers';
import { $app } from '../app';
import configRepository from '../service/config';
import { watchState } from '../service/watchState';
import { branches } from '../shared/constants';
import { changeLogRemoveLinks } from '../shared/utils';
import { useAuthStore } from './auth';
import { useUiStore } from './ui';
import { useI18n } from 'vue-i18n-bridge';
import { AppGlobal } from '../service/appConfig';
export const useVRCXUpdaterStore = defineStore('VRCXUpdater', () => {
const uiStore = useUiStore();
const { t } = useI18n();
const state = reactive({
appVersion: '',
autoUpdateVRCX: 'Auto Download',
latestAppVersion: '',
branch: 'Stable',
vrcxId: '',
checkingForVRCXUpdate: false,
VRCXUpdateDialog: {
visible: false,
updatePending: false,
updatePendingIsLatest: false,
release: '',
releases: [],
json: {}
},
changeLogDialog: {
visible: false,
buildName: '',
changeLog: ''
},
pendingVRCXUpdate: false,
pendingVRCXInstall: false,
updateInProgress: false,
updateProgress: 0
});
async function initVRCXUpdaterSettings() {
const [autoUpdateVRCX, vrcxId] = await Promise.all([
configRepository.getString('VRCX_autoUpdateVRCX', 'Auto Download'),
configRepository.getString('VRCX_id', '')
]);
if (autoUpdateVRCX === 'Auto Install') {
state.autoUpdateVRCX = 'Auto Download';
} else {
state.autoUpdateVRCX = autoUpdateVRCX;
}
state.appVersion = await AppApi.GetVersion();
state.vrcxId = vrcxId;
await initBranch();
await loadVrcxId();
if (await compareAppVersion()) {
showChangeLogDialog();
}
if (state.autoUpdateVRCX !== 'Off') {
await checkForVRCXUpdate();
}
}
const appVersion = computed(() => state.appVersion);
const autoUpdateVRCX = computed(() => state.autoUpdateVRCX);
const latestAppVersion = computed(() => state.latestAppVersion);
const branch = computed({
get: () => state.branch,
set: (value) => {
state.branch = value;
}
});
const currentVersion = computed(() =>
state.appVersion.replace(' (Linux)', '')
);
const vrcxId = computed(() => state.vrcxId);
const checkingForVRCXUpdate = computed({
get: () => state.checkingForVRCXUpdate,
set: (value) => {
state.checkingForVRCXUpdate = value;
}
});
const VRCXUpdateDialog = computed({
get: () => state.VRCXUpdateDialog,
set: (value) => {
state.VRCXUpdateDialog = { ...state.VRCXUpdateDialog, ...value };
}
});
const changeLogDialog = computed({
get: () => state.changeLogDialog,
set: (value) => {
state.changeLogDialog = value;
}
});
const pendingVRCXUpdate = computed({
get: () => state.pendingVRCXUpdate,
set: (value) => {
state.pendingVRCXUpdate = value;
}
});
const pendingVRCXInstall = computed({
get: () => state.pendingVRCXInstall,
set: (value) => {
state.pendingVRCXInstall = value;
}
});
const updateInProgress = computed({
get: () => state.updateInProgress,
set: (value) => {
state.updateInProgress = value;
}
});
const updateProgress = computed({
get: () => state.updateProgress,
set: (value) => {
state.updateProgress = value;
}
});
/**
* @param {string} value
*/
async function setAutoUpdateVRCX(value) {
if (value === 'Off') {
state.pendingVRCXUpdate = false;
}
state.autoUpdateVRCX = value;
await configRepository.setString('VRCX_autoUpdateVRCX', value);
}
/**
* @param {string} value
*/
function setLatestAppVersion(value) {
state.latestAppVersion = value;
}
/**
* @param {string} value
*/
function setBranch(value) {
state.branch = value;
configRepository.setString('VRCX_branch', value);
}
async function initBranch() {
if (!state.appVersion) {
return;
}
if (currentVersion.value.includes('VRCX Nightly')) {
state.branch = 'Nightly';
} else {
state.branch = 'Stable';
}
await configRepository.setString('VRCX_branch', state.branch);
}
async function compareAppVersion() {
const lastVersion = await configRepository.getString(
'VRCX_lastVRCXVersion',
''
);
if (lastVersion !== currentVersion.value) {
await configRepository.setString(
'VRCX_lastVRCXVersion',
currentVersion.value
);
return state.branch === 'Stable' && !!lastVersion;
}
return false;
}
async function loadVrcxId() {
if (!state.vrcxId) {
state.vrcxId = crypto.randomUUID();
await configRepository.setString('VRCX_id', state.vrcxId);
}
}
async function checkForVRCXUpdate() {
const authStore = useAuthStore();
if (
!currentVersion.value ||
currentVersion.value === 'VRCX Nightly Build' ||
currentVersion.value === 'VRCX Build'
) {
// ignore custom builds
return;
}
if (state.branch === 'Beta') {
// move Beta users to stable
setBranch('Stable');
}
if (typeof branches[state.branch] === 'undefined') {
// handle invalid branch
setBranch('Stable');
}
const url = branches[state.branch].urlLatest;
state.checkingForVRCXUpdate = true;
let response;
try {
response = await webApiService.execute({
url,
method: 'GET',
headers: {
'VRCX-ID': state.vrcxId
}
});
} finally {
state.checkingForVRCXUpdate = false;
}
state.pendingVRCXUpdate = false;
const json = JSON.parse(response.data);
if (AppGlobal.debugWebRequests) {
console.log(json, response);
}
if (json === Object(json) && json.name && json.published_at) {
state.VRCXUpdateDialog.updateJson = json;
state.changeLogDialog.buildName = json.name;
state.changeLogDialog.changeLog = changeLogRemoveLinks(json.body);
const releaseName = json.name;
setLatestAppVersion(releaseName);
state.VRCXUpdateDialog.updatePendingIsLatest = false;
if (releaseName === state.pendingVRCXInstall) {
// update already downloaded
state.VRCXUpdateDialog.updatePendingIsLatest = true;
} else if (releaseName > state.currentVersion) {
let downloadUrl = '';
let downloadName = '';
let hashUrl = '';
let size = 0;
for (const asset of json.assets) {
if (asset.state !== 'uploaded') {
continue;
}
if (
!LINUX &&
(asset.content_type === 'application/x-msdownload' ||
asset.content_type ===
'application/x-msdos-program')
) {
downloadUrl = asset.browser_download_url;
downloadName = asset.name;
size = asset.size;
continue;
}
if (
LINUX &&
asset.content_type === 'application/octet-stream'
) {
downloadUrl = asset.browser_download_url;
downloadName = asset.name;
size = asset.size;
continue;
}
if (
asset.name === 'SHA256SUMS.txt' &&
asset.content_type === 'text/plain'
) {
hashUrl = asset.browser_download_url;
continue;
}
}
if (!downloadUrl) {
return;
}
state.pendingVRCXUpdate = true;
uiStore.notifyMenu('settings');
const type = 'Auto';
if (!watchState.isLoggedIn) {
showVRCXUpdateDialog();
} else if (state.autoUpdateVRCX === 'Notify') {
// this.showVRCXUpdateDialog();
} else if (state.autoUpdateVRCX === 'Auto Download') {
await downloadVRCXUpdate(
downloadUrl,
downloadName,
hashUrl,
size,
releaseName,
type
);
}
}
}
}
async function showVRCXUpdateDialog() {
const D = state.VRCXUpdateDialog;
D.visible = true;
D.updatePendingIsLatest = false;
D.updatePending = await AppApi.CheckForUpdateExe();
await loadBranchVersions();
}
async function loadBranchVersions() {
const D = state.VRCXUpdateDialog;
const url = branches[state.branch].urlReleases;
state.checkingForVRCXUpdate = true;
let response;
try {
response = await webApiService.execute({
url,
method: 'GET',
headers: {
'VRCX-ID': state.vrcxId
}
});
} finally {
state.checkingForVRCXUpdate = false;
}
const json = JSON.parse(response.data);
if (AppGlobal.debugWebRequests) {
console.log(json, response);
}
const releases = [];
if (typeof json !== 'object' || json.message) {
$app.$message({
message: t('message.vrcx_updater.failed', {
message: json.message
}),
type: 'error'
});
return;
}
for (const release of json) {
for (const asset of release.assets) {
if (
(asset.content_type === 'application/x-msdownload' ||
asset.content_type === 'application/x-msdos-program') &&
asset.state === 'uploaded'
) {
releases.push(release);
}
}
}
D.releases = releases;
D.release = json[0].name;
state.VRCXUpdateDialog.updatePendingIsLatest = false;
if (D.release === state.pendingVRCXInstall) {
// update already downloaded and latest version
state.VRCXUpdateDialog.updatePendingIsLatest = true;
}
setBranch(state.branch);
}
async function downloadVRCXUpdate(
downloadUrl,
downloadName,
hashUrl,
size,
releaseName,
type
) {
if (state.updateInProgress) {
return;
}
try {
state.updateInProgress = true;
await downloadFileProgress();
await AppApi.DownloadUpdate(
downloadUrl,
downloadName,
hashUrl,
size
);
state.pendingVRCXInstall = releaseName;
} catch (err) {
console.error(err);
$app.$message({
message: `${t('message.vrcx_updater.failed_install')} ${err}`,
type: 'error'
});
} finally {
state.updateInProgress = false;
state.updateProgress = 0;
}
}
async function downloadFileProgress() {
state.updateProgress = await AppApi.CheckUpdateProgress();
if (state.updateInProgress) {
workerTimers.setTimeout(() => downloadFileProgress(), 150);
}
}
function installVRCXUpdate() {
for (const release of state.VRCXUpdateDialog.releases) {
if (release.name !== state.VRCXUpdateDialog.release) {
continue;
}
let downloadUrl = '';
let downloadName = '';
let hashUrl = '';
let size = 0;
for (const asset of release.assets) {
if (asset.state !== 'uploaded') {
continue;
}
if (
WINDOWS &&
(asset.content_type === 'application/x-msdownload' ||
asset.content_type === 'application/x-msdos-program')
) {
downloadUrl = asset.browser_download_url;
downloadName = asset.name;
size = asset.size;
continue;
}
if (
LINUX &&
asset.content_type === 'application/octet-stream'
) {
downloadUrl = asset.browser_download_url;
downloadName = asset.name;
size = asset.size;
continue;
}
if (
asset.name === 'SHA256SUMS.txt' &&
asset.content_type === 'text/plain'
) {
hashUrl = asset.browser_download_url;
continue;
}
}
if (!downloadUrl) {
return;
}
const releaseName = release.name;
const type = 'Manual';
downloadVRCXUpdate(
downloadUrl,
downloadName,
hashUrl,
size,
releaseName,
type
);
break;
}
}
function showChangeLogDialog() {
state.changeLogDialog.visible = true;
checkForVRCXUpdate();
}
function restartVRCX(isUpgrade) {
if (!LINUX) {
AppApi.RestartApplication(isUpgrade);
} else {
window.electron.restartApp();
}
}
function updateProgressText() {
if (state.updateProgress === 100) {
return t('message.vrcx_updater.checking_hash');
}
return `${state.updateProgress}%`;
}
async function cancelUpdate() {
await AppApi.CancelUpdate();
state.updateInProgress = false;
state.updateProgress = 0;
}
initVRCXUpdaterSettings();
return {
state,
appVersion,
autoUpdateVRCX,
latestAppVersion,
branch,
currentVersion,
vrcxId,
checkingForVRCXUpdate,
VRCXUpdateDialog,
changeLogDialog,
pendingVRCXUpdate,
pendingVRCXInstall,
updateInProgress,
updateProgress,
setAutoUpdateVRCX,
setBranch,
compareAppVersion,
checkForVRCXUpdate,
loadBranchVersions,
installVRCXUpdate,
showVRCXUpdateDialog,
showChangeLogDialog,
restartVRCX,
updateProgressText,
cancelUpdate
};
});
+332
View File
@@ -0,0 +1,332 @@
import { defineStore } from 'pinia';
import { computed, reactive, watch } from 'vue';
import { instanceRequest, miscRequest, worldRequest } from '../api';
import { $app } from '../app';
import { database } from '../service/database';
import { watchState } from '../service/watchState';
import {
checkVRChatCache,
getAvailablePlatforms,
getBundleDateSize,
getWorldMemo,
isRealInstance,
parseLocation,
replaceBioSymbols
} from '../shared/utils';
import { useFavoriteStore } from './favorite';
import { useInstanceStore } from './instance';
import { useLocationStore } from './location';
import { useUserStore } from './user';
export const useWorldStore = defineStore('World', () => {
const locationStore = useLocationStore();
const favoriteStore = useFavoriteStore();
const instanceStore = useInstanceStore();
const userStore = useUserStore();
const state = reactive({
worldDialog: {
visible: false,
loading: false,
id: '',
memo: '',
$location: {},
ref: {},
isFavorite: false,
avatarScalingDisabled: false,
focusViewDisabled: false,
rooms: [],
treeData: [],
bundleSizes: [],
lastUpdated: '',
inCache: false,
cacheSize: 0,
cacheLocked: false,
cachePath: '',
lastVisit: '',
visitCount: 0,
timeSpent: 0,
isPC: false,
isQuest: false,
isIos: false,
hasPersistData: false
},
cachedWorlds: new Map()
});
const worldDialog = computed({
get: () => state.worldDialog,
set: (value) => {
state.worldDialog = value;
}
});
const cachedWorlds = computed({
get: () => state.cachedWorlds,
set: (value) => {
state.cachedWorlds = value;
}
});
watch(
() => watchState.isLoggedIn,
() => {
state.worldDialog.visible = false;
state.cachedWorlds.clear();
},
{ flush: 'sync' }
);
/**
* aka: `$app.methods.showWorldDialog`
* @param {string} tag
* @param {string} shortName
*/
function showWorldDialog(tag, shortName = null) {
const D = state.worldDialog;
const L = parseLocation(tag);
if (L.worldId === '') {
return;
}
L.shortName = shortName;
D.id = L.worldId;
D.$location = L;
D.treeData = [];
D.bundleSizes = [];
D.lastUpdated = '';
D.visible = true;
D.loading = true;
D.inCache = false;
D.cacheSize = 0;
D.cacheLocked = false;
D.rooms = [];
D.lastVisit = '';
D.visitCount = '';
D.timeSpent = 0;
D.isFavorite = false;
D.avatarScalingDisabled = false;
D.focusViewDisabled = false;
D.isPC = false;
D.isQuest = false;
D.isIos = false;
D.hasPersistData = false;
D.memo = '';
const LL = parseLocation(locationStore.lastLocation.location);
let currentWorldMatch = false;
if (LL.worldId === D.id) {
currentWorldMatch = true;
}
getWorldMemo(D.id).then((memo) => {
if (memo.worldId === D.id) {
D.memo = memo.memo;
}
});
database.getLastVisit(D.id, currentWorldMatch).then((ref) => {
if (ref.worldId === D.id) {
D.lastVisit = ref.created_at;
}
});
database.getVisitCount(D.id).then((ref) => {
if (ref.worldId === D.id) {
D.visitCount = ref.visitCount;
}
});
database.getTimeSpentInWorld(D.id).then((ref) => {
if (ref.worldId === D.id) {
D.timeSpent = ref.timeSpent;
}
});
worldRequest
.getCachedWorld({
worldId: L.worldId
})
.catch((err) => {
D.loading = false;
D.visible = false;
$app.$message({
message: 'Failed to load world',
type: 'error'
});
throw err;
})
.then((args) => {
if (D.id === args.ref.id) {
D.loading = false;
D.ref = args.ref;
D.isFavorite = favoriteStore.cachedFavoritesByObjectId.has(
D.id
);
if (!D.isFavorite) {
D.isFavorite =
favoriteStore.localWorldFavoritesList.includes(
D.id
);
}
let { isPC, isQuest, isIos } = getAvailablePlatforms(
args.ref.unityPackages
);
D.avatarScalingDisabled = args.ref?.tags.includes(
'feature_avatar_scaling_disabled'
);
D.focusViewDisabled = args.ref?.tags.includes(
'feature_focus_view_disabled'
);
D.isPC = isPC;
D.isQuest = isQuest;
D.isIos = isIos;
updateVRChatWorldCache();
miscRequest
.hasWorldPersistData({
worldId: D.id
})
.then((args) => {
if (
args.params.worldId === state.worldDialog.id &&
state.worldDialog.visible
) {
state.worldDialog.hasPersistData =
args.json !== false;
}
});
if (args.cache) {
worldRequest
.getWorld(args.params)
.catch((err) => {
throw err;
})
.then((args1) => {
if (D.id === args1.ref.id) {
D.ref = args1.ref;
updateVRChatWorldCache();
}
return args1;
});
}
}
return args;
});
}
function updateVRChatWorldCache() {
const D = state.worldDialog;
if (D.visible) {
D.inCache = false;
D.cacheSize = 0;
D.cacheLocked = false;
D.cachePath = '';
checkVRChatCache(D.ref).then((cacheInfo) => {
if (cacheInfo.Item1 > 0) {
D.inCache = true;
D.cacheSize = `${(cacheInfo.Item1 / 1048576).toFixed(2)} MB`;
D.cachePath = cacheInfo.Item3;
}
D.cacheLocked = cacheInfo.Item2;
});
}
}
/**
*
* @param {object} json
* @returns {object} ref
*/
function applyWorld(json) {
if (json.name) {
json.name = replaceBioSymbols(json.name);
}
if (json.description) {
json.description = replaceBioSymbols(json.description);
}
let ref = state.cachedWorlds.get(json.id);
if (typeof ref === 'undefined') {
ref = {
id: '',
name: '',
description: '',
defaultContentSettings: {},
authorId: '',
authorName: '',
capacity: 0,
recommendedCapacity: 0,
tags: [],
releaseStatus: '',
imageUrl: '',
thumbnailImageUrl: '',
assetUrl: '',
assetUrlObject: {},
pluginUrl: '',
pluginUrlObject: {},
unityPackageUrl: '',
unityPackageUrlObject: {},
unityPackages: [],
version: 0,
favorites: 0,
created_at: '',
updated_at: '',
publicationDate: '',
labsPublicationDate: '',
visits: 0,
popularity: 0,
heat: 0,
publicOccupants: 0,
privateOccupants: 0,
occupants: 0,
instances: [],
featured: false,
organization: '',
previewYoutubeId: '',
// VRCX
$isLabs: false,
//
...json
};
state.cachedWorlds.set(ref.id, ref);
} else {
Object.assign(ref, json);
}
ref.$isLabs = ref.tags.includes('system_labs');
favoriteStore.applyFavorite('world', ref.id);
const userDialog = userStore.userDialog;
if (userDialog.visible && userDialog.$location.worldId === ref.id) {
userStore.applyUserDialogLocation();
}
const worldDialog = state.worldDialog;
if (worldDialog.visible && worldDialog.id === ref.id) {
worldDialog.ref = ref;
worldDialog.avatarScalingDisabled = ref.tags?.includes(
'feature_avatar_scaling_disabled'
);
worldDialog.focusViewDisabled = ref.tags?.includes(
'feature_focus_view_disabled'
);
instanceStore.applyWorldDialogInstances();
for (const room of worldDialog.rooms) {
if (isRealInstance(room.tag)) {
instanceRequest.getInstance({
worldId: worldDialog.id,
instanceId: room.id
});
}
}
if (worldDialog.bundleSizes.length === 0) {
getBundleDateSize(ref).then((bundleSizes) => {
worldDialog.bundleSizes = bundleSizes;
});
}
}
if (favoriteStore.localWorldFavoritesList.includes(ref.id)) {
// update db cache
database.addWorldToCache(ref);
}
return ref;
}
return {
state,
worldDialog,
cachedWorlds,
showWorldDialog,
updateVRChatWorldCache,
applyWorld
};
});