Compare commits

..

42 Commits

Author SHA1 Message Date
xdrm-brackets 5166847f77 added audio room connection/deco management 2018-04-24 11:08:06 +02:00
xdrm-brackets b25d0b6249 [scss.dialog] added icons for each 'voice' room connected user [vue.auth.dialog] added connected members to 'voice' rooms [lib.room-controller] added sending { rid: ROOM_ID } for voice rooms (COMMENTED FOR NOW) waiting for SeekDaSky to implement it on the server 2018-04-11 17:11:23 +02:00
xdrm-brackets 3f06b3dc39 [lib.audio-manager].launch(room_id) tested + [lib.room-controller].nav('voice') always kills audio-manager before starting it 2018-04-11 16:20:09 +02:00
xdrm-brackets d205985305 [lib.audio-manager].launch(room_id) takes now an argument + [lib.room-controller].nav('voice') now launches with room_id 2018-04-11 16:13:38 +02:00
xdrm-brackets d21674e5ad [README] created 2018-04-10 16:54:20 +02:00
xdrm-brackets 9695d30e5f [package.json] added 'electron-packager' to build sources + 'electron-installer-debian' for .deb files + added electron.js to load index.html + added electron.json to describe our app 2018-04-10 16:04:52 +02:00
xdrm-brackets bdbfa902fb [lib.audio-manager] added dynamic mic volume 2018-04-10 14:20:27 +02:00
xdrm-brackets fc0150cc74 [lib.audio-manager] maybe latency minfix ?? 2018-04-10 13:55:15 +02:00
xdrm-brackets 95d3487820 [package.json] added electron build commands + dependencies [install.sh] linux installation script (/usr/share/applications) 2018-04-10 10:27:39 +02:00
xdrm-brackets 066cae054e [lib.audio-manager] chromium/webkit final patch 2018-04-10 01:07:57 +02:00
xdrm-brackets baf7804857 [lib.audio-manager] chromium/webkit fix + revert to MediaRecorder + removed debugs 2018-04-10 00:56:10 +02:00
xdrm-brackets 04c6e37527 [lib.audio-manager] chromium/webkit fix (must connect scriptProcessor to output 2018-04-10 00:53:17 +02:00
xdrm-brackets 9f6bc383bf [lib.audio-manager] back to 4096 2018-04-09 23:44:41 +02:00
xdrm-brackets 151e76b30a [lib.audio-manager] tweaked + pushed 4096 to 8192 2018-04-09 23:41:45 +02:00
xdrm-brackets 3aab1a93ba [vue.auth.dialog] updated dialog 'frequency' canvas now it take the whole height 2018-04-09 23:37:42 +02:00
xdrm-brackets 80e9bd2b05 [vue.auth.dialog] updated dialog 'frequency' canvas ++ 2018-04-09 23:32:55 +02:00
xdrm-brackets 86864ab8b2 [vue.auth.dialog] updated dialog 'frequency' canvas 2018-04-09 23:30:14 +02:00
xdrm-brackets aa3d896ed9 [lib.audio-manager] added stack max size = 2 2018-04-09 19:26:30 +02:00
xdrm-brackets 8cc640c47b [lib.audio-manager] added stack max size = 3 2018-04-09 19:24:19 +02:00
xdrm-brackets 7d767343d6 [lib.audio-manager] added stack max size 2018-04-09 19:21:26 +02:00
xdrm-brackets a06e9c1789 [lib.audio-manager] fixed buffer read (send() ScriptProcessor) + receive() only pushes to stack 2018-04-09 19:11:00 +02:00
xdrm-brackets f7d6e530b4 [lib.audio-manager] fixed buffer chain 2018-04-09 19:05:08 +02:00
xdrm-brackets cdd2c42129 [lib.audio-manager] added input volume + websocket buffer chain 2018-04-09 18:49:50 +02:00
xdrm-brackets e783de3675 [packages.json] 'npm run clean' does not fail anymore 2018-04-08 12:59:10 +02:00
xdrm-brackets 1e08abbd5e [packages.json] added electron 'npm run build:electron' 2018-04-08 12:53:27 +02:00
xdrm-brackets 381107e459 fix attempt 2018-04-07 16:54:13 +02:00
xdrm-brackets c9a2ac0f4a fix > removed HotModuleReload server for 'npm run dev' 2018-04-07 16:42:33 +02:00
xdrm-brackets 0b484a88a7 fix > 'parcel watch' works but 'parcel build' fails, removed 'minifyers' 2018-04-07 16:29:11 +02:00
xdrm-brackets 52dc5909b0 [index] updated redundancy 2018-04-07 16:24:21 +02:00
xdrm-brackets 139184a981 [setup] is now an asynchronous Promise() 2018-04-07 16:22:02 +02:00
xdrm-brackets 1baf2b5e06 [package.json].build() removed source maps 2018-04-07 16:13:11 +02:00
xdrm-brackets 9150874287 [main.js] now asynchronous page data loading -> each page script is a Promise() that must success before calling next() on VueRouter to load the according Vue 2018-04-07 16:11:59 +02:00
xdrm-brackets 51875ca4e8 [main.js] now asynchronous page data loading 2018-04-07 15:51:27 +02:00
xdrm-brackets c84037e2eb [parcel] moved into this folder all assets that will be bundled + [webpack.config] removed 2018-04-07 15:11:50 +02:00
xdrm-brackets a59903b1ad [ALL] setup for 'parcel bundler' 2018-04-07 15:06:03 +02:00
xdrm-brackets c24c6a95b1 [setup] by default do not show 'audio status' 2018-04-06 18:43:05 +02:00
xdrm-brackets d862215cd8 [lib.audio-manager] FREQUENCY or WAVE management (dispatch to callbacks if defined) + [vue.auth.dialog] added draw_freq() + default one + added draw_wave() can toggle between 2 viewers with 'gs.get.toggleDrawStyle()' 2018-04-06 18:39:14 +02:00
xdrm-brackets 2742fd818a [vue.auth.dialog] refactor + relayout audio status + added logout working icon that logout from 'voice' room + [lib.audio-manager] when calling kill() do not set volume to 0 because POP notifications will be then blocked 2018-04-06 17:02:40 +02:00
xdrm-brackets 19a9c39a5f [vue.auth.dialog] added audio status (connecting, listening, voice_connected) bound from [lib.audio-manager] 2018-04-06 16:44:15 +02:00
xdrm-brackets 3d557a8193 [lib.audio-manager] debuug is not automatically started, you must use debug() method 2018-04-06 16:15:15 +02:00
xdrm-brackets 2181bbb317 [lib.content-controller] added audio notification POP when receiving another's message on the current channel 2018-04-06 14:29:56 +02:00
xdrm-brackets 09d0f55666 [lib.audio-manager] added pop() notification sound 2018-04-06 14:26:15 +02:00
92 changed files with 1837 additions and 1191 deletions

5
.babelrc Normal file
View File

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

10
.gitignore vendored
View File

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

6
.postcssrc Normal file
View File

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

98
README.md Normal file
View File

@ -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;
```

43
electron.js Normal file
View File

@ -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;
});
});

19
electron.json Normal file
View File

@ -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": {
}
}

25
install.sh Normal file
View File

@ -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 <<<";

View File

@ -1,24 +1,30 @@
{
"name": "ptut-vhost",
"name": "douscord",
"description": "PTUT",
"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,
"scripts": {
"bundle:clean": "exit 0",
"bundle:prod": "cross-env NODE_ENV=production webpack --progress --hide-modules",
"bundle:dev": "cross-env NODE_ENV=development webpack --progress --hide-modules",
"bundle:watch": "cross-env NODE_ENV=development webpack --progress --watch --hide-modules",
"scss": "node-sass -r --output-style compressed --output ./public_html/css ./webpack/scss",
"watch-css": "node-sass -w -r --output-style compressed --output ./public_html/css ./webpack/scss",
"dev": "npm run bundle:clean; npm run bundle:dev; npm run watch-css",
"devjs": "npm run bundle:clean; npm run bundle:watch",
"build": "npm run bundle:clean; npm run bundle:prod; npm run scss"
"clean": "rm ./public_html/*.html; rm ./public_html/*.js; rm ./public_html/*.css; rm ./public_html/*svg; rm ./public_html/*.map; exit 0",
"clean:all": "npm run clean; npm run clean:electron; npm run clean:release; npm run clean:package; exit 0",
"clean:electron": "rm -r ./electron-build; exit 0",
"clean:release": "rm -r ./releases; exit 0",
"clean:package": "rm -r ./package; exit 0",
"dev": "parcel watch ./parcel/index.html --out-dir ./public_html --no-hmr",
"build:alternative": "cross-env NODE_ENV=production parcel watch ./parcel/index.html --out-dir ./public_html --no-hmr",
"build": "parcel build ./parcel/index.html --public-url ./ --out-dir ./public_html --no-source-maps --no-minify",
"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": {
"uglifyjs-webpack-plugin": "^1.2.3",
"vue": "^2.5.9",
"vue-hot-reload-api": "^2.3.0",
"vue-router": "^2.5.3"
},
"browserslist": [
@ -27,20 +33,17 @@
"not ie <= 8"
],
"devDependencies": {
"@vue/component-compiler-utils": "^1.0.0",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"cross-env": "^5.0.5",
"babel-preset-env": "^1.6.0",
"babel-preset-stage-3": "^6.24.1",
"css-loader": "^0.28.7",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.4",
"node-sass": "^4.7.2",
"sass-loader": "^6.0.6",
"vue-loader": "^13.0.5",
"vue-svg-loader": "^0.5.0",
"vue-template-compiler": "^2.5.9",
"webpack": "^3.8.1",
"webpack-dev-server": "^2.9.5"
"cross-env": "^5.0.5",
"electron": "^1.8.4",
"electron-installer-debian": "^0.8.1",
"electron-packager": "^12.0.0",
"node-sass": "^4.8.3",
"parcel-bundler": "^1.7.0",
"vue-template-compiler": "^2.5.16"
}
}

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -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

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 801 B

After

Width:  |  Height:  |  Size: 801 B

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 768 B

After

Width:  |  Height:  |  Size: 768 B

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 928 B

After

Width:  |  Height:  |  Size: 928 B

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 295 B

View File

Before

Width:  |  Height:  |  Size: 226 B

After

Width:  |  Height:  |  Size: 226 B

View File

Before

Width:  |  Height:  |  Size: 214 B

After

Width:  |  Height:  |  Size: 214 B

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 450 B

View File

Before

Width:  |  Height:  |  Size: 402 B

After

Width:  |  Height:  |  Size: 402 B

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -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

35
parcel/index.html Normal file
View File

@ -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>

548
parcel/lib/audio-manager.js Normal file
View File

@ -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 );
}
}

View File

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

View File

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

50
parcel/main.js Normal file
View File

@ -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); }
});
});

View File

@ -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();
});

View File

@ -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();
});

View File

@ -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();
});

View File

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

464
parcel/scss/dialog.scss Normal file
View File

@ -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; }
}
}
}

72
parcel/setup.js Normal file
View File

@ -0,0 +1,72 @@
/* (1) VueJS data */
import VueRouter from 'vue-router'
import GlobalStore from './lib/gstore'
import routes from './routes'
/* (2) Custom libs */
import Authentication from './lib/authentication.js'
import XHRClientDriver from './lib/client/xhr.js'
import WebSocketClientDriver from './lib/client/ws.js'
import APIClient from './lib/api-client.js'
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();
});

193
parcel/vue/auth/dialog.vue Normal file
View File

@ -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>

View File

@ -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>

View File

@ -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
})
])
}

View File

@ -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);
}
}

View File

@ -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); }
})

View File

@ -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

View File

@ -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);

View File

@ -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);

View File

@ -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; }
}
}

View File

@ -1,72 +0,0 @@
/* (1) VueJS data */
import VueRouter from 'vue-router'
import GlobalStore from './lib/gstore'
import routes from './routes'
/* (2) Custom libs */
import Authentication from './lib/authentication.js'
import XHRClientDriver from './lib/client/xhr.js'
import WebSocketClientDriver from './lib/client/ws.js'
import APIClient from './lib/api-client.js'
/* (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)();

View File

@ -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>