mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 12:55:55 +00:00
4.0.0 Beta 1
This commit is contained in:
parent
c2688646af
commit
262757a20d
187 changed files with 22878 additions and 38882 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,6 +1,7 @@
|
|||
node_modules
|
||||
npm-debug.log
|
||||
build
|
||||
dist
|
||||
Extension Building
|
||||
Old Files
|
||||
badges
|
||||
|
|
28
README.md
28
README.md
|
@ -1,7 +1,7 @@
|
|||
FrankerFaceZ
|
||||
============
|
||||
|
||||
Copyright (c) 2016 Dan Salvato LLC
|
||||
Copyright (c) 2017 Dan Salvato LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0. See LICENSE.
|
||||
|
||||
|
@ -12,26 +12,18 @@ Developing
|
|||
FrankerFaceZ uses node.js to manage development dependencies and to run an HTTP
|
||||
server for development. To get everything you need:
|
||||
|
||||
1. Install node.js
|
||||
2. Run ```npm install -g gulp``` to install the ```gulp``` command line utility.
|
||||
3. Run ```npm install``` within the FrankerFaceZ directory.
|
||||
1. Install node.js and npm
|
||||
2. Run ```npm install`` within the FrankerFaceZ directory.
|
||||
|
||||
|
||||
From there, you can use gulp to build the extension from source simply by
|
||||
running ```gulp```. For development, you can instruct gulp to watch the source
|
||||
files for changes and re-build automatically with ```gulp watch```
|
||||
From there, you can use npm to build the extension from source simply by
|
||||
running ```npm run build```. For development, you can instruct gulp to watch
|
||||
the source files for changes and re-build automatically with ```npm start```
|
||||
|
||||
FrankerFaceZ comes with a local development server that listens on port 8000
|
||||
and it serves up local development copies of files, falling back to the CDN
|
||||
when a local copy of a file isn't present. To start the server,
|
||||
run ```gulp server```
|
||||
when a local copy of a file isn't present.
|
||||
|
||||
For convenience, the server is run automatically along with ```gulp watch```
|
||||
|
||||
|
||||
Use the command ```/ffz developer_mode on``` or ```/ffz developer_mode off```
|
||||
in Twitch chat to toggle developer mode on or off. You must then refresh the
|
||||
page for changes to take effect. If FFZ is not working or the command otherwise
|
||||
fails to work, you can open the JavaScript console on twitch.tv and run
|
||||
```localStorage.ffzDebugMode = true;``` or
|
||||
```localStorage.ffzDebugMode = false;``` to enable or disable the feature.
|
||||
To make FrankerFaceZ load from your local development server, you must set
|
||||
the local storage variable ```ffzDebugMode``` to true. Just run the following
|
||||
in your console on Twitch: ```localStorage.ffzDebugMode = true;```
|
||||
|
|
|
@ -1,70 +1,7 @@
|
|||
<div class="list-header">3.5.536 <time datetime="2017-11-07">(2017-11-07)</time></div>
|
||||
<div class="list-header">4.0.0-beta1 <time datetime="2017-11-12">(2017-11-12)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Changed: Enable Transparent (Colored) badges in non-Webkit browsers. Note: The feature may not function correctly in all browsers.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">3.5.535 <time datetime="2017-11-02">(2017-11-02)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Fixed: Raid notices not rendering in chat.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">3.5.534 <time datetime="2017-10-04">(2017-10-04)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Fixed: Reset Player button not getting added to player because the menu element was renamed.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">3.5.533 <time datetime="2017-10-01">(2017-10-01)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Added: Experimental new feature, promoted messages.</li>
|
||||
<li> </li>
|
||||
<li>Moderators can promote a message to have it appear in Recent Highlights for users that
|
||||
have the feature enabled. To promote a message, a moderator must use the command
|
||||
<code>/ffz promote <id></code> with the appropriate message id. The easiest
|
||||
way to do this is to add a custom in-line moderation icon with the command
|
||||
<code>/ffz promote {id}</code></li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">3.5.532 <time datetime="2017-09-30">(2017-09-30)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Changed: And the tooltip rendering tweaks keep coming</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">3.5.531 <time datetime="2017-09-29">(2017-09-29)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Changed: Properly calculate tooltip positions when added to an element other than the document root.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">3.5.530 <time datetime="2017-09-29">(2017-09-29)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Fixed: Make sure the dashboard module is loaded before trying to modify dashboard widgets.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">3.5.529 <time datetime="2017-09-28">(2017-09-28)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Changed: Rewrite tooltip rendering to try making them better positioned.</li>
|
||||
<li>Added: Clickable links in the Following tooltip, as a result of the rewrite.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">3.5.528 <time datetime="2017-09-26">(2017-09-26)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Fixed: Bug loading emote data from the server.</li>
|
||||
<li>Fixed: Initialize the full Ember modifications when loading the settings pages and products page that has been brought into the Ember app.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">3.5.527 <time datetime="2017-09-25">(2017-09-25)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Changed: More tweaks to tooltip rendering to support backend improvements.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">3.5.526 <time datetime="2017-09-22">(2017-09-22)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Changed: Minor tweaks to tooltip rendering to support improvements to the backend.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">3.5.525 <time datetime="2017-09-20">(2017-09-20)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Fixed: Twitch's native dark theme breaking the re-colored site header.</li>
|
||||
<li>Fixed: Take control of Twitch's native dark theme when enabling or disabling FFZ's dark theme.</li>
|
||||
<li>This is the initial release of the complete rewrite, FrankerFaceZ v4.</li>
|
||||
<li>Changed: Every single thing.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header" id="ffz-old-news-button"><a href="#">View Older</a></div>
|
||||
|
|
292
gulpfile.js
292
gulpfile.js
|
@ -1,292 +0,0 @@
|
|||
// Dependencies
|
||||
var fs = require('fs'),
|
||||
gulp = require('gulp'),
|
||||
browserify = require('gulp-browserify'),
|
||||
header = require('gulp-header'),
|
||||
footer = require('gulp-footer'),
|
||||
concat = require('gulp-concat'),
|
||||
clean = require('gulp-clean'),
|
||||
util = require('gulp-util'),
|
||||
rename = require('gulp-rename'),
|
||||
uglify = require('gulp-uglify');
|
||||
|
||||
// Templates
|
||||
var jsEscape = require('gulp-js-escape'),
|
||||
wrap = require('gulp-wrap'),
|
||||
declare = require('gulp-declare'),
|
||||
cleanCSS = require('gulp-clean-css');
|
||||
|
||||
|
||||
// LESS
|
||||
var less = require('gulp-less'),
|
||||
sourcemaps = require('gulp-sourcemaps');
|
||||
|
||||
|
||||
// Deploy Dependencies
|
||||
var ftp = require('vinyl-ftp'),
|
||||
request = require('request');
|
||||
|
||||
|
||||
// Server Dependencies
|
||||
var http = require("http"),
|
||||
https = require("https"),
|
||||
net = require('net'),
|
||||
path = require("path"),
|
||||
request = require("request"),
|
||||
url = require("url");
|
||||
|
||||
var server_version = "0.1.1";
|
||||
|
||||
|
||||
// Tasks
|
||||
|
||||
gulp.task('clean', function() {
|
||||
return gulp.src('build', {read:false})
|
||||
.pipe(clean());
|
||||
});
|
||||
|
||||
gulp.task('prepare', ['clean'], function() {
|
||||
return gulp.src(['src/**/*'])
|
||||
.pipe(gulp.dest('build/'));
|
||||
});
|
||||
|
||||
|
||||
//gulp.task('templates', ['prepare'], function() {
|
||||
// return gulp.src(['build/templates/**/*.hbs'])
|
||||
// .pipe(jsEscape())
|
||||
// .pipe(wrap('Handlebars.compile(<%= contents %>)'))
|
||||
// .pipe(declare({
|
||||
// root: 'exports',
|
||||
// noRedeclare: true,
|
||||
// processName: function(filePath) {
|
||||
// var match = filePath.match(/build[\\\/]templates[\\\/](.*)\.hbs$/);
|
||||
// return declare.processNameByPath((match && match.length > 1) ? match[1] : filePath);
|
||||
// }
|
||||
// }))
|
||||
// .pipe(concat('templates.js'))
|
||||
// .pipe(gulp.dest('build/'))
|
||||
// .on('error', util.log);
|
||||
//});
|
||||
|
||||
gulp.task('styles', ['prepare'], function() {
|
||||
//return;
|
||||
return gulp.src(['build/less/*.less', '!build/less/style.less'])
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(less())
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest(__dirname))
|
||||
.on('error', util.log);
|
||||
});
|
||||
|
||||
|
||||
gulp.task('embedded_styles', ['prepare'], function() {
|
||||
return gulp.src(['build/styles/**/*.css'])
|
||||
.pipe(cleanCSS())
|
||||
.pipe(jsEscape())
|
||||
.pipe(declare({
|
||||
root: 'exports',
|
||||
noRedeclare: true,
|
||||
processName: function(filePath) {
|
||||
var match = filePath.match(/build[\\\/]styles[\\\/](.*)\.css$/);
|
||||
return declare.processNameByPath((match && match.length > 1) ? match[1] : filePath);
|
||||
}
|
||||
}))
|
||||
.pipe(concat('compiled_styles.js'))
|
||||
.pipe(gulp.dest('build/'))
|
||||
.on('error', util.log)
|
||||
});
|
||||
|
||||
|
||||
gulp.task('scripts', ['embedded_styles'], function() {
|
||||
return gulp.src(['build/main.js'])
|
||||
.pipe(browserify())
|
||||
.pipe(concat('script.js'))
|
||||
.pipe(header('(function(window) {'))
|
||||
.pipe(footer(';window.ffz = new FrankerFaceZ()}(window));'))
|
||||
.pipe(gulp.dest(__dirname))
|
||||
.on('error', util.log);
|
||||
});
|
||||
|
||||
|
||||
gulp.task('watch', ['default', 'server'], function() {
|
||||
return gulp.watch('src/**/*', ['default']);
|
||||
});
|
||||
|
||||
gulp.task('default', ['styles', 'scripts']);
|
||||
|
||||
|
||||
// Deploy
|
||||
|
||||
gulp.task('minify_script', ['scripts'], function() {
|
||||
return gulp.src(['*.js', '!*.min.js', '!gulpfile.js'])
|
||||
.pipe(uglify())
|
||||
.pipe(rename(function(path) {
|
||||
path.basename += '.min';
|
||||
}))
|
||||
.pipe(gulp.dest(__dirname))
|
||||
.on('error', util.log);
|
||||
});
|
||||
|
||||
gulp.task('minify_style', function() {
|
||||
return gulp.src(['*.css', '!*.min.css'])
|
||||
.pipe(cleanCSS())
|
||||
.pipe(rename(function(path) {
|
||||
path.basename += '.min';
|
||||
}))
|
||||
.pipe(gulp.dest(__dirname))
|
||||
.on('error', util.log);
|
||||
});
|
||||
|
||||
gulp.task('minify', ['minify_script', 'minify_style']);
|
||||
|
||||
|
||||
gulp.task('upload', ['minify'], function() {
|
||||
// Load credentials from an external file.
|
||||
var contents = fs.readFileSync('credentials.json', 'utf8'),
|
||||
cred = JSON.parse(contents);
|
||||
|
||||
cred.log = util.log;
|
||||
|
||||
// Create the connection.
|
||||
var conn = ftp.create(cred);
|
||||
|
||||
// What we're transfering.
|
||||
var ftp_path = cred.remote_path,
|
||||
|
||||
globs = [
|
||||
"script.min.js",
|
||||
"style.min.css",
|
||||
"dark.min.css",
|
||||
"changelog.html"
|
||||
];
|
||||
|
||||
util.log(cred.remote_path);
|
||||
|
||||
return gulp.src(globs, {base: '.', buffer: false})
|
||||
.pipe(conn.newerOrDifferentSize(ftp_path))
|
||||
.pipe(conn.dest(ftp_path))
|
||||
.on('error', util.log);
|
||||
});
|
||||
|
||||
gulp.task('clear_cache', ['upload'], function(cb) {
|
||||
// Load credentials from an external file.
|
||||
var contents = fs.readFileSync('credentials.json', 'utf8'),
|
||||
cred = JSON.parse(contents);
|
||||
|
||||
// Build the URLs.
|
||||
var base = "://cdn.frankerfacez.com/script/",
|
||||
files = [],
|
||||
globs = [
|
||||
"script.min.js",
|
||||
"style.min.css",
|
||||
"dark.min.css",
|
||||
"changelog.html"
|
||||
];
|
||||
|
||||
for(var i=0; i < globs.length; i++) {
|
||||
files.push("http" + base + globs[i]);
|
||||
files.push("https" + base + globs[i]);
|
||||
}
|
||||
|
||||
request({
|
||||
method: 'DELETE',
|
||||
uri: "https://api.cloudflare.com/client/v4/zones/" + cred.cloudflare_zone + "/purge_cache",
|
||||
headers: {
|
||||
"X-Auth-Email": cred.cloudflare_email,
|
||||
"X-Auth-Key": cred.cloudflare_key
|
||||
},
|
||||
json: {
|
||||
"files": files
|
||||
}
|
||||
}, function(error, request, body) {
|
||||
if ( error )
|
||||
return util.log("[FAIL] Error: " + error);
|
||||
else if ( request.statusCode !== 200 )
|
||||
return util.log("[FAIL] Non-200 Status: " + request.statusCode);
|
||||
|
||||
util.log("[SUCCESS] Cache cleared.");
|
||||
cb();
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task('deploy', ['upload', 'clear_cache']);
|
||||
|
||||
|
||||
// Server
|
||||
|
||||
gulp.task('server', function() {
|
||||
var handle_req = function(req, res) {
|
||||
var uri = url.parse(req.url).pathname,
|
||||
lpath = path.join(uri).split(path.sep);
|
||||
|
||||
if ( uri == "/dev_server" ) {
|
||||
util.log("[" + util.colors.cyan("HTTP") + "] " + util.colors.green("200") + " GET " + util.colors.magenta(uri));
|
||||
res.writeHead(200, {"Content-Type": "application/json", "Access-Control-Allow-Origin": "*"});
|
||||
return res.end(JSON.stringify({path: process.cwd(), version: server_version}));
|
||||
}
|
||||
|
||||
if ( ! lpath[0] )
|
||||
lpath.shift();
|
||||
|
||||
if ( lpath[0] == "script" )
|
||||
lpath.shift();
|
||||
else
|
||||
lpath.splice(0, 0, "cdn");
|
||||
|
||||
var file = path.join(process.cwd(), lpath.join(path.sep));
|
||||
|
||||
fs.exists(file, function(exists) {
|
||||
if ( ! exists ) {
|
||||
util.log("[" + util.colors.cyan("HTTP") + "] " + util.colors.bold.blue("CDN") + " GET " + util.colors.magenta(uri));
|
||||
return request.get("http://cdn.frankerfacez.com/" + uri).on('error', function(err) { res.end() }).pipe(res);
|
||||
}
|
||||
|
||||
var headers = {"Access-Control-Allow-Origin": "*"};
|
||||
|
||||
if ( fs.lstatSync(file).isDirectory() ) {
|
||||
util.log("[" + util.colors.cyan("HTTP") + "] " + util.colors.red("403") + " GET " + util.colors.magenta(uri));
|
||||
res.writeHead(403, headers);
|
||||
res.write('403 Forbidden');
|
||||
return res.end();
|
||||
}
|
||||
|
||||
if ( file.substr(file.length-4) === ".svg" )
|
||||
headers['Content-Type'] = 'image/svg+xml';
|
||||
|
||||
util.log("[" + util.colors.cyan("HTTP") + "] " + util.colors.green("200") + " GET " + util.colors.magenta(uri));
|
||||
res.writeHead(200, headers);
|
||||
fs.createReadStream(file).pipe(res);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
if ( fs.existsSync("dev_key.pem") ) {
|
||||
var https_options = {
|
||||
key: fs.readFileSync("dev_key.pem"),
|
||||
cert: fs.readFileSync("dev_cert.pem")
|
||||
};
|
||||
|
||||
http.createServer(handle_req).listen(8001, "localhost");
|
||||
https.createServer(https_options, handle_req).listen(8002, "localhost");
|
||||
|
||||
net.createServer(function(conn) {
|
||||
conn.on('error', function(e) {
|
||||
util.log("[" + util.colors.cyan("HTTP") + "] Connection Error: " + util.colors.magenta('' + e));
|
||||
});
|
||||
|
||||
conn.once('data', function(buf) {
|
||||
var address = (buf[0] === 22) ? 8002 : 8001;
|
||||
var proxy = net.createConnection(address, function() {
|
||||
proxy.write(buf);
|
||||
conn.pipe(proxy).pipe(conn);
|
||||
});
|
||||
});
|
||||
}).listen(8000);
|
||||
|
||||
util.log("[" + util.colors.cyan("HTTPS") + "] Listening on Port: " + util.colors.magenta("8000"));
|
||||
|
||||
} else {
|
||||
http.createServer(handle_req).listen(8000, "localhost");
|
||||
util.log("[" + util.colors.cyan("HTTP") + "] Listening on Port: " + util.colors.magenta("8000"));
|
||||
}
|
||||
});
|
|
@ -1,3 +1,72 @@
|
|||
<div class="list-header">3.5.536 <time datetime="2017-11-07">(2017-11-07)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Changed: Enable Transparent (Colored) badges in non-Webkit browsers. Note: The feature may not function correctly in all browsers.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">3.5.535 <time datetime="2017-11-02">(2017-11-02)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Fixed: Raid notices not rendering in chat.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">3.5.534 <time datetime="2017-10-04">(2017-10-04)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Fixed: Reset Player button not getting added to player because the menu element was renamed.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">3.5.533 <time datetime="2017-10-01">(2017-10-01)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Added: Experimental new feature, promoted messages.</li>
|
||||
<li> </li>
|
||||
<li>Moderators can promote a message to have it appear in Recent Highlights for users that
|
||||
have the feature enabled. To promote a message, a moderator must use the command
|
||||
<code>/ffz promote <id></code> with the appropriate message id. The easiest
|
||||
way to do this is to add a custom in-line moderation icon with the command
|
||||
<code>/ffz promote {id}</code></li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">3.5.532 <time datetime="2017-09-30">(2017-09-30)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Changed: And the tooltip rendering tweaks keep coming</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">3.5.531 <time datetime="2017-09-29">(2017-09-29)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Changed: Properly calculate tooltip positions when added to an element other than the document root.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">3.5.530 <time datetime="2017-09-29">(2017-09-29)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Fixed: Make sure the dashboard module is loaded before trying to modify dashboard widgets.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">3.5.529 <time datetime="2017-09-28">(2017-09-28)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Changed: Rewrite tooltip rendering to try making them better positioned.</li>
|
||||
<li>Added: Clickable links in the Following tooltip, as a result of the rewrite.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">3.5.528 <time datetime="2017-09-26">(2017-09-26)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Fixed: Bug loading emote data from the server.</li>
|
||||
<li>Fixed: Initialize the full Ember modifications when loading the settings pages and products page that has been brought into the Ember app.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">3.5.527 <time datetime="2017-09-25">(2017-09-25)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Changed: More tweaks to tooltip rendering to support backend improvements.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">3.5.526 <time datetime="2017-09-22">(2017-09-22)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Changed: Minor tweaks to tooltip rendering to support improvements to the backend.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">3.5.525 <time datetime="2017-09-20">(2017-09-20)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Fixed: Twitch's native dark theme breaking the re-colored site header.</li>
|
||||
<li>Fixed: Take control of Twitch's native dark theme when enabling or disabling FFZ's dark theme.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">3.5.524 <time datetime="2017-09-15">(2017-09-15)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Fixed: Tooltips for global emotes not saying where they're from.</li>
|
||||
|
|
7387
package-lock.json
generated
Normal file
7387
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
63
package.json
63
package.json
|
@ -1,32 +1,47 @@
|
|||
{
|
||||
"name": "FrankerFaceZ",
|
||||
"author": "Stendec",
|
||||
"version": "3.0.0",
|
||||
"description": "FrankerFaceZ gives Twitch users custom emoticons.",
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.0.0",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"main": "script.js",
|
||||
"scripts": {
|
||||
"start": "webpack-dev-server --config webpack.web.dev.js",
|
||||
"build": "webpack --config webpack.web.prod.js --define process.env.NODE_ENV='production'",
|
||||
"build:prod": "webpack --config webpack.web.prod.js --define process.env.NODE_ENV='production'",
|
||||
"build:dev": "webpack --config webpack.web.dev.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-browserify": "^0.5.1",
|
||||
"gulp-changed": "^1.3.2",
|
||||
"gulp-clean": "^0.3.2",
|
||||
"gulp-clean-css": "^2.0.12",
|
||||
"gulp-concat": "^2.6.0",
|
||||
"gulp-declare": "^0.3.0",
|
||||
"gulp-filesize": "0.0.6",
|
||||
"gulp-footer": "^1.0.5",
|
||||
"gulp-header": "^1.8.8",
|
||||
"gulp-js-escape": "^1.0.1",
|
||||
"gulp-less": "^3.1.0",
|
||||
"gulp-rename": "^1.2.2",
|
||||
"gulp-sourcemaps": "^1.6.0",
|
||||
"gulp-uglify": "^2.0.0",
|
||||
"gulp-util": "^3.0.7",
|
||||
"gulp-wrap": "^0.13.0",
|
||||
"request": "^2.74.0",
|
||||
"vinyl-ftp": "^0.5.0"
|
||||
"clean-webpack-plugin": "^0.1.17",
|
||||
"copy-webpack-plugin": "^4.1.0",
|
||||
"css-loader": "^0.28.7",
|
||||
"eslint": "^4.9.0",
|
||||
"extract-loader": "^1.0.1",
|
||||
"file-loader": "^1.1.5",
|
||||
"less": "^2.7.2",
|
||||
"node-sass": "^4.5.3",
|
||||
"raw-loader": "^0.5.1",
|
||||
"sass-loader": "^6.0.6",
|
||||
"style-loader": "^0.18.2",
|
||||
"to-string-loader": "^1.1.5",
|
||||
"uglifyjs-webpack-plugin": "^1.0.0-beta.2",
|
||||
"vue-loader": "^13.0.5",
|
||||
"webpack": "^3.6.0",
|
||||
"webpack-dev-middleware": "^1.12.0",
|
||||
"webpack-dev-server": "^2.9.1",
|
||||
"webpack-manifest-plugin": "^1.3.2",
|
||||
"webpack-merge": "^4.1.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://bitbucket.org/stendec/frankerfacez.git"
|
||||
"url": "https://github.com/FrankerFaceZ/FrankerFaceZ.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"displacejs": "^1.2.4",
|
||||
"path-to-regexp": "^2.0.0",
|
||||
"popper.js": "^1.12.6",
|
||||
"sortablejs": "^1.6.1",
|
||||
"vue": "^2.5.2",
|
||||
"vue-clickaway": "^2.1.0",
|
||||
"vue-template-compiler": "^2.5.2"
|
||||
}
|
||||
}
|
||||
|
|
BIN
res/font/ffz-fontello.eot
Normal file
BIN
res/font/ffz-fontello.eot
Normal file
Binary file not shown.
66
res/font/ffz-fontello.svg
Normal file
66
res/font/ffz-fontello.svg
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<metadata>Copyright (C) 2017 by original authors @ fontello.com</metadata>
|
||||
<defs>
|
||||
<font id="ffz-fontello" horiz-adv-x="1000" >
|
||||
<font-face font-family="ffz-fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
|
||||
<missing-glyph horiz-adv-x="1000" />
|
||||
<glyph glyph-name="cancel" unicode="" d="M724 112q0-22-15-38l-76-76q-16-15-38-15t-38 15l-164 165-164-165q-16-15-38-15t-38 15l-76 76q-16 16-16 38t16 38l164 164-164 164q-16 16-16 38t16 38l76 76q16 16 38 16t38-16l164-164 164 164q16 16 38 16t38-16l76-76q15-15 15-38t-15-38l-164-164 164-164q15-15 15-38z" horiz-adv-x="785.7" />
|
||||
|
||||
<glyph glyph-name="zreknarf" unicode="" d="M620 841c21 1 43 4 63-4 18-9 36-21 56-26 11-18 32-28 42-46 19-30 37-60 43-95 8-48 36-91 65-130 18-39 10-84 18-126 12-26 21-53 35-78 6-11 23-4 25 6 6 37-13 72-11 108 2 19-4 39 2 57 14 19 23 41 38 59 18 28 53 43 86 40 11-15 26-27 39-41 14-22 33-41 50-61 10-14 16-29 23-44 14-25 21-54 34-80 11-31 29-61 28-94 3-10 4-21 5-31 5-21 14-44 3-64-12-27-36-46-59-63-31-24-60-52-95-71-20-13-42-26-65-36-18-8-31-26-50-31-12-9-21-24-36-26-23-7-46 19-69 6-16-9-32-16-48-23-44-29-93-49-144-60-43 1-86-3-129-2-18 9-39 5-57 14-32 8-56 31-86 44-14 7-28 17-41 26-13 10-22 25-36 33-20 1-33-18-52-20-12-2-24-2-36-2-18 19-29 45-54 56-26 18-55 34-77 57-12 13-26 25-42 35-24 21-40 49-58 75-16 23-27 50-24 78 3 41 3 83 13 123 21 54 57 99 93 144 19 27 51 52 86 43 17-8 29-23 37-40 6-14 17-26 19-41 5-30 9-61 4-91-9-32-15-65-12-97 0-15 15-28 30-24 16 4 20 21 27 34 9 20 22 39 34 57 19 27 27 60 40 89 10 24 8 50 13 74 4 40-3 80 3 119 7 19 10 40 20 58 15 19 35 34 50 53 9 11 22 17 34 23 40 14 80 35 123 36z" horiz-adv-x="1277" />
|
||||
|
||||
<glyph glyph-name="search" unicode="" d="M643 386q0 103-73 176t-177 74-177-74-73-176 73-177 177-73 177 73 73 177z m286-465q0-29-22-50t-50-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 153-31 125-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />
|
||||
|
||||
<glyph glyph-name="clock" unicode="" d="M500 546v-250q0-7-5-12t-13-5h-178q-8 0-13 5t-5 12v36q0 8 5 13t13 5h125v196q0 8 5 13t12 5h36q8 0 13-5t5-13z m232-196q0 83-41 152t-110 111-152 41-153-41-110-111-41-152 41-152 110-111 153-41 152 41 110 111 41 152z m125 0q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
|
||||
|
||||
<glyph glyph-name="star" unicode="" d="M929 489q0-12-15-27l-202-197 48-279q0-4 0-12 0-11-6-19t-17-9q-10 0-22 7l-251 132-250-132q-12-7-23-7-11 0-17 9t-6 19q0 4 1 12l48 279-203 197q-14 15-14 27 0 21 31 26l280 40 126 254q11 23 27 23t28-23l125-254 280-40q32-5 32-26z" horiz-adv-x="928.6" />
|
||||
|
||||
<glyph glyph-name="star-empty" unicode="" d="M635 290l170 166-235 34-106 213-105-213-236-34 171-166-41-235 211 111 211-111z m294 199q0-12-15-27l-202-197 48-279q0-4 0-12 0-28-23-28-10 0-22 7l-251 132-250-132q-12-7-23-7-11 0-17 9t-6 19q0 4 1 12l48 279-203 197q-14 15-14 27 0 21 31 26l280 40 126 254q11 23 27 23t28-23l125-254 280-40q32-5 32-26z" horiz-adv-x="928.6" />
|
||||
|
||||
<glyph glyph-name="down-dir" unicode="" d="M571 457q0-14-10-25l-250-250q-11-11-25-11t-25 11l-250 250q-11 11-11 25t11 25 25 11h500q14 0 25-11t10-25z" horiz-adv-x="571.4" />
|
||||
|
||||
<glyph glyph-name="right-dir" unicode="" d="M321 350q0-14-10-25l-250-250q-11-11-25-11t-25 11-11 25v500q0 15 11 25t25 11 25-11l250-250q10-10 10-25z" horiz-adv-x="357.1" />
|
||||
|
||||
<glyph glyph-name="attention" unicode="" d="M571 83v106q0 8-5 13t-12 5h-108q-7 0-12-5t-5-13v-106q0-8 5-13t12-6h108q7 0 12 6t5 13z m-1 208l10 257q0 6-5 10-7 6-14 6h-122q-6 0-14-6-5-4-5-12l9-255q0-5 6-9t13-3h103q8 0 14 3t5 9z m-7 522l428-786q20-35-1-70-9-17-26-26t-35-10h-858q-18 0-35 10t-26 26q-21 35-1 70l429 786q9 17 26 27t36 10 36-10 27-27z" horiz-adv-x="1000" />
|
||||
|
||||
<glyph glyph-name="ok" unicode="" d="M933 534q0-22-16-38l-404-404-76-76q-16-15-38-15t-38 15l-76 76-202 202q-15 16-15 38t15 38l76 76q16 16 38 16t38-16l164-165 366 367q16 16 38 16t38-16l76-76q16-15 16-38z" horiz-adv-x="1000" />
|
||||
|
||||
<glyph glyph-name="cog" unicode="" d="M571 350q0 59-41 101t-101 42-101-42-42-101 42-101 101-42 101 42 41 101z m286 61v-124q0-7-4-13t-11-7l-104-16q-10-30-21-51 19-27 59-77 6-6 6-13t-5-13q-15-21-55-61t-53-39q-7 0-14 5l-77 60q-25-13-51-21-9-76-16-104-4-16-20-16h-124q-8 0-14 5t-6 12l-16 103q-27 9-50 21l-79-60q-6-5-14-5-8 0-14 6-70 64-92 94-4 5-4 13 0 6 5 12 8 12 28 37t30 40q-15 28-23 55l-102 15q-7 1-11 7t-5 13v124q0 7 5 13t10 7l104 16q8 25 22 51-23 32-60 77-6 7-6 14 0 5 5 12 15 20 55 60t53 40q7 0 15-5l77-60q24 13 50 21 9 76 17 104 3 16 20 16h124q7 0 13-5t7-12l15-103q28-9 51-20l79 59q5 5 13 5 7 0 14-5 72-67 92-95 4-5 4-12 0-7-4-13-9-12-29-37t-30-40q15-28 23-54l102-16q7-1 12-7t4-13z" horiz-adv-x="857.1" />
|
||||
|
||||
<glyph glyph-name="plus" unicode="" d="M786 439v-107q0-22-16-38t-38-15h-232v-233q0-22-16-37t-38-16h-107q-22 0-38 16t-15 37v233h-232q-23 0-38 15t-16 38v107q0 23 16 38t38 16h232v232q0 22 15 38t38 16h107q23 0 38-16t16-38v-232h232q23 0 38-16t16-38z" horiz-adv-x="785.7" />
|
||||
|
||||
<glyph glyph-name="folder-open" unicode="" d="M1049 319q0-17-18-37l-187-221q-24-28-67-48t-81-20h-607q-19 0-33 7t-15 24q0 17 17 37l188 221q24 28 67 48t80 20h607q19 0 34-7t15-24z m-192 192v-90h-464q-53 0-110-26t-92-67l-188-221-2-3q0 2-1 7t0 7v536q0 51 37 88t88 37h179q51 0 88-37t37-88v-18h303q52 0 88-37t37-88z" horiz-adv-x="1071.4" />
|
||||
|
||||
<glyph glyph-name="download" unicode="" d="M714 100q0 15-10 25t-25 11-25-11-11-25 11-25 25-11 25 11 10 25z m143 0q0 15-10 25t-26 11-25-11-10-25 10-25 25-11 26 11 10 25z m72 125v-179q0-22-16-37t-38-16h-821q-23 0-38 16t-16 37v179q0 22 16 38t38 16h259l75-76q33-32 76-32t76 32l76 76h259q22 0 38-16t16-38z m-182 318q10-23-8-39l-250-250q-10-11-25-11t-25 11l-250 250q-17 16-8 39 10 21 33 21h143v250q0 15 11 25t25 11h143q14 0 25-11t10-25v-250h143q24 0 33-21z" horiz-adv-x="928.6" />
|
||||
|
||||
<glyph glyph-name="upload" unicode="" d="M714 29q0 14-10 25t-25 10-25-10-11-25 11-25 25-11 25 11 10 25z m143 0q0 14-10 25t-26 10-25-10-10-25 10-25 25-11 26 11 10 25z m72 125v-179q0-22-16-38t-38-16h-821q-23 0-38 16t-16 38v179q0 22 16 38t38 15h238q12-31 39-51t62-20h143q34 0 61 20t40 51h238q22 0 38-15t16-38z m-182 361q-9-22-33-22h-143v-250q0-15-10-25t-25-11h-143q-15 0-25 11t-11 25v250h-143q-23 0-33 22-9 22 8 39l250 250q10 10 25 10t25-10l250-250q18-17 8-39z" horiz-adv-x="928.6" />
|
||||
|
||||
<glyph glyph-name="floppy" unicode="" d="M214-7h429v214h-429v-214z m500 0h72v500q0 8-6 21t-11 20l-157 156q-5 6-19 12t-22 5v-232q0-22-15-38t-38-16h-322q-22 0-37 16t-16 38v232h-72v-714h72v232q0 22 16 38t37 16h465q22 0 38-16t15-38v-232z m-214 518v178q0 8-5 13t-13 5h-107q-7 0-13-5t-5-13v-178q0-7 5-13t13-5h107q7 0 13 5t5 13z m357-18v-518q0-22-15-38t-38-16h-750q-23 0-38 16t-16 38v750q0 22 16 38t38 16h517q23 0 50-12t42-26l156-157q16-15 27-42t11-49z" horiz-adv-x="857.1" />
|
||||
|
||||
<glyph glyph-name="twitter" unicode="" d="M904 622q-37-54-90-93 0-8 0-23 0-73-21-145t-64-139-103-117-144-82-181-30q-151 0-276 81 19-2 43-2 126 0 224 77-59 1-105 36t-64 89q19-3 34-3 24 0 48 6-63 13-104 62t-41 115v2q38-21 82-23-37 25-59 64t-22 86q0 49 25 91 68-83 164-133t208-55q-5 21-5 41 0 75 53 127t127 53q79 0 132-57 61 12 115 44-21-64-80-100 52 6 104 28z" horiz-adv-x="928.6" />
|
||||
|
||||
<glyph glyph-name="gauge" unicode="" d="M214 207q0 30-21 51t-50 21-51-21-21-51 21-50 51-21 50 21 21 50z m107 250q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m239-268l57 213q3 14-5 27t-21 16-27-3-17-22l-56-213q-33-3-60-25t-35-55q-11-43 11-81t66-50 81 11 50 66q9 33-4 65t-40 51z m369 18q0 30-21 51t-51 21-50-21-21-51 21-50 50-21 51 21 21 50z m-358 357q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m250-107q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m179-250q0-145-79-269-10-17-30-17h-782q-20 0-30 17-79 123-79 269 0 102 40 194t106 160 160 107 194 39 194-39 160-107 106-160 40-194z" horiz-adv-x="1000" />
|
||||
|
||||
<glyph glyph-name="download-cloud" unicode="" d="M714 332q0 8-5 13t-13 5h-125v196q0 8-5 13t-12 5h-108q-7 0-12-5t-5-13v-196h-125q-8 0-13-5t-5-13q0-8 5-13l196-196q5-5 13-5t13 5l196 196q5 6 5 13z m357-125q0-89-62-151t-152-63h-607q-103 0-177 73t-73 177q0 72 39 134t105 92q-1 17-1 24 0 118 84 202t202 84q87 0 159-49t105-129q40 35 93 35 59 0 101-42t42-101q0-43-23-77 72-17 119-76t46-133z" horiz-adv-x="1071.4" />
|
||||
|
||||
<glyph glyph-name="upload-cloud" unicode="" d="M714 368q0 8-5 13l-196 196q-5 5-13 5t-13-5l-196-196q-5-6-5-13 0-8 5-13t13-5h125v-196q0-8 5-13t12-5h108q7 0 12 5t5 13v196h125q8 0 13 5t5 13z m357-161q0-89-62-151t-152-63h-607q-103 0-177 73t-73 177q0 72 39 134t105 92q-1 17-1 24 0 118 84 202t202 84q87 0 159-49t105-129q40 35 93 35 59 0 101-42t42-101q0-43-23-77 72-17 119-76t46-133z" horiz-adv-x="1071.4" />
|
||||
|
||||
<glyph glyph-name="keyboard" unicode="" d="M214 198v-53q0-9-9-9h-53q-9 0-9 9v53q0 9 9 9h53q9 0 9-9z m72 143v-53q0-9-9-9h-125q-9 0-9 9v53q0 9 9 9h125q9 0 9-9z m-72 143v-54q0-9-9-9h-53q-9 0-9 9v54q0 9 9 9h53q9 0 9-9z m572-286v-53q0-9-9-9h-482q-9 0-9 9v53q0 9 9 9h482q9 0 9-9z m-357 143v-53q0-9-9-9h-54q-9 0-9 9v53q0 9 9 9h54q9 0 9-9z m-72 143v-54q0-9-9-9h-53q-9 0-9 9v54q0 9 9 9h53q9 0 9-9z m214-143v-53q0-9-8-9h-54q-9 0-9 9v53q0 9 9 9h54q8 0 8-9z m-71 143v-54q0-9-9-9h-53q-9 0-9 9v54q0 9 9 9h53q9 0 9-9z m214-143v-53q0-9-9-9h-53q-9 0-9 9v53q0 9 9 9h53q9 0 9-9z m215-143v-53q0-9-9-9h-54q-9 0-9 9v53q0 9 9 9h54q9 0 9-9z m-286 286v-54q0-9-9-9h-54q-9 0-9 9v54q0 9 9 9h54q9 0 9-9z m143 0v-54q0-9-9-9h-54q-9 0-9 9v54q0 9 9 9h54q9 0 9-9z m143 0v-196q0-9-9-9h-125q-9 0-9 9v53q0 9 9 9h62v134q0 9 9 9h54q9 0 9-9z m71-420v500h-929v-500h929z m71 500v-500q0-29-20-50t-51-21h-929q-29 0-50 21t-21 50v500q0 30 21 51t50 21h929q30 0 51-21t20-51z" horiz-adv-x="1071.4" />
|
||||
|
||||
<glyph glyph-name="ellipsis-vert" unicode="" d="M214 154v-108q0-22-15-37t-38-16h-107q-23 0-38 16t-16 37v108q0 22 16 38t38 15h107q22 0 38-15t15-38z m0 285v-107q0-22-15-38t-38-15h-107q-23 0-38 15t-16 38v107q0 23 16 38t38 16h107q22 0 38-16t15-38z m0 286v-107q0-22-15-38t-38-16h-107q-23 0-38 16t-16 38v107q0 22 16 38t38 16h107q22 0 38-16t15-38z" horiz-adv-x="214.3" />
|
||||
|
||||
<glyph glyph-name="twitch" unicode="" d="M500 608v-242h-81v242h81z m222 0v-242h-81v242h81z m0-424l141 141v444h-666v-585h182v-121l121 121h222z m222 666v-565l-242-242h-182l-121-122h-121v122h-222v646l61 161h827z" horiz-adv-x="1000" />
|
||||
|
||||
<glyph glyph-name="trash" unicode="" d="M286 82v393q0 8-5 13t-13 5h-36q-8 0-13-5t-5-13v-393q0-8 5-13t13-5h36q8 0 13 5t5 13z m143 0v393q0 8-5 13t-13 5h-36q-8 0-13-5t-5-13v-393q0-8 5-13t13-5h36q8 0 13 5t5 13z m142 0v393q0 8-5 13t-12 5h-36q-8 0-13-5t-5-13v-393q0-8 5-13t13-5h36q7 0 12 5t5 13z m-303 554h250l-27 65q-4 5-9 6h-177q-6-1-10-6z m518-18v-36q0-8-5-13t-13-5h-54v-529q0-46-26-80t-63-34h-464q-37 0-63 33t-27 79v531h-53q-8 0-13 5t-5 13v36q0 8 5 13t13 5h172l39 93q9 21 31 35t44 15h178q23 0 44-15t30-35l39-93h173q8 0 13-5t5-13z" horiz-adv-x="785.7" />
|
||||
|
||||
<glyph glyph-name="window-maximize" unicode="" d="M143 64h714v429h-714v-429z m857 625v-678q0-37-26-63t-63-27h-822q-36 0-63 27t-26 63v678q0 37 26 63t63 27h822q37 0 63-27t26-63z" horiz-adv-x="1000" />
|
||||
|
||||
<glyph glyph-name="window-minimize" unicode="" d="M1000 118v-107q0-37-26-63t-63-27h-822q-36 0-63 27t-26 63v107q0 37 26 63t63 26h822q37 0 63-26t26-63z" horiz-adv-x="1000" />
|
||||
|
||||
<glyph glyph-name="window-restore" unicode="" d="M143-7h428v286h-428v-286z m571 286h286v428h-429v-143h54q37 0 63-26t26-63v-196z m429 482v-536q0-37-26-63t-63-26h-340v-197q0-37-26-63t-63-26h-536q-36 0-63 26t-26 63v536q0 37 26 63t63 26h340v197q0 37 26 63t63 26h536q36 0 63-26t26-63z" horiz-adv-x="1142.9" />
|
||||
|
||||
<glyph glyph-name="window-close" unicode="" d="M656 113l81 81q6 6 6 13t-6 13l-130 130 130 130q6 6 6 13t-6 13l-81 81q-6 6-13 6t-13-6l-130-130-130 130q-6 6-13 6t-13-6l-81-81q-6-6-6-13t6-13l130-130-130-130q-6-6-6-13t6-13l81-81q6-6 13-6t13 6l130 130 130-130q6-6 13-6t13 6z m344 576v-678q0-37-26-63t-63-27h-822q-36 0-63 27t-26 63v678q0 37 26 63t63 27h822q37 0 63-27t26-63z" horiz-adv-x="1000" />
|
||||
</font>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 12 KiB |
BIN
res/font/ffz-fontello.ttf
Normal file
BIN
res/font/ffz-fontello.ttf
Normal file
Binary file not shown.
BIN
res/font/ffz-fontello.woff
Normal file
BIN
res/font/ffz-fontello.woff
Normal file
Binary file not shown.
BIN
res/font/ffz-fontello.woff2
Normal file
BIN
res/font/ffz-fontello.woff2
Normal file
Binary file not shown.
188
src/FileSaver.js
188
src/FileSaver.js
|
@ -1,188 +0,0 @@
|
|||
/* FileSaver.js
|
||||
* A saveAs() FileSaver implementation.
|
||||
* 1.3.2
|
||||
* 2016-06-16 18:25:19
|
||||
*
|
||||
* By Eli Grey, http://eligrey.com
|
||||
* License: MIT
|
||||
* See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
/*global self */
|
||||
/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */
|
||||
|
||||
/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
|
||||
|
||||
var saveAs = saveAs || (function(view) {
|
||||
"use strict";
|
||||
// IE <10 is explicitly unsupported
|
||||
if (typeof view === "undefined" || typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) {
|
||||
return;
|
||||
}
|
||||
var
|
||||
doc = view.document
|
||||
// only get URL when necessary in case Blob.js hasn't overridden it yet
|
||||
, get_URL = function() {
|
||||
return view.URL || view.webkitURL || view;
|
||||
}
|
||||
, save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a")
|
||||
, can_use_save_link = "download" in save_link
|
||||
, click = function(node) {
|
||||
var event = new MouseEvent("click");
|
||||
node.dispatchEvent(event);
|
||||
}
|
||||
, is_safari = /constructor/i.test(view.HTMLElement) || view.safari
|
||||
, is_chrome_ios =/CriOS\/[\d]+/.test(navigator.userAgent)
|
||||
, throw_outside = function(ex) {
|
||||
(view.setImmediate || view.setTimeout)(function() {
|
||||
throw ex;
|
||||
}, 0);
|
||||
}
|
||||
, force_saveable_type = "application/octet-stream"
|
||||
// the Blob API is fundamentally broken as there is no "downloadfinished" event to subscribe to
|
||||
, arbitrary_revoke_timeout = 1000 * 40 // in ms
|
||||
, revoke = function(file) {
|
||||
var revoker = function() {
|
||||
if (typeof file === "string") { // file is an object URL
|
||||
get_URL().revokeObjectURL(file);
|
||||
} else { // file is a File
|
||||
file.remove();
|
||||
}
|
||||
};
|
||||
setTimeout(revoker, arbitrary_revoke_timeout);
|
||||
}
|
||||
, dispatch = function(filesaver, event_types, event) {
|
||||
event_types = [].concat(event_types);
|
||||
var i = event_types.length;
|
||||
while (i--) {
|
||||
var listener = filesaver["on" + event_types[i]];
|
||||
if (typeof listener === "function") {
|
||||
try {
|
||||
listener.call(filesaver, event || filesaver);
|
||||
} catch (ex) {
|
||||
throw_outside(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
, auto_bom = function(blob) {
|
||||
// prepend BOM for UTF-8 XML and text/* types (including HTML)
|
||||
// note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF
|
||||
if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
|
||||
return new Blob([String.fromCharCode(0xFEFF), blob], {type: blob.type});
|
||||
}
|
||||
return blob;
|
||||
}
|
||||
, FileSaver = function(blob, name, no_auto_bom) {
|
||||
if (!no_auto_bom) {
|
||||
blob = auto_bom(blob);
|
||||
}
|
||||
// First try a.download, then web filesystem, then object URLs
|
||||
var
|
||||
filesaver = this
|
||||
, type = blob.type
|
||||
, force = type === force_saveable_type
|
||||
, object_url
|
||||
, dispatch_all = function() {
|
||||
dispatch(filesaver, "writestart progress write writeend".split(" "));
|
||||
}
|
||||
// on any filesys errors revert to saving with object URLs
|
||||
, fs_error = function() {
|
||||
if ((is_chrome_ios || (force && is_safari)) && view.FileReader) {
|
||||
// Safari doesn't allow downloading of blob urls
|
||||
var reader = new FileReader();
|
||||
reader.onloadend = function() {
|
||||
var url = is_chrome_ios ? reader.result : reader.result.replace(/^data:[^;]*;/, 'data:attachment/file;');
|
||||
var popup = view.open(url, '_blank');
|
||||
if(!popup) view.location.href = url;
|
||||
url=undefined; // release reference before dispatching
|
||||
filesaver.readyState = filesaver.DONE;
|
||||
dispatch_all();
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
filesaver.readyState = filesaver.INIT;
|
||||
return;
|
||||
}
|
||||
// don't create more object URLs than needed
|
||||
if (!object_url) {
|
||||
object_url = get_URL().createObjectURL(blob);
|
||||
}
|
||||
if (force) {
|
||||
view.location.href = object_url;
|
||||
} else {
|
||||
var opened = view.open(object_url, "_blank");
|
||||
if (!opened) {
|
||||
// Apple does not allow window.open, see https://developer.apple.com/library/safari/documentation/Tools/Conceptual/SafariExtensionGuide/WorkingwithWindowsandTabs/WorkingwithWindowsandTabs.html
|
||||
view.location.href = object_url;
|
||||
}
|
||||
}
|
||||
filesaver.readyState = filesaver.DONE;
|
||||
dispatch_all();
|
||||
revoke(object_url);
|
||||
}
|
||||
;
|
||||
filesaver.readyState = filesaver.INIT;
|
||||
|
||||
if (can_use_save_link) {
|
||||
object_url = get_URL().createObjectURL(blob);
|
||||
setTimeout(function() {
|
||||
save_link.href = object_url;
|
||||
save_link.download = name;
|
||||
click(save_link);
|
||||
dispatch_all();
|
||||
revoke(object_url);
|
||||
filesaver.readyState = filesaver.DONE;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
fs_error();
|
||||
}
|
||||
, FS_proto = FileSaver.prototype
|
||||
, saveAs = function(blob, name, no_auto_bom) {
|
||||
return new FileSaver(blob, name || blob.name || "download", no_auto_bom);
|
||||
}
|
||||
;
|
||||
// IE 10+ (native saveAs)
|
||||
if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) {
|
||||
return function(blob, name, no_auto_bom) {
|
||||
name = name || blob.name || "download";
|
||||
|
||||
if (!no_auto_bom) {
|
||||
blob = auto_bom(blob);
|
||||
}
|
||||
return navigator.msSaveOrOpenBlob(blob, name);
|
||||
};
|
||||
}
|
||||
|
||||
FS_proto.abort = function(){};
|
||||
FS_proto.readyState = FS_proto.INIT = 0;
|
||||
FS_proto.WRITING = 1;
|
||||
FS_proto.DONE = 2;
|
||||
|
||||
FS_proto.error =
|
||||
FS_proto.onwritestart =
|
||||
FS_proto.onprogress =
|
||||
FS_proto.onwrite =
|
||||
FS_proto.onabort =
|
||||
FS_proto.onerror =
|
||||
FS_proto.onwriteend =
|
||||
null;
|
||||
|
||||
return saveAs;
|
||||
}(
|
||||
typeof self !== "undefined" && self
|
||||
|| typeof window !== "undefined" && window
|
||||
|| this.content
|
||||
));
|
||||
// `self` is undefined in Firefox for Android content script context
|
||||
// while `this` is nsIContentFrameMessageManager
|
||||
// with an attribute `content` that corresponds to the window
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports.saveAs = saveAs;
|
||||
} else if ((typeof define !== "undefined" && define !== null) && (define.amd !== null)) {
|
||||
define("FileSaver.js", function() {
|
||||
return saveAs;
|
||||
});
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
String.prototype.tokens=function(h){h=h?h:!1;var a,g,b=0,k=this.length,f,c,e=[],l="and;or;not;in;not in;is;is not;".split(";"),d=function(a,c,d){if(!d)return{type:a,value:c,position:[g,b]}};if(this){for(a=this.charAt(b);a;)if(g=b," ">=a)b+=1,a=this.charAt(b);else if("a"<=a&&"z">=a||"A"<=a&&"Z">=a||"_"==a){c=a;for(b+=1;;)if(a=this.charAt(b),"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"0"<=a&&"9">=a||"_"===a)c+=a,b+=1;else break;0<=l.indexOf(c)?(f=e[e.length-1],"not"===c&&f&&"is"===f.value?(c=d("op","is not"),
|
||||
c.position[0]=f.position[0],e[e.length-1]=c):"in"===c&&f&&"not"===e[e.length-1].value?(c=d("op","not in"),c.position[0]=f.position[0],e[e.length-1]=c):e.push(d("op",c))):e.push(d("name",c))}else if("0"<=a&&"9">=a){c=a;for(b+=1;;){a=this.charAt(b);if("0">a||"9"<a)break;b+=1;c+=a}if("."===a)for(b+=1,c+=a;;){a=this.charAt(b);if("0">a||"9"<a)break;b+=1;c+=a}if("e"===a||"E"===a){b+=1;c+=a;a=this.charAt(b);if("-"===a||"+"===a)b+=1,c+=a,a=this.charAt(b);("0">a||"9"<a)&&d("number",c,"Bad exponent");do b+=
|
||||
1,c+=a,a=this.charAt(b);while("0"<=a&&"9">=a)}"a"<=a&&"z">=a&&(c+=a,b+=1,d("number",c,"Bad number"));f=+c;isFinite(f)?e.push(d("number",f)):d("number",c,"Bad number")}else if("'"===a||'"'===a){c="";f=a;for(b+=1;;){a=this.charAt(b);" ">a&&d("str",c,"\n"===a||"\r"===a||""===a?"Unterminated string.":"Control character in string.",d("",c));if(a===f)break;if("\\"===a)switch(b+=1,b>=k&&d("str",c,"Unterminated string"),a=this.charAt(b),a){case "b":a="\b";break;case "f":a="\f";break;case "n":a="\n";break;
|
||||
case "r":a="\r";break;case "t":a="\t";break;case "u":b>=k&&d("str",c,"Unterminated string"),a=parseInt(this.substr(b+1,4),16),(!isFinite(a)||0>a)&&d("str",c,"Unterminated string"),a=String.fromCharCode(a),b+=4}c+=a;b+=1}b+=1;e.push(d("str",c));a=this.charAt(b)}else 0<="<>.".indexOf(a)?(c=a,b+=1,a=this.charAt(b),"<"===c&&"="===a?c="<=":">"===c&&"="===a?c=">=":"."===c&&"."===a&&(c=".."),1<c.length&&(b+=1,a=this.charAt(b)),e.push(d("op",c))):(b+=1,"$"===a?e.push(d("(root)",a)):"@"===a?e.push(d("(current)",
|
||||
a)):"!"===a?e.push(d("(context)",a)):0<="+-*/%<>:[,]{}:()".indexOf(a)?e.push(d("op",a)):d("op",a,a+" is not an ObjectPath operator!"),a=this.charAt(b));h&&clog("}tokens with",e);return e}};
|
||||
var makeTree=function(){var h={},e,n,l,u=["true","t"],v=["false","f"],w=["none","null","n","nil"],m=function(){return this},f=function(a){var b,c,d;a&&e.id!==a&&e.error("Expected '"+a+"', got '"+e.id+"'.");if(l>=n.length)e=h["(end)"];else return c=n[l],l+=1,d=c.value,a=c.type,"name"===a?(0<=v.indexOf(d.toLowerCase())&&(d="false"),0<=u.indexOf(d.toLowerCase())&&(d="true"),0<=w.indexOf(d.toLowerCase())&&(d="null"),(b=h[d])||(b=h["(name)"])):"(root)"===a?b=h["(root)"]:"(current)"===a?b=h["(current)"]:
|
||||
"(context)"===a?b=h["(context)"]:"op"===a?(b=h[d])||c.error("Unknown operator."):"str"===a||"number"===a?(b=h["(literal)"],a="literal"):c.error("Unexpected token."),e=Object.create(b),e.position=c.position,e.value=d,e.arity=a,e.error=function(a){},e},k=function(a){var b,c=e;a="undefined"===typeof a?0:a;f();for(b=c.nud();a<e.lbp;)c=e,f(),b=c.led(b);return b},x={nud:function(){this.error("Undefined nud().")},led:function(a){this.error("Missing operator.")}},d=function(a,b){var c=h[a];b=b||0;c?b>=c.lbp&&
|
||||
(c.lbp=b):(c=Object.create(x),c.id=c.value=a,c.lbp=b,h[a]=c);return c},p=function(a,b){var c=d(a);c.nud=function(){this.value=h[this.id].value;this.arity="literal";this.id="(literal)";return this};c.value=b;return c},g=function(a,b,c){a=d(a,b);a.led=c||function(a){this.first=a;this.second=k(b);this.arity="binary";return this};return a},r=function(a,b,c){a=d(a,b);a.led=c||function(a){this.first=a;this.second=k(b-1);this.arity="binary";return this};return a},q=function(a,b,c){a=d(a);a.nud=c||function(){this.first=
|
||||
k(b);this.arity="unary";return this};return a},t=function(a){this.first=a;0>["(name)","*"].indexOf(e.id)&&SyntaxError("Expected an attribute name.");"*"===e.id&&(e.arity="wildcard");this.second=e;f();return this};d("(end)");d("(name)").nud=m;d("(literal)").nud=m;d("(root)").nud=m;d("(current)").nud=m;d("(context)").nud=m;d(":");d(",");p("true",!0);p("false",!1);p("null",null);r("or",30);r("and",40);q("not",50);g(":",120);g("in",60);g("not in",60);g("is",60);g("is not",60);g("<",60);g("<=",60);g(">",
|
||||
60);g(">=",60);g("+",110);g("-",110);g("*",120);g("/",120);g("%",120);q("-",130);q("+",130);d(".",150).led=t;d("..",150).led=t;d("]");g("[",150,function(a){this.first=a;this.second=k(0);this.arity="binary";f("]");return this});d("[").led=function(a){this.first=a;this.second=k();f("]");return this};d("[").nud=function(){var a=[];if("]"!==e.id)for(;;){a.push(k());if(","!==e.id)break;f(",")}f("]");this.first=a;this.arity="unary";return this};d(")");d("(",150).led=function(a){var b=[];this.arity="binary";
|
||||
this.id="fn";this.first=a.value;if(")"!==e.id)for(;;){b.push(k());if(","!==e.id)break;f(",")}f(")");this.second=b;return this};d("(",150).nud=function(){var a=k();f(")");return a};d("}");d("{").nud=function(){var a=[],b,c;if("}"!==e.id)for(;;){b=k();f(":");c=k();c.key=b;a.push(c);if(","!==e.id)break;f(",")}f("}");this.first=a;this.arity="unary";return this};return function(a,b){var c=this.D=b&&b.debug||!1;n="string"===typeof a?a.tokens(c):a;l=0;f();c=k();f("(end)");return c}},parse=makeTree();
|
||||
module.exports = ObjectPath=function(a,h){this.exprCache=[];this._init_(a,h);return this};
|
||||
ObjectPath.prototype={D:!1,current:null,data:null,SELECTOR_OPS:"is;>;<;is not;>=;<=;in;not in;:;and;or".split(";"),simpleTypes:["string","number"],_init_:function(a,h){this.setData(a);h&&this.setDebug(h.debug||this.D)},setCurrent:function(a){this.current=a},resetCurrent:function(){this.current=null},setContext:function(a){this.context=a},compile:function(a){return this.exprCache.hasOwnProperty(a)?this.exprCache[a]:this.exprCache[a]=parse.call(this,a,{debug:this.D})},setData:function(a){return 0>["object",
|
||||
"array"].indexOf(typeof a)?(this.log(a+" is not object nor array! Data not changed."),a):this.data=a},setDebug:function(a){this.D=a},flatten:function(a){var h=[],l=function(a){if(Array.isArray(a))for(var g=0;g<a.length;g++)l(a[g]);else if("object"===typeof a)for(g in h.push(a),a)l(a[g])};l(a);return h},execute:function(a){var h=this,l=this.flatten,m=this.simpleTypes,g,f=function(b){if(Array.isArray(b)){for(var c=[],e=0;e<b.length;e++)c.push(f(b[e]));return c}var a=b.id;if(0<="+;-;*;%;/;>;>=;<;<=;in;not in;is;is not".split(";").indexOf(b.id))var c=
|
||||
"object"===typeof b.first&&b.first.id?f(b.first):null,d="object"===typeof b.second&&b.second.id?f(b.second):null,g=typeof c,k=typeof d;switch(a){case "(literal)":return b.value;case "+":if("number"===g&&"number"!==k)d=parseInt(d);else if(Array.isArray(c)){if(Array.isArray(d))return c.concat(d);c.push(d)}return c+d;case "-":return d?c-d:-c;case "*":return"wildcard"!=b.arity?c*d:null;case "%":return c%d;case "/":return c/d;case ">":return c>d;case "<":return c<d;case ">=":return c>=d;case "<=":return c<=
|
||||
d;case "not":return!f(b.first);case "or":return f(b.first)||f(b.second);case "and":return f(b.first)&&f(b.second);case "in":case "not in":return e=null,"string"===k?e=0<=d.search(c.toString()):Array.isArray(d)?e=0<=d.indexOf(c):"object"===k&&(e=d.hasOwnProperty(c)),"in"===a?e:!e;case "is":case "is not":e=null;if(0<=m.indexOf(g))e=c==d;else if("array"===g||"object"===g)try{throw e=JSON.stringify(c)==JSON.stringify(d),{name:"comparison Error",message:"JSONStringifyNotAvailable. Your web browser doesn't support JSON.stringify(). Vote for support at"};
|
||||
}catch(n){}return"is"===a?e:!e;case "(root)":return h.data;case "(current)":return h.current;case "(context)":return h.context;case ":":return b;case "(name)":return b.value;case "[":if("unary"===b.arity){a=[];for(e in b.first)a.push(f(b.first[e]));return a}if("op"===b.arity){c=f(b.first);if(!c)return c;if("string"===typeof c||Array.isArray(c)){d=f(b.second);k=typeof d;if(d&&":"===d.id)return c.slice(f(d.first),f(d.second));if("number"===k)return-1===d?c.slice(-1)[0]:0>d?c.slice(d,d+1)[0]:c[d];if("string"===
|
||||
k){a=[];for(e=0;e<c.length;e++)c[e][d]&&a.push(c[e][d]);return a}if("object"===typeof b.second&&0<=h.SELECTOR_OPS.indexOf(b.second.id)){b=b.second;a=[];b=Object.create(b);for(e in c)d=c[e],h.current=d,b.first=d,f(b)&&a.push(d);return a}programmingError("left is array and right is not number")}else if("object"===g&&("(name)"===b.second.id||"string"===k))return c[d];return 1}return null;case "(":switch(b.first.value){case "str":return e=f(b.second),"object"===typeof e?JSON.stringify(e):e.toString}return null;
|
||||
case "{":case "":throw{error:"NotImplementedYet",message:a+" is not implemented yet!",data:b};case "..":c=l(f(b.first));case ".":c=c||f(b.first);if("*"===b.second.id)return c;if(c){if(Array.isArray(c)){a=[];d=f(b.second);for(e=0;e<c.length;e++)c[e][d]&&a.push(c[e][d]);return a}return c[b.second.value]}return null;case "fn":switch(b.first){case "float":case "int":return parseFloat(f(b.second));case "str":return f(b.second).toString();case "array":return e=f(b.second),Array.isArray(e[0])?e[0]:"string"===
|
||||
typeof e[0]?e[0].split(""):[];case "replace":return a=f(b.second),a[0]?a[0].replace(new RegExp(a[1],"g"),a[2]):"";case "join":a=f(b.second);try{return a[0].join(a[1])}catch(p){return null}case "split":return a=f(b.second),a[0]?a[0].split(a[1]):"";case "max":return Math.max.apply(null,f(b.second)[0]);case "min":return Math.min.apply(null,f(b.second)[0])}throw{error:"WrongFunction",message:"Function "+a+" is not proper ObjectPath function.",data:b};}throw{error:"WrongOperator",message:"Operator "+a+
|
||||
" is not proper ObjectPath operator.",data:b};};if(!a)return a;"string"===typeof a&&(g=this.compile(a));return f(g)}};
|
847
src/badges.js
847
src/badges.js
|
@ -1,847 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ,
|
||||
constants = require('./constants'),
|
||||
utils = require('./utils'),
|
||||
|
||||
WEBKIT = constants.IS_WEBKIT ? '-webkit-' : '',
|
||||
|
||||
SPECIAL_BADGES = ['staff', 'admin', 'global_mod'],
|
||||
OTHER_KNOWN = ['turbo', 'bits', 'premium', 'partner'],
|
||||
|
||||
CSS_BADGES = {
|
||||
staff: { 1: { color: "#200f33", use_svg: true } },
|
||||
admin: { 1: { color: "#faaf19", use_svg: true } },
|
||||
global_mod: { 1: { color: "#0c6f20", use_svg: true } },
|
||||
broadcaster: { 1: { color: "#e71818", use_svg: true } },
|
||||
moderator: { 1: { color: "#34ae0a", use_svg: true } },
|
||||
twitchbot: { 1: { color: "#34ae0a" } },
|
||||
partner: { 1: { color: "transparent", has_trans: true, trans_color: "#6441a5" } },
|
||||
|
||||
turbo: { 1: { color: "#6441a5", use_svg: true } },
|
||||
premium: { 1: { color: "#009cdc" } },
|
||||
|
||||
subscriber: { 0: { color: "#6441a4" }, 1: { color: "#6441a4" }},
|
||||
|
||||
bits: {
|
||||
1: { color: "#cbc8d0" },
|
||||
100: { color: "#ca7eff" },
|
||||
1000: { color: "#3ed8b3" },
|
||||
5000: { color: "#49acff" },
|
||||
10000: { color: "#ff271e" },
|
||||
25000: { color: "#f560ab" },
|
||||
50000: { color: "#ff881f" },
|
||||
75000: { color: "#16d03d" },
|
||||
100000: { color: "#ffcb13" },
|
||||
200000: { color: "#cbc8d0" },
|
||||
300000: { color: "#c97ffd" },
|
||||
400000: { color: "#3dd8b3" },
|
||||
500000: { color: "#48acfe" },
|
||||
600000: { color: "#ff281f" },
|
||||
700000: { color: "#f560ab" },
|
||||
800000: { color: "#ff881f" },
|
||||
900000: { color: "#16d03d" },
|
||||
1000000: { color: "#fecb11" }
|
||||
}
|
||||
},
|
||||
|
||||
NO_INVERT_BADGES = ['ssubscriber', 'ffz-badge-1'],
|
||||
|
||||
INVERT_INVERT_BADGES = ['bits'],
|
||||
TRANSPARENT_BADGES = [], //'subscriber'],
|
||||
|
||||
BTTV_TYPE_REPLACEMENTS = {
|
||||
'global-moderator': 'global_mod'
|
||||
},
|
||||
|
||||
BADGE_POSITIONS = {
|
||||
'broadcaster': 0,
|
||||
'staff': 0,
|
||||
'admin': 0,
|
||||
'global_mod': 0,
|
||||
'mod': 1,
|
||||
'moderator': 1,
|
||||
'twitchbot': 1,
|
||||
'subscriber': 10,
|
||||
},
|
||||
|
||||
BADGE_NAMES = {
|
||||
'global_mod': 'Global Moderator'
|
||||
},
|
||||
|
||||
BADGE_KLASSES = {
|
||||
'global_mod': 'global-moderator'
|
||||
};
|
||||
|
||||
|
||||
// --------------------
|
||||
// Settings
|
||||
// --------------------
|
||||
|
||||
FFZ.settings_info.show_badges = {
|
||||
type: "boolean",
|
||||
value: true,
|
||||
|
||||
category: "Chat Appearance",
|
||||
name: "Additional Badges",
|
||||
help: "Show additional badges for bots, FrankerFaceZ donors, and other special users."
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.loyalty_badges = {
|
||||
type: "boolean",
|
||||
value: true,
|
||||
|
||||
category: "Chat Appearance",
|
||||
name: "Display Subscriber Loyalty Badges",
|
||||
help: "Show different badge images for users that have been subscribed 3, 6, 12, and 24 months in supported channels.",
|
||||
|
||||
on_update: function(val) {
|
||||
utils.toggle_cls('ffz-no-loyalty')(!val);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.hidden_badges = {
|
||||
type: "button",
|
||||
value: [],
|
||||
|
||||
category: "Chat Appearance",
|
||||
name: "Hidden Badges",
|
||||
help: "Any badges added to this list will not be displayed in chat.",
|
||||
|
||||
on_update: function(val) {
|
||||
if ( this.has_bttv_6 )
|
||||
return;
|
||||
|
||||
var controller = utils.ember_lookup('controller:chat'),
|
||||
messages = controller && controller.get('currentRoom.messages');
|
||||
|
||||
if ( ! messages )
|
||||
return;
|
||||
|
||||
for(var i=0; i < messages.length; i++)
|
||||
messages[i]._line && messages[i]._line.ffzUpdateBadges();
|
||||
},
|
||||
|
||||
method: function() {
|
||||
var f = this,
|
||||
service = utils.ember_lookup('service:badges'),
|
||||
badgeCollection = service && service.badgeCollection,
|
||||
old_val = f.settings.hidden_badges.join(", "),
|
||||
values = [];
|
||||
|
||||
if ( badgeCollection ) {
|
||||
if ( badgeCollection.global )
|
||||
for(var badge in badgeCollection.global)
|
||||
if ( badgeCollection.global.hasOwnProperty(badge) && badge !== 'broadcasterName' ) {
|
||||
var badge_data = badgeCollection.global[badge] || {},
|
||||
version = badge_data.versions && Object.keys(badge_data.versions)[0];
|
||||
|
||||
if ( version )
|
||||
values.push([badge, f.render_badges(f.get_twitch_badges(badge + "/" + version))]);
|
||||
}
|
||||
|
||||
if ( badgeCollection.channel )
|
||||
for(var badge in badgeCollection.channel)
|
||||
if ( badgeCollection.channel.hasOwnProperty(badge) && badge !== 'broadcasterName' ) {
|
||||
var badge_data = badgeCollection.channel[badge] || {},
|
||||
version = badge_data.versions && Object.keys(badge_data.versions)[0];
|
||||
|
||||
if ( version )
|
||||
values.push([badge, f.render_badges(f.get_twitch_badges(badge + "/" + version))]);
|
||||
}
|
||||
}
|
||||
|
||||
for(var badge_id in f.badges) {
|
||||
if ( ! f.badges.hasOwnProperty(badge_id) )
|
||||
continue;
|
||||
|
||||
var badge = f.badges[badge_id],
|
||||
hide_key = (badge.source_ext ? f._apis[badge.source_ext].name_key : 'ffz') + '-' + (badge.name || badge.id),
|
||||
render_badge = {};
|
||||
|
||||
render_badge[badge.slot] = f._get_badge_object({}, badge);
|
||||
values.push([hide_key, f.render_badges(render_badge)]);
|
||||
}
|
||||
|
||||
if ( this.has_bttv_6 && window.BetterTTV ) {
|
||||
try {
|
||||
for(var badge_id in BetterTTV.chat.store.__badgeTypes)
|
||||
values.push(['bttv-' + badge_id, null]);
|
||||
|
||||
values.push(['bot', null]);
|
||||
|
||||
} catch(err) {
|
||||
this.error("Unable to load known BetterTTV badges.", err);
|
||||
}
|
||||
}
|
||||
|
||||
var already_used = [],
|
||||
output = [];
|
||||
|
||||
for(var i=0; i < values.length; i++) {
|
||||
var badge = values[i];
|
||||
if ( already_used.indexOf(badge[0]) !== -1 )
|
||||
continue;
|
||||
|
||||
already_used.push(badge[0]);
|
||||
output.push((badge[1] ? '<div class="ffz-hidden-badges badges">' + badge[1] + '</div>' : '') + '<code>' + badge[0] + '</code>');
|
||||
}
|
||||
|
||||
utils.prompt(
|
||||
"Hidden Badges",
|
||||
"Please enter a comma-separated list of badges that you would like to be hidden in chat. You can use the special value <code>game</code> to hide all the game-specific badges at once.</p><p><b>Possible Values:</b> " + output.join(", "),
|
||||
old_val,
|
||||
function(new_val) {
|
||||
if ( new_val === null || new_val === undefined )
|
||||
return;
|
||||
|
||||
f.settings.set("hidden_badges", _.unique(new_val.trim().toLowerCase().split(/\s*,\s*/)).without(""));
|
||||
}, 600
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.sub_notice_badges = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
|
||||
category: "Chat Appearance",
|
||||
name: "Old-Style Subscriber Notice Badges",
|
||||
no_bttv: true,
|
||||
|
||||
help: "Display a subscriber badge on old-style chat messages about new subscribers.",
|
||||
|
||||
on_update: function(val) {
|
||||
this.toggle_style('badges-sub-notice', ! this.has_bttv && ! val);
|
||||
this.toggle_style('badges-sub-notice-on', ! this.has_bttv && val);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.legacy_badges = {
|
||||
type: "select",
|
||||
options: {
|
||||
0: "Default",
|
||||
1: "Moderator Only",
|
||||
2: "Mod + Turbo",
|
||||
3: "All Legacy Badges"
|
||||
},
|
||||
value: 0,
|
||||
|
||||
category: "Chat Appearance",
|
||||
|
||||
name: "Legacy Badges",
|
||||
help: "Use the old, pre-vector chat badges from Twitch in place of the new.",
|
||||
|
||||
process_value: utils.process_int(0, 0, 3),
|
||||
|
||||
on_update: function(val) {
|
||||
this.toggle_style('badges-legacy', val === 3);
|
||||
this.toggle_style('badges-legacy-mod', val !== 0);
|
||||
this.toggle_style('badges-legacy-turbo', val > 1);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.transparent_badges = {
|
||||
type: "select",
|
||||
options: {
|
||||
0: "Default",
|
||||
1: "Rounded",
|
||||
2: "Circular",
|
||||
3: "Circular (Color Only)",
|
||||
4: "Circular (Color Only, Small)",
|
||||
5: "Transparent",
|
||||
6: "Transparent (Colored)"
|
||||
},
|
||||
|
||||
value: 0,
|
||||
|
||||
category: "Chat Appearance",
|
||||
no_bttv: 6,
|
||||
|
||||
name: "Badge Style",
|
||||
help: "Make badges appear rounded, completely circular, or transparent with no background at all.",
|
||||
|
||||
process_value: utils.process_int(0, 0, 5),
|
||||
|
||||
on_update: function(val) {
|
||||
if ( this.has_bttv_6 )
|
||||
return;
|
||||
|
||||
this.toggle_style('badges-rounded', val === 1);
|
||||
this.toggle_style('badges-circular', val === 2 || val === 3 || val === 4);
|
||||
this.toggle_style('badges-blank', val === 3 || val === 4);
|
||||
this.toggle_style('badges-circular-small', val === 4);
|
||||
this.toggle_style('badges-transparent', val >= 5);
|
||||
utils.toggle_cls('ffz-transparent-badges')(val >= 5);
|
||||
utils.toggle_cls('ffz-blank-badges')(val === 3 || val === 4);
|
||||
|
||||
// Update existing chat lines.
|
||||
var CL = utils.ember_resolve('component:chat/chat-line'),
|
||||
CW = utils.ember_resolve('component:twitch-conversations/conversation-window'),
|
||||
DP = utils.ember_resolve('component:chat/from-display-preview'),
|
||||
views = (CL || CW || DP) ? utils.ember_views() : [];
|
||||
|
||||
for(var vid in views) {
|
||||
var view = views[vid];
|
||||
if ( CL && view instanceof CL && view.buildBadgesHTML )
|
||||
view.$('.badges').replaceWith(view.buildBadgesHTML());
|
||||
else if ( DP && view instanceof DP && view.ffzRenderBadges )
|
||||
view.ffzRenderBadges();
|
||||
else if ( CW && view instanceof CW && view.ffzReplaceBadges )
|
||||
view.ffzReplaceBadges();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// --------------------
|
||||
// Initialization
|
||||
// --------------------
|
||||
|
||||
FFZ.prototype.setup_badges = function() {
|
||||
this.log("Preparing badge system.");
|
||||
if ( ! this.has_bttv_6 ) {
|
||||
var val = this.settings.transparent_badges;
|
||||
this.toggle_style('badges-rounded', val === 1);
|
||||
this.toggle_style('badges-circular', val === 2 || val === 3 || val === 4);
|
||||
this.toggle_style('badges-blank', val === 3 || val === 4);
|
||||
this.toggle_style('badges-circular-small', val === 4);
|
||||
this.toggle_style('badges-transparent', val >= 5);
|
||||
|
||||
utils.toggle_cls('ffz-transparent-badges')(val >= 5);
|
||||
utils.toggle_cls('ffz-blank-badges')(val === 3 || val === 4);
|
||||
utils.toggle_cls('ffz-no-loyalty')(!this.settings.loyalty_badges);
|
||||
}
|
||||
|
||||
if ( ! this.has_bttv ) {
|
||||
this.toggle_style('badges-sub-notice', ! this.settings.sub_notice_badges);
|
||||
this.toggle_style('badges-sub-notice-on', this.settings.sub_notice_badges);
|
||||
}
|
||||
|
||||
this.toggle_style('badges-legacy', this.settings.legacy_badges === 3);
|
||||
this.toggle_style('badges-legacy-mod', this.settings.legacy_badges !== 0);
|
||||
this.toggle_style('badges-legacy-turbo', this.settings.legacy_badges > 1);
|
||||
|
||||
this.log("Creating badge style element.");
|
||||
var s = this._badge_style = document.createElement('style');
|
||||
s.id = "ffz-badge-css";
|
||||
document.head.appendChild(s);
|
||||
|
||||
this.log("Generating CSS for existing API badges.");
|
||||
for(var badge_id in this.badges)
|
||||
if ( this.badges.hasOwnProperty(badge_id) )
|
||||
utils.update_css(s, badge_id, utils.badge_css(this.badges[badge_id]));
|
||||
|
||||
this.log("Generating CSS for existing Twitch badges.");
|
||||
for(var badge_id in CSS_BADGES) {
|
||||
var badge_data = CSS_BADGES[badge_id],
|
||||
klass = BADGE_KLASSES[badge_id] || badge_id;
|
||||
for(var version in badge_data)
|
||||
utils.update_css(s, 'twitch-' + badge_id + '-' + version, utils.cdn_badge_css(klass, version, badge_data[version]));
|
||||
}
|
||||
|
||||
this.log("Loading badges.");
|
||||
this.load_badges();
|
||||
}
|
||||
|
||||
|
||||
// --------------------
|
||||
// Reloading Badges
|
||||
// --------------------
|
||||
|
||||
FFZ.ws_commands.reload_badges = function() {
|
||||
this.load_badges();
|
||||
}
|
||||
|
||||
|
||||
FFZ.ws_commands.set_badge = function(data) {
|
||||
var user_id = data[0],
|
||||
slot = data[1],
|
||||
badge = data[2],
|
||||
|
||||
user = this.users[user_id] = this.users[user_id] || {},
|
||||
badges = user.badges = user.badges || {};
|
||||
|
||||
if ( typeof badge === "number" )
|
||||
badge = {id: badge};
|
||||
|
||||
if ( badge === undefined || badge === null )
|
||||
badges[slot] = null;
|
||||
else
|
||||
badges[slot] = badge;
|
||||
}
|
||||
|
||||
|
||||
// --------------------
|
||||
// Badge Selection
|
||||
// --------------------
|
||||
|
||||
FFZ.prototype.get_badges = function(user, room_id, badges, msg) {
|
||||
var data = this.users[user],
|
||||
room = this.rooms[room_id],
|
||||
room_data = room && room.users && room.users[user],
|
||||
hidden_badges = this.settings.hidden_badges,
|
||||
badge_data = data && data.badges || {};
|
||||
|
||||
if ( room_data && room_data.badges )
|
||||
badge_data = _.extend({}, badge_data, room_data.badges);
|
||||
|
||||
if ( ! badge_data || ! this.settings.show_badges )
|
||||
return badges;
|
||||
|
||||
for(var slot in badge_data) {
|
||||
var badge = badge_data[slot];
|
||||
if ( ! badge_data.hasOwnProperty(slot) || ! badge )
|
||||
continue;
|
||||
|
||||
var badge_id = badge.real_id || badge.id,
|
||||
full_badge = this.badges[badge_id] || {},
|
||||
full_badge_id = full_badge.real_id || full_badge.id,
|
||||
old_badge = badges[slot],
|
||||
|
||||
hide_key = (full_badge.source_ext ? this._apis[full_badge.source_ext].name_key : 'ffz') + '-' + (full_badge.name || full_badge.id);
|
||||
|
||||
if ( hidden_badges.indexOf(hide_key) !== -1 )
|
||||
continue;
|
||||
|
||||
if ( full_badge.visible !== undefined ) {
|
||||
var visible = full_badge.visible;
|
||||
if ( typeof visible === "function" )
|
||||
visible = visible.call(this, room_id, user, msg, badges);
|
||||
|
||||
if ( ! visible )
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( old_badge ) {
|
||||
var replaces = badge.hasOwnProperty('replaces') ? badge.replaces : full_badge.replaces,
|
||||
replace_mode = badge.replace_mode || full_badge.replace_mode || 'merge';
|
||||
if ( ! replaces )
|
||||
continue;
|
||||
|
||||
if ( replace_mode === 'merge' ) {
|
||||
old_badge.image = badge.image || null;
|
||||
old_badge.klass += ' ffz-badge-replacement ffz-replacer-ffz-badge-' + (badge_id || full_badge_id);
|
||||
old_badge.title += ', ' + (badge.title || full_badge.title);
|
||||
continue;
|
||||
|
||||
} else if ( replace_mode === 'keep_title' ) {
|
||||
var b = badges[slot] = this._get_badge_object(badge, full_badge);
|
||||
b.title = old_badge.title + ', ' + b.title;
|
||||
continue;
|
||||
|
||||
} else if ( replace_mode === 'title_only' ) {
|
||||
old_badge.title += ', ' + (badge.title || full_badge.title);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
badges[slot] = this._get_badge_object(badge, full_badge);
|
||||
}
|
||||
|
||||
return badges;
|
||||
}
|
||||
|
||||
FFZ.prototype._get_badge_object = function(badge, full_badge) {
|
||||
var id = badge.real_id || badge.id || full_badge.real_id || full_badge.id;
|
||||
return {
|
||||
id: id,
|
||||
klass: 'ffz-badge-' + id,
|
||||
title: badge.title || full_badge.title || ('Unknown FFZ Badge\nID: ' + id),
|
||||
image: badge.image,
|
||||
full_image: full_badge.image,
|
||||
color: badge.color,
|
||||
no_tooltip: badge.no_tooltip || full_badge.no_tooltip,
|
||||
click_action: badge.click_action || full_badge.click_action,
|
||||
click_url: badge.click_url || full_badge.click_url,
|
||||
no_invert: badge.no_invert || full_badge.no_invert,
|
||||
no_color: badge.no_color || full_badge.no_color,
|
||||
invert_invert: badge.invert_invert || full_badge.invert_invert,
|
||||
transparent: badge.transparent || full_badge.transparent || (badge.color || full_badge.color) === "transparent",
|
||||
extra_css: (badge.extra_css || full_badge.extra_css)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.get_line_badges = function(msg) {
|
||||
var room = msg.get && msg.get('room') || msg.room,
|
||||
from = msg.get && msg.get('from') || msg.from,
|
||||
tags = msg.get && msg.get('tags') || msg.tags || {},
|
||||
badge_tag = tags.badges || {};
|
||||
|
||||
// Twitch Badges
|
||||
var badges = this.get_twitch_badges(badge_tag, room);
|
||||
|
||||
// FFZ Badges
|
||||
return this.get_badges(from, room, badges, msg);
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.get_twitch_badges = function(badge_tag, room_id) {
|
||||
var badges = {},
|
||||
hidden_badges = this.settings.hidden_badges,
|
||||
|
||||
last_id = -1,
|
||||
had_last = false,
|
||||
|
||||
service = utils.ember_lookup('service:badges'),
|
||||
badgeCollection = service && service.badgeCollection,
|
||||
|
||||
globals = badgeCollection && badgeCollection.global || {},
|
||||
channel = badgeCollection && badgeCollection.channel || {};
|
||||
|
||||
// Is this the right channel?
|
||||
if ( room_id && room_id !== channel.broadcasterName ) {
|
||||
var ffz_room = this.rooms && this.rooms[room_id];
|
||||
channel = ffz_room && ffz_room.badges || {};
|
||||
}
|
||||
|
||||
// Whisper Chat Lines have a non-associative array for some reason.
|
||||
if ( Array.isArray(badge_tag) ) {
|
||||
var val = badge_tag;
|
||||
badge_tag = {};
|
||||
for(var i=0; i < val.length; i++)
|
||||
badge_tag[val[i].id] = val[i].version;
|
||||
}
|
||||
|
||||
// VoD Chat lines don't have the badges pre-parsed for some reason.
|
||||
else if ( typeof badge_tag === 'string' )
|
||||
badge_tag = utils.parse_badge_tag(badge_tag);
|
||||
|
||||
|
||||
for(var badge in badge_tag) {
|
||||
var version = badge_tag[badge];
|
||||
if ( ! badge_tag.hasOwnProperty(badge) || version === undefined || version === null )
|
||||
continue;
|
||||
|
||||
var versions = channel[badge] || globals[badge],
|
||||
binfo = versions && versions.versions && versions.versions[version],
|
||||
is_game = badge.substr(-2) === '_1';
|
||||
|
||||
if ( hidden_badges.indexOf(badge) !== -1 || (is_game && hidden_badges.indexOf('game') !== -1) )
|
||||
continue;
|
||||
|
||||
if ( BADGE_POSITIONS.hasOwnProperty(badge) )
|
||||
last_id = BADGE_POSITIONS[badge];
|
||||
else {
|
||||
last_id = had_last ? last_id + 1 : 15;
|
||||
had_last = true;
|
||||
}
|
||||
|
||||
var is_known = BADGE_POSITIONS.hasOwnProperty(badge) || OTHER_KNOWN.indexOf(badge) !== -1;
|
||||
|
||||
badges[last_id] = {
|
||||
klass: (BADGE_KLASSES[badge] || badge) + (is_known ? '' : ' unknown-badge') + ' version-' + version,
|
||||
title: binfo && binfo.title || BADGE_NAMES[badge] || badge.capitalize(),
|
||||
click_url: binfo && binfo.click_action === 'visit_url' && binfo.click_url,
|
||||
no_invert: ! (versions && versions.allow_invert) && NO_INVERT_BADGES.indexOf(badge) !== -1,
|
||||
no_color: ! CSS_BADGES.hasOwnProperty(badge),
|
||||
invert_invert: (versions && versions.invert_invert) || INVERT_INVERT_BADGES.indexOf(badge) !== -1,
|
||||
transparent: TRANSPARENT_BADGES.indexOf(badge) !== -1
|
||||
};
|
||||
|
||||
if ( ! is_known && binfo ) {
|
||||
badges[last_id].image = binfo.image_url_1x;
|
||||
if ( binfo.image_url_2x || binfo.image_url_4x )
|
||||
badges[last_id].srcSet = 'url("' + binfo.image_url_1x + '") 1x' + (binfo.image_url_2x ? ', url("' + binfo.image_url_2x + '") 2x' : '') + (binfo.image_url_4x ? ', url("' + binfo.image_url_4x + '") 4x' : '');
|
||||
}
|
||||
}
|
||||
|
||||
return badges;
|
||||
}
|
||||
|
||||
|
||||
// --------------------
|
||||
// Render Badge
|
||||
// --------------------
|
||||
|
||||
FFZ.prototype.render_badges = function(badges) {
|
||||
var out = [],
|
||||
setting = this.settings.transparent_badges;
|
||||
|
||||
for(var key in badges) {
|
||||
var badge = badges[key],
|
||||
klass = badge.klass,
|
||||
css = '',
|
||||
is_colored = !(badge.no_color !== undefined ? badge.no_color : badge.transparent);
|
||||
|
||||
if ( badge.image )
|
||||
if ( is_colored && setting === 6 )
|
||||
css += WEBKIT + 'mask-image:url("' + utils.quote_attr(badge.image) + '");';
|
||||
else
|
||||
css += 'background-image:url("' + utils.quote_attr(badge.image) + '");';
|
||||
|
||||
if ( badge.srcSet )
|
||||
if ( is_colored && setting === 6 )
|
||||
css += WEBKIT + 'mask-image:' + WEBKIT + 'image-set(' + badge.srcSet + ');';
|
||||
else
|
||||
css += 'background-image:' + WEBKIT + 'image-set(' + badge.srcSet + ');';
|
||||
|
||||
if ( badge.color )
|
||||
if ( is_colored && setting === 6 )
|
||||
css += 'background: linear-gradient(' + badge.color + ',' + badge.color + ');';
|
||||
else
|
||||
css += 'background-color:' + badge.color + ';'
|
||||
|
||||
if ( badge.extra_css )
|
||||
css += badge.extra_css;
|
||||
|
||||
if ( badge.click_url )
|
||||
klass += ' click_url';
|
||||
|
||||
if ( badge.click_action )
|
||||
klass += ' click_action';
|
||||
|
||||
if ( badge.no_invert )
|
||||
klass += ' no-invert';
|
||||
|
||||
if ( badge.invert_invert )
|
||||
klass += ' invert-invert';
|
||||
|
||||
if ( is_colored && setting === 6 )
|
||||
klass += ' colored';
|
||||
|
||||
if ( badge.transparent )
|
||||
klass += ' transparent';
|
||||
|
||||
out.push('<div class="badge ' + (badge.no_tooltip ? '' : 'html-tooltip ') + utils.quote_attr(klass) + '"' + (badge.id ? ' data-badge-id="' + badge.id + '"' : '') + (badge.click_url ? ' data-url="' + utils.quote_attr(badge.click_url) + '"' : '') + (css ? ' style="' + utils.quote_attr(css) + '"' : '') + ' title="' + utils.quote_attr(badge.title) + '"></div>');
|
||||
}
|
||||
|
||||
return out.join("");
|
||||
}
|
||||
|
||||
|
||||
// --------------------
|
||||
// Extension Support
|
||||
// --------------------
|
||||
|
||||
FFZ.prototype.bttv_badges = function(data) {
|
||||
if ( ! this.settings.show_badges )
|
||||
return;
|
||||
|
||||
var user_id = data.sender,
|
||||
user = this.users[user_id],
|
||||
room = this.rooms[data.room],
|
||||
room_data = room && room.users && room.users[user_id],
|
||||
badges_out = [],
|
||||
insert_at = -1,
|
||||
|
||||
hidden_badges = this.settings.hidden_badges,
|
||||
alpha = BetterTTV.settings.get('alphaTags');
|
||||
|
||||
if ( ! data.badges )
|
||||
data.badges = [];
|
||||
|
||||
// Determine where in the list to insert these badges.
|
||||
// Also, strip out banned badges while we're at it.
|
||||
for(var i=0; i < data.badges.length; i++) {
|
||||
var badge = data.badges[i],
|
||||
space_ind = badge.type.indexOf(' '),
|
||||
hidden_key = space_ind !== -1 ? badge.type.substr(0, space_ind) : badge.type;
|
||||
|
||||
if ( hidden_key.indexOf('twitch-') === 0 )
|
||||
hidden_key = hidden_key.substr(7);
|
||||
|
||||
if ( BTTV_TYPE_REPLACEMENTS.hasOwnProperty(hidden_key) )
|
||||
hidden_key = BTTV_TYPE_REPLACEMENTS[hidden_key];
|
||||
else {
|
||||
var ind = hidden_key.indexOf('-');
|
||||
if ( ind !== -1 )
|
||||
hidden_key = hidden_key.substr(0, ind);
|
||||
}
|
||||
|
||||
var is_game = hidden_key.substr(-2) === '_1';
|
||||
if ( hidden_badges.indexOf(hidden_key) !== -1 || (is_game && hidden_badges.indexOf('game') !== -1) ) {
|
||||
data.badges.splice(i, 1);
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( insert_at === -1 && (badge.type === "subscriber" || badge.type === "turbo" || badge.type.substr(0, 7) === 'twitch-') )
|
||||
insert_at = i;
|
||||
}
|
||||
|
||||
var badge_data = user && user.badges || {};
|
||||
if ( room_data && room_data.badges )
|
||||
badge_data = _.extend({}, badge_data, room_data.badges);
|
||||
|
||||
// If there's no user, we're done now.
|
||||
if ( ! badge_data )
|
||||
return;
|
||||
|
||||
// We have a user. Start replacing badges.
|
||||
for (var slot in badge_data) {
|
||||
var badge = badge_data[slot];
|
||||
if ( ! badge_data.hasOwnProperty(slot) || ! badge )
|
||||
continue;
|
||||
|
||||
var badge_id = badge.real_id || badge.id,
|
||||
full_badge = this.badges[badge_id] || {},
|
||||
full_badge_id = full_badge.real_id || full_badge.id,
|
||||
desc = badge.title || full_badge.title,
|
||||
style = "",
|
||||
klass = 'ffz-badge-' + badge_id + (alpha ? ' alpha' : ''),
|
||||
|
||||
hide_key = (full_badge.source_ext ? this._apis[full_badge.source_ext].name_key : 'ffz') + '-' + (full_badge.name || full_badge.id);
|
||||
|
||||
if ( hidden_badges.indexOf(hide_key) !== -1 )
|
||||
continue;
|
||||
|
||||
if ( full_badge.visible !== undefined ) {
|
||||
var visible = full_badge.visible;
|
||||
if ( typeof visible === "function" )
|
||||
visible = visible.call(this, data.room, user_id);
|
||||
|
||||
if ( ! visible )
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( alpha && badge.transparent_image )
|
||||
style += 'background-image: url("' + badge.transparent_image + '");';
|
||||
else if ( badge.image )
|
||||
style += 'background-image: url("' + badge.image + '");';
|
||||
|
||||
if ( badge.color && ! alpha )
|
||||
style += 'background-color: ' + badge.color + '; ';
|
||||
|
||||
if ( badge.extra_css )
|
||||
style += badge.extra_css;
|
||||
|
||||
if ( style )
|
||||
desc += '" style="' + utils.quote_attr(style);
|
||||
|
||||
var replaces = badge.hasOwnProperty('replaces') ? badge.replaces : full_badge.replaces,
|
||||
replace_mode = badge.replace_mode || full_badge.replace_mode || 'merge';
|
||||
|
||||
if ( replaces ) {
|
||||
var replaced = false;
|
||||
for(var i=0; i < data.badges.length; i++) {
|
||||
var b = data.badges[i];
|
||||
if ( b.type === full_badge.replaces_type ) {
|
||||
if ( replace_mode === 'merge' ) {
|
||||
b.type += " ffz-badge-replacement ffz-replacer-ffz-badge-" + (badge_id || full_badge_id);
|
||||
b.description += ", " + (badge.title || full_badge.title) +
|
||||
(badge.image ? '" style="background-image: url(' + utils.quote_attr('"' + badge.image + '"') + ')' : '');
|
||||
|
||||
} else if ( replace_mode === 'keep_title' ) {
|
||||
data.badges[i] = {
|
||||
type: klass,
|
||||
name: '',
|
||||
description: b.description + ', ' + desc
|
||||
};
|
||||
|
||||
} else if ( replace_mode === 'title_only' ) {
|
||||
b.description += ', ' + (badge.title || full_badge.title);
|
||||
} else {
|
||||
data.badges[i] = {type: klass, name: '', description: desc};
|
||||
}
|
||||
|
||||
replaced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( replaced )
|
||||
continue;
|
||||
}
|
||||
|
||||
badges_out.push([(insert_at == -1 ? 1 : -1) * slot, {type: klass, name: "", description: desc}]);
|
||||
}
|
||||
|
||||
badges_out.sort(function(a,b){return a[0] - b[0]});
|
||||
|
||||
if ( insert_at == -1 ) {
|
||||
while(badges_out.length)
|
||||
data.badges.push(badges_out.shift()[1]);
|
||||
} else {
|
||||
while(badges_out.length)
|
||||
data.badges.insertAt(insert_at, badges_out.shift()[1]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --------------------
|
||||
// Badge Loading
|
||||
// --------------------
|
||||
|
||||
FFZ.bttv_known_bots = ["nightbot","moobot","sourbot","xanbot","manabot","mtgbot","ackbot","baconrobot","tardisbot","deejbot","valuebot","stahpbot"];
|
||||
|
||||
FFZ.prototype.load_badges = function(callback, tries) {
|
||||
var f = this;
|
||||
jQuery.getJSON(constants.API_SERVER + "v1/badges")
|
||||
.done(function(data) {
|
||||
var badge_total = 0,
|
||||
badge_count = 0,
|
||||
badge_data = {};
|
||||
|
||||
for(var i=0; i < data.badges.length; i++) {
|
||||
var badge = data.badges[i];
|
||||
if ( badge && badge.name ) {
|
||||
f._load_badge_json(badge.id, badge);
|
||||
badge_total++;
|
||||
}
|
||||
}
|
||||
|
||||
if ( data.users )
|
||||
for(var badge_id in data.users)
|
||||
if ( data.users.hasOwnProperty(badge_id) && f.badges[badge_id] ) {
|
||||
var badge = f.badges[badge_id],
|
||||
users = data.users[badge_id];
|
||||
|
||||
badge_data[badge.name] = users.length;
|
||||
|
||||
for(var i=0; i < users.length; i++) {
|
||||
var user = users[i],
|
||||
ud = f.users[user] = f.users[user] || {},
|
||||
badges = ud.badges = ud.badges || {};
|
||||
|
||||
badge_count++;
|
||||
badges[badge.slot] = {id: badge.id};
|
||||
}
|
||||
|
||||
f.log('Added "' + badge.name + '" badge to ' + utils.number_commas(users.length) + ' users.');
|
||||
}
|
||||
|
||||
// Special Badges
|
||||
var zw = f.users.zenwan = f.users.zenwan || {},
|
||||
badges = zw.badges = zw.badges || {};
|
||||
if ( ! badges[1] )
|
||||
badge_count++;
|
||||
badges[1] = {id: 2, image: "//cdn.frankerfacez.com/script/momiglee_badge.png", title: "WAN"};
|
||||
|
||||
f.log("Loaded " + utils.number_commas(badge_count) + " total badges across " + badge_total + " types.");
|
||||
typeof callback === "function" && callback(true, badge_count, badge_total, badge_data);
|
||||
|
||||
}).fail(function(data) {
|
||||
if ( data.status === 404 )
|
||||
return typeof callback === "function" && callback(false);
|
||||
|
||||
tries = (tries || 0) + 1;
|
||||
if ( tries < 10 )
|
||||
return setTimeout(f.load_badges.bind(f, callback, tries), 500 + 500*tries);
|
||||
|
||||
f.error("Unable to load badge data. [HTTP Status " + data.status + "]", data);
|
||||
typeof callback === "function" && callback(false);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype._load_badge_json = function(badge_id, data) {
|
||||
this.badges[badge_id] = data;
|
||||
if ( data.replaces ) {
|
||||
data.replaces_type = data.replaces;
|
||||
data.replaces = true;
|
||||
}
|
||||
|
||||
if ( data.name === 'bot' )
|
||||
data.visible = function(r,user) { return !(this.has_bttv && FFZ.bttv_known_bots.indexOf(user)!==-1); };
|
||||
|
||||
if ( data.name === 'developer' || data.name === 'supporter' )
|
||||
data.click_url = 'https://www.frankerfacez.com/donate';
|
||||
|
||||
utils.update_css(this._badge_style, badge_id, utils.badge_css(data));
|
||||
}
|
531
src/commands.js
531
src/commands.js
|
@ -1,531 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ,
|
||||
constants = require('./constants'),
|
||||
utils = require('./utils'),
|
||||
|
||||
KNOWN_COMMANDS = ['ffz', 'unban', 'ban', 'timeout', 'r9kbeta', 'r9kbetaoff', 'slow', 'slowoff', 'subscribers', 'subscribersoff', 'mod', 'unmod', 'me', 'emoteonly', 'emoteonlyoff', 'host', 'unhost', 'commercial'],
|
||||
|
||||
STATUS_CODES = {
|
||||
400: "Bad Request",
|
||||
401: "Unauthorized",
|
||||
403: "Forbidden",
|
||||
404: "Not Found",
|
||||
500: "Internal Server Error"
|
||||
},
|
||||
|
||||
format_result = function(response) {
|
||||
if ( typeof response === "string" )
|
||||
return response;
|
||||
|
||||
else if ( Array.isArray(response) )
|
||||
return _.map(response, format_result).join(", ");
|
||||
|
||||
return JSON.stringify(response);
|
||||
},
|
||||
|
||||
ObjectPath = require('./ObjectPath');
|
||||
|
||||
|
||||
FFZ.ObjectPath = ObjectPath;
|
||||
|
||||
// -----------------
|
||||
// Settings
|
||||
// -----------------
|
||||
|
||||
FFZ.settings_info.command_aliases = {
|
||||
type: "button",
|
||||
value: [],
|
||||
|
||||
category: "Chat Moderation",
|
||||
no_bttv: 6,
|
||||
|
||||
name: "Command Aliases",
|
||||
help: "Define custom commands for chat that are shortcuts for other commands or messages to send in chat.",
|
||||
|
||||
on_update: function() {
|
||||
this.cache_command_aliases();
|
||||
},
|
||||
|
||||
method: function() {
|
||||
var f = this,
|
||||
old_val = [],
|
||||
input = utils.createElement('textarea');
|
||||
|
||||
input.style.marginBottom = '20px';
|
||||
|
||||
for(var i=0; i < this.settings.command_aliases.length; i++) {
|
||||
var pair = this.settings.command_aliases[i],
|
||||
name = pair[0],
|
||||
command = pair[1],
|
||||
label = pair[2];
|
||||
|
||||
old_val.push(name + (label ? ' ' + label : '') + '=' + command);
|
||||
}
|
||||
|
||||
utils.prompt(
|
||||
"Command Aliases",
|
||||
"Please enter a list of custom commands that you would like to use in Twitch chat. " +
|
||||
"One item per line. To send multiple commands, separate them with <code><LINE></code>. " +
|
||||
"Variables, such as arguments you provide running the custom command, can be inserted into the output.<hr>" +
|
||||
|
||||
"All custom commands require names. Names go at the start of each line, and are separated from " +
|
||||
"the actual command by an equals sign. Do not include the leading slash or dot. Those are automatically included. " +
|
||||
"You can also include a description of the arguments after the name but before the equals-sign " +
|
||||
"to include a helpful reminder when using tab-completion with the command.<br>" +
|
||||
"<strong>Example:</strong> <code>boop <user>=/timeout {0} 15 Boop!</code><hr>" +
|
||||
|
||||
"<code>{0}</code>, <code>{1}</code>, <code>{2}</code>, etc. will be replaced with any arguments you've supplied. " +
|
||||
"Follow an argument index with a <code>$</code> to also include all remaining arguments.<br>" +
|
||||
"<strong>Example:</strong> <code>boop=/timeout {0} 15 {1$}</code><hr>" +
|
||||
|
||||
"<strong>Allowed Variables</strong><br><table><tbody>" +
|
||||
"<tr><td><code>{room}</code></td><td>chat room's name</td>" +
|
||||
"<td><code>{room_name}</code></td><td>chat room's name</td></tr>" +
|
||||
"<tr><td><code>{room_display_name}</code></td><td>chat room's display name</td>" +
|
||||
"<td><code>{room_id}</code></td><td>chat room's numeric ID</td></tr>" +
|
||||
"</tbody></table>",
|
||||
old_val.join("\n"),
|
||||
function(new_val) {
|
||||
if ( new_val === null || new_val === undefined )
|
||||
return;
|
||||
|
||||
var vals = new_val.trim().split(/\s*\n\s*/g),
|
||||
output = [];
|
||||
|
||||
for(var i=0; i < vals.length; i++) {
|
||||
var cmd = vals[i];
|
||||
if ( cmd.charAt(0) === '.' || cmd.charAt(0) === '/' )
|
||||
cmd = cmd.substr(1);
|
||||
|
||||
var name,
|
||||
label,
|
||||
name_match = /^([^=]+)=/.exec(cmd);
|
||||
|
||||
if ( ! cmd || ! cmd.length )
|
||||
continue;
|
||||
|
||||
if ( name_match ) {
|
||||
var ind = name_match[1].indexOf(' ');
|
||||
if ( ind === -1 ) {
|
||||
name = name_match[1].toLowerCase();
|
||||
label = null;
|
||||
} else {
|
||||
name = name_match[1].substr(0,ind).toLowerCase();
|
||||
label = name_match[1].substr(ind).trim();
|
||||
}
|
||||
|
||||
cmd = cmd.substr(name_match[0].length);
|
||||
}
|
||||
|
||||
output.push([name, cmd, label]);
|
||||
}
|
||||
|
||||
f.settings.set("command_aliases", output);
|
||||
|
||||
}, 600, input);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.prototype._command_aliases = {};
|
||||
|
||||
FFZ.prototype.cache_command_aliases = function() {
|
||||
var aliases = this._command_aliases = {};
|
||||
for(var i=0; i < this.settings.command_aliases.length; i++) {
|
||||
var pair = this.settings.command_aliases[i],
|
||||
name = pair[0],
|
||||
command = pair[1],
|
||||
label = pair[2];
|
||||
|
||||
// Skip taken/invalid names.
|
||||
if ( ! name || ! name.length || aliases[name] || KNOWN_COMMANDS.indexOf(name) !== -1 )
|
||||
continue;
|
||||
|
||||
aliases[name] = [command, label];
|
||||
}
|
||||
|
||||
if ( this._inputv )
|
||||
Ember.propertyDidChange(this._inputv, 'ffz_commands');
|
||||
}
|
||||
|
||||
|
||||
|
||||
// -----------------
|
||||
// Log Export
|
||||
// -----------------
|
||||
|
||||
FFZ.ffz_commands.log = function(room, args) {
|
||||
var f = this;
|
||||
this.get_debugging_info().then(function(result) {
|
||||
f._pastebin(result).then(function(url) {
|
||||
f.room_message(room, "Your FrankerFaceZ logs have been pasted to: " + url);
|
||||
}).catch(function() {
|
||||
f.room_message(room, "An error occured uploading the logs to a pastebin.");
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// -----------------
|
||||
// Data Reload
|
||||
// -----------------
|
||||
|
||||
FFZ.ffz_commands.reload = function(room, args) {
|
||||
var f = this,
|
||||
promises = [];
|
||||
|
||||
// Feature Friday. There's no feedback possible so don't use a promise.
|
||||
this.check_ff();
|
||||
|
||||
// Badge Information
|
||||
promises.push(new Promise(function(done, fail) {
|
||||
f.load_badges(function(success, badge_count, badge_total, badge_data) {
|
||||
done(success ? [badge_count, badge_total, badge_data] : [0, 0, {}]);
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
// Emote Sets
|
||||
for(var set_id in this.emote_sets) {
|
||||
var es = this.emote_sets[set_id];
|
||||
if ( ! es || es.hasOwnProperty('source_ext') )
|
||||
continue;
|
||||
|
||||
promises.push(new Promise(function(done, fail) {
|
||||
f.load_set(set_id, done);
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
// Do it!
|
||||
Promise.all(promises).then(function(results) {
|
||||
try {
|
||||
var success = 0,
|
||||
badge_count = results[0][0],
|
||||
badge_total = results[0][1],
|
||||
badges = results[0][2],
|
||||
total = results.length - 1,
|
||||
badge_string = [];
|
||||
|
||||
if ( results.length > 1 ) {
|
||||
for(var i=1; i < results.length; i++) {
|
||||
if ( results[i] )
|
||||
success++;
|
||||
}
|
||||
}
|
||||
|
||||
for(var key in badges) {
|
||||
if ( badges.hasOwnProperty(key) )
|
||||
badge_string.push(key + ': ' + badges[key])
|
||||
}
|
||||
|
||||
f.room_message(room, "Loaded " + utils.number_commas(badge_count) + " badge" + utils.pluralize(badge_count) + " across " + utils.number_commas(badge_total) + " badge type" + utils.pluralize(badge_total) + (badge_string.length ? " (" + badge_string.join(", ") + ")" : "") + ". Successfully reloaded " + utils.number_commas(success) + " of " + utils.number_commas(total) + " emoticon set" + utils.pluralize(total) + ".");
|
||||
|
||||
} catch(err) {
|
||||
f.room_message(room, "An error occured running the command.");
|
||||
f.error("Error Running FFZ Reload", err);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// -----------------
|
||||
// Moderation Cards
|
||||
// -----------------
|
||||
|
||||
FFZ.chat_commands.card = function(room, args) {
|
||||
if ( ! args || ! args.length || args.length > 1 )
|
||||
return "Usage: /card <username>";
|
||||
|
||||
if ( ! this._roomv )
|
||||
return "An error occured. (We don't have the Room View.)";
|
||||
|
||||
// Get the position of the input box.
|
||||
var el = this._roomv.get('element'),
|
||||
ta = el && el.querySelector('textarea'),
|
||||
bounds = ta && ta.getBoundingClientRect(),
|
||||
|
||||
x = 0, y = 0, bottom, right;
|
||||
|
||||
if ( ! bounds )
|
||||
bounds = el && el.getBoundingClientRect() || document.body.getBoundingClientRect();
|
||||
|
||||
if ( bounds ) {
|
||||
if ( bounds.left > 400 ) {
|
||||
right = bounds.left - 40;
|
||||
bottom = bounds.top + bounds.height;
|
||||
} else {
|
||||
x = bounds.left - 20;
|
||||
bottom = bounds.top - 20;
|
||||
}
|
||||
}
|
||||
|
||||
this._roomv.actions.showModOverlay.call(this._roomv, {
|
||||
top: y,
|
||||
left: x,
|
||||
bottom: bottom,
|
||||
right: right,
|
||||
sender: args[0]
|
||||
});
|
||||
}
|
||||
|
||||
FFZ.chat_commands.card.label = '/card <user>';
|
||||
FFZ.chat_commands.card.info = 'Open Moderation Card';
|
||||
|
||||
|
||||
FFZ.chat_commands.rules = function(room, args) {
|
||||
var f = this,
|
||||
r = room.room;
|
||||
|
||||
r.waitForRoomProperties().then(function() {
|
||||
var rules = r.get("roomProperties.chat_rules");
|
||||
if ( ! rules || ! rules.length )
|
||||
return f.room_message(room, "This chat room does not have rules set.");
|
||||
|
||||
r.set("chatRules", rules);
|
||||
r.set("shouldDisplayChatRules", true);
|
||||
});
|
||||
}
|
||||
|
||||
FFZ.chat_commands.rules.info = 'Show Chat Room Rules';
|
||||
|
||||
|
||||
FFZ.chat_commands.open_link = function(room, args) {
|
||||
if ( ! args || ! args.length )
|
||||
return "Usage: /open_link <url>";
|
||||
|
||||
var wnd = window.open(args.join(" "), "_blank");
|
||||
wnd.opener = null;
|
||||
}
|
||||
|
||||
FFZ.chat_commands.open_link.label = '/open_link <url>';
|
||||
FFZ.chat_commands.open_link.info = 'Open URL in Tab';
|
||||
|
||||
|
||||
FFZ.chat_commands.fetch_link = function(room, args) {
|
||||
if ( ! args || ! args.length )
|
||||
return "Usage: /fetch_link <url> [template]\nTemplates use http://objectpath.org/ to format data. Default Template is \"Response: #$#\"";
|
||||
|
||||
var f = this,
|
||||
url = args.shift(),
|
||||
headers = {};
|
||||
|
||||
if ( /https?:\/\/[^.]+\.twitch\.tv\//.test(url) )
|
||||
headers['Client-ID'] = constants.CLIENT_ID;
|
||||
|
||||
jQuery.ajax({
|
||||
url: url,
|
||||
headers: headers,
|
||||
|
||||
success: function(data) {
|
||||
f.log("Response Received", data);
|
||||
args = (args && args.length) ? args.join(" ").split(/#/g) : ["Response: ", "$"];
|
||||
|
||||
if ( typeof data === "string" )
|
||||
data = [data];
|
||||
|
||||
var is_special = true,
|
||||
output = [],
|
||||
op = new ObjectPath(data);
|
||||
|
||||
for(var i=0; i < args.length; i++) {
|
||||
var segment = args[i];
|
||||
is_special = !is_special;
|
||||
if ( ! is_special )
|
||||
output.push(segment);
|
||||
else
|
||||
try {
|
||||
output.push(format_result(op.execute(segment)));
|
||||
} catch(err) {
|
||||
f.log("Error", err);
|
||||
output.push("[Error: " + (err.message || err) + "]");
|
||||
}
|
||||
}
|
||||
|
||||
f.room_message(room, output.join(''));
|
||||
|
||||
},
|
||||
error: function(xhr) {
|
||||
f.log("Request Error", xhr);
|
||||
f.room_message(room, "Request Failed: " + (xhr.status === 0 ? 'Unknown Error. ' + (url.indexOf('https') === -1 ? 'Please make sure you\'re making HTTPS requests.' : 'Likely a CORS problem. Check your browser\'s Networking console for more.') : xhr.status + ' ' + (STATUS_CODES[xhr.status] || '' )));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
FFZ.chat_commands.fetch_link.label = '/fetch_link <url> <i>[template]</i>';
|
||||
FFZ.chat_commands.fetch_link.info = 'Fetch URL and Display in Chat';
|
||||
|
||||
|
||||
// --------------------------
|
||||
// Message-Specific Deletion
|
||||
// --------------------------
|
||||
|
||||
FFZ.chat_commands.timeout_message = function(room, args) {
|
||||
var username = args.shift(),
|
||||
target_message = args.shift(),
|
||||
duration = args.shift(),
|
||||
reason = args.join(' ');
|
||||
|
||||
if ( ! username || ! target_message )
|
||||
return 'Usage: /timeout_message <username> <target-message-id> [duration=600] [reason]\nTimeout a user and provide the ID of a specific message to remove.';
|
||||
|
||||
room.room.sendTags('/timeout ' + username + ' ' + (duration || '600') + (reason ? ' ' + reason : ''), {
|
||||
'target-message-id': target_message
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ---------------------
|
||||
// Group Chat Renaming
|
||||
// ---------------------
|
||||
|
||||
FFZ.chat_commands.renamegroup = function(room, args) {
|
||||
var f = this,
|
||||
new_name = args.join(' ');
|
||||
if ( ! new_name.length )
|
||||
return "Usage: /renamegroup <name>\nGroup owner only. Rename a group chat.";
|
||||
|
||||
// Check that the length of the arguments is less than 120 bytes
|
||||
else if ( utils.utf8_encode(new_name).length > 120 )
|
||||
return "You entered a room name that is too long.";
|
||||
|
||||
// Set the group name
|
||||
room.room.tmiRoom.session._depotApi.put("/rooms/" + room.id, {
|
||||
display_name: new_name
|
||||
}).then(function(result) {
|
||||
if ( result && result.room && result.room.display_name === new_name )
|
||||
f.room_message(room, 'The room was renamed to: ' + new_name);
|
||||
else
|
||||
f.room_message(room, 'The room name was not changed successfully.')
|
||||
});
|
||||
}
|
||||
|
||||
FFZ.chat_commands.renamegroup.label = '/renamegroup <name>';
|
||||
FFZ.chat_commands.renamegroup.info = 'Rename a group chat. Group owner only.'
|
||||
FFZ.chat_commands.renamegroup.enabled = function(room) {
|
||||
// Are we in a group chat and are we the owner?
|
||||
return room && room.room && room.room.get('isGroupRoom') && room.room.get('isOwner');
|
||||
}
|
||||
|
||||
|
||||
// -----------------
|
||||
// Promoted Messages
|
||||
// -----------------
|
||||
|
||||
FFZ.ffz_commands.promote = function(room, args) {
|
||||
args = args.join(" ").trim();
|
||||
var f = this;
|
||||
|
||||
if ( ! args.length || args.indexOf(' ') !== -1 )
|
||||
return "You must provide only a single message ID. This is easiest to add as a custom in-line moderation action.";
|
||||
|
||||
if ( ! this.ws_send("promote_message", [room.id, args], function(success, data) {
|
||||
if ( ! success ) {
|
||||
f.log("Promotion Error: " + JSON.stringify(data));
|
||||
f.room_message(room, "There was an error promoting the message.");
|
||||
}
|
||||
}) )
|
||||
return "There was an error communicating with the server.";
|
||||
}
|
||||
|
||||
|
||||
// -----------------
|
||||
// Mass Moderation
|
||||
// -----------------
|
||||
|
||||
FFZ.ffz_commands.massunmod = function(room, args) {
|
||||
args = args.join(" ").trim();
|
||||
|
||||
if ( ! args.length )
|
||||
return "You must provide a list of users to unmod.";
|
||||
|
||||
args = args.split(/\W*,\W*/);
|
||||
|
||||
var user = this.get_user();
|
||||
if ( ! user || ! user.login == room.id )
|
||||
return "You must be the broadcaster to use massunmod.";
|
||||
|
||||
if ( args.length > 50 )
|
||||
return "Each user you unmod counts as a single message. To avoid being globally banned, please limit yourself to 50 at a time and wait between uses.";
|
||||
|
||||
var count = args.length;
|
||||
while(args.length) {
|
||||
var name = args.shift();
|
||||
room.room.tmiRoom.sendMessage("/unmod " + name);
|
||||
}
|
||||
|
||||
return "Sent unmod command for " + count + " users.";
|
||||
}
|
||||
|
||||
FFZ.ffz_commands.massunmod.help = "Usage: /ffz massunmod <list, of, users>\nBroadcaster only. Unmod all the users in the provided list.";
|
||||
|
||||
|
||||
FFZ.ffz_commands.massmod = function(room, args) {
|
||||
args = args.join(" ").trim();
|
||||
|
||||
if ( ! args.length )
|
||||
return "You must provide a list of users to mod.";
|
||||
|
||||
args = args.split(/\W*,\W*/);
|
||||
|
||||
var user = this.get_user();
|
||||
if ( ! user || ! user.login == room.id )
|
||||
return "You must be the broadcaster to use massmod.";
|
||||
|
||||
if ( args.length > 50 )
|
||||
return "Each user you mod counts as a single message. To avoid being globally banned, please limit yourself to 50 at a time and wait between uses.";
|
||||
|
||||
var count = args.length;
|
||||
while(args.length) {
|
||||
var name = args.shift();
|
||||
room.room.tmiRoom.sendMessage("/mod " + name);
|
||||
}
|
||||
|
||||
return "Sent mod command for " + count + " users.";
|
||||
}
|
||||
|
||||
FFZ.ffz_commands.massmod.help = "Usage: /ffz massmod <list, of, users>\nBroadcaster only. Mod all the users in the provided list.";
|
||||
|
||||
|
||||
// -----------------
|
||||
// Mass Unbanning
|
||||
// -----------------
|
||||
|
||||
FFZ.prototype.get_banned_users = function() {
|
||||
var f = this;
|
||||
return new Promise(function(succeed, fail) {
|
||||
var user = f.get_user();
|
||||
if ( ! user )
|
||||
return fail();
|
||||
|
||||
jQuery.get("/settings/channel").done(function(data) {
|
||||
try {
|
||||
var dom = new DOMParser().parseFromString(data, 'text/html'),
|
||||
users = _.pluck(dom.querySelectorAll('.ban .obj'), 'textContent');
|
||||
|
||||
succeed(_.map(users, function(x) { return x.trim() }));
|
||||
|
||||
} catch(err) {
|
||||
f.error("Failed to parse banned users", err);
|
||||
fail();
|
||||
}
|
||||
|
||||
}).fail(function(err) {
|
||||
f.error("Failed to load banned users", err);
|
||||
fail();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/*FFZ.ffz_commands.massunban = function(room, args) {
|
||||
var user = this.get_user();
|
||||
if ( ! user || (user.login !== room.id && ! user.is_admin && ! user.is_staff) )
|
||||
return "You must be the broadcaster to use massunban.";
|
||||
|
||||
|
||||
}*/
|
||||
|
||||
|
||||
/*FFZ.ffz_commands.massunban = function(room, args) {
|
||||
args = args.join(" ").trim();
|
||||
|
||||
}*/
|
146
src/constants.js
146
src/constants.js
File diff suppressed because one or more lines are too long
38
src/debug.js
38
src/debug.js
|
@ -1,38 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ;
|
||||
|
||||
|
||||
// -----------------------
|
||||
// Developer Mode
|
||||
// -----------------------
|
||||
|
||||
FFZ.settings_info.developer_mode = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
storage_key: "ffzDebugMode",
|
||||
|
||||
visible: function() { return this.settings.developer_mode || (Date.now() - parseInt(localStorage.ffzLastDevMode || "0")) < 604800000; },
|
||||
category: "Debugging",
|
||||
name: "Developer Mode",
|
||||
help: "Load FrankerFaceZ from the local development server instead of the CDN. Please refresh after changing this setting.",
|
||||
|
||||
on_update: function() {
|
||||
localStorage.ffzLastDevMode = Date.now();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.ffz_commands.developer_mode = function(room, args) {
|
||||
var enabled, args = args && args.length ? args[0].toLowerCase() : null;
|
||||
if ( args == "y" || args == "yes" || args == "true" || args == "on" )
|
||||
enabled = true;
|
||||
else if ( args == "n" || args == "no" || args == "false" || args == "off" )
|
||||
enabled = false;
|
||||
|
||||
if ( enabled === undefined )
|
||||
return "Developer Mode is currently " + (this.settings.developer_mode ? "enabled." : "disabled.");
|
||||
|
||||
this.settings.set("developer_mode", enabled);
|
||||
return "Developer Mode is now " + (enabled ? "enabled" : "disabled") + ". Please refresh your browser.";
|
||||
}
|
||||
|
||||
FFZ.ffz_commands.developer_mode.help = "Usage: /ffz developer_mode <on|off>\nEnable or disable Developer Mode. When Developer Mode is enabled, the script will be reloaded from //localhost:8000/script.js instead of from the CDN.";
|
|
@ -1,335 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ,
|
||||
utils = require('../utils'),
|
||||
constants = require('../constants');
|
||||
|
||||
|
||||
// --------------------
|
||||
// Settings
|
||||
// --------------------
|
||||
|
||||
FFZ.settings_info.collect_bits = {
|
||||
type: "select",
|
||||
options: {
|
||||
0: "Disabled",
|
||||
1: "Grouped by Type",
|
||||
2: "All in One"
|
||||
},
|
||||
|
||||
value: 0,
|
||||
process_value: utils.process_int(0),
|
||||
|
||||
category: "Chat Appearance",
|
||||
no_bttv: 6,
|
||||
|
||||
name: "Bits Stacking",
|
||||
help: "Collect all the bits emoticons in a message into a single one at the start of the message."
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.bits_animated = {
|
||||
type: "boolean",
|
||||
value: true,
|
||||
|
||||
category: "Chat Appearance",
|
||||
no_bttv: 6,
|
||||
|
||||
name: "Bits Animation",
|
||||
help: "Display bits with animation.",
|
||||
|
||||
on_update: function() {
|
||||
var bits = utils.ember_lookup('service:bits-emotes') ||
|
||||
utils.ember_lookup('service:bits-rendering-config');
|
||||
if ( bits && bits.ffz_has_css )
|
||||
bits.ffz_update_css();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
FFZ.settings_info.bits_tags_container = {
|
||||
type: "boolean",
|
||||
value: true,
|
||||
|
||||
category: "Chat Appearance",
|
||||
no_bttv: 6,
|
||||
|
||||
name: "Bits Tag Display",
|
||||
help: "Display competitive bits tags at the top of chats that have it enabled.",
|
||||
|
||||
on_update: utils.toggle_cls('ffz-show-bits-tags')
|
||||
}
|
||||
|
||||
|
||||
FFZ.settings_info.bits_pinned = {
|
||||
type: "select",
|
||||
options: {
|
||||
0: "Disabled",
|
||||
1: "Show Recent",
|
||||
2: "Show Top",
|
||||
3: "Show All (Default)"
|
||||
},
|
||||
|
||||
value: 3,
|
||||
process_value: utils.process_int(3, 0, 3),
|
||||
|
||||
category: "Chat Appearance",
|
||||
|
||||
name: "Display Pinned Cheers",
|
||||
help: "Show pinned messages with bits at the top of chat in channels that have it enabled.",
|
||||
|
||||
on_update: function(val) {
|
||||
var PinnedCheers = utils.ember_lookup('service:bits-pinned-cheers');
|
||||
if ( val === 3 || ! PinnedCheers )
|
||||
return;
|
||||
|
||||
if ( val !== 1 )
|
||||
PinnedCheers.set('recentPinnedCheer', null);
|
||||
|
||||
if ( val !== 2 )
|
||||
PinnedCheers.set('topPinnedCheer', null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
FFZ.settings_info.bits_redesign = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
|
||||
category: "Chat Appearance",
|
||||
name: "Bits Redesign",
|
||||
help: "Use the special cheering animations from April 1st, 2017.",
|
||||
|
||||
on_update: function() {
|
||||
var bits = utils.ember_lookup('service:bits-emotes') ||
|
||||
utils.ember_lookup('service:bits-rendering-config');
|
||||
if ( bits && bits.ffz_has_css )
|
||||
bits.ffz_update_css();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --------------------
|
||||
// Initialization
|
||||
// --------------------
|
||||
|
||||
var redesign = function(x) { return x.replace('/actions/cheer/', '/actions/cheer-redesign/') };
|
||||
|
||||
FFZ.prototype.setup_bits = function() {
|
||||
utils.toggle_cls('ffz-show-bits-tags')(this.settings.bits_tags_container);
|
||||
|
||||
this.update_views('component:bits/chat-token', this._modify_bits_token);
|
||||
|
||||
var f = this,
|
||||
Service = utils.ember_lookup('service:bits-emotes'),
|
||||
PinnedCheers = utils.ember_lookup('service:bits-pinned-cheers'),
|
||||
|
||||
image_css = function(images) {
|
||||
var im_1 = images[1],
|
||||
im_2 = images[2],
|
||||
im_4 = images[4];
|
||||
|
||||
if ( f.settings.bits_redesign ) {
|
||||
im_1 = redesign(im_1);
|
||||
im_2 = redesign(im_2);
|
||||
im_4 = redesign(im_4);
|
||||
}
|
||||
|
||||
return 'background-image: url("' + im_1 + '");' +
|
||||
'background-image: ' + (constants.IS_WEBKIT ? ' -webkit-' : '') + 'image-set(' +
|
||||
'url("' + im_1 + '") 1x, url("' + im_2 + '") 2x, url("' + im_4 + '") 4x);';
|
||||
},
|
||||
|
||||
tier_css = function(ind, prefix, tier) {
|
||||
var selector = '.ffz-bit.bit-prefix-' + prefix + '.bit-tier-' + ind,
|
||||
color = f._handle_color(tier.color),
|
||||
animated = f.settings.bits_animated,
|
||||
output;
|
||||
|
||||
output = selector + '{' +
|
||||
'color: ' + color[0] + ';' +
|
||||
this._ffz_image_css(tier.images.light[animated ? 'animated' : 'static']) +
|
||||
'}';
|
||||
|
||||
return output + '.tipsy ' + selector + ',.dark ' + selector + ',.force-dark ' + selector + ',.theatre ' + selector + '{' +
|
||||
'color: ' + color[1] + ';' +
|
||||
this._ffz_image_css(tier.images.dark[animated ? 'animated' : 'static']) +
|
||||
'}';
|
||||
};
|
||||
|
||||
if ( PinnedCheers ) {
|
||||
PinnedCheers.reopen({
|
||||
_updatePinnedCheerData: function(data) {
|
||||
var setting = f.settings.bits_pinned;
|
||||
if ( setting < 2 )
|
||||
data.top = null;
|
||||
else if ( data.top )
|
||||
data.top.is_pinned_cheer = 2;
|
||||
|
||||
if ( setting !== 3 && setting !== 1 )
|
||||
data.recent = null;
|
||||
else if ( data.recent )
|
||||
data.recent.is_pinned_cheer = true;
|
||||
|
||||
return this._super(data);
|
||||
}
|
||||
});
|
||||
|
||||
FFZ.settings_info.bits_pinned.on_update.call(this, this.settings.bits_pinned);
|
||||
}
|
||||
|
||||
if ( Service ) {
|
||||
Service.reopen({
|
||||
ffz_has_css: false,
|
||||
|
||||
ffz_get_tier: function(prefix, amount) {
|
||||
if ( ! this.ffz_has_css )
|
||||
this.ffz_update_css();
|
||||
|
||||
var config = this.getPrefixData(prefix) || {},
|
||||
tiers = config.tiers || [],
|
||||
tier = null,
|
||||
index = null;
|
||||
|
||||
for(var i=0, l = tiers.length; i < l; i++) {
|
||||
var t = tiers[i];
|
||||
if ( amount < t.min_bits )
|
||||
break;
|
||||
|
||||
tier = t;
|
||||
index = i;
|
||||
}
|
||||
|
||||
return [index, tier];
|
||||
},
|
||||
|
||||
ffz_get_preview: function(prefix, amount) {
|
||||
var src = this.getImageSrc(amount, prefix, true, !f.settings.bits_animated, 4);
|
||||
return f.settings.bits_redesign ? redesign(src) : src;
|
||||
},
|
||||
|
||||
_ffz_image_css: image_css,
|
||||
_ffz_tier_css: tier_css,
|
||||
|
||||
ffz_update_css: function() {
|
||||
var output = [],
|
||||
prefixes = _.map(this.get('regexes') || [], function(x) {
|
||||
return x && x.prefix || null;
|
||||
});
|
||||
|
||||
for(var i=0; i < prefixes.length; i++) {
|
||||
var prefix = prefixes[i],
|
||||
data = prefix && this.getPrefixData(prefix);
|
||||
|
||||
if ( ! data )
|
||||
continue;
|
||||
|
||||
var tiers = data && data.tiers || [];
|
||||
for(var x=0; x < tiers.length; x++)
|
||||
output.push(this._ffz_tier_css(x, prefix, tiers[x]));
|
||||
}
|
||||
|
||||
utils.update_css(f._chat_style, 'bit-styles', output.join(''));
|
||||
this.ffz_has_css = true;
|
||||
}.observes('emoteConfig', 'regexes')
|
||||
});
|
||||
|
||||
} else {
|
||||
this.log("Unable to find the Ember service:bits-emotes. Falling back...");
|
||||
|
||||
Service = utils.ember_lookup('service:bits-rendering-config');
|
||||
if ( ! Service )
|
||||
return this.error("Unable to locate the Ember service:bits-rendering-config");
|
||||
|
||||
Service.reopen({
|
||||
ffz_has_css: false,
|
||||
|
||||
ffz_get_tier: function(prefix, amount) {
|
||||
if ( ! this.get('isLoaded') ) {
|
||||
this._actionPromiseCache = false;
|
||||
this.loadRenderConfig();
|
||||
} else if ( ! this.ffz_has_css )
|
||||
this.ffz_update_css();
|
||||
|
||||
var config = this._getConfigPrefix(prefix) || {},
|
||||
tiers = config.tiers || [],
|
||||
tier = null,
|
||||
index = null;
|
||||
|
||||
for(var i=0, l = tiers.length; i < l; i++) {
|
||||
var t = tiers[i];
|
||||
if ( amount < t.min_bits )
|
||||
break;
|
||||
|
||||
tier = t;
|
||||
index = i;
|
||||
}
|
||||
|
||||
return [index, tier];
|
||||
},
|
||||
|
||||
ffz_get_preview: function(prefix, amount) {
|
||||
var data = this.ffz_get_tier(prefix, amount),
|
||||
tier = data && data[1],
|
||||
src = tier ? this._constructImageSrc([4], tier, {
|
||||
background: 'dark',
|
||||
scale: 4,
|
||||
state: f.settings.bits_animated ? 'animated' : 'static'
|
||||
}).src : '';
|
||||
|
||||
return f.settings.bits_redesign ? redesign(src) : src;
|
||||
},
|
||||
|
||||
_ffz_image_css: image_css,
|
||||
_ffz_tier_css: tier_css,
|
||||
|
||||
ffz_update_css: function() {
|
||||
var output = [],
|
||||
config = this.get('config') || {prefixes: []};
|
||||
|
||||
for(var i=0; i < config.prefixes.length; i++) {
|
||||
var prefix = config.prefixes[i],
|
||||
data = this._getConfigPrefix(prefix),
|
||||
tiers = data && data.tiers || [];
|
||||
|
||||
for(var x=0; x < tiers.length; x++)
|
||||
output.push(this._ffz_tier_css(x, prefix, tiers[x]));
|
||||
}
|
||||
|
||||
utils.update_css(f._chat_style, 'bit-styles', output.join(''));
|
||||
this.ffz_has_css = true;
|
||||
}.observes('config'),
|
||||
|
||||
loadRenderConfig: function() {
|
||||
var out = this._super();
|
||||
if ( ! this.get('config') )
|
||||
this._actionPromiseCache = false;
|
||||
return out;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ( Service.get('isLoaded') && Service.loadRenderConfig )
|
||||
Service.loadRenderConfig();
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype._modify_bits_token = function(component) {
|
||||
var f = this;
|
||||
utils.ember_reopen_view(component, {
|
||||
ffz_init: function() {
|
||||
this.ffzRender();
|
||||
},
|
||||
|
||||
ffzRender: function() {
|
||||
var el = this.get('element'),
|
||||
prefix = this.get('prefix'),
|
||||
amount = this.get('amount');
|
||||
|
||||
el.innerHTML = f.render_token(false, false, true, {
|
||||
type: 'bits',
|
||||
amount: amount,
|
||||
prefix: prefix
|
||||
});
|
||||
|
||||
}.observes('prefix', 'amount')
|
||||
})
|
||||
}
|
|
@ -1,754 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ,
|
||||
utils = require('../utils'),
|
||||
constants = require('../constants');
|
||||
|
||||
|
||||
// --------------------
|
||||
// Initialization
|
||||
// --------------------
|
||||
|
||||
FFZ.prototype.setup_channel = function() {
|
||||
// Style Stuff!
|
||||
this.log("Creating channel style element.");
|
||||
var s = this._channel_style = document.createElement("style");
|
||||
s.id = "ffz-channel-css";
|
||||
document.head.appendChild(s);
|
||||
|
||||
// Settings stuff!
|
||||
document.body.classList.toggle("ffz-hide-view-count", !this.settings.channel_views);
|
||||
document.body.classList.toggle('ffz-theater-stats', this.settings.theater_stats === 2);
|
||||
document.body.classList.toggle('ffz-theater-basic-stats', this.settings.theater_stats > 0);
|
||||
|
||||
var banner_hidden = this.settings.hide_channel_banner;
|
||||
banner_hidden = banner_hidden === 1 ? this.settings.channel_bar_bottom : banner_hidden > 0;
|
||||
|
||||
utils.toggle_cls('ffz-hide-channel-banner')(banner_hidden);
|
||||
utils.toggle_cls('ffz-channel-bar-bottom')(this.settings.channel_bar_bottom);
|
||||
utils.toggle_cls('ffz-minimal-channel-title')(this.settings.channel_title_top === 2);
|
||||
utils.toggle_cls('ffz-channel-title-top')(this.settings.channel_title_top > 0);
|
||||
utils.toggle_cls('ffz-minimal-channel-bar')(this.settings.channel_bar_collapse);
|
||||
|
||||
this.log("Hooking the Ember Channel Index redesign.");
|
||||
this.update_views('component:channel-redesign', this.modify_channel_redesign);
|
||||
this.update_views('component:channel-redesign/live', this.modify_channel_live);
|
||||
|
||||
this.update_views('component:share-box', this.modify_channel_share_box);
|
||||
this.update_views('component:channel-options', this.modify_channel_options);
|
||||
this.update_views('component:edit-broadcast-link', this.modify_channel_broadcast_link);
|
||||
|
||||
/*this.log("Hooking the Ember Channel Index component.");
|
||||
if ( ! this.update_views('component:legacy-channel', this.modify_channel_index) )
|
||||
return;*/
|
||||
|
||||
var f = this,
|
||||
Channel = utils.ember_lookup('controller:channel');
|
||||
if ( ! Channel )
|
||||
return f.error("Unable to find the Ember Channel controller");
|
||||
|
||||
this.log("Hooking the Ember Channel controller.");
|
||||
|
||||
Channel.reopen({
|
||||
ffzUpdateInfo: function() {
|
||||
if ( this._ffz_update_timer )
|
||||
clearTimeout(this._ffz_update_timer);
|
||||
|
||||
this._ffz_update_timer = setTimeout(this.ffzCheckUpdate.bind(this), 55000 + (Math.random() * 10000));
|
||||
|
||||
}.observes("channelModel", "channelModel.hostModeTarget"),
|
||||
|
||||
ffzCheckUpdate: function() {
|
||||
this.ffzUpdateInfo();
|
||||
this._ffzUpdateModel(this.get('channelModel'));
|
||||
this._ffzUpdateModel(this.get('channelModel.hostModeTarget'), true);
|
||||
},
|
||||
|
||||
_ffzUpdateModel: function(model, is_host) {
|
||||
var channel_id = model && model.get('id');
|
||||
if ( ! channel_id || model.get('isLoading') )
|
||||
return;
|
||||
|
||||
utils.api.get("streams/" + channel_id, {}, {version: 3})
|
||||
.done(function(data) {
|
||||
// If there's no stream, we can't update much.
|
||||
if ( ! data || ! data.stream ) {
|
||||
model.set('stream.createdAt', null);
|
||||
model.set('stream.viewers', 0);
|
||||
return;
|
||||
}
|
||||
|
||||
model.set('stream.createdAt', utils.parse_date(data.stream.created_at));
|
||||
model.set('stream.viewers', data.stream.viewers || 0);
|
||||
model.set('stream.game', data.stream.game);
|
||||
|
||||
if ( data.stream.channel ) {
|
||||
var info = data.stream.channel;
|
||||
model.set('game', info.game);
|
||||
model.set('status', info.status);
|
||||
model.set('views', info.views);
|
||||
if ( model.get('followers.isFulfilled') )
|
||||
model.set('followers.content.meta.total', info.followers);
|
||||
}
|
||||
|
||||
if ( f._cindex )
|
||||
if ( is_host )
|
||||
f._cindex.ffzFixHostTitle();
|
||||
else
|
||||
f._cindex.ffzFixTitle();
|
||||
});
|
||||
},
|
||||
|
||||
ffzHostTarget: function() {
|
||||
var target = this.get('channelModel.hostModeTarget'),
|
||||
name = target && target.get('name'),
|
||||
id = target && target.get('id'),
|
||||
display_name = target && target.get('display_name');
|
||||
|
||||
if ( display_name && display_name !== 'jtv' )
|
||||
FFZ.capitalization[name] = [display_name, Date.now()];
|
||||
|
||||
if ( f._chatv )
|
||||
f._chatv.ffzUpdateHost(target);
|
||||
|
||||
if ( f._cindex )
|
||||
f._cindex.ffzUpdateMetadata();
|
||||
|
||||
}.observes("channelModel.hostModeTarget")
|
||||
});
|
||||
|
||||
Channel.ffzUpdateInfo();
|
||||
}
|
||||
|
||||
|
||||
// These have to be done in order to ensure the channel metadata all sorts correctly.
|
||||
|
||||
FFZ.prototype.modify_channel_share_box = function(view) {
|
||||
utils.ember_reopen_view(view, {
|
||||
ffz_init: function() {
|
||||
this.get('element').classList.toggle('ffz-share-box', true)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
FFZ.prototype.modify_channel_options = function(view) {
|
||||
utils.ember_reopen_view(view, {
|
||||
ffz_init: function() {
|
||||
this.get('element').classList.toggle('ffz-channel-options', true)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
FFZ.prototype.modify_channel_broadcast_link = function(view) {
|
||||
utils.ember_reopen_view(view, {
|
||||
ffz_init: function() {
|
||||
this.get('element').classList.toggle('ffz-channel-broadcast-link', true)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Channel Live
|
||||
|
||||
FFZ.prototype.modify_channel_live = function(view) {
|
||||
var f = this;
|
||||
utils.ember_reopen_view(view, {
|
||||
ffz_host: null,
|
||||
|
||||
ffz_init: function() {
|
||||
var channel_id = this.get("channel.id"),
|
||||
el = this.get("element");
|
||||
|
||||
f._cindex = this;
|
||||
f.ws_sub("channel." + channel_id);
|
||||
|
||||
this.ffzUpdateAttributes();
|
||||
this.ffzFixTitle();
|
||||
this.ffzFixHostTitle();
|
||||
|
||||
this.ffzUpdateMetadata();
|
||||
|
||||
if ( f.settings.auto_theater ) {
|
||||
var layout = this.get('layout'),
|
||||
func = function(tries) {
|
||||
var player = f._player && f._player.get('player');
|
||||
if ( ! player || typeof player.isLoading === 'function' && player.isLoading() )
|
||||
return (tries||0) < 20 ? setTimeout(func.bind(this, (tries||0) + 1), 500) : null;
|
||||
|
||||
// In case this happens before the event bindings are in, we just set
|
||||
// the layout into theater mode manually.
|
||||
player.setTheatre(true);
|
||||
layout.setTheatreMode(true);
|
||||
}
|
||||
|
||||
func();
|
||||
}
|
||||
|
||||
this.$().on("click", ".ffz-creative-tag-link", utils.transition_link(function(e) {
|
||||
utils.transition('directory.creative.hashtag.index', this.getAttribute('data-tag'));
|
||||
}));
|
||||
|
||||
var t = this;
|
||||
this.$('.player-placeholder').on('click', function() { t.updatePlayerPosition() })
|
||||
|
||||
if ( this.updatePlayerPosition ) {
|
||||
this._ffz_loaded = Date.now();
|
||||
this._ffz_player_repositoner = setInterval(this.ffzUpdatePlayerPosition.bind(this), 250);
|
||||
}
|
||||
},
|
||||
|
||||
ffzUpdatePlayerPosition: function() {
|
||||
if ( f.has_bttv || this._ffz_player_repositoner && Date.now() - this._ffz_loaded > 60000 ) {
|
||||
clearInterval(this._ffz_player_repositoner);
|
||||
this._ffz_player_repositoner = null;
|
||||
if ( f.has_bttv )
|
||||
return;
|
||||
}
|
||||
|
||||
this.updatePlayerPosition();
|
||||
},
|
||||
|
||||
ffzUpdateAttributes: function() {
|
||||
var channel_id = this.get("channel.id"),
|
||||
hosted_id = this.get("channel.hostModeTarget.id"),
|
||||
el = this.get("element");
|
||||
|
||||
if ( hosted_id !== this.ffz_host ) {
|
||||
if ( this.ffz_host )
|
||||
f.ws_unsub("channel." + this.ffz_host);
|
||||
|
||||
if ( hosted_id )
|
||||
f.ws_sub("channel." + hosted_id);
|
||||
|
||||
this.ffz_host = hosted_id;
|
||||
|
||||
// Destroy the popup if we have a metadata popup.
|
||||
if ( f._popup && f._popup.id === 'ffz-metadata-popup' )
|
||||
f.close_popup();
|
||||
|
||||
if ( hosted_id )
|
||||
this.ffzFixHostTitle();
|
||||
|
||||
this.ffzUpdateMetadata();
|
||||
}
|
||||
|
||||
el.classList.add('ffz-channel');
|
||||
el.classList.toggle('ffz-host', hosted_id || false);
|
||||
el.setAttribute('data-channel', channel_id || '');
|
||||
el.setAttribute('data-hosted', hosted_id || '');
|
||||
|
||||
}.observes('channel.id', 'channel.hostModeTarget'),
|
||||
|
||||
ffz_destroy: function() {
|
||||
var channel_id = this.get("channel.id"),
|
||||
el = this.get("element");
|
||||
|
||||
if ( channel_id )
|
||||
f.ws_unsub("channel." + channel_id);
|
||||
|
||||
if ( this.ffz_host ) {
|
||||
f.ws_unsub("channel." + this.ffz_host);
|
||||
this.ffz_host = null;
|
||||
}
|
||||
|
||||
var timers = this.ffz_timers || {};
|
||||
for(var key in timers) {
|
||||
var to = timers[key];
|
||||
if ( to )
|
||||
clearTimeout(to);
|
||||
}
|
||||
|
||||
var popup = el.querySelector('#ffz-metadata-popup');
|
||||
if ( popup && popup === f._popup )
|
||||
f.close_popup();
|
||||
|
||||
if ( f._cindex === this )
|
||||
f._cindex = null;
|
||||
|
||||
if ( this._fix_host_timer ) {
|
||||
clearTimeout(this._fix_host_timer);
|
||||
this._fix_host_timer = null;
|
||||
}
|
||||
|
||||
if ( this._ffz_player_repositoner ) {
|
||||
clearInterval(this._ffz_player_repositoner);
|
||||
this._ffz_player_repositoner = null;
|
||||
}
|
||||
|
||||
utils.update_css(f._channel_style, channel_id, null);
|
||||
},
|
||||
|
||||
ffzFixHostTitle: function() {
|
||||
if ( this._fix_host_timer ) {
|
||||
clearTimeout(this._fix_host_timer);
|
||||
this._fix_host_timer = null;
|
||||
}
|
||||
|
||||
var channel = this.get('channel.hostModeTarget');
|
||||
if ( ! channel || channel.get('isLoading') )
|
||||
return;
|
||||
|
||||
var el = this.get('element'),
|
||||
container = el && el.querySelector('.cn-hosting--top .card');
|
||||
|
||||
if ( ! container ) {
|
||||
// Wait for the host bar to appear.
|
||||
this._fix_host_timer = setTimeout(this.ffzFixHostTitle.bind(this), 250);
|
||||
return;
|
||||
}
|
||||
|
||||
var old_ui = container.querySelector('.ffz.card__layout');
|
||||
if ( old_ui )
|
||||
container.removeChild(old_ui);
|
||||
|
||||
var image = utils.createElement('img'),
|
||||
figure = utils.createElement('figure', 'card__img card__img--avatar', image),
|
||||
avatar_link = utils.createElement('a', '', figure),
|
||||
|
||||
user_link = utils.createElement('a', '', utils.sanitize(channel.get('displayName'))),
|
||||
|
||||
card_title = utils.createElement('h3', 'card__title'),
|
||||
card_info = utils.createElement('p', 'card__info', 'Hosting '),
|
||||
card_body = utils.createElement('div', 'card__body', card_title),
|
||||
|
||||
layout = utils.createElement('div', 'ffz card__layout', avatar_link);
|
||||
|
||||
card_info.appendChild(user_link);
|
||||
layout.appendChild(card_body);
|
||||
card_body.appendChild(card_info);
|
||||
|
||||
container.classList.add('ffz-host-info');
|
||||
container.appendChild(layout);
|
||||
|
||||
var channel_id = channel.get('id'),
|
||||
status = channel.get('status'),
|
||||
game = channel.get('game'),
|
||||
|
||||
tokens = f.tokenize_line(channel_id, channel_id, status, true);
|
||||
|
||||
if ( game === 'Creative' )
|
||||
tokens = f.tokenize_ctags(tokens);
|
||||
|
||||
if ( game ) {
|
||||
var game_link = utils.createElement('a', '', utils.sanitize(game));
|
||||
card_info.appendChild(document.createTextNode(game === 'Creative' ? ' being ' : ' playing '));
|
||||
card_info.appendChild(game_link);
|
||||
|
||||
game_link.href = Twitch.uri.game(game);
|
||||
game_link.addEventListener('click', utils.transition_link(utils.transition_game.bind(this, game)));
|
||||
}
|
||||
|
||||
avatar_link.href = user_link.href = '/' + channel_id;
|
||||
|
||||
var user_handler = utils.transition_link(utils.transition_user.bind(this, channel_id));
|
||||
avatar_link.addEventListener('click', user_handler);
|
||||
user_link.addEventListener('click', user_handler);
|
||||
|
||||
image.src = channel.get('logo') || constants.NO_LOGO;
|
||||
card_title.innerHTML = f.render_tokens(tokens);
|
||||
|
||||
}.observes('channel.hostModeTarget', 'channel.hostModeTarget.isLoading', 'channel.hostModeTarget.id', 'channel.hostModeTarget.status', 'channel.hostModeTarget.game'),
|
||||
|
||||
ffzFixTitle: function() {
|
||||
if ( ! f.settings.stream_title )
|
||||
return;
|
||||
|
||||
var channel_id = this.get("channel.id"),
|
||||
status = this.get("channel.status"),
|
||||
game = this.get("channel.game"),
|
||||
|
||||
tokens = f.tokenize_line(channel_id, channel_id, status, true);
|
||||
|
||||
if ( game === 'Creative' )
|
||||
tokens = f.tokenize_ctags(tokens);
|
||||
|
||||
var el = this.$(".cn-metabar > div:first-child .js-card__title");
|
||||
el && el.html(f.render_tokens(tokens));
|
||||
}.observes('channel.id', 'channel.status', 'channel.game'),
|
||||
|
||||
ffzUpdateMetadata: function(key) {
|
||||
var t = this,
|
||||
keys = key ? [key] : Object.keys(FFZ.channel_metadata),
|
||||
is_hosting = !!this.get('channel.hostModeTarget'),
|
||||
basic_info = [this, this.get(is_hosting ? 'channel.hostModeTarget' : 'channel'), is_hosting, this.get('channel')],
|
||||
timers = this.ffz_timers = this.ffz_timers || {},
|
||||
|
||||
refresh_func = this.ffzUpdateMetadata.bind(this),
|
||||
|
||||
container = this.get('element'),
|
||||
metabar = container && container.querySelector(is_hosting ? '.cn-hosting--bottom' : '.cn-metabar__more');
|
||||
|
||||
// Stop once this is destroyed.
|
||||
if ( this.isDestroyed || ! metabar )
|
||||
return;
|
||||
|
||||
for(var i=0; i < keys.length; i++)
|
||||
f.render_metadata(keys[i], basic_info, metabar, timers, refresh_func, is_hosting);
|
||||
},
|
||||
|
||||
ffzUpdateHostButton: function() {
|
||||
this.set('ffz_host_updating', false);
|
||||
return this.ffzUpdateMetadata('host');
|
||||
}.observes('channel.id', 'channel.hostModeTarget.id')
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.modify_channel_redesign = function(view) {
|
||||
var f = this,
|
||||
Layout = utils.ember_lookup('service:layout');
|
||||
|
||||
utils.ember_reopen_view(view, {
|
||||
ffz_init: function() {
|
||||
// Twitch y u make me do dis
|
||||
// (If this isn't the outer channel-redesign abort)
|
||||
if ( this.parentView instanceof view )
|
||||
return;
|
||||
|
||||
var channel_id = this.get("channel.id"),
|
||||
el = this.get("element");
|
||||
|
||||
f._credesign = this;
|
||||
|
||||
this.ffzUpdateCoverHeight();
|
||||
|
||||
el.setAttribute('data-channel', channel_id);
|
||||
el.classList.add('ffz-channel-container');
|
||||
},
|
||||
|
||||
ffz_destroy: function() {
|
||||
var channel_id = this.get("channel.id"),
|
||||
el = this.get("element");
|
||||
|
||||
el.setAttribute('data-channel', '');
|
||||
el.classList.remove('ffz-channel-container');
|
||||
|
||||
if ( f._credesign === this )
|
||||
f._credesign = null;
|
||||
},
|
||||
|
||||
ffzUpdateCoverHeight: function() {
|
||||
var old_height = this.channelCoverHeight,
|
||||
setting = f.settings.hide_channel_banner,
|
||||
banner_hidden = setting === 1 ? f.settings.channel_bar_bottom : setting > 0,
|
||||
|
||||
new_height = banner_hidden ? 0 : 380;
|
||||
|
||||
this.channelCoverHeight = new_height;
|
||||
this.$("#channel").toggleClass('ffz-bar-fixed', this.get('isFixed'));
|
||||
|
||||
if ( this.$scrollContainer && old_height !== new_height )
|
||||
this.scrollTo(this.$scrollContainer.scrollTop() + (new_height - old_height));
|
||||
|
||||
if ( this.updatePlayerPosition )
|
||||
setTimeout(this.updatePlayerPosition.bind(this));
|
||||
|
||||
}.observes('isFixed')
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// ---------------
|
||||
// Settings
|
||||
// ---------------
|
||||
|
||||
FFZ.settings_info.auto_theater = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
|
||||
category: "Appearance",
|
||||
no_mobile: true,
|
||||
no_bttv: true,
|
||||
|
||||
name: "Automatic Theater Mode",
|
||||
help: "Automatically enter theater mode when opening a channel."
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.chatter_count = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
no_mobile: true,
|
||||
|
||||
category: "Channel Metadata",
|
||||
|
||||
name: "Chatter Count",
|
||||
help: "Display the current number of users connected to chat beneath the channel.",
|
||||
|
||||
on_update: function(val) {
|
||||
if ( ! this.rooms )
|
||||
return;
|
||||
|
||||
// Refresh the data.
|
||||
for(var room_id in this.rooms) {
|
||||
var r = this.rooms[room_id] && this.rooms[room_id].room;
|
||||
r && r.ffzInitChatterCount();
|
||||
}
|
||||
|
||||
if ( this._cindex )
|
||||
this._cindex.ffzUpdateMetadata('chatters');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.channel_views = {
|
||||
type: "boolean",
|
||||
value: true,
|
||||
no_mobile: true,
|
||||
|
||||
category: "Channel Metadata",
|
||||
name: "Channel Views",
|
||||
help: 'Display the number of times the channel has been viewed beneath the stream.',
|
||||
on_update: function(val) {
|
||||
document.body.classList.toggle("ffz-hide-view-count", !val);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.hosted_channels = {
|
||||
type: "boolean",
|
||||
value: true,
|
||||
no_mobile: true,
|
||||
|
||||
category: "Channel Metadata",
|
||||
name: "Channel Hosting",
|
||||
help: "Display other channels that have been featured by the current channel.",
|
||||
on_update: function(val) {
|
||||
var cb = document.querySelector('input.ffz-setting-hosted-channels');
|
||||
if ( cb )
|
||||
cb.checked = val;
|
||||
|
||||
var Chat = utils.ember_lookup('controller:chat'),
|
||||
room = Chat && Chat.get('currentChannelRoom');
|
||||
|
||||
if ( room )
|
||||
room.setHostMode({
|
||||
hostTarget: room.ffz_host_target,
|
||||
recentlyJoined: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.stream_host_button = {
|
||||
type: "boolean",
|
||||
value: true,
|
||||
no_mobile: true,
|
||||
|
||||
category: "Channel Metadata",
|
||||
name: "Host This Channel Button",
|
||||
help: "Display a button underneath streams that make it easy to host them with your own channel.",
|
||||
on_update: function(val) {
|
||||
if ( this._cindex )
|
||||
this._cindex.ffzUpdateHostButton();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.stream_uptime = {
|
||||
type: "select",
|
||||
options: {
|
||||
0: "Disabled",
|
||||
1: "Enabled",
|
||||
2: "Enabled (with Seconds)",
|
||||
3: "Enabled (Channel Only)",
|
||||
4: "Enabled (Channel Only with Seconds)"
|
||||
},
|
||||
|
||||
value: 1,
|
||||
process_value: utils.process_int(1, 0, 2),
|
||||
|
||||
no_mobile: true,
|
||||
category: "Channel Metadata",
|
||||
name: "Stream Uptime",
|
||||
help: 'Display the stream uptime under a channel by the viewer count.',
|
||||
on_update: function(val) {
|
||||
if ( this._cindex )
|
||||
this._cindex.ffzUpdateMetadata('uptime');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.stream_title = {
|
||||
type: "boolean",
|
||||
value: true,
|
||||
no_bttv: 6,
|
||||
no_mobile: true,
|
||||
|
||||
category: "Channel Metadata",
|
||||
name: "Title Links",
|
||||
help: "Make links in stream titles clickable.",
|
||||
on_update: function(val) {
|
||||
if ( this._cindex )
|
||||
this._cindex.ffzFixTitle();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.channel_bar_bottom = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
no_bttv: true,
|
||||
no_mobile: true,
|
||||
|
||||
category: "Appearance",
|
||||
name: "Channel Bar on Bottom",
|
||||
help: "Hide the profile banner and position the channel bar at the bottom of the screen.",
|
||||
|
||||
on_update: function(val) {
|
||||
if ( this.has_bttv )
|
||||
return;
|
||||
|
||||
var banner_hidden = this.settings.hide_channel_banner;
|
||||
banner_hidden = banner_hidden === 1 ? val : banner_hidden > 0;
|
||||
|
||||
utils.toggle_cls('ffz-channel-bar-bottom')(val);
|
||||
utils.toggle_cls('ffz-hide-channel-banner')(banner_hidden);
|
||||
|
||||
if ( this._credesign )
|
||||
this._credesign.ffzUpdateCoverHeight();
|
||||
|
||||
var Layout = utils.ember_lookup('service:layout');
|
||||
if ( Layout )
|
||||
Ember.propertyDidChange(Layout, 'ffzExtraHeight');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
FFZ.settings_info.hide_channel_banner = {
|
||||
type: "select",
|
||||
options: {
|
||||
0: "Never",
|
||||
1: "When Channel Bar is on Bottom",
|
||||
2: "Always"
|
||||
},
|
||||
|
||||
value: 1,
|
||||
process_value: utils.process_int(1),
|
||||
|
||||
no_bttv: true,
|
||||
no_mobile: true,
|
||||
|
||||
category: "Appearance",
|
||||
name: "Hide Channel Banner",
|
||||
help: "Hide the banner at the top of channel pages.",
|
||||
|
||||
on_update: function(val) {
|
||||
if ( this.has_bttv )
|
||||
return;
|
||||
|
||||
var is_hidden = val === 1 ? this.settings.channel_bar_bottom : val > 0;
|
||||
utils.toggle_cls('ffz-hide-channel-banner')(is_hidden);
|
||||
if ( this._credesign )
|
||||
this._credesign.ffzUpdateCoverHeight();
|
||||
|
||||
var Layout = utils.ember_lookup('service:layout');
|
||||
if ( Layout )
|
||||
Ember.propertyDidChange(Layout, 'ffzExtraHeight');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
FFZ.settings_info.channel_bar_collapse = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
no_bttv: true,
|
||||
no_mobile: true,
|
||||
|
||||
category: "Appearance",
|
||||
name: "Minimal Channel Bar",
|
||||
help: "Slide the channel bar mostly out of view when it's not being used.",
|
||||
|
||||
on_update: function(val) {
|
||||
if ( this.has_bttv )
|
||||
return;
|
||||
|
||||
utils.toggle_cls('ffz-minimal-channel-bar')(val);
|
||||
|
||||
var Layout = utils.ember_lookup('service:layout');
|
||||
if ( Layout )
|
||||
Ember.propertyDidChange(Layout, 'ffzExtraHeight');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
FFZ.settings_info.channel_title_top = {
|
||||
type: "select",
|
||||
options: {
|
||||
0: "Disabled",
|
||||
1: "On Top",
|
||||
2: "On Top, Minimal"
|
||||
},
|
||||
|
||||
value: 0,
|
||||
process_value: utils.process_int(0),
|
||||
|
||||
no_bttv: true,
|
||||
no_mobile: true,
|
||||
|
||||
category: "Appearance",
|
||||
name: "Channel Title on Top",
|
||||
help: "Display the channel title and game above the player rather than below.",
|
||||
|
||||
on_update: function(val) {
|
||||
if ( this.has_bttv )
|
||||
return;
|
||||
|
||||
document.body.classList.toggle('ffz-minimal-channel-title', val === 2);
|
||||
document.body.classList.toggle('ffz-channel-title-top', val > 0);
|
||||
|
||||
var Layout = utils.ember_lookup('service:layout');
|
||||
if ( Layout )
|
||||
Ember.propertyDidChange(Layout, 'ffzExtraHeight');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
FFZ.settings_info.theater_stats = {
|
||||
type: "select",
|
||||
options: {
|
||||
0: "Disabled",
|
||||
1: "Basic",
|
||||
2: "Full"
|
||||
},
|
||||
|
||||
value: 2,
|
||||
process_value: utils.process_int(2, 0, 2),
|
||||
|
||||
no_mobile: true,
|
||||
|
||||
category: "Channel Metadata",
|
||||
name: "Display on Theater Mode Hover",
|
||||
help: "Show the channel metadata and actions over the video player in theater mode when you hover it with your mouse.",
|
||||
|
||||
on_update: function(val) {
|
||||
document.body.classList.toggle('ffz-theater-stats', val === 2);
|
||||
document.body.classList.toggle('ffz-theater-basic-stats', val > 0);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.basic_settings.channel_info = {
|
||||
type: "select",
|
||||
options: {
|
||||
0: "Disabled",
|
||||
1: "Enabled",
|
||||
2: "Enabled (with Seconds)",
|
||||
3: "Enabled (Channel Only)",
|
||||
4: "Enabled (Channel Only with Seconds)"
|
||||
},
|
||||
|
||||
category: "General",
|
||||
name: "Stream Uptime",
|
||||
help: "Display the current stream's uptime under the player.",
|
||||
|
||||
get: function() {
|
||||
return this.settings.stream_uptime;
|
||||
},
|
||||
|
||||
set: function(val) {
|
||||
if ( typeof val === 'string' )
|
||||
val = parseInt(val || "0");
|
||||
|
||||
this.settings.set('stream_uptime', val);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,330 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ,
|
||||
utils = require('../utils'),
|
||||
constants = require('../constants');
|
||||
|
||||
|
||||
// --------------------
|
||||
// Settings
|
||||
// --------------------
|
||||
|
||||
FFZ.settings_info.show_commerce = {
|
||||
type: "select",
|
||||
options: {
|
||||
0: "Never",
|
||||
1: "When Revenue is Shared",
|
||||
2: "Always"
|
||||
},
|
||||
|
||||
value: 2,
|
||||
process_value: utils.process_int(0),
|
||||
|
||||
no_mobile: true,
|
||||
|
||||
category: "Commerce",
|
||||
name: "Display Commerce Bar",
|
||||
help: "Show the commerce bar under channels that allows you to purchase supported games.",
|
||||
|
||||
on_update: function(val) {
|
||||
utils.toggle_cls('ffz-hide-purchase-game')(val === 0);
|
||||
var views = utils.ember_views(),
|
||||
ChannelBox = utils.ember_resolve('component:commerce/channel-box');
|
||||
|
||||
if ( ! ChannelBox )
|
||||
return;
|
||||
|
||||
for(var key in views)
|
||||
if ( views[key] instanceof ChannelBox )
|
||||
try {
|
||||
views[key].ffzUpdateVisibility();
|
||||
} catch(err) { }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
FFZ.settings_info.show_itad = {
|
||||
type: "boolean",
|
||||
value: true,
|
||||
|
||||
no_mobile: true,
|
||||
|
||||
category: "Commerce",
|
||||
name: "Display Competitor Pricing",
|
||||
help: "Add a button on the commerce bar with pricing from other stores.",
|
||||
|
||||
on_update: function(val) {
|
||||
var views = utils.ember_views(),
|
||||
BuyGameNow = utils.ember_resolve('component:commerce/buy-game-now');
|
||||
|
||||
if ( ! BuyGameNow )
|
||||
return;
|
||||
|
||||
for(var key in views)
|
||||
if ( views[key] instanceof BuyGameNow )
|
||||
try {
|
||||
views[key].ffzRenderPricing();
|
||||
} catch(err) { }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --------------------
|
||||
// Initialization
|
||||
// --------------------
|
||||
|
||||
FFZ.prototype.setup_commerce = function() {
|
||||
this._itad_game_to_plain = {};
|
||||
|
||||
// Styles
|
||||
utils.toggle_cls('ffz-hide-purchase-game')(this.settings.show_commerce === 0);
|
||||
|
||||
// Ember Modifications
|
||||
this.update_views('component:commerce/channel-box', this.modify_commerce_box);
|
||||
this.update_views('component:commerce/buy-game-now', this.modify_buy_game_now);
|
||||
}
|
||||
|
||||
|
||||
// --------------------
|
||||
// Modifications
|
||||
// --------------------
|
||||
|
||||
FFZ.prototype.modify_commerce_box = function(view) {
|
||||
var f = this;
|
||||
utils.ember_reopen_view(view, {
|
||||
ffz_init: function() {
|
||||
this.ffzUpdateVisibility();
|
||||
},
|
||||
|
||||
ffzUpdateVisibility: function() {
|
||||
var el = this.parentView.get('element'),
|
||||
real_el = el && el.querySelector('.cmrc-channel-box');
|
||||
|
||||
if ( ! real_el )
|
||||
! this.isDestroyed && setTimeout(this.ffzUpdateVisibility.bind(this), 250);
|
||||
else
|
||||
real_el.classList.toggle('hidden', f.settings.show_commerce === 1 && ! this.get('showSupports'));
|
||||
|
||||
}.observes('showSupports')
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.modify_buy_game_now = function(view) {
|
||||
var f = this;
|
||||
utils.ember_reopen_view(view, {
|
||||
itad_plain: null,
|
||||
itad_price: null,
|
||||
itad_country: null,
|
||||
|
||||
ffz_init: function() {
|
||||
if ( ! this.$().parents('.cmrc-game-details-box,.cmrc-channel-box').length || ! this.$('button').length )
|
||||
return;
|
||||
|
||||
//f.log("Buy-Game-New Component", this);
|
||||
this.itad_count = 0;
|
||||
this.ffzUpdateITADPlain();
|
||||
|
||||
var t = this;
|
||||
f.get_location().then(function(data) {
|
||||
t.set('itad_country', data && data.country);
|
||||
});
|
||||
},
|
||||
|
||||
ffzTitle: function() {
|
||||
// Do this because Twitch's ToS say you're not allowed to use data collected from
|
||||
// the API to show users commercial offers. This scrapes the game title from the
|
||||
// page itself and not from any kind of JS API.
|
||||
|
||||
// Granted, they're probably more worried about automated chat spam and people
|
||||
// sending spam to email addresses recovered from authenticated user profile requests.
|
||||
|
||||
var el;
|
||||
if ( document.body.dataset.currentPath === 'directory.game-details' )
|
||||
el = document.querySelector('.game-details__page-title');
|
||||
else
|
||||
el = document.querySelector('.js-card__info [data-tt_content="current_game"]');
|
||||
|
||||
var output = el ? _.pluck(_.filter(el.childNodes, function(x) { return x.nodeType === document.TEXT_NODE }), 'textContent').join(' ').trim() : null;
|
||||
|
||||
if ( ! output && this.itad_count < 50 ) {
|
||||
var t = this;
|
||||
setTimeout(function() {
|
||||
t.itad_count += 1;
|
||||
Ember.propertyDidChange(this, 'ffzTitle');
|
||||
}, 250);
|
||||
}
|
||||
|
||||
return output;
|
||||
|
||||
}.property(),
|
||||
|
||||
didReceiveAttrs: function() {
|
||||
this._super();
|
||||
Ember.propertyDidChange(this, 'ffzTitle');
|
||||
},
|
||||
|
||||
ffzUpdateITADPlain: function() {
|
||||
var title = this.get('ffzTitle'),
|
||||
old_plain = this.get('itad_plain'),
|
||||
plain = f._itad_game_to_plain[title] || null;
|
||||
|
||||
//f.log("Update ITAD Plain: " + title + " -- [" + plain + "]", this);
|
||||
|
||||
// If we already have the value, fetch it now.
|
||||
if ( ! title || plain ) {
|
||||
if ( old_plain !== plain )
|
||||
this.set('itad_plain', plain);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( old_plain )
|
||||
this.set('itad_plain', null);
|
||||
|
||||
var t = this;
|
||||
f.ws_send("get_itad_plain", title, function(success, data) {
|
||||
if ( ! success ) return;
|
||||
|
||||
f._itad_game_to_plain[title] = data;
|
||||
t.ffzUpdateITADPlain();
|
||||
}, true);
|
||||
|
||||
}.observes('ffzTitle'),
|
||||
|
||||
ffzUpdateITADPrice: function() {
|
||||
var t = this,
|
||||
old_price = this.get('itad_price'),
|
||||
country = this.get('itad_country'),
|
||||
plain = this.get('itad_plain');
|
||||
|
||||
if ( old_price && old_price[0] === plain )
|
||||
return;
|
||||
|
||||
if ( ! plain || ! country )
|
||||
return this.set('itad_price', null);
|
||||
|
||||
this.set('itad_price', [plain, null]);
|
||||
f.ws_send("get_itad_prices", [plain, country], function(success, data) {
|
||||
if ( ! success ) return;
|
||||
|
||||
t.set('itad_price', [plain, data]);
|
||||
});
|
||||
|
||||
}.observes('itad_plain', 'itad_country'),
|
||||
|
||||
|
||||
ffzRenderPricing: function() {
|
||||
var t = this,
|
||||
el = this.get('element'),
|
||||
cont = el && el.querySelector('.ffz-price-info'),
|
||||
btn_price,
|
||||
data = this.get('itad_price');
|
||||
|
||||
if ( ! f.settings.show_itad || ! data || ! data[1] || ! data[1].list || ! data[1].list.length ) {
|
||||
if ( cont )
|
||||
jQuery(cont).remove();
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! cont ) {
|
||||
cont = utils.createElement('div', 'ffz-price-info mg-l-1 balloon-wrapper');
|
||||
btn_price = utils.createElement('span', 'ffz-price-num button__num-block pd-x-1 mg-1-0');
|
||||
|
||||
var btn = utils.createElement('button', 'button itad-button button--dropmenu',
|
||||
utils.createElement('span', 'ffz-price-label inline-block pd-r-1', 'ITAD'));
|
||||
|
||||
btn.appendChild(btn_price);
|
||||
cont.appendChild(btn);
|
||||
el.appendChild(cont);
|
||||
|
||||
btn.addEventListener('click', function(event) {
|
||||
t.ffzRenderPopup(event);
|
||||
});
|
||||
|
||||
} else
|
||||
btn_price = cont.querySelector('.ffz-price-num');
|
||||
|
||||
// Determine the cheapest price.
|
||||
var sales = data[1].list,
|
||||
cheapest = sales[0].price_new,
|
||||
|
||||
currency = data[1].currency,
|
||||
formatter = new Intl.NumberFormat(undefined, (currency && currency.code) ? {style: 'currency', currency: currency.code, minimumFractionDigits: 2} : {minimumFractionDigits: 2});
|
||||
|
||||
btn_price.textContent = formatter.format(cheapest);
|
||||
|
||||
}.observes('itad_price'),
|
||||
|
||||
ffzRenderPopup: function(e) {
|
||||
if ( e.button !== 0 || e.altKey || e.ctrlKey || e.shiftKey || e.metaKey )
|
||||
return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
var popup = f._popup ? f.close_popup() : f._last_popup,
|
||||
t = this,
|
||||
data = t.get('itad_price'),
|
||||
el = this.get('element'),
|
||||
has_support = el && el.parentElement && (el.parentElement.querySelector('.cmrc-channel-box__support') || el.parentElement.querySelector('.cmrc-game-detail-box__support')),
|
||||
cont = el && el.querySelector('.ffz-price-info');
|
||||
|
||||
if ( popup && popup.id === 'ffz-price-popup' || ! data || ! data[1] || ! data[1].list || ! data[1].list.length )
|
||||
return;
|
||||
|
||||
var balloon = utils.createElement('div', 'itad-balloon balloon balloon--md show', '<table><thead><tr><th>Store</th><th>Price Cut</th><th>Current</th><th>Regular</th></tr></thead><tbody></tbody></table>'),
|
||||
tbody = balloon.querySelector('tbody');
|
||||
|
||||
balloon.id = 'ffz-price-popup';
|
||||
|
||||
// Render the table.
|
||||
|
||||
var currency = data[1].currency,
|
||||
formatter = new Intl.NumberFormat(undefined, (currency && currency.code) ? {style: 'currency', currency: currency.code, minimumFractionDigits: 2} : {minimumFractionDigits: 2});
|
||||
|
||||
var sales = data[1].list;
|
||||
for(var i=0; i < sales.length; i++) {
|
||||
var entry = sales[i],
|
||||
row = utils.createElement('tr');
|
||||
|
||||
row.innerHTML = '<td><a class="store-link" rel="noreferrer" target="_blank" href="' + utils.quote_san(entry.url) + '">' + utils.sanitize(entry.shop.name) + '</a></td>' +
|
||||
'<td>' + (entry.price_cut < 0 ? '' : '-') + utils.sanitize(entry.price_cut) + '%</td>' +
|
||||
'<td>' + formatter.format(entry.price_new) + '</td>' +
|
||||
'<td>' + formatter.format(entry.price_old) + '</td>';
|
||||
|
||||
tbody.appendChild(row);
|
||||
}
|
||||
|
||||
if ( has_support )
|
||||
jQuery('.store-link', tbody).click(function(e) {
|
||||
var name = has_support.querySelector('strong').textContent,
|
||||
link_text = e.target.textContent;
|
||||
|
||||
if ( ! confirm("By following this link and purchasing from " + link_text + " you will NOT be supporting " + name + ".\n\nAre you sure you wish to contune?") )
|
||||
return false;
|
||||
});
|
||||
|
||||
// Add a by-line for IsThereAnyDeal.
|
||||
|
||||
var url = data[1].urls && data[1].urls.game || "https://isthereanydeal.com",
|
||||
by_line = utils.createElement('span', 'ffz-attributions',
|
||||
'<hr>Source: <a rel="noreferrer" target="_blank" href="' + utils.quote_san(url) + '">IsThereAnyDeal.com</a><br><br>Any affiliate links in the provided data are the responsibility of IsThereAnyDeal and do not benefit FrankerFaceZ. You may consider visiting the store directly.' +
|
||||
'<hr>Reminder: When you buy a game from other services, you miss out on the benefits of purchasing from Twitch directly including: supporting partnered streamers and earning <a target="_blank" href="https://blog.twitch.tv/twitch-crates-are-coming-soon-f50fa0cd4cdf">Twitch Crates</a> containing emotes and badges.');
|
||||
|
||||
balloon.appendChild(by_line);
|
||||
|
||||
// Now calculate the position and add the balloon to the DOM.
|
||||
|
||||
var container = document.querySelector('#main_col'),
|
||||
outer = container.getBoundingClientRect(),
|
||||
rect = cont.getBoundingClientRect();
|
||||
|
||||
var is_up = (rect.top - outer.top) > (outer.bottom - rect.bottom);
|
||||
balloon.classList.add('balloon--' + (is_up ? 'up' : 'down'));
|
||||
balloon.classList.toggle('balloon--right', (rect.left - outer.left) > (outer.right - rect.right));
|
||||
|
||||
f._popup_allow_parent = true;
|
||||
f._popup = balloon;
|
||||
|
||||
cont.appendChild(balloon);
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,271 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ,
|
||||
utils = require('../utils'),
|
||||
constants = require('../constants'),
|
||||
|
||||
createElement = utils.createElement;
|
||||
|
||||
|
||||
// ---------------
|
||||
// Settings
|
||||
// ---------------
|
||||
|
||||
FFZ.settings_info.conv_focus_on_click = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
no_mobile: true,
|
||||
visible: false,
|
||||
|
||||
category: "Whispers",
|
||||
name: "Focus Input on Click",
|
||||
help: "Focus on a conversation's input box when you click it."
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.top_conversations = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
no_mobile: true,
|
||||
|
||||
category: "Whispers",
|
||||
name: "Position on Top",
|
||||
help: "Display the whisper UI at the top of the window instead of the bottom.",
|
||||
on_update: function(val) {
|
||||
document.body.classList.toggle('ffz-top-conversations', val);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.hide_whispers_in_embedded_chat = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
no_bttv: 6,
|
||||
|
||||
category: "Whispers",
|
||||
name: "Hide Whispers in Embedded Chat",
|
||||
help: "Do not display whispers on the dashboard, in pop-out chat, or in chat embedded into other websites.",
|
||||
|
||||
on_update: function(val) {
|
||||
if ( ! val || this.has_bttv_6 )
|
||||
return;
|
||||
|
||||
for(var room_id in this.rooms) {
|
||||
var room = this.rooms[room_id].room;
|
||||
if ( ! room )
|
||||
continue;
|
||||
|
||||
var messages = room.get('messages'),
|
||||
length = messages && messages.length || 0,
|
||||
i = length;
|
||||
|
||||
while(--i >= 0) {
|
||||
if ( messages[i] && messages[i].style === 'whisper' )
|
||||
messages.removeAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.hide_conversations_in_theatre = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
no_mobile: true,
|
||||
|
||||
category: "Whispers",
|
||||
name: "Hide Whispers in Theater Mode",
|
||||
help: "Hide the whisper UI when the page is in theater mode.",
|
||||
on_update: function(val) {
|
||||
document.body.classList.toggle('ffz-theatre-conversations', val);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.minimize_conversations = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
no_mobile: true,
|
||||
|
||||
category: "Whispers",
|
||||
name: "Minimize Whisper UI",
|
||||
help: "Slide the whisper UI mostly out of view when it's not being used and you have no unread messages.",
|
||||
on_update: function(val) {
|
||||
document.body.classList.toggle('ffz-minimize-conversations', val);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// ---------------
|
||||
// Initialization
|
||||
// ---------------
|
||||
|
||||
FFZ.prototype.setup_conversations = function() {
|
||||
document.body.classList.toggle('ffz-top-conversations', this.settings.top_conversations);
|
||||
document.body.classList.toggle('ffz-minimize-conversations', this.settings.minimize_conversations);
|
||||
document.body.classList.toggle('ffz-theatre-conversations', this.settings.hide_conversations_in_theatre);
|
||||
|
||||
this.update_views('component:twitch-conversations/conversation-window', this.modify_conversation_window);
|
||||
this.update_views('component:twitch-conversations/conversation-settings-menu', this.modify_conversation_menu);
|
||||
this.update_views('component:twitch-conversations/conversation-line', this.modify_conversation_line);
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.modify_conversation_menu = function(component) {
|
||||
var f = this;
|
||||
utils.ember_reopen_view(component, {
|
||||
ffz_init: function() {
|
||||
var user = this.get('thread.otherUsername'),
|
||||
el = this.get('element'),
|
||||
sections = el && el.querySelectorAll('.options-section');
|
||||
|
||||
if ( ! user || ! user.length || f.has_bttv_6 )
|
||||
return;
|
||||
|
||||
if ( sections && sections.length )
|
||||
el.appendChild(createElement('div', 'options-divider'));
|
||||
|
||||
var ffz_options = createElement('div', 'options-section'),
|
||||
card_link = createElement('a', 'ffz-show-card', "Open Moderation Card");
|
||||
|
||||
card_link.addEventListener('click', function(e) {
|
||||
el.parentElement.classList.add('hidden');
|
||||
FFZ.chat_commands.card.call(f, null, [user]);
|
||||
});
|
||||
|
||||
ffz_options.appendChild(card_link);
|
||||
el.appendChild(ffz_options);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.modify_conversation_window = function(component) {
|
||||
var f = this,
|
||||
Layout = utils.ember_lookup('service:layout');
|
||||
|
||||
utils.ember_reopen_view(component, {
|
||||
headerBadges: Ember.computed("thread.participants", "currentUsername", function() {
|
||||
return [];
|
||||
}),
|
||||
|
||||
ffzHeaderBadges: Ember.computed("thread.participants", "currentUsername", function() {
|
||||
var e = this.get("otherUser");
|
||||
return f.get_badges(e.get('username'), null, f.get_twitch_badges(e.get('badges')), null);
|
||||
}),
|
||||
|
||||
ffzReplaceBadges: function() {
|
||||
var el = this.get('element'),
|
||||
badge_el = el && el.querySelector('.badges'),
|
||||
badges = this.get('ffzHeaderBadges');
|
||||
|
||||
if ( ! el )
|
||||
return;
|
||||
|
||||
if ( ! badge_el ) {
|
||||
badge_el = createElement('span', 'badges');
|
||||
var header = el && el.querySelector('.convoHeader .username');
|
||||
if ( ! header )
|
||||
return;
|
||||
|
||||
header.insertBefore(badge_el, header.firstChild);
|
||||
}
|
||||
|
||||
badge_el.innerHTML = f.render_badges(badges);
|
||||
}.observes('ffzHeaderBadges'),
|
||||
|
||||
ffz_init: function() {
|
||||
var el = this.get('element'),
|
||||
header = el && el.querySelector('.convoHeader'),
|
||||
header_name = header && header.querySelector('.username'),
|
||||
|
||||
raw_color = this.get('otherUser.color'),
|
||||
colors = raw_color && f._handle_color(raw_color),
|
||||
|
||||
is_dark = (Layout && Layout.get('isTheatreMode')) || f.settings.dark_twitch;
|
||||
|
||||
this.ffzReplaceBadges();
|
||||
|
||||
if ( header_name && raw_color ) {
|
||||
header_name.style.color = (is_dark ? colors[1] : colors[0]);
|
||||
header_name.classList.add('has-color');
|
||||
header_name.setAttribute('data-color', raw_color);
|
||||
}
|
||||
|
||||
jQuery('.badge', el).zipsy({gravity: utils.newtip_placement(constants.TOOLTIP_DISTANCE, 'n')});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.modify_conversation_line = function(component) {
|
||||
var f = this,
|
||||
Layout = utils.ember_lookup('service:layout');
|
||||
|
||||
utils.ember_reopen_view(component, {
|
||||
tokenizedMessage: function() {
|
||||
try {
|
||||
return f.tokenize_conversation_line(this.get('message'));
|
||||
} catch(err) {
|
||||
f.error("convo-line tokenizedMessage: " + err);
|
||||
return this._super();
|
||||
}
|
||||
|
||||
}.property("message", "currentUsername"),
|
||||
|
||||
click: function(e) {
|
||||
if ( e.target && e.target.classList.contains('deleted-link') )
|
||||
return f._deleted_link_click.call(e.target, e);
|
||||
|
||||
if ( f._click_emote(e.target, e) )
|
||||
return;
|
||||
|
||||
return this._super(e);
|
||||
},
|
||||
|
||||
didUpdate: function() { this.ffzRender() },
|
||||
ffz_init: function() { this.ffzRender() },
|
||||
|
||||
ffzRender: function() {
|
||||
var el = this.get('element'),
|
||||
e = [],
|
||||
|
||||
username = this.get('message.from.username').toLowerCase(),
|
||||
raw_display = this.get('message.from.displayName'),
|
||||
alias = f.aliases[username],
|
||||
|
||||
raw_color = this.get('message.from.color'),
|
||||
|
||||
is_dark = (Layout && Layout.get('isTheatreMode')) || f.settings.dark_twitch,
|
||||
colors = raw_color && f._handle_color(raw_color),
|
||||
|
||||
style = colors ? 'color:' + (is_dark ? colors[1] : colors[0]) : '',
|
||||
colored = colors ? ' has-color' : '',
|
||||
|
||||
results = f.format_display_name(raw_display, username),
|
||||
|
||||
myself = f.get_user(),
|
||||
from_me = myself && myself.login === username;
|
||||
|
||||
|
||||
e.push('<span class="from' +
|
||||
(alias ? ' ffz-alias' : '') +
|
||||
(results[1] ? ' html-tooltip' : '') +
|
||||
colored +
|
||||
'" style="' + style + '"' +
|
||||
(colors ? ' data-color="' + raw_color + '"' : '') +
|
||||
(results[1] ? ' title="' + utils.quote_attr(results[1]) + '"' : '') +
|
||||
'>' + results[0] + '</span>');
|
||||
|
||||
e.push('<span class="colon">:</span> ');
|
||||
|
||||
if ( ! this.get('isActionMessage') ) {
|
||||
style = '';
|
||||
colored = '';
|
||||
}
|
||||
|
||||
e.push('<span class="message' + colored + '" style="' + style + (colors ? '" data-color="' + raw_color : '') + '">');
|
||||
e.push(f.render_tokens(this.get('tokenizedMessage'), true, f.settings.filter_whispered_links && !from_me));
|
||||
e.push('</span>');
|
||||
el.innerHTML = e.join('');
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ,
|
||||
utils = require('../utils'),
|
||||
constants = require('../constants');
|
||||
|
||||
|
||||
// --------------------
|
||||
// Settings
|
||||
// --------------------
|
||||
|
||||
FFZ.settings_info.dash_widget_click_to_expand = {
|
||||
type: "boolean",
|
||||
value: true,
|
||||
|
||||
category: "Dashboard",
|
||||
name: "Click-to-Expand Widgets",
|
||||
help: "Expand and contract widgets when you click anywhere in their header, not just when you click the toggle button."
|
||||
}
|
||||
|
||||
|
||||
// --------------------
|
||||
// Initialization
|
||||
// --------------------
|
||||
|
||||
FFZ.prototype.setup_dashboard = function() {
|
||||
// Standalone Mode
|
||||
if ( location.search === '?standalone' )
|
||||
utils.toggle_cls('ffz-minimal-dashboard')(true);
|
||||
|
||||
this.try_modify_dashboard();
|
||||
|
||||
//this.update_views('component:dashboards/live/stream-stats', this.modify_dashboard_stats);
|
||||
//this.update_views('component:dashboards/live/stream-health', this.modify_dashboard_health);
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.try_modify_dashboard = function() {
|
||||
if ( this._dashboard_modified )
|
||||
return;
|
||||
|
||||
var loaded = window.features && window.features.includes('dashboard');
|
||||
if ( ! loaded )
|
||||
return this.log('Dashboard still not loaded.');
|
||||
|
||||
this._dashboard_modified = true;
|
||||
this.update_views('component:dashboards/live-widget', this.modify_dashboard_widget);
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.modify_dashboard_widget = function(component) {
|
||||
var f = this;
|
||||
utils.ember_reopen_view(component, {
|
||||
ffz_init: function() {
|
||||
var t = this;
|
||||
this.$(".dash-widget__header").click(function(e) {
|
||||
if ( ! f.settings.dash_widget_click_to_expand || e.target.tagName === 'button' || jQuery(e.target).parents('button').length || jQuery(e.target).parents('.balloon-wrapper').length )
|
||||
return;
|
||||
|
||||
t.actions.collapseWidget.call(t);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,793 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ,
|
||||
utils = require('../utils'),
|
||||
constants = require('../constants');
|
||||
|
||||
|
||||
// --------------------
|
||||
// Settings
|
||||
// --------------------
|
||||
|
||||
/*FFZ.settings_info.directory_creative_all_tags = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
|
||||
category: "Directory",
|
||||
no_mobile: true,
|
||||
|
||||
name: "Display All Creative Tags",
|
||||
help: "Alter the creative tags display to list them all in a cloud rather than having to scroll.",
|
||||
|
||||
on_update: function(val) {
|
||||
document.body.classList.toggle('ffz-creative-tags', val);
|
||||
}
|
||||
};*/
|
||||
|
||||
|
||||
FFZ.settings_info.directory_creative_showcase = {
|
||||
type: "boolean",
|
||||
value: true,
|
||||
|
||||
category: "Directory",
|
||||
no_mobile: true,
|
||||
|
||||
name: "Creative Showcase",
|
||||
help: "Display the showcase on the Creative directory page.",
|
||||
|
||||
on_update: function(val) {
|
||||
document.body.classList.toggle('ffz-creative-showcase', val);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.directory_logos = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
|
||||
category: "Directory",
|
||||
no_mobile: true,
|
||||
|
||||
name: "Channel Logos",
|
||||
help: "Display channel logos in the Twitch directory."
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.directory_group_hosts = {
|
||||
type: "boolean",
|
||||
value: true,
|
||||
|
||||
category:"Directory",
|
||||
no_mobile: true,
|
||||
|
||||
name: "Group Hosts",
|
||||
help: "Only show a given hosted channel once in the directory.",
|
||||
|
||||
on_update: function() {
|
||||
var f = this,
|
||||
HostModel = utils.ember_resolve('model:host'),
|
||||
Following = HostModel && HostModel.collections[HostModel.collectionId("following")];
|
||||
|
||||
if ( ! Following )
|
||||
return;
|
||||
|
||||
Following.clear();
|
||||
Following.load();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.enable_recommended_vods = {
|
||||
type: "boolean",
|
||||
value: true,
|
||||
|
||||
category: "Directory",
|
||||
no_mobile: true,
|
||||
experiment_warn: true,
|
||||
|
||||
name: 'Show Twitch\'s Recommended Videos',
|
||||
help: 'Show the "Based on your Viewing History" section of the directory rather than <nobr>Most Recent Videos.</nobr>',
|
||||
|
||||
on_update: function(val) {
|
||||
Ember.propertyDidChange(utils.ember_lookup('service:vod-coviews'), 'areVodsViewable');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
FFZ.settings_info.recommended_above_hosts = {
|
||||
type: "boolean",
|
||||
value: function() { var s = utils.ember_lookup('service:vod-coviews'); return s && s.get('isFollowingAboveHost') },
|
||||
|
||||
category: "Directory",
|
||||
no_mobile: true,
|
||||
experiment_warn: true,
|
||||
|
||||
name: "Show Twitch's Recommended Videos above Hosts",
|
||||
help: 'Enable this to place the "Based on your Viewing History" section above Live Hosts.',
|
||||
|
||||
on_update: function(val) {
|
||||
Ember.propertyDidChange(utils.ember_lookup('service:vod-coviews'), 'isFollowingAboveHost');
|
||||
//utils.ember_lookup('service:vod-coviews').set('isFollowingAboveHost', val);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
FFZ.settings_info.banned_games = {
|
||||
visible: false,
|
||||
value: [],
|
||||
|
||||
on_update: function() {
|
||||
var banned = this.settings.banned_games,
|
||||
els = document.querySelectorAll('.ffz-directory-preview'),
|
||||
SidebarThing = utils.ember_resolve('component:social-column/followed-channel'),
|
||||
views = utils.ember_views();
|
||||
|
||||
if ( SidebarThing )
|
||||
for(var key in views)
|
||||
if ( views[key] instanceof SidebarThing )
|
||||
try {
|
||||
views[key].ffzUpdateVisibility();
|
||||
} catch(err) { }
|
||||
|
||||
for(var i=0; i < els.length; i++) {
|
||||
var el = els[i],
|
||||
game = el.getAttribute('data-game');
|
||||
|
||||
el.classList.toggle('ffz-game-banned', banned.indexOf(game && game.toLowerCase()) !== -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
FFZ.settings_info.spoiler_games = {
|
||||
visible: false,
|
||||
value: [],
|
||||
|
||||
on_update: function() {
|
||||
var spoiled = this.settings.spoiler_games,
|
||||
els = document.querySelectorAll('.ffz-directory-preview');
|
||||
|
||||
for(var i=0; i < els.length; i++) {
|
||||
var el = els[i],
|
||||
game = el.getAttribute('data-game');
|
||||
|
||||
el.classList.toggle('ffz-game-spoilered', spoiled.indexOf(game && game.toLowerCase()) !== -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
FFZ.settings_info.directory_host_menus = {
|
||||
type: "select",
|
||||
options: {
|
||||
0: "Disabled",
|
||||
1: "When Multiple are Hosting",
|
||||
2: "Always"
|
||||
},
|
||||
|
||||
value: 1,
|
||||
process_value: utils.process_int(1),
|
||||
|
||||
category: "Directory",
|
||||
no_mobile: true,
|
||||
|
||||
name: "Hosted Channel Menus",
|
||||
help: "Display a menu to select which channel to visit when clicking a hosted channel in the directory.",
|
||||
|
||||
on_update: function() {
|
||||
var f = this,
|
||||
HostModel = utils.ember_resolve('model:host'),
|
||||
Following = HostModel && HostModel.collections[HostModel.collectionId("following")];
|
||||
|
||||
if ( ! Following )
|
||||
return;
|
||||
|
||||
Following.clear();
|
||||
Following.load();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.directory_uploads_position = {
|
||||
type: "select",
|
||||
options: {
|
||||
0: "Default",
|
||||
1: "At Bottom",
|
||||
2: "Hidden"
|
||||
},
|
||||
|
||||
value: 0,
|
||||
process_value: utils.process_int(0),
|
||||
|
||||
category: "Directory",
|
||||
no_mobile: true,
|
||||
|
||||
name: "Display Latest Uploads",
|
||||
help: "Choose where to display the Latest Uploads section.",
|
||||
|
||||
on_update: function(val) {
|
||||
utils.toggle_cls('ffz-hide-directory-uploads')(val === 2);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.directory_hide_info_on_hover = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
|
||||
category: "Directory",
|
||||
no_mobile: true,
|
||||
|
||||
name: "Hide Boxart on Hover",
|
||||
help: "Do not display boxart and other information over a stream or video's thumbnail when hovering over it.",
|
||||
|
||||
on_update: utils.toggle_cls('ffz-hide-thumb-info-on-hover')
|
||||
}
|
||||
|
||||
|
||||
// --------------------
|
||||
// Initialization
|
||||
// --------------------
|
||||
|
||||
FFZ._image_cache = {};
|
||||
|
||||
FFZ.prototype.setup_directory = function() {
|
||||
utils.toggle_cls('ffz-creative-tags')(this.settings.directory_creative_all_tags);
|
||||
utils.toggle_cls('ffz-creative-showcase')(this.settings.directory_creative_showcase);
|
||||
utils.toggle_cls('ffz-hide-directory-uploads')(this.settings.directory_uploads_position === 2);
|
||||
utils.toggle_cls('ffz-hide-thumb-info-on-hover')(this.settings.directory_hide_info_on_hover);
|
||||
|
||||
var f = this,
|
||||
VodCoviews = utils.ember_lookup('service:vod-coviews');
|
||||
|
||||
if ( VodCoviews ) {
|
||||
VodCoviews.reopen({
|
||||
// checkExperiment likes setting this back. Don't let it.
|
||||
isFollowingAboveHost: Ember.computed('_ffz', {
|
||||
get: function(key) {
|
||||
return f.settings.recommended_above_hosts;
|
||||
},
|
||||
set: function(key, val) {
|
||||
return f.settings.recommended_above_hosts;
|
||||
}
|
||||
}),
|
||||
|
||||
areVodsViewable: function() {
|
||||
var vods = this.get('recommendedVods');
|
||||
return f.settings.enable_recommended_vods && vods && vods.length > 0;
|
||||
}.property('recommendedVods')
|
||||
});
|
||||
|
||||
Ember.propertyDidChange(VodCoviews, 'isFollowingAboveHost');
|
||||
Ember.propertyDidChange(VodCoviews, 'areVodsViewable');
|
||||
|
||||
} else
|
||||
this.log("Unable to locate the Ember service:vod-coviews");
|
||||
|
||||
this.log("Hooking the Ember Directory views.");
|
||||
|
||||
this.update_views('component:stream-preview', this.modify_directory_live, true);
|
||||
this.update_views('component:creative-preview', this.modify_directory_live, true);
|
||||
this.update_views('component:csgo-channel-preview', function(x) { this.modify_directory_live(x, 'channel.') }, true);
|
||||
this.update_views('component:stream/lol-metadata', function(x) { this.modify_directory_live(x, 'content.') }, true);
|
||||
this.update_views('component:twitch-carousel/stream-item', this.modify_directory_live, true);
|
||||
this.update_views('component:stream/snapshot-card', this.modify_directory_live, true);
|
||||
this.update_views('component:host-preview', this.modify_directory_host, true, true);
|
||||
this.update_views('component:video-preview', this.modify_video_preview, true);
|
||||
this.update_views('component:video/resumable-wrapper', this.modify_video_preview, true);
|
||||
this.update_views("component:video/following-uploads", this.modify_following_uploads);
|
||||
|
||||
this.update_views('component:game-follow-button', this.modify_game_follow_button);
|
||||
|
||||
this.log("Attempting to modify the Following collection.");
|
||||
this._modify_following();
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.modify_following_uploads = function(component) {
|
||||
var f = this;
|
||||
utils.ember_reopen_view(component, {
|
||||
ffz_init: function() {
|
||||
var el = this.get('element');
|
||||
el.classList.add('ffz-following-uploads');
|
||||
|
||||
if ( f.settings.directory_uploads_position === 1 ) {
|
||||
var p = el.parentElement;
|
||||
p.removeChild(el);
|
||||
p.appendChild(el);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype._modify_following = function() {
|
||||
var HostModel = utils.ember_resolve('model:host'),
|
||||
f = this;
|
||||
|
||||
if ( HostModel ) {
|
||||
var Following = HostModel.collections[HostModel.collectionId("following")];
|
||||
if ( Following ) {
|
||||
this.log("Found Following model.");
|
||||
Following.reopen({
|
||||
ffz_streams: {},
|
||||
ffz_hosts_for: {},
|
||||
ffz_skipped: 0,
|
||||
|
||||
empty: function() {
|
||||
this._super();
|
||||
this.set("ffz_streams", {});
|
||||
this.set("ffz_hosts_for", {});
|
||||
this.set("ffz_skipped", 0);
|
||||
},
|
||||
|
||||
request: function(e) {
|
||||
// We have to override request with nearly the same logic
|
||||
// to prevent infinitely trying to load more streams.
|
||||
if (!Twitch.user.isLoggedIn() || window.App.get("disableFollowingDirectory")) return RSVP.resolve({
|
||||
hosts: [], _total: 0
|
||||
});
|
||||
|
||||
var t = {
|
||||
limit: this.limit,
|
||||
offset: this.get('content.length') + this.get('ffz_skipped')
|
||||
};
|
||||
|
||||
// Don't use FFZ's Client ID because loading hosts is a normal part
|
||||
// of the dashboard. We're just manipulating the logic a bit.
|
||||
return this.get("api").request("get", "/api/users/:login/followed/hosting", t);
|
||||
},
|
||||
|
||||
afterSuccess: function(e) {
|
||||
var valid_hosts = [],
|
||||
streams = this.get('ffz_streams'),
|
||||
skipped = this.get('ffz_skipped'),
|
||||
hosts_for = this.get('ffz_hosts_for'),
|
||||
|
||||
t = this;
|
||||
|
||||
for(var i=0; i < e.hosts.length; i++) {
|
||||
var host = e.hosts[i],
|
||||
target = host && host.target && host.target.id;
|
||||
|
||||
if ( host.rollbackData )
|
||||
host.rollbackData = undefined;
|
||||
|
||||
if ( f.settings.directory_group_hosts && streams[target] ) {
|
||||
skipped++;
|
||||
//hosts_for[target] && hosts_for[target]
|
||||
streams[target].ffz_hosts && streams[target].ffz_hosts.push({logo: host.logo, name: host.name, display_name: host.display_name});
|
||||
continue;
|
||||
}
|
||||
|
||||
streams[target] = host;
|
||||
//hosts_for[target] = [{logo: host.logo, name: host.name, display_name: host.display_name}];
|
||||
host.ffz_hosts = [{logo: host.logo, name: host.name, display_name: host.display_name}];
|
||||
|
||||
valid_hosts.push(host);
|
||||
}
|
||||
|
||||
//f.log("Stuff!", [this, e, valid_hosts, skipped]);
|
||||
|
||||
this.set('ffz_skipped', skipped);
|
||||
this.setContent(valid_hosts);
|
||||
|
||||
// We could get non-empty results even with no new hosts.
|
||||
this.set('gotNonEmptyResults', e.hosts && e.hosts.length);
|
||||
this.set('total', e._total - skipped);
|
||||
}
|
||||
});
|
||||
|
||||
// Filter the streams immediately.
|
||||
if ( true && ! Following.get('isLoading') ) {
|
||||
var content = Following.get('content'),
|
||||
total = Following.get('total'),
|
||||
host_copy = [];
|
||||
|
||||
// TODO: Something less stupid.
|
||||
for(var i=0; i < content.length; i++) {
|
||||
var host = content[i];
|
||||
host_copy.push({
|
||||
display_name: host.display_name,
|
||||
game: host.game,
|
||||
id: host.id,
|
||||
logo: host.logo,
|
||||
name: host.name,
|
||||
target: {
|
||||
_id: host.target._id,
|
||||
channel: {
|
||||
display_name: host.target.channel.display_name,
|
||||
id: host.target.channel.id,
|
||||
logo: host.target.channel.logo,
|
||||
name: host.target.channel.name,
|
||||
url: host.target.channel.url
|
||||
},
|
||||
id: host.target.id,
|
||||
meta_game: host.target.meta_game,
|
||||
preview: host.target.preview,
|
||||
title: host.target.title,
|
||||
url: host.target.url,
|
||||
viewers: host.target.viewers
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Following.clear();
|
||||
Following.afterSuccess({hosts: host_copy, _total: total});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Couldn't find it. Reschedule.
|
||||
setTimeout(this._modify_following.bind(this), 250);
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.modify_game_follow_button = function(component) {
|
||||
var f = this;
|
||||
utils.ember_reopen_view(component, {
|
||||
ffz_init: function() {
|
||||
var el = this.get('element'),
|
||||
game = this.get('game.id').toLowerCase(),
|
||||
|
||||
click_button = function(setting, update_func) {
|
||||
return function(e) {
|
||||
e.preventDefault();
|
||||
var games = f.settings.get(setting),
|
||||
ind = games.indexOf(game);
|
||||
|
||||
if ( ind === -1 )
|
||||
games.push(game);
|
||||
else
|
||||
games.splice(ind, 1);
|
||||
|
||||
f.settings.set(setting, games);
|
||||
update_func();
|
||||
}
|
||||
};
|
||||
|
||||
// Block Button
|
||||
var block = utils.createElement('button', 'button tooltip ffz-block-button'),
|
||||
update_block = function() {
|
||||
var is_blocked = f.settings.banned_games.indexOf(game) !== -1;
|
||||
block.classList.toggle('active', is_blocked);
|
||||
|
||||
block.innerHTML = (is_blocked ? 'Unblock' : 'Block');
|
||||
block.title = 'Click to ' + (is_blocked ? 'unblock' : 'block') + " this game.\n\nBlocking a game hides all the streams and videos of the game when you're not viewing it directly.";
|
||||
jQuery(block).trigger('mouseout').trigger('mouseover');
|
||||
};
|
||||
|
||||
update_block();
|
||||
block.addEventListener('click', click_button('banned_games', update_block));
|
||||
el.appendChild(block);
|
||||
|
||||
// Spoiler Button
|
||||
var spoiler = utils.createElement('button', 'button tooltip ffz-spoiler-button'),
|
||||
update_spoiler = function() {
|
||||
var is_spoiled = f.settings.spoiler_games.indexOf(game) !== -1;
|
||||
spoiler.classList.toggle('active', is_spoiled);
|
||||
|
||||
spoiler.innerHTML = (is_spoiled ? 'Show Thumbnails' : 'Hide Thumbnails');
|
||||
spoiler.title = 'Click to ' + (is_spoiled ? 'show' : 'hide') + " thumbnails for this game.\n\nHiding thumbnails for a game will help you avoid spoilers for a game that you haven't played yet.";
|
||||
jQuery(spoiler).trigger('mouseout').trigger('mouseover');
|
||||
}
|
||||
|
||||
update_spoiler();
|
||||
spoiler.addEventListener('click', click_button('spoiler_games', update_spoiler));
|
||||
el.appendChild(spoiler);
|
||||
|
||||
jQuery('.tooltip', el).zipsy();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.modify_directory_live = function(component, mode) {
|
||||
var f = this,
|
||||
pref = mode || 'stream.';
|
||||
|
||||
utils.ember_reopen_view(component, {
|
||||
ffz_init: function() {
|
||||
var el = this.get('element'),
|
||||
meta = el && el.querySelector('.card__body'),
|
||||
thumb = el && el.querySelector('.card__img'),
|
||||
cap = thumb && thumb.querySelector('a:not(.card__boxpin)'),
|
||||
uptime_setting = f.settings.stream_uptime,
|
||||
channel_id = this.get(pref + 'channel.name'),
|
||||
game = this.get(pref + 'game');
|
||||
|
||||
el.classList.add('ffz-directory-preview');
|
||||
el.setAttribute('data-channel', channel_id);
|
||||
el.setAttribute('data-game', game);
|
||||
|
||||
el.classList.toggle('ffz-game-banned', f.settings.banned_games.indexOf(game && game.toLowerCase()) !== -1);
|
||||
el.classList.toggle('ffz-game-spoilered', f.settings.spoiler_games.indexOf(game && game.toLowerCase()) !== -1);
|
||||
|
||||
if ( uptime_setting && uptime_setting < 3 ) {
|
||||
this._ffz_uptime_timer = setInterval(
|
||||
this.ffzUpdateUptime.bind(this),
|
||||
uptime_setting === 2 ? 1000 : 60000);
|
||||
this.ffzUpdateUptime();
|
||||
}
|
||||
|
||||
this._ffz_image_timer = setInterval(this.ffzRotateImage.bind(this), 30000);
|
||||
this.ffzRotateImage();
|
||||
|
||||
if ( f.settings.directory_logos ) {
|
||||
el.classList.add('ffz-directory-logo');
|
||||
|
||||
var link = document.createElement('a'),
|
||||
logo = document.createElement('img'),
|
||||
t = this;
|
||||
|
||||
logo.className = 'profile-photo';
|
||||
logo.classList.toggle('is-csgo', mode === 'channel.');
|
||||
|
||||
logo.src = this.get(pref + 'channel.logo') || constants.NO_LOGO;
|
||||
logo.alt = f.format_display_name(this.get(pref + 'channel.display_name'), channel_id, true, true)[0];
|
||||
|
||||
link.href = '/' + channel_id;
|
||||
link.addEventListener('click', utils.transition_link(utils.transition_user.bind(this, channel_id)));
|
||||
|
||||
link.appendChild(logo);
|
||||
meta.insertBefore(link, meta.firstChild);
|
||||
}
|
||||
},
|
||||
|
||||
ffz_destroy: function() {
|
||||
if ( this._ffz_uptime ) {
|
||||
jQuery(this._ffz_uptime).remove();
|
||||
this._ffz_uptime = this._ffz_uptime_span = null;
|
||||
}
|
||||
|
||||
if ( this._ffz_uptime_timer )
|
||||
clearInterval(this._ffz_uptime_timer);
|
||||
|
||||
if ( this._ffz_image_timer )
|
||||
clearInterval(this._ffz_image_timer);
|
||||
},
|
||||
|
||||
ffzRotateImage: function() {
|
||||
var url = this.get(pref + 'preview.medium'),
|
||||
now = Math.round((new Date).getTime() / 150000);
|
||||
|
||||
if ( FFZ._image_cache[url] && FFZ._image_cache[url] !== now )
|
||||
url += '?_=' + now;
|
||||
else
|
||||
FFZ._image_cache[url] = now;
|
||||
|
||||
this.$('.thumb .cap img').attr('src', url);
|
||||
},
|
||||
|
||||
ffzUpdateUptime: function() {
|
||||
var up_since = this.get(pref + 'created_at');
|
||||
if ( typeof up_since === "string" )
|
||||
up_since = utils.parse_date(up_since);
|
||||
|
||||
var now = Date.now() - (f._ws_server_offset || 0),
|
||||
uptime = up_since && Math.floor((now - up_since.getTime()) / 1000) || 0;
|
||||
|
||||
if ( uptime > 0 ) {
|
||||
if ( ! this._ffz_uptime ) {
|
||||
var el = this.get('element'),
|
||||
cont = el && el.querySelector('.card__img');
|
||||
|
||||
if ( ! cont )
|
||||
return;
|
||||
|
||||
var t_el = this._ffz_uptime = utils.createElement('div', 'overlay_info length live', constants.CLOCK),
|
||||
t_span = this._ffz_uptime_span = utils.createElement('span');
|
||||
|
||||
t_el.appendChild(t_span);
|
||||
t_el.setAttribute('original-title', 'Stream Uptime <nobr>(since ' + up_since.toLocaleString() + ')</nobr>');
|
||||
|
||||
jQuery(t_el).zipsy({html: true, gravity: utils.newtip_placement(constants.TOOLTIP_DISTANCE, 's')});
|
||||
cont.appendChild(t_el);
|
||||
}
|
||||
|
||||
this._ffz_uptime_span.textContent = utils.time_to_string(uptime, false, false, false, f.settings.stream_uptime === 1);
|
||||
|
||||
} else if ( this._ffz_uptime ) {
|
||||
jQuery(this._ffz_uptime).remove();
|
||||
this._ffz_uptime = this._ffz_uptime_span = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.modify_video_preview = function(component) {
|
||||
var f = this;
|
||||
utils.ember_reopen_view(component, {
|
||||
ffz_init: function() {
|
||||
var el = this.get('element'),
|
||||
game = this.get('video.game'),
|
||||
|
||||
thumb = el && el.querySelector('.thumb'),
|
||||
boxart = thumb && thumb.querySelector('.boxart');
|
||||
|
||||
el.classList.add('ffz-directory-preview');
|
||||
el.setAttribute('data-channel', this.get('video.channel.name'));
|
||||
el.setAttribute('data-game', game);
|
||||
|
||||
el.classList.toggle('ffz-game-banned', f.settings.banned_games.indexOf(game && game.toLowerCase()) !== -1);
|
||||
el.classList.toggle('ffz-game-spoilered', f.settings.spoiler_games.indexOf(game && game.toLowerCase()) !== -1);
|
||||
|
||||
if ( ! boxart && thumb && game ) {
|
||||
var img = utils.createElement('img');
|
||||
|
||||
boxart = utils.createElement('a', 'boxart');
|
||||
boxart.href = this.get('video.gameUrl');
|
||||
boxart.setAttribute('original-title', game);
|
||||
boxart.addEventListener('click', utils.transition_link(utils.transition_game.bind(this, game)));
|
||||
|
||||
img.src = this.get('video.gameBoxart');
|
||||
boxart.appendChild(img);
|
||||
thumb.appendChild(boxart);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.modify_directory_host = function(component) {
|
||||
var f = this;
|
||||
utils.ember_reopen_view(component, {
|
||||
ffzVisitChannel: function(target, e) {
|
||||
if ( e ) {
|
||||
if ( e.button !== 0 || e.altKey || e.ctrlKey || e.shiftKey || e.metaKey )
|
||||
return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
f.close_popup();
|
||||
utils.transition_user(target);
|
||||
return false;
|
||||
},
|
||||
|
||||
ffzShowHostMenu: function(e) {
|
||||
if ( e.button !== 0 || e.altKey || e.ctrlKey || e.shiftKey || e.metaKey )
|
||||
return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
var hosts = this.get('stream.ffz_hosts'),
|
||||
target = this.get('stream.target.channel.name');
|
||||
|
||||
if ( f.settings.directory_host_menus === 0 || ! hosts || (f.settings.directory_host_menus === 1 && hosts.length < 2) )
|
||||
return this.ffzVisitChannel((hosts && hosts.length < 2) ? hosts[0].name : target);
|
||||
|
||||
var popup = f._popup ? f.close_popup() : f._last_popup,
|
||||
t = this;
|
||||
|
||||
// Don't re-show the popup if we were clicking to show it.
|
||||
if ( popup && popup.classList.contains('ffz-channel-selector') && popup.getAttribute('data-channel') === target )
|
||||
return;
|
||||
|
||||
var menu = document.createElement('div'), hdr,
|
||||
make_link = function(target) {
|
||||
var link = document.createElement('a'),
|
||||
results = f.format_display_name(target.display_name, target.name, true);
|
||||
|
||||
link.className = 'dropmenu_action';
|
||||
link.setAttribute('data-channel', target.name);
|
||||
link.href = '/' + target.name;
|
||||
link.innerHTML = '<img class="image" src="' + utils.sanitize(target.logo || constants.NO_LOGO) + '"><span class="title' + (results[1] ? ' html-tooltip" title="' + utils.quote_attr(results[1]) : '') + '">' + results[0] + '</span>';
|
||||
link.addEventListener('click', t.ffzVisitChannel.bind(t, target.name));
|
||||
menu.appendChild(link);
|
||||
return link;
|
||||
};
|
||||
|
||||
menu.className = 'ffz-channel-selector dropmenu menu-like';
|
||||
menu.setAttribute('data-channel', target);
|
||||
|
||||
hdr = document.createElement('div');
|
||||
hdr.className = 'header';
|
||||
hdr.textContent = 'Hosted Channel';
|
||||
menu.appendChild(hdr);
|
||||
|
||||
make_link(this.get('stream.target.channel'));
|
||||
|
||||
hdr = document.createElement('div');
|
||||
hdr.className = 'header';
|
||||
hdr.textContent = 'Hosting Channels';
|
||||
menu.appendChild(hdr);
|
||||
|
||||
for(var i=0; i < hosts.length; i++)
|
||||
make_link(hosts[i]);
|
||||
|
||||
var cont = document.querySelector('#main_col > .tse-scroll-content > .tse-content'),
|
||||
bounds = cont && cont.getBoundingClientRect(),
|
||||
|
||||
x = e.clientX - 60,
|
||||
y = e.clientY - 60;
|
||||
|
||||
if ( bounds )
|
||||
x = Math.max(bounds.left, Math.min(x, (bounds.left + bounds.width) - 302));
|
||||
|
||||
f.show_popup(menu, [x, y], document.querySelector('#main_col > .tse-scroll-content > .tse-content'));
|
||||
},
|
||||
|
||||
ffzRotateImage: function() {
|
||||
var url = this.get('stream.target.preview'),
|
||||
now = Math.round((new Date).getTime() / 150000);
|
||||
|
||||
if ( FFZ._image_cache[url] && FFZ._image_cache[url] !== now )
|
||||
url += '?_=' + now;
|
||||
else
|
||||
FFZ._image_cache[url] = now;
|
||||
|
||||
this.$('.thumb .cap img').attr('src', url);
|
||||
},
|
||||
|
||||
ffz_destroy: function() {
|
||||
var target = this.get('stream.target.channel');
|
||||
if ( f._popup && f._popup.classList.contains('ffz-channel-selector') && f._popup.getAttribute('data-channel') === target )
|
||||
f.close_popup();
|
||||
|
||||
if ( this._ffz_image_timer )
|
||||
clearInterval(this._ffz_image_timer);
|
||||
},
|
||||
|
||||
ffz_init: function() {
|
||||
var el = this.get('element'),
|
||||
meta = el && el.querySelector('.card__body'), //meta'),
|
||||
thumb = el && el.querySelector('.card__img'), //thumb'),
|
||||
cap = thumb && thumb.querySelector('a:not(.card__boxpin)'), //.cap'),
|
||||
title = meta && meta.querySelector('.card__title a'),
|
||||
|
||||
target = this.get('stream.target.channel'),
|
||||
game = this.get('stream.target.meta_game'),
|
||||
hosts = this.get('stream.ffz_hosts');
|
||||
|
||||
el.classList.add('ffz-directory-preview');
|
||||
el.setAttribute('data-channel', target.name);
|
||||
el.setAttribute('data-game', game);
|
||||
|
||||
el.classList.toggle('ffz-game-banned', f.settings.banned_games.indexOf(game && game.toLowerCase()) !== -1);
|
||||
el.classList.toggle('ffz-game-spoilered', f.settings.spoiler_games.indexOf(game && game.toLowerCase()) !== -1);
|
||||
|
||||
this._ffz_image_timer = setInterval(this.ffzRotateImage.bind(this), 30000);
|
||||
this.ffzRotateImage();
|
||||
|
||||
if ( f.settings.directory_logos ) {
|
||||
el.classList.add('ffz-directory-logo');
|
||||
var logo = document.createElement('img'),
|
||||
link = document.createElement('a');
|
||||
|
||||
logo.className = 'profile-photo';
|
||||
logo.src = this.get('stream.target.channel.logo') || constants.NO_LOGO;
|
||||
logo.alt = f.format_display_name(target.display_name, target.name, true, true)[0];
|
||||
|
||||
link.href = '/' + target.name;
|
||||
link.addEventListener('click', this.ffzVisitChannel.bind(this, target.name));
|
||||
|
||||
link.appendChild(logo);
|
||||
meta.insertBefore(link, meta.firstChild);
|
||||
}
|
||||
|
||||
var update_links = f.settings.directory_host_menus === 2 || (hosts && hosts.length > 1);
|
||||
|
||||
if ( title ) {
|
||||
if ( update_links ) {
|
||||
title.href = '/' + target.name;
|
||||
title.addEventListener('click', this.ffzShowHostMenu.bind(this));
|
||||
}
|
||||
|
||||
if ( hosts && hosts.length > 1 ) {
|
||||
title.textContent = utils.number_commas(hosts.length) + ' hosting ' + utils.sanitize(target.display_name);
|
||||
title.title = _.sortBy(hosts, "name").mapBy("display_name").join(", ");
|
||||
jQuery(title).zipsy({gravity: 's'});
|
||||
}
|
||||
}
|
||||
|
||||
if ( cap && update_links ) {
|
||||
cap.href = '/' + target.name;
|
||||
cap.addEventListener('click', this.ffzShowHostMenu.bind(this));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,121 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ,
|
||||
utils = require('../utils'),
|
||||
constants = require('../constants'),
|
||||
|
||||
parse_emotes = function(emotes) {
|
||||
var output = {};
|
||||
if ( ! emotes || ! emotes.length )
|
||||
return output;
|
||||
|
||||
for(var i=0; i < emotes.length; i++) {
|
||||
var emote = emotes[i];
|
||||
if ( ! emote || ! emote.id )
|
||||
continue;
|
||||
|
||||
var entries = output[emote.id] = output[emote.id] || [];
|
||||
entries.push([emote.start, emote.end]);
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
|
||||
FFZ.prototype.setup_feed_cards = function() {
|
||||
this.update_views('component:twitch-feed/story-card', this.modify_feed_card);
|
||||
this.update_views('component:twitch-feed/comment', this.modify_feed_comment);
|
||||
|
||||
this.rerender_feed_cards();
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.rerender_feed_cards = function(for_set) {
|
||||
var FeedCard = utils.ember_resolve('component:twitch-feed/story-card'),
|
||||
FeedComment = utils.ember_resolve('component:twitch-feed/comment'),
|
||||
views = utils.ember_views();
|
||||
|
||||
if ( ! FeedCard )
|
||||
return;
|
||||
|
||||
for(var view_id in views) {
|
||||
var view = views[view_id];
|
||||
if ( view instanceof FeedCard ) {
|
||||
try {
|
||||
if ( ! view.ffz_init )
|
||||
this.modify_feed_card(view);
|
||||
view.ffz_init(for_set);
|
||||
} catch(err) {
|
||||
this.error("setup component:twitch-feed/story-card ffzInit", err)
|
||||
}
|
||||
}
|
||||
|
||||
if ( FeedComment && view instanceof FeedComment ) {
|
||||
try {
|
||||
if ( ! view.ffz_init )
|
||||
this.modify_feed_comment(view);
|
||||
view.ffz_init(for_set);
|
||||
} catch(err) {
|
||||
this.error("setup component:twitch-feed/comment ffzInit", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.modify_feed_card = function(component) {
|
||||
var f = this;
|
||||
utils.ember_reopen_view(component, {
|
||||
ffz_init: function(for_set) {
|
||||
var el = this.get('element'),
|
||||
message = this.get('post.body'),
|
||||
emotes = parse_emotes(this.get('post.emotes')),
|
||||
user_id = this.get('post.user.login'),
|
||||
room_id = this.get('channelId') || user_id,
|
||||
pbody = el && el.querySelector('.activity-body');
|
||||
|
||||
if ( ! message || ! el || ! pbody )
|
||||
return;
|
||||
|
||||
// If this is for a specific emote set, only rerender if it matters.
|
||||
if ( for_set && f.rooms && f.rooms[room_id] ) {
|
||||
var sets = f.getEmotes(user_id, room_id);
|
||||
if ( sets.indexOf(for_set) === -1 )
|
||||
return;
|
||||
}
|
||||
|
||||
var tokens = f.tokenize_feed_body(message, emotes, user_id, room_id),
|
||||
output = f.render_tokens(tokens, true, false);
|
||||
|
||||
pbody.innerHTML = '<p>' + output + '</p>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.modify_feed_comment = function(component) {
|
||||
var f = this;
|
||||
utils.ember_reopen_view(component, {
|
||||
ffz_init: function(for_set) {
|
||||
var el = this.get('element'),
|
||||
message = this.get('comment.body'),
|
||||
emotes = parse_emotes(this.get('comment.emotes')),
|
||||
user_id = this.get('comment.user.login'),
|
||||
room_id = this.get('parentView.parentView.channelId') || this.get('parentView.parentView.post.user.login') || null,
|
||||
pbody = el && el.querySelector('.activity-body');
|
||||
|
||||
if ( ! message || ! el || ! pbody )
|
||||
return;
|
||||
|
||||
// If this is for a specific emote set, only rerender if it matters.
|
||||
if ( for_set && f.rooms && f.rooms[room_id] ) {
|
||||
var sets = f.getEmotes(user_id, room_id);
|
||||
if ( sets.indexOf(for_set) === -1 )
|
||||
return;
|
||||
}
|
||||
|
||||
var tokens = f.tokenize_feed_body(message, emotes, user_id, room_id),
|
||||
output = f.render_tokens(tokens, true, false);
|
||||
|
||||
pbody.innerHTML = '<p>' + output + '</p>';
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,336 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ,
|
||||
utils = require('../utils'),
|
||||
constants = require('../constants'),
|
||||
|
||||
createElement = utils.createElement,
|
||||
|
||||
FOLLOWING_RE = /^\/kraken\/users\/([^/]+)\/follows\/channels/,
|
||||
FOLLOWER_RE = /^\/kraken\/channels\/([^/]+)\/follows/;
|
||||
|
||||
|
||||
// --------------------
|
||||
// Settings
|
||||
// --------------------
|
||||
|
||||
FFZ.settings_info.enhance_profile_following = {
|
||||
type: "boolean",
|
||||
value: true,
|
||||
|
||||
category: "Directory",
|
||||
no_mobile: true,
|
||||
|
||||
name: "Enhanced Following Control",
|
||||
help: "Display additional controls on your own profile's Following tab to make management easier, as well as telling you how long everyone has been following everyone else in the profile."
|
||||
}
|
||||
|
||||
|
||||
// --------------------
|
||||
// Initialization
|
||||
// --------------------
|
||||
|
||||
FFZ.prototype.setup_profile_following = function() {
|
||||
if ( ! window.App )
|
||||
return;
|
||||
|
||||
var f = this;
|
||||
|
||||
// Build our is-following cache.
|
||||
this._following_cache = {};
|
||||
this._follower_cache = {};
|
||||
|
||||
|
||||
// We want to hook the API to gather this information. It's easier than
|
||||
// modifying the deserialization path.
|
||||
var process_follows = function(channel_id, data, cache) {
|
||||
f.log("Loading Follow Information for: " + channel_id, data);
|
||||
|
||||
var user_cache = cache[channel_id] = cache[channel_id] || {},
|
||||
now = Date.now();
|
||||
|
||||
for(var i=0; i < data.length; i++) {
|
||||
var follow = data[i],
|
||||
user = follow && (follow.user || follow.channel);
|
||||
|
||||
if ( ! user || ! user.name )
|
||||
continue;
|
||||
|
||||
if ( user.display_name && user.display_name !== 'jtv' )
|
||||
FFZ.capitalization[user.name] = [user.display_name, now];
|
||||
|
||||
user_cache[user.name] = [
|
||||
follow.created_at ? utils.parse_date(follow.created_at) : null,
|
||||
follow.notifications || false];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var ServiceAPI = utils.ember_lookup('service:api');
|
||||
if ( ServiceAPI )
|
||||
ServiceAPI.reopen({
|
||||
request: function(method, url, data, options) {
|
||||
if ( method !== 'get' || url.indexOf('/kraken/') !== 0 )
|
||||
return this._super(method, url, data, options);
|
||||
|
||||
var t = this;
|
||||
return new Promise(function(success, fail) {
|
||||
t._super(method, url, data, options).then(function(result) {
|
||||
if ( result.follows ) {
|
||||
var match = FOLLOWING_RE.exec(url);
|
||||
if ( match )
|
||||
// Following Information
|
||||
process_follows(match[1], result.follows, f._following_cache);
|
||||
|
||||
match = FOLLOWER_RE.exec(url);
|
||||
if ( match )
|
||||
// Follower Information
|
||||
process_follows(match[1], result.follows, f._follower_cache);
|
||||
}
|
||||
|
||||
success(result);
|
||||
|
||||
}).catch(function(err) {
|
||||
fail(err);
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
else
|
||||
this.error("Unable to locate the Ember service:api");
|
||||
|
||||
|
||||
// Modify followed items.
|
||||
//this.update_views('component:display-followed-item', this.modify_display_followed_item);
|
||||
this.update_views('component:twitch-profile-card', this.modify_twitch_profile_card);
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.modify_twitch_profile_card = function(component) {
|
||||
var f = this;
|
||||
utils.ember_reopen_view(component, {
|
||||
ffzParentModel: function() {
|
||||
var x = this.get('parentView');
|
||||
while(x) {
|
||||
var model = x.get('model');
|
||||
if ( model )
|
||||
return model;
|
||||
x = x.get('parentView');
|
||||
}
|
||||
}.property('parentView'),
|
||||
|
||||
ffz_init: function() {
|
||||
var el = this.get('element');
|
||||
|
||||
el.classList.add('ffz-processed');
|
||||
jQuery('.aspect', el).zipsy();
|
||||
|
||||
if ( ! f.settings.enhance_profile_following )
|
||||
return;
|
||||
|
||||
this.ffzUpdate();
|
||||
},
|
||||
|
||||
ffzUpdate: function() {
|
||||
var el = this.get('element'),
|
||||
t_el = el.querySelector('.ffz-followed-since'),
|
||||
//notif_el = el.querySelector('.ffz-followed-notifications'),
|
||||
|
||||
channel_id = this.get('ffzParentModel.model.id'),
|
||||
is_following = this.get('ffzParentModel.relationshipName') === 'following',
|
||||
|
||||
user = f.get_user(),
|
||||
mine = user && user.login && user.login === channel_id,
|
||||
big_cache = is_following ? f._following_cache : f._follower_cache,
|
||||
user_cache = big_cache[channel_id] = big_cache[channel_id] || {},
|
||||
|
||||
user_id = this.get('channelInfo.id'),
|
||||
data = user_cache[user_id];
|
||||
|
||||
//f.log("Profile Card [" + channel_id + "] " + user_id + " <" + JSON.stringify(data) + ">", this);
|
||||
|
||||
if ( ! data || ! el ) {
|
||||
if ( t_el )
|
||||
jQuery(t_el).remove();
|
||||
/*if ( notif_el )
|
||||
jQuery(notif_el).remove();*/
|
||||
return false;
|
||||
}
|
||||
|
||||
var now = Date.now() - (f._ws_server_offset || 0),
|
||||
age = data[0] ? Math.floor((now - data[0].getTime()) / 1000) : 0,
|
||||
t_el = el.querySelector('.ffz-followed-since')
|
||||
|
||||
update_time = function() {
|
||||
var data = user_cache[user_id],
|
||||
now = Date.now() - (f._ws_server_offset || 0),
|
||||
age = data && data[0] ? Math.floor((now - data[0].getTime()) / 1000) : undefined;
|
||||
|
||||
if ( age !== undefined ) {
|
||||
t_el.innerHTML = constants.CLOCK + ' ' + (age < 60 ? 'now' : utils.human_time(age, 10));
|
||||
t_el.title = 'Follow' + (is_following ? 'ed by ' : 'er of ') + channel_id + ' since: <nobr>' + data[0].toLocaleString() + '</nobr>';
|
||||
t_el.style.display = '';
|
||||
} else
|
||||
t_el.style.display = 'none';
|
||||
};
|
||||
|
||||
if ( ! t_el ) {
|
||||
t_el = createElement('div', 'overlay_info length html-tooltip ffz-followed-since');
|
||||
el.appendChild(t_el);
|
||||
}
|
||||
|
||||
update_time();
|
||||
|
||||
/*if ( ! mine || ! is_following ) {
|
||||
if ( notif_el )
|
||||
jQuery(notif_el).remove();
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! notif_el ) {
|
||||
notif_el = createElement('button', 'ffz-followed-notifications button html-tooltip');
|
||||
var cont = el.querySelector('.profile-card__actions');
|
||||
if ( ! cont )
|
||||
return;
|
||||
cont.appendChild(notif_el);
|
||||
}
|
||||
|
||||
var update_notif = function() {
|
||||
var data = user_cache[user_id];
|
||||
notif_el.classList.toggle('notifications-on', data && data[1]);
|
||||
notif_el.textContent = 'Notifications';
|
||||
notif_el.setAttribute('original-title', 'Email Notifications: ' + (data && data[1] ? 'En' : 'Dis') + 'abled');
|
||||
jQuery(notif_el).trigger('mouseout');
|
||||
};
|
||||
|
||||
update_notif();*/
|
||||
|
||||
}.observes('channelInfo')
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*FFZ.prototype.modify_display_followed_item = function(component) {
|
||||
var f = this;
|
||||
utils.ember_reopen_view(component, {
|
||||
ffzParentModel: function() {
|
||||
var x = this.get('parentView');
|
||||
while(x) {
|
||||
var model = x.get('model');
|
||||
if ( model )
|
||||
return model;
|
||||
x = x.get('parentView');
|
||||
}
|
||||
}.property('parentView'),
|
||||
|
||||
ffz_init: function() {
|
||||
var el = this.get('element'),
|
||||
channel_id = this.get('ffzParentModel.id'), //.get('parentView.parentView.parentView.model.id'),
|
||||
is_following = document.body.getAttribute('data-current-path').indexOf('.following') !== -1,
|
||||
|
||||
user = f.get_user(),
|
||||
mine = user && user.login && user.login === channel_id,
|
||||
big_cache = is_following ? f._following_cache : f._follower_cache,
|
||||
user_cache = big_cache[channel_id] = big_cache[channel_id] || {},
|
||||
|
||||
user_id = this.get('followed.id'),
|
||||
data = user_cache[user_id];
|
||||
|
||||
if ( ! f.settings.enhance_profile_following )
|
||||
return;
|
||||
|
||||
el.classList.add('ffz-processed');
|
||||
|
||||
jQuery('.aspect', el).zipsy();
|
||||
|
||||
if ( ! data )
|
||||
return false;
|
||||
|
||||
var now = Date.now() - (f._ws_server_offset || 0),
|
||||
age = data[0] ? Math.floor((now - data[0].getTime()) / 1000) : 0,
|
||||
t_el = createElement('div', 'overlay_info length html-tooltip'),
|
||||
|
||||
update_time = function() {
|
||||
var now = Date.now() - (f._ws_server_offset || 0),
|
||||
age = data && data[0] ? Math.floor((now - data[0].getTime()) / 1000) : undefined;
|
||||
|
||||
if ( age !== undefined ) {
|
||||
t_el.innerHTML = constants.CLOCK + ' ' + (age < 60 ? 'now' : utils.human_time(age, 10));
|
||||
t_el.title = 'Follow' + (is_following ? 'ing' : 'er') + ' Since: <nobr>' + data[0].toLocaleString() + '</nobr>';
|
||||
t_el.style.display = '';
|
||||
} else
|
||||
t_el.style.display = 'none';
|
||||
};
|
||||
|
||||
update_time();
|
||||
el.appendChild(t_el);
|
||||
|
||||
if ( ! mine || ! is_following )
|
||||
return;
|
||||
|
||||
var actions = createElement('div', 'actions'),
|
||||
follow = createElement('button', 'button ffz-no-bg follow'),
|
||||
notif = createElement('button', 'button ffz-no-bg notifications html-tooltip'),
|
||||
|
||||
update_follow = function() {
|
||||
data = user_cache[user_id];
|
||||
el.classList.toggle('followed', data);
|
||||
follow.innerHTML = constants.HEART + constants.UNHEART + '<span> Follow</span>';
|
||||
},
|
||||
|
||||
update_notif = function() {
|
||||
data = user_cache[user_id];
|
||||
notif.classList.toggle('notifications-on', data && data[1]);
|
||||
notif.textContent = 'Notifications'; // ' + (data && data[1] ? 'On' : 'Off');
|
||||
notif.setAttribute('original-title', 'Email Notifications: ' + (data && data[1] ? 'En' : 'Dis') + 'abled');
|
||||
jQuery(notif).trigger('mouseout');
|
||||
};
|
||||
|
||||
update_follow();
|
||||
update_notif();
|
||||
|
||||
follow.addEventListener('click', function() {
|
||||
var was_following = !!data;
|
||||
|
||||
follow.disabled = true;
|
||||
notif.disabled = true;
|
||||
follow.textContent = 'Updating';
|
||||
|
||||
(was_following ?
|
||||
utils.api.del("users/:login/follows/channels/" + user_id) :
|
||||
utils.api.put("users/:login/follows/channels/" + user_id, {notifications: false}))
|
||||
.done(function() {
|
||||
data = user_cache[user_id] = was_following ? null : [new Date(Date.now() - (f._ws_server_offset||0)), false];
|
||||
})
|
||||
.always(function() {
|
||||
update_follow();
|
||||
update_notif();
|
||||
update_time();
|
||||
follow.disabled = false;
|
||||
notif.disabled = false;
|
||||
})
|
||||
});
|
||||
|
||||
notif.addEventListener('click', function() {
|
||||
var was_following = data[1];
|
||||
|
||||
follow.disabled = true;
|
||||
notif.disabled = true;
|
||||
notif.textContent = 'Updating';
|
||||
|
||||
utils.api.put("users/:login/follows/channels/" + user_id, {notifications: !was_following})
|
||||
.done(function() {
|
||||
data[1] = ! was_following;
|
||||
})
|
||||
.always(function() {
|
||||
update_notif();
|
||||
follow.disabled = false;
|
||||
notif.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
actions.appendChild(follow);
|
||||
actions.appendChild(notif);
|
||||
|
||||
el.appendChild(actions);
|
||||
}
|
||||
});
|
||||
}*/
|
|
@ -1,461 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ,
|
||||
utils = require('../utils'),
|
||||
|
||||
route_helper;
|
||||
|
||||
|
||||
// --------------------
|
||||
// Settings
|
||||
// --------------------
|
||||
|
||||
FFZ.settings_info.portrait_mode = {
|
||||
type: "select",
|
||||
options: {
|
||||
0: "Disabled",
|
||||
1: "Automatic (Use Window Aspect Ratio)",
|
||||
2: "Always On",
|
||||
3: "Automatic (Video Below)",
|
||||
4: "Always On (Video Below)"
|
||||
},
|
||||
|
||||
value: 0,
|
||||
process_value: utils.process_int(0, 0, 1),
|
||||
|
||||
category: "Appearance",
|
||||
no_mobile: true,
|
||||
no_bttv: true,
|
||||
|
||||
name: "Portrait Mode (Chat Below Video)",
|
||||
help: "Display the right sidebar beneath (or above) the video player for viewing in portrait orientations.",
|
||||
|
||||
on_update: function(val) {
|
||||
if ( this.has_bttv )
|
||||
return;
|
||||
|
||||
var Layout = utils.ember_lookup('service:layout');
|
||||
if ( ! Layout )
|
||||
return;
|
||||
|
||||
Layout.set('rawPortraitMode', val);
|
||||
this._fix_menu_position();
|
||||
}
|
||||
}
|
||||
|
||||
FFZ.settings_info.portrait_warning = {
|
||||
value: false,
|
||||
visible: false
|
||||
}
|
||||
|
||||
|
||||
FFZ.settings_info.swap_sidebars = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
|
||||
category: "Sidebar",
|
||||
no_mobile: true,
|
||||
no_bttv: true,
|
||||
|
||||
name: "Swap Sidebar Positions",
|
||||
help: "Swap the positions of the left and right sidebars, placing chat on the left.",
|
||||
|
||||
on_update: function(val) {
|
||||
if ( this.has_bttv )
|
||||
return;
|
||||
|
||||
document.body.classList.toggle("ffz-sidebar-swap", val);
|
||||
this._fix_menu_position();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.flip_dashboard = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
|
||||
category: "Dashboard",
|
||||
no_mobile: true,
|
||||
no_bttv: true,
|
||||
|
||||
name: "Swap Column Positions",
|
||||
help: "Swap the positions of the left and right columns of the dashboard.",
|
||||
|
||||
on_update: function(val) {
|
||||
if ( this.has_bttv )
|
||||
return;
|
||||
|
||||
document.body.classList.toggle("ffz-flip-dashboard", val);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.right_column_width = {
|
||||
type: "button",
|
||||
value: 340,
|
||||
|
||||
category: "Appearance",
|
||||
no_mobile: true,
|
||||
no_bttv: true,
|
||||
|
||||
name: "Right Sidebar Width",
|
||||
help: "Set the width of the right sidebar for chat.",
|
||||
|
||||
method: function() {
|
||||
var f = this,
|
||||
old_val = this.settings.right_column_width || 340;
|
||||
|
||||
utils.prompt("Right Sidebar Width", "Please enter a new width for the right sidebar, in pixels.</p><p><b>Minimum:</b> 250<br><b>Default:</b> 340", old_val, function(new_val) {
|
||||
if ( new_val === null || new_val === undefined )
|
||||
return;
|
||||
|
||||
var width = parseInt(new_val);
|
||||
if ( ! width || Number.isNaN(width) || ! Number.isFinite(width) )
|
||||
width = 340;
|
||||
|
||||
f.settings.set('right_column_width', Math.max(250, width));
|
||||
});
|
||||
},
|
||||
|
||||
on_update: function(val) {
|
||||
if ( this.has_bttv )
|
||||
return;
|
||||
|
||||
var Layout = utils.ember_lookup('service:layout');
|
||||
if ( ! Layout )
|
||||
return;
|
||||
|
||||
Layout.set('rightColumnWidth', val);
|
||||
Ember.propertyDidChange(Layout, 'contentWidth');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.minimize_navigation = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
|
||||
category: "Sidebar",
|
||||
no_mobile: true,
|
||||
no_bttv: true,
|
||||
|
||||
name: "Minimize Navigation",
|
||||
help: "Slide the navigation bar mostly out of view when it's not being used.",
|
||||
|
||||
on_update: function(val) {
|
||||
if ( this.has_bttv )
|
||||
return;
|
||||
|
||||
var Layout = utils.ember_lookup('service:layout');
|
||||
if ( ! Layout )
|
||||
return;
|
||||
|
||||
utils.toggle_cls('ffz-sidebar-minimize')(val);
|
||||
Layout.set('ffzMinimizeNavigation', val);
|
||||
//Ember.propertyDidChange(Layout, 'contentWidth');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --------------------
|
||||
// Initialization
|
||||
// --------------------
|
||||
|
||||
FFZ.prototype.setup_layout = function() {
|
||||
if ( this.has_bttv )
|
||||
return;
|
||||
|
||||
utils.toggle_cls("ffz-sidebar-swap")(this.settings.swap_sidebars);
|
||||
utils.toggle_cls('ffz-sidebar-minimize')(this.settings.minimize_navigation);
|
||||
|
||||
this.log("Creating layout style element.");
|
||||
var s = this._layout_style = document.createElement('style');
|
||||
s.id = 'ffz-layout-css';
|
||||
document.head.appendChild(s);
|
||||
|
||||
try {
|
||||
route_helper = window.require("web-client/utilities/route-matcher");
|
||||
} catch(err) {
|
||||
this.error("Unable to require the route-matcher utility.", err);
|
||||
}
|
||||
|
||||
var Layout = utils.ember_lookup('service:layout'),
|
||||
LS = function(x) { return x },
|
||||
f = this;
|
||||
|
||||
if ( ! Layout )
|
||||
return this.log("Unable to locate the Ember service:layout");
|
||||
|
||||
try {
|
||||
LS = window.require("web-client/utilities/layout-scaling").scalePixelValue;
|
||||
} catch(err) { }
|
||||
|
||||
this.log("Hooking the Ember service:layout");
|
||||
|
||||
Layout.reopen({
|
||||
rightColumnWidth: 340,
|
||||
rawPortraitMode: 0,
|
||||
|
||||
portraitVideoBelow: false,
|
||||
|
||||
channelCoverHeight: function() {
|
||||
var setting = f.settings.hide_channel_banner,
|
||||
banner_hidden = setting === 1 ? f.settings.channel_bar_bottom : setting > 0;
|
||||
|
||||
return ( banner_hidden || ! route_helper || route_helper.routeMatches && !route_helper.routeMatches(this.get('globals.currentPath'), route_helper.ROUTES.CHANNEL_ANY) ) ?
|
||||
0 : 380;
|
||||
|
||||
}.property("globals.currentPath"),
|
||||
|
||||
portraitMode: function() {
|
||||
var raw = this.get("rawPortraitMode");
|
||||
this.set('portraitVideoBelow', raw === 3 || raw === 4);
|
||||
|
||||
if ( raw === 0 )
|
||||
return false;
|
||||
if ( raw === 2 || raw === 4 )
|
||||
return true;
|
||||
|
||||
// Not sure if I should be adding some other value to offset the ratio. What feels best?
|
||||
var ratio = this.get("windowWidth") / (this.get("windowHeight") + 120 + 60);
|
||||
return ratio < 1;
|
||||
|
||||
}.property("rawPortraitMode", "windowHeight", "windowWidth"),
|
||||
|
||||
isTooSmallForRightColumn: function() {
|
||||
if ( ! f.has_bttv && this.get('portraitMode') ) {
|
||||
var size = this.get('fullSizePlayerDimensions'),
|
||||
extra = this.get('ffzExtraHeight'),
|
||||
height = size.height + extra;
|
||||
|
||||
// Make sure we have at least a bit of room for the chat.
|
||||
return this.get("windowHeight") < height;
|
||||
|
||||
} else
|
||||
return this.get("windowWidth") < (1090 - this.get('rightColumnWidth'))
|
||||
|
||||
}.property("ffzExtraHeight", "windowWidth", "rightColumnWidth", "fullSizePlayerDimensions", "windowHeight"),
|
||||
|
||||
contentWidth: function() {
|
||||
var left_width = LS(f.settings.socialbar_hide ? 0 : this.get('isSocialColumnCollapsed') ? 50 : 240),
|
||||
right_width = ! f.has_bttv && this.get('portraitMode') ? 0 : this.get("isRightColumnClosed") ? 0 : this.get("rightColumnWidth");
|
||||
|
||||
return this.get("windowWidth") - left_width - right_width - LS(60);
|
||||
|
||||
}.property("windowWidth", 'ffzMinimizeNavigation', "portraitMode", "isRightColumnClosed", "rightColumnWidth", "isSocialColumnCollapsed"),
|
||||
|
||||
ffzExtraHeight: function() {
|
||||
return (this.get('ffzMinimizeNavigation') ? 10 : 50) +
|
||||
(f.settings.channel_bar_collapse ? 10 : 60) + 15 +
|
||||
(f.settings.channel_title_top === 2 ? 20 : f.settings.channel_title_top > 0 ? 55 : 0) +
|
||||
(f.settings.channel_title_top ? 70 : 80);
|
||||
}.property("ffzMinimizeNavigation"),
|
||||
|
||||
fullSizePlayerDimensions: function() {
|
||||
var h = this.get('windowHeight'),
|
||||
c = this.get('PLAYER_CONTROLS_HEIGHT'),
|
||||
r = this.get('contentWidth'),
|
||||
|
||||
extra_height = this.get('ffzExtraHeight'),
|
||||
extra_theater_height = 120 + (f.settings.channel_bar_collapse ? 10 : 60) + 40,
|
||||
|
||||
i = Math.round(9 * r / 16) + c,
|
||||
d = h - extra_height,
|
||||
e = h - extra_theater_height,
|
||||
|
||||
l = Math.floor(r),
|
||||
o = Math.floor(Math.min(i, d)),
|
||||
s = Math.floor(Math.min(i, e));
|
||||
|
||||
return {
|
||||
width: l,
|
||||
height: o,
|
||||
targetHeight: s
|
||||
}
|
||||
|
||||
}.property("ffzExtraHeight", "contentWidth", "windowHeight", "portraitMode", "PLAYER_CONTROLS_HEIGHT"),
|
||||
|
||||
playerStyle: function() {
|
||||
var size = this.get('fullSizePlayerDimensions');
|
||||
|
||||
return '<style>' +
|
||||
'.dynamic-player, .dynamic-player object, .dynamic-player video {' +
|
||||
'width:' + size.width + 'px !important;' +
|
||||
'height:' + size.height + 'px !important}' +
|
||||
'.dynamic-target-player, .dynamic-target-player object, .dynamic-target-player video {' +
|
||||
'width:' + size.width + 'px !important;' +
|
||||
'height:' + size.targetHeight + 'px !important}' +
|
||||
'.dynamic-player .player object,' +
|
||||
'.dynamic-player .player video {' +
|
||||
'width: 100% !important;' +
|
||||
'height: 100% !important}';
|
||||
|
||||
}.property("fullSizePlayerDimensions"),
|
||||
|
||||
ffzPortraitWarning: function() {
|
||||
var t = this;
|
||||
// Delay this, in case we're just resizing the window.
|
||||
setTimeout(function() {
|
||||
if ( ! f.settings.portrait_mode || f._portrait_warning || f.settings.portrait_warning || document.body.getAttribute('data-current-path').indexOf('user.') !== 0 || ! t.get('isTooSmallForRightColumn') )
|
||||
return;
|
||||
|
||||
f._portrait_warning = true;
|
||||
f.show_message('Twitch\'s Chat Sidebar has been hidden as a result of FrankerFaceZ\'s Portrait Mode because the window is too wide.<br><br>Please <a href="#" onclick="ffz.settings.set(\'portrait_mode\',0);jQuery(this).parents(\'.ffz-noty\').remove();ffz._portrait_warning = false;return false">disable Portrait Mode</a> or make your window narrower.<br><br><a href="#" onclick="ffz.settings.set(\'portrait_warning\',true);jQuery(this).parents(\'.ffz-noty\').remove();return false">Do not show this message again</a>');
|
||||
}, 50);
|
||||
|
||||
}.observes("isTooSmallForRightColumn"),
|
||||
|
||||
ffzUpdateCss: function() {
|
||||
var window_height = this.get('windowHeight'),
|
||||
window_width = this.get('windowWidth'),
|
||||
width = this.get('rightColumnWidth'),
|
||||
out = '';
|
||||
|
||||
if ( ! f.has_bttv ) {
|
||||
if ( ! this.get('isRightColumnClosed') ) {
|
||||
if ( this.get('portraitMode') ) {
|
||||
var size = this.get('fullSizePlayerDimensions'),
|
||||
video_below = this.get('portraitVideoBelow'),
|
||||
top_height = this.get('ffzMinimizeNavigation') ? 10 : 50,
|
||||
|
||||
video_height = size.height + this.get('ffzExtraHeight'),
|
||||
chat_height = window_height - video_height,
|
||||
|
||||
video_top = video_below ? chat_height : 0,
|
||||
video_bottom = window_height - (video_top + video_height),
|
||||
chat_top = video_below ? 0 : video_height,
|
||||
|
||||
theatre_video_height = Math.floor(Math.max(window_height * 0.1, Math.min(window_height - 300, 9 * window_width / 16))),
|
||||
theatre_chat_height = window_height - theatre_video_height,
|
||||
|
||||
theatre_video_top = video_below ? theatre_chat_height : 0,
|
||||
theatre_video_bottom = window_height - (theatre_video_top + theatre_video_height),
|
||||
theatre_chat_top = video_below ? 0 : theatre_video_height;
|
||||
|
||||
out += '.player-mini {' +
|
||||
'bottom: ' + (10 + video_bottom) + 'px}' +
|
||||
'.ffz-channel-bar-bottom .player-mini {' +
|
||||
'bottom: ' + (60 + video_bottom) + 'px}' +
|
||||
'.ffz-channel-bar-bottom.ffz-minimal-channel-bar .player-mini {' +
|
||||
'bottom: ' + (20 + video_bottom) + 'px}' +
|
||||
'.ffz-sidebar-swap .player-mini {' +
|
||||
'left: 10px !important}' +
|
||||
'body[data-current-path^="user."] #left_col .warp { min-height: inherit }' +
|
||||
'body[data-current-path^="user."] #left_col { overflow: hidden }' +
|
||||
'body[data-current-path^="user."] .social-column {' +
|
||||
'top:' + video_top + 'px;' +
|
||||
'height:' + (video_height - top_height) + 'px}' +
|
||||
'body[data-current-path^="user."] #left_col .warp,' +
|
||||
'body[data-current-path^="user."] #left_col,' +
|
||||
'body[data-current-path^="user."] .searchPanel--slide,' +
|
||||
'body[data-current-path^="user."]:not(.ffz-sidebar-swap) #main_col{' +
|
||||
'margin-right:0 !important;' +
|
||||
'top:' + video_top + 'px;' +
|
||||
'height:' + (video_height - top_height) + 'px}' +
|
||||
'body[data-current-path^="user."].ffz-sidebar-swap #main_col{' +
|
||||
'margin-left:0 !important;' +
|
||||
'top:' + video_top + 'px;' +
|
||||
'height:' + (video_height - top_height) + 'px}' +
|
||||
'body[data-current-path^="user."] #right_col{' +
|
||||
'width:100%;' +
|
||||
'top:' + (video_below ? chat_top : chat_top - top_height) + 'px;' +
|
||||
'height:' + chat_height + 'px}' +
|
||||
'body[data-current-path^="user."] .app-main.theatre .social-column,' +
|
||||
'body[data-current-path^="user."] .app-main.theatre #left_col .warp,' +
|
||||
'body[data-current-path^="user."] .app-main.theatre #left_col,' +
|
||||
'body[data-current-path^="user."] .app-main.theatre #player,' +
|
||||
'body[data-current-path^="user."] .app-main.theatre #main_col{' +
|
||||
'top:' + theatre_video_top + 'px;' +
|
||||
'height:' + theatre_video_height + 'px !important}' +
|
||||
'body[data-current-path^="user."] .app-main.theatre #right_col{' +
|
||||
'top:' + theatre_chat_top + 'px;' +
|
||||
'height:' + theatre_chat_height + 'px}' +
|
||||
'.app-main.theatre #player {' +
|
||||
'left: 0 !important;right: 0 !important}' +
|
||||
'body.ffz-minimal-channel-bar:not(.ffz-channel-bar-bottom) .cn-bar-fixed {' +
|
||||
'top: ' + (video_top - 40) + 'px}' +
|
||||
'body.ffz-minimal-channel-bar:not(.ffz-channel-bar-bottom) .cn-bar-fixed:hover,' +
|
||||
'body:not(.ffz-channel-bar-bottom) .cn-bar-fixed {' +
|
||||
'top: ' + video_top + 'px}' +
|
||||
'.ffz-minimal-channel-bar.ffz-channel-bar-bottom .cn-bar {' +
|
||||
'bottom: ' + (video_bottom - 40) + 'px}' +
|
||||
'.ffz-minimal-channel-bar.ffz-channel-bar-bottom .cn-bar:hover,' +
|
||||
'.ffz-channel-bar-bottom .cn-bar {' +
|
||||
'bottom: ' + video_bottom + 'px}' +
|
||||
'body:not(.ffz-sidebar-swap) .cn-bar-fixed { right: 0 !important }' +
|
||||
'body.ffz-sidebar-swap .cn-bar-fixed { left: 0 !important }' +
|
||||
'.ffz-theater-stats .app-main.theatre .cn-hosting--bottom,' +
|
||||
'.ffz-theater-stats .app-main.theatre .cn-metabar__more {' +
|
||||
'max-width:calc(100% - 350px);' +
|
||||
'bottom:' + (theatre_video_bottom + 85) + 'px !important}' +
|
||||
'.ffz-theater-stats:not(.ffz-theatre-conversations):not(.ffz-top-conversations) .app-main.theatre .cn-metabar__more {' +
|
||||
'bottom:' + (theatre_video_bottom + 130) + 'px !important}' +
|
||||
(video_below ? '.js-player-persistent {' +
|
||||
'margin-top:-' + video_top + 'px}' +
|
||||
'.ffz-sidebar-minimize .has-sc .js-player-persistent {' +
|
||||
'margin-top:-' + (video_top - 40) + 'px}' : '');
|
||||
|
||||
} else {
|
||||
out += '.ffz-sidebar-swap .player-mini{left:' + (width + 10) + 'px !important}' +
|
||||
'#main_col.expandRight #right_close{left: none !important}' +
|
||||
'#right_col{width:' + width + 'px}' +
|
||||
'body:not(.ffz-sidebar-swap) #main_col:not(.expandRight){' +
|
||||
'margin-right:' + width + 'px !important}' +
|
||||
'body.ffz-sidebar-swap .theatre #main_col:not(.expandRight),' +
|
||||
'body.ffz-sidebar-swap #main_col:not(.expandRight){' +
|
||||
'margin-left:' + width + 'px !important}' +
|
||||
'body:not(.ffz-sidebar-swap) .app-main.theatre #main_col:not(.expandRight) #player {' +
|
||||
'right: ' + width + 'px !important}' +
|
||||
'body.ffz-sidebar-swap .app-main.theatre #main_col:not(.expandRight) #player {' +
|
||||
'right: 0 !important;' +
|
||||
'left:' + width + 'px !important}' +
|
||||
'body:not(.ffz-sidebar-swap) #main_col:not(.expandRight) .cn-bar-fixed {' +
|
||||
'right: ' + width + 'px}' +
|
||||
'body.ffz-sidebar-swap .theatre .cn-hosting--bottom,' +
|
||||
'body.ffz-sidebar-swap .theatre .cn-metabar__more {' +
|
||||
'left: ' + (width + 10) + 'px !important}' +
|
||||
'body.ffz-sidebar-swap #main_col:not(.expandRight) .cn-bar-fixed {' +
|
||||
'left: ' + width + 'px !important}' +
|
||||
'.ffz-theater-stats .app-main.theatre .cn-hosting--bottom,' +
|
||||
'.ffz-theater-stats .app-main.theatre .cn-metabar__more {' +
|
||||
'max-width: calc(100% - ' + (width + 350) + 'px)}';
|
||||
}
|
||||
}
|
||||
|
||||
f._layout_style.innerHTML = out;
|
||||
}
|
||||
|
||||
}.observes("ffzExtraHeight", "isRightColumnClosed", "fullSizePlayerDimensions", "rightColumnWidth", "portraitMode", "windowHeight", "windowWidth"),
|
||||
|
||||
ffzUpdatePlayerStyle: function() {
|
||||
Ember.propertyDidChange(Layout, 'playerStyle');
|
||||
}.observes('windowHeight', 'windowWidth'),
|
||||
|
||||
ffzUpdatePortraitCSS: function() {
|
||||
var portrait = this.get("portraitMode");
|
||||
document.body.classList.toggle("ffz-portrait", ! f.has_bttv && portrait);
|
||||
|
||||
}.observes("portraitMode"),
|
||||
|
||||
ffzFixTabs: function() {
|
||||
if ( f.settings.group_tabs && f._chatv && f._chatv._ffz_tabs ) {
|
||||
setTimeout(function() {
|
||||
var cr = f._chatv && f._chatv.$('.chat-room');
|
||||
cr && cr.css && cr.css('top', f._chatv._ffz_tabs.offsetHeight + "px");
|
||||
},0);
|
||||
}
|
||||
}.observes("isRightColumnClosed", "rightColumnWidth", "portraitMode", "fullSizePlayerDimensions")
|
||||
});
|
||||
|
||||
|
||||
// Force the layout to update.
|
||||
Layout.set('rightColumnWidth', this.settings.right_column_width);
|
||||
Layout.set('rawPortraitMode', this.settings.portrait_mode);
|
||||
Layout.set('ffzMinimizeNavigation', this.settings.minimize_navigation);
|
||||
|
||||
// Force re-calculation of everything.
|
||||
Ember.propertyDidChange(Layout, 'windowWidth');
|
||||
Ember.propertyDidChange(Layout, 'windowHeight');
|
||||
Ember.propertyDidChange(Layout, 'ffzExtraHeight');
|
||||
Ember.propertyDidChange(Layout, 'isTooSmallForRightColumn');
|
||||
Ember.propertyDidChange(Layout, 'fullSizePlayerDimensions');
|
||||
Layout.ffzUpdatePortraitCSS();
|
||||
}
|
2127
src/ember/line.js
2127
src/ember/line.js
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,307 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ,
|
||||
utils = require('../utils'),
|
||||
constants = require('../constants');
|
||||
|
||||
|
||||
// ---------------
|
||||
// Settings
|
||||
// ---------------
|
||||
|
||||
FFZ.settings_info.player_stats = {
|
||||
type: 'select',
|
||||
options: {
|
||||
0: ['Disabled', -2],
|
||||
'-1': ['Monochrome', -1],
|
||||
10: 'Warning Colors (10s+)',
|
||||
15: 'Warning Colors (15s+)',
|
||||
20: 'Warning Colors (20s+)',
|
||||
25: 'Warning Colors (25s+)',
|
||||
30: 'Warning Colors (30s+)',
|
||||
},
|
||||
|
||||
value: 0,
|
||||
process_value: utils.process_int(0, 0, -1),
|
||||
|
||||
no_mobile: true,
|
||||
|
||||
category: "Channel Metadata",
|
||||
|
||||
name: "Stream Latency",
|
||||
help: "Display your current stream latency (how far behind the broadcast you are) under the player, with a few useful statistics in a tooltip.",
|
||||
|
||||
on_update: function(val) {
|
||||
if ( this._cindex )
|
||||
this._cindex.ffzUpdateMetadata('player_stats');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.classic_player = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
no_mobile: true,
|
||||
|
||||
category: "Player",
|
||||
|
||||
name: "Classic Player",
|
||||
help: "Alter the appearance of the player to resemble the older Twitch player with always visible controls.",
|
||||
|
||||
on_update: function(val) {
|
||||
utils.toggle_cls('ffz-classic-player')(val);
|
||||
var Layout = utils.ember_lookup('service:layout');
|
||||
if ( Layout )
|
||||
Layout.set('PLAYER_CONTROLS_HEIGHT', val ? 32 : 0);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.player_volume_bar = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
no_mobile: true,
|
||||
|
||||
category: "Player",
|
||||
|
||||
name: "Volume Always Expanded",
|
||||
help: "Keep the volume slider expanded even when not hovering over it with the mouse.",
|
||||
|
||||
on_update: utils.toggle_cls('ffz-player-volume')
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.player_volume_scroll = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
no_mobile: true,
|
||||
|
||||
category: "Player",
|
||||
|
||||
name: "Adjust Volume by Scrolling",
|
||||
help: "Adjust the player's volume by scrolling up and down with your mouse wheel."
|
||||
};
|
||||
|
||||
|
||||
/*FFZ.settings_info.player_pause_hosts = {
|
||||
type: "select",
|
||||
options: {
|
||||
0: "Disabled",
|
||||
1: "When Hosting Channel was Paused",
|
||||
2: "Always"
|
||||
},
|
||||
|
||||
value: 1,
|
||||
process_value: utils.process_int(1),
|
||||
|
||||
category: "Player",
|
||||
name: "Auto-Pause Hosted Channels",
|
||||
help: "Automatically pause hosted channels if you paused the channel doing the hosting, or just pause all hosts."
|
||||
}*/
|
||||
|
||||
|
||||
// ---------------
|
||||
// Initialization
|
||||
// ---------------
|
||||
|
||||
FFZ.prototype.setup_player = function() {
|
||||
utils.toggle_cls('ffz-player-volume')(this.settings.player_volume_bar);
|
||||
utils.toggle_cls('ffz-classic-player')(this.settings.classic_player);
|
||||
|
||||
var Layout = utils.ember_lookup('service:layout');
|
||||
if ( Layout )
|
||||
Layout.set('PLAYER_CONTROLS_HEIGHT', this.settings.classic_player ? 32 : 0);
|
||||
|
||||
this.update_views('component:twitch-player2', this.modify_twitch_player);
|
||||
this.update_views('component:persistent-player', this.modify_persistent_player);
|
||||
}
|
||||
|
||||
|
||||
// ---------------
|
||||
// Component
|
||||
// ---------------
|
||||
|
||||
FFZ.prototype.modify_persistent_player = function(player) {
|
||||
var f = this;
|
||||
utils.ember_reopen_view(player, {
|
||||
ffz_init: function() {
|
||||
var t = this;
|
||||
this.$().off('mousewheel').on('mousewheel', function(event) {
|
||||
if ( ! f.settings.player_volume_scroll )
|
||||
return;
|
||||
|
||||
// I ain't about that life, jQuery.
|
||||
event = event.originalEvent || event;
|
||||
var delta = event.wheelDelta || -event.detail,
|
||||
player = t.childViews && t.childViews[0] && t.childViews[0].get('player');
|
||||
|
||||
if ( player )
|
||||
player.volume = Math.max(0, Math.min(1, player.volume + (delta > 0 ? .1 : -.1)));
|
||||
|
||||
event.preventDefault();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.modify_twitch_player = function(player) {
|
||||
var f = this;
|
||||
utils.ember_reopen_view(player, {
|
||||
ffz_init: function() {
|
||||
// We can have multiple players in page now, thanks to persistent players.
|
||||
// Usually the second one will be something we don't want though. Like
|
||||
// the creative showcase.
|
||||
if ( ! f._player || f._player.isDestroying || f._player.isDestroyed )
|
||||
f._player = this;
|
||||
|
||||
var player = this.get('player');
|
||||
if ( player && !this.get('ffz_post_player') )
|
||||
this.ffzPostPlayer();
|
||||
},
|
||||
|
||||
ffz_destroy: function() {
|
||||
if ( f._player === this )
|
||||
f._player = undefined;
|
||||
},
|
||||
|
||||
/*insertPlayer: function(ffz_reset) {
|
||||
// We want to see if this is a hosted video on a play
|
||||
var should_start_paused = this.get('shouldStartPaused'),
|
||||
channel_id = this.get('hostChannel.name'),
|
||||
hosted_id = this.get('channel.name'),
|
||||
is_hosting = channel_id !== hosted_id;
|
||||
|
||||
// Always start unpaused if the person used the FFZ setting to Reset Player.
|
||||
if ( ffz_reset )
|
||||
this.set('shouldStartPaused', false);
|
||||
|
||||
// Alternatively, depending on the setting...
|
||||
else if ( f.settings.player_pause_hosts === 2 && is_hosting )
|
||||
this.set('shouldStartPaused', true);
|
||||
|
||||
this._super();
|
||||
|
||||
// Restore the previous value so it doesn't mess anything up.
|
||||
this.set('shouldStartPaused', should_start_paused);
|
||||
|
||||
}.on('didInsertElement'),*/
|
||||
|
||||
postPlayerSetup: function() {
|
||||
this._super();
|
||||
try {
|
||||
if ( ! this.get('ffz_post_player') )
|
||||
this.ffzPostPlayer();
|
||||
} catch(err) {
|
||||
f.error("Player2 postPlayerSetup: " + err);
|
||||
}
|
||||
},
|
||||
|
||||
ffzRecreatePlayer: function() {
|
||||
var t = this,
|
||||
player = this.get('player'),
|
||||
theatre, fullscreen, had_player = false;
|
||||
|
||||
// Tell the player to destroy itself.
|
||||
if ( player ) {
|
||||
had_player = true;
|
||||
fullscreen = player.fullscreen;
|
||||
theatre = player.theatre;
|
||||
player.fullscreen = false;
|
||||
player.theatre = false;
|
||||
player.destroy();
|
||||
}
|
||||
|
||||
// Break down everything left over from that player.
|
||||
this.$('#player').html('');
|
||||
Mousetrap.unbind(['alt+x', 'alt+t', 'esc']);
|
||||
this.set('player', null);
|
||||
this.set('ffz_post_player', false);
|
||||
|
||||
// Now, let Twitch create a new player as usual.
|
||||
Ember.run.next(function() {
|
||||
t.didInsertElement();
|
||||
had_player && setTimeout(function() {
|
||||
var player = t.get('player');
|
||||
if ( player ) {
|
||||
//player.fullscreen = fullscreen;
|
||||
player.theatre = theatre;
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
/*ffzUpdatePlayerPaused: function() {
|
||||
var channel_id = this.get('hostChannel.name'),
|
||||
hosted_id = this.get('channel.name'),
|
||||
is_hosting = channel_id !== hosted_id,
|
||||
|
||||
player = this.get('player'),
|
||||
is_paused = player.paused;
|
||||
|
||||
f.log("Player Pause State for " + channel_id + ": " + is_paused);
|
||||
|
||||
if ( ! is_hosting ) {
|
||||
this.set('ffz_host_paused', false);
|
||||
this.set('ffz_original_paused', is_paused);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! f.settings.player_pause_hosts || is_paused || this.get('ffz_host_paused') )
|
||||
return;
|
||||
|
||||
this.set('ffz_host_paused', true);
|
||||
|
||||
if ( this.get('ffz_original_paused') || f.settings.player_pause_hosts === 2 )
|
||||
player.pause();
|
||||
},
|
||||
|
||||
ffzHostChange: function() {
|
||||
this.set('ffz_host_paused', false);
|
||||
}.observes('channel'),*/
|
||||
|
||||
ffzPostPlayer: function() {
|
||||
var t = this,
|
||||
/*channel_id = this.get('hostChannel.name'),
|
||||
hosted_id = this.get('channel.name'),
|
||||
is_hosting = channel_id !== hosted_id,*/
|
||||
|
||||
player = this.get('player');
|
||||
if ( ! player )
|
||||
return;
|
||||
|
||||
this.set('ffz_post_player', true);
|
||||
|
||||
//if ( ! is_hosting )
|
||||
// this.set('ffz_original_paused', player.paused);
|
||||
|
||||
//player.addEventListener('pause', this.ffzUpdatePlayerPaused.bind(this));
|
||||
//player.addEventListener('play', this.ffzUpdatePlayerPaused.bind(this));
|
||||
|
||||
// Make the stats window draggable and fix the button.
|
||||
var stats = this.$('.player .js-playback-stats');
|
||||
stats.draggable({cancel: 'li', containment: 'parent'});
|
||||
|
||||
// Add an option to the menu to recreate the player.
|
||||
var t = this,
|
||||
el = this.$('.player-buttons-right .pl-flex')[0],
|
||||
container = el && el.parentElement;
|
||||
|
||||
if ( el && ! container.querySelector('.ffz-player-reset') ) {
|
||||
var btn = utils.createElement('button', 'player-button player-button--reset ffz-player-reset');
|
||||
btn.type = 'button';
|
||||
|
||||
btn.innerHTML = '<span class="player-tip js-control-tip" data-tip="Double-Click to Reset Player"></span>' +
|
||||
constants.CLOSE;
|
||||
|
||||
jQuery(btn).on('dblclick', function(e) {
|
||||
//btn.addEventListener('click', function(e) {
|
||||
t.ffzRecreatePlayer();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
|
||||
container.insertBefore(btn, el.nextSibling);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
2887
src/ember/room.js
2887
src/ember/room.js
File diff suppressed because it is too large
Load diff
|
@ -1,47 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ,
|
||||
utils = require("../utils");
|
||||
|
||||
|
||||
// --------------------
|
||||
// Initialization
|
||||
// --------------------
|
||||
|
||||
FFZ.prototype.setup_router = function() {
|
||||
this.log("Hooking the Ember router.");
|
||||
if ( ! window.App )
|
||||
return;
|
||||
|
||||
var f = this,
|
||||
Router = utils.ember_lookup('router:main');
|
||||
|
||||
if ( Router )
|
||||
Router.reopen({
|
||||
ffzTransition: function() {
|
||||
// TODO: Do this before the transition happens.
|
||||
if ( f._force_refresh ) {
|
||||
location.href = this.get('url');
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're coming from a page without app-main, make sure we install the
|
||||
// scroll listener.
|
||||
f.fix_scroll();
|
||||
f.try_modify_dashboard();
|
||||
|
||||
try {
|
||||
document.body.setAttribute('data-current-path', App.get('currentPath'));
|
||||
} catch(err) {
|
||||
f.error("ffzTransition: " + err);
|
||||
}
|
||||
}.on('didTransition')
|
||||
});
|
||||
|
||||
document.body.setAttribute('data-current-path', App.get('currentPath'));
|
||||
}
|
||||
|
||||
|
||||
|
||||
FFZ.ws_commands.please_refresh = function() {
|
||||
this.log("Refreshing the page upon the next transition.");
|
||||
this._force_refresh = true;
|
||||
}
|
|
@ -1,514 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ,
|
||||
utils = require('../utils'),
|
||||
constants = require('../constants'),
|
||||
|
||||
PRESENCE_SERVICE = 'service:twitch-presence/presence',
|
||||
was_invisible = false;
|
||||
|
||||
|
||||
// --------------------
|
||||
// Settings
|
||||
// --------------------
|
||||
|
||||
/*FFZ.settings_info.sidebar_followed_games = {
|
||||
type: "select",
|
||||
options: {
|
||||
0: "Disabled",
|
||||
5: "Normal (5)",
|
||||
10: "Large (10)",
|
||||
999: "No Limit"
|
||||
},
|
||||
|
||||
value: 5,
|
||||
process_value: utils.process_int(5),
|
||||
|
||||
no_mobile: true,
|
||||
|
||||
category: "Sidebar",
|
||||
name: "Followed Games",
|
||||
help: "Display this number of followed games on the sidebar.",
|
||||
|
||||
on_update: function(val) {
|
||||
var controller = utils.ember_lookup('controller:games-following');
|
||||
if ( controller )
|
||||
controller.set('ffz_sidebar_games', val);
|
||||
}
|
||||
};*/
|
||||
|
||||
|
||||
FFZ.settings_info.sidebar_hide_recommended_channels = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
|
||||
category: "Sidebar",
|
||||
no_mobile: true,
|
||||
|
||||
name: "Hide Recommended Channels",
|
||||
help: "Hide the Recommended Channels section from the sidebar.",
|
||||
|
||||
on_update: utils.toggle_cls('ffz-hide-recommended-channels')
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.socialbar_hide = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
|
||||
no_mobile: true,
|
||||
|
||||
category: "Sidebar",
|
||||
name: "Hide Social Bar",
|
||||
help: "Hide the social bar to the left of the page.",
|
||||
|
||||
on_update: function(val) {
|
||||
utils.toggle_cls('ffz-hide-socialbar')(val);
|
||||
var Layout = utils.ember_lookup('service:layout');
|
||||
Layout && Ember.propertyDidChange(Layout, 'contentWidth');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
FFZ.settings_info.sidebar_hide_prime = {
|
||||
type: "select",
|
||||
options: {
|
||||
0: "Disabled",
|
||||
1: "When Collapsed",
|
||||
2: "Always"
|
||||
},
|
||||
|
||||
value: 0,
|
||||
process_value: utils.process_int(0),
|
||||
|
||||
no_mobile: true,
|
||||
|
||||
category: "Sidebar",
|
||||
name: "Hide Twitch Prime Offers",
|
||||
help: "Hide the Free with Prime section from the sidebar.",
|
||||
|
||||
on_update: function(val) {
|
||||
utils.toggle_cls('ffz-hide-prime')(val === 2);
|
||||
utils.toggle_cls('ffz-hide-prime-collapsed')(val === 1);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.sidebar_hide_promoted_games = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
|
||||
category: "Sidebar",
|
||||
no_mobile: true,
|
||||
|
||||
name: "Hide Promoted Games",
|
||||
help: "Hide the Promoted Games section from the sidebar.",
|
||||
|
||||
on_update: utils.toggle_cls('ffz-hide-promoted-games')
|
||||
};
|
||||
|
||||
|
||||
|
||||
FFZ.settings_info.sidebar_hide_recommended_friends = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
|
||||
category: "Sidebar",
|
||||
no_mobile: true,
|
||||
|
||||
name: "Hide Recommended Friends",
|
||||
help: "Hide the Recommended Friends section from the sidebar.",
|
||||
|
||||
on_update: utils.toggle_cls('ffz-hide-recommended-friends')
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.sidebar_hide_friends_collapsed = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
|
||||
category: "Sidebar",
|
||||
no_mobile: true,
|
||||
|
||||
name: "Hide Friends when Collapsed",
|
||||
help: "Hide your friends from the sidebar when it's collapsed.",
|
||||
|
||||
on_update: utils.toggle_cls('ffz-hide-friends-collapsed')
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.sidebar_hide_more_at_twitch = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
|
||||
category: "Sidebar",
|
||||
no_mobile: true,
|
||||
|
||||
name: "Hide More at Twitch",
|
||||
help: "Hide the More at Twitch section from the sidebar.",
|
||||
|
||||
on_update: utils.toggle_cls('ffz-hide-more-at-twitch')
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.disable_friend_notices = {
|
||||
type: 'boolean',
|
||||
value: false,
|
||||
|
||||
category: 'Chat Filtering',
|
||||
no_mobile: true,
|
||||
|
||||
name: 'Disable Watching Friends Notices',
|
||||
help: 'Do not display notices in chat when your friends are watching the same stream.'
|
||||
};
|
||||
|
||||
|
||||
FFZ.settings_info.sidebar_disable_friends = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
|
||||
category: "Sidebar",
|
||||
no_mobile: true,
|
||||
|
||||
name: "Disable Friends",
|
||||
help: "Hide the Friends UI entirely and set you as Invisible.",
|
||||
|
||||
on_update: function(val) {
|
||||
utils.toggle_cls('ffz-hide-friends')(val);
|
||||
var presence = utils.ember_lookup(PRESENCE_SERVICE);
|
||||
if ( presence ) {
|
||||
if ( val )
|
||||
presence.setVisibility('none');
|
||||
else if ( ! was_invisible )
|
||||
presence.setVisibility('full');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var TWITCH_NAV_COLOR = "#4b367c",
|
||||
TWITCH_NAV_RGB = FFZ.Color.RGBA.fromCSS(TWITCH_NAV_COLOR),
|
||||
TWITCH_NAV_Luv = TWITCH_NAV_RGB.toLUVA();
|
||||
|
||||
FFZ.settings_info.top_nav_color = {
|
||||
type: "button",
|
||||
value: "#4b367c",
|
||||
|
||||
category: "Sidebar",
|
||||
no_mobile: true,
|
||||
|
||||
name: "Top Navigation Color",
|
||||
help: "Set a custom background color for the top navigation bar.",
|
||||
|
||||
on_update: function(val) {
|
||||
var process = true;
|
||||
val = val.trim();
|
||||
|
||||
if ( val.charAt(0) === '!' ) {
|
||||
process = false;
|
||||
val = val.substr(1).trimLeft();
|
||||
}
|
||||
|
||||
var color = val && FFZ.Color.RGBA.fromCSS(val),
|
||||
color_luv = color && color.toLUVA();
|
||||
|
||||
if ( ! val || process && ! color )
|
||||
return utils.update_css(this._theme_style, 'top-nav-color');
|
||||
|
||||
else if ( process && color_luv.l > TWITCH_NAV_Luv.l ) {
|
||||
color = color_luv._l(TWITCH_NAV_Luv.l).toRGBA();
|
||||
val = color.toCSS();
|
||||
}
|
||||
|
||||
var out = '.theme--dark .top-nav__menu,.top-nav__menu,.top-nav__drawer-anchor,.top-nav__logo{background-color:' + val + '}';
|
||||
|
||||
if ( color.luminance() > 0.2 ) {
|
||||
out += '.top-nav__search .form__icon svg,.top-nav .notification-center__icon svg,.top-nav .prime-logo-crown.prime-logo-crown--white svg,.top-nav__logo svg path, .top-nav__overflow svg path{fill: #000}' +
|
||||
'.top-nav__user-card:after{border-top-color:#000}' +
|
||||
'.top-nav__nav-link .pill{background-color:rgba(0,0,0,0.2)}' +
|
||||
'.top-nav__user-status,.ffz-dark .top-nav__nav-link,.top-nav__nav-link{color: #111!important}' +
|
||||
'.top-nav__nav-link .ffz-follow-count,.top-nav #user_display_name,.ffz-dark .top-nav__nav-link:hover,.top-nav__nav-link:hover{color: #000!important}' +
|
||||
'.ffz-dark .top-nav__search .form__input[type=text],.top-nav__search .form__input[type=text]{color:#000;background-color:rgba(0,0,0,0.05);box-shadow:rgba(0,0,0,0.2) 0 0 0 1px inset}' +
|
||||
'.ffz-dark .top-nav__search .form__input[type=text]:focus,.top-nav__search .form__input[type=text]:focus{box-shadow:rgba(0,0,0,0.4) 0 0 0 1px inset}';
|
||||
} else
|
||||
out += '.top-nav__nav-link{color: #d7d7d7}';
|
||||
|
||||
utils.update_css(this._theme_style, 'top-nav-color', out);
|
||||
},
|
||||
|
||||
method: function() {
|
||||
var f = this;
|
||||
utils.prompt(
|
||||
"Top Navigation Color",
|
||||
"Please enter a custom color for the top navigation bar. This supports any valid CSS color or color name.</p><p><b>Examples:</b> <code>red</code>, <code>orange</code>, <code>#333</code>, <code>rgb(255,127,127)</code></p><p><b>Note:</b> Colors will be darkened by default. To prevent a color being darkened, please start your input with an exclamation mark. Example: <code>!orange</code>",
|
||||
this.settings.top_nav_color,
|
||||
function(new_val) {
|
||||
if ( new_val === null || new_val === undefined )
|
||||
return;
|
||||
|
||||
new_val = new_val.trim();
|
||||
f.settings.set("top_nav_color", new_val);
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*FFZ.settings_info.sidebar_start_open = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
|
||||
category: "Sidebar",
|
||||
no_mobile: true,
|
||||
|
||||
name: "Automatically Open Drawer",
|
||||
help: "Open the drawer at the bottom of the sidebar by default when the page is loaded."
|
||||
};*/
|
||||
|
||||
|
||||
FFZ.settings_info.sidebar_directly_to_followed_channels = {
|
||||
type: "boolean",
|
||||
value: false,
|
||||
|
||||
category: "Sidebar",
|
||||
no_mobile: true,
|
||||
|
||||
name: "Open Following to Channels",
|
||||
help: "When going to your Following directory, view the Live Channels tab by default."
|
||||
};
|
||||
|
||||
|
||||
// --------------------
|
||||
// Initialization
|
||||
// --------------------
|
||||
|
||||
FFZ.prototype.setup_sidebar = function() {
|
||||
var s = this._theme_style = utils.createElement('style');
|
||||
s.id = 'ffz-theme-style';
|
||||
s.type = 'text/css';
|
||||
document.head.appendChild(s);
|
||||
|
||||
FFZ.settings_info.top_nav_color.on_update.call(this, this.settings.top_nav_color);
|
||||
|
||||
// CSS to Hide Stuff
|
||||
utils.toggle_cls('ffz-hide-promoted-games')(this.settings.sidebar_hide_promoted_games);
|
||||
utils.toggle_cls('ffz-hide-recommended-channels')(this.settings.sidebar_hide_recommended_channels);
|
||||
utils.toggle_cls('ffz-hide-recommended-friends')(this.settings.sidebar_hide_recommended_friends);
|
||||
utils.toggle_cls('ffz-hide-friends-collapsed')(this.settings.sidebar_hide_friends_collapsed);
|
||||
utils.toggle_cls('ffz-hide-more-at-twitch')(this.settings.sidebar_hide_more_at_twitch);
|
||||
utils.toggle_cls('ffz-hide-friends')(this.settings.sidebar_disable_friends);
|
||||
utils.toggle_cls('ffz-hide-prime')(this.settings.sidebar_hide_prime === 2);
|
||||
utils.toggle_cls('ffz-hide-prime-collapsed')(this.settings.sidebar_hide_prime === 1);
|
||||
utils.toggle_cls('ffz-hide-socialbar')(this.settings.socialbar_hide);
|
||||
|
||||
if ( this.settings.sidebar_disable_friends ) {
|
||||
try {
|
||||
var presence = utils.ember_lookup(PRESENCE_SERVICE);
|
||||
if ( presence ) {
|
||||
was_invisible = presence.get('isInvisible') || false;
|
||||
presence.setVisibility('none');
|
||||
}
|
||||
} catch(err) {
|
||||
this.error("Setting Friends Visibility", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Sidebar Followed Games
|
||||
/*var f = this,
|
||||
GamesFollowing = utils.ember_lookup('controller:games-following');
|
||||
|
||||
if ( GamesFollowing ) {
|
||||
this.log("Hooking the Ember games-following controller.");
|
||||
GamesFollowing.reopen({
|
||||
ffz_sidebar_games: this.settings.sidebar_followed_games,
|
||||
|
||||
sidePanelFollowing: function() {
|
||||
var content = this.get('liveFollowing.sortedContent'),
|
||||
limit = this.get('ffz_sidebar_games');
|
||||
|
||||
return limit === 999 ? content : _.first(content, limit);
|
||||
}.property("liveFollowing.@each", "ffz_sidebar_games")
|
||||
});
|
||||
|
||||
Ember.propertyDidChange(GamesFollowing, 'sidePanelFollowing');
|
||||
} else
|
||||
this.error("Unable to load the Ember games-following controller.", null);*/
|
||||
|
||||
|
||||
// Navigation Component
|
||||
var f = this;
|
||||
this.update_views('component:twitch-navigation', function(x) { return f.modify_navigation(x, false) });
|
||||
this.update_views('component:top-nav', function(x) { return f.modify_navigation(x, true) });
|
||||
this.update_views('component:recommended-channels', this.modify_recommended_channels);
|
||||
this.update_views('component:social-column/followed-channel', this.modify_social_followed_channel)
|
||||
|
||||
// Navigation Service
|
||||
/*var NavService = utils.ember_lookup('service:navigation');
|
||||
if ( NavService ) {
|
||||
// Open Drawer by Default
|
||||
var Layout = utils.ember_lookup('service:layout');
|
||||
if ( this.settings.sidebar_start_open && Layout && ! Layout.get('isSocialColumnEnabled') )
|
||||
NavService.set('isDrawerOpen', true);
|
||||
} else
|
||||
this.error("Unable to load the Ember Navigation service.")*/
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.setup_following_link = function() {
|
||||
var f = this,
|
||||
following_link = document.body.querySelector('#header_following');
|
||||
if ( following_link ) {
|
||||
following_link.href = '/directory/following' + (f.settings.sidebar_directly_to_followed_channels ? '/live' : '');
|
||||
following_link.addEventListener('click', function(e) {
|
||||
following_link.href = '/directory/following' + (f.settings.sidebar_directly_to_followed_channels ? '/live' : '');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.modify_recommended_channels = function(component) {
|
||||
utils.ember_reopen_view(component, {
|
||||
ffz_init: function() {
|
||||
var el = this.get('element');
|
||||
el.classList.add('js-recommended-channels');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
FFZ._sc_followed_tooltip_id = 0;
|
||||
|
||||
FFZ.prototype.modify_social_followed_channel = function(component) {
|
||||
var f = this;
|
||||
utils.ember_reopen_view(component, {
|
||||
ffzUpdateVisibility: function() {
|
||||
var el = this.get('element'),
|
||||
game = this.get('stream.game'),
|
||||
is_blocked = game ? f.settings.banned_games.indexOf(game.toLowerCase()) !== -1 : false;
|
||||
|
||||
el && el.classList.toggle('hidden', is_blocked);
|
||||
|
||||
}.observes('stream.game'),
|
||||
|
||||
ffz_init: function() {
|
||||
var t = this,
|
||||
el = this.get('element'),
|
||||
card = jQuery('.js-sc-card', el),
|
||||
data = card && card.data('tipsy');
|
||||
|
||||
this.ffzUpdateVisibility();
|
||||
|
||||
if ( ! data || ! data.options )
|
||||
return;
|
||||
|
||||
data.options.html = true;
|
||||
data.options.gravity = utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'w');
|
||||
data.options.title = function(el) {
|
||||
var old_text = t.get('tooltipText');
|
||||
if ( ! f.settings.following_count )
|
||||
return old_text; //utils.sanitize(old_text);
|
||||
|
||||
var tt_id = FFZ._sc_followed_tooltip_id++;
|
||||
utils.api.get("streams/" + t.get('stream.id'), null, {version: 5}).then(function(data) {
|
||||
var el = document.querySelector('#ffz-sc-tooltip-' + tt_id);
|
||||
if ( ! el || ! data || ! data.stream )
|
||||
return;
|
||||
|
||||
var channel = data.stream.channel,
|
||||
is_spoilered = f.settings.spoiler_games.indexOf(channel.game && channel.game.toLowerCase()) !== -1,
|
||||
|
||||
up_since = f.settings.stream_uptime && data.stream.created_at && utils.parse_date(data.stream.created_at),
|
||||
now = Date.now() - (f._ws_server_offset || 0),
|
||||
uptime = up_since && (Math.floor((now - up_since.getTime()) / 60000) * 60) || 0;
|
||||
|
||||
var cl = el.parentElement.parentElement.classList;
|
||||
cl.add('ffz-wide-tip');
|
||||
cl.add('ffz-follow-tip');
|
||||
|
||||
el.innerHTML = '<img class="ffz-image-hover" src="' + utils.quote_san(is_spoilered ? 'https://static-cdn.jtvnw.net/ttv-static/404_preview-320x180.jpg' : data.stream.preview.large) + '">' +
|
||||
'<span class="ffz-tt-channel-title">' + utils.sanitize(channel.status) + '</span><hr>' +
|
||||
(uptime > 0 ? '<span class="stat">' + constants.CLOCK + ' ' + utils.duration_string(uptime) + '</span>' : '') +
|
||||
'<span class="stat">' + constants.LIVE + ' ' + utils.number_commas(data.stream.viewers) + '</span>' +
|
||||
'<b>' + utils.sanitize(channel.display_name || channel.name) + '</b><br>' +
|
||||
'<span class="playing">' +
|
||||
(data.stream.stream_type === 'watch_party' ? '<span class="pill is-watch-party">Vodcast</span> ' : '') +
|
||||
(channel.game === 'Creative' ?
|
||||
'Being Creative' :
|
||||
(channel.game ?
|
||||
'Playing ' + utils.sanitize(channel.game) :
|
||||
'Not Playing')) +
|
||||
'</span>';
|
||||
});
|
||||
|
||||
return '<div id="ffz-sc-tooltip-' + tt_id + '">' + old_text + '</div>';
|
||||
};
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.modify_navigation = function(component, is_top_nav) {
|
||||
var f = this;
|
||||
|
||||
utils.ember_reopen_view(component, {
|
||||
ffz_init: function() {
|
||||
f._nav = this;
|
||||
|
||||
// Fix tooltips now that we've overrode the function.
|
||||
! is_top_nav && this._initTooltips();
|
||||
|
||||
// Override behavior for the Following link.
|
||||
var el = this.get('element'),
|
||||
following_link = el && el.querySelector(is_top_nav ? 'a[data-tt_content="directory_following"]' : 'a[data-href="following"]');
|
||||
|
||||
if ( following_link ) {
|
||||
following_link.href = '/directory/following' + (f.settings.sidebar_directly_to_followed_channels ? '/live' : '');
|
||||
|
||||
following_link.addEventListener('click', function(e) {
|
||||
following_link.href = '/directory/following' + (f.settings.sidebar_directly_to_followed_channels ? '/live' : '');
|
||||
if ( e && (e.button !== 0 || e.ctrlKey || e.metaKey) )
|
||||
return;
|
||||
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
|
||||
utils.transition('directory.following.' + (f.settings.sidebar_directly_to_followed_channels ? 'channels' : 'index'));
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// Find the Settings warp item.
|
||||
/*var settings_svg = el && el.querySelector('.svg-nav_settings');
|
||||
if ( settings_svg ) {
|
||||
var warp = settings_svg.parentElement.parentElement.parentElement,
|
||||
container = warp.parentElement;
|
||||
|
||||
var figure = utils.createElement('figure', 'warp__avatar', constants.ZREKNARF),
|
||||
span = utils.createElement('span', 'is-drawer-closed--hide', 'FrankerFaceZ Settings'),
|
||||
ffz_setting = utils.createElement('a', 'ffz-settings-link html-tooltip', figure),
|
||||
ffz_warp = utils.createElement('li', 'warp__item', ffz_setting);
|
||||
|
||||
ffz_setting.title = 'FrankerFaceZ Settings';
|
||||
ffz_setting.appendChild(span);
|
||||
container.insertBefore(ffz_warp, warp.nextElementSibling);
|
||||
}*/
|
||||
},
|
||||
|
||||
_initTooltips: function() {
|
||||
this._tipsySelector = this.$("#js-warp a, #small_search button, #small_more button");
|
||||
this._tipsySelector.off("mouseenter").off("mouseleave").teardownTipsy();
|
||||
this._tipsySelector.zipsy({
|
||||
gravity: utils.newtip_placement(constants.TOOLTIP_DISTANCE, 'w')});
|
||||
|
||||
this.$('a[data-href="following"]').zipsy({
|
||||
html: true,
|
||||
className: function() { return f.settings.following_count ? 'ffz-wide-tip' : '' },
|
||||
title: function() { return f._build_following_tooltip(this) },
|
||||
gravity: utils.newtip_placement(constants.TOOLTIP_DISTANCE * 2, 'w')
|
||||
});
|
||||
},
|
||||
|
||||
ffz_destroy: function() {
|
||||
if ( f._nav === this )
|
||||
f._nav = null;
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ,
|
||||
utils = require('../utils'),
|
||||
|
||||
VIEWER_CATEGORIES = [
|
||||
['staff', 'Staff'],
|
||||
['admins', 'Admins'],
|
||||
['global_mods', 'Global Moderators'],
|
||||
['moderators', 'Moderators'],
|
||||
['viewers', 'Viewers']
|
||||
];
|
||||
|
||||
|
||||
// --------------------
|
||||
// Settings
|
||||
// --------------------
|
||||
|
||||
FFZ.settings_info.sort_viewers = {
|
||||
type: "boolean",
|
||||
value: true,
|
||||
|
||||
category: "Chat Appearance",
|
||||
name: "Sort Viewer List",
|
||||
help: "Make sure the viewer list is alphabetically sorted and place the Broadcaster in their own category."
|
||||
};
|
||||
|
||||
|
||||
// --------------------
|
||||
// Initialization
|
||||
// --------------------
|
||||
|
||||
FFZ.prototype.setup_viewers = function() {
|
||||
this.update_views('component:chat/twitch-chat-viewers', this.modify_viewer_list);
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.modify_viewer_list = function(component) {
|
||||
var f = this;
|
||||
|
||||
utils.ember_reopen_view(component, {
|
||||
lines: function() {
|
||||
var viewers = this._super();
|
||||
if ( ! f.settings.sort_viewers )
|
||||
return this._super();
|
||||
|
||||
try {
|
||||
var viewers = [],
|
||||
has_broadcaster = false,
|
||||
raw_viewers = this.get('model.chatters') || {};
|
||||
|
||||
// Get the broadcaster name.
|
||||
var Channel = utils.ember_lookup('controller:channel'),
|
||||
broadcaster = room_id = this.get('model.id');
|
||||
|
||||
// We can get capitalization for the broadcaster from the channel.
|
||||
if ( Channel && Channel.get('channelModel.id') === room_id ) {
|
||||
var display_name = Channel.get('channelModel.displayName');
|
||||
if ( display_name && display_name !== 'jtv' )
|
||||
FFZ.capitalization[broadcaster] = [display_name, Date.now()];
|
||||
}
|
||||
|
||||
// Iterate over everything~!
|
||||
for(var i=0; i < VIEWER_CATEGORIES.length; i++) {
|
||||
var data = raw_viewers[VIEWER_CATEGORIES[i][0]],
|
||||
label = VIEWER_CATEGORIES[i][1],
|
||||
first_user = true;
|
||||
|
||||
if ( ! data || ! data.length )
|
||||
continue;
|
||||
|
||||
for(var x=0; x < data.length; x++) {
|
||||
if ( data[x] === broadcaster ) {
|
||||
has_broadcaster = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( first_user ) {
|
||||
viewers.push({category: i18n(label)});
|
||||
viewers.push({chatter: ""});
|
||||
first_user = false;
|
||||
}
|
||||
|
||||
var display_name = FFZ.get_capitalization(data[x]);
|
||||
if ( display_name.trim().toLowerCase() !== data[x] )
|
||||
display_name = data[x];
|
||||
|
||||
viewers.push({chatter: display_name});
|
||||
}
|
||||
}
|
||||
|
||||
if ( has_broadcaster )
|
||||
viewers.splice(0, 0,
|
||||
{category: i18n("Broadcaster")},
|
||||
{chatter: ""},
|
||||
{chatter: FFZ.get_capitalization(broadcaster)});
|
||||
|
||||
return viewers;
|
||||
|
||||
} catch(err) {
|
||||
f.error("ViewersController lines: " + err);
|
||||
return this._super();
|
||||
}
|
||||
|
||||
}.property("model.chatters")
|
||||
});
|
||||
}
|
|
@ -1,164 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ,
|
||||
utils = require("../utils"),
|
||||
constants = require("../constants");
|
||||
|
||||
// ---------------------
|
||||
// Settings
|
||||
// ---------------------
|
||||
|
||||
|
||||
// ---------------------
|
||||
// Initialization
|
||||
// ---------------------
|
||||
|
||||
FFZ.prototype.setup_vod_chat = function() {
|
||||
// Get the VOD Chat Service
|
||||
var f = this,
|
||||
VODService = utils.ember_lookup('service:vod-chat-service');
|
||||
|
||||
if ( VODService )
|
||||
VODService.reopen({
|
||||
messageBufferSize: f.settings.scrollback_length,
|
||||
|
||||
pushMessage: function(msg) {
|
||||
if ( msg.get("color") === null ) {
|
||||
var colors = this.get("colorSettings"),
|
||||
from = msg.get("from");
|
||||
|
||||
if ( from ) {
|
||||
var clr = colors.get(from);
|
||||
if ( ! clr ) {
|
||||
clr = constants.CHAT_COLORS[Math.floor(Math.random() * constants.CHAT_COLORS.length)];
|
||||
colors.set(from, clr);
|
||||
}
|
||||
|
||||
if ( typeof clr === 'string' )
|
||||
msg.set('color', clr);
|
||||
}
|
||||
}
|
||||
|
||||
this.get("messages").pushObject(msg);
|
||||
|
||||
var messages = this.get("messages"),
|
||||
len = this.get("messages.length"),
|
||||
limit = this.get("messageBufferSize");
|
||||
|
||||
if ( len > limit )
|
||||
messages.removeAt(0, len - limit);
|
||||
}
|
||||
});
|
||||
else
|
||||
f.error("Unable to locate VOD Chat Service.");
|
||||
|
||||
this.update_views('component:video/rechat/display-container', this.modify_vod_chat_display);
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.modify_vod_chat_display = function(component) {
|
||||
var f = this,
|
||||
VODService = utils.ember_lookup('service:vod-chat-service');
|
||||
|
||||
utils.ember_reopen_view(component, _.extend({
|
||||
ffz_init: function() {
|
||||
f._vodc = this;
|
||||
|
||||
if ( f.settings.dark_twitch )
|
||||
this.$().parents('.chat-container').addClass('dark').addClass('theme--dark');
|
||||
|
||||
this.parentView.addObserver('layout.isTheatreMode', function() {
|
||||
if ( f._vodc && f.settings.dark_twitch )
|
||||
setTimeout(function(){
|
||||
f._vodc.$().parents('.chat-container').addClass('dark').addClass('theme--dark');
|
||||
});
|
||||
});
|
||||
|
||||
this.ffzUpdateBadges();
|
||||
|
||||
// Load the room, if necessary.
|
||||
var room_id = this.get('video.channel.id');
|
||||
if ( room_id && ! f.rooms[room_id] ) {
|
||||
// Load the room model.
|
||||
f.log("Loading Room for VOD: " + room_id);
|
||||
var Room = utils.ember_resolve('model:room'),
|
||||
room = Room && Room.findOne(room_id);
|
||||
if ( room && App.currentPath === 'user.chat' )
|
||||
room.set('isEmbedChat', true);
|
||||
}
|
||||
|
||||
if ( ! f.has_bttv_6 ) {
|
||||
this.ffzFixStickyBottom();
|
||||
this.ffzAddKeyHook();
|
||||
|
||||
if ( f.settings.chat_hover_pause )
|
||||
this.ffzEnableFreeze();
|
||||
}
|
||||
},
|
||||
|
||||
ffz_destroy: function() {
|
||||
if ( f._vodc === this )
|
||||
f._vodc = undefined;
|
||||
|
||||
var room_id = this.get('video.channel.id'),
|
||||
room = f.rooms && f.rooms[room_id];
|
||||
|
||||
// We don't need the chat room anymore, in theory. This will
|
||||
// check if the room is still important after a short delay.
|
||||
if ( room && room.room )
|
||||
room.room.ffzScheduleDestroy();
|
||||
|
||||
this.ffzDisableFreeze();
|
||||
this.ffzRemoveKeyHook();
|
||||
},
|
||||
|
||||
ffzUpdateBadges: function() {
|
||||
var t = this,
|
||||
channel_name = this.get('video.channel.id'),
|
||||
owner_name = this.get('video.owner.name'),
|
||||
owner_id = this.get('video.owner._id');
|
||||
|
||||
if ( channel_name !== owner_name ) {
|
||||
t.set('ffzBadgeSet', null);
|
||||
return Ember.propertyDidChange(t, 'badgeStyle');
|
||||
}
|
||||
|
||||
fetch("https://badges.twitch.tv/v1/badges/channels/" + owner_id + "/display?language=" + (Twitch.receivedLanguage || "en"), {
|
||||
headers: {
|
||||
'Client-ID': constants.CLIENT_ID
|
||||
}
|
||||
}).then(utils.json).then(function(data) {
|
||||
t.set('ffzBadgeSet', data.badge_sets);
|
||||
Ember.propertyDidChange(t, 'badgeStyle');
|
||||
});
|
||||
},
|
||||
|
||||
badgeStyle: function() {
|
||||
var badges = this.get('ffzBadgeSet');
|
||||
if ( ! badges )
|
||||
return this._super();
|
||||
|
||||
var room_id = this.get('video.channel.id'),
|
||||
output = [];
|
||||
|
||||
for(var badge_id in badges) {
|
||||
var versions = badges[badge_id] && badges[badge_id].versions || {};
|
||||
for(var version in versions)
|
||||
output.push(utils.room_badge_css(room_id, badge_id, version, versions[version]));
|
||||
}
|
||||
|
||||
return Ember.String.htmlSafe('<style>' + output.join('') + '</style>');
|
||||
|
||||
}.property('badgeSet', 'ffzBadgeSet'),
|
||||
|
||||
ffzFreezeUpdateBuffer: function(val) {
|
||||
if ( val === undefined )
|
||||
val = this.get('stuckToBottom');
|
||||
|
||||
VODService && VODService.set("messageBufferSize", f.settings.scrollback_length + (val ? 0 : 150));
|
||||
},
|
||||
|
||||
_scheduleScrollToBottom: function() {
|
||||
this._scrollToBottom();
|
||||
}
|
||||
|
||||
}, FFZ.HoverPause));
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ,
|
||||
utils = require("../utils"),
|
||||
constants = require("../constants");
|
||||
|
||||
// --------------------
|
||||
// Initialization
|
||||
// --------------------
|
||||
|
||||
FFZ.prototype.setup_ember_wrapper = function() {
|
||||
this._views_to_update = [];
|
||||
this._ember_finalized = false;
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.update_views = function(klass, modifier, if_not_exists, immediate, no_modify_existing) {
|
||||
var original_klass;
|
||||
if ( typeof klass === 'string' ) {
|
||||
original_klass = klass;
|
||||
klass = utils.ember_resolve(klass);
|
||||
if ( ! klass && if_not_exists ) {
|
||||
if ( typeof if_not_exists === "function" )
|
||||
if_not_exists.call(this, klass, modifier);
|
||||
else {
|
||||
klass = Ember.Component.extend({});
|
||||
App.__registry__.register(original_klass, klass);
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! klass ) {
|
||||
this.error("Unable to locate the Ember " + original_klass);
|
||||
return false;
|
||||
}
|
||||
} else
|
||||
original_klass = klass.toString();
|
||||
|
||||
if ( this._ember_finalized || immediate || ! this._views_to_update )
|
||||
this._update_views([[original_klass, klass, modifier, no_modify_existing || false]]);
|
||||
else
|
||||
this._views_to_update.push([original_klass, klass, modifier, no_modify_existing || false]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.finalize_ember_wrapper = function() {
|
||||
this._ember_finalized = true;
|
||||
var views = this._views_to_update;
|
||||
this._views_to_update = null;
|
||||
this._update_views(views);
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype._update_views = function(klasses) {
|
||||
this.log("Updating Ember classes and instances.", klasses);
|
||||
var updated_instances = 0,
|
||||
updated_klasses = 0;
|
||||
|
||||
// Modify all pending classes and clear them from cache.
|
||||
for(var i=0; i < klasses.length; i++) {
|
||||
klasses[i][2].call(this, klasses[i][1]);
|
||||
updated_klasses++;
|
||||
|
||||
try {
|
||||
klasses[i][1].create().destroy()
|
||||
} catch(err) {
|
||||
if ( constants.DEBUG )
|
||||
this.log("There was an error creating and destroying an instance of the Ember class \"" + klasses[i][0] + "\" to clear its cache.", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate over all existing views and update them as necessary.
|
||||
var views = utils.ember_views();
|
||||
for(var view_id in views) {
|
||||
var view = views[view_id];
|
||||
if ( ! view )
|
||||
continue;
|
||||
|
||||
for(var i=0; i < klasses.length; i++)
|
||||
if ( view instanceof klasses[i][1] ) {
|
||||
updated_instances++;
|
||||
|
||||
try {
|
||||
if ( ! view.ffz_modified && ! klasses[i][3] )
|
||||
klasses[i][2].call(this, view);
|
||||
|
||||
var func = view.ffz_update || view.ffz_init;
|
||||
if ( func )
|
||||
func.call(view);
|
||||
|
||||
} catch(err) {
|
||||
this.error("An error occured when updating an existing Ember instance of: " + klasses[i][0], err);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.log("Updated " + utils.number_commas(updated_instances) + " existing instances across " + updated_klasses + " classes.");
|
||||
}
|
572
src/emoticons.js
572
src/emoticons.js
|
@ -1,572 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ,
|
||||
CSS = /\.([\w\-_]+)\s*?\{content:\s*?"([^"]+)";\s*?background-image:\s*?url\("([^"]+)"\);\s*?height:\s*?(\d+)px;\s*?width:\s*?(\d+)px;\s*?margin:([^;}]+);?([^}]*)\}/mg,
|
||||
MOD_CSS = /[^\n}]*\.badges\s+\.moderator\s*{\s*background-image:\s*url\(\s*['"]([^'"]+)['"][^}]+(?:}|$)/,
|
||||
constants = require('./constants'),
|
||||
utils = require('./utils'),
|
||||
|
||||
IS_OSX = constants.IS_OSX,
|
||||
|
||||
MODIFIERS = {
|
||||
59847: {
|
||||
modifier_offset: '0 15px 15px 0',
|
||||
modifier: true
|
||||
},
|
||||
|
||||
70852: {
|
||||
modifier: true,
|
||||
modifier_offset: '0 5px 20px 0',
|
||||
extra_width: 5,
|
||||
shrink_to_fit: true
|
||||
},
|
||||
|
||||
70854: {
|
||||
modifier: true,
|
||||
modifier_offset: '30px 0 0'
|
||||
},
|
||||
|
||||
147049: {
|
||||
modifier: true,
|
||||
modifier_offset: '4px 1px 0 3px'
|
||||
},
|
||||
|
||||
147011: {
|
||||
modifier: true,
|
||||
modifier_offset: '0'
|
||||
},
|
||||
|
||||
70864: {
|
||||
modifier: true,
|
||||
modifier_offset: '0'
|
||||
},
|
||||
|
||||
147038: {
|
||||
modifier: true,
|
||||
modifier_offset: '0'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// ---------------------
|
||||
// Initialization
|
||||
// ---------------------
|
||||
|
||||
FFZ.prototype.setup_emoticons = function() {
|
||||
this.log("Preparing emoticon system.");
|
||||
|
||||
// Usage Data
|
||||
this.emote_usage = {};
|
||||
|
||||
this.log("Creating emoticon style element.");
|
||||
var s = this._emote_style = document.createElement('style');
|
||||
s.id = "ffz-emoticon-css";
|
||||
document.head.appendChild(s);
|
||||
|
||||
this.log("Generating CSS for existing API emoticon sets.");
|
||||
for(var set_id in this.emote_sets) {
|
||||
var es = this.emote_sets[set_id];
|
||||
if ( es && es.pending_css ) {
|
||||
utils.update_css(s, set_id, es.pending_css + (es.css || ''));
|
||||
es.pending_css = null;
|
||||
}
|
||||
}
|
||||
|
||||
this.log("Loading global emote sets.");
|
||||
this.load_global_sets();
|
||||
|
||||
this.log("Loading emoji data.");
|
||||
this.load_emoji_data();
|
||||
|
||||
//this.log("Watching Twitch emoticon parser to ensure it loads.");
|
||||
//this._twitch_emote_check = setTimeout(this.check_twitch_emotes.bind(this), 10000);
|
||||
|
||||
this._twitch_inventory_sets = [];
|
||||
this.refresh_twitch_inventory();
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.refresh_twitch_inventory = function() {
|
||||
var f = this;
|
||||
return new Promise(function(succeed) {
|
||||
var o = [],
|
||||
user = f.get_user();
|
||||
|
||||
if ( ! user ) {
|
||||
f._twitch_inventory_sets = o;
|
||||
return succeed(o);
|
||||
}
|
||||
|
||||
utils.api.get("/v5/inventory/emoticons", null, {version: 5}, user.chat_oauth_token)
|
||||
.done(function(data) {
|
||||
|
||||
o = Object.keys(data.emoticon_sets || {});
|
||||
var ets = f._twitch_emote_to_set = f._twitch_emote_to_set || {};
|
||||
|
||||
for(var i=0; i < o.length; i++) {
|
||||
var set_id = o[i],
|
||||
emotes = data.emoticon_sets[set_id];
|
||||
for(var j=0; j < emotes.length; j++)
|
||||
ets[emotes[j].id] = set_id;
|
||||
}
|
||||
|
||||
f._twitch_inventory_sets = o;
|
||||
return succeed(o);
|
||||
|
||||
}).fail(function() {
|
||||
f._twitch_inventory_sets = o;
|
||||
return succeed(o);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ------------------------
|
||||
// Emote Usage
|
||||
// ------------------------
|
||||
|
||||
FFZ.prototype.add_usage = function(room_id, emote, count) {
|
||||
// Only report usage from FFZ emotes. Not extensions to FFZ.
|
||||
var emote_set = this.emote_sets[emote.set_id];
|
||||
if ( ! emote_set || emote_set.source_ext )
|
||||
return;
|
||||
|
||||
var emote_id = emote.id,
|
||||
rooms = this.emote_usage[emote_id] = this.emote_usage[emote_id] || {};
|
||||
|
||||
rooms[room_id] = (rooms[room_id] || 0) + (count || 1);
|
||||
|
||||
if ( this._emote_report_scheduled )
|
||||
return;
|
||||
|
||||
this._emote_report_scheduled = setTimeout(this._report_emotes.bind(this), 30000);
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype._report_emotes = function() {
|
||||
if ( this._emote_report_scheduled )
|
||||
delete this._emote_report_scheduled;
|
||||
|
||||
var usage = this.emote_usage;
|
||||
this.emote_usage = {};
|
||||
this.ws_send("emoticon_uses", [usage], function(){}, true);
|
||||
}
|
||||
|
||||
|
||||
// ------------------------
|
||||
// Emote Click Handler
|
||||
// ------------------------
|
||||
|
||||
FFZ.prototype._click_emote = function(target, event) {
|
||||
if ( ! target || ! target.classList.contains('emoticon') )
|
||||
return;
|
||||
|
||||
if ( event && ! event.shiftKey && ! event.shiftLeft && ((!IS_OSX && event.ctrlKey) || (IS_OSX && event.metaKey)) ) {
|
||||
var eid, favorite_key;
|
||||
|
||||
if ( target.classList.contains('emoji') ) {
|
||||
var emoji = target.getAttribute('data-ffz-emoji'),
|
||||
emoji_data = this.emoji_data[emoji];
|
||||
|
||||
if ( emoji_data ) {
|
||||
favorite_key = 'emoji';
|
||||
eid = emoji_data.raw;
|
||||
}
|
||||
|
||||
} else {
|
||||
eid = target.getAttribute('data-emote');
|
||||
|
||||
if ( eid ) {
|
||||
eid = parseInt(eid);
|
||||
var twitch_set = this._twitch_emote_to_set[eid];
|
||||
if ( twitch_set )
|
||||
if ( this._twitch_inventory_sets.indexOf(twitch_set) !== -1 )
|
||||
favorite_key = 'twitch-inventory';
|
||||
else
|
||||
favorite_key = 'twitch-' + twitch_set;
|
||||
|
||||
} else {
|
||||
eid = target.getAttribute('data-ffz-emote');
|
||||
var set_id = target.getAttribute('data-ffz-set'),
|
||||
emote_set = set_id && this.emote_sets[set_id];
|
||||
|
||||
if ( emote_set && emote_set.emoticons && emote_set.emoticons[eid] ) {
|
||||
favorite_key = 'ffz-' + (emote_set.hasOwnProperty('source_ext') ? 'ext-' + emote_set.source_ext + '-' + emote_set.source_id : set_id);
|
||||
eid = emote_set.emoticons[eid].id || eid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( favorite_key ) {
|
||||
var favs = this.settings.favorite_emotes[favorite_key] = this.settings.favorite_emotes[favorite_key] || [],
|
||||
is_favorited = favs.indexOf(eid) !== -1;
|
||||
|
||||
if ( is_favorited )
|
||||
favs.removeObject(eid);
|
||||
else
|
||||
favs.push(eid);
|
||||
|
||||
this.settings.set("favorite_emotes", this.settings.favorite_emotes, true);
|
||||
jQuery(target).trigger('mouseout').trigger('mouseover');
|
||||
this._inputv && this._inputv.propertyDidChange('ffz_emoticons');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( ! this.settings.clickable_emoticons || (event && !(event.shiftKey || event.shiftLeft)) )
|
||||
return;
|
||||
|
||||
var eid = target.getAttribute('data-emote');
|
||||
if ( eid )
|
||||
window.open("https://twitchemotes.com/emote/" + eid);
|
||||
else {
|
||||
eid = target.getAttribute("data-ffz-emote");
|
||||
var es = target.getAttribute("data-ffz-set"),
|
||||
emote_set = es && this.emote_sets[es],
|
||||
url;
|
||||
|
||||
if ( ! emote_set )
|
||||
return;
|
||||
|
||||
if ( emote_set.hasOwnProperty('source_ext') ) {
|
||||
var api = this._apis[emote_set.source_ext];
|
||||
if ( api && api.emote_url_generator )
|
||||
url = api.emote_url_generator(emote_set.source_id, eid);
|
||||
} else
|
||||
url = "https://www.frankerfacez.com/emoticons/" + eid;
|
||||
|
||||
if ( url ) {
|
||||
window.open(url);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ------------------------
|
||||
// Twitch Emoticon Checker
|
||||
// ------------------------
|
||||
|
||||
/*FFZ.prototype.check_twitch_emotes = function() {
|
||||
if ( this._twitch_emote_check ) {
|
||||
clearTimeout(this._twitch_emote_check);
|
||||
delete this._twitch_emote_check;
|
||||
}
|
||||
|
||||
var room;
|
||||
if ( this.rooms ) {
|
||||
for(var key in this.rooms) {
|
||||
if ( this.rooms.hasOwnProperty(key) ) {
|
||||
room = this.rooms[key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! room || ! room.room || ! room.room.tmiSession ) {
|
||||
this._twitch_emote_check = setTimeout(this.check_twitch_emotes.bind(this), 10000);
|
||||
return;
|
||||
}
|
||||
|
||||
var parser = room.room.tmiSession._emotesParser,
|
||||
emotes = Object.keys(parser.emoticonRegexToIds).length;
|
||||
|
||||
// If we have emotes, we're done!
|
||||
if ( emotes > 0 )
|
||||
return;
|
||||
|
||||
// No emotes. Try loading them.
|
||||
var sets = parser.emoticonSetIds;
|
||||
parser.emoticonSetIds = "";
|
||||
parser.updateEmoticons(sets);
|
||||
|
||||
// Check again in a bit to see if we've got them.
|
||||
this._twitch_emote_check = setTimeout(this.check_twitch_emotes.bind(this), 10000);
|
||||
}*/
|
||||
|
||||
|
||||
|
||||
// ---------------------
|
||||
// Set Management
|
||||
// ---------------------
|
||||
|
||||
FFZ.prototype.getEmotes = function(user_id, room_id) {
|
||||
var user = this.users && this.users[user_id],
|
||||
room = this.rooms && this.rooms[room_id],
|
||||
room_user = room && room.users && room.users[user_id];
|
||||
|
||||
return _.union(user && user.sets || [], room_user && room_user.sets || [], room && room.set && [room.set] || [], room && room.extra_sets || [], room && room.ext_sets || [], this.default_sets);
|
||||
}
|
||||
|
||||
|
||||
// ---------------------
|
||||
// Commands
|
||||
// ---------------------
|
||||
|
||||
FFZ.ws_commands.reload_set = function(set_id) {
|
||||
if ( this.emote_sets.hasOwnProperty(set_id) )
|
||||
this.load_set(set_id);
|
||||
}
|
||||
|
||||
|
||||
FFZ.ws_commands.load_set = function(set_id) {
|
||||
this.load_set(set_id);
|
||||
}
|
||||
|
||||
|
||||
// ---------------------
|
||||
// Emoji Loading
|
||||
// ---------------------
|
||||
|
||||
FFZ.prototype.load_emoji_data = function(callback, tries) {
|
||||
var f = this,
|
||||
puny = window.punycode && punycode.ucs2;
|
||||
|
||||
jQuery.getJSON(constants.SERVER + "emoji/emoji-data.json")
|
||||
.done(function(data) {
|
||||
var new_data = {},
|
||||
by_name = {};
|
||||
for(var eid in data) {
|
||||
var emoji = data[eid];
|
||||
eid = eid.toLowerCase();
|
||||
emoji.code = eid;
|
||||
|
||||
new_data[eid] = emoji;
|
||||
if ( emoji.short_name )
|
||||
by_name[emoji.short_name] = eid;
|
||||
if ( emoji.names && emoji.names.length )
|
||||
for(var x=0,y=emoji.names.length; x < y; x++)
|
||||
by_name[emoji.names[x]] = eid;
|
||||
|
||||
emoji.raw = _.map(emoji.code.split("-"), utils.codepoint_to_emoji).join("");
|
||||
|
||||
emoji.tw_src = constants.SERVER + 'emoji/tw/' + eid + '.svg';
|
||||
emoji.noto_src = constants.SERVER + 'emoji/noto-' + eid + '.svg';
|
||||
emoji.one_src = constants.SERVER + 'emoji/one/' + eid + '.svg';
|
||||
|
||||
emoji.token = {
|
||||
type: "emoticon",
|
||||
imgSrc: true,
|
||||
|
||||
length: puny ? puny.decode(emoji.raw).length : emoji.raw.length,
|
||||
|
||||
tw_src: emoji.tw_src,
|
||||
noto_src: emoji.noto_src,
|
||||
one_src: emoji.one_src,
|
||||
|
||||
tw: emoji.tw,
|
||||
noto: emoji.noto,
|
||||
one: emoji.one,
|
||||
|
||||
ffzEmoji: eid,
|
||||
altText: emoji.raw
|
||||
};
|
||||
}
|
||||
|
||||
f.emoji_data = new_data;
|
||||
f.emoji_names = by_name;
|
||||
|
||||
f.log("Loaded data on " + Object.keys(new_data).length + " emoji.");
|
||||
if ( typeof callback === "function" )
|
||||
callback(true, data);
|
||||
|
||||
}).fail(function(data) {
|
||||
if ( data.status === 404 )
|
||||
return typeof callback === "function" && callback(false);
|
||||
|
||||
tries = (tries || 0) + 1;
|
||||
if ( tries < 50 )
|
||||
return f.load_emoji(callback, tries);
|
||||
|
||||
return typeof callback === "function" && callback(false);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ---------------------
|
||||
// Set Loading
|
||||
// ---------------------
|
||||
|
||||
FFZ.prototype.load_global_sets = function(callback, tries) {
|
||||
var f = this;
|
||||
jQuery.getJSON(constants.API_SERVER + "v1/set/global")
|
||||
.done(function(data) {
|
||||
// Apply default sets.
|
||||
var ds = f.default_sets,
|
||||
gs = f.global_sets,
|
||||
sets = data.sets || {};
|
||||
|
||||
// Remove non-API sets from default and global sets.
|
||||
for(var i=ds.length; i--; ) {
|
||||
var set_id = ds[i];
|
||||
if ( data.default_sets.indexOf(set_id) === -1 && (!f.feature_friday || f.feature_friday.set !== set_id) && (! f.emote_sets[set_id] || ! f.emote_sets[set_id].source_ext) )
|
||||
ds.splice(i, 1);
|
||||
}
|
||||
|
||||
for(var i=0; i < data.default_sets.length; i++) {
|
||||
var set_id = data.default_sets[i];
|
||||
if ( ds.indexOf(set_id) === -1 )
|
||||
ds.push(set_id);
|
||||
}
|
||||
|
||||
for(var i=gs.length; i--; ) {
|
||||
var set_id = gs[i];
|
||||
if ( ! sets[set_id] && (!f.feature_friday || f.feature_friday.set !== set_id) && (! f.emote_sets[set_id] || ! f.emote_sets[set_id].source_ext) )
|
||||
gs.splice(i, 1);
|
||||
}
|
||||
|
||||
for(var set_id in sets) {
|
||||
var set = sets[set_id];
|
||||
if ( gs.indexOf(set_id) === -1 )
|
||||
gs.push(set_id);
|
||||
|
||||
f._load_set_json(set_id, undefined, set);
|
||||
}
|
||||
|
||||
f._load_set_users(data.users);
|
||||
|
||||
}).fail(function(data) {
|
||||
if ( data.status == 404 )
|
||||
return typeof callback == "function" && callback(false);
|
||||
|
||||
tries = tries || 0;
|
||||
tries++;
|
||||
if ( tries < 50 )
|
||||
return f.load_global_sets(callback, tries);
|
||||
|
||||
return typeof callback == "function" && callback(false);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype._load_set_users = function(data) {
|
||||
if ( data )
|
||||
for(var set_id in data)
|
||||
if ( data.hasOwnProperty(set_id) ) {
|
||||
var emote_set = this.emote_sets[set_id],
|
||||
users = data[set_id];
|
||||
|
||||
for(var i=0; i < users.length; i++) {
|
||||
var user = users[i],
|
||||
ud = this.users[user] = this.users[user] || {},
|
||||
sets = ud.sets = ud.sets || [];
|
||||
|
||||
if ( sets.indexOf(set_id) === -1 )
|
||||
sets.push(set_id);
|
||||
}
|
||||
|
||||
this.log('Added "' + (emote_set ? emote_set.title : set_id) + '" emote set to ' + utils.number_commas(users.length) + ' users.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.load_set = function(set_id, callback, tries) {
|
||||
var f = this;
|
||||
jQuery.getJSON(constants.API_SERVER + "v1/set/" + set_id)
|
||||
.done(function(data) {
|
||||
f._load_set_json(set_id, callback, data && data.set);
|
||||
f._load_set_users(data.users);
|
||||
|
||||
}).fail(function(data) {
|
||||
if ( data.status == 404 )
|
||||
return typeof callback == "function" && callback(false);
|
||||
|
||||
tries = tries || 0;
|
||||
tries++;
|
||||
if ( tries < 10 )
|
||||
return f.load_set(set_id, callback, tries);
|
||||
|
||||
return typeof callback == "function" && callback(false);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.unload_set = function(set_id) {
|
||||
var set = this.emote_sets[set_id];
|
||||
if ( ! set )
|
||||
return;
|
||||
|
||||
this.log("Unloading emoticons for set: " + set_id);
|
||||
|
||||
utils.update_css(this._emote_style, set_id, null);
|
||||
delete this.emote_sets[set_id];
|
||||
|
||||
if ( set.hasOwnProperty('source_ext') ) {
|
||||
var api = this._apis[set.source_ext];
|
||||
if ( api && api.emote_sets && api.emote_sets[set_id] )
|
||||
api.emote_sets[set_id] = undefined;
|
||||
}
|
||||
|
||||
if ( this._inputv )
|
||||
Ember.propertyDidChange(this._inputv, 'ffz_emoticons');
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype._load_set_json = function(set_id, callback, data) {
|
||||
if ( ! data )
|
||||
return typeof callback == "function" && callback(false);
|
||||
|
||||
// Do we have existing users?
|
||||
var users = this.emote_sets[set_id] && this.emote_sets[set_id].users || [];
|
||||
|
||||
// Store our set.
|
||||
this.emote_sets[set_id] = data;
|
||||
data.users = users;
|
||||
data.count = 0;
|
||||
|
||||
|
||||
// Iterate through all the emoticons, building CSS and regex objects as appropriate.
|
||||
var output_css = "",
|
||||
ems = data.emoticons;
|
||||
|
||||
data.emoticons = {};
|
||||
|
||||
for(var i=0; i < ems.length; i++) {
|
||||
var emote = ems[i];
|
||||
|
||||
//emote.klass = "ffz-emote-" + emote.id;
|
||||
emote.set_id = set_id;
|
||||
|
||||
emote.srcSet = emote.urls[1] + " 1x";
|
||||
if ( emote.urls[2] )
|
||||
emote.srcSet += ", " + emote.urls[2] + " 2x";
|
||||
if ( emote.urls[4] )
|
||||
emote.srcSet += ", " + emote.urls[4] + " 4x";
|
||||
|
||||
if ( emote.name[emote.name.length-1] === "!" )
|
||||
emote.regex = new RegExp("(^|\\W|\\b)(" + utils.escape_regex(emote.name) + ")(?=\\W|$)", "g");
|
||||
else
|
||||
emote.regex = new RegExp("(^|\\W|\\b)(" + utils.escape_regex(emote.name) + ")\\b", "g");
|
||||
|
||||
emote.token = {
|
||||
type: "emoticon",
|
||||
srcSet: emote.srcSet,
|
||||
imgSrc: emote.urls[1],
|
||||
ffzEmote: emote.id,
|
||||
ffzEmoteSet: set_id,
|
||||
altText: emote.hidden ? '???' : emote.name
|
||||
};
|
||||
|
||||
if ( MODIFIERS.hasOwnProperty(emote.id) )
|
||||
emote = _.extend(emote, MODIFIERS[emote.id]);
|
||||
|
||||
output_css += utils.emote_css(emote);
|
||||
data.count++;
|
||||
data.emoticons[emote.id] = emote;
|
||||
}
|
||||
|
||||
utils.update_css(this._emote_style, set_id, output_css + (data.css || ""));
|
||||
this.log("Updated emoticons for set #" + set_id + ": " + data.title, data);
|
||||
|
||||
if ( this._cindex )
|
||||
this._cindex.ffzFixTitle();
|
||||
|
||||
this.update_ui_link();
|
||||
|
||||
if ( this._inputv )
|
||||
Ember.propertyDidChange(this._inputv, 'ffz_emoticons');
|
||||
|
||||
this.rerender_feed_cards(set_id);
|
||||
|
||||
if ( callback )
|
||||
callback(true, data);
|
||||
}
|
16
src/entry.js
Normal file
16
src/entry.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
/* eslint strict: off */
|
||||
'use strict';
|
||||
(() => {
|
||||
if ( location.hostname === 'player.twitch.tv' )
|
||||
return;
|
||||
|
||||
const DEBUG = localStorage.ffzDebugMode == 'true' && document.body.classList.contains('ffz-dev'),
|
||||
SERVER = DEBUG ? '//localhost:8000' : '//cdn.frankerfacez.com',
|
||||
FLAVOR = window.Ember ? 'umbral' : 'avalon',
|
||||
|
||||
script = document.createElement('script');
|
||||
|
||||
script.id = 'ffz-script';
|
||||
script.src = `${SERVER}/script/${FLAVOR}.js?_=${Date.now()}`;
|
||||
document.head.appendChild(script);
|
||||
})();
|
682
src/ext/api.js
682
src/ext/api.js
|
@ -1,682 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ,
|
||||
utils = require('../utils'),
|
||||
constants = require('../constants');
|
||||
|
||||
|
||||
// ---------------------
|
||||
// Badware Check
|
||||
// ---------------------
|
||||
|
||||
FFZ.prototype.check_badware = function() {
|
||||
if ( this.embed_in_dash || ! window.jQuery || ! window.jQuery.noty )
|
||||
return;
|
||||
|
||||
// Check for the stolen version of BTTV4FFZ.
|
||||
if ( FFZ.settings_info.bttv_global_emotes && FFZ.settings_info.bttv_global_emotes.category === "BetterTTV" ) {
|
||||
var shown = localStorage.ffz_warning_bttv4ffz_clone;
|
||||
if ( shown !== "true" ) {
|
||||
localStorage.ffz_warning_bttv4ffz_clone = "true";
|
||||
this.show_message("You appear to be using an unofficial version of BTTV4FFZ that was copied without the developer's permission. Please use the official version available at <a href=\"https://lordmau5.com/bttv4ffz/\">https://lordmau5.com/bttv4ffz/</a>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------------------
|
||||
// API Constructor
|
||||
// ---------------------
|
||||
|
||||
var API = FFZ.API = function(instance, name, icon, version, name_key) {
|
||||
this.ffz = instance || FFZ.get();
|
||||
|
||||
// Check for a known API!
|
||||
if ( name ) {
|
||||
for(var id in this.ffz._known_apis) {
|
||||
if ( this.ffz._known_apis[id] === name ) {
|
||||
this.id = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! this.id ) {
|
||||
var i = 0;
|
||||
while( ! this.id ) {
|
||||
if ( ! this.ffz._known_apis.hasOwnProperty(i) ) {
|
||||
this.id = i;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
if ( name ) {
|
||||
this.ffz._known_apis[this.id] = name;
|
||||
localStorage.ffz_known_apis = JSON.stringify(this.ffz._known_apis);
|
||||
}
|
||||
}
|
||||
|
||||
this._events = {};
|
||||
|
||||
this.ffz._apis[this.id] = this;
|
||||
|
||||
this.emote_sets = {};
|
||||
this.global_sets = [];
|
||||
this.default_sets = [];
|
||||
|
||||
this.badges = {};
|
||||
|
||||
this.users = {};
|
||||
|
||||
this.name = name || ("Extension#" + this.id);
|
||||
this.name_key = name_key || this.name.replace(/[^A-Z0-9_\-]/g, '').toLowerCase();
|
||||
|
||||
// We can't start name key with a number.
|
||||
if ( /^[0-9]/.test(this.name_key) )
|
||||
this.name_key = '_' + this.name_key;
|
||||
|
||||
this.icon = icon || null;
|
||||
this.version = version || null;
|
||||
|
||||
this.ffz.log('Registered New Extension #' + this.id + ' (' + this.name_key + '): ' + this.name);
|
||||
};
|
||||
|
||||
|
||||
FFZ.prototype.api = function(name, icon, version, name_key) {
|
||||
// Load the known APIs list.
|
||||
if ( ! this._known_apis ) {
|
||||
this._known_apis = {};
|
||||
var stored_val = localStorage.getItem("ffz_known_apis");
|
||||
if ( stored_val !== null )
|
||||
try {
|
||||
this._known_apis = JSON.parse(stored_val);
|
||||
} catch(err) {
|
||||
this.error("Error loading Known APIs", err);
|
||||
}
|
||||
}
|
||||
|
||||
return new API(this, name, icon, version, name_key);
|
||||
}
|
||||
|
||||
|
||||
API.prototype.log = function(msg, data, to_json, log_json) {
|
||||
this.ffz.log('Ext #' + this.id + ' (' + this.name_key + '): ' + msg, data, to_json, log_json);
|
||||
}
|
||||
|
||||
|
||||
API.prototype.error = function(msg, error, to_json, log_json) {
|
||||
this.ffz.error('Ext #' + this.id + ' (' + this.name_key + '): ' + msg, error, to_json, log_json);
|
||||
}
|
||||
|
||||
|
||||
// ---------------------
|
||||
// Events
|
||||
// ---------------------
|
||||
|
||||
API.prototype.on = function(event, func) {
|
||||
var e = this._events[event] = this._events[event] || [];
|
||||
if ( e.indexOf(func) === -1 )
|
||||
e.push(func);
|
||||
}
|
||||
|
||||
|
||||
API.prototype.off = function(event, func) {
|
||||
if ( func === undefined )
|
||||
this._events[event] = [];
|
||||
else {
|
||||
var e = this._events[event] = this._events[event] || [],
|
||||
ind = e.indexOf(func);
|
||||
if ( ind !== -1 )
|
||||
e.splice(ind, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var slice = Array.prototype.slice;
|
||||
|
||||
API.prototype.trigger = function(event /*, args... */) {
|
||||
var e = this._events[event];
|
||||
if ( e && e.length )
|
||||
for(var i=0; i < e.length; i++)
|
||||
e[i].apply(this, slice.call(arguments, 1));
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.api_trigger = function(/*event, args...*/) {
|
||||
for(var api_id in this._apis) {
|
||||
var api = this._apis[api_id];
|
||||
api.trigger.apply(api, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------------------
|
||||
// Metadata~!
|
||||
// ---------------------
|
||||
|
||||
API.prototype.register_metadata = function(key, data) {
|
||||
data.source_ext = this.id;
|
||||
data.source_id = key;
|
||||
FFZ.channel_metadata[this.id + '-' + key] = data;
|
||||
this.update_metadata(key);
|
||||
}
|
||||
|
||||
|
||||
API.prototype.unregister_metadata = function(key) {
|
||||
delete FFZ.channel_metadata[this.id + '-' + key];
|
||||
this.update_metadata(key, true);
|
||||
}
|
||||
|
||||
|
||||
API.prototype.update_metadata = function(key, full_update) {
|
||||
var real_key = this.id + '-' + key,
|
||||
channel = this.ffz._cindex;
|
||||
|
||||
if ( channel ) {
|
||||
if ( full_update )
|
||||
channel.$('.cn-metabar__ffz[data-key="' + real_key + '"]').remove();
|
||||
|
||||
channel.ffzUpdateMetadata(real_key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------------------
|
||||
// Set Loading
|
||||
// ---------------------
|
||||
|
||||
API.prototype._load_set = function(real_id, set_id, data) {
|
||||
if ( ! data )
|
||||
return null;
|
||||
|
||||
// Check for an existing set to copy the users.
|
||||
var users = [];
|
||||
if ( this.emote_sets[set_id] && this.emote_sets[set_id].users )
|
||||
users = this.emote_sets[set_id].users;
|
||||
|
||||
var emote_set = _.extend({
|
||||
source: this.name,
|
||||
icon: this.icon || null,
|
||||
title: "Global Emoticons",
|
||||
_type: 0
|
||||
}, data, {
|
||||
source_ext: this.id,
|
||||
source_id: set_id,
|
||||
id: real_id,
|
||||
users: users,
|
||||
count: 0,
|
||||
emoticons: {},
|
||||
});
|
||||
|
||||
this.emote_sets[set_id] = emote_set;
|
||||
|
||||
// Use the real ID for FFZ's own tracking.
|
||||
if ( this.ffz.emote_sets )
|
||||
this.ffz.emote_sets[real_id] = emote_set;
|
||||
|
||||
var output_css = "",
|
||||
ems = data.emoticons,
|
||||
emoticons = emote_set.emoticons;
|
||||
|
||||
for(var i=0; i < ems.length; i++) {
|
||||
var emote = ems[i],
|
||||
id = emote.id || (this.name + '-' + set_id + '-' + i);
|
||||
|
||||
if ( ! emote.name )
|
||||
continue;
|
||||
|
||||
/*var new_emote = _.extend({}, emote, {
|
||||
id: id,
|
||||
set_id: real_id,
|
||||
srcSet: emote.urls[1] + ' 1x'
|
||||
});*/
|
||||
|
||||
emote.id = id;
|
||||
emote.set_id = real_id;
|
||||
emote.srcSet = emote.urls[1] + ' 1x';
|
||||
|
||||
if ( emote.urls[2] )
|
||||
/*new_*/emote.srcSet += ', ' + emote.urls[2] + ' 2x';
|
||||
|
||||
if ( emote.urls[3] )
|
||||
/*new_*/emote.srcSet += ', ' + emote.urls[3] + ' 3x';
|
||||
|
||||
if ( emote.urls[4] )
|
||||
/*new_*/emote.srcSet += ', ' + emote.urls[4] + ' 4x';
|
||||
|
||||
/*new_*/emote.token = {
|
||||
type: "emoticon",
|
||||
srcSet: /*new_*/emote.srcSet,
|
||||
imgSrc: /*new_*/emote.urls[1],
|
||||
ffzEmote: id,
|
||||
ffzEmoteSet: real_id,
|
||||
altText: /*new_*/emote.hidden ? '???' : /*new_*/emote.name
|
||||
};
|
||||
|
||||
output_css += utils.emote_css(/*new_*/emote);
|
||||
emote_set.count++;
|
||||
emoticons[id] = /*new_*/emote;
|
||||
}
|
||||
|
||||
// Use the real ID for building CSS.
|
||||
if ( this.ffz._emote_style )
|
||||
utils.update_css(this.ffz._emote_style, real_id, output_css + (emote_set.css || ""));
|
||||
else
|
||||
emote_set.pending_css = output_css;
|
||||
|
||||
if ( this.ffz._cindex )
|
||||
this.ffz._cindex.ffzFixTitle();
|
||||
|
||||
try {
|
||||
this.ffz.update_ui_link();
|
||||
} catch(err) { }
|
||||
|
||||
return emote_set;
|
||||
}
|
||||
|
||||
|
||||
// -------------------------
|
||||
// Loading / Unloading Sets
|
||||
// -------------------------
|
||||
|
||||
API.prototype.load_set = function(set_id, emote_set) {
|
||||
var exact_id = this.id + '-' + set_id,
|
||||
already_loaded = this.emote_sets[set_id];
|
||||
|
||||
emote_set.title = emote_set.title || "Global Emoticons";
|
||||
emote_set._type = emote_set._type || 0;
|
||||
|
||||
emote_set = this._load_set(exact_id, set_id, emote_set);
|
||||
|
||||
// Avoid spamming the console if and when other extensions
|
||||
// spend time constantly updating emote sets.
|
||||
if ( this.log_sets )
|
||||
this.log("Loaded Emoticon Set #" + set_id + ": " + emote_set.title + " (" + emote_set.count + " emotes)", emote_set);
|
||||
|
||||
return emote_set;
|
||||
}
|
||||
|
||||
|
||||
API.prototype.unload_set = function(set_id) {
|
||||
var exact_id = this.id + '-' + set_id,
|
||||
emote_set = this.emote_sets[set_id];
|
||||
|
||||
if ( ! emote_set )
|
||||
return;
|
||||
|
||||
// First, let's unregister it as a global.
|
||||
this.unregister_global_set(set_id);
|
||||
|
||||
// Now, remove the set data.
|
||||
if ( this.ffz._emote_style )
|
||||
utils.update_css(this.ffz._emote_style, exact_id, null);
|
||||
|
||||
this.emote_sets[set_id] = undefined;
|
||||
if ( this.ffz.emote_sets )
|
||||
this.ffz.emote_sets[exact_id] = undefined;
|
||||
|
||||
// Remove from all its Rooms
|
||||
if ( emote_set.users ) {
|
||||
for(var i=0; i < emote_set.users.length; i++) {
|
||||
var room_id = emote_set.users[i],
|
||||
room = this.ffz.rooms && this.ffz.rooms[room_id];
|
||||
|
||||
if ( ! room )
|
||||
continue;
|
||||
|
||||
var ind = room.ext_sets ? room.ext_sets.indexOf(exact_id) : -1;
|
||||
if ( ind !== -1 )
|
||||
room.ext_sets.splice(ind,1);
|
||||
}
|
||||
|
||||
emote_set.users = [];
|
||||
}
|
||||
|
||||
|
||||
return emote_set;
|
||||
}
|
||||
|
||||
|
||||
API.prototype.get_set = function(set_id) {
|
||||
return this.emote_sets[set_id];
|
||||
}
|
||||
|
||||
|
||||
// ---------------------
|
||||
// Global Emote Sets
|
||||
// ---------------------
|
||||
|
||||
API.prototype.register_global_set = function(set_id, emote_set) {
|
||||
var exact_id = this.id + '-' + set_id;
|
||||
|
||||
if ( emote_set ) {
|
||||
// If a set was provided, load it.
|
||||
emote_set = this.load_set(set_id, emote_set);
|
||||
} else
|
||||
emote_set = this.emote_sets[set_id];
|
||||
|
||||
if ( ! emote_set )
|
||||
throw new Error("Invalid set ID");
|
||||
|
||||
|
||||
// Make sure the set is still available with FFZ.
|
||||
if ( this.ffz.emote_sets && ! this.ffz.emote_sets[exact_id] )
|
||||
this.ffz.emote_sets[exact_id] = emote_set;
|
||||
|
||||
|
||||
// It's a valid set if we get here, so make it global.
|
||||
if ( this.global_sets.indexOf(set_id) === -1 )
|
||||
this.global_sets.push(set_id);
|
||||
|
||||
if ( this.default_sets.indexOf(set_id) === -1 )
|
||||
this.default_sets.push(set_id);
|
||||
|
||||
if ( this.ffz.global_sets && this.ffz.global_sets.indexOf(exact_id) === -1 )
|
||||
this.ffz.global_sets.push(exact_id);
|
||||
|
||||
if ( this.ffz.default_sets && this.ffz.default_sets.indexOf(exact_id) === -1 )
|
||||
this.ffz.default_sets.push(exact_id);
|
||||
|
||||
// Update tab completion.
|
||||
if ( this.ffz._inputv )
|
||||
Ember.propertyDidChange(this.ffz._inputv, 'ffz_emoticons');
|
||||
};
|
||||
|
||||
|
||||
API.prototype.unregister_global_set = function(set_id) {
|
||||
var exact_id = this.id + '-' + set_id,
|
||||
emote_set = this.emote_sets[set_id];
|
||||
|
||||
if ( ! emote_set )
|
||||
return;
|
||||
|
||||
// Remove the set from global sets.
|
||||
var ind = this.global_sets.indexOf(set_id);
|
||||
if ( ind !== -1 )
|
||||
this.global_sets.splice(ind,1);
|
||||
|
||||
ind = this.default_sets.indexOf(set_id);
|
||||
if ( ind !== -1 )
|
||||
this.default_sets.splice(ind,1);
|
||||
|
||||
ind = this.ffz.global_sets ? this.ffz.global_sets.indexOf(exact_id) : -1;
|
||||
if ( ind !== -1 )
|
||||
this.ffz.global_sets.splice(ind,1);
|
||||
|
||||
ind = this.ffz.default_sets ? this.ffz.default_sets.indexOf(exact_id) : -1;
|
||||
if ( ind !== -1 )
|
||||
this.ffz.default_sets.splice(ind,1);
|
||||
|
||||
// Update tab completion.
|
||||
if ( this.ffz._inputv )
|
||||
Ember.propertyDidChange(this.ffz._inputv, 'ffz_emoticons');
|
||||
};
|
||||
|
||||
|
||||
// -----------------------
|
||||
// Per-Channel Emote Sets
|
||||
// -----------------------
|
||||
|
||||
API.prototype.register_room_set = function(room_id, set_id, emote_set) {
|
||||
var exact_id = this.id + '-' + set_id,
|
||||
room = this.ffz.rooms && this.ffz.rooms[room_id];
|
||||
|
||||
if ( ! room )
|
||||
throw new Error("Room not loaded");
|
||||
|
||||
if ( emote_set ) {
|
||||
// If a set was provided, load it.
|
||||
emote_set.title = emote_set.title || "Channel: " + (room.display_name || room_id);
|
||||
emote_set._type = emote_set._type || 1;
|
||||
|
||||
emote_set = this.load_set(set_id, emote_set);
|
||||
} else
|
||||
emote_set = this.emote_sets[set_id];
|
||||
|
||||
if ( ! emote_set )
|
||||
throw new Error("Invalid set ID");
|
||||
|
||||
// Make sure the set is still available with FFZ.
|
||||
if ( ! this.ffz.emote_sets[exact_id] )
|
||||
this.ffz.emote_sets[exact_id] = emote_set;
|
||||
|
||||
// Register it on the room.
|
||||
if ( room.ext_sets && room.ext_sets.indexOf(exact_id) === -1 )
|
||||
room.ext_sets.push(exact_id);
|
||||
if ( emote_set.users.indexOf(room_id) === -1 )
|
||||
emote_set.users.push(room_id);
|
||||
|
||||
// Update tab completion.
|
||||
if ( this.ffz._inputv )
|
||||
Ember.propertyDidChange(this.ffz._inputv, 'ffz_emoticons');
|
||||
}
|
||||
|
||||
|
||||
API.prototype.unregister_room_set = function(room_id, set_id) {
|
||||
var exact_id = this.id + '-' + set_id,
|
||||
emote_set = this.emote_sets[set_id],
|
||||
room = this.ffz.rooms && this.ffz.rooms[room_id];
|
||||
|
||||
if ( ! emote_set || ! room )
|
||||
return;
|
||||
|
||||
var ind = room.ext_sets ? room.ext_sets.indexOf(exact_id) : -1;
|
||||
if ( ind !== -1 )
|
||||
room.ext_sets.splice(ind,1);
|
||||
|
||||
ind = emote_set.users.indexOf(room_id);
|
||||
if ( ind !== -1 )
|
||||
emote_set.users.splice(ind,1);
|
||||
|
||||
// Update tab completion.
|
||||
if ( this.ffz._inputv )
|
||||
Ember.propertyDidChange(this.ffz._inputv, 'ffz_emoticons');
|
||||
}
|
||||
|
||||
|
||||
// -----------------------
|
||||
// Badge APIs
|
||||
// -----------------------
|
||||
|
||||
API.prototype.add_badge = function(badge_id, badge) {
|
||||
var exact_id = this.name_key + '-' + badge_id;
|
||||
|
||||
badge.id = badge_id;
|
||||
badge.source_ext = this.id,
|
||||
badge.real_id = exact_id;
|
||||
|
||||
if ( badge.replaces && badge.replaces !== true && ! badge.replaces_type ) {
|
||||
badge.replaces_type = badge.replaces;
|
||||
badge.replaces = true;
|
||||
}
|
||||
|
||||
if ( ! badge.color )
|
||||
badge.color = "transparent";
|
||||
|
||||
this.badges[badge_id] = badge;
|
||||
|
||||
if ( this.ffz.badges )
|
||||
this.ffz.badges[exact_id] = badge;
|
||||
|
||||
if ( this.ffz._badge_style )
|
||||
utils.update_css(this.ffz._badge_style, exact_id, utils.badge_css(badge));
|
||||
}
|
||||
|
||||
|
||||
API.prototype.remove_badge = function(badge_id) {
|
||||
var exact_id = this.name_key + '-' + badge_id;
|
||||
this.badges[badge_id] = undefined;
|
||||
|
||||
if ( this.ffz.badges )
|
||||
this.ffz.badges[exact_id] = undefined;
|
||||
|
||||
if ( this.ffz._badge_style )
|
||||
utils.update_css(this.ffz._badge_style, exact_id);
|
||||
}
|
||||
|
||||
|
||||
// -----------------------
|
||||
// User Modifications
|
||||
// -----------------------
|
||||
|
||||
API.prototype.user_add_badge = function(username, slot, badge_id) {
|
||||
var user = this.users[username] = this.users[username] || {},
|
||||
ffz_user = this.ffz.users[username] = this.ffz.users[username] || {},
|
||||
|
||||
badges = user.badges = user.badges || {},
|
||||
ffz_badges = ffz_user.badges = ffz_user.badges || {},
|
||||
|
||||
badge = typeof badge_id !== "object" ? {id: badge_id} : badge_id;
|
||||
|
||||
badge.real_id = this.name_key + '-' + badge.id;
|
||||
badges[slot] = ffz_badges[slot] = badge;
|
||||
}
|
||||
|
||||
|
||||
API.prototype.user_remove_badge = function(username, slot) {
|
||||
var user = this.users[username] = this.users[username] || {},
|
||||
ffz_user = this.ffz.users[username] = this.ffz.users[username] || {},
|
||||
|
||||
badges = user.badges = user.badges || {},
|
||||
ffz_badges = ffz_user.badges = ffz_user.badges || {};
|
||||
|
||||
badges[slot] = ffz_badges[slot] = null;
|
||||
}
|
||||
|
||||
|
||||
API.prototype.room_add_user_badge = function(room_name, username, slot, badge_id) {
|
||||
var ffz_room_users = this.ffz.rooms[room_name] && this.ffz.rooms[room_name].users;
|
||||
if ( ! ffz_room_users )
|
||||
return;
|
||||
|
||||
var ffz_user = ffz_room_users[username] = ffz_room_users[username] || {badges: {}, sets: []},
|
||||
ffz_badges = ffz_user && ffz_user.badges,
|
||||
|
||||
badge = typeof badge_id !== "object" ? {id: badge_id} : badge_id;
|
||||
|
||||
badge.real_id = this.name_key + '-' + badge.id;
|
||||
ffz_badges[slot] = badge;
|
||||
}
|
||||
|
||||
|
||||
API.prototype.room_remove_user_badge = function(room_name, username, slot) {
|
||||
var ffz_room_users = this.ffz.rooms[room_name] && this.ffz.rooms[room_name].users,
|
||||
ffz_user = ffz_room_users && ffz_room_users[username],
|
||||
ffz_badges = ffz_user && ffz_user.badges;
|
||||
|
||||
if ( ffz_badges )
|
||||
ffz_badges[slot] = null;
|
||||
}
|
||||
|
||||
|
||||
API.prototype.user_add_set = function(username, set_id) {
|
||||
var user = this.users[username] = this.users[username] || {},
|
||||
ffz_user = this.ffz.users[username] = this.ffz.users[username] || {},
|
||||
|
||||
emote_sets = user.sets = user.sets || [],
|
||||
ffz_sets = ffz_user.sets = ffz_user.sets || [],
|
||||
|
||||
exact_id = this.id + '-' + set_id;
|
||||
|
||||
if ( emote_sets.indexOf(set_id) === -1 )
|
||||
emote_sets.push(set_id);
|
||||
|
||||
if ( ffz_sets.indexOf(exact_id) === -1 )
|
||||
ffz_sets.push(exact_id);
|
||||
|
||||
// Update tab completion.
|
||||
var user = this.ffz.get_user();
|
||||
if ( this.ffz._inputv && user && user.login === username )
|
||||
Ember.propertyDidChange(this.ffz._inputv, 'ffz_emoticons');
|
||||
}
|
||||
|
||||
|
||||
API.prototype.user_remove_set = function(username, set_id) {
|
||||
var user = this.users[username],
|
||||
ffz_user = this.ffz.users[username],
|
||||
|
||||
emote_sets = user && user.sets,
|
||||
ffz_sets = ffz_user && ffz_user.sets,
|
||||
|
||||
exact_id = this.id + '-' + set_id;
|
||||
|
||||
var ind = emote_sets ? emote_sets.indexOf(set_id) : -1;
|
||||
if ( ind !== -1 )
|
||||
emote_sets.splice(ind, 1);
|
||||
|
||||
ind = ffz_sets ? ffz_sets.indexOf(exact_id) : -1;
|
||||
if ( ind !== -1 )
|
||||
ffz_sets.splice(ind, 1);
|
||||
|
||||
// Update tab completion.
|
||||
var user = this.ffz.get_user();
|
||||
if ( this.ffz._inputv && user && user.login === username )
|
||||
Ember.propertyDidChange(this.ffz._inputv, 'ffz_emoticons');
|
||||
}
|
||||
|
||||
|
||||
API.prototype.retokenize_messages = function(room, user, max_age, update_badges) {
|
||||
var rooms = room ? [room] : Object.keys(this.ffz.rooms),
|
||||
ffz_rooms = this.ffz && this.ffz.rooms || {};
|
||||
for(var i=0; i < rooms.length; i++) {
|
||||
var room_id = rooms[i],
|
||||
room = ffz_rooms[room_id];
|
||||
room && room.room && room.room.ffzRetokenizeUser(user, max_age, update_badges);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// -----------------------
|
||||
// Chat Callback
|
||||
// -----------------------
|
||||
|
||||
API.prototype.register_chat_filter = function(filter) {
|
||||
this.on('room-message', filter);
|
||||
}
|
||||
|
||||
API.prototype.unregister_chat_filter = function(filter) {
|
||||
this.off('room-message', filter);
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// Channel Callbacks
|
||||
// -----------------------
|
||||
|
||||
API.prototype.iterate_chat_views = function(func) {
|
||||
if ( func === undefined )
|
||||
func = this.trigger.bind(this, 'chat-view-init');
|
||||
|
||||
if ( this.ffz._chatv ) {
|
||||
var view = this.ffz._chatv;
|
||||
func(view.get('element'), view);
|
||||
}
|
||||
}
|
||||
|
||||
API.prototype.iterate_rooms = function(func) {
|
||||
if ( func === undefined )
|
||||
func = this.trigger.bind(this, 'room-add');
|
||||
|
||||
for(var room_id in this.ffz.rooms)
|
||||
func(room_id);
|
||||
}
|
||||
|
||||
API.prototype.register_on_room_callback = function(callback, dont_iterate) {
|
||||
var cb = this.register_room_set.bind(this, room_id),
|
||||
thing = function(room_id) {
|
||||
return callback(room_id, cb);
|
||||
};
|
||||
|
||||
thing.original_func = callback;
|
||||
this.on('room-add', thing);
|
||||
|
||||
if ( ! dont_iterate )
|
||||
this.iterate_rooms(thing);
|
||||
}
|
||||
|
||||
API.prototype.unregister_on_room_callback = function(callback) {
|
||||
var e = this._events['room-add'] || [];
|
||||
for(var i=e.length; i--;) {
|
||||
var cb = e[i];
|
||||
if ( cb && cb.original_func === callback )
|
||||
e.splice(i, 1);
|
||||
}
|
||||
}
|
|
@ -1,539 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ,
|
||||
constants = require('../constants'),
|
||||
utils = require('../utils'),
|
||||
SENDER_REGEX = /(\sdata-sender="[^"]*"(?=>))/,
|
||||
|
||||
HOP = Object.prototype.hasOwnProperty;
|
||||
|
||||
|
||||
// --------------------
|
||||
// Initialization
|
||||
// --------------------
|
||||
|
||||
FFZ.prototype.find_bttv = function(increment, delay) {
|
||||
this.has_bttv = this.has_bttv_6 = this.has_bttv_7 = false;
|
||||
if ( window.BetterTTV && BetterTTV.version && BetterTTV.version.indexOf('7.') === 0 )
|
||||
return this.setup_bttv_7(delay||0);
|
||||
|
||||
if ( window.BTTVLOADED )
|
||||
return this.setup_bttv(delay||0);
|
||||
|
||||
if ( delay >= 60000 )
|
||||
this.log("BetterTTV was not detected after 60 seconds.");
|
||||
else
|
||||
setTimeout(this.find_bttv.bind(this, increment, (delay||0) + increment),
|
||||
increment);
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.setup_bttv_7 = function(delay) {
|
||||
this.log("BetterTTV v7 was detected after " + delay + "ms. Hooking.");
|
||||
this.has_bttv = 7;
|
||||
this.has_bttv_6 = false;
|
||||
this.has_bttv_7 = true;
|
||||
|
||||
var settings = BetterTTV.settings,
|
||||
cl = document.body.classList;
|
||||
|
||||
// Disable FFZ Dark if it's enabled.
|
||||
cl.remove("ffz-dark");
|
||||
if ( this._dark_style ) {
|
||||
this._dark_style.parentElement.removeChild(this._dark_style);
|
||||
this._dark_style = undefined;
|
||||
}
|
||||
|
||||
// Disable other styling.
|
||||
if ( this._layout_style ) {
|
||||
this._layout_style.parentElement.removeChild(this._layout_style);
|
||||
this._layout_style = undefined;
|
||||
}
|
||||
|
||||
if ( this._chat_style ) {
|
||||
utils.update_css(this._chat_style, 'chat_font_size', '');
|
||||
utils.update_css(this._chat_style, 'chat_ts_font_size', '');
|
||||
}
|
||||
|
||||
this.toggle_style('chat-padding');
|
||||
this.toggle_style('chat-background');
|
||||
|
||||
this.toggle_style('chat-separator');
|
||||
this.toggle_style('chat-separator-3d');
|
||||
this.toggle_style('chat-separator-3d-inset');
|
||||
this.toggle_style('chat-separator-wide');
|
||||
|
||||
this.toggle_style('chat-colors-gray');
|
||||
/*this.toggle_style('badges-rounded');
|
||||
this.toggle_style('badges-circular');
|
||||
this.toggle_style('badges-blank');
|
||||
this.toggle_style('badges-circular-small');
|
||||
this.toggle_style('badges-transparent');*/
|
||||
this.toggle_style('badges-sub-notice');
|
||||
this.toggle_style('badges-sub-notice-on');
|
||||
|
||||
//cl.remove('ffz-transparent-badges');
|
||||
cl.remove("ffz-sidebar-swap");
|
||||
cl.remove("ffz-portrait");
|
||||
cl.remove("ffz-minimal-channel-title");
|
||||
cl.remove("ffz-flip-dashboard");
|
||||
cl.remove('ffz-minimal-channel-bar');
|
||||
cl.remove('ffz-channel-bar-bottom');
|
||||
cl.remove('ffz-channel-title-top');
|
||||
cl.remove('ffz-sidebar-minimize');
|
||||
cl.remove('ffz-alias-italics');
|
||||
|
||||
// Update the layout service.
|
||||
var Layout = utils.ember_lookup('service:layout');
|
||||
if ( Layout ) {
|
||||
Layout.set('ffzMinimizeNavigation', false);
|
||||
Layout.set('rawPortraitMode', 0);
|
||||
}
|
||||
|
||||
// Remove Following Count
|
||||
if ( this.settings.following_count ) {
|
||||
this._schedule_following_count();
|
||||
this._draw_following_count();
|
||||
this._draw_following_channels();
|
||||
}
|
||||
|
||||
|
||||
/* Update the chat input to not use FFZ's input handling.
|
||||
if ( this._inputv ) {
|
||||
var t = this._inputv.$("textarea");
|
||||
t.off("keyup");
|
||||
t.off("keydown");
|
||||
t.off("keypress");
|
||||
t.on("keyup", this._inputv._onKeyUp.bind(this._inputv));
|
||||
t.on("keydown", this._inputv._onKeyDown.bind(this._inputv));
|
||||
}//*/
|
||||
|
||||
|
||||
|
||||
// Hook into BTTV's dark mode.
|
||||
cl.add('ffz-bttv');
|
||||
cl.toggle('ffz-bttv-dark', settings.get('darkenedMode'));
|
||||
|
||||
settings.on('changed.darkenedMode', function(val) {
|
||||
cl.toggle('ffz-bttv-dark', val);
|
||||
});
|
||||
|
||||
this.update_ui_link();
|
||||
this._roomv && this._roomv.ffzUpdateRecent();
|
||||
this.api_trigger('bttv-initialized', 7);
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.setup_bttv = function(delay) {
|
||||
this.log("BetterTTV was detected after " + delay + "ms. Hooking.");
|
||||
this.has_bttv = true;
|
||||
this.has_bttv_6 = true;
|
||||
this.has_bttv_7 = false;
|
||||
|
||||
// Disable Dark if it's enabled.
|
||||
document.body.classList.remove("ffz-dark");
|
||||
if ( this._dark_style ) {
|
||||
this._dark_style.parentElement.removeChild(this._dark_style);
|
||||
this._dark_style = undefined;
|
||||
}
|
||||
|
||||
if ( this._layout_style ) {
|
||||
this._layout_style.parentElement.removeChild(this._layout_style);
|
||||
this._layout_style = undefined;
|
||||
}
|
||||
|
||||
if ( this._chat_style ) {
|
||||
utils.update_css(this._chat_style, 'chat_font_size', '');
|
||||
utils.update_css(this._chat_style, 'chat_ts_font_size', '');
|
||||
}
|
||||
|
||||
// Remove Sub Count and the Chart
|
||||
if ( this.is_dashboard ) {
|
||||
this._update_subscribers();
|
||||
this._remove_dash_chart();
|
||||
}
|
||||
|
||||
document.body.classList.add('ffz-bttv');
|
||||
|
||||
var last_dark = BetterTTV.settings.get('darkenedMode');
|
||||
document.body.classList.toggle('ffz-bttv-dark', last_dark);
|
||||
setInterval(function() {
|
||||
var new_dark = BetterTTV.settings.get('darkenedMode');
|
||||
if ( new_dark !== last_dark ) {
|
||||
document.body.classList.toggle('ffz-bttv-dark', new_dark);
|
||||
last_dark = new_dark;
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Disable Chat Tabs
|
||||
if ( this._chatv ) {
|
||||
if ( this.settings.group_tabs )
|
||||
this._chatv.ffzDisableTabs();
|
||||
|
||||
this._chatv.ffzTeardownMenu();
|
||||
this._chatv.ffzUnloadHost();
|
||||
}
|
||||
|
||||
if ( this._roomv ) {
|
||||
// Disable Chat Pause
|
||||
this._roomv.ffzDisableFreeze();
|
||||
this._roomv.ffzRemoveKeyHook();
|
||||
|
||||
// And hide the status
|
||||
if ( this.settings.room_status )
|
||||
this._roomv.ffzUpdateStatus();
|
||||
}
|
||||
|
||||
this.disconnect_extra_chat();
|
||||
|
||||
// Disable style blocks.
|
||||
this.toggle_style('chat-padding');
|
||||
this.toggle_style('chat-background');
|
||||
|
||||
this.toggle_style('chat-separator');
|
||||
this.toggle_style('chat-separator-3d');
|
||||
this.toggle_style('chat-separator-3d-inset');
|
||||
this.toggle_style('chat-separator-wide');
|
||||
|
||||
this.toggle_style('chat-colors-gray');
|
||||
this.toggle_style('badges-rounded');
|
||||
this.toggle_style('badges-circular');
|
||||
this.toggle_style('badges-blank');
|
||||
this.toggle_style('badges-circular-small');
|
||||
this.toggle_style('badges-transparent');
|
||||
this.toggle_style('badges-sub-notice');
|
||||
this.toggle_style('badges-sub-notice-on');
|
||||
|
||||
// Disable other features too.
|
||||
var cl = document.body.classList;
|
||||
cl.remove('ffz-transparent-badges');
|
||||
cl.remove("ffz-sidebar-swap");
|
||||
cl.remove("ffz-portrait");
|
||||
cl.remove("ffz-minimal-channel-title");
|
||||
cl.remove("ffz-flip-dashboard");
|
||||
cl.remove('ffz-minimal-channel-bar');
|
||||
cl.remove('ffz-channel-bar-bottom');
|
||||
cl.remove('ffz-channel-title-top');
|
||||
cl.remove('ffz-sidebar-minimize');
|
||||
cl.remove('ffz-alias-italics');
|
||||
|
||||
// Update the layout service.
|
||||
var Layout = utils.ember_lookup('service:layout');
|
||||
if ( Layout ) {
|
||||
Layout.set('ffzMinimizeNavigation', false);
|
||||
Layout.set('rawPortraitMode', 0);
|
||||
}
|
||||
|
||||
// Remove Following Count
|
||||
if ( this.settings.following_count ) {
|
||||
this._schedule_following_count();
|
||||
this._draw_following_count();
|
||||
this._draw_following_channels();
|
||||
}
|
||||
|
||||
// Send Message Behavior
|
||||
var f = this,
|
||||
BC = BetterTTV.chat,
|
||||
original_send = BC.helpers.sendMessage;
|
||||
|
||||
BC.helpers.sendMessage = function(message) {
|
||||
var cmd = message.split(' ', 1)[0].toLowerCase();
|
||||
|
||||
if ( cmd === '/ffz' )
|
||||
f.run_ffz_command(message.substr(5), BC.store.currentRoom);
|
||||
else
|
||||
return original_send(message);
|
||||
}
|
||||
|
||||
|
||||
// Ugly Hack for Current Room, as this is stripped out before we get to
|
||||
// the actual privmsg renderer.
|
||||
var original_handler = BC.handlers.onPrivmsg,
|
||||
received_room;
|
||||
|
||||
BC.handlers.onPrivmsg = function(room, data) {
|
||||
received_room = room;
|
||||
var output = original_handler(room, data);
|
||||
received_room = null;
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
// Message Display Behavior
|
||||
var original_privmsg = BC.templates.privmsg;
|
||||
BC.templates.privmsg = function(data, opts) {
|
||||
try {
|
||||
opts = opts || {};
|
||||
|
||||
// Handle badges.
|
||||
data.room = data.room || received_room;
|
||||
f.bttv_badges(data);
|
||||
|
||||
// API Support
|
||||
f.api_trigger('bttv-room-message', data, opts);
|
||||
|
||||
// Now, do everything else manually because things are hard-coded.
|
||||
return '<div class="chat-line'+(opts.highlight?' highlight':'')+(opts.action?' action':'')+(opts.server?' admin':'')+(opts.notice?' notice':'')+'" data-id="' + data.id + '" data-sender="'+(data.sender||"").toLowerCase()+'" data-room="'+received_room+'">'+
|
||||
BC.templates.timestamp(data.time)+' '+
|
||||
(opts.isMod ? BC.templates.modicons():'')+' '+
|
||||
BC.templates.badges(data.badges)+
|
||||
BC.templates.from(data.nickname, data.color)+
|
||||
BC.templates.message(data.sender, data.message, {
|
||||
emotes: data.emotes,
|
||||
colored: (opts.action && !opts.highlight) ? data.color : false,
|
||||
bits: data.bits
|
||||
}) +
|
||||
'</div>';
|
||||
} catch(err) {
|
||||
f.log("Error: ", err);
|
||||
return original_privmsg(data, opts);
|
||||
}
|
||||
}
|
||||
|
||||
// Whispers too!
|
||||
var original_whisper = BC.templates.whisper;
|
||||
BC.templates.whisper = function(data) {
|
||||
try {
|
||||
// Handle badges.
|
||||
f.bttv_badges(data);
|
||||
|
||||
// Now, do everything else manually because things are hard-coded.
|
||||
return '<div class="chat-line whisper" data-sender="' + data.sender + '">' +
|
||||
BC.templates.timestamp(data.time) + ' ' +
|
||||
(data.badges && data.badges.length ? BC.templates.badges(data.badges) : '') +
|
||||
BC.templates.whisperName(data.sender, data.receiver, data.from, data.to, data.fromColor, data.toColor) +
|
||||
BC.templates.message(data.sender, data.message, {
|
||||
emotes: data.emotes,
|
||||
colored: false
|
||||
}) +
|
||||
'</div>';
|
||||
} catch(err) {
|
||||
f.log("Error: ", err);
|
||||
return original_whisper(data);
|
||||
}
|
||||
}
|
||||
|
||||
// Message Renderer. I had to completely rewrite this method to get it to
|
||||
// use my replacement emoticonizer.
|
||||
var original_message = BC.templates.message,
|
||||
received_sender;
|
||||
BC.templates.message = function(sender, message, data) {
|
||||
try {
|
||||
data = data || {};
|
||||
var colored = data.colored || false,
|
||||
force = data.force || false,
|
||||
emotes = data.emotes,
|
||||
bits = data.bits,
|
||||
rawMessage = encodeURIComponent(message);
|
||||
|
||||
if(sender !== 'jtv') {
|
||||
// Hackilly send our state across.
|
||||
received_sender = sender;
|
||||
var tokenizedMessage = BC.templates.emoticonize(message, emotes);
|
||||
received_sender = null;
|
||||
|
||||
for(var i=0; i<tokenizedMessage.length; i++) {
|
||||
if(typeof tokenizedMessage[i] === 'string') {
|
||||
tokenizedMessage[i] = BC.templates.bttvMessageTokenize(sender, tokenizedMessage[i], bits);
|
||||
} else {
|
||||
tokenizedMessage[i] = tokenizedMessage[i][0];
|
||||
}
|
||||
}
|
||||
|
||||
message = tokenizedMessage.join(' ');
|
||||
}
|
||||
|
||||
return '<span class="message" ' + (colored ? 'style="color: ' + colored + '" ' : '') + 'data-raw="' + rawMessage + '" data-bits="' + (bits ? encodeURIComponent(JSON.stringify(bits)) : 'false') + '" data-emotes="' + (emotes ? encodeURIComponent(JSON.stringify(emotes)) : 'false') + '">' + message + '</span>';
|
||||
|
||||
} catch(err) {
|
||||
f.log("Error: ", err);
|
||||
return original_message(sender, message, emotes, colored);
|
||||
}
|
||||
};
|
||||
|
||||
// Tab Completion
|
||||
var original_emotes = BC.emotes;
|
||||
|
||||
BC.emotes = function() {
|
||||
var output = original_emotes(),
|
||||
user = f.get_user(),
|
||||
room_id = BetterTTV.getChannel(),
|
||||
|
||||
ffz_sets = f.getEmotes(user && user.login, room_id);
|
||||
|
||||
for(var i=0; i < ffz_sets.length; i++) {
|
||||
var emote_set = f.emote_sets[ffz_sets[i]];
|
||||
if ( ! emote_set )
|
||||
continue;
|
||||
|
||||
var set_name = (emote_set.source || "FFZ") + " " + (emote_set.title || "Global") + " Emotes",
|
||||
set_icon = emote_set.icon || (emote_set.hasOwnProperty('source_ext') && f._apis[emote_set.source_ext] && f._apis[emote_set.source_ext].icon) || '//cdn.frankerfacez.com/script/devicon.png';
|
||||
|
||||
for(var emote_id in emote_set.emoticons) {
|
||||
var emote = emote_set.emoticons[emote_id];
|
||||
if ( ! emote.hidden && emote.name ) {
|
||||
output.push({
|
||||
text: emote.name,
|
||||
channel: set_name,
|
||||
badge: set_icon,
|
||||
url: emote.urls[1]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
// Emoji!
|
||||
var parse_emoji = function(token) {
|
||||
var setting = f.settings.parse_emoji,
|
||||
output = [],
|
||||
segments = token.split(constants.EMOJI_REGEX),
|
||||
text = null;
|
||||
|
||||
while(segments.length) {
|
||||
text = (text || '') + segments.shift();
|
||||
if ( segments.length ) {
|
||||
var match = segments.shift(),
|
||||
eid = utils.emoji_to_codepoint(match),
|
||||
data = f.emoji_data[eid],
|
||||
src = data && (setting === 3 ? data.one_src : (setting === 2 ? data.noto_src : data.tw_src));
|
||||
|
||||
if ( src ) {
|
||||
if ( text && text.length )
|
||||
output.push(text);
|
||||
|
||||
// We still want to use a special token even if emoji display is disabled
|
||||
// as, otherwise, BTTV will render the emoji itself, which the user has no
|
||||
// way of disabling if not for this.
|
||||
if ( setting === 0 )
|
||||
output.push([data.raw]);
|
||||
else {
|
||||
var code = utils.quote_attr(data.raw),
|
||||
html = '<img class="emoticon emoji ffz-tooltip" height="18px" data-ffz-emoji="' + eid + '" src="' + utils.quote_attr(src) + '" alt="' + code + '">';
|
||||
output.push([html, html, []]);
|
||||
}
|
||||
text = null;
|
||||
} else
|
||||
text = (text || '') + match;
|
||||
}
|
||||
}
|
||||
|
||||
if ( text && text.length )
|
||||
output.push(text);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// Emoticonize
|
||||
var emote_token = function(emote) {
|
||||
return '<img class="emoticon ffz-tooltip" data-ffz-set="' + emote.set_id + '" data-ffz-emote="' + emote.id + '" srcset="' + utils.quote_attr(emote.srcSet || "") + '" src="' + utils.quote_attr(emote.urls[1]) + '" alt="' + utils.quote_attr(emote.name) + '">';
|
||||
};
|
||||
|
||||
var original_emoticonize = BC.templates.emoticonize;
|
||||
BC.templates.emoticonize = function(message, emotes) {
|
||||
var tokens = original_emoticonize(message, emotes),
|
||||
|
||||
room = (received_room || BetterTTV.getChannel()),
|
||||
l_room = room && room.toLowerCase(),
|
||||
l_sender = received_sender && received_sender.toLowerCase(),
|
||||
sets = f.getEmotes(l_sender, l_room),
|
||||
emotes = {}, emote,
|
||||
user = f.get_user(),
|
||||
new_tokens = [],
|
||||
mine = user && user.login === l_sender;
|
||||
|
||||
// Build an object with all of our emotes.
|
||||
for(var i=0; i < sets.length; i++) {
|
||||
var emote_set = f.emote_sets[sets[i]];
|
||||
if ( emote_set && emote_set.emoticons )
|
||||
for(var emote_id in emote_set.emoticons) {
|
||||
emote = emote_set.emoticons[emote_id];
|
||||
if ( ! HOP.call(emotes, emote.name) )
|
||||
emotes[emote.name] = emote;
|
||||
}
|
||||
}
|
||||
|
||||
//var last_token = null;
|
||||
for(var i=0, l=tokens.length; i < l; i++) {
|
||||
var token = tokens[i];
|
||||
if ( typeof token !== "string" ) {
|
||||
// Detect emoticons!
|
||||
/*if ( /class="emoticon/.test(token[0]) ) {
|
||||
if ( token.length === 1 ) {
|
||||
token = [token[0], token[0], []];
|
||||
}
|
||||
|
||||
last_token = token;
|
||||
|
||||
} else
|
||||
last_token = null;*/
|
||||
|
||||
new_tokens.push(token);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Split the token!
|
||||
var segments = token.split(' '),
|
||||
text = [], segment;
|
||||
|
||||
for(var x=0,y=segments.length; x < y; x++) {
|
||||
segment = segments[x];
|
||||
if ( HOP.call(emotes, segment) ) {
|
||||
emote = emotes[segment];
|
||||
|
||||
/*if ( false && emote.modifier && last_token && last_token.length > 1 ) {
|
||||
if ( last_token[2].indexOf(emote) === -1 ) {
|
||||
last_token[2].push(emote);
|
||||
|
||||
last_token[0] = '<span class="emoticon modified-emoticon">' + last_token[1] + _.map(last_token[2], function(em) {
|
||||
return ' <span>' + emote_token(em) + '</span>';
|
||||
}).join('') + '</span>'
|
||||
|
||||
}
|
||||
|
||||
if ( mine && l_room )
|
||||
f.add_usage(l_room, emote);
|
||||
|
||||
continue;
|
||||
}*/
|
||||
|
||||
if ( text.length ) {
|
||||
var toks = parse_emoji(text.join(' ') + ' ');
|
||||
for(var q=0; q < toks.length; q++) {
|
||||
var tok = toks[q];
|
||||
/*if ( tok.length > 1 )
|
||||
last_token = tok;
|
||||
else
|
||||
last_token = null;*/
|
||||
|
||||
new_tokens.push(tok);
|
||||
}
|
||||
|
||||
text = [];
|
||||
}
|
||||
|
||||
var html = emote_token(emote);
|
||||
new_tokens.push([html, html, []]);
|
||||
|
||||
if ( mine && l_room )
|
||||
f.add_usage(l_room, emote);
|
||||
|
||||
text.push('');
|
||||
} else
|
||||
text.push(segment);
|
||||
}
|
||||
|
||||
if ( text.length > 1 || (text.length === 1 && text[0] !== '') ) {
|
||||
var toks = parse_emoji(text.join(' ') + ' ');
|
||||
for(var q=0; q < toks.length; q++)
|
||||
new_tokens.push(toks[q]);
|
||||
}
|
||||
}
|
||||
|
||||
return new_tokens;
|
||||
}
|
||||
|
||||
this.update_ui_link();
|
||||
this.api_trigger('bttv-initialized');
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ,
|
||||
utils = require('../utils');
|
||||
|
||||
|
||||
// --------------------
|
||||
// Initialization
|
||||
// --------------------
|
||||
|
||||
FFZ.prototype.find_emote_menu = function(increment, delay) {
|
||||
this.has_emote_menu = false;
|
||||
if ( window.emoteMenu && emoteMenu.registerEmoteGetter )
|
||||
return this.setup_emote_menu(delay||0);
|
||||
|
||||
if ( delay >= 60000 )
|
||||
this.log("Emote Menu for Twitch was not detected after 60 seconds.");
|
||||
else
|
||||
setTimeout(this.find_emote_menu.bind(this, increment, (delay||0) + increment),
|
||||
increment);
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.setup_emote_menu = function(delay) {
|
||||
this.log("Emote Menu for Twitch was detected after " + delay + "ms. Registering emote enumerator.");
|
||||
emoteMenu.registerEmoteGetter("FrankerFaceZ", this._emote_menu_enumerator.bind(this));
|
||||
}
|
||||
|
||||
|
||||
// --------------------
|
||||
// Emote Enumerator
|
||||
// --------------------
|
||||
|
||||
FFZ.prototype._emote_menu_enumerator = function() {
|
||||
if ( this.has_bttv_6 )
|
||||
return [];
|
||||
|
||||
var twitch_user = this.get_user(),
|
||||
user_id = twitch_user ? twitch_user.login : null,
|
||||
controller = utils.ember_lookup('controller:chat'),
|
||||
room_id = controller ? controller.get('currentRoom.id') : null,
|
||||
sets = this.getEmotes(user_id, room_id),
|
||||
emotes = [];
|
||||
|
||||
for(var x = 0; x < sets.length; x++) {
|
||||
var set = this.emote_sets[sets[x]];
|
||||
if ( ! set || ! set.emoticons )
|
||||
continue;
|
||||
|
||||
for(var emote_id in set.emoticons) {
|
||||
if ( ! set.emoticons.hasOwnProperty(emote_id) )
|
||||
continue;
|
||||
|
||||
var emote = set.emoticons[emote_id];
|
||||
if ( emote.hidden )
|
||||
continue;
|
||||
|
||||
var title = (set.source || "FrankerFaceZ") + " " + set.title,
|
||||
badge = set.icon || '//cdn.frankerfacez.com/script/devicon.png';
|
||||
|
||||
emotes.push({text: emote.name, url: emote.urls[1],
|
||||
hidden: false, channel: title, badge: badge});
|
||||
}
|
||||
}
|
||||
|
||||
return emotes;
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ;
|
||||
|
||||
// --------------------
|
||||
// Initialization
|
||||
// --------------------
|
||||
|
||||
FFZ.settings_info.warp_world = {
|
||||
type: "boolean",
|
||||
value: true,
|
||||
|
||||
category: "Channel Metadata",
|
||||
no_mobile: true,
|
||||
|
||||
name: "Warp World <small>(Requires Refresh)</small>",
|
||||
help: 'Automatically load <a href="https://warp.world" target="_blank">Warp World</a> when viewing a channel that uses Warp World.'
|
||||
}
|
||||
|
||||
FFZ.ws_commands.warp_world = function(data) {
|
||||
if ( ! data || ! this.settings.warp_world )
|
||||
return;
|
||||
|
||||
// Make sure that Warp World isn't already loaded or loading.
|
||||
var ww_script = document.querySelector('script#ww_script');
|
||||
if ( ww_script || window.WarpWorld )
|
||||
return;
|
||||
|
||||
ww_script = document.createElement('script');
|
||||
ww_script.id = 'ww_script';
|
||||
ww_script.src = '//cdn.warp.world/twitch_script/main.min.js?_=' + Date.now();
|
||||
document.head.appendChild(ww_script);
|
||||
}
|
|
@ -1,149 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ,
|
||||
constants = require('./constants'),
|
||||
utils = require('./utils');
|
||||
|
||||
|
||||
// --------------------
|
||||
// Initialization
|
||||
// --------------------
|
||||
|
||||
FFZ.prototype.feature_friday = null;
|
||||
|
||||
|
||||
// --------------------
|
||||
// Check FF
|
||||
// --------------------
|
||||
|
||||
FFZ.prototype.check_ff = function(tries) {
|
||||
if ( ! tries )
|
||||
this.log("Checking for Feature Friday data...");
|
||||
|
||||
jQuery.ajax(constants.SERVER + "script/event.json?_=" + (constants.DEBUG ? Date.now() : FFZ.version_info), {dataType: "json", context: this})
|
||||
.done(function(data) {
|
||||
return this._load_ff(data);
|
||||
}).fail(function(data) {
|
||||
if ( data.status == 404 )
|
||||
return this._load_ff(null);
|
||||
|
||||
tries = tries || 0;
|
||||
tries++;
|
||||
if ( tries < 10 )
|
||||
return setTimeout(this.check_ff.bind(this, tries), 250);
|
||||
|
||||
return this._load_ff(null);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
FFZ.ws_commands.reload_ff = function() {
|
||||
this.check_ff();
|
||||
}
|
||||
|
||||
|
||||
// --------------------
|
||||
// Rendering UI
|
||||
// --------------------
|
||||
|
||||
FFZ.prototype._feature_friday_ui = function(room_id, parent, view) {
|
||||
if ( ! this.feature_friday || this.feature_friday.channel === room_id )
|
||||
return;
|
||||
|
||||
this._emotes_for_set(parent, view, this.feature_friday.set, this.feature_friday.title, this.feature_friday.icon, "FrankerFaceZ");
|
||||
|
||||
// Before we add the button, make sure the channel isn't the
|
||||
// current channel.
|
||||
var Channel = utils.ember_lookup('controller:channel');
|
||||
if ( ! this.feature_friday.channel || (Channel && Channel.get('id') === this.feature_friday.channel) )
|
||||
return;
|
||||
|
||||
|
||||
var ff = this.feature_friday, f = this,
|
||||
btnc = document.createElement('div'),
|
||||
btn = document.createElement('a');
|
||||
|
||||
btnc.className = 'chat-menu-content';
|
||||
btnc.style.textAlign = 'center';
|
||||
|
||||
var message = ff.display_name + (ff.live ? " is live now!" : "");
|
||||
|
||||
btn.className = 'button primary';
|
||||
btn.classList.toggle('live', ff.live);
|
||||
btn.classList.toggle('blue', this.has_bttv_6 && BetterTTV.settings.get('showBlueButtons'));
|
||||
|
||||
btn.href = "//www.twitch.tv/" + ff.channel;
|
||||
btn.title = message;
|
||||
btn.target = "_new";
|
||||
btn.innerHTML = "<span>" + message + "</span>";
|
||||
|
||||
// Track the number of users to click this button.
|
||||
// btn.addEventListener('click', function() { f.track('trackLink', this.href, 'link'); });
|
||||
|
||||
btnc.appendChild(btn);
|
||||
parent.appendChild(btnc);
|
||||
}
|
||||
|
||||
|
||||
// --------------------
|
||||
// Loading Data
|
||||
// --------------------
|
||||
|
||||
FFZ.prototype._load_ff = function(data) {
|
||||
// Check for previous Feature Friday data and remove it.
|
||||
if ( this.feature_friday ) {
|
||||
// Remove the global set, delete the data, and reset the UI link.
|
||||
var ind = this.global_sets.indexOf(this.feature_friday.set);
|
||||
if ( ind !== -1 )
|
||||
this.global_sets.splice(ind, 1);
|
||||
|
||||
ind = this.default_sets.indexOf(this.feature_friday.set);
|
||||
if ( ind !== -1 )
|
||||
this.default_sets.splice(ind, 1);
|
||||
|
||||
this.feature_friday = null;
|
||||
this.update_ui_link();
|
||||
}
|
||||
|
||||
// If there's no data, just leave.
|
||||
if ( ! data || ! data.set )
|
||||
return;
|
||||
|
||||
// We have our data! Set it up.
|
||||
this.feature_friday = {
|
||||
set: data.set,
|
||||
channel: data.channel,
|
||||
live: false,
|
||||
title: data.title || "Feature Friday",
|
||||
icon: data.icon,
|
||||
display_name: data.channel ? FFZ.get_capitalization(data.channel, this._update_ff_name.bind(this)) : data.title || "Feature Friday"
|
||||
};
|
||||
|
||||
// Add the set.
|
||||
this.global_sets.push(data.set);
|
||||
this.default_sets.push(data.set);
|
||||
this.load_set(data.set);
|
||||
|
||||
// Check to see if the channel is live.
|
||||
this._update_ff_live();
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype._update_ff_live = function() {
|
||||
if ( ! this.feature_friday || ! this.feature_friday.channel )
|
||||
return;
|
||||
|
||||
var f = this;
|
||||
utils.api.get("streams/" + this.feature_friday.channel)
|
||||
.done(function(data) {
|
||||
f.feature_friday.live = data.stream != null;
|
||||
f.update_ui_link();
|
||||
})
|
||||
.always(function() {
|
||||
f.feature_friday.timer = setTimeout(f._update_ff_live.bind(f), 120000);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype._update_ff_name = function(name) {
|
||||
if ( this.feature_friday )
|
||||
this.feature_friday.display_name = name;
|
||||
}
|
489
src/i18n.js
Normal file
489
src/i18n.js
Normal file
|
@ -0,0 +1,489 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Localization
|
||||
// This is based on Polyglot, but with some changes to avoid dependencies on
|
||||
// additional libraries and with support for Vue.
|
||||
// ============================================================================
|
||||
|
||||
import {SERVER} from 'utilities/constants';
|
||||
import {has} from 'utilities/object';
|
||||
import Module from 'utilities/module';
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// TranslationManager
|
||||
// ============================================================================
|
||||
|
||||
export class TranslationManager extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.inject('settings');
|
||||
|
||||
this.availableLocales = ['en']; //, 'de', 'ja'];
|
||||
|
||||
this.localeData = {
|
||||
en: { name: 'English' },
|
||||
//de: { name: 'Deutsch' },
|
||||
//ja: { name: '日本語' }
|
||||
}
|
||||
|
||||
this.settings.add('i18n.locale', {
|
||||
default: -1,
|
||||
process: (ctx, val) => {
|
||||
if ( val === -1 )
|
||||
val = ctx.get('context.session.languageCode');
|
||||
|
||||
return this.availableLocales.includes(val) ? val : 'en'
|
||||
},
|
||||
|
||||
_ui: {
|
||||
path: 'Appearance > Localization >> General',
|
||||
title: 'Language',
|
||||
// description: '',
|
||||
|
||||
component: 'setting-select-box',
|
||||
data: (profile, val) => [{
|
||||
selected: val === -1,
|
||||
value: -1,
|
||||
i18n_key: 'setting.appearance.localization.general.language.twitch',
|
||||
title: "Use Twitch's Language"
|
||||
}].concat(this.availableLocales.map(l => ({
|
||||
selected: val === l,
|
||||
value: l,
|
||||
title: this.localeData[l].name
|
||||
})))
|
||||
},
|
||||
|
||||
changed: val => this.locale = val
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
this._ = new TranslationCore; /*({
|
||||
awarn: (...args) => this.log.info(...args)
|
||||
});*/
|
||||
|
||||
this.locale = this.settings.get('i18n.locale');
|
||||
}
|
||||
|
||||
get locale() {
|
||||
return this._.locale;
|
||||
}
|
||||
|
||||
set locale(new_locale) {
|
||||
this.setLocale(new_locale);
|
||||
}
|
||||
|
||||
|
||||
toLocaleString(thing) {
|
||||
if ( thing && thing.toLocaleString )
|
||||
return thing.toLocaleString(this._.locale);
|
||||
return thing;
|
||||
}
|
||||
|
||||
|
||||
async loadLocale(locale) {
|
||||
/*if ( locale === 'en' )
|
||||
return {};
|
||||
|
||||
if ( locale === 'de' )
|
||||
return {
|
||||
site: {
|
||||
menu_button: 'FrankerFaceZ Leitstelle'
|
||||
},
|
||||
|
||||
player: {
|
||||
reset_button: 'Doppelklicken, um den Player zurückzusetzen'
|
||||
},
|
||||
|
||||
setting: {
|
||||
reset: 'Zurücksetzen',
|
||||
|
||||
appearance: {
|
||||
_: 'Aussehen',
|
||||
description: 'Personalisieren Sie das Aussehen von Twitch. Ändern Sie das Farbschema und die Schriften und stimmen Sie das Layout so ab, dass Sie ein optimales Erlebnis erleben.<br><br>(Yes, this is Google Translate still.)',
|
||||
localization: {
|
||||
_: 'Lokalisierung',
|
||||
|
||||
general: {
|
||||
language: {
|
||||
_: 'Sprache',
|
||||
twitch: "Verwenden Sie Twitch's Sprache"
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
dates_and_times: {
|
||||
_: 'Termine und Zeiten',
|
||||
allow_relative_times: {
|
||||
_: 'Relative Zeiten zulassen',
|
||||
description: 'Wenn dies aktiviert ist, zeigt FrankerFaceZ einige Male in einem relativen Format an. <br>Beispiel: vor 3 Stunden'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
layout: 'Layout',
|
||||
theme: 'Thema'
|
||||
},
|
||||
|
||||
profiles: {
|
||||
_: 'Profile',
|
||||
|
||||
active: 'Dieses Profil ist aktiv.',
|
||||
inactive: {
|
||||
_: 'Dieses Profil ist nicht aktiv.',
|
||||
description: 'Dieses Profil stimmt nicht mit dem aktuellen Kontext überein und ist momentan nicht aktiv, so dass Sie keine Änderungen sehen, die Sie hier bei Twitch vorgenommen haben.'
|
||||
},
|
||||
|
||||
configure: 'Konfigurieren',
|
||||
|
||||
default: {
|
||||
_: 'Standard Profil',
|
||||
description: 'Einstellungen, die überall auf Twitch angewendet werden.'
|
||||
},
|
||||
|
||||
moderation: {
|
||||
_: 'Mäßigung',
|
||||
description: 'Einstellungen, die gelten, wenn Sie ein Moderator des aktuellen Kanals sind.'
|
||||
}
|
||||
},
|
||||
|
||||
add_ons: {
|
||||
_: 'Erweiterung'
|
||||
},
|
||||
|
||||
'inherited-from': 'Vererbt von: %{title}',
|
||||
'overridden-by': 'Überschrieben von: %{title}'
|
||||
},
|
||||
|
||||
'main-menu': {
|
||||
search: 'Sucheinstellungen',
|
||||
|
||||
about: {
|
||||
_: 'Über',
|
||||
news: 'Nachrichten',
|
||||
support: 'Unterstützung'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( locale === 'ja' )
|
||||
return {
|
||||
greeting: 'こんにちは',
|
||||
|
||||
site: {
|
||||
menu_button: 'FrankerFaceZコントロールセンター'
|
||||
},
|
||||
|
||||
setting: {
|
||||
appearance: {
|
||||
_: '外観',
|
||||
localization: '局地化',
|
||||
layout: '設計',
|
||||
theme: '題材'
|
||||
}
|
||||
},
|
||||
|
||||
'main-menu': {
|
||||
search: '検索設定',
|
||||
version: 'バージョン%{version}',
|
||||
|
||||
about: {
|
||||
_: '約',
|
||||
news: '便り',
|
||||
support: '対応'
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
const resp = await fetch(`${SERVER}/script/i18n/${locale}.json`);
|
||||
if ( ! resp.ok ) {
|
||||
if ( resp.status === 404 ) {
|
||||
this.log.info(`Cannot Load Locale: ${locale}`);
|
||||
return {};
|
||||
}
|
||||
|
||||
this.log.warn(`Cannot Load Locale: ${locale} -- Status: ${resp.status}`);
|
||||
throw new Error(`http error ${resp.status} loading phrases`);
|
||||
}
|
||||
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
async setLocale(new_locale) {
|
||||
const old_locale = this._.locale;
|
||||
if ( new_locale === old_locale )
|
||||
return [];
|
||||
|
||||
this._.locale = new_locale;
|
||||
this._.clear();
|
||||
this.log.info(`Changed Locale: ${new_locale} -- Old: ${old_locale}`);
|
||||
this.emit(':changed', new_locale, old_locale);
|
||||
this.emit(':update');
|
||||
|
||||
if ( new_locale === 'en' ) {
|
||||
// All the built-in messages are English. We don't need special
|
||||
// logic to load the translations.
|
||||
this.emit(':loaded', []);
|
||||
return [];
|
||||
}
|
||||
|
||||
const phrases = await this.loadLocale(new_locale);
|
||||
|
||||
if ( this._.locale !== new_locale )
|
||||
throw new Error('locale has changed since we started loading');
|
||||
|
||||
const added = this._.extend(phrases);
|
||||
if ( added.length ) {
|
||||
this.log.info(`Loaded Locale: ${new_locale} -- Phrases: ${added.length}`);
|
||||
this.emit(':loaded', added);
|
||||
this.emit(':update');
|
||||
}
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
has(key) {
|
||||
return this._.has(key);
|
||||
}
|
||||
|
||||
formatNumber(...args) {
|
||||
return this._.formatNumber(...args);
|
||||
}
|
||||
|
||||
t(...args) {
|
||||
return this._.t(...args);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// TranslationCore
|
||||
// ============================================================================
|
||||
|
||||
const REPLACE = String.prototype.replace,
|
||||
SPLIT = String.prototype.split;
|
||||
|
||||
const DEFAULT_FORMATTERS = {
|
||||
en_plural: n => n !== 1 ? 's' : '',
|
||||
number: (n, locale) => n.toLocaleString(locale)
|
||||
}
|
||||
|
||||
|
||||
export default class TranslationCore {
|
||||
constructor(options) {
|
||||
options = options || {};
|
||||
this.warn = options.warn;
|
||||
this.phrases = new Map;
|
||||
this.extend(options.phrases);
|
||||
this.locale = options.locale || 'en';
|
||||
this.defaultLocale = options.defaultLocale || this.locale;
|
||||
|
||||
const allowMissing = options.allowMissing ? transformPhrase : null;
|
||||
this.onMissingKey = typeof options.onMissingKey === 'function' ? options.onMissingKey : allowMissing;
|
||||
this.transformPhrase = typeof options.transformPhrase === 'function' ? options.transformPhrase : transformPhrase;
|
||||
this.delimiter = options.delimiter || /\s*\|\|\|\|\s*/;
|
||||
this.tokenRegex = options.tokenRegex || /%\{(.*?)(?:\|(.*?))?\}/g;
|
||||
this.formatters = Object.assign({}, DEFAULT_FORMATTERS, options.formatters || {});
|
||||
}
|
||||
|
||||
|
||||
formatNumber(value) {
|
||||
return value.toLocaleString(this.locale);
|
||||
}
|
||||
|
||||
|
||||
extend(phrases, prefix) {
|
||||
const added = [];
|
||||
for(const key in phrases)
|
||||
if ( has(phrases, key) ) {
|
||||
let phrase = phrases[key];
|
||||
const pref_key = prefix ? key === '_' ? prefix : `${prefix}.${key}` : key;
|
||||
|
||||
if ( typeof phrase === 'object' )
|
||||
added.push(...this.extend(phrase, pref_key));
|
||||
else {
|
||||
if ( typeof phrase === 'string' && phrase.indexOf(this.delimiter) !== -1 )
|
||||
phrase = SPLIT.call(phrase, this.delimiter);
|
||||
this.phrases.set(pref_key, phrase);
|
||||
added.push(pref_key);
|
||||
}
|
||||
}
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
unset(phrases, prefix) {
|
||||
if ( typeof phrases === 'string' )
|
||||
phrases = [phrases];
|
||||
|
||||
const keys = Array.isArray(phrases) ? phrases : Object.keys(phrases);
|
||||
for(const key of keys) {
|
||||
const pref_key = prefix ? `${prefix}.${key}` : key;
|
||||
const phrase = phrases[key];
|
||||
if ( typeof phrase === 'object' )
|
||||
this.unset(phrase, pref_key);
|
||||
else
|
||||
this.phrases.delete(pref_key);
|
||||
}
|
||||
}
|
||||
|
||||
has(key) {
|
||||
return this.phrases.has(key);
|
||||
}
|
||||
|
||||
set(key, phrase) {
|
||||
if ( typeof phrase === 'string' && phrase.indexOf(this.delimiter) !== -1 )
|
||||
phrase = SPLIT.call(phrase, this.delimiter);
|
||||
|
||||
this.phrases.set(key, phrase);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.phrases.clear();
|
||||
}
|
||||
|
||||
replace(phrases) {
|
||||
this.clear();
|
||||
this.extend(phrases);
|
||||
}
|
||||
|
||||
t(key, phrase, options, use_default) {
|
||||
const opts = options == null ? {} : options;
|
||||
let p, locale;
|
||||
|
||||
if ( use_default ) {
|
||||
p = phrase;
|
||||
locale = this.defaultLocale;
|
||||
|
||||
} else if ( key === undefined && phrase ) {
|
||||
p = phrase;
|
||||
locale = this.defaultLocale;
|
||||
if ( this.warn )
|
||||
this.warn(`Translation key not generated with phrase "${phrase}"`);
|
||||
|
||||
} else if ( this.phrases.has(key) ) {
|
||||
p = this.phrases.get(key);
|
||||
locale = this.locale;
|
||||
} else if ( phrase ) {
|
||||
if ( this.warn && this.locale !== this.defaultLocale )
|
||||
this.warn(`Missing translation for key "${key}" in locale "${this.locale}"`);
|
||||
|
||||
p = phrase;
|
||||
locale = this.defaultLocale;
|
||||
} else if ( this.onMissingKey )
|
||||
return this.onMissingKey(key, opts, this.locale, this.tokenRegex, this.formatters);
|
||||
else {
|
||||
if ( this.warn )
|
||||
this.warn(`Missing translation for key "${key}" in locale "${this.locale}"`);
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
return this.transformPhrase(p, opts, locale, this.tokenRegex, this.formatters);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Transformations
|
||||
// ============================================================================
|
||||
|
||||
const DOLLAR_REGEX = /\$/g;
|
||||
|
||||
export function transformPhrase(phrase, substitutions, locale, token_regex, formatters) {
|
||||
const is_array = Array.isArray(phrase);
|
||||
if ( substitutions == null )
|
||||
return is_array ? phrase[0] : phrase;
|
||||
|
||||
let result = phrase;
|
||||
const options = typeof substitutions === 'number' ? {count: substitutions} : substitutions;
|
||||
|
||||
if ( is_array )
|
||||
result = result[pluralTypeIndex(
|
||||
locale || 'en',
|
||||
has(options, 'count') ? options.count : 1
|
||||
)] || result[0];
|
||||
|
||||
if ( typeof result === 'string' )
|
||||
result = REPLACE.call(result, token_regex, (expr, arg, fmt) => {
|
||||
if ( ! has(options, arg) )
|
||||
return '';
|
||||
|
||||
let val = options[arg];
|
||||
const formatter = formatters[fmt];
|
||||
if ( typeof formatter === 'function' )
|
||||
val = formatter(val, locale, options);
|
||||
else if ( typeof val === 'string' )
|
||||
val = REPLACE.call(val, DOLLAR_REGEX, '$$');
|
||||
|
||||
return val;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Plural Nonsense
|
||||
// ============================================================================
|
||||
|
||||
const PLURAL_TYPE_TO_LANG = {
|
||||
arabic: ['ar'],
|
||||
chinese: ['fa', 'id', 'ja', 'ko', 'lo', 'ms', 'th', 'tr', 'zh'],
|
||||
german: ['da', 'de', 'en', 'es', 'es', 'fi', 'el', 'he', 'hu', 'it', 'nl', 'no', 'pt', 'sv'],
|
||||
french: ['fr', 'tl', 'pt-br'],
|
||||
russian: ['hr', 'ru', 'lt'],
|
||||
czech: ['cs', 'sk'],
|
||||
polish: ['pl'],
|
||||
icelandic: ['is']
|
||||
};
|
||||
|
||||
const PLURAL_LANG_TO_TYPE = {};
|
||||
|
||||
for(const type in PLURAL_TYPE_TO_LANG) // eslint-disable-line guard-for-in
|
||||
for(const lang of PLURAL_TYPE_TO_LANG[type])
|
||||
PLURAL_LANG_TO_TYPE[lang] = type;
|
||||
|
||||
const PLURAL_TYPES = {
|
||||
arabic: n => {
|
||||
if ( n < 3 ) return n;
|
||||
const n1 = n % 100;
|
||||
if ( n1 >= 3 && n1 <= 10 ) return 3;
|
||||
return n1 >= 11 ? 4 : 5;
|
||||
},
|
||||
chinese: () => 0,
|
||||
german: n => n !== 1 ? 1 : 0,
|
||||
french: n => n > 1 ? 1 : 0,
|
||||
russian: n => {
|
||||
const n1 = n % 10, n2 = n % 100;
|
||||
if ( n1 === 1 && n2 !== 11 ) return 0;
|
||||
return n1 >= 2 && n1 <= 4 && (n2 < 10 || n2 >= 20) ? 1 : 2;
|
||||
},
|
||||
czech: n => {
|
||||
if ( n === 1 ) return 0;
|
||||
return n >= 2 && n <= 4 ? 1 : 2;
|
||||
},
|
||||
polish: n => {
|
||||
if ( n === 1 ) return 0;
|
||||
const n1 = n % 10, n2 = n % 100;
|
||||
return n1 >= 2 && n1 <= 4 && (n2 < 10 || n2 >= 20) ? 1 : 2;
|
||||
},
|
||||
icelandic: n => n % 10 !== 1 || n % 100 === 11 ? 1 : 0
|
||||
};
|
||||
|
||||
|
||||
export function pluralTypeIndex(locale, n) {
|
||||
let type = PLURAL_LANG_TO_TYPE[locale];
|
||||
if ( ! type ) {
|
||||
const idx = locale.indexOf('-');
|
||||
if ( idx !== -1 )
|
||||
type = PLURAL_LANG_TO_TYPE[locale.slice(0, idx)]
|
||||
}
|
||||
|
||||
return PLURAL_TYPES[type || 'german'](n);
|
||||
}
|
8
src/jsconfig.json
Normal file
8
src/jsconfig.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"paths": {
|
||||
"utilities": ["./utilities"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,417 +0,0 @@
|
|||
/* Colors */
|
||||
@fg-color: #a49ab5;
|
||||
@bg-color: #101010;
|
||||
@link-color: #a68ed2;
|
||||
@remove-link-color: #fc3636;
|
||||
|
||||
@hollow-button-color: darken(@link-color, 5%); //#9d8dba;
|
||||
|
||||
@bright-bg: lighten(@bg-color, 15%);
|
||||
@bright-fg: lighten(@fg-color, 15%);
|
||||
|
||||
@nav-bg-color: #191919;
|
||||
|
||||
.ffz-dark {
|
||||
html&, body {
|
||||
background-color: @bg-color;
|
||||
color: @fg-color;
|
||||
}
|
||||
|
||||
a {
|
||||
color: @link-color;
|
||||
|
||||
&:hover, &:focus {
|
||||
color: lighten(@link-color, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
border-color: fade(white, 10%);
|
||||
}
|
||||
|
||||
|
||||
.sub-text { color: darken(@fg-color, 10%) }
|
||||
|
||||
|
||||
// Navigation
|
||||
.nav {
|
||||
background-color: @nav-bg-color;
|
||||
border-bottom-color: lighten(@nav-bg-color, 10%);
|
||||
}
|
||||
|
||||
.svg-logo_twitch, .clips-nav__logo { fill: white }
|
||||
|
||||
.nav-banner {
|
||||
background-color: lighten(@nav-bg-color, 10%);
|
||||
}
|
||||
|
||||
|
||||
// Chat
|
||||
|
||||
.clip-chat-message {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.view-clip__main--darker {
|
||||
background-color: darken(@nav-bg-color, 2%);
|
||||
}
|
||||
|
||||
.view-clip__smallTitle {
|
||||
color: @fg-color;
|
||||
|
||||
& > div {
|
||||
background-color: @nav-bg-color;
|
||||
border-bottom: lighten(@nav-bg-color, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
// Content Meta
|
||||
.nv-clip-content {
|
||||
background-color: @nav-bg-color;
|
||||
|
||||
.nv-clip-content__meta {
|
||||
background-color: @nav-bg-color;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.view-clip__meta-wrapper {
|
||||
background-color: @nav-bg-color;
|
||||
}
|
||||
|
||||
.view-clip__actions,
|
||||
.view-clip__bc {
|
||||
border-color: lighten(@nav-bg-color, 10%);
|
||||
}
|
||||
|
||||
.view-clip__title {
|
||||
color: @fg-color;
|
||||
}
|
||||
|
||||
.emote-picker {
|
||||
background-color: @nav-bg-color;
|
||||
}
|
||||
|
||||
.emote-picker__emotes .reaction__emote:hover {
|
||||
background-color: lighten(@nav-bg-color, 10%);
|
||||
}
|
||||
|
||||
|
||||
.yin-alert {
|
||||
background-color: lighten(@nav-bg-color, 10%);
|
||||
|
||||
.yin-link {
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background-color: fade(white, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.ct-editable-text {
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
&:after {
|
||||
background-color: fade(white, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ct-text-input {
|
||||
background-color: fade(white, 10%);
|
||||
color: white;
|
||||
border-color: fade(white, 20%);
|
||||
|
||||
&:focus {
|
||||
border-color: fade(white, 50%);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Clip Center?
|
||||
.ce-clip-center:not(.ce-clip-center--editing) .ce-clip-center__pre-control {
|
||||
background-color: @nav-bg-color;
|
||||
border-color: fade(white, 10%);
|
||||
}
|
||||
|
||||
.ce-filmstrip-fade--left {
|
||||
background-image: linear-gradient(90deg, @bg-color 13%, fade(@bg-color, 0%) 70%)
|
||||
}
|
||||
|
||||
.ce-filmstrip-fade--right {
|
||||
background-image: linear-gradient(-90deg, @bg-color 13%, fade(@bg-color, 0%) 70%)
|
||||
}
|
||||
|
||||
|
||||
// Buttons
|
||||
|
||||
.emote-picker__trigger {
|
||||
fill: @link-color;
|
||||
}
|
||||
|
||||
.button {
|
||||
&, &:hover, &:focus {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
figure img, figure svg {
|
||||
fill: @link-color;
|
||||
}
|
||||
}
|
||||
|
||||
.button--hollow {
|
||||
box-shadow: inset 0 0 0 1px fade(@link-color, 50%);
|
||||
color: @hollow-button-color;
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: fade(@link-color, 10%);
|
||||
color: @hollow-button-color;
|
||||
}
|
||||
}
|
||||
|
||||
.button--following {
|
||||
background-color: darken(@nav-bg-color, 2%);
|
||||
color: @fg-color;
|
||||
box-shadow: inset 0 0 0 1px lighten(@bg-color, 10%);
|
||||
}
|
||||
|
||||
.button--disabled {
|
||||
background-color: fade(black, 20%);
|
||||
color: fade(@fg-color, 25%);
|
||||
border-color: fade(@fg-color, 35%);
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: fade(black, 20%);
|
||||
color: fade(@fg-color, 25%);
|
||||
border-color: fade(@fg-color, 35%);
|
||||
}
|
||||
}
|
||||
|
||||
.button--hollow.button--dropmenu {
|
||||
&:after {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Balloons
|
||||
|
||||
.balloon {
|
||||
background-color: lighten(@bg-color, 10%);
|
||||
color: #ccc;
|
||||
box-shadow: 0 0 0 1px fade(white, 20%), 0 1px 1px fade(white, 5%);
|
||||
|
||||
&:after {
|
||||
background-color: lighten(@bg-color, 10%);
|
||||
}
|
||||
|
||||
&--fancy {
|
||||
color: lighten(@link-color, 5%);
|
||||
box-shadow: 0 0 0 1px fade(white, 10%);
|
||||
}
|
||||
|
||||
&--left:after { box-shadow: 1px -1px 0 fade(white, 20%) }
|
||||
&--right:after { box-shadow: -1px 1px 0 fade(white, 20%) }
|
||||
&--up:after { box-shadow: 1px 1px 0 fade(white, 20%) }
|
||||
&--down:after { box-shadow: -1px -1px 0 fade(white, 20%) }
|
||||
|
||||
&--cols .balloon__list ~ .balloon__list { box-shadow: -1px 0 0 #202021 }
|
||||
|
||||
&__stroke { border-bottom-color: fade(white, 10%) }
|
||||
|
||||
.balloon__link {
|
||||
color: @link-color !important;
|
||||
|
||||
&:hover {
|
||||
background-color: @link-color !important;
|
||||
color: @bg-color !important;
|
||||
}
|
||||
|
||||
&.request-removal-link,
|
||||
&.clip-remove-link {
|
||||
color: @remove-link-color !important;
|
||||
|
||||
&:hover {
|
||||
background-color: @remove-link-color !important;
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Card~
|
||||
.card__title {
|
||||
color: @fg-color;
|
||||
}
|
||||
|
||||
.more-clips {
|
||||
border-color: fade(white, 10%);
|
||||
|
||||
.card__layout:hover {
|
||||
background-color: lighten(@nav-bg-color, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Popup Card
|
||||
.popup-card {
|
||||
background-color: lighten(@bg-color, 10%);
|
||||
box-shadow:
|
||||
0 0 0 1px fade(white, 10%),
|
||||
0 1rem 2rem -.8rem fade(#6441A4, 25%);
|
||||
|
||||
.popup-card__header {
|
||||
color: lighten(@fg-color, 10%);
|
||||
}
|
||||
|
||||
.cf-footer {
|
||||
background-color: lighten(@bg-color, 5%);
|
||||
border-top: fade(white, 10%);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.form__input {
|
||||
background-color: fade(white, 5%);
|
||||
color: lighten(@fg-color, 10%);
|
||||
border-color: fade(white, 10%);
|
||||
}
|
||||
|
||||
.cf-textarea-input {
|
||||
background-color: fade(white, 5%);
|
||||
color: lighten(@fg-color, 10%);
|
||||
|
||||
textarea {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Modal Content
|
||||
.modal {
|
||||
&__content {
|
||||
background-color: @bg-color;
|
||||
box-shadow: 0 1px 1px fade(white, 10%);
|
||||
}
|
||||
|
||||
&-title {
|
||||
color: @bright-fg;
|
||||
}
|
||||
|
||||
&-body-text {
|
||||
color: @fg-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Table
|
||||
.table {
|
||||
background-color: @bg-color;
|
||||
box-shadow: 0 2px 4px 0 fade(white, 20%);
|
||||
|
||||
&__row {
|
||||
background-color: @bg-color;
|
||||
color: @fg-color;
|
||||
|
||||
&:nth-child(even) {
|
||||
background-color: lighten(@bg-color, 5%);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: lighten(@bg-color, 10%);
|
||||
}
|
||||
|
||||
&--no-hover:hover { background-color: @bg-color }
|
||||
}
|
||||
|
||||
&__header {
|
||||
background-color: lighten(@bg-color, 5%)
|
||||
}
|
||||
|
||||
&__cell {
|
||||
border-top-color: @bright-bg;
|
||||
border-bottom-color: @bright-bg;
|
||||
color: @fg-color;
|
||||
}
|
||||
|
||||
&__cell--header {
|
||||
border-bottom-color: @bright-bg;
|
||||
color: @bright-fg;
|
||||
}
|
||||
|
||||
&__zero-title {
|
||||
color: @bright-fg;
|
||||
}
|
||||
|
||||
&__zero-body {
|
||||
color: @fg-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Tabs
|
||||
.tabs {
|
||||
box-shadow: inset 0 -1px 0 #202021;
|
||||
|
||||
&__item {
|
||||
box-shadow: inset 0 -1px 0 #202021;
|
||||
|
||||
& > a { color: @link-color }
|
||||
|
||||
&:hover, &--active {
|
||||
box-shadow: inset 0 -1px 0 @link-color;
|
||||
|
||||
& > a {
|
||||
color: @bright-fg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.loading-spinner {
|
||||
border-color: fade(white, 18%);
|
||||
border-left-color: @bright-fg;
|
||||
}
|
||||
|
||||
.expand-button__border,
|
||||
.ldboard-head {
|
||||
border-bottom-color: #3e3d40;
|
||||
}
|
||||
|
||||
|
||||
// Previewer
|
||||
.ld-previewer__iframe, .previewer__pane {
|
||||
background-color: @bg-color;
|
||||
}
|
||||
|
||||
.previewer__close {
|
||||
fill: @link-color;
|
||||
|
||||
&:hover, &:focus {
|
||||
fill: lighten(@link-color, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Labels
|
||||
.label {
|
||||
color: @bright-fg;
|
||||
}
|
||||
|
||||
// Input
|
||||
select,
|
||||
.input--text {
|
||||
background-color: @bg-color;
|
||||
color: @bright-fg;
|
||||
border-color: @bright-bg;
|
||||
|
||||
&:focus {
|
||||
border-color: lighten(@bright-bg, 15%);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
.ffz-darken-clips {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ;
|
||||
|
||||
|
||||
FFZ.prototype.tr = function(s) {
|
||||
return s;
|
||||
}
|
717
src/main.js
717
src/main.js
|
@ -1,659 +1,114 @@
|
|||
// ----------------
|
||||
// The Constructor
|
||||
// ----------------
|
||||
'use strict';
|
||||
|
||||
var FFZ = window.FrankerFaceZ = function() {
|
||||
FFZ.instance = this;
|
||||
import Logger from 'utilities/logging';
|
||||
import Module from 'utilities/module';
|
||||
|
||||
// Logging
|
||||
this._log_data = [];
|
||||
this._apis = {};
|
||||
import {DEBUG} from 'utilities/constants';
|
||||
|
||||
// Data structures
|
||||
this.badges = {};
|
||||
this.users = {};
|
||||
this.rooms = {};
|
||||
import SettingsManager from './settings/index';
|
||||
import {TranslationManager} from './i18n';
|
||||
import SocketClient from './socket';
|
||||
import Site from 'site';
|
||||
import Vue from 'utilities/vue';
|
||||
|
||||
this.emoji_data = {};
|
||||
this.emoji_names = {};
|
||||
class FrankerFaceZ extends Module {
|
||||
constructor() {
|
||||
super();
|
||||
const start_time = performance.now(),
|
||||
VER = FrankerFaceZ.version_info;
|
||||
|
||||
this.emote_sets = {};
|
||||
this.global_sets = [];
|
||||
this.default_sets = [];
|
||||
this._last_emote_id = 0;
|
||||
FrankerFaceZ.instance = this;
|
||||
|
||||
this.name = 'frankerfacez';
|
||||
this.__state = 0;
|
||||
this.__modules.core = this;
|
||||
|
||||
this.log = new Logger(this);
|
||||
this.core_log = this.log.get('core');
|
||||
|
||||
this.log.info(`FrankerFaceZ v${VER} (build ${VER.build})`);
|
||||
|
||||
|
||||
// Error Logging
|
||||
var t = this;
|
||||
window.addEventListener('error', function(event) {
|
||||
if ( ! event.error )
|
||||
return;
|
||||
// ========================================================================
|
||||
// Core Systems
|
||||
// ========================================================================
|
||||
|
||||
//var has_stack = event.error && event.error.stack;
|
||||
t.error("Uncaught JavaScript Error", event.error);
|
||||
//t.log("JavaScript Error: " + event.message + " [" + event.filename + ":" + event.lineno + ":" + event.colno + "]", has_stack ? event.error.stack : undefined, false, has_stack);
|
||||
});
|
||||
this.inject('settings', SettingsManager);
|
||||
this.inject('i18n', TranslationManager);
|
||||
this.inject('socket', SocketClient);
|
||||
this.inject('site', Site);
|
||||
|
||||
// Initialize settings as early as possible.
|
||||
this.load_settings();
|
||||
|
||||
// Detect if we need to polyfill
|
||||
if ( ! window.fetch ) {
|
||||
this.log("Fetch is not detected. Requesting polyfill.");
|
||||
var script = document.createElement('script');
|
||||
script.id = 'ffz-polyfill';
|
||||
script.type = 'text/javascript';
|
||||
script.src = FrankerFaceZ.constants.SERVER + 'script/fetch.polyfill.' + (FrankerFaceZ.constants.DEBUG ? '' : 'min.') + 'js?_=' + Date.now();
|
||||
document.head.appendChild(script);
|
||||
|
||||
} else
|
||||
// Get things started.
|
||||
this.initialize();
|
||||
}
|
||||
this.register('vue', Vue);
|
||||
|
||||
|
||||
FFZ.get = function() { return FFZ.instance; }
|
||||
// ========================================================================
|
||||
// Startup
|
||||
// ========================================================================
|
||||
|
||||
// TODO: This should be in a module.
|
||||
FFZ.msg_commands = {};
|
||||
FFZ.channel_metadata = {};
|
||||
this.discoverModules();
|
||||
|
||||
this.enable().then(() => this.enableInitialModules()).then(() => {
|
||||
const duration = performance.now() - start_time;
|
||||
this.core_log.info(`Initialization complete in ${duration.toFixed(5)}ms.`);
|
||||
|
||||
// Version
|
||||
var VER = FFZ.version_info = {
|
||||
major: 3, minor: 5, revision: 536,
|
||||
toString: function() {
|
||||
return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || "");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Logging
|
||||
|
||||
var ua = navigator.userAgent,
|
||||
IS_WEBKIT = ua.indexOf('AppleWebKit/') !== -1 && ua.indexOf('Edge/') === -1,
|
||||
IS_FIREFOX = ua.indexOf('Firefox/') !== -1,
|
||||
|
||||
RED_COLOR = "color: red",
|
||||
FFZ_COLOR = "color:#755000; font-weight: bold",
|
||||
TXT_COLOR = "color:auto; font-weight: normal";
|
||||
|
||||
FFZ.prototype.log = function(msg, data, to_json, log_json) {
|
||||
if ( to_json )
|
||||
msg = msg + ' -- ' + JSON.stringify(data);
|
||||
|
||||
this._log_data.push(msg + ((!to_json && log_json) ? " -- " + JSON.stringify(data) : ""));
|
||||
|
||||
if ( data !== undefined && console.groupCollapsed && console.dir ) {
|
||||
if ( IS_WEBKIT )
|
||||
console.groupCollapsed("%cFFZ:%c " + msg, FFZ_COLOR, TXT_COLOR);
|
||||
else
|
||||
console.groupCollapsed("FFZ: " + msg);
|
||||
|
||||
if ( typeof data === "string" || IS_FIREFOX )
|
||||
console.log(data);
|
||||
else
|
||||
console.dir(data);
|
||||
|
||||
if ( IS_WEBKIT )
|
||||
console.groupEnd("%cFFZ:%c " + msg, FFZ_COLOR, TXT_COLOR);
|
||||
else
|
||||
console.groupEnd("FFZ: " + msg);
|
||||
|
||||
} else
|
||||
console.log("%cFFZ:%c " + msg, FFZ_COLOR, TXT_COLOR);
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.error = function(msg, error, to_json, log_json) {
|
||||
var data = error && error.stack || error;
|
||||
msg = "Error: " + msg + " [" + error + "]" + (to_json ? " -- " + JSON.stringify(data) : "");
|
||||
this._log_data.push(msg + ((!to_json && log_json) ? " -- " + JSON.stringify(data) : ""));
|
||||
|
||||
if ( data === undefined ) {
|
||||
var err = new Error();
|
||||
data = err.stack;
|
||||
}
|
||||
|
||||
if ( data !== undefined && console.groupCollapsed && console.dir ) {
|
||||
if ( IS_WEBKIT )
|
||||
console.groupCollapsed("%cFFZ " + msg, RED_COLOR);
|
||||
else
|
||||
console.groupCollapsed("FFZ " + msg);
|
||||
|
||||
if ( typeof data === "string" || IS_FIREFOX )
|
||||
console.log(data);
|
||||
else
|
||||
console.dir(data);
|
||||
|
||||
if ( IS_WEBKIT )
|
||||
console.groupEnd("%cFFZ " + msg, RED_COLOR);
|
||||
else
|
||||
console.groupEnd("FFZ " + msg);
|
||||
} else
|
||||
console.log("%cFFZ " + msg, RED_COLOR);
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.paste_logs = function() {
|
||||
var f = this,
|
||||
output = function(result) {
|
||||
f._pastebin(result).then(function(url) {
|
||||
f.log("Your FrankerFaceZ logs have been uploaded to: " + url);
|
||||
}).catch(function() {
|
||||
f.error("An error occured uploading the logs to a pastebin.");
|
||||
});
|
||||
}
|
||||
|
||||
this.get_debugging_info().then(function(data) {
|
||||
output(data);
|
||||
}).catch(function(err) {
|
||||
f.error("Error building debugging information.", err);
|
||||
output(f._log_data.join("\n"));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype._pastebin = function(data) {
|
||||
return new Promise(function(succeed, fail) {
|
||||
jQuery.ajax({url: "https://putco.de/", type: "PUT", data: data})
|
||||
.success(function(e) { succeed(e.trim() + ".log"); })
|
||||
.fail(function(e) { fail(null); });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// -------------------
|
||||
// User Data
|
||||
// -------------------
|
||||
|
||||
FFZ.prototype.get_location = function(force_reload) {
|
||||
var f = this;
|
||||
return new Promise(function(succeed, fail) {
|
||||
if ( ! force_reload && f.__location )
|
||||
succeed(f.__location);
|
||||
|
||||
if ( window.Twitch && Twitch.geo )
|
||||
Twitch.geo.then(function(data) {
|
||||
f.__location = {
|
||||
region: data.region,
|
||||
country: data.geo,
|
||||
lang: data.preferred_language
|
||||
};
|
||||
|
||||
succeed(f.__location);
|
||||
});
|
||||
else
|
||||
// TODO: Implement my own lookup.
|
||||
fail('no provider available');
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.get_user = function(force_reload) {
|
||||
if ( ! force_reload && this.__user && this.__user.chat_oauth_token )
|
||||
return this.__user;
|
||||
|
||||
var LC = FFZ.utils.ember_lookup('service:login'),
|
||||
user = LC ? LC.get('userData') : undefined;
|
||||
|
||||
if ( ! user && window.PP && PP.login )
|
||||
user = PP;
|
||||
|
||||
if ( user )
|
||||
this.__user = user;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
FFZ.prototype._editor_of = null;
|
||||
|
||||
FFZ.prototype.get_user_editor_of = function() {
|
||||
var f = this;
|
||||
return new Promise(function(succeed,fail) {
|
||||
var user = f.get_user();
|
||||
if ( ! user || ! user.login )
|
||||
return fail();
|
||||
|
||||
jQuery.get("/" + user.login + "/dashboard/permissions").done(function(data) {
|
||||
try {
|
||||
var dom = new DOMParser().parseFromString(data, 'text/html'),
|
||||
links = dom.querySelectorAll('#editable .label');
|
||||
|
||||
f._editor_of = _.map(links, function(e) {
|
||||
var href = e.getAttribute('href');
|
||||
return href && href.substr(href.lastIndexOf('/') + 1);
|
||||
});
|
||||
|
||||
succeed(f._editor_of);
|
||||
|
||||
} catch(err) {
|
||||
f.error("Failed to parse User Editor State", err);
|
||||
fail();
|
||||
}
|
||||
|
||||
}).fail(function(e) {
|
||||
f.error("Failed to load User Editor State", e);
|
||||
fail();
|
||||
}).catch(err => {
|
||||
this.core_log.error('An error occurred during initialization.', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// -------------------
|
||||
// Import Everything!
|
||||
// -------------------
|
||||
|
||||
// Import these first to set up data structures
|
||||
require('./localization');
|
||||
require('./ui/menu');
|
||||
require('./settings');
|
||||
require('./socket');
|
||||
|
||||
require('./colors');
|
||||
require('./emoticons');
|
||||
require('./badges');
|
||||
require('./tokenize');
|
||||
//require('./filtering');
|
||||
|
||||
require('./ember/wrapper');
|
||||
require('./ember/router');
|
||||
require('./ember/bits');
|
||||
require('./ember/channel');
|
||||
require('./ember/player');
|
||||
require('./ember/room');
|
||||
require('./ember/vod-chat');
|
||||
require('./ember/layout');
|
||||
require('./ember/line');
|
||||
require('./ember/chatview');
|
||||
require('./ember/conversations');
|
||||
require('./ember/viewers');
|
||||
require('./ember/moderation-card');
|
||||
require('./ember/chat-input');
|
||||
//require('./ember/teams');
|
||||
require('./ember/directory');
|
||||
require('./ember/following');
|
||||
require('./ember/feed-card');
|
||||
require('./ember/sidebar');
|
||||
require('./ember/dashboard');
|
||||
require('./ember/commerce');
|
||||
|
||||
require('./debug');
|
||||
|
||||
require('./ext/betterttv');
|
||||
require('./ext/emote_menu');
|
||||
|
||||
require('./featurefriday');
|
||||
|
||||
require('./ui/channel_stats');
|
||||
require('./ui/logviewer');
|
||||
//require('./ui/chatpane');
|
||||
require('./ui/popups');
|
||||
require('./ui/styles');
|
||||
require('./ui/dark');
|
||||
require('./ui/tooltips');
|
||||
require('./ui/notifications');
|
||||
//require('./ui/viewer_count');
|
||||
require('./ui/sub_count');
|
||||
//require('./ui/dash_stats');
|
||||
require('./ui/dash_feed');
|
||||
|
||||
require('./ui/menu_button');
|
||||
require('./ui/following');
|
||||
require('./ui/following-count');
|
||||
require('./ui/races');
|
||||
require('./ui/my_emotes');
|
||||
require('./ui/about_page');
|
||||
require('./ui/schedule');
|
||||
|
||||
require('./commands');
|
||||
require('./ext/api');
|
||||
require('./ext/warpworld');
|
||||
|
||||
|
||||
// ---------------
|
||||
// Initialization
|
||||
// ---------------
|
||||
|
||||
FFZ.prototype.initialized = false;
|
||||
|
||||
FFZ.prototype.initialize = function(increment, delay) {
|
||||
// Make sure that FrankerFaceZ doesn't start setting itself up until the
|
||||
// Twitch ember application is ready.
|
||||
|
||||
// Pages we don't want to interact with at all.
|
||||
if ( ['passport.twitch.tv', 'im.twitch.tv', 'api.twitch.tv', 'chatdepot.twitch.tv', 'spade.twitch.tv'].indexOf(location.hostname) !== -1 || /^\/pr\//.test(location.pathname) || /^\/user\/two_factor/.test(location.pathname) ) {
|
||||
this.log("Found banned sub-domain. Not initializing.");
|
||||
window.FrankerFaceZ = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for the player
|
||||
if ( location.hostname === 'player.twitch.tv' )
|
||||
return this.init_player(delay);
|
||||
|
||||
// Clips~
|
||||
if ( location.hostname === 'clips.twitch.tv' )
|
||||
return this.init_clips(delay);
|
||||
|
||||
// Check for special non-ember pages.
|
||||
if ( /^\/(?:team\/|user\/|p\/|m\/|settings\/(?:prime|turbo|channel|security|connections)|messages?\/)/.test(location.pathname) )
|
||||
return this.init_normal(delay);
|
||||
|
||||
// Check for the dashboard.
|
||||
/*if ( window.PP && /\/[^\/]+\/dashboard/.test(location.pathname) && !/bookmarks$/.test(location.pathname) )
|
||||
return this.init_dashboard(delay);*/
|
||||
|
||||
var loaded = FFZ.utils.ember_resolve('model:room');
|
||||
if ( !loaded ) {
|
||||
increment = increment || 10;
|
||||
if ( delay >= 60000 )
|
||||
this.log("Twitch application not detected in \"" + location.toString() + "\". Aborting.");
|
||||
else
|
||||
setTimeout(this.initialize.bind(this, increment, (delay||0) + increment),
|
||||
increment);
|
||||
return;
|
||||
static get() {
|
||||
return FrankerFaceZ.instance;
|
||||
}
|
||||
|
||||
this.init_ember(delay);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Modules
|
||||
// ========================================================================
|
||||
|
||||
FFZ.prototype.init_clips = function(delay) {
|
||||
var start = (window.performance && performance.now) ? performance.now() : Date.now();
|
||||
this.log("Found Twitch Clips after " + (delay||0) + " ms at: " + location);
|
||||
this.log("Initializing FrankerFaceZ version " + FFZ.version_info);
|
||||
discoverModules() {
|
||||
const ctx = require.context('src/modules', true, /(?:^(?:\.\/)?[^/]+|index)\.js$/),
|
||||
modules = this.populate(ctx, this.core_log);
|
||||
|
||||
this.is_dashboard = false;
|
||||
this.embed_in_dash = false;
|
||||
this.is_clips = true;
|
||||
try {
|
||||
this.embed_in_clips = window.top !== window && window.top.location.hostname === 'clips.twitch.tv';
|
||||
} catch(err) { this.embed_in_clips = false; }
|
||||
|
||||
this.setup_dark();
|
||||
this.setup_css();
|
||||
|
||||
this.add_clips_darken_button();
|
||||
|
||||
this.initialized = true;
|
||||
this.api_trigger('initialized');
|
||||
|
||||
var end = (window.performance && performance.now) ? performance.now() : Date.now(),
|
||||
duration = end - start;
|
||||
|
||||
this.log("Initialization complete in " + duration + "ms");
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.init_player = function(delay) {
|
||||
var start = (window.performance && performance.now) ? performance.now() : Date.now();
|
||||
this.log("Found Twitch Player after " + (delay||0) + " ms at: " + location);
|
||||
this.log("Initializing FrankerFaceZ version " + FFZ.version_info);
|
||||
|
||||
this.is_dashboard = false;
|
||||
try {
|
||||
this.embed_in_dash = window.top !== window && /\/[^\/]+\/dashboard/.test(window.top.location.pathname) && !/bookmarks$/.test(window.top.location.pathname);
|
||||
} catch(err) { this.embed_in_dash = false; }
|
||||
|
||||
// Literally only make it dark.
|
||||
this.setup_dark();
|
||||
this.setup_css();
|
||||
this.setup_player();
|
||||
|
||||
this.initialized = true;
|
||||
this.api_trigger('initialized');
|
||||
|
||||
var end = (window.performance && performance.now) ? performance.now() : Date.now(),
|
||||
duration = end - start;
|
||||
|
||||
this.log("Initialization complete in " + duration + "ms");
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.init_normal = function(delay, no_socket) {
|
||||
var start = (window.performance && performance.now) ? performance.now() : Date.now();
|
||||
this.log("Found non-Ember Twitch after " + (delay||0) + " ms at: " + location);
|
||||
this.log("Initializing FrankerFaceZ version " + FFZ.version_info);
|
||||
|
||||
this.is_dashboard = false;
|
||||
try {
|
||||
this.embed_in_dash = window.top !== window && /\/[^\/]+\/dashboard/.test(window.top.location.pathname) && !/bookmarks$/.test(window.top.location.pathname);
|
||||
} catch(err) { this.embed_in_dash = false; }
|
||||
|
||||
// Initialize all the modules.
|
||||
this.setup_ember_wrapper();
|
||||
|
||||
// Start this early, for quick loading.
|
||||
this.setup_dark();
|
||||
this.setup_css();
|
||||
this.setup_popups();
|
||||
|
||||
this.setup_following_link();
|
||||
|
||||
if ( ! no_socket ) {
|
||||
this.setup_time();
|
||||
this.ws_create();
|
||||
this.core_log.info(`Loaded descriptions of ${Object.keys(modules).length} modules.`);
|
||||
}
|
||||
|
||||
this.setup_colors();
|
||||
this.setup_emoticons();
|
||||
this.setup_badges();
|
||||
this.setup_sidebar();
|
||||
|
||||
this.setup_notifications();
|
||||
this.setup_following_count(false);
|
||||
this.setup_menu();
|
||||
|
||||
this.finalize_ember_wrapper();
|
||||
this.setup_message_event();
|
||||
this.fix_tooltips();
|
||||
this.find_bttv(10);
|
||||
|
||||
this.initialized = true;
|
||||
this.api_trigger('initialized');
|
||||
|
||||
var end = (window.performance && performance.now) ? performance.now() : Date.now(),
|
||||
duration = end - start;
|
||||
|
||||
this.log("Initialization complete in " + duration + "ms");
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.is_dashboard = false;
|
||||
|
||||
FFZ.prototype.init_dashboard = function(delay) {
|
||||
var start = (window.performance && performance.now) ? performance.now() : Date.now();
|
||||
this.log("Found Twitch Dashboard after " + (delay||0) + " ms at: " + location);
|
||||
this.log("Initializing FrankerFaceZ version " + FFZ.version_info);
|
||||
|
||||
var match = location.pathname.match(/\/([^\/]+)/);
|
||||
this.dashboard_channel = match && match[1] || undefined;
|
||||
|
||||
this.is_dashboard = true;
|
||||
this.embed_in_dash = false;
|
||||
|
||||
// Initialize all the modules.
|
||||
this.setup_ember_wrapper();
|
||||
|
||||
// Start this early, for quick loading.
|
||||
this.setup_dark();
|
||||
this.setup_css();
|
||||
this.setup_popups();
|
||||
|
||||
this.setup_following_link();
|
||||
|
||||
this.setup_time();
|
||||
this.ws_create();
|
||||
|
||||
this.setup_colors();
|
||||
this.setup_emoticons();
|
||||
this.setup_badges();
|
||||
|
||||
this.setup_tokenization();
|
||||
this.setup_notifications();
|
||||
this.setup_following_count(false);
|
||||
this.setup_menu();
|
||||
//this.setup_dash_stats();
|
||||
this.setup_dash_feed();
|
||||
|
||||
this.finalize_ember_wrapper();
|
||||
|
||||
this._update_subscribers();
|
||||
|
||||
// Set up the FFZ message passer.
|
||||
this.setup_message_event();
|
||||
|
||||
this.cache_command_aliases();
|
||||
this.fix_tooltips();
|
||||
this.find_bttv(10);
|
||||
|
||||
this.initialized = true;
|
||||
this.api_trigger('initialized');
|
||||
|
||||
var end = (window.performance && performance.now) ? performance.now() : Date.now(),
|
||||
duration = end - start;
|
||||
|
||||
this.log("Initialization complete in " + duration + "ms");
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.init_ember = function(delay) {
|
||||
var start = (window.performance && performance.now) ? performance.now() : Date.now();
|
||||
this.log("Found Twitch application after " + (delay||0) + " ms at: " + location);
|
||||
this.log("Initializing FrankerFaceZ version " + FFZ.version_info);
|
||||
|
||||
this.is_dashboard = false;
|
||||
|
||||
try {
|
||||
this.embed_in_dash = window.top !== window && /\/[^\/]+\/dashboard/.test(window.top.location.pathname) && !/bookmarks$/.test(window.top.location.pathname);
|
||||
} catch(err) { this.embed_in_dash = false; }
|
||||
|
||||
// Is debug mode enabled? Scratch that, everyone gets error handlers!
|
||||
if ( true ) { //this.settings.developer_mode ) {
|
||||
// Set up an error listener for RSVP.
|
||||
var f = this;
|
||||
if ( Ember.RSVP && Ember.RSVP.on )
|
||||
Ember.RSVP.on('error', function(error) {
|
||||
// We want to ignore errors that are just 4xx HTTP responses.
|
||||
if ( error && error.responseJSON && typeof error.responseJSON.status === "number" && error.responseJSON.status >= 400 )
|
||||
return;
|
||||
|
||||
f.error("There was an error within an Ember RSVP.", error);
|
||||
});
|
||||
|
||||
Ember.onerror = function(error) {
|
||||
// We want to ignore errors that are just 4xx HTTP responses.
|
||||
if ( error && error.responseJSON && typeof error.responseJSON.status === "number" && error.responseJSON.status >= 400 )
|
||||
return;
|
||||
|
||||
f.error("There was an unknown error within Ember.", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Set up all the everything.
|
||||
this.setup_ember_wrapper();
|
||||
|
||||
// Start this early, for quick loading.
|
||||
this.setup_dark();
|
||||
this.setup_css();
|
||||
this.setup_popups();
|
||||
|
||||
this.setup_time();
|
||||
this.ws_create();
|
||||
|
||||
this.setup_emoticons();
|
||||
this.setup_badges();
|
||||
|
||||
this.setup_router();
|
||||
this.setup_colors();
|
||||
this.setup_tokenization();
|
||||
//this.setup_filtering();
|
||||
|
||||
this.setup_player();
|
||||
this.setup_channel();
|
||||
this.setup_room();
|
||||
this.setup_vod_chat();
|
||||
this.setup_line();
|
||||
this.setup_bits();
|
||||
this.setup_layout();
|
||||
this.setup_chatview();
|
||||
this.setup_conversations();
|
||||
this.setup_viewers();
|
||||
this.setup_mod_card();
|
||||
this.setup_chat_input();
|
||||
this.setup_directory();
|
||||
this.setup_profile_following();
|
||||
this.setup_feed_cards();
|
||||
this.setup_sidebar();
|
||||
this.setup_dashboard();
|
||||
this.setup_commerce();
|
||||
|
||||
//this.setup_teams();
|
||||
|
||||
this.setup_notifications();
|
||||
this.setup_menu();
|
||||
this.setup_my_emotes();
|
||||
this.setup_following();
|
||||
this.setup_following_count(true);
|
||||
this.setup_races();
|
||||
|
||||
|
||||
// Do all Ember modification before this point.
|
||||
this.finalize_ember_wrapper();
|
||||
|
||||
this.cache_command_aliases();
|
||||
this.fix_tooltips();
|
||||
this.fix_scroll();
|
||||
|
||||
setTimeout(this.connect_extra_chat.bind(this), 250);
|
||||
|
||||
this.setup_message_event();
|
||||
this.find_bttv(10);
|
||||
this.find_emote_menu(10);
|
||||
|
||||
setTimeout(this.check_badware.bind(this), 10000);
|
||||
|
||||
//this.check_news();
|
||||
this.check_ff();
|
||||
this.refresh_chat();
|
||||
|
||||
this.initialized = true;
|
||||
this.api_trigger('initialized');
|
||||
|
||||
var end = (window.performance && performance.now) ? performance.now() : Date.now(),
|
||||
duration = end - start;
|
||||
|
||||
this.log("Initialization complete in " + duration + "ms");
|
||||
}
|
||||
|
||||
|
||||
// ------------------------
|
||||
// Dashboard Message Event
|
||||
// ------------------------
|
||||
|
||||
FFZ.prototype.setup_message_event = function() {
|
||||
this.log("Listening for Window Messages.");
|
||||
window.addEventListener("message", this._on_window_message.bind(this), false);
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype._on_window_message = function(e) {
|
||||
var msg = e.data;
|
||||
if ( typeof msg === "string" )
|
||||
try {
|
||||
msg = JSON.parse(msg);
|
||||
} catch(err) {
|
||||
// Not JSON? We don't care.
|
||||
return;
|
||||
async enableInitialModules() {
|
||||
const promises = [];
|
||||
/* eslint guard-for-in: off */
|
||||
for(const key in this.__modules) {
|
||||
const module = this.__modules[key];
|
||||
if ( module instanceof Module && module.should_enable )
|
||||
promises.push(module.enable());
|
||||
}
|
||||
|
||||
if ( ! msg || ! msg.from_ffz )
|
||||
return;
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
var handler = FFZ.msg_commands[msg.command];
|
||||
if ( handler )
|
||||
handler.call(this, msg.data);
|
||||
else
|
||||
this.log("Invalid Message: " + msg.command, msg.data, false, true);
|
||||
}
|
||||
|
||||
/* eslint class-methods-use-this: off */
|
||||
api() {
|
||||
throw new Error('Not Implemented');
|
||||
}
|
||||
}
|
||||
|
||||
FrankerFaceZ.Logger = Logger;
|
||||
|
||||
const VER = FrankerFaceZ.version_info = {
|
||||
major: 4, minor: 0, revision: 0, extra: '-beta1',
|
||||
build: __webpack_hash__,
|
||||
toString: () =>
|
||||
`${VER.major}.${VER.minor}.${VER.revision}${VER.extra || ''}${DEBUG ? '-dev' : ''}`
|
||||
}
|
||||
|
||||
|
||||
FrankerFaceZ.utilities = {
|
||||
dom: require('utilities/dom'),
|
||||
color: require('utilities/color'),
|
||||
events: require('utilities/events')
|
||||
}
|
||||
|
||||
|
||||
|
||||
window.FrankerFaceZ = FrankerFaceZ;
|
||||
window.ffz = new FrankerFaceZ();
|
27
src/modules/chat/badges.js
Normal file
27
src/modules/chat/badges.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Badge Handling
|
||||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
|
||||
export const CSS_BADGES = {
|
||||
staff: { 1: { color: '#200f33', use_svg: true } },
|
||||
admin: { 1: { color: '#faaf19', use_svg: true } },
|
||||
global_mod: { 1: { color: '#0c6f20', use_svg: true } },
|
||||
broadcaster: { 1: { color: '#e71818', use_svg: true } },
|
||||
moderator: { 1: { color: '#34ae0a', use_svg: true } },
|
||||
twitchbot: { 1: { color: '#34ae0a' } },
|
||||
partner: { 1: { color: 'transparent', has_trans: true, trans_color: '#6441a5' } },
|
||||
|
||||
turbo: { 1: { color: '#6441a5', use_svg: true } },
|
||||
premium: { 1: { color: '#009cdc' } },
|
||||
|
||||
subscriber: { 0: { color: '#6441a4' }, 1: { color: '#6441a4' }},
|
||||
}
|
||||
|
||||
|
||||
export default class Badges extends Module {
|
||||
|
||||
}
|
325
src/modules/chat/emotes.js
Normal file
325
src/modules/chat/emotes.js
Normal file
|
@ -0,0 +1,325 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Emote Handling and Default Provider
|
||||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
import {ManagedStyle} from 'utilities/dom';
|
||||
import {has, timeout} from 'utilities/object';
|
||||
import {API_SERVER} from 'utilities/constants';
|
||||
|
||||
|
||||
const MODIFIERS = {
|
||||
59847: {
|
||||
modifier_offset: '0 15px 15px 0',
|
||||
modifier: true
|
||||
},
|
||||
|
||||
70852: {
|
||||
modifier: true,
|
||||
modifier_offset: '0 5px 20px 0',
|
||||
extra_width: 5,
|
||||
shrink_to_fit: true
|
||||
},
|
||||
|
||||
70854: {
|
||||
modifier: true,
|
||||
modifier_offset: '30px 0 0'
|
||||
},
|
||||
|
||||
147049: {
|
||||
modifier: true,
|
||||
modifier_offset: '4px 1px 0 3px'
|
||||
},
|
||||
|
||||
147011: {
|
||||
modifier: true,
|
||||
modifier_offset: '0'
|
||||
},
|
||||
|
||||
70864: {
|
||||
modifier: true,
|
||||
modifier_offset: '0'
|
||||
},
|
||||
|
||||
147038: {
|
||||
modifier: true,
|
||||
modifier_offset: '0'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
export default class Emotes extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.inject('socket');
|
||||
|
||||
this.twitch_inventory_sets = [];
|
||||
this.__twitch_emote_to_set = new Map;
|
||||
this.__twitch_set_to_channel = new Map;
|
||||
|
||||
this.default_sets = new Set;
|
||||
this.global_sets = new Set;
|
||||
|
||||
this.emote_sets = {};
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
this.style = new ManagedStyle('emotes');
|
||||
|
||||
if ( Object.keys(this.emote_sets).length ) {
|
||||
this.log.info('Generating CSS for existing emote sets.');
|
||||
for(const set_id in this.emote_sets)
|
||||
if ( has(this.emote_sets, set_id) ) {
|
||||
const emote_set = this.emote_sets[set_id];
|
||||
if ( emote_set && emote_set.pending_css ) {
|
||||
this.style.set(`es--${set_id}`, emote_set.pending_css + (emote_set.css || ''));
|
||||
emote_set.pending_css = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.load_global_sets();
|
||||
this.load_emoji_data();
|
||||
this.refresh_twitch_inventory();
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Access
|
||||
// ========================================================================
|
||||
|
||||
getSetIDs(user_id, user_login, room_id, room_login) {
|
||||
const room = this.parent.getRoom(room_id, room_login, true),
|
||||
user = this.parent.getUser(user_id, user_login, true);
|
||||
|
||||
return (user ? user.emote_sets : []).concat(
|
||||
room ? room.emote_sets : [],
|
||||
Array.from(this.default_sets)
|
||||
);
|
||||
}
|
||||
|
||||
getSets(user_id, user_login, room_id, room_login) {
|
||||
return this.getSetIDs(user_id, user_login, room_id, room_login)
|
||||
.map(set_id => this.emote_sets[set_id]);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// FFZ Emote Sets
|
||||
// ========================================================================
|
||||
|
||||
async load_global_sets(tries = 0) {
|
||||
let response, data;
|
||||
try {
|
||||
response = await fetch(`${API_SERVER}/v1/set/global`)
|
||||
} catch(err) {
|
||||
tries++;
|
||||
if ( tries < 10 )
|
||||
return setTimeout(() => this.load_global_sets(tries), 500 * tries);
|
||||
|
||||
this.log.error('Error loading global emote sets.', err);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! response.ok )
|
||||
return false;
|
||||
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch(err) {
|
||||
this.log.error('Error parsing global emote data.', err);
|
||||
return false;
|
||||
}
|
||||
|
||||
const sets = data.sets || {};
|
||||
|
||||
for(const set_id of data.default_sets)
|
||||
this.default_sets.add(set_id);
|
||||
|
||||
for(const set_id in sets)
|
||||
if ( has(sets, set_id) ) {
|
||||
this.global_sets.add(set_id);
|
||||
this.load_set_data(set_id, sets[set_id]);
|
||||
}
|
||||
|
||||
if ( data.users )
|
||||
this.load_set_users(data.users);
|
||||
}
|
||||
|
||||
|
||||
load_set_users(data) {
|
||||
for(const set_id in data)
|
||||
if ( has(data, set_id) ) {
|
||||
const emote_set = this.emote_sets[set_id],
|
||||
users = data[set_id];
|
||||
|
||||
for(const login of users) {
|
||||
const user = this.parent.getUser(undefined, login),
|
||||
sets = user.emote_sets;
|
||||
|
||||
if ( sets.indexOf(set_id) === -1 )
|
||||
sets.push(set_id);
|
||||
}
|
||||
|
||||
this.log.info(`Added "${emote_set ? emote_set.title : set_id}" emote set to ${users.length} users.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
load_set_data(set_id, data) {
|
||||
const old_set = this.emote_sets[set_id];
|
||||
if ( ! data ) {
|
||||
if ( old_set )
|
||||
this.emote_sets[set_id] = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.emote_sets[set_id] = data;
|
||||
|
||||
data.users = old_set ? old_set.users : 0;
|
||||
|
||||
let count = 0;
|
||||
const ems = data.emotes || data.emoticons,
|
||||
new_ems = data.emotes = {},
|
||||
css = [];
|
||||
|
||||
data.emoticons = undefined;
|
||||
|
||||
for(const emote of ems) {
|
||||
emote.set_id = set_id;
|
||||
emote.srcSet = `${emote.urls[1]} 1x`;
|
||||
if ( emote.urls[2] )
|
||||
emote.srcSet += `, ${emote.urls[2]} 2x`;
|
||||
if ( emote.urls[4] )
|
||||
emote.srcSet += `, ${emote.urls[4]} 4x`;
|
||||
|
||||
emote.token = {
|
||||
type: 'emote',
|
||||
id: emote.id,
|
||||
set: set_id,
|
||||
provider: 'ffz',
|
||||
src: emote.urls[1],
|
||||
srcSet: emote.srcSet,
|
||||
text: emote.hidden ? '???' : emote.name,
|
||||
length: emote.name.length
|
||||
};
|
||||
|
||||
if ( has(MODIFIERS, emote.id) )
|
||||
Object.assign(emote, MODIFIERS[emote.id]);
|
||||
|
||||
const emote_css = this.generateEmoteCSS(emote);
|
||||
if ( emote_css )
|
||||
css.push(emote_css);
|
||||
|
||||
count++;
|
||||
new_ems[emote.id] = emote;
|
||||
}
|
||||
|
||||
data.count = count;
|
||||
|
||||
if ( this.style && (css.length || data.css) )
|
||||
this.style.set(`es--${set_id}`, css.join('') + (data.css || ''));
|
||||
else if ( css.length )
|
||||
data.pending_css = css.join('');
|
||||
|
||||
this.log.info(`Updated emotes for set #${set_id}: ${data.title}`);
|
||||
this.emit(':loaded', set_id, data);
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Emote CSS
|
||||
// ========================================================================
|
||||
|
||||
generateEmoteCSS(emote) { // eslint-disable-line class-methods-use-this
|
||||
if ( ! emote.margins && ( ! emote.modifier || ( ! emote.modifier_offset && ! emote.extra_width && ! emote.shrink_to_fit ) ) && ! emote.css )
|
||||
return '';
|
||||
|
||||
let output = '';
|
||||
if ( emote.modifier && (emote.modifier_offset || emote.margins || emote.extra_width || emote.shrink_to_fit) ) {
|
||||
let margins = emote.modifier_offset || emote.margins || '0';
|
||||
margins = margins.split(/\s+/).map(x => parseInt(x, 10));
|
||||
if ( margins.length === 3 )
|
||||
margins.push(margins[1]);
|
||||
|
||||
const l = margins.length,
|
||||
m_top = margins[0 % l],
|
||||
m_right = margins[1 % l],
|
||||
m_bottom = margins[2 % l],
|
||||
m_left = margins[3 % l];
|
||||
|
||||
output = `.modified-emote span .ffz-emote[data-id="${emote.id}"] {
|
||||
padding: ${m_top}px ${m_right}px ${m_bottom}px ${m_left}px;
|
||||
${emote.shrink_to_fit ? `max-width: calc(100% - ${40 - m_left - m_right - (emote.extra_width||0)}px);` : ''}
|
||||
margin: 0 !important;
|
||||
}`;
|
||||
}
|
||||
|
||||
return `${output}.ffz-emote[data-id="${emote.id}"] {
|
||||
${(emote.margins && ! emote.modifier) ? `margin: ${emote.margins} !important;` : ''}
|
||||
${emote.css||''}
|
||||
}`;
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Emoji
|
||||
// ========================================================================
|
||||
|
||||
load_emoji_data() {
|
||||
this.log.debug('Unimplemented: load_emoji_data');
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Twitch Data Lookup
|
||||
// ========================================================================
|
||||
|
||||
refresh_twitch_inventory() {
|
||||
this.log.debug('Unimplemented: refresh_twitch_inventory');
|
||||
}
|
||||
|
||||
|
||||
twitch_emote_to_set(emote_id, callback) {
|
||||
const tes = this.__twitch_emote_to_set;
|
||||
|
||||
if ( isNaN(emote_id) || ! isFinite(emote_id) )
|
||||
return null;
|
||||
|
||||
if ( tes.has(emote_id) )
|
||||
return tes.get(emote_id);
|
||||
|
||||
tes.set(emote_id, null);
|
||||
timeout(this.socket.call('get_emote', emote_id), 1000).then(data => {
|
||||
const set_id = data['s_id'];
|
||||
tes.set(emote_id, set_id);
|
||||
this.__twitch_set_to_channel.set(set_id, data);
|
||||
|
||||
if ( callback )
|
||||
callback(data['s_id']);
|
||||
|
||||
}).catch(() => tes.delete(emote_id));
|
||||
}
|
||||
|
||||
|
||||
twitch_set_to_channel(set_id, callback) {
|
||||
const tes = this.__twitch_set_to_channel;
|
||||
if ( isNaN(set_id) || ! isFinite(set_id) )
|
||||
return null;
|
||||
|
||||
if ( tes.has(set_id) )
|
||||
return tes.get(set_id);
|
||||
|
||||
tes.set(set_id, null);
|
||||
timeout(this.socket.call('get_emote_set', set_id), 1000).then(data => {
|
||||
tes.set(set_id, data);
|
||||
if ( callback )
|
||||
callback(data);
|
||||
|
||||
}).catch(() => tes.delete(set_id));
|
||||
}
|
||||
}
|
513
src/modules/chat/index.js
Normal file
513
src/modules/chat/index.js
Normal file
|
@ -0,0 +1,513 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Chat
|
||||
// ============================================================================
|
||||
|
||||
import {IS_WEBKIT} from 'utilities/constants';
|
||||
const WEBKIT = IS_WEBKIT ? '-webkit-' : '';
|
||||
|
||||
import Module from 'utilities/module';
|
||||
import {createElement, ManagedStyle} from 'utilities/dom';
|
||||
import {timeout, has} from 'utilities/object';
|
||||
|
||||
import Badges from './badges';
|
||||
import Emotes from './emotes';
|
||||
|
||||
import Room from './room';
|
||||
import * as TOKENIZERS from './tokenizers';
|
||||
|
||||
|
||||
export default class Chat extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.should_enable = true;
|
||||
|
||||
this.inject('settings');
|
||||
this.inject('i18n');
|
||||
this.inject('tooltips');
|
||||
this.inject('socket');
|
||||
|
||||
this.inject(Badges);
|
||||
this.inject(Emotes);
|
||||
|
||||
this._link_info = {};
|
||||
|
||||
this.style = new ManagedStyle;
|
||||
|
||||
this.context = this.settings.context({});
|
||||
|
||||
this.rooms = {};
|
||||
this.users = {};
|
||||
|
||||
this.room_ids = {};
|
||||
this.user_ids = {};
|
||||
|
||||
this.tokenizers = {};
|
||||
this.__tokenizers = [];
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Settings
|
||||
// ========================================================================
|
||||
|
||||
this.settings.add('tooltip.images', {
|
||||
default: true,
|
||||
ui: {
|
||||
path: 'Chat > Tooltips >> General @{"sort": -1}',
|
||||
title: 'Display images in tooltips.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('tooltip.badge-images', {
|
||||
default: true,
|
||||
requires: ['tooltip.images'],
|
||||
process(ctx, val) {
|
||||
return ctx.get('tooltip.images') ? val : false
|
||||
},
|
||||
|
||||
ui: {
|
||||
path: 'Chat > Tooltips >> Badges',
|
||||
title: 'Display large images of badges.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('tooltip.emote-sources', {
|
||||
default: true,
|
||||
ui: {
|
||||
path: 'Chat > Tooltips >> Emotes',
|
||||
title: 'Display known sources.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('tooltip.emote-images', {
|
||||
default: true,
|
||||
requires: ['tooltip.images'],
|
||||
process(ctx, val) {
|
||||
return ctx.get('tooltip.images') ? val : false
|
||||
},
|
||||
|
||||
ui: {
|
||||
path: 'Chat > Tooltips >> Emotes',
|
||||
title: 'Display large images of emotes.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('tooltip.rich-links', {
|
||||
default: true,
|
||||
ui: {
|
||||
sort: -1,
|
||||
path: 'Chat > Tooltips >> Links',
|
||||
title: 'Display rich tooltips for links.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('tooltip.link-images', {
|
||||
default: true,
|
||||
requires: ['tooltip.images'],
|
||||
process(ctx, val) {
|
||||
return ctx.get('tooltip.images') ? val : false
|
||||
},
|
||||
|
||||
ui: {
|
||||
path: 'Chat > Tooltips >> Links',
|
||||
title: 'Display images for links.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('tooltip.link-nsfw-images', {
|
||||
default: false,
|
||||
ui: {
|
||||
path: 'Chat > Tooltips >> Links',
|
||||
title: 'Display potentially NSFW images.',
|
||||
description: 'When enabled, FrankerFaceZ will include images that are tagged as unsafe or that are not rated.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.settings.add('chat.adjustment-mode', {
|
||||
default: 1,
|
||||
ui: {
|
||||
path: 'Chat > Appearance >> Colors',
|
||||
title: 'Adjustment',
|
||||
description: 'Alter user colors to ensure that they remain readable.',
|
||||
|
||||
component: 'setting-select-box',
|
||||
|
||||
data: [
|
||||
{value: -1, title: 'No Color'},
|
||||
{value: 0, title: 'Unchanged'},
|
||||
{value: 1, title: 'HSL Luma'},
|
||||
{value: 2, title: 'Luv Luma'},
|
||||
{value: 3, title: 'HSL Loop (BTTV-Like)'}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.adjustment-contrast', {
|
||||
default: 4.5,
|
||||
ui: {
|
||||
path: 'Chat > Appearance >> Colors',
|
||||
title: 'Minimum Contrast',
|
||||
description: 'Set the minimum contrast ratio used by Luma adjustments when determining readability.',
|
||||
|
||||
component: 'setting-text-box',
|
||||
|
||||
process(val) {
|
||||
return parseFloat(val)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.bits.stack', {
|
||||
default: 0,
|
||||
ui: {
|
||||
path: 'Chat > Bits and Cheering >> Appearance',
|
||||
title: 'Cheer Stacking',
|
||||
description: 'Collect all the cheers in a message into a single cheer at the start of the message.',
|
||||
component: 'setting-select-box',
|
||||
|
||||
data: [
|
||||
{value: 0, title: 'Disabled'},
|
||||
{value: 1, title: 'Grouped by Type'},
|
||||
{value: 2, title: 'All in One'}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.bits.animated', {
|
||||
default: true,
|
||||
|
||||
ui: {
|
||||
path: 'Chat > Bits and Cheering >> Appearance',
|
||||
title: 'Display animated cheers.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
this.context.on('changed:theme.is-dark', () => {
|
||||
for(const key in this.rooms)
|
||||
if ( this.rooms[key] )
|
||||
this.rooms[key].updateBitsCSS();
|
||||
});
|
||||
|
||||
this.context.on('changed:chat.bits.animated', () => {
|
||||
for(const key in this.rooms)
|
||||
if ( this.rooms[key] )
|
||||
this.rooms[key].updateBitsCSS();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
onEnable() {
|
||||
for(const key in TOKENIZERS)
|
||||
if ( has(TOKENIZERS, key) )
|
||||
this.addTokenizer(TOKENIZERS[key]);
|
||||
}
|
||||
|
||||
|
||||
getBadge(badge, version, room) {
|
||||
let b;
|
||||
if ( this.room_ids[room] ) {
|
||||
const versions = this.room_ids[room].badges.get(badge);
|
||||
b = versions && versions.get(version);
|
||||
}
|
||||
|
||||
if ( ! b ) {
|
||||
const versions = this.badges.get(badge);
|
||||
b = versions && versions.get(version);
|
||||
}
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
|
||||
|
||||
updateBadges(badges) {
|
||||
this.badges = badges;
|
||||
this.updateBadgeCSS();
|
||||
}
|
||||
|
||||
|
||||
updateBadgeCSS() {
|
||||
if ( ! this.badges )
|
||||
this.style.delete('badges');
|
||||
|
||||
const out = [];
|
||||
for(const [key, versions] of this.badges)
|
||||
for(const [version, data] of versions) {
|
||||
out.push(`.ffz-badge.badge--${key}.version--${version} {
|
||||
background-color: transparent;
|
||||
filter: none;
|
||||
${WEBKIT}mask-image: none;
|
||||
background-image: url("${data.image1x}");
|
||||
background-image: ${WEBKIT}image-set(
|
||||
url("${data.image1x}") 1x,
|
||||
url("${data.image2x}") 2x,
|
||||
url("${data.image4x}") 4x
|
||||
);
|
||||
}`)
|
||||
}
|
||||
|
||||
this.style.set('badges', out.join('\n'));
|
||||
}
|
||||
|
||||
|
||||
getUser(id, login, no_create, no_login) {
|
||||
let user;
|
||||
|
||||
if ( this.user_ids[id] )
|
||||
user = this.user_ids[id];
|
||||
|
||||
else if ( this.users[login] && ! no_login )
|
||||
user = this.users[login];
|
||||
|
||||
else if ( no_create )
|
||||
return null;
|
||||
|
||||
else
|
||||
user = {id, login, badges: [], emote_sets: []};
|
||||
|
||||
if ( id && id !== user.id ) {
|
||||
// If the ID isn't what we expected, something is very wrong here.
|
||||
// Blame name changes.
|
||||
if ( user.id )
|
||||
throw new Error('id mismatch');
|
||||
|
||||
// Otherwise, we're just here to set the ID.
|
||||
user.id = id;
|
||||
this.user_ids[id] = user;
|
||||
}
|
||||
|
||||
if ( login ) {
|
||||
const other = this.users[login];
|
||||
if ( other ) {
|
||||
if ( other !== this && ! no_login ) {
|
||||
// If the other has an ID, something weird happened. Screw it
|
||||
// and just take over.
|
||||
if ( other.id )
|
||||
this.users[login] = user;
|
||||
else {
|
||||
// TODO: Merge Logic~~
|
||||
}
|
||||
}
|
||||
} else
|
||||
this.users[login] = user;
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
|
||||
getRoom(id, login, no_create, no_login) {
|
||||
let room;
|
||||
|
||||
if ( this.room_ids[id] )
|
||||
room = this.room_ids[id];
|
||||
|
||||
else if ( this.rooms[login] && ! no_login )
|
||||
room = this.rooms[login];
|
||||
|
||||
else if ( no_create )
|
||||
return null;
|
||||
|
||||
else
|
||||
room = new Room(this, id, login);
|
||||
|
||||
if ( id && id !== room.id ) {
|
||||
// If the ID isn't what we expected, something is very wrong here.
|
||||
// Blame name changes. Or React not being atomic.
|
||||
if ( room.id )
|
||||
throw new Error('id mismatch');
|
||||
|
||||
// Otherwise, we're just here to set the ID.
|
||||
room.id = id;
|
||||
this.room_ids[id] = room;
|
||||
}
|
||||
|
||||
if ( login ) {
|
||||
const other = this.rooms[login];
|
||||
if ( other ) {
|
||||
if ( other !== this && ! no_login ) {
|
||||
// If the other has an ID, something weird happened. Screw it
|
||||
// and just take over.
|
||||
if ( other.id )
|
||||
this.rooms[login] = room;
|
||||
else {
|
||||
// TODO: Merge Logic~~
|
||||
}
|
||||
}
|
||||
|
||||
} else
|
||||
this.rooms[login] = room;
|
||||
}
|
||||
|
||||
return room;
|
||||
}
|
||||
|
||||
|
||||
formatTime(time) {
|
||||
if (!( time instanceof Date ))
|
||||
time = new Date(time);
|
||||
|
||||
let hours = time.getHours();
|
||||
|
||||
const minutes = time.getMinutes(),
|
||||
seconds = time.getSeconds(),
|
||||
|
||||
fmt = this.settings.get('chat.timestamp-format');
|
||||
|
||||
if ( hours > 12 )
|
||||
hours -= 12;
|
||||
else if ( hours === 0 )
|
||||
hours = 12;
|
||||
|
||||
return `${hours}:${minutes < 10 ? '0' : ''}${minutes}`; //:${seconds < 10 ? '0' : ''}${seconds}`;
|
||||
}
|
||||
|
||||
|
||||
addTokenizer(tokenizer) {
|
||||
const type = tokenizer.type;
|
||||
this.tokenizers[type] = tokenizer;
|
||||
if ( tokenizer.priority == null )
|
||||
tokenizer.priority = 0;
|
||||
|
||||
if ( tokenizer.tooltip )
|
||||
this.tooltips.types[type] = tokenizer.tooltip.bind(this);
|
||||
|
||||
this.__tokenizers.push(tokenizer);
|
||||
this.__tokenizers.sort((a, b) => {
|
||||
if ( a.priority > b.priority ) return -1;
|
||||
if ( a.priority < b.priority ) return 1;
|
||||
return a.type < b.sort;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
tokenizeString(message, msg) {
|
||||
let tokens = [{type: 'text', text: message}];
|
||||
|
||||
for(const tokenizer of this.__tokenizers)
|
||||
tokens = tokenizer.process.call(this, tokens, msg);
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
|
||||
tokenizeMessage(msg) {
|
||||
let tokens = [{type: 'text', text: msg.message}];
|
||||
|
||||
for(const tokenizer of this.__tokenizers)
|
||||
tokens = tokenizer.process.call(this, tokens, msg);
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
|
||||
renderBadges(msg, e) { // eslint-disable-line class-methods-use-this
|
||||
const out = [],
|
||||
badges = msg.badges || {};
|
||||
|
||||
for(const key in badges)
|
||||
if ( has(badges, key) ) {
|
||||
const version = badges[key];
|
||||
out.push(e('span', {
|
||||
className: `ffz-tooltip ffz-badge badge--${key} version--${version}`,
|
||||
'data-tooltip-type': 'badge',
|
||||
'data-badge': key,
|
||||
'data-version': version
|
||||
}))
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
renderTokens(tokens, e) {
|
||||
if ( ! e )
|
||||
e = createElement;
|
||||
|
||||
const out = [],
|
||||
tokenizers = this.tokenizers,
|
||||
l = tokens.length;
|
||||
|
||||
for(let i=0; i < l; i++) {
|
||||
const token = tokens[i],
|
||||
type = token.type,
|
||||
tk = tokenizers[type];
|
||||
|
||||
if ( token.hidden )
|
||||
continue;
|
||||
|
||||
let res;
|
||||
|
||||
if ( type === 'text' )
|
||||
res = token.text;
|
||||
|
||||
else if ( tk )
|
||||
res = tk.render.call(this, token, e);
|
||||
|
||||
else
|
||||
res = e('em', {
|
||||
className: 'ffz-unknown-token ffz-tooltip',
|
||||
'data-tooltip-type': 'json',
|
||||
'data-data': JSON.stringify(token, null, 2)
|
||||
}, `[unknown token: ${type}]`)
|
||||
|
||||
if ( res )
|
||||
out.push(res);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
// ====
|
||||
// Twitch Crap
|
||||
// ====
|
||||
|
||||
get_link_info(url, no_promises) {
|
||||
let info = this._link_info[url],
|
||||
expires = info && info[1];
|
||||
|
||||
if ( expires && Date.now() > expires )
|
||||
info = this._link_info[url] = null;
|
||||
|
||||
if ( info && info[0] )
|
||||
return no_promises ? info[2] : Promise.resolve(info[2]);
|
||||
|
||||
if ( no_promises )
|
||||
return null;
|
||||
|
||||
else if ( info )
|
||||
return new Promise((resolve, reject) => info[2].push([resolve, reject]))
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
info = this._link_info[url] = [false, null, [[resolve, reject]]];
|
||||
|
||||
const handle = (success, data) => {
|
||||
const callbacks = ! info[0] && info[2];
|
||||
info[0] = true;
|
||||
info[1] = Date.now() + 120000;
|
||||
info[2] = success ? data : null;
|
||||
|
||||
if ( callbacks )
|
||||
for(const cbs of callbacks)
|
||||
cbs[success ? 0 : 1](data);
|
||||
}
|
||||
|
||||
|
||||
timeout(this.socket.call('get_link', url), 15000)
|
||||
.then(data => handle(true, data))
|
||||
.catch(err => handle(false, err));
|
||||
});
|
||||
}
|
||||
}
|
216
src/modules/chat/room.js
Normal file
216
src/modules/chat/room.js
Normal file
|
@ -0,0 +1,216 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Room
|
||||
// ============================================================================
|
||||
|
||||
import {API_SERVER, IS_WEBKIT} from 'utilities/constants';
|
||||
|
||||
import {EventEmitter} from 'utilities/events';
|
||||
import {createElement as e, ManagedStyle} from 'utilities/dom';
|
||||
import {has} from 'utilities/object';
|
||||
|
||||
const WEBKIT = IS_WEBKIT ? '-webkit-' : '';
|
||||
|
||||
|
||||
export default class Room extends EventEmitter {
|
||||
constructor(manager, id, login) {
|
||||
super();
|
||||
|
||||
this._destroy_timer = null;
|
||||
|
||||
this.manager = manager;
|
||||
this.id = id;
|
||||
this.login = login;
|
||||
|
||||
if ( login )
|
||||
this.manager.rooms[login] = this;
|
||||
|
||||
if ( id )
|
||||
this.manager.room_ids[id] = this;
|
||||
|
||||
this.refs = new Set;
|
||||
this.style = new ManagedStyle(`room--${login}`);
|
||||
|
||||
this.emote_sets = [];
|
||||
this.users = [];
|
||||
this.user_ids = [];
|
||||
|
||||
this.load_data();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
clearTimeout(this._destroy_timer);
|
||||
this._destroy_timer = null;
|
||||
this.destroyed = true;
|
||||
|
||||
this.style.destroy();
|
||||
|
||||
if ( this.manager.room_ids[this.id] === this )
|
||||
this.manager.room_ids[this.id] = null;
|
||||
|
||||
if ( this.manager.rooms[this.login] === this )
|
||||
this.manager.rooms[this.login] = null;
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// FFZ Data
|
||||
// ========================================================================
|
||||
|
||||
async load_data(tries = 0) {
|
||||
if ( this.destroyed )
|
||||
return;
|
||||
|
||||
let response, data;
|
||||
try {
|
||||
response = await fetch(`${API_SERVER}/v1/room/${this.login}`);
|
||||
} catch(err) {
|
||||
tries++;
|
||||
if ( tries < 10 )
|
||||
return setTimeout(() => this.load_data(tries), 500 * tries);
|
||||
|
||||
this.manager.log.error(`Error loading room data for ${this.id}:${this.login}`, err);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! response.ok )
|
||||
return false;
|
||||
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch(err) {
|
||||
this.manager.log.error(`Error parsing room data for ${this.id}:${this.login}`, err);
|
||||
return false;
|
||||
}
|
||||
|
||||
const d = this.data = data.room;
|
||||
if ( d.set && this.emote_sets.indexOf(d) === -1 )
|
||||
this.emote_sets.push(d.set);
|
||||
|
||||
if ( data.sets )
|
||||
for(const set_id in data.sets)
|
||||
if ( has(data.sets, set_id) )
|
||||
this.manager.emotes.load_set_data(set_id, data.sets[set_id]);
|
||||
|
||||
|
||||
// TODO: User data.
|
||||
// TODO: Generate CSS.
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Life Cycle
|
||||
// ========================================================================
|
||||
|
||||
ref(referrer) {
|
||||
clearTimeout(this._destroy_timer);
|
||||
this._destroy_timer = null;
|
||||
this.refs.add(referrer);
|
||||
}
|
||||
|
||||
unref(referrer) {
|
||||
this.refs.delete(referrer);
|
||||
if ( ! this.users.size && ! this._destroy_timer )
|
||||
this._destroy_timer = setTimeout(() => this.destroy(), 5000);
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Badge Data
|
||||
// ========================================================================
|
||||
|
||||
updateBadges(badges) {
|
||||
this.badges = badges;
|
||||
this.updateBadgeCSS();
|
||||
}
|
||||
|
||||
updateBadgeCSS() {
|
||||
if ( ! this.badges )
|
||||
return this.style.delete('badges');
|
||||
|
||||
const out = [],
|
||||
id = this.id;
|
||||
|
||||
for(const [key, versions] of this.badges) {
|
||||
for(const [version, data] of versions) {
|
||||
const rule = `.ffz-badge.badge--${key}.version--${version}`;
|
||||
|
||||
out.push(`[data-room-id="${id}"] ${rule} {
|
||||
background-color: transparent;
|
||||
filter: none;
|
||||
${WEBKIT}mask-image: none;
|
||||
background-image: url("${data.image1x}");
|
||||
background-image: ${WEBKIT}image-set(
|
||||
url("${data.image1x}") 1x,
|
||||
url("${data.image2x}") 2x,
|
||||
url("${data.image4x}") 4x
|
||||
);
|
||||
}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.style.set('badges', out.join('\n'));
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Bits Data
|
||||
// ========================================================================
|
||||
|
||||
updateBitsConfig(config) {
|
||||
this.bitsConfig = config;
|
||||
this.updateBitsCSS();
|
||||
}
|
||||
|
||||
updateBitsCSS() {
|
||||
if ( ! this.bitsConfig )
|
||||
return this.style.delete('bits');
|
||||
|
||||
const animated = this.manager.context.get('chat.bits.animated') ? 'animated' : 'static',
|
||||
is_dark = this.manager.context.get('theme.is-dark'),
|
||||
theme = is_dark ? 'DARK' : 'LIGHT',
|
||||
antitheme = is_dark ? 'LIGHT' : 'DARK',
|
||||
out = [];
|
||||
|
||||
for(const key in this.bitsConfig)
|
||||
if ( has(this.bitsConfig, key) ) {
|
||||
const action = this.bitsConfig[key],
|
||||
prefix = action.prefix.toLowerCase(),
|
||||
tiers = action.tiers,
|
||||
l = tiers.length;
|
||||
|
||||
for(let i=0; i < l; i++) {
|
||||
const images = tiers[i].images[theme][animated],
|
||||
anti_images = tiers[i].images[antitheme][animated];
|
||||
|
||||
out.push(`.ffz-cheer[data-prefix="${prefix}"][data-tier="${i}"] {
|
||||
color: ${tiers[i].color};
|
||||
background-image: url("${images[1]}");
|
||||
background-image: ${WEBKIT}image-set(
|
||||
url("${images[1]}") 1x,
|
||||
url("${images[2]}") 2x,
|
||||
url("${images[4]}") 4x
|
||||
);
|
||||
}
|
||||
.ffz__tooltip .ffz-cheer[data-prefix="${prefix}"][data-tier="${i}"] {
|
||||
background-image: url("${anti_images[1]}");
|
||||
background-image: ${WEBKIT}image-set(
|
||||
url("${anti_images[1]}") 1x,
|
||||
url("${anti_images[2]}") 2x,
|
||||
url("${anti_images[4]}") 4x
|
||||
);
|
||||
}
|
||||
.ffz-cheer-preview[data-prefix="${prefix}"][data-tier="${i}"] {
|
||||
background-image: url("${anti_images[4]}");
|
||||
}`)
|
||||
}
|
||||
}
|
||||
|
||||
this.style.set('bits', out.join('\n'));
|
||||
}
|
||||
}
|
702
src/modules/chat/tokenizers.js
Normal file
702
src/modules/chat/tokenizers.js
Normal file
|
@ -0,0 +1,702 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Default Tokenizers
|
||||
// ============================================================================
|
||||
|
||||
import {sanitize, createElement as e} from 'utilities/dom';
|
||||
import {has} from 'utilities/object';
|
||||
|
||||
const EMOTE_CLASS = 'chat-line__message--emote',
|
||||
LINK_REGEX = /([^\w@#%\-+=:~])?((?:(https?:\/\/)?(?:[\w@#%\-+=:~]+\.)+[a-z]{2,6}(?:\/[\w.\/@#%&()\-+=:?~]*)?))([^\w.\/@#%&()\-+=:?~]|\s|$)/g,
|
||||
MENTION_REGEX = /([^\w@#%\-+=:~])?(@([^\u0000-\u007F]+|\w+)+)([^\w.\/@#%&()\-+=:?~]|\s|$)/g,
|
||||
SPLIT_REGEX = /[^\uD800-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDFFF]/g,
|
||||
|
||||
TWITCH_BASE = '//static-cdn.jtvnw.net/emoticons/v1/';
|
||||
|
||||
|
||||
function split_chars(str) {
|
||||
return str.match(SPLIT_REGEX);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Links
|
||||
// ============================================================================
|
||||
|
||||
const TOOLTIP_VERSION = 4;
|
||||
|
||||
export const Links = {
|
||||
type: 'link',
|
||||
priority: 50,
|
||||
|
||||
render(token, e) {
|
||||
return e('a', {
|
||||
className: 'ffz-tooltip',
|
||||
'data-tooltip-type': 'link',
|
||||
'data-url': token.url,
|
||||
'data-is-mail': token.is_mail,
|
||||
|
||||
rel: 'noopener',
|
||||
target: '_blank',
|
||||
href: token.url
|
||||
}, token.text);
|
||||
},
|
||||
|
||||
tooltip(target, tip) {
|
||||
if ( ! this.context.get('tooltip.rich-links') )
|
||||
return '';
|
||||
|
||||
if ( target.dataset.isMail === 'true' )
|
||||
return [this.i18n.t('tooltip.email-link', 'E-Mail %{address}', {address: target.textContent})];
|
||||
|
||||
return this.get_link_info(target.dataset.url).then(data => {
|
||||
if ( ! data || (data.v || 1) > TOOLTIP_VERSION )
|
||||
return '';
|
||||
|
||||
let content = data.content || data.html || '';
|
||||
|
||||
// TODO: Replace timestamps.
|
||||
|
||||
if ( data.urls && data.urls.length > 1 )
|
||||
content += (content.length ? '<hr>' : '') +
|
||||
sanitize(this.i18n.t(
|
||||
'tooltip.link-destination',
|
||||
'Destination: %{url}',
|
||||
{url: data.urls[data.urls.length-1]}
|
||||
));
|
||||
|
||||
if ( data.unsafe ) {
|
||||
const reasons = Array.from(new Set(data.urls.map(x => x[2]).filter(x => x))).join(', ');
|
||||
content = this.i18n.t(
|
||||
'tooltip.link-unsafe',
|
||||
"Caution: This URL is on Google's Safe Browsing List for: %{reasons}",
|
||||
{reasons: sanitize(reasons.toLowerCase())}
|
||||
) + (content.length ? `<hr>${content}` : '');
|
||||
}
|
||||
|
||||
const show_image = this.context.get('tooltip.link-images') && (data.image_safe || this.context.get('tooltip.link-nsfw-images'));
|
||||
|
||||
if ( show_image ) {
|
||||
if ( data.image && ! data.image_iframe )
|
||||
content = `<img class="preview-image" src="${sanitize(data.image)}">${content}`
|
||||
|
||||
setTimeout(() => {
|
||||
if ( tip.element )
|
||||
for(const el of tip.element.querySelectorAll('video,img'))
|
||||
el.addEventListener('load', tip.update)
|
||||
});
|
||||
|
||||
} else if ( content.length )
|
||||
content = content.replace(/<!--MS-->.*<!--ME-->/g, '');
|
||||
|
||||
if ( data.tooltip_class )
|
||||
tip.element.classList.add(data.tooltip_class);
|
||||
|
||||
return content;
|
||||
|
||||
}).catch(error =>
|
||||
sanitize(this.i18n.t('tooltip.error', 'An error occurred. (%{error})', {error}))
|
||||
);
|
||||
},
|
||||
|
||||
process(tokens, msg) {
|
||||
if ( ! tokens || ! tokens.length )
|
||||
return tokens;
|
||||
|
||||
const out = [];
|
||||
for(const token of tokens) {
|
||||
if ( token.type !== 'text' ) {
|
||||
out.push(token);
|
||||
continue;
|
||||
}
|
||||
|
||||
LINK_REGEX.lastIndex = 0;
|
||||
const text = token.text;
|
||||
let idx = 0, match;
|
||||
|
||||
while((match = LINK_REGEX.exec(text))) {
|
||||
const nix = match.index + (match[1] ? match[1].length : 0);
|
||||
if ( idx !== nix )
|
||||
out.push({type: 'text', text: text.slice(idx, nix)});
|
||||
|
||||
const is_mail = ! match[3] && match[2].indexOf('/') === -1 && match[2].indexOf('@') !== -1;
|
||||
|
||||
out.push({
|
||||
type: 'link',
|
||||
url: (match[3] ? '' : is_mail ? 'mailto:' : 'https://') + match[2],
|
||||
is_mail,
|
||||
text: match[2]
|
||||
});
|
||||
|
||||
idx = nix + match[2].length;
|
||||
}
|
||||
|
||||
if ( idx < text.length )
|
||||
out.push({type: 'text', text: text.slice(idx)});
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Rich Content
|
||||
// ============================================================================
|
||||
|
||||
/*export const RichContent = {
|
||||
type: 'rich-content',
|
||||
|
||||
render(token, e) {
|
||||
return e('div', {
|
||||
className: 'ffz--rich-content elevation-1 mg-y-05',
|
||||
}, e('a', {
|
||||
className: 'clips-chat-card flex flex-nowrap pd-05',
|
||||
target: '_blank',
|
||||
href: token.url
|
||||
}, [
|
||||
e('div', {
|
||||
className: 'clips-chat-card__thumb align-items-center flex justify-content-center'
|
||||
})
|
||||
]));
|
||||
},
|
||||
|
||||
process(tokens, msg) {
|
||||
if ( ! tokens || ! tokens.length )
|
||||
return tokens;
|
||||
|
||||
for(const token of tokens) {
|
||||
if ( token.type !== 'link' )
|
||||
continue;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Mentions
|
||||
// ============================================================================
|
||||
|
||||
export const Mentions = {
|
||||
type: 'mention',
|
||||
priority: 40,
|
||||
|
||||
render(token, e) {
|
||||
return e('strong', {
|
||||
className: 'chat-line__message-mention'
|
||||
}, `@${token.recipient}`);
|
||||
},
|
||||
|
||||
process(tokens, msg) {
|
||||
if ( ! tokens || ! tokens.length )
|
||||
return tokens;
|
||||
|
||||
const out = [];
|
||||
for(const token of tokens) {
|
||||
if ( token.type !== 'text' ) {
|
||||
out.push(token);
|
||||
continue;
|
||||
}
|
||||
|
||||
MENTION_REGEX.lastIndex = 0;
|
||||
const text = token.text;
|
||||
let idx = 0, match;
|
||||
|
||||
while((match = MENTION_REGEX.exec(text))) {
|
||||
const nix = match.index + (match[1] ? match[1].length : 0);
|
||||
if ( idx !== nix )
|
||||
out.push({type: 'text', text: text.slice(idx, nix)});
|
||||
|
||||
out.push({
|
||||
type: 'mention',
|
||||
recipient: match[3],
|
||||
length: match[3].length + 1
|
||||
});
|
||||
|
||||
idx = nix + match[2].length;
|
||||
}
|
||||
|
||||
if ( idx < text.length )
|
||||
out.push({type: 'text', text: text.slice(idx)});
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Cheers
|
||||
// ============================================================================
|
||||
|
||||
export const CheerEmotes = {
|
||||
type: 'cheer',
|
||||
|
||||
render(token, e) {
|
||||
return e('span', {
|
||||
className: `ffz-cheer ffz-tooltip`,
|
||||
'data-tooltip-type': 'cheer',
|
||||
'data-prefix': token.prefix,
|
||||
'data-amount': this.i18n.formatNumber(token.amount),
|
||||
'data-tier': token.tier,
|
||||
'data-individuals': token.individuals ? JSON.stringify(token.individuals) : 'null',
|
||||
alt: token.text
|
||||
});
|
||||
},
|
||||
|
||||
tooltip(target) {
|
||||
const ds = target.dataset,
|
||||
amount = parseInt(ds.amount.replace(/,/g, ''), 10),
|
||||
prefix = ds.prefix,
|
||||
tier = ds.tier,
|
||||
individuals = ds.individuals && JSON.parse(ds.individuals),
|
||||
length = individuals && individuals.length;
|
||||
|
||||
const out = [
|
||||
this.context.get('tooltip.emote-images') && e('div', {
|
||||
className: 'preview-image ffz-cheer-preview',
|
||||
'data-prefix': prefix,
|
||||
'data-tier': tier
|
||||
}),
|
||||
this.i18n.t('tooltip.bits', '%{count|number} Bits', amount),
|
||||
];
|
||||
|
||||
if ( length > 1 ) {
|
||||
out.push(e('br'));
|
||||
|
||||
individuals.sort(i => -i[0]);
|
||||
|
||||
for(let i=0; i < length && i < 12; i++) {
|
||||
const [amount, tier, prefix] = individuals[i];
|
||||
out.push(this.tokenizers.cheer.render.call(this, {
|
||||
amount,
|
||||
prefix,
|
||||
tier
|
||||
}, e));
|
||||
}
|
||||
|
||||
if ( length > 12 ) {
|
||||
out.push(e('br'));
|
||||
out.push(this.i18n.t('tooltip.bits.more', '(and %{count} more)', length-12));
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
process(tokens, msg) {
|
||||
if ( ! tokens || ! tokens.length || ! msg.bits )
|
||||
return tokens;
|
||||
|
||||
const SiteChat = this.resolve('site.chat'),
|
||||
chat = SiteChat && SiteChat.currentChat,
|
||||
bitsConfig = chat && chat.props.bitsConfig;
|
||||
|
||||
if ( ! bitsConfig )
|
||||
return tokens;
|
||||
|
||||
const actions = bitsConfig.indexedActions,
|
||||
matcher = new RegExp('\\b(' + Object.keys(actions).join('|') + ')(\\d+)\\b', 'ig');
|
||||
|
||||
const out = [],
|
||||
collected = {},
|
||||
collect = this.context.get('chat.bits.stack');
|
||||
|
||||
for(const token of tokens) {
|
||||
if ( token.type !== 'text' ) {
|
||||
out.push(token);
|
||||
continue;
|
||||
}
|
||||
|
||||
matcher.lastIndex = 0;
|
||||
const text = token.text;
|
||||
let idx = 0, match;
|
||||
|
||||
while((match = matcher.exec(text))) {
|
||||
const prefix = match[1].toLowerCase(),
|
||||
cheer = actions[prefix];
|
||||
|
||||
if ( ! cheer )
|
||||
continue;
|
||||
|
||||
if ( idx !== match.index )
|
||||
out.push({type: 'text', text: text.slice(idx, match.index)});
|
||||
|
||||
const amount = parseInt(match[2], 10),
|
||||
tiers = cheer.orderedTiers;
|
||||
|
||||
let tier, token;
|
||||
|
||||
for(let i=0, l = tiers.length; i < l; i++)
|
||||
if ( amount >= tiers[i].bits ) {
|
||||
tier = i;
|
||||
break;
|
||||
}
|
||||
|
||||
out.push(token = {
|
||||
type: 'cheer',
|
||||
prefix,
|
||||
tier,
|
||||
amount: parseInt(match[2], 10),
|
||||
text: match[0]
|
||||
});
|
||||
|
||||
if ( collect ) {
|
||||
const pref = collect === 2 ? 'cheer' : prefix,
|
||||
group = collected[pref] = collected[pref] || {total: 0, individuals: []};
|
||||
|
||||
group.total += amount;
|
||||
group.individuals.push([amount, tier, prefix]);
|
||||
token.hidden = true;
|
||||
}
|
||||
|
||||
idx = match.index + match[0].length;
|
||||
}
|
||||
|
||||
if ( idx < text.length )
|
||||
out.push({type: 'text', text: text.slice(idx)});
|
||||
}
|
||||
|
||||
if ( collect ) {
|
||||
for(const prefix in collected)
|
||||
if ( has(collected, prefix) ) {
|
||||
const cheers = collected[prefix],
|
||||
cheer = actions[prefix],
|
||||
tiers = cheer.orderedTiers;
|
||||
|
||||
let tier = 0;
|
||||
for(let l = tiers.length; tier < l; tier++)
|
||||
if ( cheers.total >= tiers[tier].bits )
|
||||
break;
|
||||
|
||||
out.unshift({
|
||||
type: 'cheer',
|
||||
prefix,
|
||||
tier,
|
||||
amount: cheers.total,
|
||||
individuals: cheers.individuals,
|
||||
length: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Addon Emotes
|
||||
// ============================================================================
|
||||
|
||||
export const AddonEmotes = {
|
||||
type: 'emote',
|
||||
|
||||
render(token, e) {
|
||||
const mods = token.modifiers || [], ml = mods.length,
|
||||
emote = e('img', {
|
||||
className: `${EMOTE_CLASS} ffz-tooltip${token.provider === 'ffz' ? ' ffz-emote' : ''}`,
|
||||
src: token.src,
|
||||
srcSet: token.srcSet,
|
||||
alt: token.text,
|
||||
'data-tooltip-type': 'emote',
|
||||
'data-provider': token.provider,
|
||||
'data-id': token.id,
|
||||
'data-set': token.set,
|
||||
'data-modifiers': ml ? mods.map(x => x.id).join(' ') : null,
|
||||
'data-modifier-info': ml ? JSON.stringify(mods.map(x => [x.set, x.id])) : null
|
||||
});
|
||||
|
||||
if ( ! ml )
|
||||
return emote;
|
||||
|
||||
return e('span', {
|
||||
className: `${EMOTE_CLASS} modified-emote`,
|
||||
'data-provider': token.provider,
|
||||
'data-id': token.id,
|
||||
'data-set': token.set
|
||||
}, [
|
||||
emote,
|
||||
mods.map(t => e('span', null, this.tokenizers.emote.render(t, e)))
|
||||
]);
|
||||
},
|
||||
|
||||
tooltip(target, tip) {
|
||||
const provider = target.dataset.provider,
|
||||
modifiers = target.dataset.modifierInfo;
|
||||
|
||||
let preview, source, owner, mods;
|
||||
|
||||
if ( modifiers && modifiers !== 'null' ) {
|
||||
mods = JSON.parse(modifiers).map(([set_id, emote_id]) => {
|
||||
const emote_set = this.emotes.emote_sets[set_id],
|
||||
emote = emote_set && emote_set.emotes[emote_id];
|
||||
|
||||
if ( emote )
|
||||
return e('span', null, [
|
||||
this.tokenizers.emote.render(emote.token, e),
|
||||
` - ${emote.hidden ? '???' : emote.name}`
|
||||
]);
|
||||
})
|
||||
}
|
||||
|
||||
if ( provider === 'twitch' ) {
|
||||
const emote_id = parseInt(target.dataset.id, 10),
|
||||
set_id = this.emotes.twitch_emote_to_set(emote_id, tip.rerender),
|
||||
emote_set = set_id != null && this.emotes.twitch_set_to_channel(set_id, tip.rerender);
|
||||
|
||||
preview = `//static-cdn.jtvnw.net/emoticons/v1/${emote_id}/4.0?_=preview`;
|
||||
|
||||
if ( emote_set ) {
|
||||
source = emote_set.c_name;
|
||||
|
||||
if ( source === '--global--' || emote_id === 80393 )
|
||||
source = this.i18n.t('emote.global', 'Twitch Global');
|
||||
|
||||
else if ( source === '--twitch-turbo--' || source === 'turbo' || source === '--turbo-faces--' )
|
||||
source = this.i18n.t('emote.turbo', 'Twitch Turbo');
|
||||
|
||||
else if ( source === '--prime--' || source === '--prime-faces--' )
|
||||
source = this.i18n.t('emote.prime', 'Twitch Prime');
|
||||
|
||||
else
|
||||
source = this.i18n.t('tooltip.channel', 'Channel: %{source}', {source});
|
||||
}
|
||||
|
||||
} else if ( provider === 'ffz' ) {
|
||||
const emote_set = this.emotes.emote_sets[target.dataset.set],
|
||||
emote = emote_set && emote_set.emotes[target.dataset.id];
|
||||
|
||||
if ( emote_set )
|
||||
source = emote_set.source_line || (`${emote_set.source || 'FFZ'} ${emote_set.title || 'Global'}`);
|
||||
|
||||
if ( emote ) {
|
||||
if ( emote.owner )
|
||||
owner = this.i18n.t(
|
||||
'emote.owner', 'By: %{owner}',
|
||||
{owner: emote.owner.display_name});
|
||||
|
||||
if ( emote.urls[4] )
|
||||
preview = emote.urls[4];
|
||||
else if ( emote.urls[2] )
|
||||
preview = emote.urls[2];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
preview && this.context.get('tooltip.emote-images') && e('img', {
|
||||
className: 'preview-image',
|
||||
src: preview,
|
||||
onLoad: tip.update
|
||||
}),
|
||||
|
||||
this.i18n.t('tooltip.emote', 'Emote: %{code}', {code: target.alt}),
|
||||
|
||||
source && this.context.get('tooltip.emote-sources') && e('div', {
|
||||
className: 'pd-t-05',
|
||||
}, source),
|
||||
|
||||
owner && this.context.get('tooltip.emote-sources') && e('div', {
|
||||
className: 'pd-t-05'
|
||||
}, owner),
|
||||
|
||||
mods && e('div', {
|
||||
className: 'pd-t-1'
|
||||
}, mods)
|
||||
];
|
||||
},
|
||||
|
||||
process(tokens, msg) {
|
||||
if ( ! tokens || ! tokens.length )
|
||||
return tokens;
|
||||
|
||||
const applicable_sets = this.emotes.getSets(
|
||||
msg.user.userID,
|
||||
msg.user.userLogin,
|
||||
msg.roomID,
|
||||
msg.roomLogin
|
||||
),
|
||||
emotes = {},
|
||||
out = [];
|
||||
|
||||
if ( ! applicable_sets || ! applicable_sets.length )
|
||||
return tokens;
|
||||
|
||||
for(const emote_set of applicable_sets)
|
||||
if ( emote_set && emote_set.emotes )
|
||||
for(const emote_id in emote_set.emotes ) { // eslint-disable-line guard-for-in
|
||||
const emote = emote_set.emotes[emote_id];
|
||||
if ( ! has(emotes, emote.name) )
|
||||
emotes[emote.name] = emote;
|
||||
}
|
||||
|
||||
|
||||
let last_token, emote;
|
||||
for(const token of tokens) {
|
||||
if ( ! token )
|
||||
continue;
|
||||
|
||||
if ( token.type !== 'text' ) {
|
||||
if ( token.type === 'emote' && ! token.modifiers )
|
||||
token.modifiers = [];
|
||||
|
||||
out.push(token);
|
||||
last_token = token;
|
||||
continue;
|
||||
}
|
||||
|
||||
let text = [];
|
||||
|
||||
for(const segment of token.text.split(/ +/)) {
|
||||
if ( has(emotes, segment) ) {
|
||||
emote = emotes[segment];
|
||||
|
||||
// Is this emote a modifier?
|
||||
if ( emote.modifier && last_token && last_token.modifiers && (!text.length || (text.length === 1 && text[0] === '')) ) {
|
||||
if ( last_token.modifiers.indexOf(emote.token) === -1 )
|
||||
last_token.modifiers.push(emote.token);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( text.length ) {
|
||||
// We have pending text. Join it together, with an extra space.
|
||||
const t = {type: 'text', text: text.join(' ') + ' '};
|
||||
out.push(t);
|
||||
if ( t.text.trim().length )
|
||||
last_token = t;
|
||||
|
||||
text = [];
|
||||
}
|
||||
|
||||
const t = Object.assign({modifiers: []}, emote.token);
|
||||
out.push(t);
|
||||
last_token = t;
|
||||
|
||||
text.push('');
|
||||
|
||||
} else
|
||||
text.push(segment);
|
||||
}
|
||||
|
||||
if ( text.length > 1 || (text.length === 1 && text[0] !== '') )
|
||||
out.push({type: 'text', text: text.join(' ')});
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Twitch Emotes
|
||||
// ============================================================================
|
||||
|
||||
export const TwitchEmotes = {
|
||||
type: 'twitch-emote',
|
||||
priority: 10,
|
||||
|
||||
process(tokens, msg) {
|
||||
if ( ! msg.emotes )
|
||||
return tokens;
|
||||
|
||||
const data = msg.emotes,
|
||||
emotes = [];
|
||||
|
||||
for(const emote_id in data)
|
||||
if ( has(data, emote_id) ) {
|
||||
for(const match of data[emote_id])
|
||||
emotes.push([emote_id, match.startIndex, match.endIndex + 1]);
|
||||
}
|
||||
|
||||
const out = [],
|
||||
e_length = emotes.length;
|
||||
|
||||
if ( ! e_length )
|
||||
return tokens;
|
||||
|
||||
emotes.sort((a,b) => a[1] - b[1]);
|
||||
|
||||
let idx = 0,
|
||||
eix = 0;
|
||||
|
||||
for(const token of tokens) {
|
||||
const length = token.length || (token.text && split_chars(token.text).length) || 0,
|
||||
t_start = idx,
|
||||
t_end = idx + length;
|
||||
|
||||
if ( token.type !== 'text' ) {
|
||||
out.push(token);
|
||||
idx = t_end;
|
||||
continue;
|
||||
}
|
||||
|
||||
const text = split_chars(token.text);
|
||||
|
||||
while( eix < e_length ) {
|
||||
const [e_id, e_start, e_end] = emotes[eix];
|
||||
|
||||
// Does this emote go outside the bounds of this token?
|
||||
if ( e_start > t_end || e_end > t_end ) {
|
||||
// Output the remainder of this token.
|
||||
if ( t_start === idx )
|
||||
out.push(token);
|
||||
else
|
||||
out.push({
|
||||
type: 'text',
|
||||
text: text.slice(idx - t_start).join('')
|
||||
});
|
||||
|
||||
// If this emote goes across token boundaries,
|
||||
// skip it.
|
||||
if ( e_start < t_end && e_end > t_end )
|
||||
eix++;
|
||||
|
||||
idx = t_end;
|
||||
break;
|
||||
}
|
||||
|
||||
// If there's text at the beginning of the token that
|
||||
// isn't part of this emote, output it.
|
||||
if ( e_start > idx )
|
||||
out.push({
|
||||
type: 'text',
|
||||
text: text.slice(idx - t_start, e_start - t_start).join('')
|
||||
});
|
||||
|
||||
out.push({
|
||||
type: 'emote',
|
||||
id: e_id,
|
||||
provider: 'twitch',
|
||||
src: `${TWITCH_BASE}${e_id}/1.0`,
|
||||
srcSet: `${TWITCH_BASE}${e_id}/1.0 1x, ${TWITCH_BASE}${e_id}/2.0 2x`,
|
||||
text: text.slice(e_start - t_start, e_end - t_start).join(''),
|
||||
modifiers: []
|
||||
});
|
||||
|
||||
idx = e_end;
|
||||
eix++;
|
||||
}
|
||||
|
||||
// We've finished processing emotes. If there is any
|
||||
// remaining text in the token, push it out.
|
||||
if ( idx < t_end ) {
|
||||
if ( t_start === idx )
|
||||
out.push(token);
|
||||
else
|
||||
out.push({
|
||||
type: 'text',
|
||||
text: text.slice(idx - t_start).join('')
|
||||
});
|
||||
|
||||
idx = t_end;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
3
src/modules/main_menu/components.js
Normal file
3
src/modules/main_menu/components.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
'use strict';
|
||||
|
||||
export default require.context('./components', false, /\.vue$/);
|
50
src/modules/main_menu/components/changelog.vue
Normal file
50
src/modules/main_menu/components/changelog.vue
Normal file
|
@ -0,0 +1,50 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--changelog border-t pd-t-1">
|
||||
<div class="align-center">
|
||||
<h2>{{ t('home.changelog', 'Changelog') }}</h2>
|
||||
</div>
|
||||
|
||||
<div ref="changes" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
import {SERVER} from 'utilities/constants';
|
||||
|
||||
export default {
|
||||
props: ['item', 'context'],
|
||||
|
||||
methods: {
|
||||
fetch(url, container) {
|
||||
const done = data => {
|
||||
if ( ! data )
|
||||
data = 'There was an error loading this page from the server.';
|
||||
|
||||
container.innerHTML = data;
|
||||
|
||||
const btn = container.querySelector('#ffz-old-news-button');
|
||||
if ( btn )
|
||||
btn.addEventListener('click', () => {
|
||||
btn.parentElement.removeChild(btn);
|
||||
const old_news = container.querySelector('#ffz-old-news');
|
||||
if ( old_news )
|
||||
this.fetch(`${SERVER}/script/old_changes.html`, old_news);
|
||||
});
|
||||
}
|
||||
|
||||
fetch(url)
|
||||
.then(resp => resp.ok ? resp.text() : null)
|
||||
.then(done)
|
||||
.catch(err => done(null));
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetch(`${SERVER}/script/changelog.html`, this.$refs.changes);
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
35
src/modules/main_menu/components/feedback-page.vue
Normal file
35
src/modules/main_menu/components/feedback-page.vue
Normal file
|
@ -0,0 +1,35 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--home border-t pd-t-1">
|
||||
<h2>Feedback</h2>
|
||||
|
||||
<div class="mg-y-1 c-background-accent c-text-overlay pd-1">
|
||||
<h3 class="ffz-i-attention">
|
||||
Please keep in mind that FrankerFaceZ v4 is under heavy development.
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Okay, still here? Great! You can provide feedback and bug reports by
|
||||
<a href="https://github.com/FrankerFaceZ/FrankerFaceZ/issues" target="_blank" rel="noopener">
|
||||
opening an issue at our GitHub repository</a>.
|
||||
|
||||
You can also <a href="https://twitter.com/FrankerFaceZ" target="_blank" rel="noopener">
|
||||
tweet at us</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
When creating a GitHub issue, please check that someone else hasn't
|
||||
already created one for what you'd like to discuss or report.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['item', 'context'],
|
||||
|
||||
}
|
||||
</script>
|
67
src/modules/main_menu/components/filter-editor.vue
Normal file
67
src/modules/main_menu/components/filter-editor.vue
Normal file
|
@ -0,0 +1,67 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--widget ffz--filter-editor">
|
||||
<div ref="list" class="ffz--rule-list">
|
||||
<section v-for="(rule, idx) in rules">
|
||||
<div
|
||||
class="ffz--rule elevation-1 c-background border mg-b-05 pd-y-05 pd-r-1 flex flex--nowrap align-items-start"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="flex flex-shrink-0 align-items-center handle pd-x-05 pd-y-1">
|
||||
<span class="ffz-i-ellipsis-vert" />
|
||||
</div>
|
||||
|
||||
<div class="flex-shrink-0 pd-y-05">
|
||||
Channel
|
||||
</div>
|
||||
|
||||
<div class="mg-x-1 flex flex-grow-1">
|
||||
<div class="flex-shrink-0 mg-r-1">
|
||||
<select class="tw-select">
|
||||
<option>is one of</option>
|
||||
<option>is not one of</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<input
|
||||
type="text"
|
||||
class="tw-input"
|
||||
value="SirStendec"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-shrink-0 align-items-center">
|
||||
<button class="tw-button tw-button--text" @click="del(idx)">
|
||||
<span class="tw-button__text ffz-i-trash">
|
||||
{{ t('setting.filters.delete', 'Delete') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="tw-button tw-button--hollow mg-y-1 full-width"
|
||||
@click="newRule"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-plus">
|
||||
{{ t('', 'Add New Rule') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['filters', 'rules', 'context'],
|
||||
|
||||
methods: {
|
||||
newRule() {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
100
src/modules/main_menu/components/home-page.vue
Normal file
100
src/modules/main_menu/components/home-page.vue
Normal file
|
@ -0,0 +1,100 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--home flex flex--nowrap">
|
||||
<div class="flex-grow-1">
|
||||
<div class="align-center">
|
||||
<h1 class="ffz-i-zreknarf ffz-i-pd-1">FrankerFaceZ</h1>
|
||||
<span class="c-text-alt">
|
||||
{{ t('home.tag-line', 'The Twitch Enhancement Suite') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<section class="pd-t-1 border-t mg-t-1">
|
||||
<h2>Welcome to the v4.0 Beta</h2>
|
||||
|
||||
<p>
|
||||
This is the initial, beta release of FrankerFaceZ v4.0 with support
|
||||
for the Twitch website rewrite.
|
||||
|
||||
As you'll notice, this release is <strong>not</strong> complete.
|
||||
There are missing features. There are bugs. If you are a moderator,
|
||||
you will want to just keep opening a Legacy Chat Popout for now.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
FrankerFaceZ v4.0 is still under heavy development and there will
|
||||
be significant changes and improvements in the coming weeks. For
|
||||
now, here are some of the bigger issues:
|
||||
</p>
|
||||
|
||||
<ul class="mg-b-2">
|
||||
<li>Settings from the old version are not being imported.</li>
|
||||
<li>Settings cannot be searched.</li>
|
||||
<li>FFZ badges do not display.</li>
|
||||
<li>Oh god everything is missing.</li>
|
||||
<li>FFZ:AP is broken.</li>
|
||||
<li>Uptime breaks occasionally.</li>
|
||||
</ul>
|
||||
|
||||
<p>And the biggest features still under development:</p>
|
||||
|
||||
<ul class="mg-b-2">
|
||||
<li>Dark Theme (Pls No Purple)</li>
|
||||
<li>Chat Pause on Hover</li>
|
||||
<li>Badge Customization</li>
|
||||
<li>Emoji Rendering</li>
|
||||
<li>Emotes Menu</li>
|
||||
<li>Chat Filtering (Highlighted Words, etc.)</li>
|
||||
<li>Room Status Indicators</li>
|
||||
<li>Custom Mod Cards</li>
|
||||
<li>Custom Mod Actions</li>
|
||||
<li>Chat Room Tabs</li>
|
||||
<li>Recent Highlights</li>
|
||||
<li>More Channel Metadata</li>
|
||||
<li>Disable Hosting</li>
|
||||
<li>Portrait Mode</li>
|
||||
<li>Hiding stuff in the directory</li>
|
||||
<li>Directory Host Stacking</li>
|
||||
<li>Basically anything to do with the directory</li>
|
||||
<li>Importing and exporting settings</li>
|
||||
<li>User Aliases</li>
|
||||
<li>Rich Content in Chat (aka Clip Embeds)</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
For a possibly more up-to-date list of what I'm working on,
|
||||
please consult <a href="https://trello.com/b/LGcYPFwi/frankerfacez-v4" target="_blank">this Trello board</a>.
|
||||
</p>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="mg-l-1 flex-shrink-0 tweet-column">
|
||||
<a class="twitter-timeline" data-width="300" data-theme="dark" href="https://twitter.com/FrankerFaceZ?ref_src=twsrc%5Etfw">
|
||||
Tweets by FrankerFaceZ
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
import {createElement as e} from 'utilities/dom';
|
||||
|
||||
export default {
|
||||
props: ['item', 'context'],
|
||||
|
||||
mounted() {
|
||||
let el;
|
||||
document.head.appendChild(el = e('script', {
|
||||
id: 'ffz--twitter-widget-script',
|
||||
async: true,
|
||||
charset: 'utf-8',
|
||||
src: 'https://platform.twitter.com/widgets.js',
|
||||
onLoad: () => el.parentElement.removeChild(el)
|
||||
}));
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
180
src/modules/main_menu/components/main-menu.vue
Normal file
180
src/modules/main_menu/components/main-menu.vue
Normal file
|
@ -0,0 +1,180 @@
|
|||
<template lang="html">
|
||||
<div class="ffz-main-menu elevation-3 c-background-alt border flex flex--nowrap flex-column" :class="{ maximized }">
|
||||
<header class="c-background pd-1 pd-l-2 full-width align-items-center flex flex-nowrap" @dblclick="resize">
|
||||
<h3 class="ffz-i-zreknarf ffz-i-pd-1">FrankerFaceZ</h3>
|
||||
<div class="flex-grow-1 pd-x-2">
|
||||
<!--div class="tw-search-input">
|
||||
<label for="ffz-main-menu.search" class="hide-accessible">{{ t('main-menu.search', 'Search Settings') }}</label>
|
||||
<div class="relative">
|
||||
<div class="tw-input__icon-group">
|
||||
<div class="tw-input__icon">
|
||||
<figure class="ffz-i-search" />
|
||||
</div>
|
||||
</div>
|
||||
<input type="search" class="tw-input tw-input--icon-left" :placeholder="t('main-menu.search', 'Search Settings')" autocapitalize="off" autocorrect="off" autocomplete="off" id="ffz-main-menu.search">
|
||||
</div>
|
||||
</div-->
|
||||
</div>
|
||||
<button class="tw-button-icon mg-x-05" @click="resize">
|
||||
<span class="tw-button-icon__icon">
|
||||
<figure :class="{'ffz-i-window-maximize': !maximized, 'ffz-i-window-restore': maximized}" />
|
||||
</span>
|
||||
</button>
|
||||
<button class="tw-button-icon mg-x-05" @click="close">
|
||||
<span class="tw-button-icon__icon">
|
||||
<figure class="ffz-i-window-close" />
|
||||
</span>
|
||||
</button>
|
||||
</header>
|
||||
<section class="border-t full-height full-width flex flex--nowrap">
|
||||
<nav class="ffz-vertical-nav c-background-alt-2 border-r full-height flex flex-column flex-shrink-0 flex-nowrap">
|
||||
<header class="border-b pd-1">
|
||||
<profile-selector
|
||||
:context="context"
|
||||
@navigate="navigate"
|
||||
/>
|
||||
</header>
|
||||
<div class="full-width full-height overflow-hidden flex flex-nowrap relative">
|
||||
<div class="ffz-vertical-nav__items full-width flex-grow-1 scrollable-area" data-simplebar>
|
||||
<div class="simplebar-scroll-content">
|
||||
<div class="simplebar-content">
|
||||
<menu-tree
|
||||
:currentItem="currentItem"
|
||||
:modal="nav"
|
||||
@change-item="changeItem"
|
||||
@navigate="navigate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="c-text-alt border-t pd-1">
|
||||
<div>
|
||||
{{ t('main-menu.version', 'Version %{version}', {version: version.toString()}) }}
|
||||
</div>
|
||||
<div class="c-text-alt-2">
|
||||
{{version.build}}
|
||||
</div>
|
||||
</footer>
|
||||
</nav>
|
||||
<main class="flex-grow-1 scrollable-area" data-simplebar>
|
||||
<div class="simplebar-scroll-content">
|
||||
<div class="simplebar-content">
|
||||
<menu-page
|
||||
ref="page"
|
||||
:context="context"
|
||||
:item="currentItem"
|
||||
@change-item="changeItem"
|
||||
@navigate="navigate"
|
||||
v-if="currentItem"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import displace from 'displacejs';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return this.$vnode.data;
|
||||
},
|
||||
|
||||
created() {
|
||||
this.context.context._add_user();
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
this.context.context._remove_user();
|
||||
},
|
||||
|
||||
methods: {
|
||||
changeProfile() {
|
||||
const new_id = this.$refs.profiles.value,
|
||||
new_profile = this.context.profiles[new_id];
|
||||
|
||||
if ( new_profile )
|
||||
this.context.currentProfile = new_profile;
|
||||
},
|
||||
|
||||
changeItem(item) {
|
||||
if ( this.$refs.page && this.$refs.page.onBeforeChange ) {
|
||||
if ( this.$refs.page.onBeforeChange(this.currentItem, item) === false )
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentItem = item;
|
||||
let current = item;
|
||||
while(current = current.parent)
|
||||
current.expanded = true;
|
||||
},
|
||||
|
||||
updateDrag() {
|
||||
if ( this.maximized )
|
||||
this.destroyDrag();
|
||||
else
|
||||
this.createDrag();
|
||||
},
|
||||
|
||||
destroyDrag() {
|
||||
if ( this.displace ) {
|
||||
this.displace.destroy();
|
||||
this.displace = null;
|
||||
}
|
||||
},
|
||||
|
||||
createDrag() {
|
||||
this.$nextTick(() => {
|
||||
if ( ! this.maximized )
|
||||
this.displace = displace(this.$el, {
|
||||
handle: this.$el.querySelector('header'),
|
||||
highlightInputs: true,
|
||||
constrain: true
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
handleResize() {
|
||||
if ( this.displace )
|
||||
this.displace.reinit();
|
||||
},
|
||||
|
||||
navigate(key) {
|
||||
let item = this.nav_keys[key];
|
||||
while(item && item.page)
|
||||
item = item.parent;
|
||||
|
||||
if ( ! item )
|
||||
return;
|
||||
|
||||
this.changeItem(item);
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
maximized() {
|
||||
this.updateDrag();
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.updateDrag();
|
||||
|
||||
this._on_resize = this.handleResize.bind(this);
|
||||
window.addEventListener('resize', this._on_resize);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.destroyDrag();
|
||||
|
||||
if ( this._on_resize ) {
|
||||
window.removeEventListener('resize', this._on_resize);
|
||||
this._on_resize = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
34
src/modules/main_menu/components/menu-container.vue
Normal file
34
src/modules/main_menu/components/menu-container.vue
Normal file
|
@ -0,0 +1,34 @@
|
|||
<template lang="html">
|
||||
<div v-bind:class="classes" v-if="item.contents">
|
||||
<header v-if="! item.no_header">
|
||||
{{ t(item.i18n_key, item.title, item) }}
|
||||
</header>
|
||||
<section
|
||||
v-if="item.description"
|
||||
v-html="t(item.desc_i18n_key, item.description, item)"
|
||||
class="pd-b-1"
|
||||
/>
|
||||
<component
|
||||
v-for="i in item.contents"
|
||||
v-bind:is="i.component"
|
||||
:context="context"
|
||||
:item="i"
|
||||
:key="i.full_key"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['item', 'context'],
|
||||
|
||||
computed: {
|
||||
classes() {
|
||||
return [
|
||||
'ffz--menu-container',
|
||||
this.item.full_box ? 'border' : 'border-t'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
89
src/modules/main_menu/components/menu-page.vue
Normal file
89
src/modules/main_menu/components/menu-page.vue
Normal file
|
@ -0,0 +1,89 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--menu-page">
|
||||
<header class="mg-b-1">
|
||||
<template v-for="i in breadcrumbs">
|
||||
<a v-if="i !== item" href="#" @click="$emit('change-item', i, false)">{{ t(i.i18n_key, i.title, i) }}</a>
|
||||
<strong v-if="i === item">{{ t(i.i18n_key, i.title, i) }}</strong>
|
||||
<template v-if="i !== item">» </template>
|
||||
</template>
|
||||
</header>
|
||||
<section v-if="! context.currentProfile.live && item.profile_warning !== false" class="border-t pd-t-1 pd-b-2">
|
||||
<div class="c-background-accent c-text-overlay pd-1">
|
||||
<h3 class="ffz-i-attention">
|
||||
{{ t('setting.profiles.inactive', "This profile isn't active.") }}
|
||||
</h3>
|
||||
|
||||
{{ t(
|
||||
'setting.profiles.inactive.description',
|
||||
"This profile's rules don't match the current context and it therefore isn't currently active, so you " +
|
||||
"won't see changes you make here reflected on Twitch."
|
||||
) }}
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
v-if="item.description"
|
||||
class="border-t pd-y-1"
|
||||
v-html="t(item.desc_i18n_key || item.i18n_key + '.description', item.description, item)"
|
||||
/>
|
||||
</section>
|
||||
<template v-if="! item.contents">
|
||||
<ul class="border-t pd-y-1">
|
||||
<li class="pd-x-1" v-for="i in item.items">
|
||||
<a href="#" @click="$emit('change-item', i, false)">
|
||||
{{ t(i.i18n_key, i.title, i) }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<component
|
||||
v-for="i in item.contents"
|
||||
v-bind:is="i.component"
|
||||
ref="children"
|
||||
:context="context"
|
||||
:item="i"
|
||||
:key="i.full_key"
|
||||
@change-item="changeItem"
|
||||
@navigate="navigate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['item', 'context'],
|
||||
|
||||
computed: {
|
||||
breadcrumbs() {
|
||||
const out = [];
|
||||
let current = this.item;
|
||||
while(current) {
|
||||
out.unshift(current);
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
changeItem(item) {
|
||||
this.$emit('change-item', item);
|
||||
},
|
||||
|
||||
navigate(...args) {
|
||||
this.$emit('navigate', ...args);
|
||||
},
|
||||
|
||||
onBeforeChange(current, new_item) {
|
||||
for(const child of this.$refs.children)
|
||||
if ( child && child.onBeforeChange ) {
|
||||
const res = child.onBeforeChange(current, new_item);
|
||||
if ( res !== undefined )
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
165
src/modules/main_menu/components/menu-tree.vue
Normal file
165
src/modules/main_menu/components/menu-tree.vue
Normal file
|
@ -0,0 +1,165 @@
|
|||
<template lang="html">
|
||||
<ul
|
||||
v-if="modal"
|
||||
class="ffz--menu-tree"
|
||||
:role="[root ? 'group' : 'tree']"
|
||||
:tabindex="tabIndex"
|
||||
@keyup.up="prevItem"
|
||||
@keyup.down="nextItem"
|
||||
@keyup.left="prevLevel"
|
||||
@keyup.right="nextLevel"
|
||||
@keyup.*="expandAll"
|
||||
>
|
||||
<li
|
||||
v-for="item in modal"
|
||||
:key="item.full_key"
|
||||
:class="[currentItem === item ? 'active' : '']"
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
class="flex__item flex flex--nowrap align-items-center pd-y-05 pd-r-05"
|
||||
|
||||
role="treeitem"
|
||||
:aria-expanded="item.expanded"
|
||||
:aria-selected="currentItem === item"
|
||||
@click="clickItem(item)"
|
||||
>
|
||||
<span
|
||||
role="presentation"
|
||||
class="arrow"
|
||||
:class="[
|
||||
item.items ? '' : 'ffz--invisible',
|
||||
item.expanded ? 'ffz-i-down-dir' : 'ffz-i-right-dir'
|
||||
]"
|
||||
/>
|
||||
<span class="flex-grow-1">
|
||||
{{ t(item.i18n_key, item.title, item) }}
|
||||
</span>
|
||||
<span v-if="item.pill" class="pill">
|
||||
{{ item.pill_i18n_key ? t(item.pill_i18n_key, item.pill, item) : item.pill }}
|
||||
</span>
|
||||
</div>
|
||||
<menu-tree
|
||||
:root="item"
|
||||
:currentItem="currentItem"
|
||||
:modal="item.items"
|
||||
v-if="item.items && item.expanded"
|
||||
@change-item="i => $emit('change-item', i)"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
function findLastVisible(node) {
|
||||
if ( node.expanded && node.items )
|
||||
return findLastVisible(node.items[node.items.length - 1]);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
|
||||
function findNextVisible(node, modal) {
|
||||
const items = node.parent ? node.parent.items : modal,
|
||||
idx = items.indexOf(node);
|
||||
|
||||
if ( items[idx + 1] )
|
||||
return items[idx+1];
|
||||
|
||||
if ( node.parent )
|
||||
return findNextVisible(node.parent, modal);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
function recursiveExpand(node) {
|
||||
node.expanded = true;
|
||||
if ( node.items )
|
||||
for(const item of node.items)
|
||||
recursiveExpand(item);
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
props: ['root', 'modal', 'currentItem'],
|
||||
|
||||
computed: {
|
||||
tabIndex() {
|
||||
return this.root ? undefined : 0;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
clickItem(item) {
|
||||
if ( ! item.expanded )
|
||||
item.expanded = true;
|
||||
else if ( this.currentItem === item )
|
||||
item.expanded = false;
|
||||
|
||||
this.$emit('change-item', item);
|
||||
},
|
||||
|
||||
expandAll() {
|
||||
for(const item of this.modal)
|
||||
recursiveExpand(item);
|
||||
},
|
||||
|
||||
prevItem() {
|
||||
if ( this.root ) return;
|
||||
|
||||
const i = this.currentItem,
|
||||
items = i.parent ? i.parent.items : this.modal,
|
||||
idx = items.indexOf(i);
|
||||
|
||||
if ( idx > 0 )
|
||||
this.$emit('change-item', findLastVisible(items[idx-1]));
|
||||
|
||||
else if ( i.parent )
|
||||
this.$emit('change-item', i.parent);
|
||||
},
|
||||
|
||||
nextItem(e) {
|
||||
if ( this.root ) return;
|
||||
|
||||
const i = this.currentItem;
|
||||
let target;
|
||||
|
||||
if ( i.expanded && i.items )
|
||||
target = i.items[0];
|
||||
|
||||
else
|
||||
target = findNextVisible(i, this.modal);
|
||||
|
||||
if ( target )
|
||||
this.$emit('change-item', target);
|
||||
},
|
||||
|
||||
prevLevel() {
|
||||
if ( this.root ) return;
|
||||
|
||||
const i = this.currentItem;
|
||||
|
||||
if ( i.expanded && i.items )
|
||||
i.expanded = false;
|
||||
else if ( i.parent )
|
||||
this.$emit('change-item', i.parent);
|
||||
},
|
||||
|
||||
nextLevel() {
|
||||
if ( this.root ) return;
|
||||
|
||||
const i = this.currentItem;
|
||||
if ( i.expanded && i.items )
|
||||
this.$emit('change-item', i.items[0]);
|
||||
else
|
||||
i.expanded = true;
|
||||
|
||||
if ( event.ctrlKey )
|
||||
recursiveExpand(this.currentItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
205
src/modules/main_menu/components/profile-editor.vue
Normal file
205
src/modules/main_menu/components/profile-editor.vue
Normal file
|
@ -0,0 +1,205 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--profile-editor">
|
||||
<div class="flex align-items-center border-t pd-1">
|
||||
<div class="flex-grow-1"></div>
|
||||
<button
|
||||
class="tw-button tw-button--text"
|
||||
@click="save"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-floppy">
|
||||
{{ t('settings.profiles.save', 'Save') }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="mg-l-1 tw-button tw-button--text"
|
||||
:disabled="item.profile && context.profiles.length < 2"
|
||||
@click="del"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-trash">
|
||||
{{ t('setting.profiles.delete', 'Delete') }}
|
||||
</span>
|
||||
</button>
|
||||
<!--button class="mg-l-1 tw-button tw-button--text">
|
||||
<span class="tw-button__text ffz-i-download">
|
||||
{{ t('setting.profiles.export', 'Export') }}
|
||||
</span>
|
||||
</button-->
|
||||
</div>
|
||||
|
||||
<div class="ffz--menu-container border-t">
|
||||
<header>
|
||||
{{ t('settings.data_management.profiles.edit.general', 'General') }}
|
||||
</header>
|
||||
|
||||
<div class="ffz--widget flex flex--nowrap">
|
||||
<label for="ffz:editor:name">
|
||||
{{ t('settings.data_management.profiles.edit.name', 'Name') }}
|
||||
</label>
|
||||
|
||||
<input
|
||||
class="tw-input"
|
||||
ref="name"
|
||||
id="ffz:editor:name"
|
||||
v-model="name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="ffz--widget flex flex--nowrap">
|
||||
<label for="ffz:editor:description">
|
||||
{{ t('settings.data_management.profiles.edit.desc', 'Description') }}
|
||||
</label>
|
||||
|
||||
<textarea
|
||||
class="tw-input"
|
||||
ref="desc"
|
||||
id="ffz:editor:description"
|
||||
v-model="desc"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ffz--menu-container border-t">
|
||||
<header>
|
||||
{{ t('settings.data_management.profiles.edit.rules', 'Rules') }}
|
||||
</header>
|
||||
<section class="pd-b-1">
|
||||
{{ t(
|
||||
'settings.data_management.profiles.edit.rules.description',
|
||||
'Rules allows you to define a series of conditions under which this profile will be active.'
|
||||
) }}
|
||||
</section>
|
||||
|
||||
<filter-editor
|
||||
:filters="filters"
|
||||
:rules="rules"
|
||||
:context="test_context"
|
||||
@change="unsaved = true"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['item', 'context'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
old_name: null,
|
||||
old_desc: null,
|
||||
|
||||
name: null,
|
||||
desc: null,
|
||||
unsaved: false,
|
||||
|
||||
filters: null,
|
||||
rules: null,
|
||||
test_context: null
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.context.context.on('context_changed', this.updateContext, this);
|
||||
this.updateContext();
|
||||
this.revert();
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.context.context.off('context_changed', this.updateContext, this);
|
||||
},
|
||||
|
||||
|
||||
watch: {
|
||||
name() {
|
||||
if ( this.name !== this.old_name )
|
||||
this.unsaved = true;
|
||||
},
|
||||
|
||||
desc() {
|
||||
if ( this.desc !== this.old_desc )
|
||||
this.unsaved = true;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
revert() {
|
||||
const profile = this.item.profile;
|
||||
|
||||
this.old_name = this.name = profile ?
|
||||
profile.i18n_key ?
|
||||
this.t(profile.i18n_key, profile.title, profile) :
|
||||
profile.title :
|
||||
'Unnamed Profile',
|
||||
|
||||
this.old_desc = this.desc = profile ?
|
||||
profile.desc_i18n_key ?
|
||||
this.t(profile.desc_i18n_key, profile.description, profile) :
|
||||
profile.description :
|
||||
'';
|
||||
|
||||
this.rules = profile ? profile.context : {};
|
||||
this.unsaved = ! profile;
|
||||
},
|
||||
|
||||
del() {
|
||||
if ( this.item.profile || this.unsaved ) {
|
||||
if ( ! confirm(this.t(
|
||||
'settings.profiles.warn-delete',
|
||||
'Are you sure you wish to delete this profile? It cannot be undone.'
|
||||
)) )
|
||||
return
|
||||
|
||||
if ( this.item.profile )
|
||||
this.context.deleteProfile(this.item.profile);
|
||||
}
|
||||
|
||||
this.unsaved = false;
|
||||
this.$emit('navigate', 'data_management.profiles');
|
||||
},
|
||||
|
||||
save() {
|
||||
if ( ! this.item.profile ) {
|
||||
this.item.profile = this.context.createProfile({
|
||||
name: this.name,
|
||||
description: this.desc
|
||||
});
|
||||
|
||||
} else if ( this.unsaved ) {
|
||||
const changes = {
|
||||
name: this.name,
|
||||
description: this.desc
|
||||
};
|
||||
|
||||
// Disable i18n if required.
|
||||
if ( this.name !== this.old_name )
|
||||
changes.i18n_key = undefined;
|
||||
|
||||
if ( this.desc !== this.old_desc )
|
||||
changes.desc_i18n_key = undefined;
|
||||
|
||||
this.item.profile.update(changes);
|
||||
}
|
||||
|
||||
this.unsaved = false;
|
||||
this.$emit('navigate', 'data_management.profiles');
|
||||
},
|
||||
|
||||
updateContext() {
|
||||
this.test_context = this.context.context.context;
|
||||
},
|
||||
|
||||
onBeforeChange() {
|
||||
if ( this.unsaved )
|
||||
return confirm(
|
||||
this.t(
|
||||
'settings.warn-unsaved',
|
||||
'You have unsaved changes. Are you sure you want to leave the editor?'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
129
src/modules/main_menu/components/profile-manager.vue
Normal file
129
src/modules/main_menu/components/profile-manager.vue
Normal file
|
@ -0,0 +1,129 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--widget ffz--profile-manager border-t pd-y-1">
|
||||
<div class="c-background-accent c-text-overlay pd-1 mg-b-1">
|
||||
<h3 class="ffz-i-attention">
|
||||
This feature is not yet finished.
|
||||
</h3>
|
||||
|
||||
Creating and editing profiles is disabled until the rule editor is finished.
|
||||
</div>
|
||||
<div class="flex align-items-center pd-b-05">
|
||||
<div class="flex-grow-1">
|
||||
{{ t('setting.profiles.drag', 'Drag profiles to change their priority.') }}
|
||||
</div>
|
||||
<button class="mg-l-1 tw-button tw-button--text" disabled @notclick="edit()">
|
||||
<span class="tw-button__text ffz-i-plus">
|
||||
{{ t('setting.profiles.new', 'New Profile') }}
|
||||
</span>
|
||||
</button>
|
||||
<!--button class="mg-l-1 tw-button tw-button--text">
|
||||
<span class="tw-button__text ffz-i-upload">
|
||||
{{ t('setting.profiles.import', 'Import…') }}
|
||||
</span>
|
||||
</button-->
|
||||
</div>
|
||||
|
||||
<div ref="list" class="ffz--profile-list">
|
||||
<section
|
||||
v-for="p in context.profiles"
|
||||
:key="p.id"
|
||||
:data-profile="p.id"
|
||||
>
|
||||
<div
|
||||
class="ffz--profile elevation-1 c-background border pd-y-05 pd-r-1 mg-y-05 flex flex--nowrap"
|
||||
:class="{live: p.live}"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="flex flex-shrink-0 align-items-center handle pd-x-05 pd-t-1 pd-b-05">
|
||||
<span class="ffz-i-ellipsis-vert" />
|
||||
</div>
|
||||
|
||||
<div class="flex-grow-1">
|
||||
<h4>{{ t(p.i18n_key, p.title, p) }}</h4>
|
||||
<div v-if="p.description" class="description">
|
||||
{{ t(p.desc_i18n_key, p.description, p) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-shrink-0 align-items-center">
|
||||
<button class="tw-button tw-button--text" disabled @notclick="edit(p)">
|
||||
<span class="tw-button__text ffz-i-cog">
|
||||
{{ t('setting.profiles.configure', 'Configure') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-shrink-0 align-items-center border-l mg-l-1 pd-l-1">
|
||||
<div v-if="p.live" class="ffz--profile__icon ffz-i-ok tw-tooltip-wrapper">
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.profiles.active', 'This profile is active.') }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="! p.live" class="ffz--profile__icon ffz-i-cancel tw-tooltip-wrapper">
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.profiles.inactive', 'This profile is not active.') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
export default {
|
||||
props: ['item', 'context'],
|
||||
|
||||
methods: {
|
||||
edit(profile) {
|
||||
const item = {
|
||||
full_key: 'data_management.profiles.edit_profile',
|
||||
key: 'edit_profile',
|
||||
|
||||
profile_warning: false,
|
||||
|
||||
title: `Edit Profile`,
|
||||
i18n_key: 'setting.data_management.profiles.edit_profile',
|
||||
parent: this.item.parent,
|
||||
|
||||
contents: [{
|
||||
page: true,
|
||||
profile,
|
||||
component: 'profile-editor'
|
||||
}]
|
||||
};
|
||||
|
||||
item.contents[0].parent = item;
|
||||
this.$emit('change-item', item);
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this._sortable = Sortable.create(this.$refs.list, {
|
||||
draggable: 'section',
|
||||
filter: 'button',
|
||||
|
||||
onUpdate: (event) => {
|
||||
const id = event.item.dataset.profile,
|
||||
profile = this.context.profile_keys[id];
|
||||
|
||||
if ( profile )
|
||||
profile.move(event.newIndex);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
if ( this._sortable )
|
||||
this._sortable.destroy();
|
||||
|
||||
this._sortable = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
218
src/modules/main_menu/components/profile-selector.vue
Normal file
218
src/modules/main_menu/components/profile-selector.vue
Normal file
|
@ -0,0 +1,218 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--widget ffz--profile-selector">
|
||||
<div
|
||||
tabindex="0"
|
||||
class="tw-select"
|
||||
:class="{active: opened}"
|
||||
ref="button"
|
||||
@keyup.up.stop.prevent="focusShow"
|
||||
@keyup.left.stop.prevent="focusShow"
|
||||
@keyup.down.stop.prevent="focusShow"
|
||||
@keyup.right.stop.prevent="focusShow"
|
||||
@keyup.enter="focusShow"
|
||||
@keyup.space="focusShow"
|
||||
@click="togglePopup"
|
||||
>
|
||||
{{ t(context.currentProfile.i18n_key, context.currentProfile.title, context.currentProfile) }}
|
||||
</div>
|
||||
<div v-if="opened" v-on-clickaway="hide" class="tw-balloon block tw-balloon--lg tw-balloon--down tw-balloon--left">
|
||||
<div
|
||||
class="ffz--profile-list elevation-2 c-background-alt"
|
||||
@keyup.escape="focusHide"
|
||||
@focusin="focus"
|
||||
@focusout="blur"
|
||||
>
|
||||
<div class="scrollable-area border-b" data-simplebar>
|
||||
<div class="simplebar-scroll-content">
|
||||
<div class="simplebar-content" ref="popup">
|
||||
<div
|
||||
v-for="(p, idx) in context.profiles"
|
||||
tabindex="0"
|
||||
class="ffz--profile-row relative border-b pd-y-05 pd-r-3 pd-l-1"
|
||||
:class="{
|
||||
live: p.live,
|
||||
current: p === context.currentProfile
|
||||
}"
|
||||
@keydown.up.stop.prevent=""
|
||||
@keydown.down.stop.prevent=""
|
||||
@keydown.page-up.stop.prevent=""
|
||||
@keydown.page-down.stop.prevent=""
|
||||
@keyup.up.stop="prevItem"
|
||||
@keyup.down.stop="nextItem"
|
||||
@keyup.home="firstItem"
|
||||
@keyup.end="lastItem"
|
||||
@keyup.page-up.stop="prevPage"
|
||||
@keyup.page-down.stop="nextPage"
|
||||
@keyup.enter="changeProfile(p)"
|
||||
@click="changeProfile(p)"
|
||||
>
|
||||
<div
|
||||
v-if="p.live"
|
||||
class="tw-tooltip-wrapper ffz--profile-row__icon ffz-i-ok absolute"
|
||||
>
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.profiles.active', 'This profile is active.') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<h4>{{ t(p.i18n_key, p.title, p) }}</h4>
|
||||
<div v-if="p.description" class="description">
|
||||
{{ t(p.desc_i18n_key, p.description, p) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pd-y-05 pd-x-05 align-right">
|
||||
<button class="tw-button tw-button--text" @click="openConfigure">
|
||||
<span class="tw-button__text ffz-i-cog">
|
||||
{{ t('setting.profiles.configure', 'Configure') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { mixin as clickaway} from 'vue-clickaway';
|
||||
|
||||
const indexOf = Array.prototype.indexOf;
|
||||
|
||||
export default {
|
||||
mixins: [clickaway],
|
||||
props: ['context'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
opened: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
openConfigure() {
|
||||
this.hide();
|
||||
this.$emit('navigate', 'data_management.profiles');
|
||||
},
|
||||
|
||||
focus() {
|
||||
this._focused = true;
|
||||
},
|
||||
|
||||
blur() {
|
||||
this._focused = false;
|
||||
if ( ! this._blur_timer )
|
||||
this._blur_timer = setTimeout(() => {
|
||||
this._blur_timer = null;
|
||||
if ( ! this._focused && document.hasFocus() )
|
||||
this.hide();
|
||||
}, 10);
|
||||
},
|
||||
|
||||
|
||||
hide() {
|
||||
this.opened = false;
|
||||
},
|
||||
|
||||
show() {
|
||||
if ( ! this.opened )
|
||||
this.opened = true;
|
||||
},
|
||||
|
||||
togglePopup() {
|
||||
if ( this.opened )
|
||||
this.hide();
|
||||
else
|
||||
this.show();
|
||||
},
|
||||
|
||||
|
||||
focusHide() {
|
||||
this.hide();
|
||||
this.$refs.button.focus();
|
||||
},
|
||||
|
||||
focusShow() {
|
||||
this.show();
|
||||
this.$nextTick(() => this.$refs.popup.querySelector('.current').focus());
|
||||
},
|
||||
|
||||
prevItem(e) {
|
||||
const el = e.target.previousSibling;
|
||||
if ( el ) {
|
||||
this.scroll(el);
|
||||
el.focus();
|
||||
}
|
||||
},
|
||||
|
||||
nextItem(e) {
|
||||
const el = e.target.nextSibling;
|
||||
if ( el ) {
|
||||
this.scroll(el);
|
||||
el.focus();
|
||||
}
|
||||
},
|
||||
|
||||
firstItem() {
|
||||
const el = this.$refs.popup.firstElementChild;
|
||||
if ( el ) {
|
||||
this.scroll(el);
|
||||
el.focus();
|
||||
}
|
||||
},
|
||||
|
||||
prevPage(e) {
|
||||
this.select(indexOf.call(this.$refs.popup.children, e.target) - 5);
|
||||
},
|
||||
|
||||
nextPage(e) {
|
||||
this.select(indexOf.call(this.$refs.popup.children, e.target) + 5);
|
||||
},
|
||||
|
||||
select(idx) {
|
||||
const kids = this.$refs.popup.children,
|
||||
el = kids[idx <= 0 ? 0 : Math.min(idx, kids.length - 1)];
|
||||
|
||||
if ( el ) {
|
||||
this.scroll(el);
|
||||
el.focus();
|
||||
}
|
||||
},
|
||||
|
||||
lastItem() {
|
||||
const el = this.$refs.popup.lastElementChild;
|
||||
if ( el ) {
|
||||
this.scroll(el);
|
||||
el.focus();
|
||||
}
|
||||
},
|
||||
|
||||
scroll(el) {
|
||||
const scroller = this.$refs.popup.parentElement,
|
||||
|
||||
top = el.offsetTop,
|
||||
bottom = el.offsetHeight + top,
|
||||
|
||||
// We need to use the margin-bottom because of the scrollbar library.
|
||||
// In fact, the scrollbar library is why any of this function exists.
|
||||
scroll_top = scroller.scrollTop,
|
||||
scroll_bottom = scroller.offsetHeight + parseInt(scroller.style.marginBottom || 0, 10) + scroll_top;
|
||||
|
||||
if ( top < scroll_top )
|
||||
scroller.scrollBy(0, top - scroll_top);
|
||||
|
||||
else if ( bottom > scroll_bottom )
|
||||
scroller.scrollBy(0, bottom - scroll_bottom);
|
||||
},
|
||||
|
||||
changeProfile(profile) {
|
||||
this.context.currentProfile = profile;
|
||||
this.focusHide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
58
src/modules/main_menu/components/setting-check-box.vue
Normal file
58
src/modules/main_menu/components/setting-check-box.vue
Normal file
|
@ -0,0 +1,58 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--widget ffz--checkbox" :class="{inherits: isInherited, default: isDefault}">
|
||||
<div class="flex align-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="tw-checkbox__input"
|
||||
ref="control"
|
||||
:id="item.full_key"
|
||||
:checked="value"
|
||||
@change="onChange"
|
||||
>
|
||||
|
||||
<label class="tw-checkbox__label" :for="item.full_key">
|
||||
{{ t(item.i18n_key, item.title, item) }}
|
||||
</label>
|
||||
|
||||
<button
|
||||
v-if="source && source !== profile"
|
||||
class="mg-l-05 tw-button tw-button--text"
|
||||
@click="context.currentProfile = source"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-right-dir">
|
||||
{{ sourceDisplay }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button v-if="has_value" class="mg-l-05 tw-button tw-button--text tw-tooltip-wrapper" @click="clear">
|
||||
<span class="tw-button__text ffz-i-cancel"></span>
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.reset', 'Reset to Default') }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<section
|
||||
v-if="item.description"
|
||||
class="c-text-alt-2"
|
||||
style="padding-left:2.2rem"
|
||||
v-html="t(item.desc_i18n_key || item.i18n_key + '.description', item.description, item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import SettingMixin from '../setting-mixin';
|
||||
|
||||
export default {
|
||||
mixins: [SettingMixin],
|
||||
props: ['item', 'context'],
|
||||
|
||||
methods: {
|
||||
onChange() {
|
||||
this.set(this.$refs.control.checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
44
src/modules/main_menu/components/setting-hotkey.vue
Normal file
44
src/modules/main_menu/components/setting-hotkey.vue
Normal file
|
@ -0,0 +1,44 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--widget ffz--hotkey-input">
|
||||
<label
|
||||
:for="item.full_key"
|
||||
v-html="t(item.i18n_key, item.title, item)"
|
||||
/>
|
||||
<div class="relative">
|
||||
<div class="tw-input__icon-group tw-input__icon-group--right">
|
||||
<div class="tw-input__icon">
|
||||
<figure class="ffz-i-keyboard" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
type="text"
|
||||
class="mg-05 tw-input tw-input--icon-right"
|
||||
ref="display"
|
||||
:id="item.full_key"
|
||||
tabindex="0"
|
||||
@keyup="onKey"
|
||||
>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<section
|
||||
v-if="item.description"
|
||||
v-html="t(item.desc_i18n_key || item.i18n_key + '.description', item.description, item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['item', 'context'],
|
||||
|
||||
methods: {
|
||||
onKey(e) {
|
||||
const name = `${e.ctrlKey ? 'Ctrl-' : ''}${e.shiftKey ? 'Shift-' : ''}${e.altKey ? 'Alt-' : ''}${e.code}`;
|
||||
this.$refs.display.innerText = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
27
src/modules/main_menu/components/setting-radio-buttons.vue
Normal file
27
src/modules/main_menu/components/setting-radio-buttons.vue
Normal file
|
@ -0,0 +1,27 @@
|
|||
<template lang="html">
|
||||
<div class="atw-input">
|
||||
<header>
|
||||
{{ t(item.i18n_key, item.title, item) }}
|
||||
</header>
|
||||
<section
|
||||
v-if="item.description"
|
||||
class="c-text-alt-2"
|
||||
v-html="t(item.desc_i18n_key || item.i18n_key + '.description', item.description, item)"
|
||||
/>
|
||||
<div v-for="(i, idx) in data" class="mg-l-1">
|
||||
<input type="radio" :name="item.full_key" :id="item.full_key + idx" :value="i.value" class="tw-radio__input">
|
||||
<label :for="item.full_key + idx" class="pd-y-05 tw-radio__label">{{ t(i.i18n_key, i.title, i) }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import SettingMixin from '../setting-mixin';
|
||||
|
||||
export default {
|
||||
mixins: [SettingMixin],
|
||||
props: ['item', 'context']
|
||||
}
|
||||
|
||||
</script>
|
64
src/modules/main_menu/components/setting-select-box.vue
Normal file
64
src/modules/main_menu/components/setting-select-box.vue
Normal file
|
@ -0,0 +1,64 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--widget ffz--select-box" :class="{inherits: isInherited, default: isDefault}">
|
||||
<div class="flex align-items-center">
|
||||
<label :for="item.full_key">
|
||||
{{ t(item.i18n_key, item.title, item) }}
|
||||
</label>
|
||||
|
||||
<select
|
||||
class="mg-05 tw-select display-inline width-auto"
|
||||
ref="control"
|
||||
:id="item.full_key"
|
||||
@change="onChange"
|
||||
>
|
||||
<option v-for="i in data" :selected="i.value === value">
|
||||
{{ i.i18n_key ? t(i.i18n_key, i.title, i) : i.title }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
v-if="source && source !== profile"
|
||||
class="mg-l-05 tw-button tw-button--text"
|
||||
@click="context.currentProfile = source"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-right-dir">
|
||||
{{ sourceDisplay }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button v-if="has_value" class="mg-l-05 tw-button tw-button--text tw-tooltip-wrapper" @click="clear">
|
||||
<span class="tw-button__text ffz-i-cancel"></span>
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.reset', 'Reset to Default') }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section
|
||||
v-if="item.description"
|
||||
class="c-text-alt-2"
|
||||
v-html="t(item.desc_i18n_key || item.i18n_key + '.description', item.description, item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import SettingMixin from '../setting-mixin';
|
||||
|
||||
export default {
|
||||
mixins: [SettingMixin],
|
||||
props: ['item', 'context'],
|
||||
|
||||
methods: {
|
||||
onChange() {
|
||||
const idx = this.$refs.control.selectedIndex,
|
||||
raw_value = this.data[idx];
|
||||
|
||||
if ( raw_value )
|
||||
this.set(raw_value.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
59
src/modules/main_menu/components/setting-text-box.vue
Normal file
59
src/modules/main_menu/components/setting-text-box.vue
Normal file
|
@ -0,0 +1,59 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--widget ffz--text-box" :class="{inherits: isInherited, default: isDefault}">
|
||||
<div class="flex align-items-center">
|
||||
<label :for="item.full_key">
|
||||
{{ t(item.i18n_key, item.title, item) }}
|
||||
</label>
|
||||
|
||||
<input
|
||||
class="mg-05 tw-input display-inline width-auto"
|
||||
ref="control"
|
||||
:id="item.full_key"
|
||||
@change="onChange"
|
||||
:value="value"
|
||||
/>
|
||||
|
||||
<button
|
||||
v-if="source && source !== profile"
|
||||
class="mg-l-05 tw-button tw-button--text"
|
||||
@click="context.currentProfile = source"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-right-dir">
|
||||
{{ sourceDisplay }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button v-if="has_value" class="mg-l-05 tw-button tw-button--text tw-tooltip-wrapper" @click="clear">
|
||||
<span class="tw-button__text ffz-i-cancel"></span>
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.reset', 'Reset to Default') }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section
|
||||
v-if="item.description"
|
||||
class="c-text-alt-2"
|
||||
v-html="t(item.desc_i18n_key || item.i18n_key + '.description', item.description, item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import SettingMixin from '../setting-mixin';
|
||||
|
||||
export default {
|
||||
mixins: [SettingMixin],
|
||||
props: ['item', 'context'],
|
||||
|
||||
methods: {
|
||||
onChange() {
|
||||
const value = this.$refs.control.value;
|
||||
if ( value != null )
|
||||
this.set(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
551
src/modules/main_menu/index.js
Normal file
551
src/modules/main_menu/index.js
Normal file
|
@ -0,0 +1,551 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Menu Module
|
||||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
import {createElement as e} from 'utilities/dom';
|
||||
import {has, deep_copy} from 'utilities/object';
|
||||
|
||||
function format_term(term) {
|
||||
return term.replace(/<[^>]*>/g, '').toLocaleLowerCase();
|
||||
}
|
||||
|
||||
// TODO: Rewrite literally everything about the menu to use vue-router and further
|
||||
// separate the concept of navigation from visible pages.
|
||||
|
||||
export default class MainMenu extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.inject('settings');
|
||||
this.inject('i18n');
|
||||
this.inject('site');
|
||||
this.inject('vue');
|
||||
|
||||
//this.should_enable = true;
|
||||
|
||||
this._settings_tree = null;
|
||||
this._settings_count = 0;
|
||||
|
||||
this._menu = null;
|
||||
this._visible = true;
|
||||
this._maximized = false;
|
||||
|
||||
|
||||
this.settings.addUI('profiles', {
|
||||
path: 'Data Management @{"sort": 1000, "profile_warning": false} > Profiles @{"profile_warning": false}',
|
||||
component: 'profile-manager'
|
||||
});
|
||||
|
||||
this.settings.addUI('home', {
|
||||
path: 'Home @{"sort": -1000, "profile_warning": false}',
|
||||
component: 'home-page'
|
||||
});
|
||||
|
||||
this.settings.addUI('feedback', {
|
||||
path: 'Home > Feedback',
|
||||
component: 'feedback-page'
|
||||
});
|
||||
|
||||
this.settings.addUI('changelog', {
|
||||
path: 'Home > Changelog',
|
||||
component: 'changelog'
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
async onLoad() {
|
||||
this.vue.component(
|
||||
(await import(/* webpackChunkName: "main-menu" */ './components.js')).default
|
||||
);
|
||||
}
|
||||
|
||||
get maximized() {
|
||||
return this._maximized;
|
||||
}
|
||||
|
||||
set maximized(val) {
|
||||
val = Boolean(val);
|
||||
if ( val === this._maximized )
|
||||
return;
|
||||
|
||||
if ( this.enabled )
|
||||
this.toggleSize();
|
||||
}
|
||||
|
||||
get visible() {
|
||||
return this._visible;
|
||||
}
|
||||
|
||||
set visible(val) {
|
||||
val = Boolean(val);
|
||||
if ( val === this._visible )
|
||||
return;
|
||||
|
||||
if ( this.enabled )
|
||||
this.toggleVisible();
|
||||
}
|
||||
|
||||
|
||||
async onEnable(event) {
|
||||
await this.site.awaitElement('.twilight-root');
|
||||
|
||||
this.on('site.menu_button:clicked', this.toggleVisible);
|
||||
if ( this._visible ) {
|
||||
this._visible = false;
|
||||
this.toggleVisible(event);
|
||||
}
|
||||
}
|
||||
|
||||
onDisable() {
|
||||
if ( this._visible ) {
|
||||
this.toggleVisible();
|
||||
this._visible = true;
|
||||
}
|
||||
|
||||
this.off('site.menu_button:clicked', this.toggleVisible);
|
||||
}
|
||||
|
||||
toggleVisible(event) {
|
||||
if ( event && event.button !== 0 )
|
||||
return;
|
||||
|
||||
const maximized = this._maximized,
|
||||
visible = this._visible = !this._visible,
|
||||
main = document.querySelector(maximized ? '.twilight-main' : '.twilight-root > .full-height');
|
||||
|
||||
if ( ! visible ) {
|
||||
if ( maximized )
|
||||
main.classList.remove('ffz-has-menu');
|
||||
|
||||
if ( this._menu ) {
|
||||
main.removeChild(this._menu);
|
||||
this._vue.$destroy();
|
||||
this._menu = this._vue = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! this._menu )
|
||||
this.createMenu();
|
||||
|
||||
if ( maximized )
|
||||
main.classList.add('ffz-has-menu');
|
||||
|
||||
main.appendChild(this._menu);
|
||||
}
|
||||
|
||||
toggleSize(event) {
|
||||
if ( ! this._visible || event && event.button !== 0 )
|
||||
return;
|
||||
|
||||
const maximized = this._maximized = !this._maximized,
|
||||
main = document.querySelector(maximized ? '.twilight-main' : '.twilight-root > .full-height'),
|
||||
old_main = this._menu.parentElement;
|
||||
|
||||
if ( maximized )
|
||||
main.classList.add('ffz-has-menu');
|
||||
else
|
||||
old_main.classList.remove('ffz-has-menu');
|
||||
|
||||
old_main.removeChild(this._menu);
|
||||
main.appendChild(this._menu);
|
||||
|
||||
this._vue.$children[0].maximized = maximized;
|
||||
}
|
||||
|
||||
|
||||
rebuildSettingsTree() {
|
||||
this._settings_tree = {};
|
||||
this._settings_count = 0;
|
||||
|
||||
for(const [key, def] of this.settings.definitions)
|
||||
this._addDefinitionToTree(key, def);
|
||||
|
||||
for(const [key, def] of this.settings.ui_structures)
|
||||
this._addDefinitionToTree(key, def);
|
||||
}
|
||||
|
||||
|
||||
_addDefinitionToTree(key, def) {
|
||||
if ( ! def.ui || ! this._settings_tree )
|
||||
return;
|
||||
|
||||
if ( ! def.ui.path_tokens ) {
|
||||
if ( def.ui.path )
|
||||
def.ui.path_tokens = parse_path(def.ui.path);
|
||||
else
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! def.ui || ! def.ui.path_tokens || ! this._settings_tree )
|
||||
return;
|
||||
|
||||
const tree = this._settings_tree,
|
||||
tokens = def.ui.path_tokens,
|
||||
len = tokens.length;
|
||||
|
||||
let prefix = null,
|
||||
token;
|
||||
|
||||
// Create and/or update all the necessary structure elements for
|
||||
// this node in the settings tree.
|
||||
for(let i=0; i < len; i++) {
|
||||
const raw_token = tokens[i],
|
||||
key = prefix ? `${prefix}.${raw_token.key}` : raw_token.key;
|
||||
|
||||
token = tree[key];
|
||||
if ( ! token )
|
||||
token = tree[key] = {
|
||||
full_key: key,
|
||||
sort: 0,
|
||||
parent: prefix,
|
||||
expanded: prefix === null,
|
||||
i18n_key: `setting.${key}`,
|
||||
desc_i18n_key: `setting.${key}.description`
|
||||
};
|
||||
|
||||
Object.assign(token, raw_token);
|
||||
prefix = key;
|
||||
}
|
||||
|
||||
// Add this setting to the tree.
|
||||
token.settings = token.settings || [];
|
||||
token.settings.push([key, def]);
|
||||
this._settings_count++;
|
||||
}
|
||||
|
||||
|
||||
getSettingsTree() {
|
||||
const started = performance.now();
|
||||
|
||||
if ( ! this._settings_tree )
|
||||
this.rebuildSettingsTree();
|
||||
|
||||
const tree = this._settings_tree,
|
||||
|
||||
root = {},
|
||||
copies = {},
|
||||
|
||||
needs_sort = new Set,
|
||||
needs_component = new Set,
|
||||
|
||||
have_locale = this.i18n.locale !== 'en';
|
||||
|
||||
|
||||
for(const key in tree) {
|
||||
if ( ! has(tree, key) )
|
||||
continue;
|
||||
|
||||
const token = copies[key] = copies[key] || Object.assign({}, tree[key]),
|
||||
p_key = token.parent,
|
||||
parent = p_key ?
|
||||
(copies[p_key] = copies[p_key] || Object.assign({}, tree[p_key])) :
|
||||
root;
|
||||
|
||||
token.parent = p_key ? parent : null;
|
||||
token.page = token.page || parent.page;
|
||||
|
||||
if ( token.page && ! token.component )
|
||||
needs_component.add(token);
|
||||
|
||||
if ( token.settings ) {
|
||||
const list = token.contents = token.contents || [];
|
||||
|
||||
for(const [setting_key, def] of token.settings)
|
||||
if ( def.ui ) { //} && def.ui.title ) {
|
||||
const i18n_key = `${token.i18n_key}.${def.ui.key}`
|
||||
const tok = Object.assign({
|
||||
i18n_key,
|
||||
desc_i18n_key: `${i18n_key}.description`,
|
||||
sort: 0,
|
||||
title: setting_key
|
||||
}, def.ui, {
|
||||
full_key: `setting:${setting_key}`,
|
||||
setting: setting_key,
|
||||
path_tokens: undefined,
|
||||
parent: token
|
||||
});
|
||||
|
||||
if ( def.default && ! tok.default ) {
|
||||
const def_type = typeof def.default;
|
||||
if ( def_type === 'object' ) {
|
||||
// TODO: Better way to deep copy this object.
|
||||
tok.default = JSON.parse(JSON.stringify(def.default));
|
||||
} else
|
||||
tok.default = def.default;
|
||||
}
|
||||
|
||||
const terms = [
|
||||
setting_key,
|
||||
this.i18n.t(tok.i18n_key, tok.title, tok, true)
|
||||
];
|
||||
|
||||
if ( have_locale && this.i18n.has(tok.i18n_key) )
|
||||
terms.push(this.i18n.t(tok.i18n_key, tok.title, tok));
|
||||
|
||||
if ( tok.description ) {
|
||||
terms.push(this.i18n.t(tok.desc_i18n_key, tok.description, tok, true));
|
||||
|
||||
if ( have_locale && this.i18n.has(tok.desc_i18n_key) )
|
||||
terms.push(this.i18n.t(tok.desc_i18n_key, tok.description, tok));
|
||||
}
|
||||
|
||||
tok.search_terms = terms.map(format_term).join('\n');
|
||||
|
||||
list.push(tok);
|
||||
}
|
||||
|
||||
token.settings = undefined;
|
||||
if ( list.length > 1 )
|
||||
needs_sort.add(list);
|
||||
}
|
||||
|
||||
if ( ! token.search_terms ) {
|
||||
const formatted = this.i18n.t(token.i18n_key, token.title, token, true);
|
||||
let terms = [token.key];
|
||||
|
||||
if ( formatted && formatted.localeCompare(token.key, undefined, {sensitivity: 'base'}) )
|
||||
terms.push(formatted);
|
||||
|
||||
if ( have_locale && this.i18n.has(token.i18n_key) )
|
||||
terms.push(this.i18n.t(token.i18n_key, token.title, token));
|
||||
|
||||
if ( token.description ) {
|
||||
terms.push(this.i18n.t(token.desc_i18n_key, token.description, token, true));
|
||||
|
||||
if ( have_locale && this.i18n.has(token.desc_i18n_key) )
|
||||
terms.push(this.i18n.t(token.desc_i18n_key, token.description, token));
|
||||
}
|
||||
|
||||
terms = terms.map(format_term);
|
||||
|
||||
for(const lk of ['tabs', 'contents', 'items'])
|
||||
if ( token[lk] )
|
||||
for(const tok of token[lk] )
|
||||
if ( tok.search_terms )
|
||||
terms.push(tok.search_terms);
|
||||
|
||||
terms = token.search_terms = terms.join('\n');
|
||||
|
||||
let p = parent;
|
||||
while(p && p.search_terms) {
|
||||
p.search_terms += '\n' + terms;
|
||||
p = p.parent;
|
||||
}
|
||||
}
|
||||
|
||||
const lk = token.tab ? 'tabs' : token.page ? 'contents' : 'items',
|
||||
list = parent[lk] = parent[lk] || [];
|
||||
|
||||
list.push(token);
|
||||
if ( list.length > 1 )
|
||||
needs_sort.add(list);
|
||||
}
|
||||
|
||||
for(const token of needs_component) {
|
||||
token.component = token.tabs ? 'tab-container' :
|
||||
token.contents ? 'menu-container' :
|
||||
'setting-check-box';
|
||||
}
|
||||
|
||||
for(const list of needs_sort)
|
||||
list.sort((a, b) => {
|
||||
if ( a.sort < b.sort ) return -1;
|
||||
if ( a.sort > b.sort ) return 1;
|
||||
|
||||
return a.key.localeCompare(b.key);
|
||||
});
|
||||
|
||||
this.log.info(`Built Tree in ${(performance.now() - started).toFixed(5)}ms with ${Object.keys(tree).length} structure nodes and ${this._settings_count} settings nodes.`);
|
||||
const items = root.items || [];
|
||||
items.keys = copies;
|
||||
return items;
|
||||
}
|
||||
|
||||
|
||||
getProfiles(context) {
|
||||
const profiles = [],
|
||||
keys = {};
|
||||
|
||||
context = context || this.settings.main_context;
|
||||
|
||||
for(const profile of this.settings.__profiles)
|
||||
profiles.push(keys[profile.id] = this.getProfileProxy(profile, context));
|
||||
|
||||
return [profiles, keys];
|
||||
}
|
||||
|
||||
|
||||
getProfileProxy(profile, context) {
|
||||
return {
|
||||
id: profile.id,
|
||||
|
||||
order: context.manager.__profiles.indexOf(profile),
|
||||
live: context.__profiles.includes(profile),
|
||||
|
||||
title: profile.name,
|
||||
i18n_key: profile.i18n_key,
|
||||
|
||||
description: profile.description,
|
||||
desc_i18n_key: profile.desc_i18n_key || profile.i18n_key && `${profile.i18n_key}.description`,
|
||||
|
||||
move: idx => context.manager.moveProfile(profile.id, idx),
|
||||
save: () => profile.save(),
|
||||
update: data => {
|
||||
profile.data = data
|
||||
profile.save()
|
||||
},
|
||||
|
||||
context: deep_copy(profile.context),
|
||||
|
||||
get: key => profile.get(key),
|
||||
set: (key, val) => profile.set(key, val),
|
||||
delete: key => profile.delete(key),
|
||||
has: key => profile.has(key),
|
||||
|
||||
on: (...args) => profile.on(...args),
|
||||
off: (...args) => profile.off(...args)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
getContext() {
|
||||
const t = this,
|
||||
Vue = this.vue.Vue,
|
||||
settings = this.settings,
|
||||
context = settings.main_context,
|
||||
[profiles, profile_keys] = this.getProfiles(),
|
||||
|
||||
_c = {
|
||||
profiles,
|
||||
profile_keys,
|
||||
currentProfile: profile_keys[0],
|
||||
|
||||
createProfile: data => {
|
||||
const profile = settings.createProfile(data);
|
||||
return t.getProfileProxy(profile, context);
|
||||
},
|
||||
|
||||
deleteProfile: profile => settings.deleteProfile(profile),
|
||||
|
||||
context: {
|
||||
_users: 0,
|
||||
|
||||
profiles: context.__profiles.map(profile => profile.id),
|
||||
get: key => context.get(key),
|
||||
uses: key => context.uses(key),
|
||||
|
||||
on: (...args) => context.on(...args),
|
||||
off: (...args) => context.off(...args),
|
||||
|
||||
order: id => context.order.indexOf(id),
|
||||
context: deep_copy(context.context),
|
||||
|
||||
_update_profiles(changed) {
|
||||
const new_list = [],
|
||||
profiles = context.manager.__profiles;
|
||||
for(let i=0; i < profiles.length; i++) {
|
||||
const profile = profile_keys[profiles[i].id];
|
||||
profile.order = i;
|
||||
new_list.push(profile);
|
||||
}
|
||||
|
||||
Vue.set(_c, 'profiles', new_list);
|
||||
|
||||
if ( changed && changed.id === _c.currentProfile.id )
|
||||
_c.currentProfile = profile_keys[changed.id];
|
||||
},
|
||||
|
||||
_profile_created(profile) {
|
||||
Vue.set(profile_keys, profile.id, t.getProfileProxy(profile, context));
|
||||
this._update_profiles()
|
||||
},
|
||||
|
||||
_profile_changed(profile) {
|
||||
Vue.set(profile_keys, profile.id, t.getProfileProxy(profile, context));
|
||||
this._update_profiles(profile);
|
||||
},
|
||||
|
||||
_profile_deleted(profile) {
|
||||
Vue.delete(profile_keys, profile.id);
|
||||
this._update_profiles();
|
||||
|
||||
if ( _c.currentProfile.id === profile.id )
|
||||
_c.currentProfile = profile_keys[0]
|
||||
},
|
||||
|
||||
_context_changed() {
|
||||
this.context = deep_copy(context.context);
|
||||
const ids = this.profiles = context.__profiles.map(profile => profile.id);
|
||||
for(const id in profiles) {
|
||||
const profile = profiles[id];
|
||||
profile.live = this.profiles.includes(profile.id);
|
||||
}
|
||||
},
|
||||
|
||||
_add_user() {
|
||||
this._users++;
|
||||
if ( this._users === 1 ) {
|
||||
settings.on(':profile-created', this._profile_created, this);
|
||||
settings.on(':profile-changed', this._profile_changed, this);
|
||||
settings.on(':profile-deleted', this._profile_deleted, this);
|
||||
settings.on(':profiles-reordered', this._update_profiles, this);
|
||||
context.on('context_changed', this._context_changed, this);
|
||||
context.on('profiles_changed', this._context_changed, this);
|
||||
this.profiles = context.__profiles.map(profile => profile.id);
|
||||
}
|
||||
},
|
||||
|
||||
_remove_user() {
|
||||
this._users--;
|
||||
if ( this._users === 0 ) {
|
||||
settings.off(':profile-created', this._profile_created, this);
|
||||
settings.off(':profile-changed', this._profile_changed, this);
|
||||
settings.off(':profile-deleted', this._profile_deleted, this);
|
||||
settings.off(':profiles-reordered', this._update_profiles, this);
|
||||
context.off('context_changed', this._context_changed, this);
|
||||
context.off('profiles_changed', this._context_changed, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return _c;
|
||||
}
|
||||
|
||||
getData() {
|
||||
const settings = this.getSettingsTree(),
|
||||
context = this.getContext();
|
||||
|
||||
return {
|
||||
context,
|
||||
|
||||
nav: settings,
|
||||
currentItem: settings.keys['home'], // settings[0],
|
||||
nav_keys: settings.keys,
|
||||
|
||||
maximized: this._maximized,
|
||||
resize: e => this.toggleSize(e),
|
||||
close: e => this.toggleVisible(e),
|
||||
version: window.FrankerFaceZ.version_info
|
||||
}
|
||||
}
|
||||
|
||||
createMenu() {
|
||||
if ( this._menu )
|
||||
return;
|
||||
|
||||
this._vue = new this.vue.Vue({
|
||||
el: e('div'),
|
||||
render: h => h('main-menu', this.getData())
|
||||
});
|
||||
|
||||
this._menu = this._vue.$el;
|
||||
}
|
||||
}
|
||||
|
||||
MainMenu.requires = ['site.menu_button'];
|
178
src/modules/main_menu/setting-mixin.js
Normal file
178
src/modules/main_menu/setting-mixin.js
Normal file
|
@ -0,0 +1,178 @@
|
|||
'use strict';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
value: undefined,
|
||||
has_value: false,
|
||||
profile: null,
|
||||
|
||||
source: null,
|
||||
source_value: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
const ctx = this.context.context,
|
||||
setting = this.item.setting;
|
||||
|
||||
ctx._add_user();
|
||||
|
||||
this._update_profile();
|
||||
this._uses_changed(ctx.uses(setting));
|
||||
|
||||
ctx.on(`uses_changed:${setting}`, this._uses_changed, this);
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
const ctx = this.context.context,
|
||||
setting = this.item.setting;
|
||||
|
||||
if ( this.profile )
|
||||
this.profile.off('changed', this._setting_changed, this);
|
||||
|
||||
if ( this.source )
|
||||
this.source.off('changed', this._source_setting_changed, this);
|
||||
|
||||
ctx.off(`uses_changed:${setting}`, this._uses_changed, this);
|
||||
|
||||
this.value = undefined;
|
||||
this.has_value = false;
|
||||
this.profile = null;
|
||||
|
||||
this.source_value = undefined;
|
||||
this.source = null;
|
||||
|
||||
ctx._remove_user();
|
||||
},
|
||||
|
||||
watch: {
|
||||
'context.currentProfile'() {
|
||||
this._update_profile();
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
data() {
|
||||
const data = this.item.data;
|
||||
if ( typeof data === 'function' )
|
||||
return data(this.profile, this.value);
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
default_value() {
|
||||
if ( typeof this.item.default === 'function' )
|
||||
return this.item.default(this.context.context);
|
||||
|
||||
return this.item.default;
|
||||
},
|
||||
|
||||
isInherited() {
|
||||
return ! this.has_value && this.source && this.sourceOrder > this.profileOrder;
|
||||
},
|
||||
|
||||
isDefault() {
|
||||
return ! this.has_value && ! this.source
|
||||
},
|
||||
|
||||
isOverridden() {
|
||||
return this.source && this.sourceOrder < this.profileOrder;
|
||||
},
|
||||
|
||||
sourceOrder() {
|
||||
return this.source ? this.source.order : Infinity
|
||||
},
|
||||
|
||||
profileOrder() {
|
||||
return this.profile ? this.profile.order : Infinity
|
||||
},
|
||||
|
||||
sourceTitle() {
|
||||
if ( this.source )
|
||||
return this.source.i18n_key ?
|
||||
this.t(this.source.i18n_key, this.source.title, this.source) :
|
||||
this.source.title;
|
||||
},
|
||||
|
||||
sourceDisplay() {
|
||||
const opts = {
|
||||
title: this.sourceTitle
|
||||
};
|
||||
|
||||
if ( this.isInherited )
|
||||
return this.t('setting.inherited-from', 'Inherited From: %{title}', opts);
|
||||
else if ( this.isOverridden )
|
||||
return this.t('setting.overridden-by', 'Overridden By: %{title}', opts);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
_update_profile() {
|
||||
if ( this.profile )
|
||||
this.profile.off('changed', this._setting_changed, this);
|
||||
|
||||
const profile = this.profile = this.context.currentProfile,
|
||||
setting = this.item.setting;
|
||||
|
||||
profile.on('changed', this._setting_changed, this);
|
||||
|
||||
this.has_value = profile.has(setting);
|
||||
this.value = this.has_value ?
|
||||
profile.get(setting) :
|
||||
this.isInherited ?
|
||||
this.source_value :
|
||||
this.default_value;
|
||||
},
|
||||
|
||||
_setting_changed(key, value, deleted) {
|
||||
if ( key !== this.item.setting )
|
||||
return;
|
||||
|
||||
this.has_value = deleted !== true;
|
||||
this.value = this.has_value ?
|
||||
value :
|
||||
this.isInherited ?
|
||||
this.source_value :
|
||||
this.default_value;
|
||||
},
|
||||
|
||||
_source_setting_changed(key, value, deleted) {
|
||||
if ( key !== this.item.setting )
|
||||
return;
|
||||
|
||||
this.source_value = value;
|
||||
if ( this.isInherited )
|
||||
this.value = deleted ? this.default_value : value;
|
||||
},
|
||||
|
||||
_uses_changed(uses) {
|
||||
if ( this.source )
|
||||
this.source.off('changed', this._source_setting_changed, this);
|
||||
|
||||
const source = this.source = this.context.profile_keys[uses],
|
||||
setting = this.item.setting;
|
||||
|
||||
if ( source ) {
|
||||
source.on('changed', this._source_setting_changed, this);
|
||||
this.source_value = source.get(setting);
|
||||
|
||||
} else
|
||||
this.source_value = undefined;
|
||||
|
||||
if ( ! this.has_value )
|
||||
this.value = this.isInherited ? this.source_value : this.default_value;
|
||||
},
|
||||
|
||||
set(value) {
|
||||
if ( this.item.process )
|
||||
value = this.item.process(value);
|
||||
|
||||
this.profile.set(this.item.setting, value);
|
||||
},
|
||||
|
||||
clear() {
|
||||
this.profile.delete(this.item.setting);
|
||||
}
|
||||
}
|
||||
}
|
342
src/modules/metadata.js
Normal file
342
src/modules/metadata.js
Normal file
|
@ -0,0 +1,342 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Channel Metadata
|
||||
// ============================================================================
|
||||
|
||||
import {createElement as e} from 'utilities/dom';
|
||||
import {has, get, maybe_call} from 'utilities/object';
|
||||
|
||||
import {duration_to_string} from 'utilities/time';
|
||||
|
||||
import Tooltip from 'utilities/tooltip';
|
||||
import Module from 'utilities/module';
|
||||
|
||||
export default class Metadata extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.inject('settings');
|
||||
this.inject('i18n');
|
||||
|
||||
this.should_enable = true;
|
||||
this.definitions = {};
|
||||
|
||||
this.settings.add('metadata.player-stats', {
|
||||
default: false,
|
||||
|
||||
ui: {
|
||||
path: 'Channel > Metadata >> Player',
|
||||
title: 'Playback Statistics',
|
||||
description: 'Show the current stream delay, with playback rate and dropped frames in the tooltip.',
|
||||
component: 'setting-check-box'
|
||||
},
|
||||
|
||||
changed: () => this.updateMetadata('player-stats')
|
||||
});
|
||||
|
||||
this.settings.add('metadata.uptime', {
|
||||
default: 1,
|
||||
|
||||
ui: {
|
||||
path: 'Channel > Metadata >> Player',
|
||||
title: 'Stream Uptime',
|
||||
|
||||
component: 'setting-select-box',
|
||||
|
||||
data: [
|
||||
{value: 0, title: 'Disabled'},
|
||||
{value: 1, title: 'Enabled'},
|
||||
{value: 2, title: 'Enabled (with Seconds)'}
|
||||
]
|
||||
},
|
||||
|
||||
changed: () => this.updateMetadata('uptime')
|
||||
});
|
||||
|
||||
|
||||
this.definitions.uptime = {
|
||||
refresh() { return this.settings.get('metadata.uptime') > 0 },
|
||||
|
||||
setup() {
|
||||
const socket = this.resolve('socket'),
|
||||
query = this.resolve('site.apollo').getQuery('ChannelPage_ChannelInfoBar_User'),
|
||||
result = query.lastResult,
|
||||
created_at = get('data.user.stream.createdAt', result);
|
||||
|
||||
if ( created_at === undefined && ! query._ffz_refetched ) {
|
||||
query._ffz_refetched = true;
|
||||
query.refetch();
|
||||
return {};
|
||||
}
|
||||
|
||||
if ( ! created_at )
|
||||
return {};
|
||||
|
||||
const created = new Date(created_at),
|
||||
now = Date.now() - socket._time_drift;
|
||||
|
||||
return {
|
||||
created,
|
||||
uptime: created ? Math.floor((now - created.getTime()) / 1000) : -1
|
||||
}
|
||||
},
|
||||
|
||||
order: 2,
|
||||
icon: 'ffz-i-clock',
|
||||
|
||||
label(data) {
|
||||
const setting = this.settings.get('metadata.uptime');
|
||||
if ( ! setting || ! data.created )
|
||||
return null;
|
||||
|
||||
return duration_to_string(data.uptime, false, false, false, setting !== 2);
|
||||
},
|
||||
|
||||
tooltip(data) {
|
||||
if ( ! data.created )
|
||||
return null;
|
||||
|
||||
return `${this.i18n.t(
|
||||
'metadata.uptime.tooltip',
|
||||
'Stream Uptime'
|
||||
)}<div class="pd-t-05">${this.i18n.t(
|
||||
'metadata.uptime.since',
|
||||
'(since %{since})',
|
||||
{since: data.created.toLocaleString()}
|
||||
)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
this.definitions['player-stats'] = {
|
||||
refresh() {
|
||||
return this.settings.get('metadata.player-stats')
|
||||
},
|
||||
|
||||
setup() {
|
||||
const Player = this.resolve('site.player'),
|
||||
socket = this.resolve('socket'),
|
||||
player = Player.current,
|
||||
stats = player && player.getVideoInfo();
|
||||
|
||||
if ( ! stats )
|
||||
return {stats};
|
||||
|
||||
let delay = stats.hls_latency_broadcaster / 1000,
|
||||
drift = 0;
|
||||
|
||||
if ( socket && socket.connected )
|
||||
drift = socket._time_drift;
|
||||
|
||||
return {
|
||||
stats,
|
||||
drift,
|
||||
delay,
|
||||
old: delay > 180
|
||||
}
|
||||
},
|
||||
|
||||
order: 3,
|
||||
icon: 'ffz-i-gauge',
|
||||
|
||||
label(data) {
|
||||
if ( ! this.settings.get('metadata.player-stats') || ! data.delay )
|
||||
return null;
|
||||
|
||||
const delayed = data.drift > 5000 ? '(!) ' : '';
|
||||
|
||||
if ( data.old )
|
||||
return `${delayed}${data.delay.toFixed(2)}s old`;
|
||||
else
|
||||
return `${delayed}${data.delay.toFixed(2)}s`;
|
||||
},
|
||||
|
||||
color(data) {
|
||||
const setting = this.settings.get('some.thing');
|
||||
if ( setting == null || ! data.delay || data.old )
|
||||
return;
|
||||
|
||||
if ( data.delay > (setting * 2) )
|
||||
return '#ec1313';
|
||||
|
||||
else if ( data.delay > setting )
|
||||
return '#fc7835';
|
||||
},
|
||||
|
||||
tooltip(data) {
|
||||
const delayed = data.drift > 5000 ?
|
||||
`${this.i18n.t(
|
||||
'metadata.player-stats.delay-warning',
|
||||
'Your local clock seems to be off by roughly %{count} seconds, which could make this inaccurate.',
|
||||
Math.round(data.drift / 10) / 100
|
||||
)}<hr>` :
|
||||
'';
|
||||
|
||||
if ( ! data.stats || ! data.delay )
|
||||
return delayed + this.i18n.t('metadata.player-stats.latency-tip', 'Stream Latency');
|
||||
|
||||
const stats = data.stats,
|
||||
video_info = this.i18n.t(
|
||||
'metadata.player-stats.video-info',
|
||||
'Video: %{vid_width}x%{vid_height}p%{current_fps}\nPlayback Rate: %{current_bitrate|number} Kbps\nDropped Frames:%{dropped_frames|number}',
|
||||
stats
|
||||
);
|
||||
|
||||
if ( data.old )
|
||||
return `${delayed}${this.i18n.t(
|
||||
'metadata.player-stats.video-tip',
|
||||
'Video Information'
|
||||
)}<div class="pd-t-05">${this.i18n.t(
|
||||
'metadata.player-stats.broadcast-ago',
|
||||
'Broadcast %{count}s Ago',
|
||||
data.delay
|
||||
)}</div><div class="pd-t-05">${video_info}</div>`;
|
||||
|
||||
return `${delayed}${this.i18n.t(
|
||||
'metadata.player-stats.latency-tip',
|
||||
'Stream Latency'
|
||||
)}<div class="pd-t-05">${video_info}</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
get keys() {
|
||||
return Object.keys(this.definitions);
|
||||
}
|
||||
|
||||
|
||||
async getData(key) {
|
||||
const def = this.definitions[key];
|
||||
if ( ! def )
|
||||
return {label: null};
|
||||
|
||||
return {
|
||||
icon: maybe_call(def.icon),
|
||||
label: maybe_call(def.label),
|
||||
refresh: maybe_call(def.refresh)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
updateMetadata(keys) {
|
||||
const bar = this.resolve('site.channel_bar');
|
||||
if ( bar ) {
|
||||
for(const inst of bar.ChannelBar.instances)
|
||||
bar.updateMetadata(inst, keys);
|
||||
|
||||
for(const inst of bar.HostBar.instances)
|
||||
bar.updateMetadata(inst, keys);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async render(key, data, container, timers, refresh_fn) {
|
||||
if ( timers[key] )
|
||||
clearTimeout(timers[key]);
|
||||
|
||||
let el = container.querySelector(`.ffz-stat[data-key="${key}"]`);
|
||||
|
||||
const def = this.definitions[key],
|
||||
destroy = () => {
|
||||
if ( el ) {
|
||||
if ( el.tooltip )
|
||||
el.tooltip.destroy();
|
||||
|
||||
if ( el.popper )
|
||||
el.popper.destroy();
|
||||
|
||||
el.tooltip = el.popper = null;
|
||||
el.parentElement.removeChild(el);
|
||||
}
|
||||
};
|
||||
|
||||
if ( ! def )
|
||||
return destroy();
|
||||
|
||||
try {
|
||||
// Process the data if a setup method is defined.
|
||||
if ( def.setup )
|
||||
data = await def.setup.call(this, data);
|
||||
|
||||
// Let's get refresh logic out of the way now.
|
||||
const refresh = maybe_call(def.refresh, this, data);
|
||||
if ( refresh )
|
||||
timers[key] = setTimeout(
|
||||
() => refresh_fn(key),
|
||||
typeof refresh === 'number' ? refresh : 1000
|
||||
);
|
||||
|
||||
|
||||
// Grab the element again in case it changed, somehow.
|
||||
el = container.querySelector(`.ffz-stat[data-key="${key}"]`);
|
||||
|
||||
let stat, old_color;
|
||||
|
||||
const label = maybe_call(def.label, this, data);
|
||||
|
||||
if ( ! label )
|
||||
return destroy();
|
||||
|
||||
const tooltip = maybe_call(def.tooltip, this, data),
|
||||
order = maybe_call(def.order, this, data),
|
||||
color = maybe_call(def.color, this, data);
|
||||
|
||||
if ( ! el ) {
|
||||
let icon = maybe_call(def.icon, this, data);
|
||||
if ( typeof icon === 'string' )
|
||||
icon = e('span', 'tw-stat__icon', e('figure', icon));
|
||||
|
||||
el = e('div', {
|
||||
className: 'ffz-stat tw-stat',
|
||||
'data-key': key,
|
||||
tip_content: tooltip
|
||||
}, [
|
||||
icon,
|
||||
stat = e('span', 'tw-stat__value')
|
||||
]);
|
||||
|
||||
el._ffz_order = order;
|
||||
|
||||
if ( order != null )
|
||||
el.style.order = order;
|
||||
|
||||
container.appendChild(el);
|
||||
|
||||
if ( def.tooltip )
|
||||
el.tooltip = new Tooltip(container, el, {
|
||||
live: false,
|
||||
html: true,
|
||||
content: () => el.tip_content,
|
||||
onShow: (t, tip) => el.tip = tip,
|
||||
onHide: () => el.tip = null
|
||||
});
|
||||
|
||||
} else {
|
||||
stat = el.querySelector('.tw-stat__value');
|
||||
old_color = el.dataset.color || '';
|
||||
|
||||
if ( el._ffz_order !== order )
|
||||
el.style.order = el._ffz_order = order;
|
||||
|
||||
if ( el.tip_content !== tooltip ) {
|
||||
el.tip_content = tooltip;
|
||||
if ( el.tip )
|
||||
el.tip.element.innerHTML = tooltip;
|
||||
}
|
||||
}
|
||||
|
||||
if ( old_color !== color )
|
||||
el.dataset.color = el.style.color = color;
|
||||
|
||||
stat.innerHTML = label;
|
||||
|
||||
if ( def.disabled !== undefined )
|
||||
el.disabled = maybe_call(def.disabled, this, data);
|
||||
|
||||
} catch(err) {
|
||||
this.log.error(`Error rendering metadata for ${key}`, err);
|
||||
return destroy();
|
||||
}
|
||||
}
|
||||
}
|
93
src/modules/tooltips.js
Normal file
93
src/modules/tooltips.js
Normal file
|
@ -0,0 +1,93 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Tooltip Handling
|
||||
// ============================================================================
|
||||
|
||||
import {createElement as e} from 'utilities/dom';
|
||||
|
||||
import Tooltip from 'utilities/tooltip';
|
||||
import Module from 'utilities/module';
|
||||
|
||||
export default class TooltipProvider extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.types = {};
|
||||
|
||||
this.should_enable = true;
|
||||
|
||||
this.inject('i18n');
|
||||
this.inject('chat');
|
||||
|
||||
this.types.json = target => {
|
||||
const title = target.dataset.title;
|
||||
return [
|
||||
title && e('strong', null, title),
|
||||
e('code', {
|
||||
className: `block${title ? ' pd-t-05 border-t mg-t-05' : ''}`,
|
||||
style: {
|
||||
fontFamily: 'monospace',
|
||||
textAlign: 'left'
|
||||
}
|
||||
}, target.dataset.data)
|
||||
]
|
||||
}
|
||||
|
||||
this.types.badge = (target, tip) => {
|
||||
const container = target.parentElement.parentElement,
|
||||
|
||||
badge = target.dataset.badge,
|
||||
version = target.dataset.version,
|
||||
room = container.dataset.roomId,
|
||||
|
||||
data = this.chat.getBadge(badge, version, room);
|
||||
|
||||
if ( ! data )
|
||||
return;
|
||||
|
||||
return [
|
||||
this.chat.context.get('tooltip.badge-images') && e('img', {
|
||||
className: 'preview-image',
|
||||
src: data.image4x,
|
||||
|
||||
style: {
|
||||
height: '72px'
|
||||
},
|
||||
|
||||
onLoad: tip.update
|
||||
}),
|
||||
|
||||
data.title
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
this.tips = new Tooltip('[data-reactroot]', 'ffz-tooltip', {
|
||||
html: true,
|
||||
content: this.process.bind(this),
|
||||
popper: {
|
||||
placement: 'top'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
process(target, tip) { //eslint-disable-line class-methods-use-this
|
||||
const type = target.dataset.tooltipType,
|
||||
handler = this.types[type];
|
||||
|
||||
if ( ! handler )
|
||||
return [
|
||||
e('strong', null, 'Unhandled Tooltip Type'),
|
||||
e('code', {
|
||||
className: 'block pd-t-05 border-t mg-t-05',
|
||||
style: {
|
||||
fontFamily: 'monospace',
|
||||
textAlign: 'left'
|
||||
}
|
||||
}, JSON.stringify(target.dataset, null, 4))
|
||||
];
|
||||
|
||||
return handler(target, tip);
|
||||
}
|
||||
}
|
3
src/modules/translation_ui/components.js
Normal file
3
src/modules/translation_ui/components.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
'use strict';
|
||||
|
||||
export default require.context('./components', false, /\.vue$/);
|
42
src/modules/translation_ui/nondex.js
Normal file
42
src/modules/translation_ui/nondex.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Translation UI
|
||||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
import {createElement as e} from 'utilities/dom';
|
||||
|
||||
export default class TranslationUI extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.inject('settings');
|
||||
this.inject('site');
|
||||
this.inject('vue');
|
||||
|
||||
//this.should_enable = true;
|
||||
|
||||
this._dialog = null;
|
||||
this._visible = true;
|
||||
}
|
||||
|
||||
async onLoad() {
|
||||
this.vue.component(
|
||||
(await import(/* webpackChunkName: "translation-ui" */ './components.js')).default
|
||||
);
|
||||
}
|
||||
|
||||
async onEnable(event) {
|
||||
await this.site.awaitElement('.twilight-root');
|
||||
this.ps = this.site.web_munch.getModule('ps');
|
||||
}
|
||||
|
||||
onDisable() {
|
||||
if ( this._visible ) {
|
||||
this.toggleVisible();
|
||||
this._visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
870
src/settings.js
870
src/settings.js
|
@ -1,870 +0,0 @@
|
|||
var FFZ = window.FrankerFaceZ,
|
||||
constants = require("./constants"),
|
||||
utils = require("./utils"),
|
||||
FileSaver = require("./FileSaver"),
|
||||
|
||||
createElement = document.createElement.bind(document),
|
||||
|
||||
make_ls = function(key) {
|
||||
return "ffz_setting_" + key;
|
||||
},
|
||||
|
||||
favorite_setting = function(swit, key, info) {
|
||||
var ind = this.settings.favorite_settings.indexOf(key);
|
||||
|
||||
if ( ind === -1 ) {
|
||||
this.settings.favorite_settings.push(key);
|
||||
swit.setAttribute('original-title', 'Unfavorite this Setting');
|
||||
swit.classList.add('active');
|
||||
} else {
|
||||
this.settings.favorite_settings.splice(ind,1);
|
||||
swit.setAttribute('original-title', 'Favorite this Setting');
|
||||
swit.classList.remove('active');
|
||||
}
|
||||
|
||||
jQuery(swit).trigger('mouseout').trigger('mouseover');
|
||||
this.settings.set('favorite_settings', this.settings.favorite_settings);
|
||||
},
|
||||
|
||||
toggle_setting = function(swit, key, info) {
|
||||
var val = !(info.get ? (typeof info.get === 'function' ? info.get.call(this) : this.settings.get(info.get)) : this.settings.get(key));
|
||||
if ( typeof info.set === "function" )
|
||||
info.set.call(this, val);
|
||||
else
|
||||
this.settings.set(info.set || key, val);
|
||||
|
||||
swit.classList.toggle('active', val);
|
||||
},
|
||||
|
||||
option_setting = function(select, key, info) {
|
||||
var val = JSON.parse(select.options[select.selectedIndex].value);
|
||||
if ( typeof info.set === "function" )
|
||||
info.set.call(this, val);
|
||||
else
|
||||
this.settings.set(info.set || key, val);
|
||||
};
|
||||
|
||||
|
||||
// --------------------
|
||||
// Initializer
|
||||
// --------------------
|
||||
|
||||
FFZ.settings_info = {
|
||||
advanced_settings: { value: false, visible: false }
|
||||
};
|
||||
|
||||
FFZ.basic_settings = {};
|
||||
|
||||
FFZ.prototype.load_settings = function() {
|
||||
this.log("Loading settings.");
|
||||
|
||||
// Build a settings object.
|
||||
this.settings = {};
|
||||
|
||||
// Helpers
|
||||
this.settings.get = this._setting_get.bind(this);
|
||||
this.settings.set = this._setting_set.bind(this);
|
||||
this.settings.del = this._setting_del.bind(this);
|
||||
this.settings.load = this._setting_load.bind(this);
|
||||
|
||||
this.settings.get_twitch = this._setting_get_twitch.bind(this);
|
||||
|
||||
for(var key in FFZ.settings_info) {
|
||||
if ( ! FFZ.settings_info.hasOwnProperty(key) )
|
||||
continue;
|
||||
|
||||
var info = FFZ.settings_info[key],
|
||||
ls_key = info && info.storage_key || make_ls(key);
|
||||
|
||||
this._setting_load(key);
|
||||
}
|
||||
|
||||
// Listen for Changes
|
||||
window.addEventListener("storage", this._setting_update.bind(this), false);
|
||||
}
|
||||
|
||||
|
||||
// --------------------
|
||||
// Backup and Restore
|
||||
// --------------------
|
||||
|
||||
FFZ.prototype.reset_settings = function() {
|
||||
if ( ! confirm(this.tr('Are you sure you wish to reset FrankerFaceZ?\n\nThis will force the tab to refresh.')) )
|
||||
return;
|
||||
|
||||
// Clear Settings
|
||||
for(var key in FFZ.settings_info) {
|
||||
if ( ! FFZ.settings_info.hasOwnProperty(key) )
|
||||
continue;
|
||||
|
||||
this.settings.del(key);
|
||||
}
|
||||
|
||||
// Clear Aliases
|
||||
this.aliases = {};
|
||||
localStorage.ffz_aliases = '{}';
|
||||
|
||||
// TODO: Filters
|
||||
|
||||
// Refresh
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype._get_settings_object = function(skip_default) {
|
||||
var data = {
|
||||
version: 1,
|
||||
script_version: FFZ.version_info + '',
|
||||
aliases: this.aliases,
|
||||
filters: this.filters,
|
||||
settings: {}
|
||||
};
|
||||
|
||||
for(var key in FFZ.settings_info) {
|
||||
if ( ! FFZ.settings_info.hasOwnProperty(key) )
|
||||
continue;
|
||||
|
||||
var info = FFZ.settings_info[key],
|
||||
ls_key = info.storage_key || make_ls(key),
|
||||
stored_val = localStorage.getItem(ls_key);
|
||||
|
||||
if ( (stored_val !== null) && (!skip_default || this.settings[key] !== info.value) )
|
||||
data.settings[key] = this.settings[key];
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.save_settings_file = function() {
|
||||
var data = this._get_settings_object(),
|
||||
blob = new Blob(
|
||||
[JSON.stringify(data, null, 4)], {type: "application/json;charset=utf-8"});
|
||||
|
||||
FileSaver.saveAs(blob, "ffz-settings.json");
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.load_settings_file = function(file) {
|
||||
if ( typeof file === "string" )
|
||||
this._load_settings_file(file);
|
||||
else {
|
||||
var reader = new FileReader(),
|
||||
f = this;
|
||||
|
||||
reader.onload = function(e) { f._load_settings_file(e.target.result); }
|
||||
reader.readAsText(file);
|
||||
}
|
||||
}
|
||||
|
||||
FFZ.prototype._load_settings_file = function(data, hide_alert) {
|
||||
if ( typeof data === "string" )
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch(err) {
|
||||
this.error("Error Loading Settings: " + err);
|
||||
if ( ! hide_alert )
|
||||
alert("There was an error attempting to read the provided settings data.");
|
||||
return [-1,-1,-1];
|
||||
}
|
||||
|
||||
this.log("Loading Settings Data", data);
|
||||
|
||||
var skipped = [], applied = [],
|
||||
aliases = 0;
|
||||
|
||||
if ( data.settings ) {
|
||||
for(var key in data.settings) {
|
||||
if ( ! FFZ.settings_info.hasOwnProperty(key) ) {
|
||||
skipped.push(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
var info = FFZ.settings_info[key],
|
||||
val = data.settings[key];
|
||||
|
||||
if ( info.process_value )
|
||||
val = info.process_value.call(this, val);
|
||||
|
||||
if ( val !== this.settings.get(key) )
|
||||
this.settings.set(key, val);
|
||||
|
||||
applied.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
if ( data.aliases ) {
|
||||
for(var key in data.aliases) {
|
||||
if ( this.aliases[key] === data.aliases[key] )
|
||||
continue;
|
||||
|
||||
this.aliases[key] = data.aliases[key];
|
||||
aliases++;
|
||||
}
|
||||
|
||||
if ( aliases )
|
||||
localStorage.ffz_aliases = JSON.stringify(this.aliases);
|
||||
}
|
||||
|
||||
if ( data.filters ) {
|
||||
// TODO: Load filters!
|
||||
}
|
||||
|
||||
// Do this in a timeout so that any styles have a moment to update.
|
||||
if ( ! hide_alert )
|
||||
setTimeout(function(){
|
||||
alert('Successfully loaded ' + applied.length + ' settings and skipped ' + skipped.length + ' settings. Added ' + aliases + ' user nicknames.');
|
||||
});
|
||||
|
||||
return [applied.length, skipped.length, aliases];
|
||||
}
|
||||
|
||||
|
||||
// --------------------
|
||||
// Menu Page
|
||||
// --------------------
|
||||
|
||||
var is_android = navigator.userAgent.indexOf('Android') !== -1,
|
||||
settings_renderer = function(settings_data, collapsable, collapsed_key, show_pin) {
|
||||
return function(view, container) {
|
||||
var f = this,
|
||||
settings = {},
|
||||
categories = [];
|
||||
|
||||
// Searching!
|
||||
if ( show_pin ) {
|
||||
var search_cont = utils.createElement('div', 'ffz-filter-container'),
|
||||
search_input = utils.createElement('input', 'emoticon-selector__filter-input form__input js-filter-input text text--full-width'),
|
||||
filtered_cont = utils.createElement('div', 'ffz-filter-children ffz-ui-sub-menu-page');
|
||||
|
||||
search_input.placeholder = 'Search for Settings';
|
||||
search_input.type = 'text';
|
||||
|
||||
filtered_cont.style.maxHeight = (parseInt(container.style.maxHeight) - 53) + 'px';
|
||||
|
||||
search_cont.appendChild(search_input);
|
||||
container.appendChild(filtered_cont);
|
||||
container.appendChild(search_cont);
|
||||
|
||||
container = filtered_cont;
|
||||
|
||||
search_input.addEventListener('input', function(e) {
|
||||
var filter = search_input.value || '',
|
||||
groups = filtered_cont.querySelectorAll('.chat-menu-content');
|
||||
|
||||
filter = filter.toLowerCase();
|
||||
|
||||
for(var i=0; i < groups.length; i++) {
|
||||
var el = groups[i],
|
||||
settings = el.querySelectorAll('.ffz-setting'),
|
||||
hidden = true;
|
||||
|
||||
for(var j=0; j < settings.length; j++) {
|
||||
var se = settings[j],
|
||||
shidden = filter.length && se.getAttribute('data-filter').indexOf(filter) === -1;
|
||||
|
||||
se.classList.toggle('hidden', shidden);
|
||||
hidden = hidden && shidden;
|
||||
}
|
||||
|
||||
var incompat = el.querySelector('.bttv-incompatibility'),
|
||||
settings = incompat && incompat.querySelectorAll('b'),
|
||||
incompat_hidden = true;
|
||||
|
||||
if ( incompat ) {
|
||||
for(var j=0; j < settings.length; j++) {
|
||||
var se = settings[j],
|
||||
shidden = filter.length && se.getAttribute('data-filter').indexOf(filter) === -1;
|
||||
|
||||
se.classList.toggle('hidden', shidden);
|
||||
incompat_hidden = incompat_hidden && shidden;
|
||||
}
|
||||
|
||||
incompat.classList.toggle('hidden', incompat_hidden);
|
||||
hidden = hidden && incompat_hidden;
|
||||
}
|
||||
|
||||
el.classList.toggle('collapsable', ! filter.length);
|
||||
el.classList.toggle('hidden', hidden);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for(var key in settings_data) {
|
||||
var info = settings_data[key],
|
||||
cat = info.category || "Miscellaneous",
|
||||
cat_store = settings[cat];
|
||||
|
||||
if ( info.hasOwnProperty('visible') ) {
|
||||
var visible = info.visible;
|
||||
if ( typeof visible === "function" )
|
||||
visible = visible.call(this);
|
||||
|
||||
if ( ! visible )
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( is_android && info.no_mobile )
|
||||
continue;
|
||||
|
||||
if ( ! cat_store ) {
|
||||
categories.push(cat);
|
||||
cat_store = settings[cat] = [];
|
||||
}
|
||||
|
||||
cat_store.push([key, info]);
|
||||
}
|
||||
|
||||
categories.sort(function(a,b) {
|
||||
var a = a.toLowerCase(),
|
||||
b = b.toLowerCase();
|
||||
|
||||
if ( a === "debugging" )
|
||||
a = "zzz" + a;
|
||||
|
||||
if ( b === "debugging" )
|
||||
b = "zzz" + b;
|
||||
|
||||
if ( a < b ) return -1;
|
||||
else if ( a > b ) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
var current_category = collapsed_key ? this[collapsed_key] || true : categories[0];
|
||||
|
||||
for(var ci=0; ci < categories.length; ci++) {
|
||||
var category = categories[ci],
|
||||
cset = settings[category],
|
||||
|
||||
bttv_skipped = [],
|
||||
added = 0,
|
||||
|
||||
menu = createElement('div'),
|
||||
heading = createElement('div');
|
||||
|
||||
|
||||
heading.className = 'heading';
|
||||
menu.className = 'chat-menu-content';
|
||||
menu.setAttribute('data-category', category);
|
||||
|
||||
if ( collapsable ) {
|
||||
menu.classList.add('collapsable');
|
||||
menu.classList.toggle('collapsed', current_category !== category);
|
||||
menu.addEventListener('click', function(e) {
|
||||
var t = this;
|
||||
if ( ! t.classList.contains('collapsable') )
|
||||
return;
|
||||
|
||||
else if ( ! t.classList.contains('collapsed') ) {
|
||||
if ( e.target.classList.contains('heading') ) {
|
||||
t.classList.add('collapsed');
|
||||
if ( collapsed_key )
|
||||
f[collapsed_key] = true;
|
||||
} else
|
||||
return;
|
||||
|
||||
} else {
|
||||
jQuery(".chat-menu-content:not(.collapsed)", container).addClass("collapsed");
|
||||
t.classList.remove('collapsed');
|
||||
if ( collapsed_key )
|
||||
f[collapsed_key] = t.getAttribute('data-category');
|
||||
}
|
||||
|
||||
setTimeout(function(){t.scrollIntoViewIfNeeded()});
|
||||
});
|
||||
}
|
||||
|
||||
heading.innerHTML = category;
|
||||
menu.appendChild(heading);
|
||||
|
||||
cset.sort(function(a,b) {
|
||||
var a = a[1],
|
||||
b = b[1],
|
||||
|
||||
at = 2, //a.type === "boolean" ? 1 : 2,
|
||||
bt = 2, //b.type === "boolean" ? 1 : 2,
|
||||
|
||||
an = a.name.toLowerCase(),
|
||||
bn = b.name.toLowerCase();
|
||||
|
||||
if ( at < bt ) return -1;
|
||||
else if ( at > bt ) return 1;
|
||||
|
||||
else if ( an < bn ) return -1;
|
||||
else if ( an > bn ) return 1;
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
for(var i=0; i < cset.length; i++) {
|
||||
var key = cset[i][0],
|
||||
info = cset[i][1],
|
||||
|
||||
el = createElement('p'),
|
||||
pin_btn = createElement('a'),
|
||||
val = info.get ? (typeof info.get === 'function' ? info.get.call(this) : this.settings.get(info.get)) : this.settings.get(key);
|
||||
|
||||
el.className = 'ffz-setting clearfix';
|
||||
|
||||
if ( (info.no_bttv === 6 && this.has_bttv_6) ||
|
||||
(info.no_bttv === 7 && this.has_bttv_7) ||
|
||||
(info.no_bttv === true && this.has_bttv) ) {
|
||||
bttv_skipped.push([info.name, info.help]);
|
||||
continue;
|
||||
} else if ( (info.require_bttv === 6 && ! this.has_bttv_6) ||
|
||||
(info.require_bttv === 7 && ! this.has_bttv_7) ||
|
||||
(info.require_bttv === true && ! this.has_bttv) ) {
|
||||
continue;
|
||||
} else {
|
||||
if ( show_pin ) {
|
||||
var faved = this.settings.favorite_settings.indexOf(key) !== -1;
|
||||
pin_btn.className = 'pin-switch html-tooltip';
|
||||
pin_btn.classList.toggle('active', faved);
|
||||
pin_btn.addEventListener('click', favorite_setting.bind(this, pin_btn, key, info));
|
||||
pin_btn.title = (faved ? 'Unf' : 'F') + 'avorite this Setting';
|
||||
pin_btn.innerHTML = constants.STAR;
|
||||
}
|
||||
|
||||
if ( info.type === "boolean" ) {
|
||||
var swit = createElement('a'),
|
||||
label = createElement('span');
|
||||
|
||||
swit.className = 'switch';
|
||||
swit.classList.toggle('active', val);
|
||||
swit.appendChild(createElement('span'))
|
||||
|
||||
label.className = 'switch-label';
|
||||
label.innerHTML = info.name;
|
||||
|
||||
el.appendChild(swit);
|
||||
if ( show_pin )
|
||||
el.appendChild(pin_btn);
|
||||
el.appendChild(label);
|
||||
|
||||
swit.addEventListener('click', toggle_setting.bind(this, swit, key, info))
|
||||
|
||||
} else if ( info.type === "select" ) {
|
||||
var select = createElement('select'),
|
||||
label = createElement('span');
|
||||
|
||||
label.className = 'option-label';
|
||||
label.innerHTML = info.name;
|
||||
|
||||
var op_list = [];
|
||||
|
||||
for(var ok in info.options) {
|
||||
var op = createElement('option'),
|
||||
desc = info.options[ok],
|
||||
sort_key = 0;
|
||||
|
||||
op.value = JSON.stringify(ok);
|
||||
if ( val == ok )
|
||||
op.setAttribute('selected', true);
|
||||
|
||||
if ( typeof desc === "object" ) {
|
||||
op.innerHTML = desc[0];
|
||||
sort_key = desc[1];
|
||||
} else
|
||||
op.innerHTML = desc;
|
||||
|
||||
op_list.push([sort_key, op]);
|
||||
}
|
||||
|
||||
op_list.sort(function(a,b) {return a[0] - b[0]});
|
||||
|
||||
for(var op_i=0; op_i < op_list.length; op_i++)
|
||||
select.appendChild(op_list[op_i][1]);
|
||||
|
||||
select.addEventListener('change', option_setting.bind(this, select, key, info));
|
||||
|
||||
if ( show_pin )
|
||||
el.appendChild(pin_btn);
|
||||
el.appendChild(label);
|
||||
el.appendChild(select);
|
||||
|
||||
} else if ( typeof info.method === "function" ) {
|
||||
el.classList.add("option");
|
||||
var link = createElement('a');
|
||||
link.innerHTML = info.name;
|
||||
link.href = '#';
|
||||
|
||||
if ( show_pin )
|
||||
el.appendChild(pin_btn);
|
||||
el.appendChild(link);
|
||||
|
||||
link.addEventListener('click', info.method.bind(this));
|
||||
|
||||
} else
|
||||
continue;
|
||||
|
||||
if ( info.help || info.experiment_warn || (this.has_bttv && info.warn_bttv) ) {
|
||||
var help = document.createElement('span');
|
||||
help.className = 'help';
|
||||
var parts = [];
|
||||
if ( info.experiment_warn )
|
||||
parts.push('<b>Note:</b> This affects an active Twitch experiment. Give feedback at: <a href="mailto:feedback@twitch.tv">feedback@twitch.tv</a>');
|
||||
|
||||
if ( this.has_bttv && info.warn_bttv )
|
||||
parts.push('<i>' + info.warn_bttv + '</i>');
|
||||
|
||||
if ( info.help )
|
||||
parts.push(info.help);
|
||||
|
||||
help.innerHTML = parts.join('<br>');
|
||||
el.appendChild(help);
|
||||
}
|
||||
}
|
||||
|
||||
// Search by any of the present text.
|
||||
el.setAttribute('data-filter', el.textContent.toLowerCase());
|
||||
|
||||
added++;
|
||||
menu.appendChild(el);
|
||||
}
|
||||
|
||||
if ( ! added )
|
||||
continue;
|
||||
|
||||
if ( bttv_skipped.length ) {
|
||||
var el = createElement('p'),
|
||||
label = createElement('span'),
|
||||
help = createElement('span');
|
||||
|
||||
el.className = 'bttv-incompatibility clearfix disabled';
|
||||
label.className = 'switch-label';
|
||||
label.innerHTML = "Features Incompatible with BetterTTV";
|
||||
|
||||
help.className = 'help';
|
||||
for(var i=0; i < bttv_skipped.length; i++) {
|
||||
var skipped = bttv_skipped[i],
|
||||
filter_text = skipped[0].toLowerCase() + (skipped[1] ? ' ' + skipped[1].toLowerCase() : '');
|
||||
help.innerHTML += '<b data-filter="' + utils.quote_attr(filter_text) + '"' + (skipped[1] ? ' class="html-tooltip" title="' + utils.quote_attr(skipped[1]) + '"' : '') + '>' + skipped[0] + '</b>';
|
||||
}
|
||||
|
||||
el.appendChild(label);
|
||||
el.appendChild(help);
|
||||
menu.appendChild(el);
|
||||
}
|
||||
|
||||
container.appendChild(menu);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render_basic = settings_renderer(FFZ.basic_settings, false, '_ffz_basic_settings_page'),
|
||||
render_advanced = settings_renderer(FFZ.settings_info, true, '_ffz_settings_page', true);
|
||||
|
||||
|
||||
FFZ.settings_info.favorite_settings = {
|
||||
value: [],
|
||||
hidden: true
|
||||
}
|
||||
|
||||
|
||||
FFZ.menu_pages.settings = {
|
||||
name: "Settings",
|
||||
icon: constants.GEAR,
|
||||
sort_order: 99999,
|
||||
wide: true,
|
||||
|
||||
default_page: function() { return this.settings.favorite_settings.length ? "favorites" : this.settings.advanced_settings ? 'advanced' : 'basic' },
|
||||
|
||||
pages: {
|
||||
favorites: {
|
||||
name: "Favorites",
|
||||
sort_order: 1,
|
||||
|
||||
render: function(view, container) {
|
||||
var favorites = this.settings.favorite_settings,
|
||||
count = 0;
|
||||
|
||||
if ( ! this.has_bttv )
|
||||
count = favorites.length;
|
||||
else
|
||||
for(var i=0; i < favorites.length; i++)
|
||||
if ( FFZ.settings_info[favorites[i]] && ! FFZ.settings_info[favorites[i]].no_bttv )
|
||||
count++;
|
||||
|
||||
if ( ! count ) {
|
||||
var el = utils.createElement('div');
|
||||
el.className = 'emoticon-grid ffz-no-emotes center';
|
||||
el.innerHTML = "You have no favorite settings.<br>" +
|
||||
'<img src=\"//cdn.frankerfacez.com/emoticon/26608/2\"><br>' +
|
||||
'To make a setting a favorite, find it on the <nobr>Advanced</nobr> tab and click the star icon to the right.';
|
||||
|
||||
container.appendChild(el);
|
||||
return;
|
||||
}
|
||||
|
||||
var favorite_settings = {};
|
||||
for(var i=0, l = favorites.length; i < l; i++) {
|
||||
var key = favorites[i],
|
||||
val = FFZ.settings_info[key];
|
||||
|
||||
if ( val )
|
||||
favorite_settings[key] = val;
|
||||
}
|
||||
|
||||
return settings_renderer(favorite_settings, false, '_ffz_favorite_settings_page').call(this, view, container);
|
||||
}
|
||||
},
|
||||
|
||||
basic: {
|
||||
name: "Basic",
|
||||
sort_order: 2,
|
||||
|
||||
render: function(view, container) {
|
||||
this.settings.set("advanced_settings", false);
|
||||
return render_basic.call(this, view, container);
|
||||
}
|
||||
},
|
||||
|
||||
advanced: {
|
||||
name: "Advanced",
|
||||
sort_order: 3,
|
||||
|
||||
render: function(view, container) {
|
||||
this.settings.set("advanced_settings", true);
|
||||
return render_advanced.call(this, view, container);
|
||||
}
|
||||
},
|
||||
|
||||
backup: {
|
||||
name: "Backup & Restore",
|
||||
sort_order: 4,
|
||||
|
||||
render: function(view, container) {
|
||||
var backup_head = createElement('div'),
|
||||
restore_head = createElement('div'),
|
||||
reset_head = createElement('div'),
|
||||
|
||||
backup_cont = createElement('div'),
|
||||
restore_cont = createElement('div'),
|
||||
reset_cont = createElement('div'),
|
||||
|
||||
backup_para = createElement('p'),
|
||||
backup_link = createElement('a'),
|
||||
backup_help = createElement('span'),
|
||||
|
||||
restore_para = createElement('p'),
|
||||
restore_input = createElement('input'),
|
||||
restore_link = createElement('a'),
|
||||
restore_help = createElement('span'),
|
||||
|
||||
reset_para = createElement('p'),
|
||||
reset_link = createElement('a'),
|
||||
reset_help = createElement('span'),
|
||||
f = this;
|
||||
|
||||
|
||||
backup_cont.className = 'chat-menu-content';
|
||||
backup_head.className = 'heading';
|
||||
backup_head.innerHTML = 'Backup Settings';
|
||||
backup_cont.appendChild(backup_head);
|
||||
|
||||
backup_para.className = 'clearfix option';
|
||||
|
||||
backup_link.href = '#';
|
||||
backup_link.innerHTML = 'Save to File';
|
||||
backup_link.addEventListener('click', this.save_settings_file.bind(this));
|
||||
|
||||
backup_help.className = 'help';
|
||||
backup_help.innerHTML = 'This generates a JSON file containing all of your settings and prompts you to save it.';
|
||||
|
||||
backup_para.appendChild(backup_link);
|
||||
backup_para.appendChild(backup_help);
|
||||
backup_cont.appendChild(backup_para);
|
||||
|
||||
restore_cont.className = 'chat-menu-content';
|
||||
restore_head.className = 'heading';
|
||||
restore_head.innerHTML = 'Restore Settings';
|
||||
restore_cont.appendChild(restore_head);
|
||||
|
||||
restore_para.className = 'clearfix option';
|
||||
|
||||
restore_input.type = 'file';
|
||||
restore_input.addEventListener('change', function() { f.load_settings_file(this.files[0]); })
|
||||
|
||||
restore_link.href = '#';
|
||||
restore_link.innerHTML = 'Restore from File';
|
||||
restore_link.addEventListener('click', function(e) { e.preventDefault(); restore_input.click(); });
|
||||
|
||||
restore_help.className = 'help';
|
||||
restore_help.innerHTML = 'This loads settings from a previously generated JSON file.';
|
||||
|
||||
restore_para.appendChild(restore_link);
|
||||
restore_para.appendChild(restore_help);
|
||||
restore_cont.appendChild(restore_para);
|
||||
|
||||
reset_cont.className = 'chat-menu-content';
|
||||
reset_head.className = 'heading';
|
||||
reset_head.innerHTML = this.tr('Reset Settings');
|
||||
reset_cont.appendChild(reset_head);
|
||||
|
||||
reset_para.className = 'clearfix option';
|
||||
|
||||
reset_link.href = '#';
|
||||
reset_link.innerHTML = this.tr('Reset FrankerFaceZ');
|
||||
reset_link.addEventListener('click', this.reset_settings.bind(this));
|
||||
|
||||
reset_help.className = 'help';
|
||||
reset_help.innerHTML = this.tr('This resets all of your FFZ data. That includes chat filters, nicknames for users, and settings.');
|
||||
|
||||
reset_para.appendChild(reset_link);
|
||||
reset_para.appendChild(reset_help);
|
||||
reset_cont.appendChild(reset_para);
|
||||
|
||||
container.appendChild(backup_cont);
|
||||
container.appendChild(restore_cont);
|
||||
container.appendChild(reset_cont);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// --------------------
|
||||
// Tracking Updates
|
||||
// --------------------
|
||||
|
||||
FFZ.prototype._setting_update = function(e) {
|
||||
if ( ! e )
|
||||
e = window.event;
|
||||
|
||||
if ( ! e.key || e.key.substr(0, 12) !== "ffz_setting_" )
|
||||
return;
|
||||
|
||||
var ls_key = e.key,
|
||||
key = ls_key.substr(12),
|
||||
val = undefined,
|
||||
info = FFZ.settings_info[key];
|
||||
|
||||
if ( ! info ) {
|
||||
// Try iterating to find the key.
|
||||
for(key in FFZ.settings_info) {
|
||||
if ( ! FFZ.settings_info.hasOwnProperty(key) )
|
||||
continue;
|
||||
|
||||
info = FFZ.settings_info[key];
|
||||
if ( info.storage_key == ls_key )
|
||||
break;
|
||||
}
|
||||
|
||||
// Not us.
|
||||
if ( info.storage_key != ls_key )
|
||||
return;
|
||||
}
|
||||
|
||||
this.log("Updated Setting: " + key);
|
||||
|
||||
try {
|
||||
val = JSON.parse(e.newValue);
|
||||
} catch(err) {
|
||||
this.log('Error loading new value for "' + key + '": ' + err);
|
||||
val = info.value || undefined;
|
||||
}
|
||||
|
||||
if ( info.process_value )
|
||||
try {
|
||||
val = info.process_value.call(this, val);
|
||||
} catch(err) {
|
||||
this.log('Error processing value for setting "' + key + '": ' + err);
|
||||
return;
|
||||
}
|
||||
|
||||
this.settings[key] = val;
|
||||
if ( info.on_update )
|
||||
try {
|
||||
info.on_update.call(this, val, false);
|
||||
} catch(err) {
|
||||
this.log('Error running updater for setting "' + key + '": ' + err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --------------------
|
||||
// Settings Access
|
||||
// --------------------
|
||||
|
||||
FFZ.prototype._setting_load = function(key, default_value) {
|
||||
var info = FFZ.settings_info[key],
|
||||
ls_key = info && info.storage_key || make_ls(key),
|
||||
val = default_value || (info && info.hasOwnProperty("value") ? info.value : undefined);
|
||||
|
||||
var stored_val = localStorage.getItem(ls_key);
|
||||
if ( stored_val !== null )
|
||||
try {
|
||||
val = JSON.parse(stored_val);
|
||||
} catch(err) {
|
||||
this.log('Error parsing value for "' + key + '": ' + err);
|
||||
}
|
||||
|
||||
if ( info && info.process_value )
|
||||
val = info.process_value.call(this, val);
|
||||
|
||||
this.settings[key] = val;
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype._setting_get = function(key) {
|
||||
if ( ! this.settings.hasOwnProperty(key) && FFZ.settings_info[key] )
|
||||
this._setting_load(key);
|
||||
|
||||
return this.settings[key];
|
||||
}
|
||||
|
||||
FFZ.prototype._setting_get_twitch = function(key) {
|
||||
var Settings = utils.ember_settings();
|
||||
return Settings && Settings.get(key);
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype._setting_set = function(key, val, suppress_log) {
|
||||
var info = FFZ.settings_info[key],
|
||||
ls_key = info.storage_key || make_ls(key);
|
||||
|
||||
if ( info.process_value )
|
||||
try {
|
||||
val = info.process_value.call(this, val)
|
||||
} catch(err) {
|
||||
this.log('Error processing value for setting "' + key + '": ' + err);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.settings[key] = val;
|
||||
|
||||
var jval = JSON.stringify(val);
|
||||
localStorage.setItem(ls_key, jval);
|
||||
|
||||
if ( ! suppress_log )
|
||||
this.log('Changed Setting "' + key + '" to: ' + jval);
|
||||
|
||||
if ( info.on_update )
|
||||
try {
|
||||
info.on_update.call(this, val, true);
|
||||
} catch(err) {
|
||||
this.log('Error running updater for setting "' + key + '": ' + err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype._setting_del = function(key) {
|
||||
var info = FFZ.settings_info[key],
|
||||
ls_key = info.storage_key || make_ls(key),
|
||||
val = undefined;
|
||||
|
||||
localStorage.removeItem(ls_key);
|
||||
if ( info )
|
||||
val = this.settings[key] = info.hasOwnProperty("value") ? info.value : undefined;
|
||||
|
||||
this.settings[key] = val;
|
||||
|
||||
if ( info.on_update )
|
||||
try {
|
||||
info.on_update.call(this, val, true);
|
||||
} catch(err) {
|
||||
this.log('Error running updater for setting "' + key + '": ' + err);
|
||||
}
|
||||
}
|
287
src/settings/context.js
Normal file
287
src/settings/context.js
Normal file
|
@ -0,0 +1,287 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Settings Contexts
|
||||
// ============================================================================
|
||||
|
||||
import {EventEmitter} from 'utilities/events';
|
||||
import {has, get as getter, array_equals} from 'utilities/object';
|
||||
|
||||
|
||||
/**
|
||||
* The SettingsContext class provides a context through which to read
|
||||
* settings values in addition to emitting events when settings values
|
||||
* are changed.
|
||||
* @extends EventEmitter
|
||||
*/
|
||||
export default class SettingsContext extends EventEmitter {
|
||||
constructor(manager, context) {
|
||||
super();
|
||||
|
||||
if ( manager instanceof SettingsContext ) {
|
||||
this.parent = manager;
|
||||
this.manager = manager.manager;
|
||||
|
||||
this.parent.on('context_changed', this._rebuildContext, this);
|
||||
|
||||
} else {
|
||||
this.parent = null;
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
this.manager.__contexts.push(this);
|
||||
this._context = context || {};
|
||||
|
||||
this.__cache = new Map;
|
||||
this.__meta = new Map;
|
||||
this.__profiles = [];
|
||||
this.order = [];
|
||||
|
||||
this._rebuildContext();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if ( this.parent )
|
||||
this.parent.off('context_changed', this._rebuildContext, this);
|
||||
|
||||
for(const profile of this.__profiles)
|
||||
profile.off('changed', this._onChanged, this);
|
||||
|
||||
const contexts = this.manager.__contexts,
|
||||
idx = contexts.indexOf(this);
|
||||
|
||||
if ( idx !== -1 )
|
||||
contexts.splice(idx, 1);
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// State Construction
|
||||
// ========================================================================
|
||||
|
||||
_rebuildContext() {
|
||||
this.__context = this.parent ?
|
||||
Object.assign({}, this.parent._context, this._context) :
|
||||
this._context;
|
||||
|
||||
// Make sure we re-build the cache. Dependency hell.
|
||||
if ( ! this.selectProfiles() )
|
||||
this.rebuildCache();
|
||||
|
||||
this.emit('context_changed');
|
||||
}
|
||||
|
||||
|
||||
selectProfiles() {
|
||||
const new_profiles = [],
|
||||
order = this.order = [];
|
||||
for(const profile of this.manager.__profiles)
|
||||
if ( profile.matches(this.__context) ) {
|
||||
new_profiles.push(profile);
|
||||
order.push(profile.id);
|
||||
}
|
||||
|
||||
if ( array_equals(this.__profiles, new_profiles) )
|
||||
return false;
|
||||
|
||||
const changed_ids = new Set;
|
||||
|
||||
for(const profile of this.__profiles)
|
||||
if ( ! new_profiles.includes(profile) ) {
|
||||
profile.off('changed', this._onChanged, this);
|
||||
changed_ids.add(profile.id);
|
||||
}
|
||||
|
||||
for(const profile of new_profiles)
|
||||
if ( ! this.__profiles.includes(profile) ) {
|
||||
profile.on('changed', this._onChanged, this);
|
||||
changed_ids.add(profile.id);
|
||||
}
|
||||
|
||||
this.__profiles = new_profiles;
|
||||
this.emit('profiles_changed');
|
||||
this.rebuildCache(changed_ids);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
rebuildCache() {
|
||||
const old_cache = this.__cache,
|
||||
old_meta = this.__meta,
|
||||
meta = this.__meta = new Map;
|
||||
|
||||
this.__cache = new Map;
|
||||
|
||||
// TODO: Limit the values we recalculate to ones affected by the change
|
||||
// that happened to the profiles. This is harder because of setting
|
||||
// dependencies.
|
||||
|
||||
for(const [key, old_value] of old_cache) {
|
||||
const new_value = this.get(key),
|
||||
new_m = meta.get(key),
|
||||
old_m = old_meta.get(key),
|
||||
new_uses = new_m ? new_m.uses : null,
|
||||
old_uses = old_m ? old_m.uses : null;
|
||||
|
||||
if ( new_value !== old_value ) {
|
||||
this.emit('changed', key, new_value, old_value);
|
||||
this.emit(`changed:${key}`, new_value, old_value);
|
||||
}
|
||||
|
||||
if ( new_uses !== old_uses ) {
|
||||
this.emit('uses_changed', key, new_uses, old_uses);
|
||||
this.emit(`uses_changed:${key}`, new_uses, old_uses);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Context Control
|
||||
// ========================================================================
|
||||
|
||||
context(context) {
|
||||
return new SettingsContext(this, context);
|
||||
}
|
||||
|
||||
|
||||
updateContext(context) {
|
||||
let changed = false;
|
||||
|
||||
for(const key in context)
|
||||
if ( has(context, key) && context[key] !== this._context[key] ) {
|
||||
this._context[key] = context[key];
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if ( changed )
|
||||
this._rebuildContext();
|
||||
}
|
||||
|
||||
|
||||
setContext(context) {
|
||||
this._context = context;
|
||||
this._rebuildContext();
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Data Access
|
||||
// ========================================================================
|
||||
|
||||
_onChanged(key) {
|
||||
this._update(key, key, []);
|
||||
}
|
||||
|
||||
_update(key, initial, visited) {
|
||||
if ( ! this.__cache.has(key) )
|
||||
return;
|
||||
|
||||
else if ( visited.includes(key) )
|
||||
throw new Error(`cyclic dependent chain when updating setting "${initial}"`);
|
||||
|
||||
visited.push(key);
|
||||
|
||||
const old_value = this.__cache.get(key),
|
||||
old_meta = this.__meta.get(key),
|
||||
new_value = this._get(key, key, []),
|
||||
new_meta = this.__meta.get(key),
|
||||
|
||||
old_uses = old_meta ? old_meta.uses : null,
|
||||
new_uses = new_meta ? new_meta.uses : null;
|
||||
|
||||
if ( old_uses !== new_uses ) {
|
||||
this.emit('uses_changed', key, new_uses, old_uses);
|
||||
this.emit(`uses_changed:${key}`, new_uses, old_uses);
|
||||
}
|
||||
|
||||
if ( old_value === new_value )
|
||||
return;
|
||||
|
||||
this.emit('changed', key, new_value, old_value);
|
||||
this.emit(`changed:${key}`, new_value, old_value);
|
||||
|
||||
const definition = this.manager.definitions.get(key);
|
||||
if ( definition && definition.required_by )
|
||||
for(const req_key of definition.required_by)
|
||||
if ( ! req_key.startsWith('context.') )
|
||||
this._update(req_key, initial, Array.from(visited));
|
||||
}
|
||||
|
||||
|
||||
_get(key, initial, visited) {
|
||||
if ( visited.includes(key) )
|
||||
throw new Error(`cyclic dependency when resolving setting "${initial}"`);
|
||||
|
||||
visited.push(key);
|
||||
|
||||
const definition = this.manager.definitions.get(key),
|
||||
raw_value = this._getRaw(key),
|
||||
meta = {
|
||||
uses: raw_value ? raw_value[1].id : null
|
||||
};
|
||||
|
||||
let value = raw_value ? raw_value[0] : undefined;
|
||||
|
||||
if ( definition ) {
|
||||
if ( Array.isArray(definition) )
|
||||
throw new Error(`non-existent setting "${key}" required when resolving setting "${initial}"`);
|
||||
|
||||
if ( meta.uses === null ) {
|
||||
const def_default = definition.default;
|
||||
if ( typeof def_default === 'function' )
|
||||
value = def_default(this);
|
||||
else
|
||||
value = def_default;
|
||||
}
|
||||
|
||||
if ( definition.requires )
|
||||
for(const req_key of definition.requires)
|
||||
if ( ! req_key.startsWith('context.') && ! this.__cache.has(req_key) )
|
||||
this._get(req_key, initial, Array.from(visited));
|
||||
|
||||
if ( definition.process )
|
||||
value = definition.process(this, value, meta);
|
||||
}
|
||||
|
||||
this.__cache.set(key, value);
|
||||
this.__meta.set(key, meta);
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
_getRaw(key) {
|
||||
for(const profile of this.__profiles)
|
||||
if ( profile.has(key) )
|
||||
return [profile.get(key), profile]
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Data Access
|
||||
// ========================================================================
|
||||
|
||||
update(key) {
|
||||
this._update(key, key, []);
|
||||
}
|
||||
|
||||
get(key) {
|
||||
if ( key.startsWith('context.') )
|
||||
return getter(key.slice(8), this.__context);
|
||||
|
||||
if ( this.__cache.has(key) )
|
||||
return this.__cache.get(key);
|
||||
|
||||
return this._get(key, key, []);
|
||||
}
|
||||
|
||||
uses(key) {
|
||||
if ( key.startsWith('context.') )
|
||||
return null;
|
||||
|
||||
if ( ! this.__meta.has(key) )
|
||||
this._get(key, key, []);
|
||||
|
||||
return this.__meta.get(key).uses;
|
||||
}
|
||||
}
|
452
src/settings/index.js
Normal file
452
src/settings/index.js
Normal file
|
@ -0,0 +1,452 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Settings System
|
||||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
import {has} from 'utilities/object';
|
||||
|
||||
import {CloudStorageProvider, LocalStorageProvider} from './providers';
|
||||
import SettingsProfile from './profile';
|
||||
import SettingsContext from './context';
|
||||
import MigrationManager from './migration';
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// SettingsManager
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* The SettingsManager module creates all the necessary class instances
|
||||
* required for the settings system to operate, facilitates communication
|
||||
* and discovery, and emits events for other modules to react to.
|
||||
* @extends Module
|
||||
*/
|
||||
export default class SettingsManager extends Module {
|
||||
/**
|
||||
* Create a SettingsManager module.
|
||||
*/
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
// State
|
||||
this.__contexts = [];
|
||||
this.__profiles = [];
|
||||
this.__profile_ids = {};
|
||||
|
||||
this.ui_structures = new Map;
|
||||
this.definitions = new Map;
|
||||
|
||||
// Create our provider as early as possible.
|
||||
const provider = this.provider = this._createProvider();
|
||||
this.log.info(`Using Provider: ${provider.constructor.name}`);
|
||||
provider.on('changed', this._onProviderChange, this);
|
||||
|
||||
|
||||
this.migrations = new MigrationManager(this);
|
||||
|
||||
|
||||
// Also create the main context as early as possible.
|
||||
this.main_context = new SettingsContext(this);
|
||||
|
||||
this.main_context.on('changed', (key, new_value, old_value) => {
|
||||
this.emit(`:changed:${key}`, new_value, old_value);
|
||||
});
|
||||
|
||||
this.main_context.on('uses_changed', (key, new_uses, old_uses) => {
|
||||
this.emit(`:uses_changed:${key}`, new_uses, old_uses);
|
||||
});
|
||||
|
||||
|
||||
// Don't wait around to be required.
|
||||
this._start_time = performance.now();
|
||||
this.enable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the SettingsManager instance should be enabled.
|
||||
*/
|
||||
async onEnable() {
|
||||
// Before we do anything else, make sure the provider is ready.
|
||||
await this.provider.awaitReady();
|
||||
|
||||
// Load profiles, but don't run any events because we haven't done
|
||||
// migrations yet.
|
||||
this.loadProfiles(true);
|
||||
|
||||
// Handle migrations.
|
||||
await this.migrations.process('core');
|
||||
|
||||
// Now we can tell our context(s) about the profiles we have.
|
||||
for(const context of this.__contexts)
|
||||
context.selectProfiles();
|
||||
|
||||
const duration = performance.now() - this._start_time;
|
||||
this.log.info(`Initialization complete after ${duration.toFixed(5)}ms -- Values: ${this.provider.size} -- Profiles: ${this.__profiles.length}`)
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Provider Interaction
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Evaluate the environment that FFZ is running in and then decide which
|
||||
* provider should be used to retrieve and store settings.
|
||||
*/
|
||||
_createProvider() {
|
||||
// If the loader has reported support for cloud settings...
|
||||
if ( document.body.classList.contains('ffz-cloud-storage') )
|
||||
return new CloudStorageProvider(this);
|
||||
|
||||
// Fallback
|
||||
return new LocalStorageProvider(this);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* React to a setting that has changed elsewhere. Generally, this is
|
||||
* the result of a setting being changed in another tab or, when cloud
|
||||
* settings are enabled, on another computer.
|
||||
*/
|
||||
_onProviderChange(key, new_value, deleted) {
|
||||
// If profiles have changed, reload our profiles.
|
||||
if ( key === 'profiles' )
|
||||
return this.loadProfiles();
|
||||
|
||||
|
||||
// If we're still here, it means an individual setting was changed.
|
||||
// Look up the profile it belongs to and emit a changed event from
|
||||
// that profile, thus notifying any contexts or UI instances.
|
||||
const idx = key.indexOf(':');
|
||||
if ( idx === -1 )
|
||||
return;
|
||||
|
||||
const profile = this.__profile_ids[key.slice(0, idx)],
|
||||
s_key = key.slice(idx + 1);
|
||||
|
||||
if ( profile )
|
||||
profile.emit('changed', s_key, new_value, deleted);
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Profile Management
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Get an existing {@link SettingsProfile} instance.
|
||||
* @param {number} id - The id of the profile.
|
||||
*/
|
||||
profile(id) {
|
||||
return this.__profile_ids[id] || null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Build {@link SettingsProfile} instances for all of the profiles
|
||||
* defined in storage, re-using existing instances when possible.
|
||||
*/
|
||||
loadProfiles(suppress_events) {
|
||||
const old_profile_ids = this.__profile_ids,
|
||||
old_profiles = this.__profiles,
|
||||
|
||||
profile_ids = this.__profile_ids = {},
|
||||
profiles = this.__profiles = [],
|
||||
|
||||
// Create a set of actual IDs with a map from the profiles
|
||||
// list rather than just getting the keys from the ID map
|
||||
// because the ID map is an object and coerces its strings
|
||||
// to keys.
|
||||
old_ids = new Set(old_profiles.map(x => x.id));
|
||||
|
||||
let changed = false,
|
||||
moved_ids = new Set,
|
||||
new_ids = new Set,
|
||||
changed_ids = new Set;
|
||||
|
||||
const raw_profiles = this.provider.get('profiles', [
|
||||
SettingsProfile.Moderation,
|
||||
SettingsProfile.Default
|
||||
]);
|
||||
|
||||
for(const profile_data of raw_profiles) {
|
||||
const id = profile_data.id,
|
||||
old_profile = old_profile_ids[id],
|
||||
old_slot_id = parseInt(old_profiles[profiles.length] || -1, 10);
|
||||
|
||||
old_ids.delete(id);
|
||||
|
||||
if ( old_slot_id !== id ) {
|
||||
moved_ids.add(old_slot_id);
|
||||
moved_ids.add(id);
|
||||
}
|
||||
|
||||
// TODO: Better method for checking if the profile data has changed.
|
||||
if ( old_profile && JSON.stringify(old_profile.data) === JSON.stringify(profile_data) ) {
|
||||
// Did the order change?
|
||||
if ( old_profiles[profiles.length] !== old_profile )
|
||||
changed = true;
|
||||
|
||||
profiles.push(profile_ids[id] = old_profile);
|
||||
continue;
|
||||
}
|
||||
|
||||
const new_profile = profiles.push(profile_ids[id] = new SettingsProfile(this, profile_data));
|
||||
if ( old_profile ) {
|
||||
// Move all the listeners over.
|
||||
new_profile.__listeners = old_profile.__listeners;
|
||||
old_profile.__listeners = {};
|
||||
|
||||
changed_ids.add(id);
|
||||
|
||||
} else
|
||||
new_ids.add(id);
|
||||
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if ( ! changed && ! old_ids.size || suppress_events )
|
||||
return;
|
||||
|
||||
for(const context of this.__contexts)
|
||||
context.selectProfiles();
|
||||
|
||||
for(const id of new_ids)
|
||||
this.emit(':profile-created', profile_ids[id]);
|
||||
|
||||
for(const id of changed_ids)
|
||||
this.emit(':profile-changed', profile_ids[id]);
|
||||
|
||||
if ( moved_ids.size )
|
||||
this.emit(':profiles-reordered');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new profile and return the {@link SettingsProfile} instance
|
||||
* representing it.
|
||||
* @returns {SettingsProfile}
|
||||
*/
|
||||
createProfile(options) {
|
||||
let i = 0;
|
||||
while( this.__profile_ids[i] )
|
||||
i++;
|
||||
|
||||
options = options || {};
|
||||
options.id = i;
|
||||
|
||||
if ( ! options.name )
|
||||
options.name = `Unnamed Profile ${i}`;
|
||||
|
||||
const profile = this.__profile_ids[i] = new SettingsProfile(this, options);
|
||||
|
||||
this.__profiles.unshift(profile);
|
||||
|
||||
this._saveProfiles();
|
||||
this.emit(':profile-created', profile);
|
||||
return profile;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete a profile.
|
||||
* @param {number|SettingsProfile} id - The profile to delete
|
||||
*/
|
||||
deleteProfile(id) {
|
||||
if ( typeof id === 'object' && id.id )
|
||||
id = id.id;
|
||||
|
||||
const profile = this.__profile_ids[id];
|
||||
if ( ! profile )
|
||||
return;
|
||||
|
||||
if ( profile.id === 0 )
|
||||
throw new Error('cannot delete default profile');
|
||||
|
||||
profile.clear();
|
||||
this.__profile_ids[id] = null;
|
||||
|
||||
const idx = this.__profiles.indexOf(profile);
|
||||
if ( idx !== -1 )
|
||||
this.__profiles.splice(idx, 1);
|
||||
|
||||
this._saveProfiles();
|
||||
this.emit(':profile-deleted', profile);
|
||||
}
|
||||
|
||||
|
||||
moveProfile(id, index) {
|
||||
if ( typeof id === 'object' && id.id )
|
||||
id = id.id;
|
||||
|
||||
const profile = this.__profile_ids[id];
|
||||
if ( ! profile )
|
||||
return;
|
||||
|
||||
const profiles = this.__profiles,
|
||||
idx = profiles.indexOf(profile);
|
||||
if ( idx === index )
|
||||
return;
|
||||
|
||||
profiles.splice(index, 0, ...profiles.splice(idx, 1));
|
||||
|
||||
this._saveProfiles();
|
||||
this.emit(':profiles-reordered');
|
||||
}
|
||||
|
||||
|
||||
saveProfile(id) {
|
||||
if ( typeof id === 'object' && id.id )
|
||||
id = id.id;
|
||||
|
||||
const profile = this.__profile_ids[id];
|
||||
if ( ! profile )
|
||||
return;
|
||||
|
||||
this._saveProfiles();
|
||||
this.emit(':profile-changed', profile);
|
||||
}
|
||||
|
||||
|
||||
_saveProfiles() {
|
||||
this.provider.set('profiles', this.__profiles.map(prof => prof.data));
|
||||
for(const context of this.__contexts)
|
||||
context.selectProfiles();
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Context Helpers
|
||||
// ========================================================================
|
||||
|
||||
context(env) { return this.main_context.context(env) }
|
||||
get(key) { return this.main_context.get(key) }
|
||||
uses(key) { return this.main_context.uses(key) }
|
||||
update(key) { return this.main_context.update(key) }
|
||||
|
||||
updateContext(context) { return this.main_context.updateContext(context) }
|
||||
setContext(context) { return this.main_context.setContext(context) }
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Definitions
|
||||
// ========================================================================
|
||||
|
||||
add(key, definition) {
|
||||
if ( typeof key === 'object' ) {
|
||||
for(const k in key)
|
||||
if ( has(key, k) )
|
||||
this.add(k, key[k]);
|
||||
return;
|
||||
}
|
||||
|
||||
const old_definition = this.definitions.get(key),
|
||||
required_by = old_definition ?
|
||||
(Array.isArray(old_definition) ? old_definition : old_definition.required_by) : [];
|
||||
|
||||
definition.required_by = required_by;
|
||||
definition.requires = definition.requires || [];
|
||||
|
||||
for(const req_key of definition.requires) {
|
||||
const req = this.definitions.get(req_key);
|
||||
if ( ! req )
|
||||
this.definitions.set(req_key, [key]);
|
||||
else if ( Array.isArray(req) )
|
||||
req.push(key);
|
||||
else
|
||||
req.required_by.push(key);
|
||||
}
|
||||
|
||||
|
||||
if ( definition.ui ) {
|
||||
const ui = definition.ui;
|
||||
ui.path_tokens = ui.path_tokens ?
|
||||
format_path_tokens(ui.path_tokens) :
|
||||
ui.path ?
|
||||
parse_path(ui.path) :
|
||||
undefined;
|
||||
|
||||
if ( ! ui.key && ui.title )
|
||||
ui.key = ui.title.toSnakeCase();
|
||||
}
|
||||
|
||||
if ( definition.changed )
|
||||
this.on(`:changed:${key}`, definition.changed);
|
||||
|
||||
this.definitions.set(key, definition);
|
||||
this.emit(':added-definition', key, definition);
|
||||
}
|
||||
|
||||
|
||||
addUI(key, definition) {
|
||||
if ( typeof key === 'object' ) {
|
||||
for(const k in key)
|
||||
if ( has(key, k) )
|
||||
this.add(k, key[k]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! definition.ui )
|
||||
definition = {ui: definition};
|
||||
|
||||
const ui = definition.ui;
|
||||
ui.path_tokens = ui.path_tokens ?
|
||||
format_path_tokens(ui.path_tokens) :
|
||||
ui.path ?
|
||||
parse_path(ui.path) :
|
||||
undefined;
|
||||
|
||||
if ( ! ui.key && ui.title )
|
||||
ui.key = ui.title.toSnakeCase();
|
||||
|
||||
this.ui_structures.set(key, definition);
|
||||
this.emit(':added-definition', key, definition);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const PATH_SPLITTER = /(?:^|\s*([~>]+))\s*([^~>@]+)\s*(?:@([^~>]+))?/g;
|
||||
|
||||
export function parse_path(path) {
|
||||
const tokens = [];
|
||||
let match;
|
||||
|
||||
while((match = PATH_SPLITTER.exec(path))) {
|
||||
const page = match[1] === '>>',
|
||||
tab = match[1] === '~>',
|
||||
title = match[2].trim(),
|
||||
key = title.toSnakeCase(),
|
||||
options = match[3],
|
||||
|
||||
opts = { key, title, page, tab };
|
||||
|
||||
if ( options )
|
||||
Object.assign(opts, JSON.parse(options));
|
||||
|
||||
tokens.push(opts);
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
|
||||
export function format_path_tokens(tokens) {
|
||||
for(let i=0, l = tokens.length; i < l; i++) {
|
||||
const token = tokens[i];
|
||||
if ( typeof token === 'string' ) {
|
||||
tokens[i] = {
|
||||
key: token.toSnakeCase(),
|
||||
title: token
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! token.key )
|
||||
token.key = token.title.toSnakeCase();
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
16
src/settings/migration.js
Normal file
16
src/settings/migration.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Settings Migrations
|
||||
// ============================================================================
|
||||
|
||||
export default class MigrationManager {
|
||||
constructor(manager) {
|
||||
this.manager = manager;
|
||||
this.provider = manager.provider;
|
||||
}
|
||||
|
||||
process(key) {
|
||||
return false;
|
||||
}
|
||||
}
|
178
src/settings/profile.js
Normal file
178
src/settings/profile.js
Normal file
|
@ -0,0 +1,178 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Settings Profiles
|
||||
// ============================================================================
|
||||
|
||||
import {EventEmitter} from 'utilities/events';
|
||||
import {has, filter_match} from 'utilities/object';
|
||||
|
||||
|
||||
/**
|
||||
* Instances of SettingsProfile are used for getting and setting raw settings
|
||||
* values, enumeration, and emit events when the raw settings are changed.
|
||||
* @extends EventEmitter
|
||||
*/
|
||||
export default class SettingsProfile extends EventEmitter {
|
||||
constructor(manager, data) {
|
||||
super();
|
||||
|
||||
this.manager = manager;
|
||||
this.provider = manager.provider;
|
||||
|
||||
this.data = data;
|
||||
this.prefix = `p:${this.id}:`;
|
||||
}
|
||||
|
||||
get data() {
|
||||
return {
|
||||
id: this.id,
|
||||
parent: this.parent,
|
||||
|
||||
name: this.name,
|
||||
i18n_key: this.i18n_key,
|
||||
|
||||
description: this.description,
|
||||
desc_i18n_key: this.desc_i18n_key,
|
||||
|
||||
context: this.context
|
||||
}
|
||||
}
|
||||
|
||||
set data(val) {
|
||||
if ( typeof val !== 'object' )
|
||||
throw new TypeError('data must be an object');
|
||||
|
||||
for(const key in val)
|
||||
if ( has(val, key) )
|
||||
this[key] = val[key];
|
||||
}
|
||||
|
||||
matches(context) {
|
||||
// If we don't have any specific context, then we work!
|
||||
if ( ! this.context )
|
||||
return true;
|
||||
|
||||
// If we do have context and didn't get any, then we don't!
|
||||
else if ( ! context )
|
||||
return false;
|
||||
|
||||
// Got context? Have context? One-sided deep comparison time.
|
||||
// Let's go for a walk!
|
||||
|
||||
return filter_match(this.context, context);
|
||||
}
|
||||
|
||||
|
||||
save() {
|
||||
this.manager.saveProfile(this.id);
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Context
|
||||
// ========================================================================
|
||||
|
||||
updateContext(context) {
|
||||
if ( this.id === 0 )
|
||||
throw new Error('cannot set context of default profile');
|
||||
|
||||
this.context = Object.assign(this.context || {}, context);
|
||||
this.manager._saveProfiles();
|
||||
}
|
||||
|
||||
setContext(context) {
|
||||
if ( this.id === 0 )
|
||||
throw new Error('cannot set context of default profile');
|
||||
|
||||
this.context = context;
|
||||
this.manager._saveProfiles();
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Setting Access
|
||||
// ========================================================================
|
||||
|
||||
get(key, default_value) {
|
||||
return this.provider.get(this.prefix + key, default_value);
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
this.provider.set(this.prefix + key, value);
|
||||
this.emit('changed', key, value);
|
||||
}
|
||||
|
||||
delete(key) {
|
||||
this.provider.delete(this.prefix + key);
|
||||
this.emit('changed', key, undefined, true);
|
||||
}
|
||||
|
||||
has(key) {
|
||||
return this.provider.has(this.prefix + key);
|
||||
}
|
||||
|
||||
keys() {
|
||||
const out = [],
|
||||
p = this.prefix,
|
||||
len = p.length;
|
||||
|
||||
for(const key of this.provider.keys())
|
||||
if ( key.startsWith(p) )
|
||||
out.push(key.slice(len));
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
clear() {
|
||||
const p = this.prefix,
|
||||
len = p.length;
|
||||
for(const key of this.provider.keys())
|
||||
if ( key.startsWith(p) ) {
|
||||
this.provider.delete(key);
|
||||
this.emit('changed', key.slice(len), undefined, true);
|
||||
}
|
||||
}
|
||||
|
||||
*entries() {
|
||||
const p = this.prefix,
|
||||
len = p.length;
|
||||
|
||||
for(const key of this.provider.keys())
|
||||
if ( key.startsWith(p) )
|
||||
yield [key.slice(len), this.provider.get(key)];
|
||||
}
|
||||
|
||||
get size() {
|
||||
const p = this.prefix;
|
||||
let count = 0;
|
||||
|
||||
for(const key of this.provider.keys())
|
||||
if ( key.startsWith(p) )
|
||||
count++;
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
SettingsProfile.Default = {
|
||||
id: 0,
|
||||
name: 'Default Profile',
|
||||
i18n_key: 'setting.profiles.default',
|
||||
|
||||
description: 'Settings that apply everywhere on Twitch.'
|
||||
}
|
||||
|
||||
|
||||
SettingsProfile.Moderation = {
|
||||
id: 1,
|
||||
name: 'Moderation',
|
||||
i18n_key: 'setting.profiles.moderation',
|
||||
|
||||
description: 'Settings that apply when you are a moderator of the current channel.',
|
||||
|
||||
context: {
|
||||
moderator: true
|
||||
}
|
||||
}
|
301
src/settings/providers.js
Normal file
301
src/settings/providers.js
Normal file
|
@ -0,0 +1,301 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Settings Providers
|
||||
// ============================================================================
|
||||
|
||||
import {EventEmitter} from 'utilities/events';
|
||||
import {has} from 'utilities/object';
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// SettingsProvider
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Base class for providers for the settings system. A provider is in charge
|
||||
* of reading and writing values from storage as well as sending events to
|
||||
* the {@link SettingsManager} when a value is changed remotely.
|
||||
*
|
||||
* @extends EventEmitter
|
||||
*/
|
||||
export class SettingsProvider extends EventEmitter {
|
||||
/**
|
||||
* Create a new SettingsProvider
|
||||
* @param {SettingsManager} manager - The manager that owns this provider.
|
||||
*/
|
||||
constructor(manager) {
|
||||
super();
|
||||
|
||||
this.manager = manager;
|
||||
this.disabled = false;
|
||||
}
|
||||
|
||||
awaitReady() {
|
||||
if ( this.ready )
|
||||
return Promise.resolve();
|
||||
|
||||
return Promise.reject(new Error('Not Implemented'));
|
||||
}
|
||||
|
||||
get(key, default_value) { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this, no-unused-vars
|
||||
set(key, value) { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this, no-unused-vars
|
||||
delete(key) { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this, no-unused-vars
|
||||
clear() { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this
|
||||
|
||||
has(key) { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this, no-unused-vars
|
||||
|
||||
keys() { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this
|
||||
entries() { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this
|
||||
get size() { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// LocalStorage
|
||||
// ============================================================================
|
||||
|
||||
export class LocalStorageProvider extends SettingsProvider {
|
||||
constructor(manager, prefix) {
|
||||
super(manager);
|
||||
this.prefix = prefix = prefix == null ? 'FFZ:setting:' : prefix;
|
||||
|
||||
const cache = this._cached = new Map,
|
||||
len = prefix.length;
|
||||
|
||||
for(const key in localStorage)
|
||||
if ( has(localStorage, key) && key.startsWith(prefix) ) {
|
||||
const val = localStorage.getItem(key);
|
||||
try {
|
||||
cache.set(key.slice(len), JSON.parse(val));
|
||||
} catch(err) {
|
||||
this.manager.log.warn(`unable to parse value for ${key}`, val);
|
||||
}
|
||||
}
|
||||
|
||||
this.ready = true;
|
||||
|
||||
this._boundHandleStorage = this.handleStorage.bind(this);
|
||||
window.addEventListener('storage', this._boundHandleStorage);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.disable();
|
||||
this._cached.clear();
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.disabled = true;
|
||||
|
||||
if ( this._boundHandleStorage ) {
|
||||
window.removeEventListener('storage', this._boundHandleStorage);
|
||||
this._boundHandleStorage = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
handleStorage(event) {
|
||||
if ( this.disabled )
|
||||
return;
|
||||
|
||||
this.manager.log.debug('storage event', event);
|
||||
if ( event.storageArea !== localStorage )
|
||||
return;
|
||||
|
||||
if ( event.key.startsWith(this.prefix) ) {
|
||||
// If value is null, the key was deleted.
|
||||
const key = event.key.slice(this.prefix.length);
|
||||
let val = event.newValue;
|
||||
|
||||
if ( val === null ) {
|
||||
this._cached.delete(key);
|
||||
this.emit('changed', key, undefined, true);
|
||||
|
||||
} else {
|
||||
val = JSON.parse(val);
|
||||
this._cached.set(key, val);
|
||||
this.emit('changed', key, val, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
get(key, default_value) {
|
||||
return this._cached.has(key) ?
|
||||
this._cached.get(key) :
|
||||
default_value;
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
this._cached.set(key, value);
|
||||
localStorage.setItem(this.prefix + key, JSON.stringify(value));
|
||||
}
|
||||
|
||||
delete(key) {
|
||||
this._cached.delete(key);
|
||||
localStorage.removeItem(this.prefix + key);
|
||||
}
|
||||
|
||||
has(key) {
|
||||
return this._cached.has(key);
|
||||
}
|
||||
|
||||
keys() {
|
||||
return this._cached.keys();
|
||||
}
|
||||
|
||||
clear() {
|
||||
for(const key of this._cached.keys())
|
||||
localStorage.removeItem(this.prefix + key);
|
||||
|
||||
this._cached.clear();
|
||||
}
|
||||
|
||||
entries() {
|
||||
return this._cached.entries();
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this._cached.size;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class CloudStorageProvider extends SettingsProvider {
|
||||
constructor(manager) {
|
||||
super(manager);
|
||||
|
||||
this._cached = new Map;
|
||||
this.ready = false;
|
||||
this._ready_wait = null;
|
||||
|
||||
this._boundHandleStorage = this.handleStorage.bind(this);
|
||||
window.addEventListener('message', this._boundHandleStorage);
|
||||
this._send('get_all');
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.disable();
|
||||
this._cached.clear();
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.disabled = true;
|
||||
|
||||
if ( this._boundHandleStorage ) {
|
||||
window.removeEventListener('message', this._boundHandleStorage)
|
||||
this._boundHandleStorage = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
awaitReady() {
|
||||
if ( this.ready )
|
||||
return Promise.resolve();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const waiters = this._ready_wait = this._ready_wait || [];
|
||||
waiters.push([resolve, reject]);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Communication
|
||||
// ========================================================================
|
||||
|
||||
handleStorage(event) {
|
||||
if ( event.source !== window || ! event.data || ! event.data.ffz )
|
||||
return;
|
||||
|
||||
const cmd = event.data.cmd,
|
||||
data = event.data.data;
|
||||
|
||||
if ( cmd === 'all_values' ) {
|
||||
const old_keys = new Set(this._cached.keys());
|
||||
|
||||
for(const key in data)
|
||||
if ( has(data, key) ) {
|
||||
const val = data[key];
|
||||
old_keys.delete(key);
|
||||
this._cached.set(key, val);
|
||||
if ( this.ready )
|
||||
this.emit('changed', key, val);
|
||||
}
|
||||
|
||||
for(const key of old_keys) {
|
||||
this._cached.delete(key);
|
||||
if ( this.ready )
|
||||
this.emit('changed', key, undefined, true);
|
||||
}
|
||||
|
||||
this.ready = true;
|
||||
if ( this._ready_wait ) {
|
||||
for(const resolve of this._ready_wait)
|
||||
resolve();
|
||||
this._ready_wait = null;
|
||||
}
|
||||
|
||||
} else if ( cmd === 'changed' ) {
|
||||
this._cached.set(data.key, data.value);
|
||||
this.emit('changed', data.key, data.value);
|
||||
|
||||
} else if ( cmd === 'deleted' ) {
|
||||
this._cached.delete(data);
|
||||
this.emit('changed', data, undefined, true);
|
||||
|
||||
} else {
|
||||
this.manager.log.info('unknown storage event', event);
|
||||
}
|
||||
}
|
||||
|
||||
_send(cmd, data) { // eslint-disable-line class-methods-use-this
|
||||
window.postMessage({
|
||||
ffz: true,
|
||||
cmd,
|
||||
data
|
||||
}, location.origin);
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Data Access
|
||||
// ========================================================================
|
||||
|
||||
get(key, default_value) {
|
||||
return this._cached.has(key) ?
|
||||
this._cached.get(key) :
|
||||
default_value;
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
this._cached.set(key, value);
|
||||
this._send('set', {key, value});
|
||||
}
|
||||
|
||||
delete(key) {
|
||||
this._cached.delete(key);
|
||||
this._send('delete', key);
|
||||
}
|
||||
|
||||
has(key) {
|
||||
return this._cached.has(key);
|
||||
}
|
||||
|
||||
keys() {
|
||||
return this._cached.keys();
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._cached.clear();
|
||||
this._send('clear');
|
||||
}
|
||||
|
||||
entries() {
|
||||
return this._cached.entries();
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this._cached.size;
|
||||
}
|
||||
}
|
24
src/shims.js
24
src/shims.js
|
@ -1,24 +0,0 @@
|
|||
Array.prototype.equals = function (array) {
|
||||
// if the other array is a falsy value, return
|
||||
if (!array)
|
||||
return false;
|
||||
|
||||
// compare lengths - can save a lot of time
|
||||
if (this.length != array.length)
|
||||
return false;
|
||||
|
||||
for (var i = 0, l=this.length; i < l; i++) {
|
||||
// Check if we have nested arrays
|
||||
if (this[i] instanceof Array && array[i] instanceof Array) {
|
||||
// recurse into the nested arrays
|
||||
if (!this[i].equals(array[i]))
|
||||
return false;
|
||||
}
|
||||
else if (this[i] != array[i]) {
|
||||
// Warning - two different object instances will never be equal: {x:20} != {x:20}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
106
src/sites/base.js
Normal file
106
src/sites/base.js
Normal file
|
@ -0,0 +1,106 @@
|
|||
'use strict';
|
||||
|
||||
import Module from 'utilities/module';
|
||||
|
||||
let last_site = 0;
|
||||
let last_call = 0;
|
||||
|
||||
export default class BaseSite extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this._id = `_ffz$${last_site++}`;
|
||||
|
||||
this.inject('settings');
|
||||
|
||||
this.log.info(`Using: ${this.constructor.name}`);
|
||||
}
|
||||
|
||||
populateModules() {
|
||||
const ctx = require.context('site/modules', true, /(?:^(?:\.\/)?[^/]+|index)\.js$/);
|
||||
const modules = this.populate(ctx, this.log);
|
||||
this.log.info(`Loaded descriptions of ${Object.keys(modules).length} modules.`);
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// DOM Manipulation
|
||||
// ========================================================================
|
||||
|
||||
awaitElement(selector, parent, timeout = 60000) {
|
||||
if ( ! parent )
|
||||
parent = document.documentElement;
|
||||
|
||||
const el = parent.querySelector(selector);
|
||||
if ( el )
|
||||
return Promise.resolve(el);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const observer_name = `${this._id}$observer`,
|
||||
data = parent[observer_name],
|
||||
call_id = last_call++,
|
||||
|
||||
timer = timeout && setTimeout(() => {
|
||||
const data = parent[observer_name];
|
||||
if ( ! data )
|
||||
return;
|
||||
|
||||
const [observer, selectors] = data;
|
||||
|
||||
for(let i=0; i < selectors.length; i++) {
|
||||
const d = selectors[i];
|
||||
if ( d[0] === call_id ) {
|
||||
selectors.splice(i, 1);
|
||||
d[3]('Timed out');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! selectors.length ) {
|
||||
observer.disconnect();
|
||||
parent[observer_name] = null;
|
||||
}
|
||||
}, timeout);
|
||||
|
||||
if ( data ) {
|
||||
data[1].push([call_id, selector, resolve, reject, timer]);
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
const data = parent[observer_name];
|
||||
if ( ! data ) {
|
||||
observer.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
const selectors = data[1];
|
||||
for(let i=0; i < selectors.length; i++) {
|
||||
const d = selectors[i];
|
||||
const el = parent.querySelector(d[1]);
|
||||
if ( el ) {
|
||||
selectors.splice(i, 1);
|
||||
i--;
|
||||
|
||||
if ( d[4] )
|
||||
clearTimeout(d[4]);
|
||||
|
||||
d[2](el);
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! selectors.length ) {
|
||||
observer.disconnect();
|
||||
parent[observer_name] = null;
|
||||
}
|
||||
});
|
||||
|
||||
parent[observer_name] = [observer, [[call_id, selector, resolve, reject, timer]]];
|
||||
observer.observe(parent, {
|
||||
childList: true,
|
||||
attributes: true,
|
||||
characterData: true,
|
||||
subtree: true
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
113
src/sites/twitch-twilight/index.js
Normal file
113
src/sites/twitch-twilight/index.js
Normal file
|
@ -0,0 +1,113 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Site Support: Twitch Twilight
|
||||
// ============================================================================
|
||||
|
||||
import BaseSite from '../base';
|
||||
|
||||
import WebMunch from 'utilities/compat/webmunch';
|
||||
import Fine from 'utilities/compat/fine';
|
||||
import FineRouter from 'utilities/compat/fine-router';
|
||||
import Apollo from 'utilities/compat/apollo';
|
||||
|
||||
import {createElement as e} from 'utilities/dom';
|
||||
|
||||
import MAIN_URL from 'site/styles/main.scss';
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// The Site
|
||||
// ============================================================================
|
||||
|
||||
export default class Twilight extends BaseSite {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.inject(WebMunch);
|
||||
this.inject(Fine);
|
||||
this.inject('router', FineRouter);
|
||||
this.inject(Apollo);
|
||||
}
|
||||
|
||||
onLoad() {
|
||||
this.populateModules();
|
||||
|
||||
this.web_munch.known(Twilight.KNOWN_MODULES);
|
||||
this.router.route(Twilight.ROUTES);
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
const root = this.fine.getParent(this.fine.react),
|
||||
ctx = this.context = root && root._context,
|
||||
|
||||
store = this.store = ctx && ctx.store;
|
||||
|
||||
if ( ! store )
|
||||
return new Promise(r => setTimeout(r, 50)).then(() => this.onEnable());
|
||||
|
||||
// Share Context
|
||||
store.subscribe(() => this.updateContext());
|
||||
this.updateContext();
|
||||
|
||||
this.router.on(':route', (route, match) => {
|
||||
this.log.info('Navigation', route && route.name, match[0]);
|
||||
});
|
||||
|
||||
document.head.appendChild(e('link', {
|
||||
href: MAIN_URL,
|
||||
rel: 'stylesheet',
|
||||
type: 'text/css'
|
||||
}));
|
||||
}
|
||||
|
||||
updateContext() {
|
||||
const state = this.store.getState();
|
||||
this.settings.updateContext({
|
||||
location: state.router.location,
|
||||
|
||||
ui: state.ui,
|
||||
session: state.session
|
||||
});
|
||||
}
|
||||
|
||||
getSession() {
|
||||
const state = this.store && this.store.getState();
|
||||
return state && state.session;
|
||||
}
|
||||
|
||||
getUser() {
|
||||
const session = this.getSession();
|
||||
return session && session.user;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Twilight.KNOWN_MODULES = {
|
||||
simplebar: n => n.globalObserver && n.initDOMLoadedElements,
|
||||
react: n => n.Component && n.createElement,
|
||||
'extension-service': n => n.extensionService
|
||||
}
|
||||
|
||||
|
||||
Twilight.ROUTES = {
|
||||
'front-page': '/',
|
||||
'collection': '/collections/:collectionID',
|
||||
'dir-community': '/communities/:communityName',
|
||||
'dir-community-index': '/directory/communities',
|
||||
'dir-creative': '/directory/creative',
|
||||
'dir-following': '/directory/following/:category?',
|
||||
'dir-game-clips': '/directory/game/:gameName/clips',
|
||||
'dir-game-details': '/directory/game/:gameName/details',
|
||||
'dir-game-videos': '/directory/game/:gameName/videos/:filter',
|
||||
'dir-game-index': '/directory/game/:gameName',
|
||||
'dir-all': '/directory/all/:filter?',
|
||||
'dir-category': '/directory/:category?',
|
||||
'event': '/event/:eventName',
|
||||
'following': '/following',
|
||||
'popout': '/popout',
|
||||
'video': '/videos/:videoID',
|
||||
'user-videos': '/:userName/videos/:filter?',
|
||||
'user-clips': '/:userName/manager/clips',
|
||||
'user': '/:userName'
|
||||
}
|
93
src/sites/twitch-twilight/modules/channel_bar.js
Normal file
93
src/sites/twitch-twilight/modules/channel_bar.js
Normal file
|
@ -0,0 +1,93 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Channel Bar
|
||||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
import {sanitize, createElement as e} from 'utilities/dom';
|
||||
|
||||
export default class ChannelBar extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.should_enable = true;
|
||||
|
||||
this.inject('site.fine');
|
||||
this.inject('metadata');
|
||||
|
||||
|
||||
this.ChannelBar = this.fine.define(
|
||||
'channel-bar',
|
||||
n => n.getTitle && n.getGame && n.renderGame
|
||||
);
|
||||
|
||||
|
||||
this.HostBar = this.fine.define(
|
||||
'host-container',
|
||||
n => n.handleReportHosterClick
|
||||
)
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
this.ChannelBar.ready((cls, instances) => {
|
||||
for(const inst of instances)
|
||||
this.updateChannelBar(inst);
|
||||
});
|
||||
|
||||
this.ChannelBar.on('unmount', this.unmountChannelBar, this);
|
||||
this.ChannelBar.on('mount', this.updateChannelBar, this);
|
||||
this.ChannelBar.on('update', this.updateChannelBar, this);
|
||||
|
||||
this.HostBar.on('mount', inst => {
|
||||
this.log.info('host-mount', inst, this.fine.getHostNode(inst));
|
||||
});
|
||||
|
||||
this.HostBar.ready((cls, instances) => {
|
||||
for(const inst of instances)
|
||||
this.log.info('host-found', inst, this.fine.getHostNode(inst));
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
unmountChannelBar(inst) { // eslint-disable-line class-methods-use-this
|
||||
const timers = inst._ffz_meta_timers;
|
||||
if ( timers )
|
||||
for(const key in timers)
|
||||
if ( timers[key] )
|
||||
clearTimeout(timers[key]);
|
||||
|
||||
inst._ffz_meta_timers = null;
|
||||
}
|
||||
|
||||
|
||||
updateChannelBar(inst) {
|
||||
this.updateMetadata(inst);
|
||||
}
|
||||
|
||||
|
||||
updateMetadata(inst, keys) {
|
||||
const container = this.fine.getHostNode(inst),
|
||||
metabar = container && container.querySelector && container.querySelector('.channel-info-bar__action-container > .flex');
|
||||
|
||||
if ( ! inst._ffz_mounted || ! metabar )
|
||||
return;
|
||||
|
||||
if ( ! keys )
|
||||
keys = this.metadata.keys;
|
||||
else if ( ! Array.isArray(keys) )
|
||||
keys = [keys];
|
||||
|
||||
const timers = inst._ffz_meta_timers = inst._ffz_meta_timers || {},
|
||||
refresh_func = key => this.updateMetadata(inst, key),
|
||||
data = {
|
||||
channel: inst.props.userData && inst.props.userData.user,
|
||||
hosting: false,
|
||||
_inst: inst
|
||||
}
|
||||
|
||||
for(const key of keys)
|
||||
this.metadata.render(key, data, metabar, timers, refresh_func);
|
||||
}
|
||||
}
|
704
src/sites/twitch-twilight/modules/chat/index.js
Normal file
704
src/sites/twitch-twilight/modules/chat/index.js
Normal file
|
@ -0,0 +1,704 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Chat Hooks
|
||||
// ============================================================================
|
||||
|
||||
import {ColorAdjuster} from 'utilities/color';
|
||||
import {setChildren} from 'utilities/dom';
|
||||
import {has} from 'utilities/object';
|
||||
|
||||
import Module from 'utilities/module';
|
||||
|
||||
const EVENTS = [
|
||||
'onJoinedEvent',
|
||||
'onDisconnectedEvent',
|
||||
'onReconnectingEvent',
|
||||
'onHostingEvent',
|
||||
'onUnhostEvent',
|
||||
'onChatMessageEvent',
|
||||
'onChatActionEvent',
|
||||
'onChatNoticeEvent',
|
||||
'onTimeoutEvent',
|
||||
'onBanEvent',
|
||||
'onModerationEvent',
|
||||
'onSubscriptionEvent',
|
||||
'onResubscriptionEvent',
|
||||
'onRoomStateEvent',
|
||||
'onSlowModeEvent',
|
||||
'onFollowerOnlyModeEvent',
|
||||
'onSubscriberOnlyModeEvent',
|
||||
'onClearChatEvent',
|
||||
'onRaidEvent',
|
||||
'onUnraidEvent',
|
||||
'onBadgesUpdatedEvent'
|
||||
];
|
||||
|
||||
|
||||
export default class ChatHook extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.should_enable = true;
|
||||
|
||||
this.colors = new ColorAdjuster;
|
||||
|
||||
this.inject('settings');
|
||||
|
||||
this.inject('site');
|
||||
this.inject('site.router');
|
||||
this.inject('site.fine');
|
||||
this.inject('site.web_munch');
|
||||
this.inject('site.css_tweaks');
|
||||
|
||||
this.inject('chat');
|
||||
|
||||
|
||||
this.ChatController = this.fine.define(
|
||||
'chat-controller',
|
||||
n => n.chatService
|
||||
);
|
||||
|
||||
this.ChatContainer = this.fine.define(
|
||||
'chat-container',
|
||||
n => n.showViewersList && n.onChatInputFocus
|
||||
);
|
||||
|
||||
this.ChatLine = this.fine.define(
|
||||
'chat-line',
|
||||
n => n.renderMessageBody
|
||||
);
|
||||
|
||||
this.PinnedCheer = this.fine.define(
|
||||
'pinned-cheer',
|
||||
n => n.collapseCheer && n.saveRenderedMessageRef
|
||||
);
|
||||
|
||||
|
||||
// Settings
|
||||
|
||||
this.settings.add('chat.width', {
|
||||
default: 340,
|
||||
ui: {
|
||||
path: 'Chat > Appearance >> General @{"sort": -1}',
|
||||
title: 'Width',
|
||||
description: 'How wide chat should be, in pixels.',
|
||||
component: 'setting-text-box',
|
||||
process(val) {
|
||||
val = parseInt(val, 10);
|
||||
if ( isNaN(val) || ! isFinite(val) || val <= 0 )
|
||||
return 340;
|
||||
|
||||
return val;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.bits.show-pinned', {
|
||||
default: true,
|
||||
ui: {
|
||||
path: 'Chat > Bits and Cheering >> Pinned Cheers',
|
||||
title: 'Display Pinned Cheer',
|
||||
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.lines.alternate', {
|
||||
default: false,
|
||||
ui: {
|
||||
path: 'Chat > Appearance >> Chat Lines',
|
||||
title: 'Display lines with alternating background colors.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.lines.padding', {
|
||||
default: false,
|
||||
ui: {
|
||||
path: 'Chat > Appearance >> Chat Lines',
|
||||
title: 'Reduce padding around lines.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.lines.borders', {
|
||||
default: 0,
|
||||
ui: {
|
||||
path: 'Chat > Appearance >> Chat Lines',
|
||||
title: 'Separators',
|
||||
component: 'setting-select-box',
|
||||
data: [
|
||||
{value: 0, title: 'Disabled'},
|
||||
{value: 1, title: 'Basic Line (1px Solid)'},
|
||||
{value: 2, title: '3D Line (2px Groove)'},
|
||||
{value: 3, title: '3D Line (2px Groove Inset)'},
|
||||
{value: 4, title: 'Wide Line (2px Solid)'}
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
get currentChat() {
|
||||
for(const inst of this.ChatController.instances)
|
||||
if ( inst && inst.chatService )
|
||||
return inst;
|
||||
}
|
||||
|
||||
|
||||
updateColors() {
|
||||
const is_dark = this.chat.context.get('theme.is-dark'),
|
||||
mode = this.chat.context.get('chat.adjustment-mode'),
|
||||
contrast = this.chat.context.get('chat.adjustment-contrast'),
|
||||
c = this.colors;
|
||||
|
||||
// TODO: Get the background color from the theme system.
|
||||
c._base = is_dark ? '#0e0c13' : '#faf9fa';
|
||||
c.mode = mode;
|
||||
c.contrast = contrast;
|
||||
|
||||
this.updateChatLines();
|
||||
}
|
||||
|
||||
|
||||
updateChatWidth() {
|
||||
const width = this.chat.context.get('chat.width');
|
||||
if ( width === 340 )
|
||||
this.css_tweaks.style.delete('chat-width');
|
||||
else
|
||||
this.css_tweaks.style.set('chat-width', `.channel-page__right-column{width:${width}px!important}`);
|
||||
}
|
||||
|
||||
updateLineBorders() {
|
||||
const mode = this.chat.context.get('chat.lines.borders');
|
||||
|
||||
this.css_tweaks.toggle('chat-borders', mode > 0);
|
||||
this.css_tweaks.toggle('chat-borders-3d', mode === 2);
|
||||
this.css_tweaks.toggle('chat-borders-3d-inset', mode === 3);
|
||||
this.css_tweaks.toggle('chat-borders-wide', mode === 4);
|
||||
}
|
||||
|
||||
|
||||
onEnable() {
|
||||
this.chat.context.on('changed:chat.width', this.updateChatWidth, this);
|
||||
this.chat.context.on('changed:chat.bits.stack', this.updateChatLines, this);
|
||||
this.chat.context.on('changed:chat.adjustment-mode', this.updateColors, this);
|
||||
this.chat.context.on('changed:chat.adjustment-contrast', this.updateColors, this);
|
||||
this.chat.context.on('changed:theme.is-dark', this.updateColors, this);
|
||||
this.chat.context.on('changed:chat.lines.borders', this.updateLineBorders, this);
|
||||
|
||||
this.chat.context.on('changed:chat.lines.alternate', val =>
|
||||
this.css_tweaks.toggle('chat-rows', val));
|
||||
|
||||
this.chat.context.on('changed:chat.lines.padding', val =>
|
||||
this.css_tweaks.toggle('chat-padding', val));
|
||||
|
||||
this.chat.context.on('changed:chat.bits.show', val =>
|
||||
this.css_tweaks.toggle('hide-bits', !val));
|
||||
this.chat.context.on('changed:chat.bits.show-pinned', val =>
|
||||
this.css_tweaks.toggleHide('pinned-cheer', !val));
|
||||
|
||||
this.css_tweaks.toggleHide('pinned-cheer', !this.chat.context.get('chat.bits.show-pinned'));
|
||||
this.css_tweaks.toggle('hide-bits', !this.chat.context.get('chat.bits.show'));
|
||||
this.css_tweaks.toggle('chat-rows', this.chat.context.get('chat.lines.alternate'));
|
||||
this.css_tweaks.toggle('chat-padding', this.chat.context.get('chat.lines.padding'));
|
||||
|
||||
this.updateChatWidth();
|
||||
this.updateColors();
|
||||
this.updateLineBorders();
|
||||
|
||||
this.ChatController.on('mount', this.chatMounted, this);
|
||||
this.ChatController.on('unmount', this.removeRoom, this);
|
||||
this.ChatController.on('receive-props', this.chatUpdated, this);
|
||||
|
||||
this.ChatController.ready((cls, instances) => {
|
||||
for(const inst of instances) {
|
||||
const service = inst.chatService;
|
||||
if ( ! service._ffz_was_here )
|
||||
this.wrapChatService(service.constructor);
|
||||
|
||||
service.client.events.removeAll();
|
||||
service.connectHandlers();
|
||||
|
||||
this.chatMounted(inst);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.ChatContainer.on('mount', this.containerMounted, this);
|
||||
this.ChatContainer.on('unmount', this.removeRoom, this);
|
||||
this.ChatContainer.on('receive-props', this.containerUpdated, this);
|
||||
|
||||
this.ChatContainer.ready((cls, instances) => {
|
||||
for(const inst of instances)
|
||||
this.containerMounted(inst);
|
||||
});
|
||||
|
||||
|
||||
this.PinnedCheer.on('mount', this.fixPinnedCheer, this);
|
||||
this.PinnedCheer.on('update', this.fixPinnedCheer, this);
|
||||
|
||||
this.PinnedCheer.ready((cls, instances) => {
|
||||
for(const inst of instances)
|
||||
this.fixPinnedCheer(inst);
|
||||
});
|
||||
|
||||
|
||||
const React = this.web_munch.getModule('react');
|
||||
|
||||
if ( React ) {
|
||||
const t = this,
|
||||
e = React.createElement;
|
||||
|
||||
this.ChatLine.ready((cls, instances) => {
|
||||
cls.prototype.shouldComponentUpdate = function(props, state) {
|
||||
const show = state.alwaysShowMessage || ! props.message.deleted,
|
||||
old_show = this._ffz_show;
|
||||
|
||||
// We can't just compare props.message.deleted to this.props.message.deleted
|
||||
// because the message object is the same object. So, store the old show
|
||||
// state for later reference.
|
||||
this._ffz_show = show;
|
||||
|
||||
return show !== old_show ||
|
||||
//state.renderDebug !== this.state.renderDebug ||
|
||||
props.message !== this.props.message ||
|
||||
props.isCurrentUserModerator !== this.props.isCurrentUserModerator ||
|
||||
props.showModerationIcons !== this.props.showModerationIcons ||
|
||||
props.showTimestamps !== this.props.showTimestamps;
|
||||
}
|
||||
|
||||
//const old_render = cls.prototype.render;
|
||||
|
||||
cls.prototype.render = function() {
|
||||
const msg = this.props.message,
|
||||
is_action = msg.type === 1,
|
||||
user = msg.user,
|
||||
color = t.colors.process(user.color),
|
||||
room = msg.channel ? msg.channel.slice(1) : undefined,
|
||||
|
||||
show = this.state.alwaysShowMessage || ! this.props.message.deleted;
|
||||
|
||||
if ( ! msg.message && msg.messageParts )
|
||||
detokenizeMessage(msg);
|
||||
|
||||
const tokens = t.chat.tokenizeMessage(msg),
|
||||
fragment = t.chat.renderTokens(tokens, e);
|
||||
|
||||
return e('div', {
|
||||
className: 'chat-line__message',
|
||||
'data-room-id': this.props.channelID,
|
||||
'data-room': room,
|
||||
'data-user-id': user.userID,
|
||||
'data-user': user.userLogin,
|
||||
|
||||
//onClick: () => this.setState({renderDebug: ((this.state.renderDebug||0) + 1) % 3})
|
||||
}, [
|
||||
this.props.showTimestamps && e('span', {
|
||||
className: 'chat-line__timestamp'
|
||||
}, t.chat.formatTime(msg.timestamp)),
|
||||
this.renderModerationIcons(),
|
||||
e('span', {
|
||||
className: 'chat-line__message--badges'
|
||||
}, t.chat.renderBadges(msg, e)),
|
||||
e('a', {
|
||||
className: 'chat-author__display-name',
|
||||
style: { color },
|
||||
onClick: this.usernameClickHandler
|
||||
}, user.userDisplayName),
|
||||
user.isIntl && e('span', {
|
||||
className: 'chat-author__intl-login',
|
||||
style: { color }
|
||||
}, ` (${user.userLogin})`),
|
||||
e('span', null, is_action ? ' ' : ': '),
|
||||
show ?
|
||||
e('span', {
|
||||
className:'message',
|
||||
style: is_action ? { color } : null
|
||||
}, fragment)
|
||||
:
|
||||
e('span', {
|
||||
className: 'chat-line__message--deleted',
|
||||
}, e('a', {
|
||||
href: '',
|
||||
onClick: this.alwaysShowMessage
|
||||
}, `<message deleted>`)),
|
||||
|
||||
/*this.state.renderDebug === 2 && e('div', {
|
||||
className: 'border mg-t-05'
|
||||
}, old_render.call(this)),
|
||||
|
||||
this.state.renderDebug === 1 && e('div', {
|
||||
className: 'message--debug',
|
||||
style: {
|
||||
fontFamily: 'monospace',
|
||||
whiteSpace: 'pre-wrap',
|
||||
lineHeight: '1.1em'
|
||||
}
|
||||
}, JSON.stringify([tokens, msg.emotes], null, 2))*/
|
||||
])
|
||||
}
|
||||
|
||||
for(const inst of instances)
|
||||
inst.forceUpdate();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
wrapChatService(cls) {
|
||||
const t = this,
|
||||
old_handler = cls.prototype.connectHandlers;
|
||||
|
||||
cls.prototype._ffz_was_here = true;
|
||||
|
||||
cls.prototype.connectHandlers = function(...args) {
|
||||
if ( ! this._ffz_init ) {
|
||||
const i = this,
|
||||
pm = this.postMessage;
|
||||
|
||||
for(const key of EVENTS) { // eslint-disable-line guard-for-in
|
||||
const original = this[key];
|
||||
if ( original )
|
||||
this[key] = function(e, t) {
|
||||
i._wrapped = e;
|
||||
const ret = original.call(i, e, t);
|
||||
i._wrapped = null;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
this.postMessage = function(e) {
|
||||
const original = this._wrapped;
|
||||
if ( original ) {
|
||||
// Check that the message is relevant to this channel.
|
||||
if ( original.channel && original.channel.slice(1) !== this.channelLogin )
|
||||
return;
|
||||
|
||||
const c = e.channel = original.channel;
|
||||
if ( c )
|
||||
e.roomLogin = c.charAt(0) === '#' ? c.slice(1) : c;
|
||||
|
||||
if ( original.message ) {
|
||||
if ( original.action )
|
||||
e.message = original.action;
|
||||
else
|
||||
e.message = original.message.body;
|
||||
|
||||
if ( original.message.user )
|
||||
e.emotes = original.message.user.emotes;
|
||||
}
|
||||
|
||||
//e.original = original;
|
||||
}
|
||||
|
||||
//t.log.info('postMessage', e);
|
||||
return pm.call(this, e);
|
||||
}
|
||||
|
||||
this._ffz_init = true;
|
||||
}
|
||||
|
||||
return old_handler.apply(this, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
updateChatLines() {
|
||||
for(const inst of this.PinnedCheer.instances)
|
||||
inst.forceUpdate();
|
||||
|
||||
for(const inst of this.ChatLine.instances)
|
||||
inst.forceUpdate();
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Pinned Cheers
|
||||
// ========================================================================
|
||||
|
||||
fixPinnedCheer(inst) {
|
||||
const el = this.fine.getHostNode(inst),
|
||||
container = el && el.querySelector && el.querySelector('.pinned-cheer__headline'),
|
||||
tc = inst.props.topCheer;
|
||||
|
||||
if ( ! container || ! tc )
|
||||
return;
|
||||
|
||||
container.dataset.roomId = inst.props.channelID;
|
||||
container.dataset.room = inst.props.channelLogin;
|
||||
container.dataset.userId = tc.user.userID;
|
||||
container.dataset.user = tc.user.userLogin;
|
||||
|
||||
if ( tc.user.color ) {
|
||||
const user_el = container.querySelector('.chat-author__display-name');
|
||||
if ( user_el )
|
||||
user_el.style.color = this.colors.process(tc.user.color);
|
||||
|
||||
const login_el = container.querySelector('.chat-author__intl-login');
|
||||
if ( login_el )
|
||||
login_el.style.color = this.colors.process(tc.user.color);
|
||||
}
|
||||
|
||||
const bit_el = container.querySelector('.chat-line__message--emote'),
|
||||
cont = bit_el ? bit_el.parentElement.parentElement : container.querySelector('.ffz--pinned-top-emote'),
|
||||
prefix = extractCheerPrefix(tc.messageParts);
|
||||
|
||||
if ( cont && prefix ) {
|
||||
const tokens = this.chat.tokenizeString(`${prefix}${tc.bits}`, tc);
|
||||
|
||||
cont.classList.add('ffz--pinned-top-emote');
|
||||
cont.innerHTML = '';
|
||||
setChildren(cont, this.chat.renderTokens(tokens));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Room Handling
|
||||
// ========================================================================
|
||||
|
||||
addRoom(thing, props) {
|
||||
if ( ! props )
|
||||
props = thing.props;
|
||||
|
||||
if ( ! props.channelID )
|
||||
return null;
|
||||
|
||||
const room = thing._ffz_room = this.chat.getRoom(props.channelID, props.channelLogin, false, true);
|
||||
room.ref(thing);
|
||||
return room;
|
||||
}
|
||||
|
||||
|
||||
removeRoom(thing) { // eslint-disable-line class-methods-use-this
|
||||
if ( ! thing._ffz_room )
|
||||
return;
|
||||
|
||||
thing._ffz_room.unref(thing);
|
||||
thing._ffz_room = null;
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Chat Controller
|
||||
// ========================================================================
|
||||
|
||||
chatMounted(chat, props) {
|
||||
if ( ! props )
|
||||
props = chat.props;
|
||||
|
||||
if ( ! this.addRoom(chat, props) )
|
||||
return;
|
||||
|
||||
this.updateRoomBitsConfig(chat, props.bitsConfig);
|
||||
}
|
||||
|
||||
|
||||
chatUpdated(chat, props) {
|
||||
if ( props.channelID !== chat.props.channelID ) {
|
||||
this.removeRoom(chat);
|
||||
this.chatMounted(chat, props);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( props.bitsConfig !== chat.props.bitsConfig )
|
||||
this.updateRoomBitsConfig(chat, props.bitsConfig);
|
||||
|
||||
// TODO: Check if this is the room for the current channel.
|
||||
|
||||
this.settings.updateContext({
|
||||
moderator: props.isCurrentUserModerator,
|
||||
chatHidden: props.isHidden
|
||||
});
|
||||
|
||||
this.chat.context.updateContext({
|
||||
moderator: props.isCurrentUserModerator,
|
||||
channel: props.channelLogin,
|
||||
channelID: props.channelID,
|
||||
ui: {
|
||||
theme: props.theme
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
updateRoomBitsConfig(chat, config) { // eslint-disable-line class-methods-use-this
|
||||
const room = chat._ffz_room;
|
||||
if ( ! room )
|
||||
return;
|
||||
|
||||
room.updateBitsConfig(formatBitsConfig(config));
|
||||
this.updateChatLines();
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Chat Containers
|
||||
// ========================================================================
|
||||
|
||||
containerMounted(cont, props) {
|
||||
if ( ! props )
|
||||
props = cont.props;
|
||||
|
||||
if ( ! this.addRoom(cont, props) )
|
||||
return;
|
||||
|
||||
if ( props.badgeSets ) {
|
||||
this.chat.updateBadges(props.badgeSets.globalsBySet);
|
||||
this.updateRoomBadges(cont, props.badgeSets.channelsBySet);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
containerUpdated(cont, props) {
|
||||
if ( props.channelID !== cont.props.channelID ) {
|
||||
this.removeRoom(cont);
|
||||
this.containerMounted(cont, props);
|
||||
return;
|
||||
}
|
||||
|
||||
// Twitch, React, and Apollo are the trifecta of terror so we
|
||||
// can't compare the badgeSets property in any reasonable way.
|
||||
// Instead, just check the lengths to see if they've changed
|
||||
// and hope that badge versions will never change separately.
|
||||
const bs = props.badgeSets,
|
||||
obs = cont.props.badgeSets,
|
||||
|
||||
bsgl = bs.globalsBySet && bs.globalsBySet.size || 0,
|
||||
obsgl = obs.globalsBySet && obs.globalsBySet.size || 0,
|
||||
|
||||
bscl = bs.channelsBySet && bs.channelsBySet.size || 0,
|
||||
obscl = obs.channelsBySet && obs.channelsBySet.size || 0;
|
||||
|
||||
if ( bsgl !== obsgl )
|
||||
this.chat.updateBadges(bs.globalsBySet);
|
||||
|
||||
if ( bscl !== obscl )
|
||||
this.updateRoomBadges(cont, bs.channelsBySet);
|
||||
}
|
||||
|
||||
updateRoomBadges(cont, badges) { // eslint-disable-line class-methods-use-this
|
||||
const room = cont._ffz_room;
|
||||
if ( ! room )
|
||||
return;
|
||||
|
||||
room.updateBadges(badges);
|
||||
this.updateChatLines();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Processing Functions
|
||||
// ============================================================================
|
||||
|
||||
export function formatBitsConfig(config) {
|
||||
if ( ! config )
|
||||
return;
|
||||
|
||||
const out = {},
|
||||
actions = config.indexedActions;
|
||||
|
||||
for(const key in actions)
|
||||
if ( has(actions, key) ) {
|
||||
const action = actions[key],
|
||||
new_act = out[key] = {
|
||||
id: action.id,
|
||||
prefix: action.prefix,
|
||||
tiers: []
|
||||
};
|
||||
|
||||
for(const tier of action.orderedTiers) {
|
||||
const images = {};
|
||||
for(const im of tier.images) {
|
||||
const themed = images[im.theme] = images[im.theme] || [],
|
||||
ak = im.isAnimated ? 'animated' : 'static',
|
||||
anim = themed[ak] = themed[ak] || {};
|
||||
|
||||
anim[im.dpiScale] = im.url;
|
||||
}
|
||||
|
||||
new_act.tiers.push({
|
||||
amount: tier.bits,
|
||||
color: tier.color,
|
||||
id: tier.id,
|
||||
images
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
function extractCheerPrefix(parts) {
|
||||
for(const part of parts) {
|
||||
if ( part.type !== 3 || ! part.content.cheerAmount )
|
||||
continue;
|
||||
|
||||
return part.content.alt;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
export function detokenizeMessage(msg) {
|
||||
const out = [],
|
||||
parts = msg.messageParts,
|
||||
l = parts.length,
|
||||
emotes = {};
|
||||
|
||||
let idx = 0, ret, last_type = null;
|
||||
|
||||
for(let i=0; i < l; i++) {
|
||||
const part = parts[i],
|
||||
type = part.type,
|
||||
content = part.content;
|
||||
|
||||
if ( type === 0 )
|
||||
ret = content;
|
||||
|
||||
else if ( type === 1 )
|
||||
ret = `@${content.recipient}`;
|
||||
|
||||
else if ( type === 2 )
|
||||
ret = content.displayText;
|
||||
|
||||
else if ( type === 3 ) {
|
||||
if ( content.cheerAmount ) {
|
||||
ret = `${content.alt}${content.cheerAmount}`;
|
||||
|
||||
} else {
|
||||
const url = (content.images.themed ? content.images.dark : content.images.sources)['1x'],
|
||||
match = /\/emoticons\/v1\/(\d+)\/[\d.]+$/.exec(url),
|
||||
id = match && match[1];
|
||||
|
||||
ret = content.alt;
|
||||
|
||||
if ( id ) {
|
||||
const em = emotes[id] = emotes[id] || [],
|
||||
offset = last_type > 0 ? 1 : 0;
|
||||
em.push({startIndex: idx + offset, endIndex: idx + ret.length - 1});
|
||||
}
|
||||
}
|
||||
|
||||
if ( last_type > 0 )
|
||||
ret = ` ${ret}`;
|
||||
|
||||
} else if ( type === 4 )
|
||||
ret = `https://clips.twitch.tv/${content.slug}`;
|
||||
|
||||
if ( ret ) {
|
||||
idx += ret.length;
|
||||
last_type = type;
|
||||
out.push(ret);
|
||||
}
|
||||
}
|
||||
|
||||
msg.message = out.join('');
|
||||
msg.emotes = emotes;
|
||||
return msg;
|
||||
}
|
214
src/sites/twitch-twilight/modules/css_tweaks/index.js
Normal file
214
src/sites/twitch-twilight/modules/css_tweaks/index.js
Normal file
|
@ -0,0 +1,214 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// CSS Tweaks for Twitch Twilight
|
||||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
import {ManagedStyle} from 'utilities/dom';
|
||||
import {has} from 'utilities/object';
|
||||
|
||||
|
||||
const CLASSES = {
|
||||
'side-nav': '.side-nav',
|
||||
'side-rec-channels': '.side-nav .recommended-channels',
|
||||
'side-rec-friends': '.side-nav .recommended-friends',
|
||||
'side-friends': '.side-nav .online-friends',
|
||||
'side-closed-friends': '.side-nav--collapsed .online-friends',
|
||||
'side-closed-rec-channels': '.side-nav--collapsed .recommended-channels',
|
||||
|
||||
'pinned-cheer': '.pinned-cheer',
|
||||
'whispers': '.whispers'
|
||||
};
|
||||
|
||||
|
||||
export default class CSSTweaks extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.should_enable = true;
|
||||
|
||||
this.inject('settings');
|
||||
this.inject('site.chat');
|
||||
this.inject('site.theme');
|
||||
|
||||
this.style = new ManagedStyle;
|
||||
this.chunks = {};
|
||||
this.chunks_loaded = false;
|
||||
|
||||
|
||||
// Layout
|
||||
|
||||
this.settings.add('layout.side-nav.show', {
|
||||
default: true,
|
||||
ui: {
|
||||
sort: -1,
|
||||
path: 'Appearance > Layout >> Side Navigation',
|
||||
title: 'Display Side Navigation',
|
||||
|
||||
component: 'setting-check-box'
|
||||
},
|
||||
changed: val => this.toggleHide('side-nav', !val)
|
||||
});
|
||||
|
||||
this.settings.add('layout.side-nav.show-rec-channels', {
|
||||
default: 1,
|
||||
ui: {
|
||||
path: 'Appearance > Layout >> Side Navigation',
|
||||
title: 'Display Recommended Channels',
|
||||
component: 'setting-select-box',
|
||||
data: [
|
||||
{value: 0, title: 'Never'},
|
||||
{value: 1, title: 'Always'},
|
||||
{value: 2, title: 'When Side Navigation is Open'}
|
||||
]
|
||||
},
|
||||
changed: val => {
|
||||
this.toggleHide('side-rec-channels', val === 0);
|
||||
this.toggleHide('side-closed-rec-channels', val === 2);
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('layout.side-nav.show-friends', {
|
||||
default: 1,
|
||||
ui: {
|
||||
path: 'Appearance > Layout >> Side Navigation',
|
||||
title: 'Display Online Friends',
|
||||
component: 'setting-select-box',
|
||||
data: [
|
||||
{value: 0, title: 'Never'},
|
||||
{value: 1, title: 'Always'},
|
||||
{value: 2, title: 'When Side Navigation is Open'}
|
||||
]
|
||||
},
|
||||
changed: val => {
|
||||
this.toggleHide('side-friends', val === 0);
|
||||
this.toggleHide('side-closed-friends', val === 2);
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('layout.side-nav.show-rec-friends', {
|
||||
default: true,
|
||||
ui: {
|
||||
path: 'Appearance > Layout >> Side Navigation',
|
||||
title: 'Display Recommended Friends',
|
||||
component: 'setting-check-box'
|
||||
},
|
||||
changed: val => this.toggleHide('side-rec-friends', !val)
|
||||
});
|
||||
|
||||
this.settings.add('layout.swap-sidebars', {
|
||||
default: false,
|
||||
ui: {
|
||||
path: 'Appearance > Layout >> General',
|
||||
title: 'Swap Sidebars',
|
||||
description: 'Swap navigation and chat to the opposite sides of the window.',
|
||||
|
||||
component: 'setting-check-box'
|
||||
},
|
||||
changed: val => this.toggle('swap-sidebars', val)
|
||||
});
|
||||
|
||||
this.settings.add('layout.minimal-navigation', {
|
||||
default: false,
|
||||
ui: {
|
||||
path: 'Appearance > Layout >> General',
|
||||
title: 'Minimize Navigation',
|
||||
description: "Slide the site navigation bar up out of view when it isn't in use.",
|
||||
|
||||
component: 'setting-check-box'
|
||||
},
|
||||
changed: val => this.toggle('minimal-navigation', val)
|
||||
});
|
||||
|
||||
|
||||
// Chat
|
||||
|
||||
this.settings.add('whispers.show', {
|
||||
default: true,
|
||||
ui: {
|
||||
path: 'Chat > Whispers >> General',
|
||||
title: 'Display Whispers',
|
||||
component: 'setting-check-box'
|
||||
},
|
||||
changed: val => this.toggleHide('whispers', !val)
|
||||
});
|
||||
|
||||
this.settings.add('chat.bits.show', {
|
||||
default: true,
|
||||
ui: {
|
||||
order: -1,
|
||||
path: 'Chat > Bits and Cheering >> Appearance',
|
||||
title: 'Display Bits',
|
||||
description: 'Display UI associated with bits. Note: This will not hide cheering in chat messages.',
|
||||
component: 'setting-check-box'
|
||||
},
|
||||
changed: val => this.toggle('hide-bits', !val)
|
||||
});
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
this.toggle('swap-sidebars', this.settings.get('layout.swap-sidebars'));
|
||||
this.toggle('minimal-navigation', this.settings.get('layout.minimal-navigation'));
|
||||
|
||||
this.toggleHide('side-nav', !this.settings.get('layout.side-nav.show'));
|
||||
this.toggleHide('side-rec-friends', !this.settings.get('layout.side-nav.show-rec-friends'));
|
||||
|
||||
const recs = this.settings.get('layout.side-nav.show-rec-channels');
|
||||
this.toggleHide('side-rec-channels', recs === 0);
|
||||
this.toggleHide('side-closed-rec-channels', recs === 2);
|
||||
|
||||
const friends = this.settings.get('layout.side-nav.show-friends');
|
||||
this.toggleHide('side-friends', friends === 0);
|
||||
this.toggleHide('side-closed-friends', friends === 2);
|
||||
|
||||
this.toggleHide('whispers', !this.settings.get('whispers.show'));
|
||||
}
|
||||
|
||||
|
||||
toggleHide(key, val) {
|
||||
const k = `hide--${key}`;
|
||||
if ( ! val ) {
|
||||
this.style.delete(k);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! has(CLASSES, key) )
|
||||
throw new Error(`cannot find class for "${key}"`);
|
||||
|
||||
this.style.set(k, `${CLASSES[key]} { display: none !important }`);
|
||||
}
|
||||
|
||||
|
||||
async toggle(key, val) {
|
||||
if ( ! val ) {
|
||||
this.style.delete(key);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! this.chunks_loaded )
|
||||
await this.populate();
|
||||
|
||||
if ( ! has(this.chunks, key) )
|
||||
throw new Error(`cannot find chunk "${key}"`);
|
||||
|
||||
this.style.set(key, this.chunks[key]);
|
||||
}
|
||||
|
||||
|
||||
populate() {
|
||||
if ( this.chunks_loaded )
|
||||
return;
|
||||
|
||||
return new Promise(async r => {
|
||||
const raw = (await import(/* webpackChunkName: "site-css-tweaks" */ './styles.js')).default;
|
||||
for(const key of raw.keys()) {
|
||||
const k = key.slice(2, key.length - (key.endsWith('.scss') ? 5 : 4));
|
||||
this.chunks[k] = raw(key);
|
||||
}
|
||||
|
||||
this.chunks_loaded = true;
|
||||
r();
|
||||
})
|
||||
}
|
||||
}
|
3
src/sites/twitch-twilight/modules/css_tweaks/styles.js
Normal file
3
src/sites/twitch-twilight/modules/css_tweaks/styles.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
'use strict';
|
||||
|
||||
export default require.context('!raw-loader!sass-loader!./styles', false, /\.s?css$/);
|
|
@ -0,0 +1,19 @@
|
|||
.chat-line__message,
|
||||
.chat-line__moderation,
|
||||
.chat-line__status,
|
||||
.chat-line__raid,
|
||||
.chat-line__subscribe {
|
||||
padding-top: calc(.5rem - 1px) !important;
|
||||
|
||||
border-top: 1px solid #aaa;
|
||||
border-bottom-color: rgba(255,255,255,0.5);
|
||||
|
||||
.theme--dark & {
|
||||
border-top-color: #000;
|
||||
border-bottom-color: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-color: transparent !important;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
.chat-line__message,
|
||||
.chat-line__moderation,
|
||||
.chat-line__status,
|
||||
.chat-line__raid,
|
||||
.chat-line__subscribe {
|
||||
padding-top: calc(.5rem - 1px) !important;
|
||||
|
||||
border-top: 1px solid rgba(255,255,255,0.5);
|
||||
border-bottom-color: #aaa;
|
||||
|
||||
.theme--dark & {
|
||||
border-top-color: rgba(255,255,255,0.1);
|
||||
border-bottom-color: #000;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-color: transparent !important;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
.chat-line__message,
|
||||
.chat-line__moderation,
|
||||
.chat-line__status,
|
||||
.chat-line__raid,
|
||||
.chat-line__subscribe {
|
||||
padding-top: calc(.5rem - 1px) !important;
|
||||
|
||||
border-top: 1px solid #dad8de;
|
||||
|
||||
.theme--dark & {
|
||||
border-top-color: #2c2541;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-color: transparent !important;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
.chat-line__message,
|
||||
.chat-line__moderation,
|
||||
.chat-line__status,
|
||||
.chat-line__raid,
|
||||
.chat-line__subscribe {
|
||||
padding-bottom: calc(.5rem - 1px) !important;
|
||||
|
||||
border-bottom: 1px solid #dad8de;
|
||||
|
||||
.theme--dark & {
|
||||
border-bottom-color: #2c2541;
|
||||
}
|
||||
|
||||
&:last-child:nth-child(odd) {
|
||||
border-bottom-color: transparent !important;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
.chat-line__message,
|
||||
.chat-line__moderation,
|
||||
.chat-line__status,
|
||||
.chat-line__raid,
|
||||
.chat-line__subscribe {
|
||||
padding: .5rem 1rem !important;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
.chat-line__moderation,
|
||||
.chat-line__status,
|
||||
.chat-line__raid,
|
||||
.chat-line__subscribe,
|
||||
.chat-line__message {
|
||||
background-color: transparent !important;
|
||||
|
||||
&:nth-child(2n+0) {
|
||||
background-color: rgba(0,0,0,0.1) !important;
|
||||
|
||||
.theme--dark & {
|
||||
background-color: rgba(255,255,255,0.05) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
.chat-input button[data-a-target="bits-button"],
|
||||
.channel-header__right > .mg-l-1 > div > div > button:not([data-a-target]) {
|
||||
display: none;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue