Massive commit that will break everything!

This commit is contained in:
Natsumi
2021-08-30 12:05:42 +12:00
parent 1eda658158
commit 2418d902cf
10 changed files with 1252 additions and 1212 deletions

View File

@@ -314,13 +314,28 @@ namespace VRCX
System.Environment.Exit(0);
}
public bool checkForUpdateZip()
public bool CheckForUpdateZip()
{
if (File.Exists(Path.Combine(Program.AppDataDirectory, "update.exe")))
return true;
return false;
}
public void ExecuteAppFunction(string function, string json)
{
MainForm.Instance.Browser.ExecuteScriptAsync($"$app.{function}", json);
}
public void ExecuteVrFeedFunction(string function, string json)
{
VRCXVR._browser1.ExecuteScriptAsync($"$app.{function}", json);
}
public void ExecuteVrOverlayFunction(string function, string json)
{
VRCXVR._browser2.ExecuteScriptAsync($"$app.{function}", json);
}
public void SetStartup(bool enabled)
{
try

View File

@@ -3,6 +3,7 @@
// This work is licensed under the terms of the MIT license.
// For a copy, see <https://opensource.org/licenses/MIT>.
using CefSharp;
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -30,6 +31,8 @@ namespace VRCX
private readonly List<string[]> m_LogList;
private Thread m_Thread;
private bool m_ResetLog;
private bool m_FirstRun = true;
private static DateTime tillDate = DateTime.Now;
// NOTE
// FileSystemWatcher() is unreliable
@@ -65,6 +68,17 @@ namespace VRCX
thread.Join();
}
public void Reset()
{
m_ResetLog = true;
m_Thread?.Interrupt();
}
public void SetDateTill(string date)
{
tillDate = DateTime.Parse(date);
}
private void ThreadLoop()
{
while (m_Thread != null)
@@ -86,6 +100,7 @@ namespace VRCX
{
if (m_ResetLog == true)
{
m_FirstRun = true;
m_ResetLog = false;
m_LogContextMap.Clear();
m_LogListLock.EnterWriteLock();
@@ -109,26 +124,17 @@ namespace VRCX
// sort by creation time
Array.Sort(fileInfos, (a, b) => a.CreationTimeUtc.CompareTo(b.CreationTimeUtc));
var utcNow = DateTime.UtcNow;
var minLimitDateTime = utcNow.AddDays(-7d);
var minRefreshDateTime = utcNow.AddMinutes(-3d);
foreach (var fileInfo in fileInfos)
{
var lastWriteTimeUtc = fileInfo.LastWriteTimeUtc;
if (lastWriteTimeUtc < minLimitDateTime)
{
continue;
}
if (lastWriteTimeUtc >= minRefreshDateTime)
{
fileInfo.Refresh();
if (fileInfo.Exists == false)
{
continue;
}
if (DateTime.Compare(fileInfo.LastWriteTime, tillDate) < 0)
{
continue;
}
if (m_LogContextMap.TryGetValue(fileInfo.Name, out LogContext logContext) == true)
@@ -155,6 +161,8 @@ namespace VRCX
{
m_LogContextMap.Remove(name);
}
m_FirstRun = false;
}
private void ParseLog(FileInfo fileInfo, LogContext logContext)
@@ -184,6 +192,20 @@ namespace VRCX
continue;
}
if (DateTime.TryParseExact(
line.Substring(0, 19),
"yyyy.MM.dd HH:mm:ss",
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeLocal,
out DateTime lineDate
))
{
if (DateTime.Compare(lineDate, tillDate) <= 0)
{
continue;
}
}
var offset = 34;
if (line[offset] == '[')
{
@@ -194,7 +216,8 @@ namespace VRCX
ParseLogJoinBlocked(fileInfo, logContext, line, offset) == true ||
ParseLogAvatarPedestalChange(fileInfo, logContext, line, offset) == true ||
ParseLogVideoError(fileInfo, logContext, line, offset) == true ||
ParseLogVideoPlay(fileInfo, logContext, line, offset) == true)
ParseLogVideoPlay(fileInfo, logContext, line, offset) == true ||
ParseLogWorldVRCX(fileInfo, logContext, line, offset) == true)
{
continue;
}
@@ -221,6 +244,12 @@ namespace VRCX
m_LogListLock.EnterWriteLock();
try
{
if (!m_FirstRun)
{
var logLine = System.Text.Json.JsonSerializer.Serialize(item);
if (MainForm.Instance != null && MainForm.Instance.Browser != null)
MainForm.Instance.Browser.ExecuteScriptAsync("$app.addGameLogEvent", logLine);
}
m_LogList.Add(item);
}
finally
@@ -244,7 +273,7 @@ namespace VRCX
dt = DateTime.UtcNow;
}
return $"{dt:yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'}";
return $"{dt:yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'}";
}
private bool ParseLogLocation(FileInfo fileInfo, LogContext logContext, string line, int offset)
@@ -515,6 +544,28 @@ namespace VRCX
return true;
}
private bool ParseLogWorldVRCX(FileInfo fileInfo, LogContext logContext, string line, int offset)
{
// [VRCX] VideoPlay(PyPyDance) "https://jd.pypy.moe/api/v1/videos/-Q3pdlsQxOk.mp4",0.5338666,260.6938,"1339 : Le Freak (Random)"
if (string.Compare(line, offset, "[VRCX] ", 0, 7, StringComparison.Ordinal) == 0)
{
var data = line.Substring(offset + 7);
AppendLog(new[]
{
fileInfo.Name,
ConvertLogTimeToISO8601(line),
"vrcx",
data
});
return true;
}
return false;
}
private bool ParseLogSDK2VideoPlay(FileInfo fileInfo, LogContext logContext, string line, int offset)
{
// 2021.04.23 13:12:25 Log - User Natsumi-sama added URL https://www.youtube.com/watch?v=dQw4w9WgXcQ
@@ -579,14 +630,10 @@ namespace VRCX
return true;
}
public void Reset()
{
m_ResetLog = true;
m_Thread?.Interrupt();
}
public string[][] Get()
{
Update();
if (m_ResetLog == false &&
m_LogList.Count > 0)
{

View File

@@ -34,8 +34,8 @@ namespace VRCX
private Device _device;
private Texture2D _texture1;
private Texture2D _texture2;
private OffScreenBrowser _browser1;
private OffScreenBrowser _browser2;
public static OffScreenBrowser _browser1;
public static OffScreenBrowser _browser2;
private bool _active;
static VRCXVR()

File diff suppressed because it is too large Load Diff

View File

@@ -185,10 +185,10 @@ html
template(#tool)
div(style="margin:0 0 10px;display:flex;align-items:center")
el-select(v-model="gameLogTable.filters[0].value" @change="saveTableFilters" multiple clearable collapse-tags style="flex:1" placeholder="Filter")
el-option(v-once v-for="type in ['Location', 'OnPlayerJoined', 'OnPlayerLeft', 'PortalSpawn', 'Event', 'VideoPlay']" :key="type" :label="type" :value="type")
el-option(v-once v-for="type in ['Location', 'OnPlayerJoined', 'OnPlayerLeft', 'PortalSpawn', 'AvatarChange', 'Event', 'VideoPlay']" :key="type" :label="type" :value="type")
el-input(v-model="gameLogTable.filters[1].value" placeholder="Search" style="flex:none;width:150px;margin:0 10px")
el-tooltip(placement="bottom" content="Reset game log" :disabled="hideTooltips")
el-button(type="default" @click="resetGameLog()" icon="el-icon-refresh" circle style="flex:none")
el-button(type="default" @click="refreshGameLog()" icon="el-icon-refresh" circle style="flex:none")
el-table-column(label="Date" prop="created_at" sortable="custom" width="90")
template(v-once #default="scope")
el-tooltip(placement="right")
@@ -196,17 +196,28 @@ html
span {{ scope.row.created_at | formatDate('YYYY-MM-DD HH24:MI:SS') }}
span {{ scope.row.created_at | formatDate('MM-DD HH24:MI') }}
el-table-column(label="Type" prop="type" width="120")
template(v-once #default="scope")
span.x-link(v-if="scope.row.location && scope.row.type !== 'Location'" v-text="scope.row.type" @click="showWorldDialog(scope.row.location)")
span(v-else v-text="scope.row.type")
el-table-column(label="User" prop="displayName" width="160")
template(v-once #default="scope")
span.x-link(v-text="scope.row.displayName" @click="lookupUser(scope.row)")
el-table-column(label="Detail" prop="data")
template(v-once #default="scope")
location(v-if="scope.row.type === 'Location'" :location="scope.row.data[0]" :hint="scope.row.data[1]")
location(v-if="scope.row.type === 'Location'" :location="scope.row.location" :hint="scope.row.worldName")
location(v-else-if="scope.row.type === 'PortalSpawn'" :location="scope.row.instanceId" :hint="scope.row.worldName")
template(v-else-if="scope.row.type === 'AvatarChange'")
span.x-link(@click="showUserDialog(scope.row.authorId)" v-text="scope.row.name")
template(v-if="scope.row.description && scope.row.name !== scope.row.description")
| - {{ scope.row.description }}
template(v-else-if="scope.row.type === 'Event'")
span(v-text="scope.row.data")
template(v-else-if="scope.row.type === 'VideoPlay'")
span.x-link(v-text="scope.row.data" @click="openExternalLink(scope.row.data)")
template(v-if="scope.row.displayName")
span.x-link(@click="lookupUser(scope.row.displayName)") ({{ scope.row.displayName }})
template(v-else-if="scope.row.type === 'Notification'")
span.x-link(v-else v-text="scope.row.data" @click="lookupUser(scope.row.data)")
span(v-if="scope.row.videoId") {{ scope.row.videoId }}:
span.x-link(v-if="scope.row.videoName" @click="openExternalLink(scope.row.videoUrl)" v-text="scope.row.videoName")
span.x-link(v-else @click="openExternalLink(scope.row.videoUrl)" v-text="scope.row.videoUrl")
template(v-else-if="scope.row.type === 'Notification' || scope.row.type === 'OnPlayerJoined' || scope.row.type === 'OnPlayerLeft'")
span.x-link(v-else v-text="scope.row.data")
//- search
.x-container(v-show="$refs.menu && $refs.menu.activeIndex === 'search'")
@@ -1924,15 +1935,22 @@ html
el-radio-button(label="Friends")
el-radio-button(label="Everyone")
.toggle-item
span.toggle-name Events
el-radio-group(v-model="sharedFeedFilters.noty.Event" size="mini")
span.toggle-name Avatar Change
el-radio-group(v-model="sharedFeedFilters.noty.AvatarChange" size="mini")
el-radio-button(label="Off")
el-radio-button(label="On")
el-radio-button(label="VIP")
el-radio-button(label="Friends")
el-radio-button(label="Everyone")
.toggle-item
span.toggle-name Video Play
el-radio-group(v-model="sharedFeedFilters.noty.VideoPlay" size="mini")
el-radio-button(label="Off")
el-radio-button(label="On")
.toggle-item
span.toggle-name Events
el-radio-group(v-model="sharedFeedFilters.noty.Event" size="mini")
el-radio-button(label="Off")
el-radio-button(label="On")
.toggle-item
span.toggle-name Blocked Player Joins
el-radio-group(v-model="sharedFeedFilters.noty.BlockedOnPlayerJoined" size="mini")
@@ -2076,13 +2094,20 @@ html
el-radio-button(label="Friends")
el-radio-button(label="Everyone")
.toggle-item
span.toggle-name Events
el-radio-group(v-model="sharedFeedFilters.wrist.Event" size="mini")
span.toggle-name Avatar Change
el-radio-group(v-model="sharedFeedFilters.wrist.AvatarChange" size="mini")
el-radio-button(label="Off")
el-radio-button(label="VIP")
el-radio-button(label="Friends")
el-radio-button(label="Everyone")
.toggle-item
span.toggle-name Video Play
el-radio-group(v-model="sharedFeedFilters.wrist.VideoPlay" size="mini")
el-radio-button(label="Off")
el-radio-button(label="On")
.toggle-item
span.toggle-name Video Play
el-radio-group(v-model="sharedFeedFilters.noty.VideoPlay" size="mini")
span.toggle-name Events
el-radio-group(v-model="sharedFeedFilters.wrist.Event" size="mini")
el-radio-button(label="Off")
el-radio-button(label="On")
.toggle-item

View File

@@ -1,7 +1,7 @@
import sqliteService from '../service/sqlite.js';
class Database {
async init(userId) {
async initUserTables(userId) {
Database.userId = userId.replaceAll('-', '').replaceAll('_', '');
await sqliteService.executeNonQuery(
`CREATE TABLE IF NOT EXISTS ${Database.userId}_feed_gps (id INTEGER PRIMARY KEY, created_at TEXT, user_id TEXT, display_name TEXT, location TEXT, world_name TEXT, previous_location TEXT, time INTEGER)`
@@ -26,6 +26,24 @@ class Database {
);
}
async initTables() {
await sqliteService.executeNonQuery(
`CREATE TABLE IF NOT EXISTS gamelog_location (id INTEGER PRIMARY KEY, created_at TEXT, location TEXT, world_id TEXT, world_name TEXT, time INTEGER, UNIQUE(created_at, location))`
);
await sqliteService.executeNonQuery(
`CREATE TABLE IF NOT EXISTS gamelog_join_leave (id INTEGER PRIMARY KEY, created_at TEXT, type TEXT, display_name TEXT, location TEXT, user_id TEXT, time INTEGER, UNIQUE(created_at, type, display_name))`
);
await sqliteService.executeNonQuery(
`CREATE TABLE IF NOT EXISTS gamelog_portal_spawn (id INTEGER PRIMARY KEY, created_at TEXT, display_name TEXT, location TEXT, user_id TEXT, instance_id TEXT, world_name TEXT, UNIQUE(created_at, display_name))`
);
await sqliteService.executeNonQuery(
`CREATE TABLE IF NOT EXISTS gamelog_video_play (id INTEGER PRIMARY KEY, created_at TEXT, video_url TEXT, video_name TEXT, video_id TEXT, location TEXT, display_name TEXT, user_id TEXT, UNIQUE(created_at, video_url))`
);
await sqliteService.executeNonQuery(
`CREATE TABLE IF NOT EXISTS gamelog_event (id INTEGER PRIMARY KEY, created_at TEXT, data TEXT, UNIQUE(created_at, data))`
);
}
async getFeedDatabase() {
var feedDatabase = [];
var date = new Date();
@@ -349,6 +367,162 @@ class Database {
}
);
}
async getGamelogDatabase() {
var gamelogDatabase = [];
var date = new Date();
date.setDate(date.getDate() - 3); // 3 day limit
var dateOffset = date.toJSON();
await sqliteService.execute((dbRow) => {
var row = {
rowId: dbRow[0],
created_at: dbRow[1],
type: 'Location',
location: dbRow[2],
worldId: dbRow[3],
worldName: dbRow[4],
time: dbRow[5]
};
gamelogDatabase.unshift(row);
}, `SELECT * FROM gamelog_location WHERE created_at >= date('${dateOffset}')`);
await sqliteService.execute((dbRow) => {
var row = {
rowId: dbRow[0],
created_at: dbRow[1],
type: dbRow[2],
displayName: dbRow[3],
location: dbRow[4],
userId: dbRow[5],
time: dbRow[6]
};
gamelogDatabase.unshift(row);
}, `SELECT * FROM gamelog_join_leave WHERE created_at >= date('${dateOffset}')`);
await sqliteService.execute((dbRow) => {
var row = {
rowId: dbRow[0],
created_at: dbRow[1],
type: 'PortalSpawn',
displayName: dbRow[2],
location: dbRow[3],
userId: dbRow[4],
instanceId: dbRow[5],
worldName: dbRow[6]
};
gamelogDatabase.unshift(row);
}, `SELECT * FROM gamelog_portal_spawn WHERE created_at >= date('${dateOffset}')`);
await sqliteService.execute((dbRow) => {
var row = {
rowId: dbRow[0],
created_at: dbRow[1],
type: 'VideoPlay',
videoUrl: dbRow[2],
videoName: dbRow[3],
videoId: dbRow[4],
location: dbRow[5],
displayName: dbRow[6],
userId: dbRow[7]
};
gamelogDatabase.unshift(row);
}, `SELECT * FROM gamelog_video_play WHERE created_at >= date('${dateOffset}')`);
await sqliteService.execute((dbRow) => {
var row = {
rowId: dbRow[0],
created_at: dbRow[1],
type: 'Event',
data: dbRow[2]
};
gamelogDatabase.unshift(row);
}, `SELECT * FROM gamelog_event WHERE created_at >= date('${dateOffset}')`);
var compareByCreatedAt = function (a, b) {
var A = a.created_at;
var B = b.created_at;
if (A < B) {
return -1;
}
if (A > B) {
return 1;
}
return 0;
};
gamelogDatabase.sort(compareByCreatedAt);
return gamelogDatabase;
}
addGamelogLocationToDatabase(entry) {
sqliteService.executeNonQuery(
`INSERT OR IGNORE INTO gamelog_location (created_at, location, world_id, world_name, time) VALUES (@created_at, @location, @world_id, @world_name, @time)`,
{
'@created_at': entry.created_at,
'@location': entry.location,
'@world_id': entry.worldId,
'@world_name': entry.worldName,
'@time': entry.time
}
);
}
updateGamelogLocationTimeToDatabase(entry) {
sqliteService.executeNonQuery(
`UPDATE gamelog_location SET time = @time WHERE created_at = @created_at`,
{
'@created_at': entry.created_at,
'@time': entry.time
}
);
}
addGamelogJoinLeaveToDatabase(entry) {
sqliteService.executeNonQuery(
`INSERT OR IGNORE INTO gamelog_join_leave (created_at, type, display_name, location, user_id, time) VALUES (@created_at, @type, @display_name, @location, @user_id, @time)`,
{
'@created_at': entry.created_at,
'@type': entry.type,
'@display_name': entry.displayName,
'@location': entry.location,
'@user_id': entry.userId,
'@time': entry.time
}
);
}
addGamelogPortalSpawnToDatabase(entry) {
sqliteService.executeNonQuery(
`INSERT OR IGNORE INTO gamelog_portal_spawn (created_at, display_name, location, user_id, instance_id, world_name) VALUES (@created_at, @display_name, @location, @user_id, @instance_id, @world_name)`,
{
'@created_at': entry.created_at,
'@display_name': entry.displayName,
'@location': entry.location,
'@user_id': entry.userId,
'@instance_id': entry.instanceId,
'@world_name': entry.worldName
}
);
}
addGamelogVideoPlayToDatabase(entry) {
sqliteService.executeNonQuery(
`INSERT OR IGNORE INTO gamelog_video_play (created_at, video_url, video_name, video_id, location, display_name, user_id) VALUES (@created_at, @video_url, @video_name, @video_id, @location, @display_name, @user_id)`,
{
'@created_at': entry.created_at,
'@video_url': entry.videoUrl,
'@video_name': entry.videoName,
'@video_id': entry.videoId,
'@location': entry.location,
'@display_name': entry.displayName,
'@user_id': entry.userId
}
);
}
addGamelogEventToDatabase(entry) {
sqliteService.executeNonQuery(
`INSERT OR IGNORE INTO gamelog_event (created_at, data) VALUES (@created_at, @data)`,
{
'@created_at': entry.created_at,
'@data': entry.data
}
);
}
}
var self = new Database();

View File

@@ -1,9 +1,7 @@
// requires binding of LogWatcher
// <string, object>
var contextMap = new Map();
function parseRawGameLog(dt, type, args) {
class GameLogService {
parseRawGameLog(dt, type, args) {
var gameLog = {
dt,
type
@@ -37,10 +35,14 @@ function parseRawGameLog(dt, type, args) {
break;
case 'video-play':
gameLog.videoURL = args[0];
gameLog.videoUrl = args[0];
gameLog.displayName = args[1];
break;
case 'vrcx':
gameLog.data = args[0];
break;
default:
break;
}
@@ -48,46 +50,29 @@ function parseRawGameLog(dt, type, args) {
return gameLog;
}
class GameLogService {
async poll() {
var rawGameLogs = await LogWatcher.Get();
async getAll() {
var gameLogs = [];
var now = Date.now();
var done = false;
while (!done) {
var rawGameLogs = await LogWatcher.Get();
// eslint-disable-next-line no-unused-vars
for (var [fileName, dt, type, ...args] of rawGameLogs) {
var context = contextMap.get(fileName);
if (typeof context === 'undefined') {
context = {
updatedAt: null,
// location
location: null
};
contextMap.set(fileName, context);
}
var gameLog = parseRawGameLog(dt, type, args);
switch (gameLog.type) {
case 'location':
context.location = gameLog.location;
break;
default:
break;
}
context.updatedAt = now;
var gameLog = this.parseRawGameLog(dt, type, args);
gameLogs.push(gameLog);
}
if (rawGameLogs.length === 0) {
done = true;
}
}
return gameLogs;
}
async setDateTill(dateTill) {
await LogWatcher.SetDateTill(dateTill);
}
async reset() {
await LogWatcher.Reset();
contextMap.clear();
}
}

View File

@@ -9,10 +9,7 @@ import Vue from 'vue';
import ElementUI from 'element-ui';
import locale from 'element-ui/lib/locale/lang/en';
import {appVersion} from './constants.js';
import sharedRepository from './repository/shared.js';
import configRepository from './repository/config.js';
import webApiService from './service/webapi.js';
speechSynthesis.getVoices();
@@ -109,273 +106,104 @@ speechSynthesis.getVoices();
};
Vue.filter('timeToText', timeToText);
//
// API
//
var API = {};
API.eventHandlers = new Map();
API.$emit = function (name, ...args) {
// console.log(name, ...args);
var handlers = this.eventHandlers.get(name);
if (typeof handlers === 'undefined') {
return;
}
try {
for (var handler of handlers) {
handler.apply(this, args);
}
} catch (err) {
console.error(err);
Vue.component('location', {
template:
'<span>{{ text }}<slot></slot><span class="famfamfam-flags" :class="region" style="display:inline-block;margin-left:5px"></span></span>',
props: {
location: String,
hint: {
type: String,
default: ''
}
},
data() {
return {
text: this.location,
region: this.region
};
API.$on = function (name, handler) {
var handlers = this.eventHandlers.get(name);
if (typeof handlers === 'undefined') {
handlers = [];
this.eventHandlers.set(name, handlers);
}
handlers.push(handler);
};
API.$off = function (name, handler) {
var handlers = this.eventHandlers.get(name);
if (typeof handlers === 'undefined') {
return;
}
var {length} = handlers;
for (var i = 0; i < length; ++i) {
if (handlers[i] === handler) {
if (length > 1) {
handlers.splice(i, 1);
},
methods: {
parse() {
this.text = this.location;
var L = $app.parseLocation(this.location);
if (L.isOffline) {
this.text = 'Offline';
} else if (L.isPrivate) {
this.text = 'Private';
} else if (typeof this.hint === 'string' && this.hint !== '') {
if (L.instanceId) {
this.text = `${this.hint} #${L.instanceName} ${L.accessType}`;
} else {
this.eventHandlers.delete(name);
}
break;
}
}
};
API.pendingGetRequests = new Map();
API.call = function (endpoint, options) {
var init = {
url: `https://api.vrchat.cloud/api/1/${endpoint}`,
method: 'GET',
...options
};
var {params} = init;
var isGetRequest = init.method === 'GET';
if (isGetRequest === true) {
// transform body to url
if (params === Object(params)) {
var url = new URL(init.url);
var {searchParams} = url;
for (var key in params) {
searchParams.set(key, params[key]);
}
init.url = url.toString();
}
// merge requests
var req = this.pendingGetRequests.get(init.url);
if (typeof req !== 'undefined') {
return req;
this.text = this.hint;
}
} else if (L.worldId) {
if (L.instanceId) {
this.text = ` #${L.instanceName} ${L.accessType}`;
} else {
init.headers = {
'Content-Type': 'application/json;charset=utf-8',
...init.headers
};
init.body =
params === Object(params) ? JSON.stringify(params) : '{}';
}
init.headers = {
'User-Agent': appVersion,
...init.headers
};
var req = webApiService
.execute(init)
.catch((err) => {
this.$throw(0, err);
})
.then((response) => {
try {
response.data = JSON.parse(response.data);
return response;
} catch (e) {}
if (response.status === 200) {
this.$throw(0, 'Invalid JSON response');
}
this.$throw(response.status);
return {};
})
.then(({data, status}) => {
if (data === Object(data)) {
if (status === 200) {
if (data.success === Object(data.success)) {
new Noty({
type: 'success',
text: escapeTag(data.success.message)
}).show();
}
return data;
}
if (data.error === Object(data.error)) {
this.$throw(
data.error.status_code || status,
data.error.message,
data.error.data
);
} else if (typeof data.error === 'string') {
this.$throw(data.status_code || status, data.error);
this.text = this.location;
}
}
this.$throw(status, data);
return data;
});
if (isGetRequest === true) {
req.finally(() => {
this.pendingGetRequests.delete(init.url);
});
this.pendingGetRequests.set(init.url, req);
}
return req;
};
API.statusCodes = {
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) {
var text = [];
if (code > 0) {
var status = this.statusCodes[code];
if (typeof status === 'undefined') {
text.push(`${code}`);
this.region = '';
if (
this.location !== '' &&
L.instanceId &&
!L.isOffline &&
!L.isPrivate
) {
if (L.region === 'eu') {
this.region = 'europeanunion';
} else if (L.region === 'jp') {
this.region = 'jp';
} else {
text.push(`${code} ${status}`);
this.region = 'us';
}
}
if (typeof error !== 'undefined') {
text.push(JSON.stringify(error));
}
text = text.map((s) => escapeTag(s)).join('<br>');
if (text.length) {
new Noty({
type: 'error',
text
}).show();
},
watch: {
location() {
this.parse();
}
},
created() {
this.parse();
}
throw new Error(text);
};
// API: Config
API.cachedConfig = {};
API.$on('CONFIG', function (args) {
args.ref = this.applyConfig(args.json);
});
API.applyConfig = function (json) {
var ref = {
clientApiKey: '',
...json
};
this.cachedConfig = ref;
return ref;
var $app = {
data: {
// 1 = 대시보드랑 손목에 보이는거
// 2 = 항상 화면에 보이는 거
appType: location.href.substr(-1),
currentTime: new Date().toJSON(),
cpuUsage: 0,
config: {},
lastLocation: {
date: 0,
location: '',
name: '',
playerList: [],
friendList: []
},
lastLocationTimer: '',
wristFeed: [],
devices: []
},
computed: {},
methods: {},
watch: {},
el: '#x-app',
mounted() {
setTimeout(function () {
AppApi.ExecuteAppFunction('vrInit', '');
}, 1000);
if (this.appType === '1') {
this.updateStatsLoop();
}
}
};
API.getConfig = function () {
return this.call('config', {
method: 'GET'
}).then((json) => {
var args = {
ref: null,
json
};
this.$emit('CONFIG', args);
return args;
});
};
// API: Location
API.parseLocation = function (tag) {
$app.methods.parseLocation = function (tag) {
var _tag = String(tag || '');
var ctx = {
tag: _tag,
@@ -448,380 +276,32 @@ speechSynthesis.getVoices();
return ctx;
};
Vue.component('location', {
template:
'<span>{{ text }}<slot></slot><span class="famfamfam-flags" :class="region" style="display:inline-block;margin-left:5px"></span></span>',
props: {
location: String,
hint: {
type: String,
default: ''
}
},
data() {
return {
text: this.location,
region: this.region
};
},
methods: {
parse() {
this.text = this.location;
var L = API.parseLocation(this.location);
if (L.isOffline) {
this.text = 'Offline';
} else if (L.isPrivate) {
this.text = 'Private';
} else if (typeof this.hint === 'string' && this.hint !== '') {
if (L.instanceId) {
this.text = `${this.hint} #${L.instanceName} ${L.accessType}`;
} else {
this.text = this.hint;
}
} else if (L.worldId) {
if (L.instanceId) {
this.text = ` #${L.instanceName} ${L.accessType}`;
} else {
this.text = this.location;
}
}
this.region = '';
if (
this.location !== '' &&
L.instanceId &&
!L.isOffline &&
!L.isPrivate
) {
if (L.region === 'eu') {
this.region = 'europeanunion';
} else if (L.region === 'jp') {
this.region = 'jp';
} else {
this.region = 'us';
}
}
}
},
watch: {
location() {
this.parse();
}
},
created() {
this.parse();
}
});
// API: World
API.cachedWorlds = new Map();
API.$on('WORLD', function (args) {
args.ref = this.applyWorld(args.json);
});
API.applyWorld = function (json) {
var ref = this.cachedWorlds.get(json.id);
if (typeof ref === 'undefined') {
ref = {
id: '',
name: '',
description: '',
authorId: '',
authorName: '',
capacity: 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: [],
// VRCX
$isLabs: false,
//
...json
};
this.cachedWorlds.set(ref.id, ref);
} else {
Object.assign(ref, json);
}
ref.$isLabs = ref.tags.includes('system_labs');
return ref;
$app.methods.configUpdate = function (json) {
this.config = JSON.parse(json);
};
/*
params: {
worldId: string
}
*/
API.getWorld = function (params) {
return this.call(`worlds/${params.worldId}`, {
method: 'GET'
}).then((json) => {
var args = {
ref: null,
json,
params
};
this.$emit('WORLD', args);
return args;
});
$app.methods.lastLocationUpdate = function (json) {
this.lastLocation = JSON.parse(json);
};
// API: User
API.cachedUsers = new Map();
API.$on('USER', function (args) {
args.ref = this.applyUser(args.json);
});
API.applyUser = function (json) {
var ref = this.cachedUsers.get(json.id);
if (typeof ref === 'undefined') {
ref = {
id: '',
username: '',
displayName: '',
userIcon: '',
bio: '',
bioLinks: [],
currentAvatarImageUrl: '',
currentAvatarThumbnailImageUrl: '',
status: '',
statusDescription: '',
state: '',
tags: [],
developerType: '',
last_login: '',
last_platform: '',
allowAvatarCopying: false,
isFriend: false,
location: '',
worldId: '',
instanceId: '',
// VRCX
...json
};
this.cachedUsers.set(ref.id, ref);
} else {
var props = {};
for (var prop in ref) {
if (ref[prop] !== Object(ref[prop])) {
props[prop] = true;
}
}
var $ref = {...ref};
Object.assign(ref, json);
for (var prop in ref) {
if (ref[prop] !== Object(ref[prop])) {
props[prop] = true;
}
}
for (var prop in props) {
var asis = $ref[prop];
var tobe = ref[prop];
if (asis === tobe) {
delete props[prop];
} else {
props[prop] = [tobe, asis];
}
}
}
return ref;
$app.methods.wristFeedUpdate = function (json) {
this.wristFeed = JSON.parse(json);
};
/*
params: {
userId: string
}
*/
API.getUser = function (params) {
return this.call(`users/${params.userId}`, {
method: 'GET'
}).then((json) => {
var args = {
json,
params
};
this.$emit('USER', args);
return args;
});
};
$app.methods.updateStatsLoop = async function () {
try {
this.currentTime = new Date().toJSON();
var cpuUsage = await AppApi.CpuUsage();
this.cpuUsage = cpuUsage.toFixed(0);
/*
params: {
userId: string
}
*/
API.getCachedUser = function (params) {
return new Promise((resolve, reject) => {
var ref = this.cachedUsers.get(params.userId);
if (typeof ref === 'undefined') {
this.getUser(params).catch(reject).then(resolve);
} else {
resolve({
cache: true,
json: ref,
params,
ref
});
}
});
};
var $app = {
data: {
API,
// 1 = 대시보드랑 손목에 보이는거
// 2 = 항상 화면에 보이는 거
appType: location.href.substr(-1),
currentTime: new Date().toJSON(),
currentUserStatus: null,
cpuUsage: 0,
config: {},
isGameRunning: false,
isGameNoVR: false,
downloadProgress: 0,
lastLocation: {
date: 0,
location: '',
name: '',
playerList: [],
friendList: []
},
lastLocationTimer: '',
wristFeedLastEntry: '',
notyFeedLastEntry: '',
wristFeed: [],
notyMap: [],
devices: []
},
computed: {},
methods: {},
watch: {},
el: '#x-app',
mounted() {
// https://media.discordapp.net/attachments/581757976625283083/611170278218924033/unknown.png
// 현재 날짜 시간
// 컨트롤러 배터리 상황
// --
// OO is in Let's Just H!!!!! [GPS]
// OO has logged in [Online] -> TODO: location
// OO has logged out [Offline] -> TODO: location
// 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) => {
if (this.appType === '1') {
this.updateCpuUsageLoop();
}
this.initLoop();
return args;
});
}
};
$app.methods.updateVRConfigVars = function () {
this.currentUserStatus = sharedRepository.getString(
'current_user_status'
);
this.isGameRunning = sharedRepository.getBool('is_game_running');
this.isGameNoVR = sharedRepository.getBool('is_Game_No_VR');
this.downloadProgress = sharedRepository.getInt('downloadProgress');
var lastLocation = sharedRepository.getObject('last_location');
if (lastLocation) {
this.lastLocation = lastLocation;
this.lastLocationTimer = '';
if (this.lastLocation.date !== 0) {
this.lastLocationTimer = timeToText(
Date.now() - this.lastLocation.date
);
} else {
this.lastLocationTimer = '';
}
}
var newConfig = sharedRepository.getObject('VRConfigVars');
if (newConfig) {
if (JSON.stringify(newConfig) !== JSON.stringify(this.config)) {
this.config = newConfig;
this.notyFeedLastEntry = '';
this.wristFeedLastEntry = '';
if (this.appType === '2') {
this.initNotyMap();
}
}
} else {
throw 'config not set';
}
};
$app.methods.initNotyMap = function () {
var notyFeed = sharedRepository.getArray('notyFeed');
if (notyFeed === null) {
return;
}
notyFeed.forEach((feed) => {
var displayName = '';
if (feed.displayName) {
displayName = feed.displayName;
} else if (feed.senderUsername) {
displayName = feed.senderUsername;
} else if (feed.sourceDisplayName) {
displayName = feed.sourceDisplayName;
} else if (feed.data) {
displayName = feed.data;
} else {
console.error('missing displayName');
}
if (
(displayName && !this.notyMap[displayName]) ||
this.notyMap[displayName] < feed.created_at
) {
this.notyMap[displayName] = feed.created_at;
}
});
};
$app.methods.initLoop = function () {
if (!sharedRepository.getBool('VRInit')) {
setTimeout(this.initLoop, 500);
} else {
this.updateLoop();
}
};
$app.methods.updateLoop = async function () {
try {
this.currentTime = new Date().toJSON();
await this.updateVRConfigVars();
if (!this.config.hideDevicesFromFeed && this.appType === '1') {
if (!this.config.hideDevicesFromFeed) {
AppApi.GetVRDevices().then((devices) => {
devices.forEach((device) => {
device[2] = parseInt(device[2], 10);
@@ -831,96 +311,25 @@ speechSynthesis.getVoices();
} else {
this.devices = '';
}
await this.updateSharedFeeds();
} catch (err) {
console.error(err);
}
setTimeout(() => this.updateLoop(), 500);
setTimeout(() => this.updateStatsLoop(), 500);
};
$app.methods.updateCpuUsageLoop = async function () {
try {
var cpuUsage = await AppApi.CpuUsage();
this.cpuUsage = cpuUsage.toFixed(0);
} catch (err) {
console.error(err);
}
setTimeout(() => this.updateCpuUsageLoop(), 1000);
};
$app.methods.updateSharedFeeds = function () {
if (this.appType === '1') {
this.wristFeed = sharedRepository.getArray('wristFeed');
}
if (this.appType === '2') {
var notyFeed = sharedRepository.getArray('notyFeed');
this.updateSharedFeedNoty(notyFeed);
}
};
$app.methods.updateSharedFeedNoty = function (notyFeed) {
var notyToPlay = [];
notyFeed.forEach((feed) => {
var displayName = '';
if (feed.displayName) {
displayName = feed.displayName;
} else if (feed.senderUsername) {
displayName = feed.senderUsername;
} else if (feed.sourceDisplayName) {
displayName = feed.sourceDisplayName;
} else if (feed.data) {
displayName = feed.data;
} else {
console.error('missing displayName');
}
if (
(displayName && !this.notyMap[displayName]) ||
this.notyMap[displayName] < feed.created_at
) {
this.notyMap[displayName] = feed.created_at;
notyToPlay.push(feed);
}
});
// disable notifications when busy
if (this.currentUserStatus === 'busy') {
return;
}
var bias = new Date(Date.now() - 60000).toJSON();
var noty = {};
var messageList = [
'inviteMessage',
'requestMessage',
'responseMessage'
];
for (var i = 0; i < notyToPlay.length; i++) {
noty = notyToPlay[i];
if (noty.created_at < bias) {
continue;
}
var message = '';
for (var k = 0; k < messageList.length; k++) {
if (
typeof noty.details !== 'undefined' &&
typeof noty.details[messageList[k]] !== 'undefined'
) {
message = noty.details[messageList[k]];
}
}
if (message) {
message = `, ${message}`;
}
if (
this.config.overlayNotifications &&
!this.isGameNoVR &&
this.isGameRunning
) {
$app.methods.playNoty = function (json) {
var {noty, message, imageUrl} = JSON.parse(json);
var text = '';
var img = '';
if (imageUrl) {
img = `<img class="noty-img" src="${imageUrl}"></img>`;
}
switch (noty.type) {
case 'OnPlayerJoined':
text = `<strong>${noty.data}</strong> has joined`;
text = `<strong>${noty.displayName}</strong> has joined`;
break;
case 'OnPlayerLeft':
text = `<strong>${noty.data}</strong> has left`;
text = `<strong>${noty.displayName}</strong> has left`;
break;
case 'OnPlayerJoining':
text = `<strong>${noty.displayName}</strong> is joining`;
@@ -975,13 +384,27 @@ speechSynthesis.getVoices();
text = `<strong>${noty.previousDisplayName}</strong> changed their name to ${noty.displayName}`;
break;
case 'PortalSpawn':
text = `<strong>${noty.data}</strong> has spawned a portal`;
var locationName = '';
if (noty.worldName) {
locationName = ` to ${this.displayLocation(
noty.instanceId,
noty.worldName
)}`;
}
text = `<strong>${noty.displayName}</strong> has spawned a portal${locationName}`;
break;
case 'AvatarChange':
text = `<strong>${noty.displayName}</strong> changed into avatar ${noty.name}`;
break;
case 'Event':
text = noty.data;
break;
case 'VideoPlay':
text = `<strong>Now playing:</strong> ${noty.data}`;
var videoName = noty.videoUrl;
if (noty.videoName) {
videoName = noty.videoName;
}
text = `<strong>Now playing:</strong> ${videoName}`;
break;
case 'BlockedOnPlayerJoined':
text = `Blocked user <strong>${noty.displayName}</strong> has joined`;
@@ -1004,11 +427,9 @@ speechSynthesis.getVoices();
theme: this.config.notificationTheme,
timeout: this.config.notificationTimeout,
layout: this.config.notificationPosition,
text
text: `${img}<div class="noty-text">${text}</div>`
}).show();
}
}
}
};
$app.methods.statusClass = function (status) {
@@ -1033,7 +454,7 @@ speechSynthesis.getVoices();
$app.methods.displayLocation = function (location, worldName) {
var text = '';
var L = API.parseLocation(location);
var L = this.parseLocation(location);
if (L.isOffline) {
text = 'Offline';
} else if (L.isPrivate) {

View File

@@ -36,17 +36,23 @@ html
.detail
span.extra
span.time {{ feed.created_at | formatDate('HH:MI') }}
| #[span.name(v-text="feed.displayName")] #[i.x-user-status(:class="statusClass(feed.status)")] {{feed.statusDescription}}
| #[span.name(v-text="feed.displayName")]
template(v-if="feed.statusDescription === feed.previousStatusDescription")
i.x-user-status(:class="statusClass(feed.previousStatus)")
i.el-icon-right
i.x-user-status(:class="statusClass(feed.status)")
template(v-else)
| #[i.x-user-status(:class="statusClass(feed.status)")] {{feed.statusDescription}}
div(v-else-if="feed.type === 'OnPlayerJoined'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }")
.detail
span.extra
span.time {{ feed.created_at | formatDate('HH:MI') }}
| ▶️ #[span.name(v-text="feed.data")]
| ▶️ #[span.name(v-text="feed.displayName")]
div(v-else-if="feed.type === 'OnPlayerLeft'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }")
.detail
span.extra
span.time {{ feed.created_at | formatDate('HH:MI') }}
| ◀️ #[span.name(v-text="feed.data")]
| ◀️ #[span.name(v-text="feed.displayName")]
div(v-else-if="feed.type === 'OnPlayerJoining'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }")
.detail
span.extra
@@ -57,7 +63,16 @@ html
.detail
span.extra
span.time {{ feed.created_at | formatDate('HH:MI') }}
location(:location="feed.data[0]" :hint="feed.data[1]")
location(:location="feed.location" :hint="feed.worldName")
div(v-else-if="feed.type === 'VideoPlay'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }")
.detail
span.extra
span.time {{ feed.created_at | formatDate('HH:MI') }}
| 🎵 #[span.name(v-text="feed.displayName")]
template(v-if="feed.videoName")
| #[span(v-text="feed.videoName")]
template(v-else)
| #[span(v-text="feed.videoUrl")]
div(v-else-if="feed.type === 'invite'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }")
.detail
span.extra
@@ -107,7 +122,16 @@ html
.detail
span.extra
span.time {{ feed.created_at | formatDate('HH:MI') }}
| ✨ #[span.name(v-text="feed.data")]
| ✨ #[span.name(v-text="feed.displayName")]
template(v-if="feed.worldName")
| #[location(:location="feed.instanceId" :hint="feed.worldName")]
div(v-else-if="feed.type === 'AvatarChange'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }")
.detail
span.extra
span.time {{ feed.created_at | formatDate('HH:MI') }}
| 🧍 #[span.name(v-text="feed.displayName")] {{ feed.name }}
template(v-if="feed.description && feed.description !== feed.name")
| - {{ feed.description }}
div(v-else-if="feed.type === 'Event'" class="x-friend-item")
.detail
span.extra
@@ -159,17 +183,23 @@ html
.detail
span.extra
span.time {{ feed.created_at | formatDate('HH:MI') }}
| #[span.name(v-text="feed.displayName")] is #[i.x-user-status(:class="statusClass(feed.status)")] {{feed.statusDescription}}
| #[span.name(v-text="feed.displayName")]
template(v-if="feed.statusDescription === feed.previousStatusDescription")
i.x-user-status(:class="statusClass(feed.previousStatus)")
i.el-icon-right
i.x-user-status(:class="statusClass(feed.status)")
template(v-else)
| #[i.x-user-status(:class="statusClass(feed.status)")] {{feed.statusDescription}}
div(v-else-if="feed.type === 'OnPlayerJoined'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }")
.detail
span.extra
span.time {{ feed.created_at | formatDate('HH:MI') }}
| #[span.name(v-text="feed.data")] has joined
| #[span.name(v-text="feed.displayName")] has joined
div(v-else-if="feed.type === 'OnPlayerLeft'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }")
.detail
span.extra
span.time {{ feed.created_at | formatDate('HH:MI') }}
| #[span.name(v-text="feed.data")] has left
| #[span.name(v-text="feed.displayName")] has left
div(v-else-if="feed.type === 'OnPlayerJoining'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }")
.detail
span.extra
@@ -179,7 +209,16 @@ html
.detail
span.extra
span.time {{ feed.created_at | formatDate('HH:MI') }}
location(:location="feed.data[0]" :hint="feed.data[1]")
location(:location="feed.location" :hint="feed.worldName")
div(v-else-if="feed.type === 'VideoPlay'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }")
.detail
span.extra
span.time {{ feed.created_at | formatDate('HH:MI') }}
| #[span.name(v-text="feed.displayName")] changed video to
template(v-if="feed.videoName")
| #[span(v-text="feed.videoName")]
template(v-else)
| #[span(v-text="feed.videoUrl")]
div(v-else-if="feed.type === 'invite'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }")
.detail
span.extra
@@ -229,7 +268,16 @@ html
.detail
span.extra
span.time {{ feed.created_at | formatDate('HH:MI') }}
| #[span.name(v-text="feed.data")] has spawned a portal
| #[span.name(v-text="feed.displayName")] has spawned a portal
template(v-if="feed.worldName")
| to #[location(:location="feed.instanceId" :hint="feed.worldName")]
div(v-else-if="feed.type === 'AvatarChange'" class="x-friend-item" :class="{ friend: feed.isFriend, favorite: feed.isFavorite }")
.detail
span.extra
span.time {{ feed.created_at | formatDate('HH:MI') }}
| #[span.name(v-text="feed.displayName")] changed into avatar {{ feed.name }}
template(v-if="feed.description && feed.description !== feed.name")
| - {{ feed.description }}
div(v-else-if="feed.type === 'Event'" class="x-friend-item")
.detail
span.extra
@@ -302,19 +350,19 @@ html
span {{ device[2] }}%
.x-containerbottom
template(v-if="config && config.minimalFeed")
template(v-if="downloadProgress === 100")
template(v-if="config.downloadProgress === 100")
span(style="display:inline-block;margin-right:5px") #[i.el-icon-loading]
template(v-else-if="downloadProgress > 0")
span(style="display:inline-block;margin-right:5px") {{ downloadProgress }}%
template(v-else-if="config.downloadProgress > 0")
span(style="display:inline-block;margin-right:5px") {{ config.downloadProgress }}%
template(v-if="lastLocation.date != 0")
span(style="float:right") {{ lastLocationTimer }}
span(style="display:inline-block") {{ lastLocation.playerList.length }}
span(style="display:inline-block;font-weight:bold") {{ lastLocation.friendList.length !== 0 ? ` (${lastLocation.friendList.length})` : ''}}
template(v-else)
template(v-if="downloadProgress === 100")
template(v-if="config.downloadProgress === 100")
span(style="display:inline-block;margin-right:5px") Downloading: #[i.el-icon-loading]
template(v-else-if="downloadProgress > 0")
span(style="display:inline-block;margin-right:5px") Downloading: {{ downloadProgress }}%
template(v-else-if="config.downloadProgress > 0")
span(style="display:inline-block;margin-right:5px") Downloading: {{ config.downloadProgress }}%
template(v-if="lastLocation.date != 0")
span(style="float:right") Timer: {{ lastLocationTimer }}
span(style="display:inline-block") Players: {{ lastLocation.playerList.length }}

View File

@@ -22,9 +22,6 @@
.noty_body {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.noty_layout {
@@ -34,6 +31,7 @@
.noty_theme__relax.noty_bar,
.noty_theme__sunset.noty_bar {
height: 42px;
position: relative;
margin: 4px 0;
overflow: hidden;
@@ -42,9 +40,7 @@
.noty_theme__relax.noty_bar .noty_body,
.noty_theme__sunset.noty_bar .noty_body {
padding: 5px 10px 10px;
font-size: 15px;
text-align: center;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);
}
@@ -143,6 +139,19 @@
opacity: 0.6;
}
.noty-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 8px 8px 0 11px;
}
.noty-img {
height: 42px;
float: left;
border-radius: 4px;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;