mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-05 14:26:06 +02:00
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:
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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
@@ -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
|
||||
};
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -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 };
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user