Compare commits
42 Commits
audio-beta
...
master
Author | SHA1 | Date |
---|---|---|
xdrm-brackets | 5166847f77 | |
xdrm-brackets | b25d0b6249 | |
xdrm-brackets | 3f06b3dc39 | |
xdrm-brackets | d205985305 | |
xdrm-brackets | d21674e5ad | |
xdrm-brackets | 9695d30e5f | |
xdrm-brackets | bdbfa902fb | |
xdrm-brackets | fc0150cc74 | |
xdrm-brackets | 95d3487820 | |
xdrm-brackets | 066cae054e | |
xdrm-brackets | baf7804857 | |
xdrm-brackets | 04c6e37527 | |
xdrm-brackets | 9f6bc383bf | |
xdrm-brackets | 151e76b30a | |
xdrm-brackets | 3aab1a93ba | |
xdrm-brackets | 80e9bd2b05 | |
xdrm-brackets | 86864ab8b2 | |
xdrm-brackets | aa3d896ed9 | |
xdrm-brackets | 8cc640c47b | |
xdrm-brackets | 7d767343d6 | |
xdrm-brackets | a06e9c1789 | |
xdrm-brackets | f7d6e530b4 | |
xdrm-brackets | cdd2c42129 | |
xdrm-brackets | e783de3675 | |
xdrm-brackets | 1e08abbd5e | |
xdrm-brackets | 381107e459 | |
xdrm-brackets | c9a2ac0f4a | |
xdrm-brackets | 0b484a88a7 | |
xdrm-brackets | 52dc5909b0 | |
xdrm-brackets | 139184a981 | |
xdrm-brackets | 1baf2b5e06 | |
xdrm-brackets | 9150874287 | |
xdrm-brackets | 51875ca4e8 | |
xdrm-brackets | c84037e2eb | |
xdrm-brackets | a59903b1ad | |
xdrm-brackets | c24c6a95b1 | |
xdrm-brackets | d862215cd8 | |
xdrm-brackets | 2742fd818a | |
xdrm-brackets | 19a9c39a5f | |
xdrm-brackets | 3d557a8193 | |
xdrm-brackets | 2181bbb317 | |
xdrm-brackets | 09d0f55666 |
|
@ -2,6 +2,10 @@
|
||||||
.sass-cache
|
.sass-cache
|
||||||
*.map
|
*.map
|
||||||
/node_modules
|
/node_modules
|
||||||
/public_html/css
|
/public_html/*
|
||||||
/public_html/js
|
!/public_htm/.htaccess
|
||||||
/package-lock.json
|
/package-lock.json
|
||||||
|
.cache
|
||||||
|
/releases
|
||||||
|
/packages
|
||||||
|
/electron-build
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"modules": false,
|
||||||
|
"plugins": {
|
||||||
|
"autoprefixer": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
# 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;
|
||||||
|
```
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"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": {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
#!/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 <<<";
|
47
package.json
|
@ -1,24 +1,30 @@
|
||||||
{
|
{
|
||||||
"name": "ptut-vhost",
|
"name": "douscord",
|
||||||
"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": {
|
||||||
"bundle:clean": "exit 0",
|
"clean": "rm ./public_html/*.html; rm ./public_html/*.js; rm ./public_html/*.css; rm ./public_html/*svg; rm ./public_html/*.map; exit 0",
|
||||||
"bundle:prod": "cross-env NODE_ENV=production webpack --progress --hide-modules",
|
"clean:all": "npm run clean; npm run clean:electron; npm run clean:release; npm run clean:package; exit 0",
|
||||||
"bundle:dev": "cross-env NODE_ENV=development webpack --progress --hide-modules",
|
"clean:electron": "rm -r ./electron-build; exit 0",
|
||||||
"bundle:watch": "cross-env NODE_ENV=development webpack --progress --watch --hide-modules",
|
"clean:release": "rm -r ./releases; exit 0",
|
||||||
"scss": "node-sass -r --output-style compressed --output ./public_html/css ./webpack/scss",
|
"clean:package": "rm -r ./package; exit 0",
|
||||||
"watch-css": "node-sass -w -r --output-style compressed --output ./public_html/css ./webpack/scss",
|
"dev": "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:alternative": "cross-env NODE_ENV=production parcel watch ./parcel/index.html --out-dir ./public_html --no-hmr",
|
||||||
"devjs": "npm run bundle:clean; npm run bundle:watch",
|
"build": "parcel build ./parcel/index.html --public-url ./ --out-dir ./public_html --no-source-maps --no-minify",
|
||||||
"build": "npm run bundle:clean; npm run bundle:prod; npm run scss"
|
"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: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": [
|
||||||
|
@ -27,20 +33,17 @@
|
||||||
"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",
|
||||||
"css-loader": "^0.28.7",
|
"cross-env": "^5.0.5",
|
||||||
"extract-text-webpack-plugin": "^3.0.2",
|
"electron": "^1.8.4",
|
||||||
"file-loader": "^1.1.4",
|
"electron-installer-debian": "^0.8.1",
|
||||||
"node-sass": "^4.7.2",
|
"electron-packager": "^12.0.0",
|
||||||
"sass-loader": "^6.0.6",
|
"node-sass": "^4.8.3",
|
||||||
"vue-loader": "^13.0.5",
|
"parcel-bundler": "^1.7.0",
|
||||||
"vue-svg-loader": "^0.5.0",
|
"vue-template-compiler": "^2.5.16"
|
||||||
"vue-template-compiler": "^2.5.9",
|
|
||||||
"webpack": "^3.8.1",
|
|
||||||
"webpack-dev-server": "^2.9.5"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1,6 @@
|
||||||
|
<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>
|
After Width: | Height: | Size: 930 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 801 B After Width: | Height: | Size: 801 B |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 768 B After Width: | Height: | Size: 768 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 928 B After Width: | Height: | Size: 928 B |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 295 B |
Before Width: | Height: | Size: 226 B After Width: | Height: | Size: 226 B |
Before Width: | Height: | Size: 214 B After Width: | Height: | Size: 214 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 450 B |
Before Width: | Height: | Size: 402 B After Width: | Height: | Size: 402 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1,22 @@
|
||||||
|
<?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>
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -0,0 +1,35 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,548 @@
|
||||||
|
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 );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -396,6 +396,10 @@ 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();
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) Tell websocket: new text room */
|
/* (5) If 'voice' room -> toggle audio */
|
||||||
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' ){
|
||||||
|
|
||||||
if( typeof this[type].current === 'number' )
|
|
||||||
AudioManager.launch();
|
|
||||||
else
|
|
||||||
AudioManager.kill();
|
AudioManager.kill();
|
||||||
|
csock.send({ buffer: { audio: false } });
|
||||||
|
|
||||||
|
if( typeof this[type].current === 'number' )
|
||||||
|
AudioManager.launch(this[type].current);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* (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] = {};
|
|
@ -0,0 +1,50 @@
|
||||||
|
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); }
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,33 @@
|
||||||
|
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();
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,83 @@
|
||||||
|
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();
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,90 @@
|
||||||
|
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();
|
||||||
|
|
||||||
|
});
|
|
@ -10,6 +10,7 @@ $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;
|
|
@ -0,0 +1,464 @@
|
||||||
|
@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; }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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'
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,193 @@
|
||||||
|
<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>
|
|
@ -1,36 +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='./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>
|
|
|
@ -1,85 +0,0 @@
|
||||||
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
|
|
||||||
})
|
|
||||||
])
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,378 +0,0 @@
|
||||||
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);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,42 +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');
|
|
||||||
|
|
||||||
|
|
||||||
/* (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); }
|
|
||||||
})
|
|
|
@ -1,28 +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'
|
|
||||||
|
|
||||||
|
|
||||||
/* (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
|
|
|
@ -1,76 +0,0 @@
|
||||||
/* (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);
|
|
|
@ -1,83 +0,0 @@
|
||||||
/* (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);
|
|
|
@ -1,209 +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} );
|
|
||||||
|
|
||||||
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; }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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'
|
|
||||||
|
|
||||||
/* (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)();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,149 +0,0 @@
|
||||||
<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>
|
|