Initial commit

This commit is contained in:
pypy
2019-08-16 22:53:04 +09:00
parent 52679ba15d
commit 90f096c497
52 changed files with 27151 additions and 0 deletions

67
html/.eslintrc.js Normal file
View File

@@ -0,0 +1,67 @@
module.exports = {
'env': {
'browser': true,
'es6': true,
'jquery': true
},
'extends': 'eslint:all',
'globals': {
'CefSharp': 'readonly',
'VRCX': 'readonly',
'VRCXStorage': 'readonly',
'LogWatcher': 'readonly',
'Discord': 'readonly',
'Noty': 'readonly',
'Vue': 'readonly',
'VueLazyload': 'readonly',
'DataTables': 'readonly',
'ELEMENT': 'readonly'
},
'parserOptions': {
'ecmaVersion': 9
},
'root': true,
'rules': {
'camelcase': 0,
'capitalized-comments': 0,
'complexity': 0,
'default-case': 0,
'func-names': 0,
'guard-for-in': 0,
'id-length': 0,
'indent': 0,
'init-declarations': 0,
'linebreak-style': 0,
'max-depth': 0,
'max-len': 0,
'max-lines': 0,
'max-lines-per-function': 0,
'max-statements': 0,
'multiline-comment-style': 0,
'newline-per-chained-call': 0,
'new-cap': 0,
'no-console': 0,
'no-empty': ['error', { 'allowEmptyCatch': true }],
'no-magic-numbers': 0,
'no-mixed-operators': 0,
'no-nested-ternary': 0,
'no-plusplus': 0,
'no-tabs': 0,
'no-ternary': 0,
'no-throw-literal': 0,
'no-undefined': 0,
'no-underscore-dangle': 0,
'no-var': 0,
'no-warning-comments': 0,
'object-curly-spacing': ['error', 'always'],
'one-var': 0,
'padded-blocks': 0,
'prefer-named-capture-group': 0,
'quotes': ['error', 'single', { 'avoidEscape': true }],
'quote-props': 0,
'sort-keys': 0,
'space-before-function-paren': ['error', { 'named': 'never' }],
'strict': 0,
'vars-on-top': 0
}
};

413
html/app.css Normal file
View File

@@ -0,0 +1,413 @@
@charset "utf-8";
/*
Copyright(c) 2019 pypy. All rights reserved.
This work is licensed under the terms of the MIT license.
For a copy, see <https://opensource.org/licenses/MIT>.
*/
.noty_layout {
word-break: break-all;
}
.noty_theme__mint.noty_bar {
margin: 4px 0;
overflow: hidden;
border-radius: 2px;
position: relative;
}
.noty_theme__mint.noty_bar .noty_body {
padding: 10px;
font-size: 14px;
}
.noty_theme__mint.noty_bar .noty_buttons {
padding: 10px;
}
.noty_theme__mint.noty_type__alert, .noty_theme__mint.noty_type__notification {
background-color: #fff;
border-bottom: 1px solid #D1D1D1;
color: #2F2F2F;
}
.noty_theme__mint.noty_type__warning {
background-color: #FFAE42;
border-bottom: 1px solid #E89F3C;
color: #fff;
}
.noty_theme__mint.noty_type__error {
background-color: #DE636F;
border-bottom: 1px solid #CA5A65;
color: #fff;
}
.noty_theme__mint.noty_type__info, .noty_theme__mint.noty_type__information {
background-color: #7F7EFF;
border-bottom: 1px solid #7473E8;
color: #fff;
}
.noty_theme__mint.noty_type__success {
background-color: #AFC765;
border-bottom: 1px solid #A0B55C;
color: #fff;
}
.el-table+.pagination-bar {
margin-top: 15px;
}
.el-dialog__body {
padding: 20px;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
border-radius: 16px;
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.25);
border-radius: 16px;
}
body, input, textarea, select, button {
font-family: 'Noto Sans JP', 'Noto Sans KR', 'Meiryo UI', 'Malgun Gothic', 'Segoe UI', sans-serif;
line-height: normal;
}
.x-link {
cursor: pointer;
}
.x-link:hover {
text-decoration: underline;
}
.x-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.x-app {
display: flex;
position: absolute;
width: 100%;
height: 100%;
overflow: hidden auto;
}
.x-container {
flex: 1;
padding: 10px;
overflow: hidden auto;
background: #fff;
position: relative;
}
.x-login-container {
display: flex;
position: absolute;
width: 100%;
height: 100%;
background: #fff;
/* modal 시작이 2000이라서 */
z-index: 1999;
}
.x-menu-container {
flex: none;
overflow: hidden auto;
background: #383838;
}
.x-menu-container>.el-menu {
background: 0;
border: 0;
}
.el-menu-item.notify::after {
position: absolute;
content: '';
right: 5px;
top: 5px;
width: 5px;
height: 5px;
background: #909399;
border-radius: 50%
}
.x-aside-container {
flex: none;
width: 236px;
display: flex;
flex-direction: column;
background: #f8f8f8;
}
.el-popper.x-quick-search {
min-width: 0 !important;
width: 225px;
}
.el-popper.x-quick-search .el-select-dropdown__item {
padding: 0 10px;
width: 100%;
height: auto;
font-size: 12px;
line-height: normal;
}
.x-friend-list {
overflow: hidden auto;
padding: 0 10px;
}
.x-friend-group>.el-icon-arrow-right {
transition: transform .3s;
}
.x-friend-group>.el-icon-arrow-right.rotate {
transform: rotate(90deg);
}
.x-aside-container .x-friend-list {
flex: 1;
}
.x-dialog .x-friend-list {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
max-height: 150px;
}
.x-friend-list>.x-friend-group {
padding: 20px 0 5px;
font-weight: bold;
font-size: 12px;
}
.x-friend-item {
display: flex;
align-items: center;
padding: 5px;
cursor: pointer;
font-size: 12px;
box-sizing: border-box;
}
.x-friend-item:hover {
background: #eee;
border-radius: 2px;
}
.x-aside-container .x-friend-item:hover {
background: #fff;
border-radius: 2px;
}
.el-select-dropdown__item .x-friend-item:hover {
background: none;
border-radius: 0;
}
.x-dialog .x-friend-item {
width: 175px;
}
.x-friend-item>.avatar {
flex: none;
width: 40px;
height: 40px;
margin-right: 8px;
display: inline-block;
position: relative;
}
.x-friend-item>img.avatar {
width: 50px;
height: 37.5px;
margin-left: 5px;
margin-right: 0;
border-radius: 2px;
}
.x-friend-item>.avatar>img {
width: 100%;
height: 100%;
border-radius: 40%;
object-fit: cover;
}
.x-friend-item>.avatar.offline>img {
filter: grayscale(1);
}
.x-friend-item:hover>.avatar.offline>img {
filter: none;
}
.x-friend-item>.avatar.active::after, .x-friend-item>.avatar.joinme::after, .x-friend-item>.avatar.busy::after {
content: '';
position: absolute;
right: 0;
bottom: 0;
width: 8px;
height: 8px;
border-radius: 50%;
border: 2px solid #fff;
background: #909399;
}
.x-friend-item>.avatar.active::after {
background: #67C23A;
}
.x-friend-item>.avatar.joinme::after {
background: #409EFF;
}
.x-friend-item>.avatar.busy::after {
background: #F56C6C;
}
.x-friend-item.offline>.avatar::after {
display: none;
}
.x-friend-item>.detail {
flex: 1;
overflow: hidden;
}
.x-friend-item>.detail>.name, .x-friend-item>.detail>.extra {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.x-friend-item>.detail>.name {
font-weight: bold;
}
.x-friend-item>.detail>.extra {
font-weight: normal;
}
.x-dialog .el-dialog {
margin-bottom: 10px;
max-width: 100%;
}
.x-user-dialog .el-dialog__header, .x-world-dialog .el-dialog__header, .x-avatar-dialog .el-dialog__header {
padding: 0;
display: none;
}
.x-user-dialog .el-dialog__body, .x-world-dialog .el-dialog__body, .x-avatar-dialog .el-dialog__body {
padding: 20px;
}
.el-popper.hex {
font-family: monospace;
text-align: center;
min-width: auto;
padding: 10px;
}
i.x-user-status {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background: gray;
}
i.x-user-status.active {
background: #67C23A;
}
i.x-user-status.joinme {
background: #409EFF;
}
i.x-user-status.busy {
background: #F56C6C;
}
.el-tag.x-tag-vip {
border-color: rgb(181, 38, 38);
color: rgb(181, 38, 38);
}
.el-tag.x-tag-friend {
/*border-color: rgb(255, 255, 0);
color: rgb(255, 255, 0);*/
border-color: rgb(255, 208, 0);
color: rgb(255, 208, 0);
}
.el-tag.x-tag-untrusted {
border-color: rgb(204, 204, 204);
color: rgb(204, 204, 204);
}
.el-tag.x-tag-basic {
border-color: rgb(23, 120, 255);
color: rgb(23, 120, 255);
}
.el-tag.x-tag-known {
border-color: rgb(43, 207, 92);
color: rgb(43, 207, 92);
}
.el-tag.x-tag-trusted {
border-color: rgb(255, 123, 66);
color: rgb(255, 123, 66);
}
.el-tag.x-tag-veteran {
border-color: rgb(129, 67, 230);
color: rgb(129, 67, 230);
}
.el-tag.x-tag-legend {
/*border-color: rgb(255, 255, 0);
color: rgb(255, 255, 0);*/
border-color: rgb(255, 208, 0);
color: rgb(255, 208, 0);
}
.el-tag.x-tag-troll {
border-color: rgb(120, 47, 47);
color: rgb(120, 47, 47);
}
.el-tag.x-tag-legendary {
border-color: rgb(0, 0, 0);
color: rgb(0, 0, 0);
}
.x-dialog .el-tree {
font-size: 12px;
}
.x-dialog .el-tree-node {
white-space: normal;
}
.x-dialog .el-tree-node__content {
height: auto;
}

6645
html/app.js Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

1722
html/index.html Normal file

File diff suppressed because it is too large Load Diff

222
html/vr.css Normal file
View File

@@ -0,0 +1,222 @@
@charset "utf-8";
/*
Copyright(c) 2019 pypy. All rights reserved.
This work is licensed under the terms of the MIT license.
For a copy, see <https://opensource.org/licenses/MIT>.
*/
/*
마지노선인듯
화면 24px -> 나나 32
손등 18px -> 나나 24
*/
.noty_body {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.noty_layout {
max-width: none;
width: 80% !important;
}
.noty_theme__relax.noty_bar {
margin: 4px 0;
overflow: hidden;
border-radius: 2px;
position: relative;
}
.noty_theme__relax.noty_bar .noty_body {
padding: 5px 10px 10px;
font-size: 24px;
text-align: center;
}
.noty_theme__relax.noty_bar .noty_buttons {
border-top: 1px solid #e7e7e7;
padding: 5px 10px;
}
.noty_theme__relax.noty_type__alert, .noty_theme__relax.noty_type__notification {
background-color: #fff;
border: 1px solid #dedede;
color: #444;
}
.noty_theme__relax.noty_type__warning {
background-color: #FFEAA8;
border: 1px solid #FFC237;
color: #826200;
}
.noty_theme__relax.noty_type__warning .noty_buttons {
border-color: #dfaa30;
}
.noty_theme__relax.noty_type__error {
background-color: #FF8181;
border: 1px solid #e25353;
color: #FFF;
}
.noty_theme__relax.noty_type__error .noty_buttons {
border-color: darkred;
}
.noty_theme__relax.noty_type__info, .noty_theme__relax.noty_type__information {
background-color: #78C5E7;
border: 1px solid #3badd6;
color: #FFF;
}
.noty_theme__relax.noty_type__info .noty_buttons, .noty_theme__relax.noty_type__information .noty_buttons {
border-color: #0B90C4;
}
.noty_theme__relax.noty_type__success {
background-color: #BCF5BC;
border: 1px solid #7cdd77;
color: darkgreen;
}
.noty_theme__relax.noty_type__success .noty_buttons {
border-color: #50C24E;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
border-radius: 16px;
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.25);
border-radius: 16px;
}
body, input, textarea, select, button {
font-family: 'Noto Sans JP', 'Noto Sans KR', 'Meiryo UI', 'Malgun Gothic', 'Segoe UI', sans-serif;
line-height: normal;
}
.x-app {
display: flex;
flex-direction: column;
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
}
.x-app-type {
background: #1f1f1f;
color: #fff;
}
.x-container {
flex: none;
padding: 10px;
overflow: hidden auto;
position: relative;
}
.x-friend-list {
overflow: hidden auto;
padding: 0 10px;
}
.x-friend-item {
display: flex;
align-items: center;
font-size: 18px;
box-sizing: border-box;
}
.x-friend-item .time {
margin-right: 5px;
}
.x-friend-item .name {
font-weight: bold;
}
.x-friend-item.friend .name {
color: #fff;
}
.x-friend-item.favorite .name {
color: #ff0;
}
.x-friend-item>.avatar {
flex: none;
width: 40px;
height: 40px;
margin-right: 8px;
display: inline-block;
position: relative;
}
.x-friend-item>img.avatar {
width: 50px;
height: 37.5px;
margin-left: 5px;
margin-right: 0;
border-radius: 2px;
}
.x-friend-item>.avatar>img {
width: 100%;
height: 100%;
border-radius: 40%;
object-fit: cover;
}
.x-friend-item>.detail {
flex: 1;
overflow: hidden;
}
.x-friend-item>.detail>.name, .x-friend-item>.detail>.extra {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.x-friend-item>.detail>.name {
font-weight: bold;
}
.x-friend-item>.detail>.extra {
font-weight: normal;
}
i.x-user-status {
display: inline-block;
width: 14px;
height: 14px;
border-radius: 50%;
background: gray;
}
i.x-user-status.active {
background: #67C23A;
}
i.x-user-status.joinme {
background: #409EFF;
}
i.x-user-status.busy {
background: #F56C6C;
}

144
html/vr.html Normal file
View File

@@ -0,0 +1,144 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<meta http-equiv="Cache-Control" content="no-cache">
<meta name="referrer" content="no-referrer">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>VRCXVR</title>
<link rel="dns-prefetch" href="https://fonts.gstatic.com">
<link rel="preconnect" href="https://api.vrchat.cloud">
<link rel="preconnect" href="https://d348imysud55la.cloudfront.net">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.7.2/animate.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/noty/3.2.0-beta/noty.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.11.1/theme-chalk/index.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Noto+Sans+JP|Noto+Sans+KR&display=swap">
</head>
<body>
<div id="x-app" class="x-app" :class="{ 'x-app-type': appType === '1' }" style="display:none">
<div class="x-container" style="flex:1">
<div ref="list" class="x-friend-list" style="color:#aaa">
<template v-for="feed in feeds">
<div v-if="feed.type === 'GPS'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }">
<div class="detail">
<span class="extra">
<span class="time">{{ feed.created_at | formatDate('HH:MI') }}</span>
<span class="name" v-text="feed.displayName"></span> is in <location :location="feed.location[0]"></location>
</span>
</div>
</div>
<div v-else-if="feed.type === 'Offline'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }">
<div class="detail">
<span class="extra">
<span class="time">{{ feed.created_at | formatDate('HH:MI') }}</span>
<span class="name" v-text="feed.displayName"></span> has logged out
</span>
</div>
</div>
<div v-else-if="feed.type === 'Online'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }">
<div class="detail">
<span class="extra">
<span class="time">{{ feed.created_at | formatDate('HH:MI') }}</span>
<span class="name" v-text="feed.displayName"></span> has logged in
</span>
</div>
</div>
<div v-else-if="feed.type === 'Status'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }">
<div class="detail">
<span class="extra">
<span class="time">{{ feed.created_at | formatDate('HH:MI') }}</span>
<span class="name" v-text="feed.displayName"></span> is <i class="x-user-status" :class="userStatusClass(feed.status[0])"></i> {{feed.status[0].statusDescription}}
</span>
</div>
</div>
<div v-else-if="feed.type === 'OnPlayerJoined'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }">
<div class="detail">
<span class="extra">
<span class="time">{{ feed.created_at | formatDate('HH:MI') }}</span>
<span class="name" v-text="feed.data"></span> has joined
</span>
</div>
</div>
<div v-else-if="feed.type === 'OnPlayerLeft'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }">
<div class="detail">
<span class="extra">
<span class="time">{{ feed.created_at | formatDate('HH:MI') }}</span>
<span class="name" v-text="feed.data"></span> has left
</span>
</div>
</div>
<div v-else-if="feed.type === 'Location'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }">
<div class="detail">
<span class="extra">
<span class="time">{{ feed.created_at | formatDate('HH:MI') }}</span>
<location :location="feed.data"></location>
</span>
</div>
</div>
</template>
</div>
</div>
<div class="x-container">
<div style="display:flex;flex-direction:row">
<template v-if="devices.length">
<div v-for="device in devices" style="flex:none;text-align:center;width:64px">
<template v-if="device[0] === 'tracker'">
<img v-if="device[1] !== 'connected'" src="images/tracker_status_off.png" style="width:32px;height:32px">
<img v-else-if="device[2] < 20" src="images/tracker_status_ready_low.png" style="width:32px;height:32px">
<img v-else src="images/tracker_status_ready.png" style="width:32px;height:32px">
</template>
<template v-else-if="device[0] === 'leftController'">
<img v-if="device[1] !== 'connected'" src="images/controller_status_off.png" style="width:32px;height:32px">
<img v-else-if="device[2] < 20" src="images/controller_status_ready_low.png" style="width:32px;height:32px">
<img v-else src="images/controller_status_ready.png" style="width:32px;height:32px">
</template>
<template v-else-if="device[0] === 'rightController'">
<img v-if="device[1] !== 'connected'" src="images/controller_status_off.png" style="width:32px;height:32px">
<img v-else-if="device[2] < 20" src="images/controller_status_ready_low.png" style="width:32px;height:32px">
<img v-else src="images/controller_status_ready.png" style="width:32px;height:32px">
</template>
<template v-else-if="device[0] === 'controller'">
<img v-if="device[1] !== 'connected'" src="images/controller_status_off.png" style="width:32px;height:32px">
<img v-else-if="device[2] < 20" src="images/controller_status_ready_low.png" style="width:32px;height:32px">
<img v-else src="images/controller_status_ready.png" style="width:32px;height:32px">
</template>
<template v-else>
<img v-if="device[1] !== 'connected'" src="images/other_status_off.png" style="width:32px;height:32px">
<img v-else-if="device[2] < 20" src="images/other_status_ready_low.png" style="width:32px;height:32px">
<img v-else src="images/other_status_ready.png" style="width:32px;height:32px">
</template>
<br><span>{{ device[2] }}%</span>
</div>
</template>
<div v-else>
<span>No SteamVR Devices</span>
</div>
</div>
</div>
<div class="x-container">
<span style="float:right">{{ currentTime | formatDate('YYYY-MM-DD HH:MI:SS AMPM') }}</span>
<span>CPU {{ cpuUsage }}%</span>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/noty/3.2.0-beta/noty.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.11.1/index.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.11.1/locale/en.min.js"></script>
<script>
(() => {
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = `vr.css?_=${Date.now()}`;
document.getElementsByTagName('head')[0].appendChild(link);
var script = document.createElement('script');
script.src = `vr.js?_=${Date.now()}`;
document.getElementsByTagName('body')[0].appendChild(script);
})();
</script>
</body>
</html>

726
html/vr.js Normal file
View File

@@ -0,0 +1,726 @@
// Copyright(c) 2019 pypy. All rights reserved.
//
// This work is licensed under the terms of the MIT license.
// For a copy, see <https://opensource.org/licenses/MIT>.
if (window.CefSharp) {
Promise.all([
CefSharp.BindObjectAsync('VRCX'),
CefSharp.BindObjectAsync('VRCXStorage')
]).catch(() => {
location = 'https://github.com/pypy-vrc/vrcx';
}).then(() => {
VRCXStorage.GetBool = function (key) {
return this.Get(key) === 'true';
};
VRCXStorage.SetBool = function (key, value) {
this.Set(key, value
? 'true'
: 'false');
};
VRCXStorage.GetInt = function (key) {
return parseInt(this.Get(key), 10) || 0;
};
VRCXStorage.SetInt = function (key, value) {
this.Set(key, String(value));
};
VRCXStorage.GetFloat = function (key) {
return parseFloat(this.Get(key), 10) || 0.0;
};
VRCXStorage.SetFloat = function (key, value) {
this.Set(key, String(value));
};
VRCXStorage.GetArray = function (key) {
try {
var json = this.Get(key);
if (json) {
var array = JSON.parse(json);
if (Array.isArray(array)) {
return array;
}
}
} catch (err) {
console.error(err);
}
return [];
};
VRCXStorage.SetArray = function (key, value) {
this.Set(key, JSON.stringify(value));
};
VRCXStorage.GetObject = function (key) {
try {
var json = this.Get(key);
if (json) {
return JSON.parse(json);
}
} catch (err) {
console.error(err);
}
return {};
};
VRCXStorage.SetObject = function (key, value) {
this.Set(key, JSON.stringify(value));
};
Noty.overrideDefaults({
animation: {
open: 'animated fadeIn',
close: 'animated zoomOut'
},
layout: 'topCenter',
theme: 'relax',
timeout: 6000
});
var escapeTag = (s) => String(s).replace(/["&'<>]/gu, (c) => `&#${c.charCodeAt(0)};`);
Vue.filter('escapeTag', escapeTag);
var commaNumber = (n) => String(Number(n) || 0).replace(/(\d)(?=(\d{3})+(?!\d))/gu, '$1,');
Vue.filter('commaNumber', commaNumber);
var formatDate = (s, format) => {
var ctx = new Date(s);
if (isNaN(ctx)) {
return escapeTag(s);
}
var hours = ctx.getHours();
var map = {
'YYYY': String(10000 + ctx.getFullYear()).substr(-4),
'MM': String(101 + ctx.getMonth()).substr(-2),
'DD': String(100 + ctx.getDate()).substr(-2),
'HH24': String(100 + hours).substr(-2),
'HH': String(100 + (hours > 12
? hours - 12
: hours)).substr(-2),
'MI': String(100 + ctx.getMinutes()).substr(-2),
'SS': String(100 + ctx.getSeconds()).substr(-2),
'AMPM': hours >= 12
? 'PM'
: 'AM'
};
return format.replace(/YYYY|MM|DD|HH24|HH|MI|SS|AMPM/gu, (c) => map[c] || c);
};
Vue.filter('formatDate', formatDate);
var textToHex = (s) => String(s).split('').map((c) => c.charCodeAt(0).toString(16)).join(' ');
Vue.filter('textToHex', textToHex);
var timeToText = (t) => {
var sec = Number(t);
if (isNaN(sec)) {
return escapeTag(t);
}
sec = Math.floor(sec / 1000);
var arr = [];
if (sec < 0) {
sec = -sec;
}
if (sec >= 86400) {
arr.push(`${Math.floor(sec / 86400)}d`);
sec %= 86400;
}
if (sec >= 3600) {
arr.push(`${Math.floor(sec / 3600)}h`);
sec %= 3600;
}
if (sec >= 60) {
arr.push(`${Math.floor(sec / 60)}m`);
sec %= 60;
}
if (sec ||
!arr.length) {
arr.push(`${sec}s`);
}
return arr.join(' ');
};
Vue.filter('timeToText', timeToText);
ELEMENT.locale(ELEMENT.lang.en);
//
// API
//
var API = {};
API.$handler = {};
API.$emit = function (event, ...args) {
try {
// console.log(event, ...args);
var h = this.$handler[event];
if (h) {
h.forEach((f) => f(...args));
}
} catch (err) {
console.error(err);
}
};
API.$on = function (event, callback) {
var h = this.$handler[event];
if (h) {
h.push(callback);
} else {
this.$handler[event] = [callback];
}
};
API.$off = function (event, callback) {
var h = this.$handler[event];
if (h) {
h.find((val, idx, arr) => {
if (val !== callback) {
return false;
}
if (arr.length > 1) {
arr.splice(idx, 1);
} else {
delete this.$handler[event];
}
return true;
});
}
};
API.$fetch = {};
API.call = function (endpoint, options) {
var input = `https://api.vrchat.cloud/api/1/${endpoint}`;
var init = {
method: 'GET',
mode: 'cors',
credentials: 'include',
cache: 'no-cache',
referrerPolicy: 'no-referrer',
...options
};
if (init.method === 'GET') {
if (init.body) {
var url = new URL(input);
for (var key in init.body) {
url.searchParams.set(key, init.body[key]);
}
input = url.toString();
init.body = null;
}
// merge requests
if (this.$fetch[input]) {
return this.$fetch[input];
}
} else {
init.headers = {
'Content-Type': 'application/json;charset=utf-8',
...init.headers
};
init.body = init.body
? JSON.stringify(init.body)
: '{}';
}
var req = fetch(input, init).catch((err) => {
this.$throw(0, err);
}).then((res) => res.json().catch(() => {
if (!res.ok) {
this.$throw(res.status);
}
this.$throw(0, 'Invalid JSON');
}).then((json) => {
if (!res.ok) {
if (typeof json.error === 'object') {
this.$throw(
json.error.status_code || res.status,
json.error.message,
json.error.data
);
} else if (typeof json.error === 'string') {
this.$throw(
json.status_code || res.status,
json.error
);
} else {
this.$throw(res.status, json);
}
}
return json;
}));
if (init.method === 'GET') {
this.$fetch[input] = req.finally(() => {
delete this.$fetch[input];
});
}
return req;
};
API.$status = {
100: 'Continue',
101: 'Switching Protocols',
102: 'Processing',
103: 'Early Hints',
200: 'OK',
201: 'Created',
202: 'Accepted',
203: 'Non-Authoritative Information',
204: 'No Content',
205: 'Reset Content',
206: 'Partial Content',
207: 'Multi-Status',
208: 'Already Reported',
226: 'IM Used',
300: 'Multiple Choices',
301: 'Moved Permanently',
302: 'Found',
303: 'See Other',
304: 'Not Modified',
305: 'Use Proxy',
306: 'Switch Proxy',
307: 'Temporary Redirect',
308: 'Permanent Redirect',
400: 'Bad Request',
401: 'Unauthorized',
402: 'Payment Required',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
406: 'Not Acceptable',
407: 'Proxy Authentication Required',
408: 'Request Timeout',
409: 'Conflict',
410: 'Gone',
411: 'Length Required',
412: 'Precondition Failed',
413: 'Payload Too Large',
414: 'URI Too Long',
415: 'Unsupported Media Type',
416: 'Range Not Satisfiable',
417: 'Expectation Failed',
418: "I'm a teapot",
421: 'Misdirected Request',
422: 'Unprocessable Entity',
423: 'Locked',
424: 'Failed Dependency',
425: 'Too Early',
426: 'Upgrade Required',
428: 'Precondition Required',
429: 'Too Many Requests',
431: 'Request Header Fields Too Large',
451: 'Unavailable For Legal Reasons',
500: 'Internal Server Error',
501: 'Not Implemented',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Timeout',
505: 'HTTP Version Not Supported',
506: 'Variant Also Negotiates',
507: 'Insufficient Storage',
508: 'Loop Detected',
510: 'Not Extended',
511: 'Network Authentication Required',
// CloudFlare Error
520: 'Web server returns an unknown error',
521: 'Web server is down',
522: 'Connection timed out',
523: 'Origin is unreachable',
524: 'A timeout occurred',
525: 'SSL handshake failed',
526: 'Invalid SSL certificate',
527: 'Railgun Listener to origin error'
};
API.$throw = function (code, error) {
throw {
'status_code': code,
error
};
};
// API: Config
API.config = {};
API.$on('CONFIG', (args) => {
args.ref = API.updateConfig(args.json);
});
API.getConfig = function () {
return this.call('config', {
method: 'GET'
}).then((json) => {
var args = {
json
};
this.$emit('CONFIG', args);
return args;
});
};
API.updateConfig = function (ref) {
var ctx = {
clientApiKey: '',
...ref
};
this.config = ctx;
return ctx;
};
// API: Location
API.parseLocation = function (tag) {
var L = {
tag: String(tag || ''),
isOffline: false,
isPrivate: false,
worldId: '',
instanceId: '',
instanceName: '',
accessType: '',
userId: null,
hiddenId: null,
privateId: null,
friendsId: null,
canRequestInvite: false
};
if (L.tag === 'offline') {
L.isOffline = true;
} else if (L.tag === 'private') {
L.isPrivate = true;
} else if (!L.tag.startsWith('local')) {
var sep = L.tag.indexOf(':');
if (sep >= 0) {
L.worldId = L.tag.substr(0, sep);
L.instanceId = L.tag.substr(sep + 1);
L.instanceId.split('~').forEach((s, i) => {
if (i) {
var A = s.indexOf('(');
var Z = A >= 0
? s.lastIndexOf(')')
: -1;
var key = Z >= 0
? s.substr(0, A)
: s;
var value = A < Z
? s.substr(A + 1, Z - A - 1)
: '';
if (key === 'hidden') {
L.hiddenId = value;
} else if (key === 'private') {
L.privateId = value;
} else if (key === 'friends') {
L.friendsId = value;
} else if (key === 'canRequestInvite') {
L.canRequestInvite = true;
}
} else {
L.instanceName = s;
}
});
L.accessType = 'public';
if (L.privateId !== null) {
if (L.canRequestInvite) {
// InvitePlus
L.accessType = 'invite+';
} else {
// InviteOnly
L.accessType = 'invite';
}
L.userId = L.privateId;
} else if (L.friendsId !== null) {
// FriendsOnly
L.accessType = 'friends';
L.userId = L.friendsId;
} else if (L.hiddenId !== null) {
// FriendsOfGuests
L.accessType = 'friends+';
L.userId = L.hiddenId;
}
} else {
L.worldId = L.tag;
}
}
return L;
};
Vue.component('location', {
template: '<span>{{ text }}<slot></slot></span>',
props: {
location: String
},
data() {
return {
text: this.location
};
},
methods: {
parse() {
var L = API.parseLocation(this.location);
if (L.isOffline) {
this.text = 'Offline';
} else if (L.isPrivate) {
this.text = 'Private';
} else if (L.worldId) {
var ref = API.world[L.worldId];
if (ref) {
this.text = `${ref.name} #${L.instanceName} ${L.accessType}`;
} else {
API.getWorld({
worldId: L.worldId
}).then((args) => {
if (L.tag === this.location) {
this.text = `${args.ref.name} #${L.instanceName} ${L.accessType}`;
}
return args;
});
}
}
}
},
watch: {
location() {
this.parse();
}
},
created() {
this.parse();
}
});
// API: World
API.world = {};
API.$on('WORLD', (args) => {
args.ref = API.updateWorld(args.json);
});
/*
param: {
worldId: string
}
*/
API.getWorld = function (param) {
return this.call(`worlds/${param.worldId}?apiKey=${this.config.clientApiKey}`, {
method: 'GET'
}).then((json) => {
var args = {
param,
json
};
this.$emit('WORLD', args);
return args;
});
};
API.updateWorld = function (ref) {
var ctx = this.world[ref.id];
if (ctx) {
Object.assign(ctx, ref);
} else {
ctx = {
id: ref.id,
name: '',
description: '',
authorId: '',
authorName: '',
capacity: 0,
tags: [],
releaseStatus: '',
imageUrl: '',
thumbnailImageUrl: '',
assetUrl: '',
assetUrlObject: {},
pluginUrl: '',
pluginUrlObject: {},
unityPackageUrl: '',
unityPackageUrlObject: {},
unityPackages: [],
version: 0,
previewYoutubeId: '',
favorites: 0,
created_at: '',
updated_at: '',
publicationDate: '',
labsPublicationDate: '',
visits: 0,
popularity: 0,
heat: 0,
publicOccupants: 0,
privateOccupants: 0,
occupants: 0,
instances: [],
// custom
labs_: false,
//
...ref
};
this.world[ctx.id] = ctx;
}
if (ctx.tags) {
ctx.labs_ = ctx.tags.includes('system_labs');
}
return ctx;
};
var $app = {
data: {
API,
VRCX,
// 1 = 대시보드랑 손목에 보이는거
// 2 = 항상 화면에 보이는 거
appType: location.href.substr(-1),
currentTime: new Date().toJSON(),
cpuUsage: 0,
feeds: [],
devices: []
},
computed: {},
methods: {},
watch: {},
el: '#x-app',
mounted() {
// https://media.discordapp.net/attachments/581757976625283083/611170278218924033/unknown.png
// 현재 날짜 시간
// 컨트롤러 배터리 상황
// --
// OO is Let's Just H!!!!! [GPS]
// OO has logged in [Online]
// OO has logged out [Offline]
// OO has joined [OnPlayerJoined]
// OO has left [OnPlayerLeft]
// [Moderation]
// OO has blocked you
// OO has muted you
// OO has hidden you
// --
API.getConfig().catch((err) => {
// FIXME: 어케 복구하냐 이건
throw err;
}).then((args) => {
setInterval(() => this.update(), 1000);
this.update();
this.$nextTick(() => {
if (this.appType === '1') {
this.$el.style.display = '';
}
});
return args;
});
}
};
$app.methods.update = function () {
this.currentTime = new Date().toJSON();
VRCX.CpuUsage().then((cpuUsage) => {
this.cpuUsage = cpuUsage.toFixed(2);
});
VRCX.GetVRDevices().then((devices) => {
devices.forEach((device) => {
device[2] = parseInt(device[2], 10);
});
this.devices = devices;
});
this.updateSharedFeed();
};
$app.methods.updateSharedFeed = function () {
// TODO: block mute hideAvatar unfriend
var _feeds = this.feeds;
this.feeds = VRCXStorage.GetArray('sharedFeeds');
if (this.appType === '2') {
var map = {};
_feeds.forEach((feed) => {
if (feed.isFavorite) {
if (feed.type === 'OnPlayerJoined' ||
feed.type === 'OnPlayerLeft') {
if (!map[feed.data] ||
map[feed.data] < feed.created_at) {
map[feed.data] = feed.created_at;
}
} else if (feed.type === 'Online' ||
feed.type === 'Offline') {
if (!map[feed.displayName] ||
map[feed.displayName] < feed.created_at) {
map[feed.displayName] = feed.created_at;
}
}
}
});
var notys = [];
this.feeds.forEach((feed) => {
if (feed.isFavorite) {
if (feed.type === 'Online' ||
feed.type === 'Offline') {
if (!map[feed.displayName] ||
map[feed.displayName] < feed.created_at) {
map[feed.displayName] = feed.created_at;
notys.push(feed);
}
} else if (feed.type === 'OnPlayerJoined' ||
feed.type === 'OnPlayerLeft') {
if (!map[feed.data] ||
map[feed.data] < feed.created_at) {
map[feed.data] = feed.created_at;
notys.push(feed);
}
}
}
});
var bias = new Date(Date.now() - 60000).toJSON();
notys.forEach((noty) => {
if (noty.created_at > bias) {
if (noty.type === 'OnPlayerJoined') {
new Noty({
type: 'alert',
text: `<strong>${noty.data}</strong> has joined`
}).show();
} else if (noty.type === 'OnPlayerLeft') {
new Noty({
type: 'alert',
text: `<strong>${noty.data}</strong> has left`
}).show();
} else if (noty.type === 'Online') {
new Noty({
type: 'alert',
text: `<strong>${noty.displayName}</strong> has logged in`
}).show();
} else if (noty.type === 'Offline') {
new Noty({
type: 'alert',
text: `<strong>${noty.displayName}</strong> has logged out`
}).show();
}
}
});
}
};
$app.methods.userStatusClass = function (user) {
var style = {};
if (user) {
if (user.location === 'offline') {
style.offline = true;
} else if (user.status === 'active') {
style.active = true;
} else if (user.status === 'join me') {
style.joinme = true;
} else if (user.status === 'busy') {
style.busy = true;
}
}
return style;
};
$app = new Vue($app);
window.$app = $app;
});
} else {
location = 'https://github.com/pypy-vrc/vrcx';
}