mirror of
https://github.com/citizenfx/cfx-server-data.git
synced 2026-09-21 02:39:29 +02:00
tweak: cleanup server-data
This commit is contained in:
@@ -2,7 +2,7 @@ AddEventHandler('gameEventTriggered', function(eventName, args)
|
||||
if eventName == 'CEventNetworkEntityDamage' then
|
||||
local victim = args[1]
|
||||
local culprit = args[2]
|
||||
local isDead = args[4] == 1
|
||||
local isDead = args[6] == 1
|
||||
|
||||
if isDead then
|
||||
local origCoords = GetEntityCoords(victim)
|
||||
|
||||
+2
-2
@@ -3,13 +3,13 @@
|
||||
|
||||
version '1.0.0'
|
||||
author 'Cfx.re <[email protected]>'
|
||||
description 'A GTA Online-styled theme for the chat resource.'
|
||||
description 'Example theme for the chat resource.'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
file 'style.css'
|
||||
file 'shadow.js'
|
||||
|
||||
chat_theme 'gtao' {
|
||||
chat_theme 'example' {
|
||||
styleSheet = 'style.css',
|
||||
script = 'shadow.js',
|
||||
msgTemplates = {
|
||||
+3
-16
@@ -19,20 +19,9 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font2';
|
||||
src: url(https://runtime.fivem.net/temp/ChaletLondonNineteenSixty.otf?a);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font2_cond';
|
||||
src: url(https://runtime.fivem.net/temp/chaletcomprime-colognesixty-webfont.ttf?a);
|
||||
}
|
||||
|
||||
.msg {
|
||||
font-family: Font2, sans-serif;
|
||||
font-family: sans-serif;
|
||||
color: #fff;
|
||||
|
||||
font-size: calc(1.8vh); /* 13px in 720p, calc'd by width */
|
||||
filter: url(#svgDropShadowFilter);
|
||||
|
||||
@@ -47,15 +36,13 @@
|
||||
}
|
||||
|
||||
.msg > span > span > b {
|
||||
font-family: Font2_cond, sans-serif;
|
||||
font-weight: normal;
|
||||
|
||||
font-family: sans-serif;
|
||||
vertical-align: baseline;
|
||||
padding-right: 11px;
|
||||
|
||||
line-height: 1;
|
||||
|
||||
font-size: calc(2.7vh);
|
||||
font-size: calc(1.8vh);
|
||||
}
|
||||
|
||||
.msg > span > span > span {
|
||||
@@ -1,4 +0,0 @@
|
||||
node_modules/
|
||||
.yarn.installed
|
||||
yarn-error.log
|
||||
dist/
|
||||
@@ -1 +0,0 @@
|
||||
# Chat
|
||||
@@ -1,308 +0,0 @@
|
||||
local isRDR = not TerraingridActivate and true or false
|
||||
|
||||
local chatInputActive = false
|
||||
local chatInputActivating = false
|
||||
local chatLoaded = false
|
||||
|
||||
RegisterNetEvent('chatMessage')
|
||||
RegisterNetEvent('chat:addTemplate')
|
||||
RegisterNetEvent('chat:addMessage')
|
||||
RegisterNetEvent('chat:addSuggestion')
|
||||
RegisterNetEvent('chat:addSuggestions')
|
||||
RegisterNetEvent('chat:addMode')
|
||||
RegisterNetEvent('chat:removeMode')
|
||||
RegisterNetEvent('chat:removeSuggestion')
|
||||
RegisterNetEvent('chat:clear')
|
||||
|
||||
-- internal events
|
||||
RegisterNetEvent('__cfx_internal:serverPrint')
|
||||
|
||||
RegisterNetEvent('_chat:messageEntered')
|
||||
|
||||
--deprecated, use chat:addMessage
|
||||
AddEventHandler('chatMessage', function(author, color, text)
|
||||
local args = { text }
|
||||
if author ~= "" then
|
||||
table.insert(args, 1, author)
|
||||
end
|
||||
SendNUIMessage({
|
||||
type = 'ON_MESSAGE',
|
||||
message = {
|
||||
color = color,
|
||||
multiline = true,
|
||||
args = args
|
||||
}
|
||||
})
|
||||
end)
|
||||
|
||||
AddEventHandler('__cfx_internal:serverPrint', function(msg)
|
||||
print(msg)
|
||||
|
||||
SendNUIMessage({
|
||||
type = 'ON_MESSAGE',
|
||||
message = {
|
||||
templateId = 'print',
|
||||
multiline = true,
|
||||
args = { msg },
|
||||
mode = '_global'
|
||||
}
|
||||
})
|
||||
end)
|
||||
|
||||
-- addMessage
|
||||
local addMessage = function(message)
|
||||
if type(message) == 'string' then
|
||||
message = {
|
||||
args = { message }
|
||||
}
|
||||
end
|
||||
|
||||
SendNUIMessage({
|
||||
type = 'ON_MESSAGE',
|
||||
message = message
|
||||
})
|
||||
end
|
||||
|
||||
exports('addMessage', addMessage)
|
||||
AddEventHandler('chat:addMessage', addMessage)
|
||||
|
||||
-- addSuggestion
|
||||
local addSuggestion = function(name, help, params)
|
||||
SendNUIMessage({
|
||||
type = 'ON_SUGGESTION_ADD',
|
||||
suggestion = {
|
||||
name = name,
|
||||
help = help,
|
||||
params = params or nil
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
exports('addSuggestion', addSuggestion)
|
||||
AddEventHandler('chat:addSuggestion', addSuggestion)
|
||||
|
||||
AddEventHandler('chat:addSuggestions', function(suggestions)
|
||||
for _, suggestion in ipairs(suggestions) do
|
||||
SendNUIMessage({
|
||||
type = 'ON_SUGGESTION_ADD',
|
||||
suggestion = suggestion
|
||||
})
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler('chat:removeSuggestion', function(name)
|
||||
SendNUIMessage({
|
||||
type = 'ON_SUGGESTION_REMOVE',
|
||||
name = name
|
||||
})
|
||||
end)
|
||||
|
||||
AddEventHandler('chat:addMode', function(mode)
|
||||
SendNUIMessage({
|
||||
type = 'ON_MODE_ADD',
|
||||
mode = mode
|
||||
})
|
||||
end)
|
||||
|
||||
AddEventHandler('chat:removeMode', function(name)
|
||||
SendNUIMessage({
|
||||
type = 'ON_MODE_REMOVE',
|
||||
name = name
|
||||
})
|
||||
end)
|
||||
|
||||
AddEventHandler('chat:addTemplate', function(id, html)
|
||||
SendNUIMessage({
|
||||
type = 'ON_TEMPLATE_ADD',
|
||||
template = {
|
||||
id = id,
|
||||
html = html
|
||||
}
|
||||
})
|
||||
end)
|
||||
|
||||
AddEventHandler('chat:clear', function(name)
|
||||
SendNUIMessage({
|
||||
type = 'ON_CLEAR'
|
||||
})
|
||||
end)
|
||||
|
||||
RegisterNUICallback('chatResult', function(data, cb)
|
||||
chatInputActive = false
|
||||
SetNuiFocus(false)
|
||||
|
||||
if not data.canceled then
|
||||
local id = PlayerId()
|
||||
|
||||
--deprecated
|
||||
local r, g, b = 0, 0x99, 255
|
||||
|
||||
if data.message:sub(1, 1) == '/' then
|
||||
ExecuteCommand(data.message:sub(2))
|
||||
else
|
||||
TriggerServerEvent('_chat:messageEntered', GetPlayerName(id), { r, g, b }, data.message, data.mode)
|
||||
end
|
||||
end
|
||||
|
||||
cb('ok')
|
||||
end)
|
||||
|
||||
local function refreshCommands()
|
||||
if GetRegisteredCommands then
|
||||
local registeredCommands = GetRegisteredCommands()
|
||||
|
||||
local suggestions = {}
|
||||
|
||||
for _, command in ipairs(registeredCommands) do
|
||||
if IsAceAllowed(('command.%s'):format(command.name)) and command.name ~= 'toggleChat' then
|
||||
table.insert(suggestions, {
|
||||
name = '/' .. command.name,
|
||||
help = ''
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
TriggerEvent('chat:addSuggestions', suggestions)
|
||||
end
|
||||
end
|
||||
|
||||
local function refreshThemes()
|
||||
local themes = {}
|
||||
|
||||
for resIdx = 0, GetNumResources() - 1 do
|
||||
local resource = GetResourceByFindIndex(resIdx)
|
||||
|
||||
if GetResourceState(resource) == 'started' then
|
||||
local numThemes = GetNumResourceMetadata(resource, 'chat_theme')
|
||||
|
||||
if numThemes > 0 then
|
||||
local themeName = GetResourceMetadata(resource, 'chat_theme')
|
||||
local themeData = json.decode(GetResourceMetadata(resource, 'chat_theme_extra') or 'null')
|
||||
|
||||
if themeName and themeData then
|
||||
themeData.baseUrl = 'nui://' .. resource .. '/'
|
||||
themes[themeName] = themeData
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
SendNUIMessage({
|
||||
type = 'ON_UPDATE_THEMES',
|
||||
themes = themes
|
||||
})
|
||||
end
|
||||
|
||||
AddEventHandler('onClientResourceStart', function(resName)
|
||||
Wait(500)
|
||||
|
||||
refreshCommands()
|
||||
refreshThemes()
|
||||
end)
|
||||
|
||||
AddEventHandler('onClientResourceStop', function(resName)
|
||||
Wait(500)
|
||||
|
||||
refreshCommands()
|
||||
refreshThemes()
|
||||
end)
|
||||
|
||||
RegisterNUICallback('loaded', function(data, cb)
|
||||
TriggerServerEvent('chat:init')
|
||||
|
||||
refreshCommands()
|
||||
refreshThemes()
|
||||
|
||||
chatLoaded = true
|
||||
|
||||
cb('ok')
|
||||
end)
|
||||
|
||||
local CHAT_HIDE_STATES = {
|
||||
SHOW_WHEN_ACTIVE = 0,
|
||||
ALWAYS_SHOW = 1,
|
||||
ALWAYS_HIDE = 2
|
||||
}
|
||||
|
||||
local kvpEntry = GetResourceKvpString('hideState')
|
||||
local chatHideState = kvpEntry and tonumber(kvpEntry) or CHAT_HIDE_STATES.SHOW_WHEN_ACTIVE
|
||||
local isFirstHide = true
|
||||
|
||||
if not isRDR then
|
||||
if RegisterKeyMapping then
|
||||
RegisterKeyMapping('toggleChat', 'Toggle chat', 'keyboard', 'l')
|
||||
end
|
||||
|
||||
RegisterCommand('toggleChat', function()
|
||||
if chatHideState == CHAT_HIDE_STATES.SHOW_WHEN_ACTIVE then
|
||||
chatHideState = CHAT_HIDE_STATES.ALWAYS_SHOW
|
||||
elseif chatHideState == CHAT_HIDE_STATES.ALWAYS_SHOW then
|
||||
chatHideState = CHAT_HIDE_STATES.ALWAYS_HIDE
|
||||
elseif chatHideState == CHAT_HIDE_STATES.ALWAYS_HIDE then
|
||||
chatHideState = CHAT_HIDE_STATES.SHOW_WHEN_ACTIVE
|
||||
end
|
||||
|
||||
isFirstHide = false
|
||||
|
||||
SetResourceKvp('hideState', tostring(chatHideState))
|
||||
end, false)
|
||||
end
|
||||
|
||||
Citizen.CreateThread(function()
|
||||
SetTextChatEnabled(false)
|
||||
SetNuiFocus(false)
|
||||
|
||||
local lastChatHideState = -1
|
||||
local origChatHideState = -1
|
||||
|
||||
while true do
|
||||
Wait(0)
|
||||
|
||||
if not chatInputActive then
|
||||
if IsControlPressed(0, isRDR and `INPUT_MP_TEXT_CHAT_ALL` or 245) --[[ INPUT_MP_TEXT_CHAT_ALL ]] then
|
||||
chatInputActive = true
|
||||
chatInputActivating = true
|
||||
|
||||
SendNUIMessage({
|
||||
type = 'ON_OPEN'
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
if chatInputActivating then
|
||||
if not IsControlPressed(0, isRDR and `INPUT_MP_TEXT_CHAT_ALL` or 245) then
|
||||
SetNuiFocus(true)
|
||||
|
||||
chatInputActivating = false
|
||||
end
|
||||
end
|
||||
|
||||
if chatLoaded then
|
||||
local forceHide = IsScreenFadedOut() or IsPauseMenuActive()
|
||||
local wasForceHide = false
|
||||
|
||||
if chatHideState ~= CHAT_HIDE_STATES.ALWAYS_HIDE then
|
||||
if forceHide then
|
||||
origChatHideState = chatHideState
|
||||
chatHideState = CHAT_HIDE_STATES.ALWAYS_HIDE
|
||||
end
|
||||
elseif not forceHide and origChatHideState ~= -1 then
|
||||
chatHideState = origChatHideState
|
||||
origChatHideState = -1
|
||||
wasForceHide = true
|
||||
end
|
||||
|
||||
if chatHideState ~= lastChatHideState then
|
||||
lastChatHideState = chatHideState
|
||||
|
||||
SendNUIMessage({
|
||||
type = 'ON_SCREEN_STATE_CHANGE',
|
||||
hideState = chatHideState,
|
||||
fromUserInteraction = not forceHide and not isFirstHide and not wasForceHide
|
||||
})
|
||||
|
||||
isFirstHide = false
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
@@ -1,30 +0,0 @@
|
||||
-- This resource is part of the default Cfx.re asset pack (cfx-server-data)
|
||||
-- Altering or recreating for local use only is strongly discouraged.
|
||||
|
||||
version '1.0.0'
|
||||
author 'Cfx.re <[email protected]>'
|
||||
description 'Provides baseline chat functionality using a NUI-based interface.'
|
||||
repository 'https://github.com/citizenfx/cfx-server-data'
|
||||
|
||||
ui_page 'dist/ui.html'
|
||||
|
||||
client_script 'cl_chat.lua'
|
||||
server_script 'sv_chat.lua'
|
||||
|
||||
files {
|
||||
'dist/ui.html',
|
||||
'dist/index.css',
|
||||
'html/vendor/*.css',
|
||||
'html/vendor/fonts/*.woff2',
|
||||
}
|
||||
|
||||
fx_version 'adamant'
|
||||
games { 'rdr3', 'gta5' }
|
||||
rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.'
|
||||
|
||||
dependencies {
|
||||
'yarn',
|
||||
'webpack'
|
||||
}
|
||||
|
||||
webpack_config 'webpack.config.js'
|
||||
@@ -1,467 +0,0 @@
|
||||
import { post } from './utils';
|
||||
import CONFIG from './config';
|
||||
import Vue from 'vue';
|
||||
|
||||
import Suggestions from './Suggestions.vue';
|
||||
import MessageV from './Message.vue';
|
||||
import { Suggestion } from './Suggestions';
|
||||
|
||||
export interface Message {
|
||||
args: string[];
|
||||
template: string;
|
||||
params?: { [key: string]: string };
|
||||
multiline?: boolean;
|
||||
color?: [ number, number, number ];
|
||||
templateId?: number;
|
||||
mode?: string;
|
||||
modeData?: Mode;
|
||||
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface ThemeData {
|
||||
style: string;
|
||||
styleSheet: string;
|
||||
baseUrl: string;
|
||||
script: string;
|
||||
templates: { [id: string]: string }; // not supported rn
|
||||
msgTemplates: { [id: string]: string };
|
||||
|
||||
}
|
||||
|
||||
export interface Mode {
|
||||
name: string;
|
||||
displayName: string;
|
||||
color: string;
|
||||
hidden?: boolean;
|
||||
isChannel?: boolean;
|
||||
isGlobal?: boolean;
|
||||
}
|
||||
|
||||
enum ChatHideStates {
|
||||
ShowWhenActive = 0,
|
||||
AlwaysShow = 1,
|
||||
AlwaysHide = 2,
|
||||
}
|
||||
|
||||
const defaultMode: Mode = {
|
||||
name: 'all',
|
||||
displayName: 'All',
|
||||
color: '#fff'
|
||||
};
|
||||
|
||||
const globalMode: Mode = {
|
||||
name: '_global',
|
||||
displayName: 'All',
|
||||
color: '#fff',
|
||||
isGlobal: true,
|
||||
hidden: true
|
||||
};
|
||||
|
||||
export default Vue.extend({
|
||||
template: "#app_template",
|
||||
name: "app",
|
||||
components: {
|
||||
Suggestions,
|
||||
MessageV
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
style: CONFIG.style,
|
||||
showInput: false,
|
||||
showWindow: false,
|
||||
showHideState: false,
|
||||
hideState: ChatHideStates.ShowWhenActive,
|
||||
backingSuggestions: [] as Suggestion[],
|
||||
removedSuggestions: [] as string[],
|
||||
templates: { ...CONFIG.templates } as { [ key: string ]: string },
|
||||
message: "",
|
||||
messages: [] as Message[],
|
||||
oldMessages: [] as string[],
|
||||
oldMessagesIndex: -1,
|
||||
tplBackups: [] as unknown as [ HTMLElement, string ][],
|
||||
msgTplBackups: [] as unknown as [ string, string ][],
|
||||
focusTimer: 0,
|
||||
showWindowTimer: 0,
|
||||
showHideStateTimer: 0,
|
||||
listener: (event: MessageEvent) => {},
|
||||
modes: [defaultMode, globalMode] as Mode[],
|
||||
modeIdx: 0,
|
||||
};
|
||||
},
|
||||
destroyed() {
|
||||
clearInterval(this.focusTimer);
|
||||
window.removeEventListener("message", this.listener);
|
||||
},
|
||||
mounted() {
|
||||
post("http://chat/loaded", JSON.stringify({}));
|
||||
|
||||
this.listener = (event: MessageEvent) => {
|
||||
const item: any = event.data || (<any>event).detail; //'detail' is for debugging via browsers
|
||||
|
||||
if (!item || !item.type) {
|
||||
return;
|
||||
}
|
||||
|
||||
const typeRef = item.type as
|
||||
'ON_OPEN' | 'ON_SCREEN_STATE_CHANGE' | 'ON_MESSAGE' | 'ON_CLEAR' | 'ON_SUGGESTION_ADD' |
|
||||
'ON_SUGGESTION_REMOVE' | 'ON_TEMPLATE_ADD' | 'ON_UPDATE_THEMES' | 'ON_MODE_ADD' | 'ON_MODE_REMOVE';
|
||||
|
||||
if (this[typeRef]) {
|
||||
this[typeRef](item);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", this.listener);
|
||||
},
|
||||
watch: {
|
||||
messages() {
|
||||
if (this.hideState !== ChatHideStates.AlwaysHide) {
|
||||
if (this.showWindowTimer) {
|
||||
clearTimeout(this.showWindowTimer);
|
||||
}
|
||||
this.showWindow = true;
|
||||
this.resetShowWindowTimer();
|
||||
}
|
||||
|
||||
const messagesObj = this.$refs.messages as HTMLDivElement;
|
||||
this.$nextTick(() => {
|
||||
messagesObj.scrollTop = messagesObj.scrollHeight;
|
||||
});
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredMessages(): Message[] {
|
||||
return this.messages.filter(
|
||||
// show messages that are
|
||||
// - (if the current mode is a channel) global, or in the current mode
|
||||
// - (if the message is a channel) in the current mode
|
||||
el => (el.modeData?.isChannel || this.modes[this.modeIdx].isChannel) ?
|
||||
(el.mode === this.modes[this.modeIdx].name || el.modeData?.isGlobal) :
|
||||
true
|
||||
);
|
||||
},
|
||||
|
||||
suggestions(): Suggestion[] {
|
||||
return this.backingSuggestions.filter(
|
||||
el => this.removedSuggestions.indexOf(el.name) <= -1
|
||||
);
|
||||
},
|
||||
|
||||
hideAnimated(): boolean {
|
||||
return this.hideState !== ChatHideStates.AlwaysHide;
|
||||
},
|
||||
|
||||
modeIdxGet(): number {
|
||||
return (this.modeIdx >= this.modes.length) ? (this.modes.length - 1) : this.modeIdx;
|
||||
},
|
||||
|
||||
modePrefix(): string {
|
||||
if (this.modes.length === 2) {
|
||||
return `➤`;
|
||||
}
|
||||
|
||||
return this.modes[this.modeIdxGet].displayName;
|
||||
},
|
||||
|
||||
modeColor(): string {
|
||||
return this.modes[this.modeIdxGet].color;
|
||||
},
|
||||
|
||||
hideStateString(): string {
|
||||
// TODO: localization
|
||||
switch (this.hideState) {
|
||||
case ChatHideStates.AlwaysShow:
|
||||
return 'Visible';
|
||||
case ChatHideStates.AlwaysHide:
|
||||
return 'Hidden';
|
||||
case ChatHideStates.ShowWhenActive:
|
||||
return 'When active';
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
ON_SCREEN_STATE_CHANGE({ hideState, fromUserInteraction }: { hideState: ChatHideStates, fromUserInteraction: boolean }) {
|
||||
this.hideState = hideState;
|
||||
|
||||
if (this.hideState === ChatHideStates.AlwaysHide) {
|
||||
if (!this.showInput) {
|
||||
this.showWindow = false;
|
||||
}
|
||||
} else if (this.hideState === ChatHideStates.AlwaysShow) {
|
||||
this.showWindow = true;
|
||||
if (this.showWindowTimer) {
|
||||
clearTimeout(this.showWindowTimer);
|
||||
}
|
||||
} else {
|
||||
this.resetShowWindowTimer();
|
||||
}
|
||||
|
||||
if (fromUserInteraction) {
|
||||
this.showHideState = true;
|
||||
|
||||
if (this.showHideStateTimer) {
|
||||
clearTimeout(this.showHideStateTimer);
|
||||
}
|
||||
|
||||
this.showHideStateTimer = window.setTimeout(() => {
|
||||
this.showHideState = false;
|
||||
}, 1500);
|
||||
}
|
||||
},
|
||||
ON_OPEN() {
|
||||
this.showInput = true;
|
||||
this.showWindow = true;
|
||||
if (this.showWindowTimer) {
|
||||
clearTimeout(this.showWindowTimer);
|
||||
}
|
||||
this.focusTimer = window.setInterval(() => {
|
||||
if (this.$refs.input) {
|
||||
(this.$refs.input as HTMLInputElement).focus();
|
||||
} else {
|
||||
clearInterval(this.focusTimer);
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
ON_MESSAGE({ message }: { message: Message }) {
|
||||
message.id = `${new Date().getTime()}${Math.random()}`;
|
||||
message.modeData = this.modes.find(mode => mode.name === message.mode);
|
||||
this.messages.push(message);
|
||||
},
|
||||
ON_CLEAR() {
|
||||
this.messages = [];
|
||||
this.oldMessages = [];
|
||||
this.oldMessagesIndex = -1;
|
||||
},
|
||||
ON_SUGGESTION_ADD({ suggestion }: { suggestion: Suggestion }) {
|
||||
this.removedSuggestions = this.removedSuggestions.filter(a => a !== suggestion.name);
|
||||
const duplicateSuggestion = this.backingSuggestions.find(
|
||||
a => a.name == suggestion.name
|
||||
);
|
||||
if (duplicateSuggestion) {
|
||||
if (suggestion.help || suggestion.params) {
|
||||
duplicateSuggestion.help = suggestion.help || "";
|
||||
duplicateSuggestion.params = suggestion.params || [];
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!suggestion.params) {
|
||||
suggestion.params = []; //TODO Move somewhere else
|
||||
}
|
||||
this.backingSuggestions.push(suggestion);
|
||||
},
|
||||
ON_SUGGESTION_REMOVE({ name }: { name: string }) {
|
||||
if (this.removedSuggestions.indexOf(name) <= -1) {
|
||||
this.removedSuggestions.push(name);
|
||||
}
|
||||
},
|
||||
ON_MODE_ADD({ mode }: { mode: Mode }) {
|
||||
this.modes = [
|
||||
...this.modes.filter(a => a.name !== mode.name),
|
||||
mode
|
||||
];
|
||||
},
|
||||
ON_MODE_REMOVE({ name }: { name: string }) {
|
||||
this.modes = this.modes.filter(a => a.name !== name);
|
||||
|
||||
if (this.modes.length === 0) {
|
||||
this.modes = [defaultMode];
|
||||
}
|
||||
},
|
||||
ON_TEMPLATE_ADD({ template }: { template: { id: string, html: string }}) {
|
||||
if (this.templates[template.id]) {
|
||||
this.warn(`Tried to add duplicate template '${template.id}'`);
|
||||
} else {
|
||||
this.templates[template.id] = template.html;
|
||||
}
|
||||
},
|
||||
ON_UPDATE_THEMES({ themes }: { themes: { [key: string]: ThemeData } }) {
|
||||
this.removeThemes();
|
||||
|
||||
this.setThemes(themes);
|
||||
},
|
||||
removeThemes() {
|
||||
for (let i = 0; i < document.styleSheets.length; i++) {
|
||||
const styleSheet = document.styleSheets[i];
|
||||
const node = styleSheet.ownerNode as Element;
|
||||
|
||||
if (node.getAttribute("data-theme")) {
|
||||
node.parentNode?.removeChild(node);
|
||||
}
|
||||
}
|
||||
|
||||
this.tplBackups.reverse();
|
||||
|
||||
for (const [elem, oldData] of this.tplBackups) {
|
||||
elem.innerText = oldData;
|
||||
}
|
||||
|
||||
this.tplBackups = [];
|
||||
|
||||
this.msgTplBackups.reverse();
|
||||
|
||||
for (const [id, oldData] of this.msgTplBackups) {
|
||||
this.templates[id] = oldData;
|
||||
}
|
||||
|
||||
this.msgTplBackups = [];
|
||||
},
|
||||
setThemes(themes: { [key: string]: ThemeData }) {
|
||||
for (const [id, data] of Object.entries(themes)) {
|
||||
if (data.style) {
|
||||
const style = document.createElement("style");
|
||||
style.type = "text/css";
|
||||
style.setAttribute("data-theme", id);
|
||||
style.appendChild(document.createTextNode(data.style));
|
||||
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
if (data.styleSheet) {
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.type = "text/css";
|
||||
link.href = data.baseUrl + data.styleSheet;
|
||||
link.setAttribute("data-theme", id);
|
||||
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
if (data.templates) {
|
||||
for (const [tplId, tpl] of Object.entries(data.templates)) {
|
||||
const elem = document.getElementById(tplId);
|
||||
|
||||
if (elem) {
|
||||
this.tplBackups.push([elem, elem.innerText]);
|
||||
elem.innerText = tpl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.script) {
|
||||
const script = document.createElement("script");
|
||||
script.type = "text/javascript";
|
||||
script.src = data.baseUrl + data.script;
|
||||
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
if (data.msgTemplates) {
|
||||
for (const [tplId, tpl] of Object.entries(data.msgTemplates)) {
|
||||
this.msgTplBackups.push([tplId, this.templates[tplId]]);
|
||||
this.templates[tplId] = tpl;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
warn(msg: string) {
|
||||
this.messages.push({
|
||||
args: [msg],
|
||||
template: "^3<b>CHAT-WARN</b>: ^0{0}"
|
||||
});
|
||||
},
|
||||
clearShowWindowTimer() {
|
||||
clearTimeout(this.showWindowTimer);
|
||||
},
|
||||
resetShowWindowTimer() {
|
||||
this.clearShowWindowTimer();
|
||||
this.showWindowTimer = window.setTimeout(() => {
|
||||
if (this.hideState !== ChatHideStates.AlwaysShow && !this.showInput) {
|
||||
this.showWindow = false;
|
||||
}
|
||||
}, CONFIG.fadeTimeout);
|
||||
},
|
||||
keyUp() {
|
||||
this.resize();
|
||||
},
|
||||
keyDown(e: KeyboardEvent) {
|
||||
if (e.which === 38 || e.which === 40) {
|
||||
e.preventDefault();
|
||||
this.moveOldMessageIndex(e.which === 38);
|
||||
} else if (e.which == 33) {
|
||||
var buf = document.getElementsByClassName("chat-messages")[0];
|
||||
buf.scrollTop = buf.scrollTop - 100;
|
||||
} else if (e.which == 34) {
|
||||
var buf = document.getElementsByClassName("chat-messages")[0];
|
||||
buf.scrollTop = buf.scrollTop + 100;
|
||||
} else if (e.which === 9) { // tab
|
||||
if (e.shiftKey || e.altKey) {
|
||||
do {
|
||||
--this.modeIdx;
|
||||
|
||||
if (this.modeIdx < 0) {
|
||||
this.modeIdx = this.modes.length - 1;
|
||||
}
|
||||
} while (this.modes[this.modeIdx].hidden);
|
||||
} else {
|
||||
do {
|
||||
this.modeIdx = (this.modeIdx + 1) % this.modes.length;
|
||||
} while (this.modes[this.modeIdx].hidden);
|
||||
}
|
||||
|
||||
const buf = document.getElementsByClassName('chat-messages')[0];
|
||||
setTimeout(() => buf.scrollTop = buf.scrollHeight, 0);
|
||||
}
|
||||
|
||||
this.resize();
|
||||
},
|
||||
moveOldMessageIndex(up: boolean) {
|
||||
if (up && this.oldMessages.length > this.oldMessagesIndex + 1) {
|
||||
this.oldMessagesIndex += 1;
|
||||
this.message = this.oldMessages[this.oldMessagesIndex];
|
||||
} else if (!up && this.oldMessagesIndex - 1 >= 0) {
|
||||
this.oldMessagesIndex -= 1;
|
||||
this.message = this.oldMessages[this.oldMessagesIndex];
|
||||
} else if (!up && this.oldMessagesIndex - 1 === -1) {
|
||||
this.oldMessagesIndex = -1;
|
||||
this.message = "";
|
||||
}
|
||||
},
|
||||
resize() {
|
||||
const input = this.$refs.input as HTMLInputElement;
|
||||
|
||||
// scrollHeight includes padding, but content-box excludes padding
|
||||
// remove padding before setting height on the element
|
||||
const style = getComputedStyle(input);
|
||||
const paddingRemove = parseFloat(style.paddingBottom) + parseFloat(style.paddingTop);
|
||||
|
||||
input.style.height = "5px";
|
||||
input.style.height = `${input.scrollHeight - paddingRemove}px`;
|
||||
},
|
||||
send() {
|
||||
if (this.message !== "") {
|
||||
post(
|
||||
"http://chat/chatResult",
|
||||
JSON.stringify({
|
||||
message: this.message,
|
||||
mode: this.modes[this.modeIdxGet].name
|
||||
})
|
||||
);
|
||||
this.oldMessages.unshift(this.message);
|
||||
this.oldMessagesIndex = -1;
|
||||
this.hideInput();
|
||||
} else {
|
||||
this.hideInput(true);
|
||||
}
|
||||
},
|
||||
hideInput(canceled = false) {
|
||||
setTimeout(() => {
|
||||
const input = this.$refs.input as HTMLInputElement;
|
||||
delete input.style.height;
|
||||
}, 50);
|
||||
|
||||
if (canceled) {
|
||||
post("http://chat/chatResult", JSON.stringify({ canceled }));
|
||||
}
|
||||
this.message = "";
|
||||
this.showInput = false;
|
||||
clearInterval(this.focusTimer);
|
||||
|
||||
if (this.hideState !== ChatHideStates.AlwaysHide) {
|
||||
this.resetShowWindowTimer();
|
||||
} else {
|
||||
this.showWindow = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<div class="chat-window" :style="this.style" :class="{
|
||||
'animated': !showWindow && hideAnimated,
|
||||
'hidden': !showWindow
|
||||
}">
|
||||
<div class="chat-messages" ref="messages">
|
||||
<message v-for="msg in filteredMessages"
|
||||
:templates="templates"
|
||||
:multiline="msg.multiline"
|
||||
:args="msg.args"
|
||||
:params="msg.params"
|
||||
:color="msg.color"
|
||||
:template="msg.template"
|
||||
:template-id="msg.templateId"
|
||||
:key="msg.id">
|
||||
</message>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-input">
|
||||
<div v-show="showInput" class="input">
|
||||
<span class="prefix" :class="{ any: modes.length > 1 }" :style="{ color: modeColor }">{{modePrefix}}</span>
|
||||
<textarea v-model="message"
|
||||
ref="input"
|
||||
type="text"
|
||||
autofocus
|
||||
spellcheck="false"
|
||||
rows="1"
|
||||
@keyup.esc="hideInput"
|
||||
@keyup="keyUp"
|
||||
@keydown="keyDown"
|
||||
@keypress.enter.prevent="send">
|
||||
</textarea>
|
||||
</div>
|
||||
<suggestions :message="message" :suggestions="suggestions">
|
||||
</suggestions>
|
||||
<div class="chat-hide-state" v-show="showHideState">
|
||||
{{hideStateString}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./App.ts"></script>
|
||||
@@ -1,102 +0,0 @@
|
||||
import CONFIG from './config';
|
||||
import Vue, { PropType } from 'vue';
|
||||
|
||||
export default Vue.component('message', {
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
textEscaped(): string {
|
||||
let s = this.template ? this.template : this.templates[this.templateId];
|
||||
|
||||
//This hack is required to preserve backwards compatability
|
||||
if (!this.template && this.templateId == CONFIG.defaultTemplateId
|
||||
&& this.args.length == 1) {
|
||||
s = this.templates[CONFIG.defaultAltTemplateId] //Swap out default template :/
|
||||
}
|
||||
|
||||
s = s.replace(`@default`, this.templates[this.templateId]);
|
||||
|
||||
s = s.replace(/{(\d+)}/g, (match, number) => {
|
||||
const argEscaped = this.args[number] != undefined ? this.escape(this.args[number]) : match;
|
||||
if (number == 0 && this.color) {
|
||||
//color is deprecated, use templates or ^1 etc.
|
||||
return this.colorizeOld(argEscaped);
|
||||
}
|
||||
return argEscaped;
|
||||
});
|
||||
|
||||
// format variant args
|
||||
s = s.replace(/\{\{([a-zA-Z0-9_\-]+?)\}\}/g, (match, id) => {
|
||||
const argEscaped = this.params[id] != undefined ? this.escape(this.params[id]) : match;
|
||||
return argEscaped;
|
||||
});
|
||||
|
||||
return this.colorize(s);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
colorizeOld(str: string): string {
|
||||
return `<span style="color: rgb(${this.color[0]}, ${this.color[1]}, ${this.color[2]})">${str}</span>`
|
||||
},
|
||||
colorize(str: string): string {
|
||||
let s = "<span>" + colorTrans(str) + "</span>";
|
||||
|
||||
const styleDict: {[ key: string ]: string} = {
|
||||
'*': 'font-weight: bold;',
|
||||
'_': 'text-decoration: underline;',
|
||||
'~': 'text-decoration: line-through;',
|
||||
'=': 'text-decoration: underline line-through;',
|
||||
'r': 'text-decoration: none;font-weight: normal;',
|
||||
};
|
||||
|
||||
const styleRegex = /\^(\_|\*|\=|\~|\/|r)(.*?)(?=$|\^r|<\/em>)/;
|
||||
while (s.match(styleRegex)) { //Any better solution would be appreciated :P
|
||||
s = s.replace(styleRegex, (str, style, inner) => `<em style="${styleDict[style]}">${inner}</em>`)
|
||||
}
|
||||
return s.replace(/<span[^>]*><\/span[^>]*>/g, '');
|
||||
|
||||
function colorTrans(str: string) {
|
||||
return str
|
||||
.replace(/\^([0-9])/g, (str, color) => `</span><span class="color-${color}">`)
|
||||
.replace(/\^#([0-9A-F]{3,6})/gi, (str, color) => `</span><span class="color" style="color: #${color}">`)
|
||||
.replace(/~([a-z])~/g, (str, color) => `</span><span class="gameColor-${color}">`);
|
||||
}
|
||||
},
|
||||
escape(unsafe: string): string {
|
||||
return String(unsafe)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
},
|
||||
},
|
||||
props: {
|
||||
templates: {
|
||||
type: Object as PropType<{ [key: string]: string }>,
|
||||
},
|
||||
args: {
|
||||
type: Array as PropType<string[]>,
|
||||
},
|
||||
params: {
|
||||
type: Object as PropType<{ [ key: string]: string }>,
|
||||
},
|
||||
template: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
templateId: {
|
||||
type: String,
|
||||
default: CONFIG.defaultTemplateId,
|
||||
},
|
||||
multiline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
color: { //deprecated
|
||||
type: Array as PropType<number[]>,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<div class="msg" :class="{ multiline }">
|
||||
<span v-html="textEscaped"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./Message.ts"></script>
|
||||
@@ -1,63 +0,0 @@
|
||||
import CONFIG from './config';
|
||||
import Vue, { PropType } from 'vue';
|
||||
|
||||
export interface Suggestion {
|
||||
name: string;
|
||||
help: string;
|
||||
params: string[];
|
||||
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export default Vue.component('suggestions', {
|
||||
props: {
|
||||
message: {
|
||||
type: String
|
||||
},
|
||||
|
||||
suggestions: {
|
||||
type: Array as PropType<Suggestion[]>
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
currentSuggestions(): Suggestion[] {
|
||||
if (this.message === '') {
|
||||
return [];
|
||||
}
|
||||
const currentSuggestions = this.suggestions.filter((s) => {
|
||||
if (!s.name.startsWith(this.message)) {
|
||||
const suggestionSplitted = s.name.split(' ');
|
||||
const messageSplitted = this.message.split(' ');
|
||||
for (let i = 0; i < messageSplitted.length; i += 1) {
|
||||
if (i >= suggestionSplitted.length) {
|
||||
return i < suggestionSplitted.length + s.params.length;
|
||||
}
|
||||
if (suggestionSplitted[i] !== messageSplitted[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}).slice(0, CONFIG.suggestionLimit);
|
||||
|
||||
currentSuggestions.forEach((s) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
s.disabled = !s.name.startsWith(this.message);
|
||||
|
||||
s.params.forEach((p, index) => {
|
||||
const wType = (index === s.params.length - 1) ? '.' : '\\S';
|
||||
const regex = new RegExp(`${s.name} (?:\\w+ ){${index}}(?:${wType}*)$`, 'g');
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
// @ts-ignore
|
||||
p.disabled = this.message.match(regex) == null;
|
||||
});
|
||||
});
|
||||
return currentSuggestions;
|
||||
},
|
||||
},
|
||||
methods: {},
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
<template>
|
||||
<div class="suggestions-wrap" v-show="currentSuggestions.length > 0">
|
||||
<ul class="suggestions">
|
||||
<li class="suggestion" v-for="s in currentSuggestions" :key="s.name">
|
||||
<p>
|
||||
<span :class="{ 'disabled': s.disabled }">
|
||||
{{s.name}}
|
||||
</span>
|
||||
<span class="param"
|
||||
v-for="p in s.params"
|
||||
:class="{ 'disabled': p.disabled }"
|
||||
:key="p.name">
|
||||
[{{p.name}}]
|
||||
</span>
|
||||
</p>
|
||||
<small class="help">
|
||||
<template v-if="!s.disabled">
|
||||
{{s.help}}
|
||||
</template>
|
||||
<template v-for="p in s.params" v-if="!p.disabled">
|
||||
{{p.help}}
|
||||
</template>
|
||||
</small>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./Suggestions.ts"></script>
|
||||
@@ -1,17 +0,0 @@
|
||||
export default {
|
||||
defaultTemplateId: 'default', //This is the default template for 2 args1
|
||||
defaultAltTemplateId: 'defaultAlt', //This one for 1 arg
|
||||
templates: { //You can add static templates here
|
||||
'default': '<b>{0}</b>: {1}',
|
||||
'defaultAlt': '{0}',
|
||||
'print': '<pre>{0}</pre>',
|
||||
'example:important': '<h1>^2{0}</h1>'
|
||||
},
|
||||
fadeTimeout: 7000,
|
||||
suggestionLimit: 5,
|
||||
style: {
|
||||
background: 'rgba(52, 73, 94, 0.7)',
|
||||
width: '38vw',
|
||||
height: '22%',
|
||||
}
|
||||
};
|
||||
@@ -1,160 +0,0 @@
|
||||
.color-0{color: #ffffff;}
|
||||
.color-1{color: #ff4444;}
|
||||
.color-2{color: #99cc00;}
|
||||
.color-3{color: #ffbb33;}
|
||||
.color-4{color: #0099cc;}
|
||||
.color-5{color: #33b5e5;}
|
||||
.color-6{color: #aa66cc;}
|
||||
.color-8{color: #cc0000;}
|
||||
.color-9{color: #cc0068;}
|
||||
|
||||
.gameColor-w{color: #ffffff;}
|
||||
.gameColor-r{color: #ff4444;}
|
||||
.gameColor-g{color: #99cc00;}
|
||||
.gameColor-y{color: #ffbb33;}
|
||||
.gameColor-b{color: #33b5e5;}
|
||||
|
||||
/* todo: more game colors */
|
||||
|
||||
* {
|
||||
font-family: 'Lato', sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.no-grow {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
#app {
|
||||
font-family: 'Lato', Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chat-window {
|
||||
position: absolute;
|
||||
top: 1.5%;
|
||||
left: 0.8%;
|
||||
width: 38%;
|
||||
height: 22%;
|
||||
max-width: 1000px;
|
||||
background-color: rgba(52, 73, 94, 0.7);
|
||||
-webkit-animation-duration: 2s;
|
||||
}
|
||||
|
||||
|
||||
.chat-messages {
|
||||
position: relative;
|
||||
height: 95%;
|
||||
font-size: 1.8vh;
|
||||
margin: 1%;
|
||||
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
|
||||
.chat-input {
|
||||
font-size: 1.65vh;
|
||||
position: absolute;
|
||||
|
||||
top: 23.8%;
|
||||
left: 0.8%;
|
||||
width: 38%;
|
||||
max-width: 1000px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.chat-input > div.input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
background-color: rgba(44, 62, 80, 1.0);
|
||||
}
|
||||
|
||||
.chat-hide-state {
|
||||
text-transform: uppercase;
|
||||
margin-left: 0.05vw;
|
||||
font-size: 1.65vh;
|
||||
}
|
||||
|
||||
.prefix {
|
||||
font-size: 1.8vh;
|
||||
/*position: absolute;
|
||||
top: 0%;*/
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
line-height: calc(1vh + 1vh + 1.85vh);
|
||||
padding-left: 0.5vh;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-size: 1.65vh;
|
||||
line-height: 1.85vh;
|
||||
display: block;
|
||||
box-sizing: content-box;
|
||||
padding: 1vh;
|
||||
padding-left: 0.5vh;
|
||||
color: white;
|
||||
border-width: 0;
|
||||
height: 3.15%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
textarea:focus, input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.msg {
|
||||
margin-bottom: 0.28%;
|
||||
}
|
||||
|
||||
.multiline {
|
||||
margin-left: 4%;
|
||||
text-indent: -1.2rem;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
list-style-type: none;
|
||||
padding: 0.5%;
|
||||
padding-left: 1.4%;
|
||||
font-size: 1.65vh;
|
||||
box-sizing: border-box;
|
||||
color: white;
|
||||
background-color: rgba(44, 62, 80, 1.0);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.help {
|
||||
color: #b0bbbd;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
color: #b0bbbd;
|
||||
}
|
||||
|
||||
.suggestion {
|
||||
margin-bottom: 0.5%;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.hidden.animated {
|
||||
transition: opacity 1s;
|
||||
}
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
declare module '*.vue' {
|
||||
import Vue from 'vue'
|
||||
export default Vue
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title></title>
|
||||
<link href="/html/vendor/latofonts.css" rel="stylesheet">
|
||||
<link href="/html/vendor/flexboxgrid.6.3.1.min.css" rel="stylesheet"></link>
|
||||
<link href="/html/vendor/animate.3.5.2.min.css" rel="stylesheet"></link>
|
||||
<link href="index.css" rel="stylesheet"></link>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,7 +0,0 @@
|
||||
import Vue from 'vue';
|
||||
import App from './App.vue';
|
||||
|
||||
const instance = new Vue({
|
||||
el: '#app',
|
||||
render: h => h(App),
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./",
|
||||
"module": "es6",
|
||||
"strict": true,
|
||||
"moduleResolution": "node",
|
||||
"target": "es6",
|
||||
"allowJs": true,
|
||||
"lib": [
|
||||
"es2017",
|
||||
"dom"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./**/*"
|
||||
],
|
||||
"exclude": []
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
export function post(url: string, data: any) {
|
||||
var request = new XMLHttpRequest();
|
||||
request.open('POST', url, true);
|
||||
request.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
request.send(data);
|
||||
}
|
||||
|
||||
function emulate(type: string, detail = {}) {
|
||||
const detailRef = {
|
||||
type,
|
||||
...detail
|
||||
};
|
||||
|
||||
window.dispatchEvent(new CustomEvent('message', {
|
||||
detail: detailRef
|
||||
}));
|
||||
}
|
||||
|
||||
(window as any)['emulate'] = emulate;
|
||||
|
||||
(window as any)['demo'] = () => {
|
||||
emulate('ON_MESSAGE', {
|
||||
message: {
|
||||
args: [ 'me', 'hello!' ]
|
||||
}
|
||||
})
|
||||
|
||||
emulate('ON_SCREEN_STATE_CHANGE', {
|
||||
shouldHide: false
|
||||
});
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,48 +0,0 @@
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Lato Light'), local('Lato-Light'), url(fonts/LatoLight.woff2);
|
||||
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Lato Light'), local('Lato-Light'), url(fonts/LatoLight2.woff2);
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Lato Regular'), local('Lato-Regular'), url(fonts/LatoRegular.woff2);
|
||||
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Lato Regular'), local('Lato-Regular'), url(fonts/LatoRegular2.woff2);
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Lato Bold'), local('Lato-Bold'), url(fonts/LatoBold.woff2);
|
||||
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Lato Bold'), local('Lato-Bold'), url(fonts/LatoBold2.woff2);
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"name": "chat",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/vue": "^2.0.0",
|
||||
"copy-webpack-plugin": "^5.1.1",
|
||||
"css-loader": "^3.4.2",
|
||||
"html-webpack-inline-source-plugin": "^0.0.10",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"ts-loader": "^6.2.1",
|
||||
"typescript": "^3.8.3",
|
||||
"vue": "^2.6.11",
|
||||
"vue-loader": "^15.9.0",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"webpack": "4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"command-line-args": "^5.1.1",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-dev-server": "^3.10.3"
|
||||
}
|
||||
}
|
||||
@@ -1,293 +0,0 @@
|
||||
RegisterServerEvent('chat:init')
|
||||
RegisterServerEvent('chat:addTemplate')
|
||||
RegisterServerEvent('chat:addMessage')
|
||||
RegisterServerEvent('chat:addSuggestion')
|
||||
RegisterServerEvent('chat:removeSuggestion')
|
||||
RegisterServerEvent('_chat:messageEntered')
|
||||
RegisterServerEvent('chat:clear')
|
||||
RegisterServerEvent('__cfx_internal:commandFallback')
|
||||
|
||||
-- this is a built-in event, but somehow needs to be registered
|
||||
RegisterNetEvent('playerJoining')
|
||||
|
||||
exports('addMessage', function(target, message)
|
||||
if not message then
|
||||
message = target
|
||||
target = -1
|
||||
end
|
||||
|
||||
if not target or not message then return end
|
||||
|
||||
TriggerClientEvent('chat:addMessage', target, message)
|
||||
end)
|
||||
|
||||
local hooks = {}
|
||||
local hookIdx = 1
|
||||
|
||||
exports('registerMessageHook', function(hook)
|
||||
local resource = GetInvokingResource()
|
||||
hooks[hookIdx + 1] = {
|
||||
fn = hook,
|
||||
resource = resource
|
||||
}
|
||||
|
||||
hookIdx = hookIdx + 1
|
||||
end)
|
||||
|
||||
local modes = {}
|
||||
|
||||
local function getMatchingPlayers(seObject)
|
||||
local players = GetPlayers()
|
||||
local retval = {}
|
||||
|
||||
for _, v in ipairs(players) do
|
||||
if IsPlayerAceAllowed(v, seObject) then
|
||||
retval[#retval + 1] = v
|
||||
end
|
||||
end
|
||||
|
||||
return retval
|
||||
end
|
||||
|
||||
exports('registerMode', function(modeData)
|
||||
if not modeData.name or not modeData.displayName or not modeData.cb then
|
||||
return false
|
||||
end
|
||||
|
||||
local resource = GetInvokingResource()
|
||||
|
||||
modes[modeData.name] = modeData
|
||||
modes[modeData.name].resource = resource
|
||||
|
||||
local clObj = {
|
||||
name = modeData.name,
|
||||
displayName = modeData.displayName,
|
||||
color = modeData.color or '#fff',
|
||||
isChannel = modeData.isChannel,
|
||||
isGlobal = modeData.isGlobal,
|
||||
}
|
||||
|
||||
if not modeData.seObject then
|
||||
TriggerClientEvent('chat:addMode', -1, clObj)
|
||||
else
|
||||
for _, v in ipairs(getMatchingPlayers(modeData.seObject)) do
|
||||
TriggerClientEvent('chat:addMode', v, clObj)
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end)
|
||||
|
||||
local function unregisterHooks(resource)
|
||||
local toRemove = {}
|
||||
|
||||
for k, v in pairs(hooks) do
|
||||
if v.resource == resource then
|
||||
table.insert(toRemove, k)
|
||||
end
|
||||
end
|
||||
|
||||
for _, v in ipairs(toRemove) do
|
||||
hooks[v] = nil
|
||||
end
|
||||
|
||||
toRemove = {}
|
||||
|
||||
for k, v in pairs(modes) do
|
||||
if v.resource == resource then
|
||||
table.insert(toRemove, k)
|
||||
end
|
||||
end
|
||||
|
||||
for _, v in ipairs(toRemove) do
|
||||
TriggerClientEvent('chat:removeMode', -1, {
|
||||
name = v
|
||||
})
|
||||
|
||||
modes[v] = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function routeMessage(source, author, message, mode, fromConsole)
|
||||
if source >= 1 then
|
||||
author = GetPlayerName(source)
|
||||
end
|
||||
|
||||
local outMessage = {
|
||||
color = { 255, 255, 255 },
|
||||
multiline = true,
|
||||
args = { message },
|
||||
mode = mode
|
||||
}
|
||||
|
||||
if author ~= "" then
|
||||
outMessage.args = { author, message }
|
||||
end
|
||||
|
||||
if mode and modes[mode] then
|
||||
local modeData = modes[mode]
|
||||
|
||||
if modeData.seObject and not IsPlayerAceAllowed(source, modeData.seObject) then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local messageCanceled = false
|
||||
local routingTarget = -1
|
||||
|
||||
local hookRef = {
|
||||
updateMessage = function(t)
|
||||
-- shallow merge
|
||||
for k, v in pairs(t) do
|
||||
if k == 'template' then
|
||||
outMessage['template'] = v:gsub('%{%}', outMessage['template'] or '@default')
|
||||
elseif k == 'params' then
|
||||
if not outMessage.params then
|
||||
outMessage.params = {}
|
||||
end
|
||||
|
||||
for pk, pv in pairs(v) do
|
||||
outMessage.params[pk] = pv
|
||||
end
|
||||
else
|
||||
outMessage[k] = v
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
cancel = function()
|
||||
messageCanceled = true
|
||||
end,
|
||||
|
||||
setSeObject = function(object)
|
||||
routingTarget = getMatchingPlayers(object)
|
||||
end,
|
||||
|
||||
setRouting = function(target)
|
||||
routingTarget = target
|
||||
end
|
||||
}
|
||||
|
||||
for _, hook in pairs(hooks) do
|
||||
if hook.fn then
|
||||
hook.fn(source, outMessage, hookRef)
|
||||
end
|
||||
end
|
||||
|
||||
if modes[mode] then
|
||||
local m = modes[mode]
|
||||
|
||||
m.cb(source, outMessage, hookRef)
|
||||
end
|
||||
|
||||
if messageCanceled then
|
||||
return
|
||||
end
|
||||
|
||||
TriggerEvent('chatMessage', source, #outMessage.args > 1 and outMessage.args[1] or '', outMessage.args[#outMessage.args])
|
||||
|
||||
if not WasEventCanceled() then
|
||||
if type(routingTarget) ~= 'table' then
|
||||
TriggerClientEvent('chat:addMessage', routingTarget, outMessage)
|
||||
else
|
||||
for _, id in ipairs(routingTarget) do
|
||||
TriggerClientEvent('chat:addMessage', id, outMessage)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not fromConsole then
|
||||
print(author .. '^7' .. (modes[mode] and (' (' .. modes[mode].displayName .. ')') or '') .. ': ' .. message .. '^7')
|
||||
end
|
||||
end
|
||||
|
||||
AddEventHandler('_chat:messageEntered', function(author, color, message, mode)
|
||||
if not message or not author then
|
||||
return
|
||||
end
|
||||
|
||||
local source = source
|
||||
|
||||
routeMessage(source, author, message, mode)
|
||||
end)
|
||||
|
||||
AddEventHandler('__cfx_internal:commandFallback', function(command)
|
||||
local name = GetPlayerName(source)
|
||||
|
||||
-- route the message as if it were a /command
|
||||
routeMessage(source, name, '/' .. command, nil, true)
|
||||
|
||||
CancelEvent()
|
||||
end)
|
||||
|
||||
-- player join messages
|
||||
AddEventHandler('playerJoining', function()
|
||||
if GetConvarInt('chat_showJoins', 1) == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
TriggerClientEvent('chatMessage', -1, '', { 255, 255, 255 }, '^2* ' .. GetPlayerName(source) .. ' joined.')
|
||||
end)
|
||||
|
||||
AddEventHandler('playerDropped', function(reason)
|
||||
if GetConvarInt('chat_showQuits', 1) == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
TriggerClientEvent('chatMessage', -1, '', { 255, 255, 255 }, '^2* ' .. GetPlayerName(source) ..' left (' .. reason .. ')')
|
||||
end)
|
||||
|
||||
RegisterCommand('say', function(source, args, rawCommand)
|
||||
routeMessage(source, (source == 0) and 'console' or GetPlayerName(source), rawCommand:sub(5), nil, true)
|
||||
end)
|
||||
|
||||
-- command suggestions for clients
|
||||
local function refreshCommands(player)
|
||||
if GetRegisteredCommands then
|
||||
local registeredCommands = GetRegisteredCommands()
|
||||
|
||||
local suggestions = {}
|
||||
|
||||
for _, command in ipairs(registeredCommands) do
|
||||
if IsPlayerAceAllowed(player, ('command.%s'):format(command.name)) then
|
||||
table.insert(suggestions, {
|
||||
name = '/' .. command.name,
|
||||
help = ''
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
TriggerClientEvent('chat:addSuggestions', player, suggestions)
|
||||
end
|
||||
end
|
||||
|
||||
AddEventHandler('chat:init', function()
|
||||
local source = source
|
||||
refreshCommands(source)
|
||||
|
||||
for _, modeData in pairs(modes) do
|
||||
local clObj = {
|
||||
name = modeData.name,
|
||||
displayName = modeData.displayName,
|
||||
color = modeData.color or '#fff',
|
||||
isChannel = modeData.isChannel,
|
||||
isGlobal = modeData.isGlobal,
|
||||
}
|
||||
|
||||
if not modeData.seObject or IsPlayerAceAllowed(source, modeData.seObject) then
|
||||
TriggerClientEvent('chat:addMode', source, clObj)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler('onServerResourceStart', function(resName)
|
||||
Wait(500)
|
||||
|
||||
for _, player in ipairs(GetPlayers()) do
|
||||
refreshCommands(player)
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler('onResourceStop', function(resName)
|
||||
unregisterHooks(resName)
|
||||
end)
|
||||
@@ -1,45 +0,0 @@
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin');
|
||||
const VueLoaderPlugin = require('vue-loader/lib/plugin');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
mode: 'production',
|
||||
entry: './html/main.ts',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
loader: 'ts-loader',
|
||||
exclude: /node_modules/,
|
||||
options: {
|
||||
appendTsSuffixTo: [/\.vue$/],
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader'
|
||||
},
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new VueLoaderPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
inlineSource: '.(js|css)$',
|
||||
template: './html/index.html',
|
||||
filename: 'ui.html'
|
||||
}),
|
||||
new HtmlWebpackInlineSourcePlugin(),
|
||||
new CopyPlugin([
|
||||
{ from: 'html/index.css', to: 'index.css' }
|
||||
]),
|
||||
],
|
||||
resolve: {
|
||||
extensions: [ '.ts', '.js' ]
|
||||
},
|
||||
output: {
|
||||
filename: 'chat.js',
|
||||
path: __dirname + '/dist/'
|
||||
},
|
||||
//devtool: 'inline-source-map'
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user