997 lines
29 KiB
JavaScript
997 lines
29 KiB
JavaScript
/*
|
|
* v0.5.1 generated at Fri Aug 2 10:04:04 AM UTC 2024
|
|
* https://xrfragment.org
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
(function(){
|
|
// this orchestrates multiplayer events from the scene graph
|
|
|
|
window.network = (opts) => new Proxy({
|
|
|
|
connected: false,
|
|
pos: '',
|
|
posName: '',
|
|
meetinglink: "",
|
|
peers: {},
|
|
plugin: {},
|
|
opts,
|
|
|
|
init(){
|
|
document.addEventListener('network.disconnect', () => this.connected = false )
|
|
document.addEventListener('network.connected', () => this.connected = true )
|
|
setTimeout( () => window.frontend.emit('network.init'), 100 )
|
|
return this
|
|
},
|
|
|
|
connect(opts){
|
|
window.frontend.emit(`network.${this.connected?'disconnect':'connect'}`,opts)
|
|
},
|
|
|
|
add(peerid,data){
|
|
data = {lastUpdated: new Date().getTime(), id: peerid, ...data }
|
|
this.peers[peerid] = data
|
|
window.frontend.emit(`network.peer.add`,{peer})
|
|
},
|
|
|
|
remove(peerid,data){
|
|
delete this.peers[peerid]
|
|
window.frontend.emit(`network.peer.remove`,{peer})
|
|
},
|
|
|
|
send(opts){
|
|
window.frontend.emit('network.send',opts)
|
|
},
|
|
|
|
receive(opts){
|
|
|
|
},
|
|
|
|
getMeetingFromUrl(url){
|
|
let hash = url.replace(/.*#/,'')
|
|
let parts = hash.split("&")
|
|
let meeting = ''
|
|
parts.map( (p) => {
|
|
if( p.split("=")[0] == 'meet' ) meeting = p.split("=")[1]
|
|
})
|
|
return meeting
|
|
},
|
|
|
|
randomRoom(){
|
|
var names = []
|
|
let add = (s) => s.length < 6 && !s.match(/[0-9$]/) && !s.match(/_/) ? names.push(s) : false
|
|
for ( var i in window ) add(i)
|
|
for ( var i in Object.prototype ) add(i)
|
|
for ( var i in Function.prototype ) add(i)
|
|
for ( var i in Array.prototype ) add(i)
|
|
for ( var i in String.prototype ) add(i)
|
|
var a = names[Math.floor(Math.random() * names.length)];
|
|
var b = names[Math.floor(Math.random() * names.length)];
|
|
return String(`${a}-${b}-${String(Math.random()).substr(13)}`).toLowerCase()
|
|
}
|
|
|
|
},
|
|
{
|
|
// auto-trigger events on changes
|
|
get(data,k,receiver){ return data[k] },
|
|
set(data,k,v){
|
|
let from = data[k]
|
|
data[k] = v
|
|
}
|
|
})
|
|
|
|
document.addEventListener('frontend:ready', (e) => {
|
|
window.network = network(e.detail).init()
|
|
document.dispatchEvent( new CustomEvent("network:ready", e ) )
|
|
})
|
|
connectionsComponent = {
|
|
|
|
html: `
|
|
<div id="connections">
|
|
<i class="gg-close-o" id="close" onclick="$connections.visible = false"></i>
|
|
<br>
|
|
<div class="tab-frame">
|
|
<input type="radio" name="tab" id="login" checked>
|
|
<label for="login">login</label>
|
|
|
|
<input type="radio" name="tab" id="io">
|
|
<label for="io">devices</label>
|
|
|
|
<input type="radio" name="tab" id="networks">
|
|
<label for="networks">advanced</label>
|
|
|
|
<div class="tab">
|
|
<div id="settings"></div>
|
|
<table>
|
|
<tr>
|
|
<td></td>
|
|
<td>
|
|
<button id="connect" onclick="network.connect( $connections )">📡 Connect!</button>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="tab">
|
|
<div id="devices">
|
|
<a class="badge ruler">Webcam and/or Audio</a>
|
|
<table>
|
|
<tr>
|
|
<td>Video</td>
|
|
<td>
|
|
<select id="videoInput"></select>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Mic</td>
|
|
<td>
|
|
<select id="audioInput"></select>
|
|
</td>
|
|
</tr>
|
|
<tr style="display:none"> <!-- not used (for now) -->
|
|
<td>Audio</td>
|
|
<td>
|
|
<select id="audioOutput"></select>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tab">
|
|
<div id="networking">
|
|
Networking a la carte:<br>
|
|
<table>
|
|
<tr>
|
|
<td>Webcam</td>
|
|
<td>
|
|
<select id="webcam"></select>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Chat</td>
|
|
<td>
|
|
<select id="chatnetwork"></select>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>World sync</td>
|
|
<td>
|
|
<select id="scene"></select>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`,
|
|
|
|
init: (el) => new Proxy({
|
|
|
|
visible: true,
|
|
|
|
webcam: [{profile:{name:"No thanks"},config: () => document.createElement('div')}],
|
|
chatnetwork: [{profile:{name:"No thanks"},config: () => document.createElement('div')}],
|
|
scene: [{profile:{name:"No thanks"},config: () => document.createElement('div')}],
|
|
|
|
selectedWebcam: '',
|
|
selectedChatnetwork:'',
|
|
selectedScene: '',
|
|
|
|
$webcam: $webcam = el.querySelector("#webcam"),
|
|
$chatnetwork: $chatnetwork = el.querySelector("#chatnetwork"),
|
|
$scene: $scene = el.querySelector("#scene"),
|
|
$settings: $settings = el.querySelector("#settings"),
|
|
$devices: $devices = el.querySelector("#devices"),
|
|
$connect: $connect = el.querySelector("#connect"),
|
|
$networking: $networking = el.querySelector("#networking"),
|
|
|
|
$audioInput: el.querySelector('select#audioInput'),
|
|
$audioOutput: el.querySelector('select#audioOutput'),
|
|
$videoInput: el.querySelector('select#videoInput'),
|
|
|
|
install(opts){
|
|
this.opts = opts;
|
|
(['change']).map( (e) => el.addEventListener(e, (ev) => this[e] && this[e](ev.target.id,ev) ) )
|
|
this.reactToNetwork()
|
|
$menu.buttons = ([
|
|
`<a class="btn" aria-label="button" aria-title="connect button" aria-description="use this to talk or chat with other people" id="meeting" onclick="$connections.show()"><i class="gg-user-add"></i> connect</a><br>`
|
|
]).concat($menu.buttons)
|
|
|
|
if( document.location.href.match(/meet=/) ) this.show()
|
|
|
|
setTimeout( () => document.dispatchEvent( new CustomEvent("$connections:ready", {detail: opts}) ), 1 )
|
|
},
|
|
|
|
toggle(){
|
|
$chat.visible = !$chat.visible
|
|
},
|
|
|
|
change(id,e){
|
|
if( id.match(/^(webcam|chatnetwork|scene)$/) ){
|
|
this.renderSettings() // trigger this when 'change' event fires on children dom elements
|
|
}
|
|
},
|
|
|
|
show(opts){
|
|
opts = opts || {}
|
|
if( opts.hide ){
|
|
if( el.parentElement ) el.parentElement.parentElement.style.display = 'none' // hide along with wrapper elements
|
|
if( !opts.showChat ) $chat.visible = false
|
|
}else{
|
|
$chat.visible = true
|
|
this.visible = true
|
|
// hide networking settings if entering thru meetinglink
|
|
$networking.style.display = document.location.href.match(/meet=/) ? 'none' : 'block'
|
|
if( !network.connected ){
|
|
document.querySelector('body > .xrf').appendChild(el)
|
|
$chat.send({message:"", el, class:['ui']})
|
|
if( !network.meetinglink ){ // set default
|
|
$webcam.value = opts.webcam || 'Peer2Peer'
|
|
$chatnetwork.value = opts.chatnetwork || 'Peer2Peer'
|
|
$scene.value = opts.scene || 'Peer2Peer'
|
|
}
|
|
this.renderSettings()
|
|
}else{
|
|
$chat.send({message:"you are already connected, refresh page to create new connection",class:['info']})
|
|
}
|
|
}
|
|
},
|
|
|
|
update(){
|
|
this.selectedWebcam = $webcam.value
|
|
this.selectedChatnetwork = $chatnetwork.value
|
|
this.selectedScene = $scene.value
|
|
},
|
|
|
|
forSelectedPluginsDo(cb){
|
|
// this function looks weird but it's handy to prevent the same plugins rendering duplicate configurations
|
|
let plugins = {}
|
|
let select = (name) => (o) => o.profile.name == name ? plugins[ o.profile.name ] = o : ''
|
|
this.webcam.find( select(this.selectedWebcam) )
|
|
this.chatnetwork.find( select(this.selectedChatnetwork) )
|
|
this.scene.find( select(this.selectedScene) )
|
|
for( let i in plugins ){
|
|
try{ cb(plugins[i]) }catch(e){ console.error(e) }
|
|
}
|
|
},
|
|
|
|
renderSettings(){
|
|
let opts = {webcam: $webcam.value, chatnetwork: $chatnetwork.value, scene: $scene.value }
|
|
this.update()
|
|
$settings.innerHTML = ''
|
|
this.forSelectedPluginsDo( (plugin) => $settings.appendChild( plugin.config({...opts,plugin}) ) )
|
|
this.renderInputs()
|
|
},
|
|
|
|
renderInputs(){
|
|
if( !this.selectedWebcam || this.selectedWebcam == 'No thanks' ){
|
|
return this.$devices.style.display = 'none'
|
|
}else this.$devices.style.display = ''
|
|
|
|
navigator.mediaDevices.getUserMedia({
|
|
audio: true,
|
|
video: true
|
|
})
|
|
.then( () => {
|
|
|
|
const selectors = [this.$audioInput, this.$audioOutput, this.$videoInput];
|
|
|
|
const gotDevices = (deviceInfos) => {
|
|
// Handles being called several times to update labels. Preserve values.
|
|
const values = selectors.map(select => select.value);
|
|
selectors.forEach(select => {
|
|
while (select.firstChild) {
|
|
select.removeChild(select.firstChild);
|
|
}
|
|
});
|
|
for (let i = 0; i !== deviceInfos.length; ++i) {
|
|
const deviceInfo = deviceInfos[i];
|
|
const option = document.createElement('option');
|
|
option.value = deviceInfo.deviceId;
|
|
if (deviceInfo.kind === 'audioinput') {
|
|
option.text = deviceInfo.label || `microphone ${this.$audioInput.length + 1}`;
|
|
this.$audioInput.appendChild(option);
|
|
} else if (deviceInfo.kind === 'audiooutput') {
|
|
option.text = deviceInfo.label || `speaker ${this.$audioOutput.length + 1}`;
|
|
this.$audioOutput.appendChild(option);
|
|
} else if (deviceInfo.kind === 'videoinput') {
|
|
option.text = deviceInfo.label || `camera this.${this.$videoInput.length + 1}`;
|
|
this.$videoInput.appendChild(option);
|
|
} else {
|
|
console.log('Some other kind of source/device: ', deviceInfo);
|
|
}
|
|
}
|
|
// hide if there's nothing to choose
|
|
let totalDevices = this.$audioInput.options.length + this.$audioOutput.options.length + this.$videoInput.options.length
|
|
this.$devices.style.display = totalDevices > 3 ? 'block' : 'none'
|
|
|
|
selectors.forEach((select, selectorIndex) => {
|
|
if (Array.prototype.slice.call(select.childNodes).some(n => n.value === values[selectorIndex])) {
|
|
select.value = values[selectorIndex];
|
|
}
|
|
});
|
|
}
|
|
// after getUserMedia we can enumerate
|
|
navigator.mediaDevices.enumerateDevices().then(gotDevices).catch(console.warn);
|
|
})
|
|
},
|
|
|
|
reactToNetwork(){ // *TODO* move to network?
|
|
|
|
document.addEventListener('network.connect', () => {
|
|
this.show({hide:true, showChat: true})
|
|
})
|
|
document.addEventListener('network.disconnect', () => {
|
|
this.connected = false
|
|
})
|
|
|
|
}
|
|
|
|
},{
|
|
|
|
get(data,k,v){ return data[k] },
|
|
set(data,k,v){
|
|
data[k] = v
|
|
switch( k ){
|
|
case "visible": el.style.display = v ? '' : 'none';
|
|
if( !v && el.parentNode && el.parentNode.parentNode ) el.parentNode.parentNode.remove()
|
|
break;
|
|
case "webcam": $webcam.innerHTML = `<option>${data[k].map((p)=>p.profile.name).join('</option><option>')}</option>`; break;
|
|
case "chatnetwork": $chatnetwork.innerHTML = `<option>${data[k].map((p)=>p.profile.name).join('</option><option>')}</option>`; break;
|
|
case "scene": $scene.innerHTML = `<option>${data[k].map((p)=>p.profile.name).join('</option><option>')}</option>`; break;
|
|
case "selectedScene": $scene.value = v; data.renderSettings(); break;
|
|
case "selectedChatnetwork": $chatnetwork.value = v; data.renderSettings(); break;
|
|
case "selectedWebcam": {
|
|
$webcam.value = v;
|
|
data.renderSettings();
|
|
$devices.style.display = v ? 'block' : 'none'
|
|
break;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
})
|
|
}
|
|
|
|
// reactify component!
|
|
document.addEventListener('$chat:ready', (opts) => {
|
|
opts = opts.detail
|
|
document.head.innerHTML += connectionsComponent.css
|
|
window.$connections = document.createElement('div')
|
|
$connections.innerHTML = connectionsComponent.html
|
|
$connections = connectionsComponent.init($connections)
|
|
$connections.install(opts)
|
|
})
|
|
|
|
// alpine component for displaying meetings
|
|
|
|
connectionsComponent.css = `
|
|
<style type="text/css">
|
|
button#connect{
|
|
height: 43px;
|
|
width:100%;
|
|
margin: 0px;
|
|
}
|
|
#messages .msg #connections{
|
|
position:relative;
|
|
}
|
|
.connecthide {
|
|
transform:translateY(-1000px);
|
|
}
|
|
#close{
|
|
display: block;
|
|
position: relative;
|
|
float: right;
|
|
top: 16px;
|
|
}
|
|
#messages .msg.ui div.tab-frame > div.tab{ padding:25px 10px 5px 10px;}
|
|
</style>`
|
|
chatComponent = {
|
|
|
|
html: `
|
|
<div id="chat">
|
|
<div id="videos" style="pointer-events:none"></div>
|
|
<div id="messages" aria-live="assertive" role="log" aria-relevant="additions"></div>
|
|
<div id="chatfooter">
|
|
<div id="chatbar">
|
|
<input id="chatline" type="text" placeholder="chat here"></input>
|
|
</div>
|
|
<button id="chatsend" class="btn" aria-label="send message">
|
|
<i class="gg-chevron-right-o"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`,
|
|
|
|
init: (el) => new Proxy({
|
|
|
|
scene: null,
|
|
visible: true,
|
|
messages: [],
|
|
oneMessagePerUser: false,
|
|
|
|
username: '', // configured by 'network.connected' event
|
|
|
|
$videos: el.querySelector("#videos"),
|
|
$messages: el.querySelector("#messages"),
|
|
$chatline: el.querySelector("#chatline"),
|
|
$chatbar: el.querySelector("#chatbar"),
|
|
$chatsend: el.querySelector("#chatsend"),
|
|
|
|
install(opts){
|
|
this.opts = opts
|
|
this.scene = opts.scene
|
|
this.$chatbar.style.display = 'none'
|
|
el.className = "xrf"
|
|
el.style.display = 'none' // start hidden
|
|
document.body.appendChild( el )
|
|
document.dispatchEvent( new CustomEvent("$chat:ready", {detail: opts}) )
|
|
this.send({message:`Welcome to <b>${document.location.search.substr(1)}</b>, a 3D scene(file) which simply links to other ones.<br>You can start a solo offline exploration in XR right away.<br>Type /help below, or use the arrow- or WASD-keys on your keyboard, and mouse-drag to rotate.<br>`, class: ["info","guide","multiline"] })
|
|
},
|
|
|
|
sendInput(value){
|
|
if( value[0] == '#' ) return xrf.navigator.to(value)
|
|
let event = value.match(/^[!\/]/) ? "chat.command" : "network.send"
|
|
let message = value.replace(/^[!\/]/,'')
|
|
let raw = {detail:{message:value, halt:false}}
|
|
document.dispatchEvent( new CustomEvent( event, {detail: {message}} ) )
|
|
document.dispatchEvent( new CustomEvent( "chat.input", raw ) )
|
|
if( event == "network.send" && !raw.detail.halt ) this.send({message: value })
|
|
this.$chatline.lastValue = value
|
|
this.$chatline.value = ''
|
|
if( window.innerHeight < 600 ) this.$chatline.blur()
|
|
},
|
|
|
|
initListeners(){
|
|
let {$chatline} = this
|
|
|
|
$chatline.addEventListener('click', (e) => this.inform() )
|
|
|
|
$chatline.addEventListener('keydown', (e) => {
|
|
if (e.key == 'Enter' ){
|
|
this.sendInput($chatline.value)
|
|
}
|
|
if (e.key == 'ArrowUp' ){
|
|
$chatline.value = $chatline.lastValue || ''
|
|
}
|
|
})
|
|
|
|
document.addEventListener('network.connect', (e) => {
|
|
this.visible = true
|
|
this.$chatbar.style.display = '' // show
|
|
})
|
|
|
|
document.addEventListener('network.connected', (e) => {
|
|
if( e.detail.username ) this.username = e.detail.username
|
|
})
|
|
|
|
document.addEventListener('chat.command', (e) => {
|
|
if( String(e.detail.message).trim() == 'help' ){
|
|
let detail = {message:`The following commands are available
|
|
<br><br>
|
|
<b class="badge">/help</b> shows this help screen
|
|
`}
|
|
document.dispatchEvent( new CustomEvent( 'chat.command.help', {detail}))
|
|
this.send({message: detail.message})
|
|
}
|
|
})
|
|
|
|
this.$chatsend.addEventListener('click', (e) => {
|
|
this.sendInput($chatline.value)
|
|
})
|
|
|
|
},
|
|
|
|
inform(){
|
|
if( !this.inform.informed && (this.inform.informed = true) ){
|
|
window.notify("You can now type messages in the textfield below.")
|
|
}
|
|
},
|
|
|
|
toggle(){
|
|
this.visible = !this.visible
|
|
if( this.visible && window.meeting.status == 'offline' ) window.meeting.start(this.opts)
|
|
},
|
|
|
|
hyphenate(str){
|
|
return String(str).replace(/[^a-zA-Z0-9]/g,'-')
|
|
},
|
|
|
|
// sending messages to the #messages div
|
|
// every user can post maximum one msg at a time
|
|
// it's more like a 'status' which is more friendly
|
|
// for accessibility reasons
|
|
// for a fullfledged chat/transcript see matrix clients
|
|
send(opts){
|
|
let {$messages} = this
|
|
opts = { linebreak:true, message:"", class:[], ...opts }
|
|
if( window.frontend && window.frontend.emit ) window.frontend.emit('$chat.send', opts )
|
|
opts.pos = opts.pos || network.posName || network.pos
|
|
let div = document.createElement('div')
|
|
let msg = document.createElement('div')
|
|
let br = document.createElement('br')
|
|
let nick = document.createElement('div')
|
|
msg.className = "msg"
|
|
let html = `${ opts.message || ''}${ opts.html ? opts.html(opts) : ''}`
|
|
if( $messages.last == html ) return
|
|
msg.innerHTML = html
|
|
if( opts.el ) msg.appendChild(opts.el)
|
|
opts.id = Math.random()
|
|
if( opts.class ){
|
|
msg.classList.add.apply(msg.classList, opts.class)
|
|
br.classList.add.apply(br.classList, opts.class)
|
|
div.classList.add.apply(div.classList, opts.class.concat(["envelope"]))
|
|
}
|
|
if( !msg.className.match(/(info|guide|ui)/) && !opts.from ){
|
|
let frag = xrf.URI.parse(document.location.hash).XRF
|
|
opts.from = 'you'
|
|
if( frag.pos ) opts.pos = frag.pos.string
|
|
msg.classList.add('self')
|
|
}
|
|
if( opts.from ){
|
|
nick.className = "user"
|
|
nick.innerText = opts.from+' '
|
|
div.appendChild(nick)
|
|
if( opts.pos ){
|
|
let a = document.createElement("a")
|
|
a.href = a.innerText = `#pos=${opts.pos}`
|
|
nick.appendChild(a)
|
|
}
|
|
}
|
|
div.appendChild(msg)
|
|
// force one message per user
|
|
if( this.oneMessagePerUser && opts.from ){
|
|
div.id = this.hyphenate(opts.from)
|
|
let oldMsg = $messages.querySelector(`#${div.id}`)
|
|
if( oldMsg ) oldMsg.remove()
|
|
}
|
|
// remove after timeout
|
|
if( opts.timeout ) setTimeout( (div) => div.remove(), opts.timeout, div )
|
|
// finally add the message on top
|
|
$messages.appendChild(div)
|
|
if( opts.linebreak ) div.appendChild(br)
|
|
$messages.scrollTop = $messages.scrollHeight // scroll down
|
|
$messages.last = msg.innerHTML
|
|
},
|
|
|
|
getChatLog(){
|
|
return ([...this.$messages.querySelectorAll('.envelope')])
|
|
.filter( (d) => !d.className.match(/(info|ui)/) )
|
|
.map( (d) => d.innerHTML )
|
|
.join('\n')
|
|
}
|
|
|
|
},{
|
|
|
|
get(me,k,v){ return me[k] },
|
|
set(me,k,v){
|
|
me[k] = v
|
|
switch( k ){
|
|
case "visible": {
|
|
el.style.display = me.visible ? 'block' : 'none'
|
|
if( !el.inited && (el.inited = true) ) me.initListeners()
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
})
|
|
}
|
|
|
|
// reactify component!
|
|
document.addEventListener('$menu:ready', (opts) => {
|
|
opts = opts.detail
|
|
document.head.innerHTML += chatComponent.css
|
|
window.$chat = document.createElement('div')
|
|
$chat.innerHTML = chatComponent.html
|
|
$chat = chatComponent.init($chat)
|
|
$chat.install(opts)
|
|
//$menu.buttons = ([`<a class="btn" aria-label="button" aria-description="toggle text" id="meeting" onclick="$chat.toggle()">📜 toggle text</a><br>`])
|
|
// .concat($menu.buttons)
|
|
})
|
|
|
|
// alpine component for displaying meetings
|
|
|
|
chatComponent.css = `
|
|
<style type="text/css">
|
|
#videos{
|
|
display:grid-auto-columns;
|
|
grid-column-gap:5px;
|
|
margin-bottom:15px;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
bottom: 0;
|
|
right: 0;
|
|
margin: 15px;
|
|
z-index:1000;
|
|
}
|
|
#videos > video{
|
|
border-radius:7px;
|
|
display:inline-block;
|
|
background:black;
|
|
width:80px;
|
|
height:60px;
|
|
margin-right:5px;
|
|
margin-bottom:5px;
|
|
vertical-align:top;
|
|
pointer-events:all;
|
|
}
|
|
#videos > video:hover{
|
|
filter: brightness(1.8);
|
|
cursor:pointer;
|
|
}
|
|
|
|
#chatbar,
|
|
button#chatsend{
|
|
z-index: 1500;
|
|
position: fixed;
|
|
bottom: 24px;
|
|
height: 34px;
|
|
left: 20px;
|
|
width: 48%;
|
|
background: white;
|
|
padding: 0px 0px 0px 15px;
|
|
border-radius: 30px;
|
|
max-width: 500px;
|
|
box-sizing: border-box;
|
|
box-shadow: 0px 0px 5px 5px #0002;
|
|
}
|
|
button#chatsend{
|
|
line-height:0px;
|
|
display:none;
|
|
z-index: 1550;
|
|
color: white;
|
|
border: 0;
|
|
height: 35px;
|
|
background: var(--xrf-dark-gray);
|
|
font-weight: bold;
|
|
width: 20px;
|
|
max-width: 20px;
|
|
border-radius: 20px 0px 0px 20px;
|
|
overflow: hidden;
|
|
}
|
|
#chatbar input{
|
|
border:none;
|
|
width:90%;
|
|
box-sizing:border-box;
|
|
height: 24px;
|
|
font-size: var(--xrf-font-size-2);
|
|
max-width:unset;
|
|
}
|
|
#messages{
|
|
/*
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
max-width: 40%;
|
|
*/
|
|
width:100%;
|
|
box-sizing:border-box;
|
|
align-items: flex-start;
|
|
position: absolute;
|
|
transition:1s;
|
|
top: 77px;
|
|
left: 0;
|
|
bottom: 49px;
|
|
padding: 20px;
|
|
overflow:hidden;
|
|
overflow-y: auto;
|
|
pointer-events:none;
|
|
transition:1s;
|
|
z-index: 100;
|
|
}
|
|
body.menu #messages{
|
|
top:50px;
|
|
}
|
|
#messages:hover {
|
|
pointer-events:all;
|
|
}
|
|
#messages *{
|
|
box-sizing:border-box;
|
|
/*
|
|
pointer-events:none;
|
|
-webkit-user-select:none;
|
|
-moz-user-select:-moz-none;
|
|
-ms-user-select:none;
|
|
user-select:none;
|
|
*/
|
|
}
|
|
#messages .msg{
|
|
transition:all 1s ease;
|
|
background: #fff;
|
|
display: inline-block;
|
|
padding: 1px 17px;
|
|
border-radius: 20px;
|
|
color: #000c;
|
|
margin-bottom: 10px;
|
|
line-height:23px;
|
|
line-height:33px;
|
|
cursor:grabbing;
|
|
border: 1px solid #0002;
|
|
}
|
|
#messages .msg *,
|
|
#messages .user *{
|
|
pointer-events:all;
|
|
-webkit-user-select:text;
|
|
-moz-user-select:-moz-text;
|
|
-ms-user-select:text;
|
|
user-select:text;
|
|
}
|
|
|
|
#messages .msg.self{
|
|
border-radius: 20px;
|
|
background:var(--xrf-dark-gray);
|
|
}
|
|
#messages .msg.self,
|
|
#messages .msg.self div{
|
|
color:#FFF;
|
|
}
|
|
#messages .msg.info{
|
|
background: var(--xrf-white);
|
|
border-radius: 20px;
|
|
color: var(--xrf-dark-gray);
|
|
text-align: left;
|
|
line-height: 19px;
|
|
}
|
|
#messages .msg.info,
|
|
#messages .msg.info *{
|
|
font-size: var(--xrf-font-size-0);
|
|
}
|
|
#messages .msg a {
|
|
text-decoration:underline;
|
|
color: var(--xrf-light-xrf-secondary);
|
|
font-weight:bold;
|
|
transition:0.3s;
|
|
}
|
|
#messages .msg.info a,
|
|
#messages a.ruler{
|
|
color:#FFF;
|
|
}
|
|
#messages .msg a:hover{
|
|
color:#000;
|
|
}
|
|
#messages .msg.ui,
|
|
#messages .msg.ui div{
|
|
background: white;
|
|
border:none;
|
|
color: #333;
|
|
border-radius: 20px;
|
|
margin:0;
|
|
padding:0px 5px 5px 5px;
|
|
}
|
|
#messages.guide, .guide{
|
|
display:unset;
|
|
}
|
|
#messages .guide, .guide{
|
|
display:none;
|
|
}
|
|
br.guide{
|
|
display:inline-block;
|
|
}
|
|
#messages .msg.info a:hover,
|
|
#messages button:hover{
|
|
filter: brightness(1.4);
|
|
}
|
|
#messages .msg.multiline {
|
|
padding: 2px 14px;
|
|
}
|
|
#messages button {
|
|
text-decoration:none;
|
|
margin: 0px 15px 10px 0px;
|
|
background: var(--xrf-primary);
|
|
font-family: var(--xrf-font-sans-serif);
|
|
color: #FFF;
|
|
border-radius: 7px;
|
|
padding: 11px 15px;
|
|
border: 0;
|
|
font-weight: bold;
|
|
box-shadow: 0px 0px 5px 5px #0002;
|
|
pointer-events:all;
|
|
}
|
|
#messages,#chatbar,#chatbar *, #messages *{
|
|
}
|
|
|
|
|
|
#messages button.emoticon,
|
|
#messages .btn.emoticon {
|
|
line-height:2px;
|
|
width: 20px;
|
|
display: inline-block;
|
|
padding: 0px 0px;
|
|
margin: 0;
|
|
vertical-align: middle;
|
|
background: none;
|
|
border: none;
|
|
min-width: 31px;
|
|
box-shadow:none;
|
|
}
|
|
|
|
#messages button.emoticon:hover,
|
|
#messages .btn.emoticon:hover {
|
|
border: 1px solid #ccc !important;
|
|
background:#EEE;
|
|
}
|
|
|
|
.nomargin{
|
|
margin:0;
|
|
}
|
|
|
|
.envelope{
|
|
margin-right:15px;
|
|
width:100%;
|
|
max-width:40%;
|
|
}
|
|
|
|
.envelope,
|
|
.envelope * {
|
|
transition:1s;
|
|
pointer-events:none;
|
|
}
|
|
.envelope a,
|
|
.envelope button,
|
|
.envelope input,
|
|
.envelope textarea,
|
|
.envelope msg,
|
|
.envelope msg * {
|
|
pointer-events:all;
|
|
}
|
|
|
|
.user{
|
|
margin-left:13px;
|
|
font-weight: bold;
|
|
color: var(--xrf-dark-gray);
|
|
}
|
|
.user, .user *{
|
|
font-size: var(--xrf-font-size-0);
|
|
}
|
|
.gg-chevron-right-o {
|
|
color:#FFF;
|
|
box-sizing: border-box;
|
|
position: relative;
|
|
display: block;
|
|
transform: scale(var(--ggs,1));
|
|
width: 22px;
|
|
height: 22px;
|
|
border: 2px solid;
|
|
border-radius: 100px
|
|
}
|
|
|
|
.gg-chevron-right-o::after {
|
|
color:#FFF;
|
|
content: "";
|
|
display: block;
|
|
box-sizing: border-box;
|
|
position: absolute;
|
|
width: 6px;
|
|
height: 6px;
|
|
border-bottom: 2px solid;
|
|
border-right: 2px solid;
|
|
transform: rotate(-45deg);
|
|
left: 5px;
|
|
top: 6px
|
|
}
|
|
</style>`
|
|
window.debug = (opts) => new Proxy({
|
|
opts,
|
|
|
|
enabled: false,
|
|
$console: false,
|
|
|
|
|
|
toggle(){ this.enabled = !this.enabled },
|
|
|
|
settings(){
|
|
this.toggle()
|
|
},
|
|
|
|
init(){
|
|
},
|
|
|
|
setupConsole(){
|
|
// add onscreen console
|
|
let $console = this.$console = document.createElement('pre')
|
|
$console.style.position = 'fixed'
|
|
$console.style.overflow = 'auto'
|
|
$console.style.top = $console.style.left = $console.style.bottom = $console.style.right = '0px'
|
|
$console.style.height = '98.5vh';
|
|
$console.style.width = '100%'
|
|
$console.style.pointerEvents = 'none'
|
|
$console.id = 'console'
|
|
document.body.appendChild($console)
|
|
const wrapper = (scope, fn, name) => {
|
|
return function(msg) {
|
|
$console.innerHTML += `[${name}] ${msg}<br>`;
|
|
if( name == 'err'){
|
|
let err = new Error()
|
|
String(err.stack).split("\n").slice(2).map( (l) => $console.innerHTML += ` └☑ ${l}\n` )
|
|
}
|
|
$console.scrollTop = $console.scrollHeight;
|
|
fn.call(scope,msg);
|
|
};
|
|
}
|
|
window.console.log = wrapper(console, console.log, "log");
|
|
window.console.warn = wrapper(console, console.warn, "wrn");
|
|
window.console.error = wrapper(console, console.error, "err");
|
|
}
|
|
|
|
},
|
|
{
|
|
// auto-trigger events on changes
|
|
get(data,k,receiver){ return data[k] },
|
|
set(data,k,v){
|
|
data[k] = v
|
|
switch( k ){
|
|
case "enabled": {
|
|
if( !data.$console ) data.setupConsole()
|
|
$('#debug.btn').style.filter= v ? 'brightness(1.0)' : 'brightness(0.5)'
|
|
data.$console.style.display = v ? 'block' : 'none'
|
|
data.enabled = v
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
document.addEventListener('$menu:ready', (e) => {
|
|
try{
|
|
debug = debug(e.detail)
|
|
debug.init()
|
|
document.dispatchEvent( new CustomEvent("debug:ready", e ) )
|
|
$menu.buttons = $menu.buttons.concat([`<a class="btn" style="background:var(--xrf-dark-gray);filter: brightness(0.5);" aria-label="button" aria-description="enable all debug features" id="debug" onclick="debug.enabled = !debug.enabled"><i class="gg-debug"></i>debug</a><br>`])
|
|
}catch(e){console.error(e)}
|
|
})
|
|
|
|
document.querySelector('head').innerHTML += `
|
|
<style type="text/css">
|
|
.gg-debug {
|
|
box-sizing: border-box;
|
|
position: relative;
|
|
display: block;
|
|
transform: scale(var(--ggs,1));
|
|
width: 12px;
|
|
height: 18px;
|
|
border: 2px solid;
|
|
border-radius: 22px;
|
|
display: inline-block;
|
|
transform: translate(0px,4px);
|
|
margin-right: 21px;
|
|
}
|
|
.gg-debug::after,
|
|
.gg-debug::before {
|
|
content: "";
|
|
display: block;
|
|
box-sizing: border-box;
|
|
position: absolute
|
|
}
|
|
.gg-debug::before {
|
|
width: 8px;
|
|
height: 4px;
|
|
border: 2px solid;
|
|
top: -4px;
|
|
border-bottom-left-radius: 10px;
|
|
border-bottom-right-radius: 10px;
|
|
border-top: 0
|
|
}
|
|
.gg-debug::after {
|
|
background: currentColor;
|
|
width: 4px;
|
|
height: 2px;
|
|
border-radius: 5px;
|
|
top: 4px;
|
|
left: 2px;
|
|
box-shadow:
|
|
0 4px 0,
|
|
-6px -2px 0,
|
|
-6px 2px 0,
|
|
-6px 6px 0,
|
|
6px -2px 0,
|
|
6px 2px 0,
|
|
6px 6px 0
|
|
}
|
|
</style>
|
|
`
|
|
|
|
}).apply({})
|