Compare commits

..

No commits in common. "master" and "audio-beta" have entirely different histories.

92 changed files with 1191 additions and 1837 deletions

View File

@ -1,5 +0,0 @@
{
"presets": [
"env"
]
}

10
.gitignore vendored
View File

@ -2,10 +2,6 @@
.sass-cache .sass-cache
*.map *.map
/node_modules /node_modules
/public_html/* /public_html/css
!/public_htm/.htaccess /public_html/js
/package-lock.json /package-lock.json
.cache
/releases
/packages
/electron-build

View File

@ -1,6 +0,0 @@
{
"modules": false,
"plugins": {
"autoprefixer": true
}
}

View File

@ -1,98 +0,0 @@
# Douscord
Created for a study project in <u>Advanced web development</u>, we had to replicate [Discord](https://discordapp.com/) the famous text/vocal/video streaming platform. We had to use *WebSockets*, the *Java Persistence API* and the option we chose is to support *audio streaming* support. We worked on this project for 3-5 weeks.
> This work is under the MIT Licence.
**LEGAL ISSUES**
Discord is an open-source project, but we decided to "*copy*" its behavior without looking at its source. We dumped assets from the original website, any legal issue is protected by non-profit university work.
### Manifest
##### Client
- author: `xdrm-brackets`
- repository: https://www.git.xdrm.io/MTI/discord-client
##### Server
- author: `SeekDaSky`
- repository: https://www.git.xdrm.io/MTI/discord-server
### Technologies
- Java Persistence API ([JPA](http://www.oracle.com/technetwork/java/javaee/tech/persistence-jsp-140049.html))
- WebSocket (*SeekDaSky'*s library [kWebSocket](https://git.seekdasky.ovh/SeekDaSKy/kWebSocket) written in [kotlin](https://kotlinlang.org/))
- [VueJS](https://vuejs.org/) a javascript template renderer framework to manage highly related components in real-time.
- [Electron](https://electronjs.org/) to build native cross-platform desktop applications from the client source.
- [Electron Packager](https://github.com/electron-userland/electron-packager) to build bundled out of the box working applications.
- [Parcel](https://github.com/parcel-bundler/parcel) bundler to build javascript dependencies for either the browser or electron.
## Getting the app
### Linux
#### Debian / Ubuntu
###### With CURL
```bash
curl https://cloud.xdrm.io/index.php/s/bzR594w2MKyc83m/download -o /tmp/douscord.deb;
dpkg -i /tmp/douscord.deb && rm /tmp/douscord.deb;
```
###### With wget
```bash
wget https://cloud.xdrm.io/index.php/s/bzR594w2MKyc83m/download -o /tmp/douscord.deb;
dpkg -i /tmp/douscord.deb && rm /tmp/douscord.deb;
```
###### Or manually from [this link](https://cloud.xdrm.io/index.php/s/bzR594w2MKyc83m/download) then run the following command in the download folder:
```bash
sudo dpkg -i douscord.deb
```
#### From source
```bash
curl https://cloud.xdrm.io/index.php/s/bzR594w2MKyc83m/download -o /douscord-src;
sh ./douscord-src/install.sh;
```

View File

@ -1,43 +0,0 @@
const electron = require('electron');
const app = electron.app; // Module to control application life.
const BrowserWindow = electron.BrowserWindow; // Module to create native browser window.
const path = require('path');
const url = require('url');
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
var mainWindow = null;
// Quit when all windows are closed.
app.on('window-all-closed', function() {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform != 'darwin') {
app.quit();
}
});
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
app.on('ready', function() {
// Create the browser window.
mainWindow = new BrowserWindow({width: 800, height: 600});
// and load the index.html of the app.
// mainWindow.loadURL('file://' + __dirname + '/index.html');
mainWindow.loadURL( url.format({
pathname: path.join(__dirname, 'index.html'),
protocol: 'file:',
slashes: true
}) );
// mainWindow.loadURL('http://douscord/');
// Emitted when the window is closed.
mainWindow.on('closed', function(){
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null;
});
});

View File

@ -1,19 +0,0 @@
{
"name": "douscord",
"description": "Douscord non-copyright respectful copy",
"version": "1.0.0",
"author": "xdrm-brackets <xdrm.brackets.dev@gmail.com> SeekDaSky <mascaro.lucas@yahoo.fr G. Fauvet <gfauvet@gmail.com>",
"license": "MIT",
"private": true,
"dependencies": {
"vue": "^2.5.9",
"vue-router": "^2.5.3"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
],
"devDependencies": {
}
}

View File

@ -1,25 +0,0 @@
#!/bin/bash
# 1. Get + Go to current directory
ROOT="`realpath $(dirname $0)`";
cd $ROOT;
# 2. Install npm dependencies
echo -e "[1/3] installing dependencies";
npm --prefix=$ROOT install;
# 3. Build electron-app
echo -e "[2/3] building electron app";
npm --prefix=$ROOT run build:electron;
# 4. Create launching application
echo -e "[3/3] creating application shorcut";
DESKTOP_APP="[Desktop Entry]\n";
DESKTOP_APP+="Name=Douscord\n";
DESKTOP_APP+="GenericName=Douscord\n"
DESKTOP_APP+="Exec=/bin/bash -c 'cd /home/xdrm-brackets/ubuntu/git.xdrm.io/discord/client/; npm run electron;'\n";
DESKTOP_APP+="Terminal=false\n";
DESKTOP_APP+="Type=Application\n";
DESKTOP_APP+="Categories=Chat;Audio;Messages;Communication\n";
echo -e "$DESKTOP_APP" | sudo tee /usr/share/applications/douscord.desktop > /dev/null && echo ">>> INSTALLATION SUCCESSFUL <<<" || echo ">>> CANNOT CREATE SHORTUT <<<";

View File

@ -1,30 +1,24 @@
{ {
"name": "douscord", "name": "ptut-vhost",
"description": "PTUT", "description": "PTUT",
"version": "1.0.0", "version": "1.0.0",
"author": "xdrm-brackets <xdrm.brackets.dev@gmail.com> SeekDaSky <mascaro.lucas@yahoo.fr G. Fauvet <gfauvet@gmail.com>", "author": "xdrm-brackets <xdrm.brackets.dev@gmail.com> SeekDaSky <mascaro.lucas@yahoo.fr G. Fauvet <gfauvet@gmail.com>",
"license": "MIT", "license": "MIT",
"private": true, "private": true,
"scripts": { "scripts": {
"clean": "rm ./public_html/*.html; rm ./public_html/*.js; rm ./public_html/*.css; rm ./public_html/*svg; rm ./public_html/*.map; exit 0", "bundle:clean": "exit 0",
"clean:all": "npm run clean; npm run clean:electron; npm run clean:release; npm run clean:package; exit 0", "bundle:prod": "cross-env NODE_ENV=production webpack --progress --hide-modules",
"clean:electron": "rm -r ./electron-build; exit 0", "bundle:dev": "cross-env NODE_ENV=development webpack --progress --hide-modules",
"clean:release": "rm -r ./releases; exit 0", "bundle:watch": "cross-env NODE_ENV=development webpack --progress --watch --hide-modules",
"clean:package": "rm -r ./package; exit 0", "scss": "node-sass -r --output-style compressed --output ./public_html/css ./webpack/scss",
"dev": "parcel watch ./parcel/index.html --out-dir ./public_html --no-hmr", "watch-css": "node-sass -w -r --output-style compressed --output ./public_html/css ./webpack/scss",
"build:alternative": "cross-env NODE_ENV=production parcel watch ./parcel/index.html --out-dir ./public_html --no-hmr", "dev": "npm run bundle:clean; npm run bundle:dev; npm run watch-css",
"build": "parcel build ./parcel/index.html --public-url ./ --out-dir ./public_html --no-source-maps --no-minify", "devjs": "npm run bundle:clean; npm run bundle:watch",
"build:electron": "parcel build ./parcel/index.html --public-url ./ --out-dir ./electron-build --no-source-maps --no-minify --target=electron; npm run build:electron:setup", "build": "npm run bundle:clean; npm run bundle:prod; npm run scss"
"build:electron:setup": "npm run build:electron:setup-config; npm run build:electron:setup-index;",
"build:electron:setup-config": "cp ./electron.json ./electron-build/package.json; npm --prefix ./electron-build install",
"build:electron:setup-index": "cp ./electron.js ./electron-build/index.js",
"electron": "electron ./electron-build",
"package": "npm run build:electron; electron-packager ./electron-build douscord --asar --platform linux --arch x64 --out ./releases --overwrite;",
"package:deb": "electron-installer-debian --src ./releases/douscord-linux-x64 --dest ./packages --arch x64"
}, },
"dependencies": { "dependencies": {
"uglifyjs-webpack-plugin": "^1.2.3",
"vue": "^2.5.9", "vue": "^2.5.9",
"vue-hot-reload-api": "^2.3.0",
"vue-router": "^2.5.3" "vue-router": "^2.5.3"
}, },
"browserslist": [ "browserslist": [
@ -33,17 +27,20 @@
"not ie <= 8" "not ie <= 8"
], ],
"devDependencies": { "devDependencies": {
"@vue/component-compiler-utils": "^1.0.0",
"babel-core": "^6.26.0", "babel-core": "^6.26.0",
"babel-loader": "^7.1.2", "babel-loader": "^7.1.2",
"cross-env": "^5.0.5",
"babel-preset-env": "^1.6.0", "babel-preset-env": "^1.6.0",
"babel-preset-stage-3": "^6.24.1", "babel-preset-stage-3": "^6.24.1",
"cross-env": "^5.0.5", "css-loader": "^0.28.7",
"electron": "^1.8.4", "extract-text-webpack-plugin": "^3.0.2",
"electron-installer-debian": "^0.8.1", "file-loader": "^1.1.4",
"electron-packager": "^12.0.0", "node-sass": "^4.7.2",
"node-sass": "^4.8.3", "sass-loader": "^6.0.6",
"parcel-bundler": "^1.7.0", "vue-loader": "^13.0.5",
"vue-template-compiler": "^2.5.16" "vue-svg-loader": "^0.5.0",
"vue-template-compiler": "^2.5.9",
"webpack": "^3.8.1",
"webpack-dev-server": "^2.9.5"
} }
} }

View File

@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<rect width="24" height="24"/>
<path fill="#FFFFFF" d="M1,0 C0.44771525,0 0,0.44771525 0,1 C-4.4408921e-16,10.3888407 7.61115925,18 17,18 C17.2652165,18 17.5195704,17.8946432 17.7071068,17.7071068 C17.8946432,17.5195704 18,17.2652165 18,17 L18,13.5 C18,12.9477153 17.5522847,12.5 17,12.5 C15.76,12.5 14.55,12.3 13.43,11.93 C13.08,11.82 12.69,11.9 12.41,12.18 L10.21,14.38 C7.38,12.94 5.07,10.62 3.62,7.79 L5.82,5.58 C6.1,5.31 6.18,4.92 6.07,4.57 C5.7,3.45 5.5,2.24 5.5,1 C5.5,0.44771525 5.05228475,0 4.5,0 M16.1213203,0.464466094 L14,2.58578644 L11.8786797,0.464466094 L10.4644661,1.87867966 L12.5857864,4 L10.4644661,6.12132034 L11.8786797,7.53553391 L14,5.41421356 L16.1213203,7.53553391 L17.5355339,6.12132034 L15.4142136,4 L17.5355339,1.87867966" transform="matrix(1 0 0 -1 3 21)"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 930 B

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="11px" height="12px" viewBox="0 0 11 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.4.1 (15681) - http://www.bohemiancoding.com/sketch -->
<title>icon-ping-3</title>
<desc>Created with Sketch.</desc>
<defs/>
<g id="General-Playground" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="Desktop-HD---Channels" sketch:type="MSArtboardGroup" transform="translate(-498.000000, -887.000000)" fill="#43B581">
<g id="//-Channels" sketch:type="MSLayerGroup" transform="translate(488.000000, 0.000000)">
<g id="=-User-Menu" transform="translate(0.000000, 873.000000)" sketch:type="MSShapeGroup">
<g id="=-Voice-Status" transform="translate(1.000000, 0.000000)">
<g id="icon-ping-3" transform="translate(9.000000, 14.000000)">
<rect id="Rectangle-1668" x="8" y="0" width="3" height="12" rx="1"/>
<rect id="Rectangle-1668" x="4" y="4" width="3" height="8" rx="1"/>
<rect id="Rectangle-1668" x="0" y="8" width="3" height="4" rx="1"/>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,35 +0,0 @@
<html>
<!-- HEADER -->
<head>
<title>Douscord</title>
<!-- META -->
<meta charset='utf-8'>
<meta name='author' content='xdrm-brackets (Adrien Marquès)'>
<meta name='description' content='[Home] Home page'>
<!-- STYLESHEET -->
<link type='text/css' rel='stylesheet' href='./scss/layout.scss'>
<link type='text/css' rel='stylesheet' href='./scss/global.scss'>
<link type='text/css' rel='stylesheet' href='./scss/menu.scss'>
<link type='text/css' rel='stylesheet' href='./scss/dialog.scss'>
<link type='text/css' rel='stylesheet' href='./scss/side-menu.scss'>
<link type='text/css' rel='stylesheet' href='./scss/container.scss'>
<link type='text/css' rel='stylesheet' href='./scss/pop-up.scss'>
<!-- FONT -->
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro" rel="stylesheet">
<!-- BODY -->
</head>
<body>
<div id='vue'></div>
<script type='text/javascript' src='./main.js'></script>
</body>
</html>

View File

@ -1,548 +0,0 @@
export default class AudioManager{
static get BUFFER_SIZE(){ return 4096; }
constructor(){
/* (1) Initialise our AudioNodes
---------------------------------------------------------*/
/* (1) Build Audio Context */
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
/* (2) Create the MASTER gain */
this.master = this.ctx.createGain();
this.volume = this.ctx.createGain();
this.peaks = { low: 0, high: 0 };
this.volume_value = 1;
/* (3) Initialise input (typically bound from recorder) */
this.input = null;
/* (4) Initialize analyser (from input) + callback */
this.analyser = this.ctx.createAnalyser();
this.freq_drawer = null;
this.wave_drawer = null;
/* (5) Shortcut our output */
this.output = this.ctx.destination;
/* (6) Connect MASTER gain to output */
this.master.connect(this.output);
/* (2) Initialise processing attributes
---------------------------------------------------------*/
/* (1) Container for our recorder */
this.recorder = null;
/* (2) Initialise filters */
this.filters = {
voice_clarity: this.ctx.createBiquadFilter(),
voice_fullness: this.ctx.createBiquadFilter(),
voice_presence: this.ctx.createBiquadFilter(),
voice_sss: this.ctx.createBiquadFilter()
};
/* (3) Create network I/O controller (WebSocket) */
this.network = {
out: this.ctx.createScriptProcessor(AudioManager.BUFFER_SIZE, 1, 1)
};
/* (4) Initialise websocket */
this.ws = null;
/* (5) Bind network controller to send() function */
this.network.out.onaudioprocess = this.send.bind(this);
/* (6) Set up our filters' parameters */
this.setUpFilters();
/* (7) Initialise coordinator to manage received */
this.stack = [];
this.stack_size = 2;
this.fade_in = 0.1;
this.fade_out = 0.1;
/* (9) Debug data */
this.dbg = {
interval: 10, // debug every ... second
def: {
packets_received: 0,
packets_sent: 0,
kB_received: 0,
kB_sent: 0
},
data: {
packets_received: 0,
packets_sent: 0,
kB_received: 0,
kB_sent: 0
}
};
this.debug = () => setInterval(function(){
console.group('debug');
for( let k in this.data ){
console.log(`${this.data[k]} ${k}`)
this.data[k] = this.def[k]
}
console.groupEnd('debug');
}.bind(this.dbg), this.dbg.interval*1000);
}
/* (2) Setup filters
*
---------------------------------------------------------*/
setUpFilters(){
/* (1) Setup filter parameters
---------------------------------------------------------*/
/* (1) Setup EQ#1 -> voice clarity */
this.filters.voice_clarity.type = 'peaking';
this.filters.voice_clarity.frequency.setValueAtTime(3000, this.ctx.currentTime);
this.filters.voice_clarity.Q.setValueAtTime(.8, this.ctx.currentTime);
this.filters.voice_clarity.gain.setValueAtTime(2, this.ctx.currentTime);
/* (2) Setup EQ#2 -> voice fullness */
this.filters.voice_fullness.type = 'peaking';
this.filters.voice_fullness.frequency.setValueAtTime(200, this.ctx.currentTime);
this.filters.voice_fullness.Q.setValueAtTime(.8, this.ctx.currentTime);
this.filters.voice_fullness.gain.setValueAtTime(2, this.ctx.currentTime);
/* (3) Setup EQ#3 -> reduce voice presence */
this.filters.voice_presence.type = 'peaking';
this.filters.voice_presence.frequency.setValueAtTime(5000, this.ctx.currentTime);
this.filters.voice_presence.Q.setValueAtTime(.8, this.ctx.currentTime);
this.filters.voice_presence.gain.setValueAtTime(-2, this.ctx.currentTime);
/* (4) Setup EQ#3 -> reduce 'sss' metallic sound */
this.filters.voice_sss.type = 'peaking';
this.filters.voice_sss.frequency.setValueAtTime(7000, this.ctx.currentTime);
this.filters.voice_sss.Q.setValueAtTime(.8, this.ctx.currentTime);
this.filters.voice_sss.gain.setValueAtTime(-8, this.ctx.currentTime);
/* (2) Connect filters
---------------------------------------------------------*/
/* (1) Connect clarity to fullness */
this.filters.voice_clarity.connect( this.filters.voice_fullness );
/* (2) Connect fullness to presence reduction */
this.filters.voice_fullness.connect( this.filters.voice_presence );
/* (3) Connect presence reduction to 'ss' removal */
this.filters.voice_presence.connect( this.filters.voice_sss );
}
/* (3) Filter toggle
*
* @unlink<boolean> Whether to unlink filters (directly bind to output)
*
---------------------------------------------------------*/
linkFilters(unlink=false){
/* (1) Disconnect all by default */
this.input.disconnect();
/* (2) Also link to analyser */
this.input.connect(this.analyser);
/*Chrome fix*/this.network.out.connect(this.output);
/* (3) Get first filter */
let first_filter = this.filters.voice_clarity;
let last_filter = this.filters.voice_sss;
/* (4) If unlink -> connect directly to NETWORK output */
if( unlink === true )
return this.input.connect(this.network.out);
/* (5) If linking -> connect input to volume */
this.input.connect(this.volume);
/* (6) If linking -> connect volume to filter stack */
this.volume.connect(first_filter);
/* (7) If linking -> connect stack end to network.out */
last_filter.connect(this.network.out);
}
/* (3) Binds an input stream
*
---------------------------------------------------------*/
bindRecorderStream(_stream){
/* (1) Bind audio stream
---------------------------------------------------------*/
/* (1) bind our audio stream to our source */
this.input = this.ctx.createMediaStreamSource(_stream);
/* (2) By default: link through filters to output
---------------------------------------------------------*/
/* (1) Link through filters */
this.linkFilters();
gs.get.audio_conn = 2; // voice connected
}
/* (4) Send chunks (Float32Array)
*
---------------------------------------------------------*/
send(_audioprocess){
/* Exit here if not connected */
if( this.ws === null || this.ws.readyState !== 1 )
return;
/* (1) WebSocket send packet
---------------------------------------------------------*/
/* (1) Initialize buffer (Float32Array) */
let buf32 = new Float32Array(AudioManager.BUFFER_SIZE);
/* (2) Extract stream into buffer */
_audioprocess.inputBuffer.copyFromChannel(buf32, 0);
/* (4) Convert for WS connection (Int16Array) */
this.peaks.low = 0;
this.peaks.high = 0;
let buf16 = this.f32toi16(buf32);
/* (5) Send buffer through websocket */
this.ws.send(buf16);
/* (5) Adapt microphone volume if had peaks */
if( this.peaks.high > .01 ) // 30% saturation -> decrease
this.volume_value *= .8;
else if( this.peaks.low > .99 && this.volume_value*1.01 < 1 ) // 90% too low volume + less than 30% saturation -> increase
this.volume_value *= 1.01;
// apply new volume
this.volume.gain.setValueAtTime(this.volume_value, this.ctx.currentTime);
/* (2) WebSocket buffer stack read
---------------------------------------------------------*/
setTimeout(function(){
/* (1) Pop too large stack */
this.stack.length > this.stack_size && this.stack.pop();
/* (2) Read input buffer stack */
if( this.stack.length > 0 ){
// 1. extract our source
let source_node = this.stack.shift();
// 2. Play source node
source_node.start();
}
}.bind(this), 0);
/* (3) Manage analyser
---------------------------------------------------------*/
/* (1) Process only if 'freq_drawer' is set */
if( this.freq_drawer instanceof Function ){
// 1. Prepare array
let freqArray = new Uint8Array(this.analyser.frequencyBinCount);
// 2. Get frequency array
this.analyser.getByteFrequencyData(freqArray);
// 3. Send to callback
setTimeout(this.freq_drawer.bind(this,freqArray), 0);
}
/* (2) Process only if 'wave_drawer' is set */
else if( this.wave_drawer instanceof Function ){
// 1. Prepare array
let waveArray = new Uint8Array(this.analyser.fftSize);
// 2. Get wave array
this.analyser.getByteTimeDomainData(waveArray);
// 3. Send to callback
setTimeout(this.wave_drawer.bind(this,waveArray), 0);
}
// DEBUG
this.dbg.data.packets_sent++;
this.dbg.data.kB_sent += buf16.length * 16. / 8 / 1024;
}
/* (5) Play received chunks (Int16Array)
*
---------------------------------------------------------*/
receive(_buffer){
/* (1) Convert to Float32Array */
let buf32 = this.i16tof32(_buffer);
/* (2) Create source node */
let source = this.ctx.createBufferSource();
/* (3) Create buffer and dump data */
let input_buffer = this.ctx.createBuffer(1, AudioManager.BUFFER_SIZE, this.ctx.sampleRate);
input_buffer.getChannelData(0).set(buf32);
/* (4) Bind buffer to source node */
source.buffer = input_buffer;
/* (5) Create a dedicated *muted* gain */
let gain = this.ctx.createGain();
/* (6) source -> gain -> MASTER + play() */
source.connect(gain);
gain.connect(this.master);
/* (7) Push in buffer stack */
this.stack.push(source);
}
/* (6) Convert Float32Array to Int16Array
*
* @buf32<Float32Array> Input
*
* @return buf16<Int16Array> Converted output
*
---------------------------------------------------------*/
f32toi16(buf32){
/* (1) Initialise output */
let buf16 = new Int16Array(buf32.length);
/* (2) Initialize loop */
let i = 0, l = buf32.length;
/* (3) Convert each value */
for( ; i < l ; i++ ){
buf16[i] = (buf32[i] < 0) ? 0x8000 * buf32[i] : 0x7FFF * buf32[i];
( buf32[i] > 0.9 ) && ( this.peaks.high++ );
( buf32[i] < 0.1 ) && ( this.peaks.low++ );
}
/* (4) Report peaks in percentage */
this.peaks.high /= l;
this.peaks.low /= l;
return buf16;
}
/* (7) Convert Int16Array to Float32Array
*
* @buf16<Int16Array> Input
*
* @return buf32<Float32Array> Converted output
*
---------------------------------------------------------*/
i16tof32(buf16){
/* (1) Initialise output */
let buf32 = new Float32Array(buf16.length);
/* (2) Initialize loop */
let i = 0, l = buf16.length;
/* (3) Convert each value */
for( ; i < l ; i++ )
buf32[i] = (buf16[i] >= 0x8000) ? -(0x10000 * buf16[i])/0x8000 : buf16[i] / 0x7FFF;
return buf32;
}
/* (8) Connect websocket
*
* @address<String> Websocket address
*
---------------------------------------------------------*/
wsconnect(_addr){
/* (1) Create websocket connection */
this.ws = new WebSocket(_addr);
gs.get.audio_conn = 0; // connecting
/* (2) Manage websocket responses */
this.ws.onmessage = function(_msg){
if( !(_msg.data instanceof Blob) )
return console.warn('[NaB] Not A Blob');
let fr = new FileReader();
fr.onload = function(){
let buf16 = new Int16Array(fr.result);
this.receive(buf16);
}.bind(this);
fr.readAsArrayBuffer(_msg.data);
}.bind(this);
/* (3) Debug */
this.ws.onopen = () => (gs.get.audio_conn !== 2 && (gs.get.audio_conn = 1)); // listening
this.ws.onclose = () => ( gs.get.audio_conn = null ); // disconnected
}
/* (9) Access microphone + launch all
*
---------------------------------------------------------*/
launch(room_id=0){
/* (1) Start websocket */
this.wsconnect(`wss://ws.douscord.xdrm.io/audio/${room_id}`);
/* (2) Set our streaming binding function */
let streaming_binding = function(stream){
this.recorder = new MediaRecorder(stream);
this.recorder.onstart = function(){
this.bindRecorderStream(stream);
console.warn('[audio] recording');
}.bind(this);
this.recorder.onstop = () => {
this.recorder.stream.getTracks().map( t => t.stop() );
this.recorder = null;
console.warn('[audio] stopped recording');
};
// start recording
this.recorder.start();
}.bind(this);
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
/* (3) If navigator.mediaDevices.getUserMedia */
if( navigator.mediaDevices && navigator.mediaDevices.getUserMedia ){
console.log('[audio] using "navigator.mediaDevices.getUserMedia"')
return navigator.mediaDevices.getUserMedia({ audio: true })
.then(streaming_binding)
.catch((e) => console.warn('[audio] microphone recorder issue', e));
}
/* (4) If old version */
if( navigator.getUserMedia ){
console.log('[audio] using "navigator.getUserMedia"')
return navigator.getUserMedia({ audio: true },
streaming_binding,
(e) => console.warn('[audio] microphone recorder issue', e));
}
console.warn('[audio] recorder not supported');
}
/* (10) Shut down microphone + kill all
*
---------------------------------------------------------*/
kill(){
/* (1) Close websocket */
this.ws && this.ws.close();
/* (2) Stop recording */
this.recorder && this.recorder.stop();
}
/* (11) Play a POP notification
*
---------------------------------------------------------*/
pop(){
/* (1) Base data */
let base_freq = 150;
let mods = [0, 75, 75]; // freq modulations (from base_freq)
let time_range = 0.05; // time between each modulation
let start = this.ctx.currentTime + 0.1;
/* (2) Build oscillator */
let osc = this.ctx.createOscillator();
osc.type = 'triangle';
/* (3) Create local gain to lower volume */
let local = this.ctx.createGain();
local.gain.setValueAtTime(0.3, 0);
/* (4) Connect all nodes to output */
osc.connect(local);
local.connect(this.master);
/* (5) Bind frequencies over time */
for( let i in mods )
osc.frequency.setValueAtTime(base_freq+mods[i], start + i*time_range );
/* (6) Start playing */
osc.start( start );
/* (7) Set when to stop playing */
osc.stop( start + time_range*mods.length );
}
}

View File

@ -1,50 +0,0 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import auth_wrapper from './vue/auth/wrapper.vue'
import noauth_wrapper from './vue/noauth/wrapper.vue'
/* (1) Setup: Vue, VueRouter, Authentication, ClientDriver-s */
require('./setup.js').default.then(() => {
/* (2) Set router hooks to load page data before loading content */
gs.get.router.beforeEach((to, from, next) => {
// {1} Ignore null name //
if( to.name == null )
return next();
// {2} Get appropriate page location //
let auth_folder = (gs.get.authed) ? 'auth' : 'noauth';
let page_file = to.name || gs.get.routes[auth_folder][0].name;
let fullpath = `${auth_folder}/${page_file}`;
// {3} Load page script //
if( fullpath === 'noauth/login')
return require('./page/noauth/login.js').default.then(next);
if( fullpath === 'noauth/register')
return require('./page/noauth/register.js').default.then(next);
if( fullpath === 'auth/channel')
return require('./page/auth/channel.js').default.then(next);
// {4} Let VueRouter do the magic //
// next();
});
/* (3) Select appropriate wrapper */
const wrapper = (gs.get.authed) ? auth_wrapper : noauth_wrapper;
/* (4) Render view */
Vue.use(VueRouter);
new Vue({
el: '#vue',
router: gs.get.router,
render(h){ return h(wrapper); }
});
});

View File

@ -1,33 +0,0 @@
import PopupController from '../../lib/popup-controller'
import ContentController from '../../lib/content-controller'
import RoomController from '../../lib/room-controller'
import ChannelController from '../../lib/channel-controller'
export default new Promise( (res, rej) => {
/* (1) Channel data gathering
---------------------------------------------------------*/
/* (1) Store route params */
window.initial_link = gs.get.router.history.current.params.link;
console.log(`[channel.URL] ${initial_link}`);
/* (2) Main components
---------------------------------------------------------*/
/* (1) Initialize popup management */
gs.set('popup', new PopupController());
/* (2) Initialize content management */
gs.set('content', new ContentController());
/* (3) Initialize rooms & room menu */
gs.set('room', new RoomController());
/* (4) Initialize channels & channel menu */
gs.set('channel', new ChannelController());
gs.get.channel.fetch(); // fetch at load time
res();
});

View File

@ -1,83 +0,0 @@
export default new Promise( (res, rej) => {
/* (1) Initialise
---------------------------------------------------------*/
/* (1) Default data structure */
gs.set('login', {
// fields
username: new FieldValidator('basic-name', ''),
password: new FieldValidator('password', ''),
// login failed
failed: false,
// functions
func: {
login(){},
forgot_pass(){},
press_enter(){}
}
});
/* (2) Login attempt
*
---------------------------------------------------------*/
gs.get.login.func.login = function(){
/* (1) Cache fields' values */
let username = this.username.mutable;
let password = this.password.mutable;
/* (2) Manage errors */
if( !this.username.is_valid() )
return false;
if( !this.password.is_valid() )
return false;
/* (3) API bindings */
api.call('GET /user/token', {}, function(rs){
// manage error
if( rs.error !== 0 || rs.token == null || rs.uid == null )
return this.failed = true;
// store TOKEN + user data
auth.token = rs.token;
auth.user = {
uid: rs.uid,
username: username
};
document.location = '';
}.bind(this), encodeURI(`${username}:${password}`));
}.bind(gs.get.login);
/* (4) Manage pressing on enter
*
---------------------------------------------------------*/
gs.get.login.func.press_enter = function(e){
// if enter -> launch login
if( e.keyCode === 13 )
this.func.login();
}.bind(gs.get.login);
res();
});

View File

@ -1,90 +0,0 @@
export default new Promise( (res, rej) => {
/* (1) Initialise
---------------------------------------------------------*/
/* (1) Default data structure */
gs.set('register', {
// fields
mail: new FieldValidator('bypass', ''),
username: new FieldValidator('basic-name', ''),
password: new FieldValidator('password', ''),
// functions
func: {
register(){},
press_enter(){}
}
});
/* (2) Login attempt
*
---------------------------------------------------------*/
gs.get.register.func.register = function(){
/* (1) Cache fields' values */
let mail = this.mail.mutable;
let username = this.username.mutable;
let password = this.password.mutable;
/* (2) Manage errors */
// mail error
if( !this.mail.is_valid() )
return false;
// username error
if( !this.username.is_valid() )
return false;
// password error
if( !this.password.is_valid() )
return false;
/* (3) API bindings */
api.call('POST /user', { username: username, password: password }, function(rs){
// manage error
if( rs.error !== 0 || rs.uid == null || rs.token == null )
return gs.get.router.push('register');
// manage login
auth.token = rs.token;
auth.user = {
uid: rs.uid,
username: username
};
document.location = '';
});
}.bind(gs.get.register);
/* (4) Manage pressing on enter
*
---------------------------------------------------------*/
gs.get.register.func.press_enter = function(e){
// if enter -> launch register
if( e.keyCode === 13 )
this.func.register();
}.bind(gs.get.register);
res();
});

View File

@ -1,464 +0,0 @@
@import 'constants';
#WRAPPER > div.dialog{
display: block;
position: absolute;
top: 0;
left: $menu-width;
width: $dialog-width;
height: 100%;
background-color: $dialog-bg;
/* (2) Container HEADER */
& > div.header{
display: block;
position: absolute;
top: 0;
left: 0;
width: calc( 100% - 2*1em );
height: calc( #{$header-height} - #{$header-height/2} );
border-bottom: #{$bb-height} solid darken($dialog-header-bg, 5%);
box-shadow: 0 #{$bb-offset - $bb-height} 0 darken($dialog-header-bg, 2%);
z-index: 200;
font-weight: bold;
padding: #{$header-height/4} 1em;
background: url('../asset/svg/arrow.down@hover.svg') right .7em center no-repeat;
background-size: auto 1.2em;
cursor: pointer;
z-index: 100;
&[data-open='1']{
background-image: url('../asset/svg/cross.svg');
}
}
/* (3) Container BODY */
& > div.body{
display: block;
position: absolute;
top: calc( #{$header-height} + #{$bb-offset} );
left: 0;
width: 100%;
height: calc( 100% - #{$header-height} - #{$bb-offset} - #{$header-height});
background-color: $dialog-bg;
color: #72767d;
z-index: 100;
// overflow: hidden;
/* (3) Toggle label */
& div.toggle{
display: inline-block;
width: calc( 100% - 2*.5em - 3em );
margin: 0 .5em;
margin-top: 2em;
padding-left: 1em;
background: left center no-repeat;
background-size: auto 60%;
color: #72767d;
font-size: .7em;
letter-spacing: .05em;
font-weight: bold;
text-transform: uppercase;
transition: color .2s ease-in-out;
&:hover{ color: #b9bbbe; }
cursor: pointer;
// when not toggle-active
&[data-toggle],
&[data-toggle='0']{
background-image: url('../asset/svg/arrow.right.svg');
&:hover{ background-image: url('../asset/svg/arrow.right@hover.svg'); }
}
// when toggle-active
&[data-toggle='1']{
background-image: url('../asset/svg/arrow.down.svg');
&:hover{ background-image: url('../asset/svg/arrow.down@hover.svg'); }
}
}
/* (4) Add button */
& div.add{
display: inline-block;
position: relative;
top: .15em;
left: -.5em;
width: 1em;
height: 1em;
background: url('../asset/svg/dialog.add.svg') center center no-repeat;
background-size: auto 100%;
cursor: pointer;
&:hover{ background-image: url('../asset/svg/dialog.add@hover.svg'); }
}
/* (5) UL after toggle label */
& div.toggle ~ ul,
& div.toggle:not([data-toggle='1']) ~ ul{
display: none;
/* (5) List items */
li{
display: block;
position: relative;
margin: .1em .5em;
padding: .3em .5em;
padding-left: 1.6em;
border-radius: 3px / 3px;
background-color: transition;
transition: color .2s ease-in-out,
background-color .2s ease-in-out;
cursor: pointer;
&:hover{
color: #ddd;
background-color: #36393f;
}
// {1} Trailing icon //
&:before{
content: '';
display: inline-block;
position: absolute;
margin-left: -1.3em;
width: 1.3em;
height: 1.3em;
background: center center no-repeat;
background-size: auto 80%;
}
// for 'text'
&[data-type='text']{
&:before{ background-image: url('../asset/svg/dialog.text.svg'); }
&.active{
color: #ddd;
background-color: rgba(79,84,92,.6);
}
}
// for 'voice'
&[data-type='voice']{
&:before{ background-image: url('../asset/svg/dialog.voice.svg'); }
&.active{ color: #ddd; }
}
// background-color: #f00;
& > span.rem{
display: none;
position: absolute;
top: calc( .5em );
left: calc( 100% - .5em - 1em );
width: 1em;
height: 1em;
background: url('../asset/svg/minipopup.remove.svg') center center no-repeat;
background-size: contain;
&:hover{ background-image: url('../asset/svg/minipopup.remove@hover.svg'); }
}
// only show 'remove' icon on hover
&:hover > span.rem{ display: block; }
// Member List
& > div.member-list{
display: block;
position: relative;
width: calc( 100% - 1em );
overflow: hidden;
& > span{
display: inline-block;
position: relative;
& > span{
display: inline-block;
position: relative;
margin-top: -1.9em;
}
div.icon{
display: inline-block;
position: relative;
width: 1.2em;
height: 1.2em;
margin: .2em .5em;
margin-left: 0;
border-radius: 50% / 50%;
background-color: url() center center no-repeat;
background-size: contain;
}
}
}
}
}
// when visible
& div.toggle[data-toggle='1'] ~ ul{ display: block; }
}
/* (4) Container FOOTER -> audio status */
& > div.footer{
display: none;
position: absolute;
top: calc( 100% - #{$header-height} );
left: 0;
width: 100%;
height: $header-height;
background-color: $audio-bg;
color: #72767d;
flex-flow: row wrap;
z-index: 100;
& > canvas{
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
z-index: 100;
}
& > div.text-container{
flex: 1 1 50%;
display: flex;
position: relative;
flex-flow: row wrap;
z-index: 101;
& > div.status{
flex: 0 1 100%;
display: flex;
position: relative;
height: 50%;
margin: 0;
padding: 0 .5em;
color: #faa61a;
font-size: .9em;
flex-flow: row nowrap;
justify-items: space-between;
align-items: center;
}
& > div.room{
flex: 0 1 100%;
display: block;
position: relative;
height: 50%;
padding: 0 .9em;
color: #72767d;
font-size: .8em;
}
}
& > div.audio-close{
flex: 0 0 calc( #{$header-height} - 1em );
display: block;
position: relative;
height: calc( #{$header-height} - 1em );
margin: auto .5em;
border-radius: 5px / 5px;
background: url('../asset/svg/audio.close.svg') center center no-repeat;
background-size: auto 60%;
transition: background-color .1s ease-in-out;
cursor: pointer;
z-index: 101;
&:hover{
background-color: rgba(0,0,0,.2);
}
}
// manage when valid state
&[data-connected='0'],
&[data-connected='1'],
&[data-connected='2']{
display: flex;
&[data-connected='1'] > div.text-container > div.status,
&[data-connected='2'] > div.text-container > div.status{
color: #43b581;
&:before{
content: '';
display: inline-block;
position: relative;
width: 1.5em;
height: 1em;
background: url('../asset/svg/voice.connected.svg') center center no-repeat;
background-size: auto 85%;
}
}
}
}
@keyframes scale-up{
0%{ transform: scale(0); }
100%{ transform: scale(1); }
}
.minipopup{
display: block;
position: absolute;
top: 0;
left: 0;
width: calc( 100% - 2*1em );
height: auto;
margin: .7em 1em;
border-radius: 5px;
box-shadow: 0 2px 10 0 rgba(0,0,0,.5);
background-color: #ffffff;
will-change: transform;
transform-origin: 100% 0;
animation: scale-up .3s ease-in-out;
overflow: hidden;
z-index: 100;
& > span{
display: block;
position: relative;
padding: .7em 1em;
padding-left: 3em;
background-color: #fff;
background: url('../asset/svg/minipopup.invite.svg') left 1em center no-repeat;
background-size: auto 45%;
color: #99aab5;
font-size: .85em;
letter-spacing: .05em;
white-space: nowrap;
cursor: pointer;
transition: background-color .2s ease-in-out,
color .2s ease-in-out;
&[data-icon='create']{ background-image: url('../asset/svg/minipopup.create.svg'); }
&[data-icon='category']{ background-image: url('../asset/svg/minipopup.category.svg'); }
&[data-icon='edit']{ background-image: url('../asset/svg/minipopup.edit.svg'); }
&[data-icon='remove']{ background-image: url('../asset/svg/minipopup.remove.svg'); }
&[data-icon='remove']{ background-image: url('../asset/svg/minipopup.remove.svg'); }
&[data-icon='logout']{ background-image: url('../asset/svg/minipopup.logout.svg'); }
&[data-icon='leave']{ background-image: url('../asset/svg/minipopup.leave.svg'); }
&[data-icon='password']{ background-image: url('../asset/svg/minipopup.password.svg'); }
&:hover{ background-color: #f9f9f9; color: #737f8d; }
/* separators */
/*after*/ &.sa{ border-bottom: 1px solid #f3f3f3; }
/*before*/ &.sb{ border-top: 1px solid #f3f3f3; }
&.special{
color: #7289da;
&:hover{ color: #677bc4; }
}
&.invalid{ color: #e65835; }
&.invalid-h:hover{ color: #e65835; }
}
}
}

View File

@ -1,72 +0,0 @@
/* (1) VueJS data */
import VueRouter from 'vue-router'
import GlobalStore from './lib/gstore'
import routes from './routes'
/* (2) Custom libs */
import Authentication from './lib/authentication.js'
import XHRClientDriver from './lib/client/xhr.js'
import WebSocketClientDriver from './lib/client/ws.js'
import APIClient from './lib/api-client.js'
export default new Promise( (res, rej) => {
/* (1) Custom lib accessors
---------------------------------------------------------*/
/* (1) Field validation */
require('./lib/field-manager.js');
/* (2) Global Store for Vue */
window.gs = new GlobalStore();
/* (3) Authentication token management */
window.auth = new Authentication();
gs.set('auth', auth);
/* (4) XHR / WebSocket drivers */
window.xhrcd = XHRClientDriver;
window.wscd = WebSocketClientDriver;
/* (5) ClientDriver instances */
window.api = new APIClient('api.douscord.xdrm.io');
window.ws = new WebSocketClientDriver('ws.douscord.xdrm.io');
/* (6) Add audio manager */
window.AudioManager = new (require('./lib/audio-manager.js').default)();
gs.set('audioManager', window.AudioManager);
/* (2) Global data
---------------------------------------------------------*/
/* (1) Get Full URI */
gs.set('URI', document.URL.replace(/^(?:[^\/]+\/\/|[^\/]+\/)/, '').split('/').filter(function(v,i){ return !!i && v.length; }));
/* (2) Store routes */
gs.set('routes', routes);
/* (3) Store if authenticated */
gs.set('authed', auth.token !== null);
/* (4) Init. vue router */
gs.set('router', new VueRouter({
routes: gs.get.authed ? gs.get.routes['auth'] : gs.get.routes['noauth']
}));
/* (5) refresh page */
gs.set('refresh', () => ( document.location = '' ) );
/* (6) Connection status */
gs.set('connection', 1); // null -> normal, 0 -> offline, 1 -> connecting, 2 -> online
gs.set('audio_conn', null); // null -> normal, 0 -> connecting, 1 -> listening, 2 -> sharing
/* (7) Ask for permission API */
Notification.requestPermission();
/* (8) DEBUG MODE */
window.DEBUG_MOD = false;
res();
});

View File

@ -1,193 +0,0 @@
<template>
<div class='dialog'>
<div class='header' @click='minipop = !minipop' :data-open='minipop?1:0'>
<div class='title'>{{ gs.content.cbuf.label }}</div>
</div>
<div class='body'>
<div v-show='minipop' class='minipopup'>
<span class='special sa' @click='gs.popup.show(`channel.invite`); minipop=false'>Invite people</span>
<span data-icon='create' @click='gs.popup.show(`channel.create`); minipop=false'>Create channel</span>
<span class ='invalid-h' data-icon='remove' @click='gs.popup.show(`channel.remove`); minipop=false'>Remove channel</span>
<span data-icon='category' @click='gs.popup.show(`room.create`); minipop=false'>Create room</span>
<span data-icon='edit' @click='gs.popup.show(`username.change`); minipop=false'>Change nickname</span>
<span data-icon='password' @click='gs.popup.show(`password.change`); minipop=false'>Change password</span>
<span class='sb invalid-h' data-icon='leave' @click='gs.popup.show(`channel.leave`); minipop=false'>Leave channel</span>
<span class='sb invalid' data-icon='logout' @click='gs.auth.token=null; gs.refresh()'>Logout</span>
</div>
<div v-for='(rooms, type) in gs.room' v-if='type[0] != `_`'>
<div class='toggle'
:data-toggle='rooms.visible?1:0'
@click='rooms.visible=!rooms.visible'>
{{ type }} <span>rooms</span>
</div>
<div class='add' @click='gs.popup.show(`room.create`)' data-title='Create channel'></div>
<ul>
<li v-for='r in rooms.list'
:class='rooms.current==r.id?`active`:``'
:data-type='r.type'
@click='gs.room.nav(r.type, r.id)'>{{ r.name }}
<span class='rem' @click="gs.popup.show('room.remove'); gs.popup.get('room.remove').data=r"></span>
<div v-if='r.type===`voice`' v-show='r.members.length>0' class='member-list'>
<span v-for='uid in r.members'>
<div class='icon' :style='`background-image: url("https://picsum.photos/150/?random&nonce=${uid}");`'></div>
<span>{{ ( uid == gs.auth.user.uid ) ? 'You' : gs.content.user(uid).username }} </span>
</span>
</div>
</li>
</ul>
</div>
</div>
<div class='footer' :data-connected='gs.audio_conn'>
<canvas ref='audio_canvas' width='1000' height='1000'></canvas>
<div class='text-container'>
<!-- Audio connection status -->
<div class='status'>{{ gs.audio_conn === 0
? `Connecting...`
: ( gs.audio_conn === 1
? `Listening`
: `Voice Connected` ) }}</div>
<!-- Current room/user -->
<div class='room'>{{ gs.room.get('text') ? gs.room.get('text').name : '?' }} / {{ gs.auth.user.username }}</div>
</div>
<!-- Logout from current audio room -->
<div class='audio-close' @click='gs.room.nav(`voice`, null)'></div>
</div>
</div>
</template><script>
export default {
name: 'dialog-',
data(){ return {
gs: gs.get,
minipop: false
}; },
methods: {
/* (1) Draw WAVE on canvas from a buffer
*
* @buffer<Uint8Array> Data buffer
*
---------------------------------------------------------*/
draw_wave(buffer=[]){
/* (0) Exit if empty buffer */
if( !(buffer instanceof Uint8Array) || buffer.length <= 0 )
return;
/* (1) Get <canvas> context + shortcuts */
let can = this.$refs.audio_canvas;
let ctx = can.getContext('2d', { antialias: false, depth: false });
/* (2) Adjust dimensions */
can.width = buffer.length;
can.height = 256;
let precision = 1; // bigger -> less precise
/* (3) Erase previous drawing */
ctx.clearRect(0, 0, can.width, can.height);
/* (4) Begin tracing */
ctx.beginPath();
ctx.moveTo(0, can.height-buffer[0]);
/* (5) Trace through each value */
for( let i = precision ; i < buffer.length ; i+=precision )
ctx.lineTo(i, can.height-buffer[i]);
/* (6) End tracing */
ctx.lineWidth = 7;
ctx.strokeStyle = '#44484f';
ctx.stroke();
},
/* (1) Draw FREQ on canvas from a buffer
*
* @buffer<Uint8Array> Data buffer
*
---------------------------------------------------------*/
draw_freq(buffer=[]){
/* (0) Exit if empty buffer */
if( !(buffer instanceof Uint8Array) || buffer.length <= 0 )
return;
/* (1) Get <canvas> context + shortcuts */
let can = this.$refs.audio_canvas;
let ctx = can.getContext('2d', { antialias: false, depth: false });
/* (2) Adjust dimensions */
can.width = buffer.length;
can.height = 256;
let precision = 1; // bigger -> less precise
/* (3) Erase previous drawing */
ctx.clearRect(0, 0, can.width, can.height);
ctx.lineWidth = 1;
/* (4) Trace through each value */
for( let i = precision ; i < buffer.length ; i+=precision ){
ctx.beginPath();
ctx.strokeStyle = '#44484f';
ctx.moveTo(i, can.height);
ctx.lineTo(i, can.height-buffer[i]);
ctx.stroke();
}
}
},
mounted(){
/* (1) By default DRAW FREQUENCY */
this.gs.audioManager.freq_drawer = this.draw_freq;
/* (2) Create toggle method */
this.gs.toggleDrawStyle = function(){
// currently WAVE
if( this.gs.audioManager.wave_drawer instanceof Function ){
this.gs.audioManager.wave_drawer = null;
this.gs.audioManager.freq_drawer = this.draw_freq;
}else{
this.gs.audioManager.wave_drawer = this.draw_wave;
this.gs.audioManager.freq_drawer = null;
}
}.bind(this);
}
}
</script>

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 801 B

After

Width:  |  Height:  |  Size: 801 B

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 768 B

After

Width:  |  Height:  |  Size: 768 B

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 928 B

After

Width:  |  Height:  |  Size: 928 B

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 295 B

View File

Before

Width:  |  Height:  |  Size: 226 B

After

Width:  |  Height:  |  Size: 226 B

View File

Before

Width:  |  Height:  |  Size: 214 B

After

Width:  |  Height:  |  Size: 214 B

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 450 B

View File

Before

Width:  |  Height:  |  Size: 402 B

After

Width:  |  Height:  |  Size: 402 B

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

36
public_html/index.html Normal file
View File

@ -0,0 +1,36 @@
<html>
<!-- HEADER -->
<head>
<title>Douscord</title>
<!-- META -->
<meta charset='utf-8'>
<meta name='author' content='xdrm-brackets (Adrien Marquès)'>
<meta name='description' content='[Home] Home page'>
<!-- STYLESHEET -->
<link type='text/css' rel='stylesheet' href='./css/layout.css'>
<link type='text/css' rel='stylesheet' href='./css/global.css'>
<link type='text/css' rel='stylesheet' href='./css/menu.css'>
<link type='text/css' rel='stylesheet' href='./css/dialog.css'>
<link type='text/css' rel='stylesheet' href='./css/side-menu.css'>
<link type='text/css' rel='stylesheet' href='./css/container.css'>
<link type='text/css' rel='stylesheet' href='./css/pop-up.css'>
<!-- FONT -->
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro" rel="stylesheet">
<!-- BODY -->
</head>
<body>
<div id='vue'></div>
<script type='text/javascript' src='./js/bundle.js'></script>
</body>
</html>

85
webpack.config.js Normal file
View File

@ -0,0 +1,85 @@
var path = require('path');
var webpack = require('webpack');
var UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const extractSass = new ExtractTextPlugin({
filename: "[name].css",
disable: process.env.NODE_ENV === "development"
});
var mod_common = {
rules: [
{
test: /\.css$/,
use: [
'vue-style-loader',
'css-loader'
],
}, {
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {} // other vue-loader options go here
}
}, {
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
}, {
test: /\.(png|jpg|gif)$/,
loader: 'file-loader',
options: { name: '[name].[ext]?[hash]' }
}, {
test: /\.svg$/,
loader: 'vue-svg-loader', // `vue-svg` for webpack 1.x
options: {
// optional [svgo](https://github.com/svg/svgo) options
svgo: {
plugins: [
{removeDoctype: true},
{removeComments: true}
]
}
}
}
]
};
module.exports = {
name: "main",
entry: './webpack/main.js',
output: {
path: path.resolve(__dirname, './public_html/js'),
publicPath: '/js/',
filename: 'bundle.js'
},
module: mod_common,
devtool: (process.env.NODE_ENV!=='production') ? '#eval-source-map' : false
}
if (process.env.NODE_ENV === 'production') {
// http://vue-loader.vuejs.org/en/workflow/production.html
module.exports.plugins = (module.exports.plugins || []).concat([
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
}),
new UglifyJSPlugin({
sourceMap: true
}),
new webpack.LoaderOptionsPlugin({
minimize: true
})
])
}

View File

@ -0,0 +1,378 @@
export default class AudioManager{
static get BUFFER_SIZE(){ return 8192; }
constructor(){
/* (1) Initialise our AudioNodes
---------------------------------------------------------*/
/* (1) Build Audio Context */
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
/* (2) Create the MASTER gain */
this.master = this.ctx.createGain();
/* (3) Initialise input (typically bound from recorder) */
this.input = null;
/* (4) Shortcut our output */
this.output = this.ctx.destination;
/* (5) Connect MASTER gain to output */
this.master.connect(this.output);
/* (2) Initialise processing attributes
---------------------------------------------------------*/
/* (1) Container for our recorder */
this.recorder = null;
/* (2) Initialise filters */
this.filters = {
voice_clarity: this.ctx.createBiquadFilter(),
voice_fullness: this.ctx.createBiquadFilter(),
voice_presence: this.ctx.createBiquadFilter(),
voice_sss: this.ctx.createBiquadFilter()
};
/* (3) Create network I/O controller (WebSocket) */
this.network = {
out: this.ctx.createScriptProcessor(AudioManager.BUFFER_SIZE, 1, 1)
};
/* (4) Initialise websocket */
this.ws = null;
/* (5) Bind network controller to send() function */
this.network.out.onaudioprocess = this.send.bind(this);
/* (6) Set up our filters' parameters */
this.setUpFilters();
/* (9) Debug data */
this.dbg = {
interval: 10, // debug every ... second
def: {
packets_received: 0,
packets_sent: 0,
kB_received: 0,
kB_sent: 0
},
data: {
packets_received: 0,
packets_sent: 0,
kB_received: 0,
kB_sent: 0
}
};
setInterval(function(){
console.group('debug');
for( let k in this.data ){
console.log(`${this.data[k]} ${k}`)
this.data[k] = this.def[k]
}
console.groupEnd('debug');
}.bind(this.dbg), this.dbg.interval*1000);
}
/* (2) Setup filters
*
---------------------------------------------------------*/
setUpFilters(){
/* (1) Setup filter parameters
---------------------------------------------------------*/
/* (1) Setup EQ#1 -> voice clarity */
this.filters.voice_clarity.type = 'peaking';
this.filters.voice_clarity.frequency.value = 3000;
this.filters.voice_clarity.Q.value = .8;
this.filters.voice_clarity.gain.value = 2;
/* (2) Setup EQ#2 -> voice fullness */
this.filters.voice_fullness.type = 'peaking';
this.filters.voice_fullness.frequency.value = 200;
this.filters.voice_fullness.Q.value = .8;
this.filters.voice_fullness.gain.value = 2;
/* (3) Setup EQ#3 -> reduce voice presence */
this.filters.voice_presence.type = 'peaking';
this.filters.voice_presence.frequency.value = 5000;
this.filters.voice_presence.Q.value = .8;
this.filters.voice_presence.gain.value = -2;
/* (4) Setup EQ#3 -> reduce 'sss' metallic sound */
this.filters.voice_sss.type = 'peaking';
this.filters.voice_sss.frequency.value = 7000;
this.filters.voice_sss.Q.value = .8;
this.filters.voice_sss.gain.value = -8;
/* (2) Connect filters
---------------------------------------------------------*/
/* (1) Connect clarity to fullness */
this.filters.voice_clarity.connect( this.filters.voice_fullness );
/* (2) Connect fullness to presence reduction */
this.filters.voice_fullness.connect( this.filters.voice_presence );
/* (3) Connect presence reduction to 'ss' removal */
this.filters.voice_presence.connect( this.filters.voice_sss );
}
/* (3) Filter toggle
*
* @unlink<boolean> Whether to unlink filters (directly bind to output)
*
---------------------------------------------------------*/
linkFilters(unlink=false){
/* (1) Disconnect all by default */
this.input.disconnect();
/* (2) Get first filter */
let first_filter = this.filters.voice_clarity;
let last_filter = this.filters.voice_sss;
/* (3) If unlink -> connect directly to NETWORK output */
if( unlink === true )
return this.input.connect(this.network.out);
/* (4) If linking -> connect input to filter stack */
this.input.connect(first_filter);
/* (5) If linking -> connect stack end to network.out */
last_filter.connect(this.network.out);
}
/* (2) Binds an input stream
*
---------------------------------------------------------*/
bindRecorderStream(_stream){
/* (1) Bind audio stream
---------------------------------------------------------*/
this.input = this.ctx.createMediaStreamSource(_stream);
/* (2) By default: link through filters to output
---------------------------------------------------------*/
this.linkFilters();
}
/* (3) Send chunks (Float32Array)
*
---------------------------------------------------------*/
send(_audioprocess){
let buf32 = new Float32Array(AudioManager.BUFFER_SIZE);
_audioprocess.inputBuffer.copyFromChannel(buf32, 0);
let buf16 = this.f32toi16(buf32);
// exit if no connection
if( this.ws === null || this.ws.readyState !== 1 )
return;
this.ws.send(buf16);
this.dbg.data.packets_sent++;
this.dbg.data.kB_sent += buf16.length * 16. / 8 / 1024;
}
/* (4) Play received chunks (Int16Array)
*
---------------------------------------------------------*/
receive(_buffer){
/* (1) Convert to Float32Array */
let buf32 = this.i16tof32(_buffer);
/* (2) Create source node */
let source = this.ctx.createBufferSource();
/* (3) Create buffer and dump data */
let input_buffer = this.ctx.createBuffer(1, AudioManager.BUFFER_SIZE, this.ctx.sampleRate);
input_buffer.getChannelData(0).set(buf32);
/* (4) Bind buffer to source node */
source.buffer = input_buffer;
/* (5) Create a dedicated *muted* gain */
let gain = this.ctx.createGain();
/* (6) source -> gain -> MASTER + play() */
source.connect(gain);
gain.connect(this.master);
/* (7) Start playing */
source.start(this.ctx.currentTime);
this.dbg.data.packets_received++;
this.dbg.data.kB_received += _buffer.length * 16. / 8 / 1024;
}
/* (4) Convert Float32Array to Int16Array
*
* @buf32<Float32Array> Input
*
* @return buf16<Int16Array> Converted output
*
---------------------------------------------------------*/
f32toi16(buf32){
/* (1) Initialise output */
let buf16 = new Int16Array(buf32.length);
/* (2) Initialize loop */
let i = 0, l = buf32.length;
/* (3) Convert each value */
for( ; i < l ; i++ )
buf16[i] = (buf32[i] < 0) ? 0x8000 * buf32[i] : 0x7FFF * buf32[i];
return buf16;
}
/* (2) Convert Int16Array to Float32Array
*
* @buf16<Int16Array> Input
*
* @return buf32<Float32Array> Converted output
*
---------------------------------------------------------*/
i16tof32(buf16){
/* (1) Initialise output */
let buf32 = new Float32Array(buf16.length);
/* (2) Initialize loop */
let i = 0, l = buf16.length;
/* (3) Convert each value */
for( ; i < l ; i++ )
buf32[i] = (buf16[i] >= 0x8000) ? -(0x10000 * buf16[i])/0x8000 : buf16[i] / 0x7FFF;
return buf32;
}
/* (8) Connect websocket
*
* @address<String> Websocket address
*
---------------------------------------------------------*/
wsconnect(_addr){
/* (1) Create websocket connection */
this.ws = new WebSocket(_addr);
/* (2) Manage websocket responses */
this.ws.onmessage = function(_msg){
if( !(_msg.data instanceof Blob) )
return console.warn('[NaB] Not A Blob');
let fr = new FileReader();
fr.onload = function(){
let buf16 = new Int16Array(fr.result);
this.receive(buf16);
}.bind(this);
fr.readAsArrayBuffer(_msg.data);
}.bind(this);
/* (3) Debug */
this.ws.onopen = () => console.warn('[audio] websocket connected');
this.ws.onclose = () => console.warn('[audio] websocket closed');
}
/* (x) Access microphone + launch all
*
---------------------------------------------------------*/
launch(wsAddress='wss://ws.douscord.xdrm.io/audio/2'){
/* (1) Start websocket */
this.wsconnect(wsAddress);
if( navigator.mediaDevices && navigator.mediaDevices.getUserMedia ){
navigator.mediaDevices.getUserMedia({ audio: true })
.then( stream => {
this.recorder = new MediaRecorder(stream);
this.bindRecorderStream(stream);
this.recorder.onstart = () => console.warn('[audio] recording');
this.recorder.onstop = () => {
this.recorder.stream.getTracks().map( t => t.stop() );
this.recorder = null;
console.warn('[audio] stopped recording');
};
// start recording
this.recorder.start();
})
.catch( e => console.warn('[audio] microphone permission issue', e) );
}else
console.warn('[audio] microphone not supported');
}
/* (x) Shut down microphone + kill all
*
---------------------------------------------------------*/
kill(){
/* (1) Close websocket */
this.ws.close();
/* (2) Stop recording */
this.recorder.stop();
/* (3) Volume 0 */
this.master.gain.setValueAtTime(0, this.ctx.currentTime);
}
}

View File

@ -396,10 +396,6 @@ export default class ContentController{
new Notification(title, { body: body }); new Notification(title, { body: body });
} }
// 9. If not self on current channel -> notification sound
if( room.messages.length > 0 && room.messages[0].uid !== auth.user.uid && ri === gs.get.content.rid )
AudioManager.pop();
} }

View File

@ -46,20 +46,20 @@ export default class RoomController{
if( type === 'text' ) if( type === 'text' )
this[type].current = room.id; this[type].current = room.id;
/* (5) If 'voice' room -> toggle audio */ /* (5) Tell websocket: new text room */
if( type === 'text' && window.csock instanceof wscd )
csock.send({ buffer: { rid: room.id } });
/* (6) If 'voice' room -> launch audio */
if( type === 'voice' ){ if( type === 'voice' ){
AudioManager.kill();
csock.send({ buffer: { audio: false } });
if( typeof this[type].current === 'number' ) if( typeof this[type].current === 'number' )
AudioManager.launch(this[type].current); AudioManager.launch();
else
AudioManager.kill();
} }
/* (6) Tell websocket we connect to room */
if( typeof this[type].current === 'number' && window.csock instanceof wscd )
csock.send({ buffer: { rid: this[type].current } });
/* (6) Update buffer */ /* (6) Update buffer */
this._buffer[type] = {}; this._buffer[type] = {};

42
webpack/main.js Normal file
View File

@ -0,0 +1,42 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import auth_wrapper from './vue/auth/wrapper.vue'
import noauth_wrapper from './vue/noauth/wrapper.vue'
/* (1) Setup: Vue, VueRouter, Authentication, ClientDriver-s */
require('./setup.js');
/* (2) Set router hooks to load page data before loading content */
gs.get.router.beforeEach((to, from, next) => {
// {1} Ignore null name //
if( to.name == null )
next();
// {2} Get appropriate page location //
let auth_folder = (gs.get.authed) ? 'auth' : 'noauth';
let page_file = to.name || gs.get.routes[auth_folder][0].name;
// {3} Load page script //
require(`./page/${auth_folder}/${page_file}.js`);
// console.log(`./page/${auth_folder}/${page_file}.js`);
// {4} Let VueRouter do the magic //
next();
});
/* (3) Select appropriate wrapper */
const wrapper = (gs.get.authed) ? auth_wrapper : noauth_wrapper;
/* (4) Render view */
Vue.use(VueRouter);
new Vue({
el: '#vue',
router: gs.get.router,
render(h){ return h(wrapper); }
})

View File

@ -0,0 +1,28 @@
import PopupController from '../../lib/popup-controller'
import ContentController from '../../lib/content-controller'
import RoomController from '../../lib/room-controller'
import ChannelController from '../../lib/channel-controller'
/* (1) Channel data gathering
---------------------------------------------------------*/
/* (1) Store route params */
window.initial_link = gs.get.router.history.current.params.link;
console.log(`[channel.URL] ${initial_link}`);
/* (2) Main components
---------------------------------------------------------*/
/* (1) Initialize popup management */
gs.set('popup', new PopupController());
/* (2) Initialize content management */
gs.set('content', new ContentController());
/* (3) Initialize rooms & room menu */
gs.set('room', new RoomController());
/* (4) Initialize channels & channel menu */
gs.set('channel', new ChannelController());
gs.get.channel.fetch(); // fetch at load time

View File

@ -0,0 +1,76 @@
/* (1) Initialise
---------------------------------------------------------*/
/* (1) Default data structure */
gs.set('login', {
// fields
username: new FieldValidator('basic-name', ''),
password: new FieldValidator('password', ''),
// login failed
failed: false,
// functions
func: {
login(){},
forgot_pass(){},
press_enter(){}
}
});
/* (2) Login attempt
*
---------------------------------------------------------*/
gs.get.login.func.login = function(){
/* (1) Cache fields' values */
let username = this.username.mutable;
let password = this.password.mutable;
/* (2) Manage errors */
if( !this.username.is_valid() )
return false;
if( !this.password.is_valid() )
return false;
/* (3) API bindings */
api.call('GET /user/token', {}, function(rs){
// manage error
if( rs.error !== 0 || rs.token == null || rs.uid == null )
return this.failed = true;
// store TOKEN + user data
auth.token = rs.token;
auth.user = {
uid: rs.uid,
username: username
};
document.location = '';
}.bind(this), encodeURI(`${username}:${password}`));
}.bind(gs.get.login);
/* (4) Manage pressing on enter
*
---------------------------------------------------------*/
gs.get.login.func.press_enter = function(e){
// if enter -> launch login
if( e.keyCode === 13 )
this.func.login();
}.bind(gs.get.login);

View File

@ -0,0 +1,83 @@
/* (1) Initialise
---------------------------------------------------------*/
/* (1) Default data structure */
gs.set('register', {
// fields
mail: new FieldValidator('bypass', ''),
username: new FieldValidator('basic-name', ''),
password: new FieldValidator('password', ''),
// functions
func: {
register(){},
press_enter(){}
}
});
/* (2) Login attempt
*
---------------------------------------------------------*/
gs.get.register.func.register = function(){
/* (1) Cache fields' values */
let mail = this.mail.mutable;
let username = this.username.mutable;
let password = this.password.mutable;
/* (2) Manage errors */
// mail error
if( !this.mail.is_valid() )
return false;
// username error
if( !this.username.is_valid() )
return false;
// password error
if( !this.password.is_valid() )
return false;
/* (3) API bindings */
api.call('POST /user', { username: username, password: password }, function(rs){
// manage error
if( rs.error !== 0 || rs.uid == null || rs.token == null )
return gs.get.router.push('register');
// manage login
auth.token = rs.token;
auth.user = {
uid: rs.uid,
username: username
};
document.location = '';
});
}.bind(gs.get.register);
/* (4) Manage pressing on enter
*
---------------------------------------------------------*/
gs.get.register.func.press_enter = function(e){
// if enter -> launch register
if( e.keyCode === 13 )
this.func.register();
}.bind(gs.get.register);

View File

@ -10,7 +10,6 @@ $side-menu-width: 14em;
$menu-bg: #202225; $menu-bg: #202225;
$dialog-header-bg: #2f3136; $dialog-header-bg: #2f3136;
$dialog-bg: #2f3136; $dialog-bg: #2f3136;
$audio-bg: #2a2c31;
$container-bg: #36393e; $container-bg: #36393e;
$header-bg: #36393f; $header-bg: #36393f;
$main: #7289da; $main: #7289da;

209
webpack/scss/dialog.scss Normal file
View File

@ -0,0 +1,209 @@
@import 'constants';
#WRAPPER > div.dialog{
display: block;
position: absolute;
top: 0;
left: $menu-width;
width: $dialog-width;
height: 100%;
background-color: $dialog-bg;
/* (2) Container HEADER */
& > div.header{
display: block;
position: absolute;
top: 0;
left: 0;
width: calc( 100% - 2*1em );
height: calc( #{$header-height} - #{$header-height/2} );
border-bottom: #{$bb-height} solid darken($dialog-header-bg, 5%);
box-shadow: 0 #{$bb-offset - $bb-height} 0 darken($dialog-header-bg, 2%);
z-index: 200;
font-weight: bold;
padding: #{$header-height/4} 1em;
background: url('../asset/svg/arrow.down@hover.svg') right .7em center no-repeat;
background-size: auto 1.2em;
cursor: pointer;
z-index: 100;
&[data-open='1']{
background-image: url('../asset/svg/cross.svg');
}
}
/* (3) Container BODY */
& > div.body{
display: block;
position: absolute;
top: calc( #{$header-height} + #{$bb-offset} );
left: 0;
width: 100%;
height: calc( 100% - #{$header-height} - #{$bb-offset} );
background-color: $dialog-bg;
color: #72767d;
z-index: 100;
// overflow: hidden;
/* (3) Toggle label */
& div.toggle{
display: inline-block;
width: calc( 100% - 2*.5em - 3em );
margin: 0 .5em;
margin-top: 2em;
padding-left: 1em;
background: left center no-repeat;
background-size: auto 60%;
color: #72767d;
font-size: .7em;
letter-spacing: .05em;
font-weight: bold;
text-transform: uppercase;
transition: color .2s ease-in-out;
&:hover{ color: #b9bbbe; }
cursor: pointer;
// when not toggle-active
&[data-toggle],
&[data-toggle='0']{
background-image: url('../asset/svg/arrow.right.svg');
&:hover{ background-image: url('../asset/svg/arrow.right@hover.svg'); }
}
// when toggle-active
&[data-toggle='1']{
background-image: url('../asset/svg/arrow.down.svg');
&:hover{ background-image: url('../asset/svg/arrow.down@hover.svg'); }
}
}
/* (4) Add button */
& div.add{
display: inline-block;
position: relative;
top: .15em;
left: -.5em;
width: 1em;
height: 1em;
background: url('../asset/svg/dialog.add.svg') center center no-repeat;
background-size: auto 100%;
cursor: pointer;
&:hover{ background-image: url('../asset/svg/dialog.add@hover.svg'); }
}
/* (5) UL after toggle label */
& div.toggle ~ ul,
& div.toggle:not([data-toggle='1']) ~ ul{
display: none;
/* (5) List items */
li{
display: block;
position: relative;
margin: .1em .5em;
padding: .3em .5em;
padding-left: 1.6em;
border-radius: 3px / 3px;
background-color: transition;
transition: color .2s ease-in-out,
background-color .2s ease-in-out;
cursor: pointer;
&:hover{
color: #ddd;
background-color: #36393f;
}
// {1} Trailing icon //
&:before{
content: '';
display: inline-block;
position: absolute;
margin-left: -1.3em;
width: 1.3em;
height: 1.3em;
background: center center no-repeat;
background-size: auto 80%;
}
// for 'text'
&[data-type='text']{
&:before{ background-image: url('../asset/svg/dialog.text.svg'); }
&.active{
color: #ddd;
background-color: rgba(79,84,92,.6);
}
}
// for 'voice'
&[data-type='voice']{
&:before{ background-image: url('../asset/svg/dialog.voice.svg'); }
&.active{ color: #ddd; }
}
// background-color: #f00;
& > span.rem{
display: none;
position: absolute;
top: calc( 50% - 1em/2 );
left: calc( 100% - .5em - 1em );
width: 1em;
height: 1em;
background: url('../asset/svg/minipopup.remove.svg') center center no-repeat;
background-size: contain;
&:hover{ background-image: url('../asset/svg/minipopup.remove@hover.svg'); }
}
// only show 'remove' icon on hover
&:hover > span.rem{ display: block; }
}
}
// when visible
& div.toggle[data-toggle='1'] ~ ul{ display: block; }
}
}

72
webpack/setup.js Normal file
View File

@ -0,0 +1,72 @@
/* (1) VueJS data */
import VueRouter from 'vue-router'
import GlobalStore from './lib/gstore'
import routes from './routes'
/* (2) Custom libs */
import Authentication from './lib/authentication.js'
import XHRClientDriver from './lib/client/xhr.js'
import WebSocketClientDriver from './lib/client/ws.js'
import APIClient from './lib/api-client.js'
/* (1) Custom lib accessors
---------------------------------------------------------*/
/* (1) Field validation */
require('./lib/field-manager.js');
/* (2) Global Store for Vue */
window.gs = new GlobalStore();
/* (3) Authentication token management */
window.auth = new Authentication();
gs.set('auth', auth);
/* (4) XHR / WebSocket drivers */
window.xhrcd = XHRClientDriver;
window.wscd = WebSocketClientDriver;
/* (5) ClientDriver instances */
window.api = new APIClient('api.douscord.xdrm.io');
window.ws = new WebSocketClientDriver('ws.douscord.xdrm.io');
/* (2) Global data
---------------------------------------------------------*/
/* (1) Get Full URI */
gs.set('URI', document.URL.replace(/^(?:[^\/]+\/\/|[^\/]+\/)/, '').split('/').filter(function(v,i){ return !!i && v.length; }));
/* (2) Store routes */
gs.set('routes', routes);
/* (3) Store if authenticated */
gs.set('authed', auth.token !== null);
/* (4) Init. vue router */
gs.set('router', new VueRouter({
routes: gs.get.authed ? gs.get.routes['auth'] : gs.get.routes['noauth']
}));
/* (5) refresh page */
gs.set('refresh', () => ( document.location = '' ) );
/* (6) Connection status */
gs.set('connection', 1); // null -> normal, 0 -> offline, 1 -> connecting, 2 -> online
/* (7) Ask for permission API */
Notification.requestPermission();
/* (8) DEBUG MODE */
window.DEBUG_MOD = false;
// audio management
window.AudioManager = new (require('./lib/audio-manager.js').default)();

149
webpack/vue/auth/dialog.vue Normal file
View File

@ -0,0 +1,149 @@
<style lang="scss">
@keyframes scale-up{
0%{ transform: scale(0); }
100%{ transform: scale(1); }
}
.minipopup{
display: block;
position: absolute;
top: 0;
left: 0;
width: calc( 100% - 2*1em );
height: auto;
margin: .7em 1em;
border-radius: 5px;
box-shadow: 0 2px 10 0 rgba(0,0,0,.5);
background-color: #ffffff;
will-change: transform;
transform-origin: 100% 0;
animation: scale-up .3s ease-in-out;
overflow: hidden;
z-index: 100;
& > span{
display: block;
position: relative;
padding: .7em 1em;
padding-left: 3em;
background-color: #fff;
background: url('/asset/svg/minipopup.invite.svg') left 1em center no-repeat;
background-size: auto 45%;
color: #99aab5;
font-size: .85em;
letter-spacing: .05em;
white-space: nowrap;
cursor: pointer;
transition: background-color .2s ease-in-out,
color .2s ease-in-out;
&[data-icon='create']{ background-image: url('/asset/svg/minipopup.create.svg'); }
&[data-icon='category']{ background-image: url('/asset/svg/minipopup.category.svg'); }
&[data-icon='edit']{ background-image: url('/asset/svg/minipopup.edit.svg'); }
&[data-icon='remove']{ background-image: url('/asset/svg/minipopup.remove.svg'); }
&[data-icon='remove']{ background-image: url('/asset/svg/minipopup.remove.svg'); }
&[data-icon='logout']{ background-image: url('/asset/svg/minipopup.logout.svg'); }
&[data-icon='leave']{ background-image: url('/asset/svg/minipopup.leave.svg'); }
&[data-icon='password']{ background-image: url('/asset/svg/minipopup.password.svg'); }
&:hover{ background-color: #f9f9f9; color: #737f8d; }
/* separators */
/*after*/ &.sa{ border-bottom: 1px solid #f3f3f3; }
/*before*/ &.sb{ border-top: 1px solid #f3f3f3; }
&.special{
color: #7289da;
&:hover{ color: #677bc4; }
}
&.invalid{ color: #e65835; }
&.invalid-h:hover{ color: #e65835; }
}
}
</style>
<template>
<div class='dialog'>
<div class='header' @click='minipop = !minipop' :data-open='minipop?1:0'>
<div class='title'>{{ gs.content.cbuf.label }}</div>
</div>
<div class='body'>
<div v-show='minipop' class='minipopup'>
<span class='special sa' @click='gs.popup.show(`channel.invite`); minipop=false'>Invite people</span>
<span data-icon='create' @click='gs.popup.show(`channel.create`); minipop=false'>Create channel</span>
<span class ='invalid-h' data-icon='remove' @click='gs.popup.show(`channel.remove`); minipop=false'>Remove channel</span>
<span data-icon='category' @click='gs.popup.show(`room.create`); minipop=false'>Create room</span>
<span data-icon='edit' @click='gs.popup.show(`username.change`); minipop=false'>Change nickname</span>
<span data-icon='password' @click='gs.popup.show(`password.change`); minipop=false'>Change password</span>
<span class='sb invalid-h' data-icon='leave' @click='gs.popup.show(`channel.leave`); minipop=false'>Leave channel</span>
<span class='sb invalid' data-icon='logout' @click='gs.auth.token=null; gs.refresh()'>Logout</span>
</div>
<div v-for='(rooms, type) in gs.room' v-if='type[0] != `_`'>
<div class='toggle'
:data-toggle='rooms.visible?1:0'
@click='rooms.visible=!rooms.visible'>
{{ type }} <span>rooms</span>
</div>
<div class='add' @click='gs.popup.show(`room.create`)' data-title='Create channel'></div>
<ul>
<li v-for='r in rooms.list'
:class='rooms.current==r.id?`active`:``'
:data-type='r.type'
@click='gs.room.nav(r.type, r.id)'>{{ r.name }}
<span class='rem' @click="gs.popup.show('room.remove'); gs.popup.get('room.remove').data=r"></span>
</li>
</ul>
</div>
</div>
</div>
</template><script>
export default {
name: 'dialog-',
data(){ return { gs: gs.get, minipop: false }; },
methods: {
/* show: */
}
}
</script>