1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-08-02 16:08:31 +00:00
* Added: Chat actions for modding and un-modding users.
* Fixed: Settings not being removed when an add-on is unloaded.
* Changed: Add a few new icons.
* API Added: Add support for header backgrounds for rich token documents.
* API Added: Methods for adding/updating emotes to and removing emotes from an emote set.
* API Added: Context flag to disable FFZ's chat message processing.
* API Changed: Add-ons can now be hot reloaded for development purposes. This feature may be somewhat unstable.
This commit is contained in:
SirStendec 2023-01-19 17:00:09 -05:00
parent 14400e16bc
commit 8e48021c43
35 changed files with 1285 additions and 214 deletions

View file

@ -805,6 +805,68 @@
"css": "volume-up",
"code": 59464,
"src": "elusive"
},
{
"uid": "1fc437d46c5ef828375b6b3de577918d",
"css": "unmod",
"code": 59465,
"src": "custom_icons",
"selected": true,
"svg": {
"path": "M350 100A200 200 0 0 0 299.3 493.5C294.4 525.5 266.7 550 233.3 550A133.3 133.3 0 0 0 100 683.4V900H200V683.4C200 664.9 214.9 650 233.3 650 278.8 650 319.9 631.9 350 602.4 380.1 631.9 421.3 650 466.7 650 485.1 650 500 664.9 500 683.4V900H600V683.4A133.3 133.3 0 0 0 466.7 550C433.3 550 405.6 525.5 400.8 493.5A200.1 200.1 0 0 0 350 100ZM250 300A100 100 0 1 0 450 300 100 100 0 0 0 250 300ZM600 420.7L670.7 350 751.8 431.1 833 350 903.7 420.7 822.6 501.8 903.7 583 833 653.7 751.9 572.5 670.7 653.7 600 583 681.1 501.9 600 420.7Z",
"width": 1000
},
"search": [
"unmod"
]
},
{
"uid": "2c1f4d302aa8281c3ed4882568669043",
"css": "mod",
"code": 59466,
"src": "custom_icons",
"selected": true,
"svg": {
"path": "M350 100A200 200 0 0 0 299.3 493.5C294.4 525.5 266.7 550 233.3 550A133.3 133.3 0 0 0 100 683.4V900H200V683.4C200 664.9 214.9 650 233.3 650 278.8 650 319.9 631.9 350 602.4 380.1 631.9 421.3 650 466.7 650 485.1 650 500 664.9 500 683.4V900H600V683.4A133.3 133.3 0 0 0 466.7 550C433.3 550 405.6 525.5 400.8 493.5A200.1 200.1 0 0 0 350 100ZM250 300A100 100 0 1 0 450 300 100 100 0 0 0 250 300ZM750 350L900 500 750 650V550H600V450H750V350Z",
"width": 1000
},
"search": [
"mod"
]
},
{
"uid": "e436d990b8c910352dba1fe3e88d9ca3",
"css": "flag",
"code": 59467,
"src": "custom_icons",
"selected": true,
"svg": {
"path": "M900 100L700 400 900 700H200V900H100V100H900ZM200 600H713.1L579.8 400 713.1 200H200V600Z",
"width": 1000
},
"search": [
"flag"
]
},
{
"uid": "c56ae110cddeae77e2e904e33f9b9718",
"css": "mange-suspicious",
"code": 59468,
"src": "custom_icons",
"selected": true,
"svg": {
"path": "M450 550V650H550V550H450ZM250.2 197.3A500 500 0 0 0 475.9 116L500 100 524 116A500 500 0 0 0 801.5 200H850L825.3 482.1A450 450 0 0 1 623.1 819.6L500 900 376.9 819.6A450 450 0 0 1 174.7 482.1L150 200H198.6C215.9 200 233.1 199.1 250.2 197.3ZM258.9 296.9L274.3 473.4A350 350 0 0 0 431.6 735.9L500 780.6 568.4 735.9A350 350 0 0 0 725.7 473.4L741.2 296.9A600 600 0 0 1 550 244.8V450H450V244.8A600 600 0 0 1 258.8 296.9Z",
"width": 1000
},
"search": [
"mange-suspicious"
]
},
{
"uid": "5408be43f7c42bccee419c6be53fdef5",
"css": "doc-text",
"code": 61686,
"src": "fontawesome"
}
]
}

View file

@ -1,7 +1,7 @@
{
"name": "frankerfacez",
"author": "Dan Salvato LLC",
"version": "4.39.0",
"version": "4.40.0",
"description": "FrankerFaceZ is a Twitch enhancement suite.",
"private": true,
"license": "Apache-2.0",

Binary file not shown.

View file

@ -152,6 +152,14 @@
<glyph glyph-name="volume-up" unicode="&#xe848;" d="M0 169l0 360 203 0 307 250 0-858-307 248-203 0z m563 33q62 63 62 151t-62 152l60 65q90-90 92-219 0-125-92-213z m101-105q106 105 106 256t-106 258l66 62q131-133 131-319t-131-321z m100-98q146 147 146 354t-146 353l62 65q82-82 128-190t46-227-46-229-128-190z" horiz-adv-x="1000" />
<glyph glyph-name="unmod" unicode="&#xe849;" d="M350 750a200 200 0 0 1-51-393c-5-32-32-57-66-57a133 133 0 0 1-133-133v-217h100v217c0 18 15 33 33 33 46 0 87 18 117 48 30-30 71-48 117-48 18 0 33-15 33-33v-217h100v217a133 133 0 0 1-133 133c-34 0-61 25-66 57a200 200 0 0 1-51 393z m-100-200a100 100 0 1 1 200 0 100 100 0 0 1-200 0z m350-121l71 71 81-81 81 81 71-71-81-81 81-81-71-71-81 82-81-82-71 71 81 81-81 81z" horiz-adv-x="1000" />
<glyph glyph-name="mod" unicode="&#xe84a;" d="M350 750a200 200 0 0 1-51-393c-5-32-32-57-66-57a133 133 0 0 1-133-133v-217h100v217c0 18 15 33 33 33 46 0 87 18 117 48 30-30 71-48 117-48 18 0 33-15 33-33v-217h100v217a133 133 0 0 1-133 133c-34 0-61 25-66 57a200 200 0 0 1-51 393z m-100-200a100 100 0 1 1 200 0 100 100 0 0 1-200 0z m500-50l150-150-150-150v100h-150v100h150v100z" horiz-adv-x="1000" />
<glyph glyph-name="flag" unicode="&#xe84b;" d="M900 750l-200-300 200-300h-700v-200h-100v800h800z m-700-500h513l-133 200 133 200h-513v-400z" horiz-adv-x="1000" />
<glyph glyph-name="mange-suspicious" unicode="&#xe84c;" d="M450 300v-100h100v100h-100z m-200 353a500 500 0 0 1 226 81l24 16 24-16a500 500 0 0 1 278-84h48l-25-282a450 450 0 0 0-202-338l-123-80-123 80a450 450 0 0 0-202 338l-25 282h49c17 0 34 1 51 3z m9-100l15-176a350 350 0 0 1 158-263l68-45 68 45a350 350 0 0 1 158 263l15 176a600 600 0 0 0-191 52v-205h-100v205a600 600 0 0 0-191-52z" horiz-adv-x="1000" />
<glyph glyph-name="move" unicode="&#xf047;" d="M1000 350q0-14-11-25l-142-143q-11-11-26-11t-25 11-10 25v72h-215v-215h72q14 0 25-10t11-25-11-25l-143-143q-10-11-25-11t-25 11l-143 143q-11 10-11 25t11 25 25 10h72v215h-215v-72q0-14-10-25t-25-11-25 11l-143 143q-11 11-11 25t11 25l143 143q10 11 25 11t25-11 10-25v-72h215v215h-72q-14 0-25 10t-11 25 11 26l143 142q11 11 25 11t25-11l143-142q11-11 11-26t-11-25-25-10h-72v-215h215v72q0 14 10 25t25 11 26-11l142-143q11-10 11-25z" horiz-adv-x="1000" />
<glyph glyph-name="link-ext" unicode="&#xf08e;" d="M786 332v-178q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h393q7 0 12-5t5-13v-36q0-8-5-13t-12-5h-393q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v178q0 8 5 13t13 5h36q8 0 13-5t5-13z m214 482v-285q0-15-11-25t-25-11-25 11l-98 98-364-364q-5-6-13-6t-12 6l-64 64q-6 5-6 12t6 13l364 364-98 98q-11 11-11 25t11 25 25 11h285q15 0 25-11t11-25z" horiz-adv-x="1000" />
@ -176,6 +184,8 @@
<glyph glyph-name="upload-cloud" unicode="&#xf0ee;" 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="doc-text" unicode="&#xf0f6;" d="M819 638q16-16 27-42t11-50v-642q0-23-15-38t-38-16h-750q-23 0-38 16t-16 38v892q0 23 16 38t38 16h500q22 0 49-11t42-27z m-248 136v-210h210q-5 17-12 23l-175 175q-6 7-23 12z m215-853v572h-232q-23 0-38 16t-16 37v233h-429v-858h715z m-572 483q0 7 5 12t13 5h393q8 0 13-5t5-12v-36q0-8-5-13t-13-5h-393q-8 0-13 5t-5 13v36z m411-125q8 0 13-5t5-13v-36q0-8-5-13t-13-5h-393q-8 0-13 5t-5 13v36q0 8 5 13t13 5h393z m0-143q8 0 13-5t5-13v-36q0-8-5-13t-13-5h-393q-8 0-13 5t-5 13v36q0 8 5 13t13 5h393z" horiz-adv-x="857.1" />
<glyph glyph-name="reply" unicode="&#xf112;" d="M1000 225q0-93-71-252-1-4-6-13t-7-17-7-12q-7-10-16-10-8 0-13 6t-5 14q0 5 1 15t2 13q3 38 3 69 0 56-10 101t-27 77-45 56-59 39-74 24-86 12-98 3h-125v-143q0-14-10-25t-26-11-25 11l-285 286q-11 10-11 25t11 25l285 286q11 10 25 10t26-10 10-25v-143h125q398 0 488-225 30-75 30-186z" horiz-adv-x="1000" />
<glyph glyph-name="smile" unicode="&#xf118;" d="M633 250q-21-67-77-109t-127-41-128 41-77 109q-4 14 3 27t21 18q14 4 27-2t17-22q14-44 52-72t85-28 84 28 52 72q4 15 18 22t27 2 21-18 2-27z m-276 243q0-30-21-51t-50-21-51 21-21 51 21 50 51 21 50-21 21-50z m286 0q0-30-21-51t-51-21-50 21-21 51 21 50 50 21 51-21 21-50z m143-143q0 73-29 139t-76 114-114 76-138 28-139-28-114-76-76-114-29-139 29-139 76-113 114-77 139-28 138 28 114 77 76 113 29 139z m71 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" />

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Before After
Before After

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -57,6 +57,8 @@ export default class AddonManager extends Module {
isAddonExternal: id => this.isAddonExternal(id),
enableAddon: id => this.enableAddon(id),
disableAddon: id => this.disableAddon(id),
reloadAddon: id => this.reloadAddon(id),
canReloadAddon: id => this.canReloadAddon(id),
isReloadRequired: () => this.reload_required,
refresh: () => window.location.reload(),
@ -280,6 +282,150 @@ export default class AddonManager extends Module {
return module.external || (module.constructor && module.constructor.external);
}
canReloadAddon(id) {
// Obviously we can't reload it if we don't have it.
if ( ! this.hasAddon(id) )
throw new Error(`Unknown add-on id: ${id}`);
// If the module isn't available, we can't reload it.
let module = this.resolve(`addon.${id}`);
if ( ! module )
return false;
// If the module cannot be disabled, or it cannot be unloaded, then
// we can't reload it.
if ( ! module.canDisable() || ! module.canUnload() )
return false;
// Check each child.
if ( module.children )
for(const child of Object.values(module.children))
if ( ! child.canDisable() || ! child.canUnload() )
return false;
// If we got here, we might be able to reload it.
return true;
}
async fullyUnloadModule(module) {
if ( ! module )
return;
if ( module.children )
for(const child of Object.values(module.children))
await this.fullyUnloadModule(child);
await module.disable();
await module.unload();
// Clean up parent references.
if ( module.parent && module.parent.children[module.name] === module )
delete module.parent.children[module.name];
// Clean up all individual references.
for(const entry of module.references) {
const other = this.resolve(entry[0]),
name = entry[1];
if ( other && other[name] === module )
other[name] = null;
}
// Clean up the global reference.
if ( this.__modules[module.__path] === module )
delete this.__modules[module.__path]; /* = [
module.dependents,
module.load_dependents,
module.references
];*/
// Remove any events we didn't unregister.
this.offContext(null, module);
// Do the same for settings.
for(const ctx of this.settings.__contexts)
ctx.offContext(null, module);
// Clean up all settings.
for(const [key, def] of Array.from(this.settings.definitions.entries())) {
if ( def && def.__source === module.__path ) {
this.settings.remove(key);
}
}
// Clean up the logger too.
module.__log = null;
}
async reloadAddon(id) {
const addon = this.getAddon(id),
button = this.resolve('site.menu_button');
if ( ! addon )
throw new Error(`Unknown add-on id: ${id}`);
const start = performance.now();
// Yeet the module into the abyss.
// This will also yeet all children.
let module = this.resolve(`addon.${id}`);
if ( module )
try {
await this.fullyUnloadModule(module);
} catch(err) {
if ( button )
button.addToast({
title_i18n: 'addons.reload.toast-error',
title: 'Error Reloading Add-On',
text_i18n: 'addons.reload.toast-error.unload',
text: 'Unable to unload existing modules for add-on "{addon_id}":\n\n{error}',
icon: 'ffz-i-attention',
addon_id: id,
error: String(err)
});
throw err;
}
// Is there a script tab?
let el = document.querySelector(`script#ffz-loaded-addon-${addon.id}`);
if ( el )
el.remove();
// Do unnatural things to webpack.
if ( window.ffzAddonsWebpackJsonp )
window.ffzAddonsWebpackJsonp = undefined;
// Now, reload it all~
try {
await this._enableAddon(id);
} catch(err) {
if ( button )
button.addToast({
title_i18n: 'addons.reload.toast-error',
title: 'Error Reloading Add-On',
text_i18n: 'addons.reload.toast-error.reload',
text: 'Unable to load new module for add-on "{addon_id}":\n\n{error}',
error: String(err),
icon: 'ffz-i-attention',
addon_id: id
});
throw err;
}
const end = performance.now();
if ( button )
button.addToast({
title_i18n: 'addons.reload.toast',
title: 'Reloaded Add-On',
text_i18n: 'addons.reload.toast.text',
text: 'Successfully reloaded add-on "{addon_id}" in {duration}ms.',
icon: 'ffz-i-info',
addon_id: id,
timeout: 5000,
duration: Math.round(100 * (end - start)) / 100
});
}
async _enableAddon(id) {
const addon = this.getAddon(id);
if ( ! addon )

View file

@ -1124,6 +1124,8 @@ export default class Actions extends Module {
if ( target._ffz_tooltip )
target._ffz_tooltip.hide();
return data.definition.click.call(this, event, data);
}

View file

@ -499,6 +499,84 @@ export const untimeout = {
}
// ============================================================================
// Mod and Unmod User
// ============================================================================
export const mod = {
presets: [{
appearance: {
type: 'icon',
icon: 'ffz-i-mod'
}
}],
required_context: ['room', 'user'],
title: 'Mod User',
tooltip(data) {
return this.i18n.t('chat.actions.mod.tooltip', 'Mod {user.login}', {user: data.user});
},
hidden(data, message, current_room, current_user, mod_icons, instance) {
// You cannot mod mods.
if ( message.user.type === 'mod' )
return true;
// You cannot mod the broadcaster.
if ( message.user.id === current_room.id )
return true;
// Only the broadcaster can mod, otherwise.
return current_room.id !== current_user.id;
},
click(event, data) {
this.sendMessage(data.room.login, `/mod ${data.user.login}`);
}
};
export const unmod = {
presets: [{
appearance: {
type: 'icon',
icon: 'ffz-i-unmod'
}
}],
required_context: ['room', 'user'],
title: 'Un-Mod User',
tooltip(data) {
return this.i18n.t('chat.actions.unmod.tooltip', 'Un-Mod {user.login}', {user: data.user});
},
hidden(data, message, current_room, current_user, mod_icons, instance) {
// You can only un-mod mods.
if ( message.user.type !== 'mod' )
return true;
// You can unmod yourself.
if ( message.user.id === current_user.id )
return false;
// You cannot unmod the broadcaster.
if ( message.user.id === current_room.id )
return false;
// Only the broadcaster can unmod, otherwise.
return current_room.id !== current_user.id;
},
click(event, data) {
this.sendMessage(data.room.login, `/unmod ${data.user.login}`);
}
};
// ============================================================================
// Whisper
// ============================================================================

View file

@ -23,6 +23,7 @@ export default {
full: null,
unsafe: false,
urls: null,
i18n_prefix: null,
allow_media: false,
allow_unsafe: false
}
@ -114,6 +115,7 @@ export default {
this.fragments = {};
this.unsafe = false;
this.urls = null;
this.i18n_prefix = null;
this.allow_media = false;
this.allow_unsafe = false;
this.load(refresh);
@ -179,6 +181,7 @@ export default {
this.fragments = data.fragments ?? {};
this.unsafe = data.unsafe;
this.urls = data.urls;
this.i18n_prefix = data.i18n_prefix;
this.allow_media = data.allow_media;
this.allow_unsafe = data.allow_unsafe;
},
@ -238,6 +241,7 @@ export default {
i18n: this.getI18n(),
fragments: this.fragments,
i18n_prefix: this.i18n_prefix,
allow_media: this.forceMedia ?? this.allow_media,
allow_unsafe: this.forceUnsafe ?? this.allow_unsafe

View file

@ -197,8 +197,8 @@ export default class Emotes extends Module {
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 || ''));
if ( emote_set && (emote_set.pending_css || emote_set.css) ) {
this.style.set(`es--${set_id}`, (emote_set.pending_css || '') + (emote_set.css || ''));
emote_set.pending_css = null;
}
}
@ -816,32 +816,9 @@ export default class Emotes extends Module {
}
loadSetData(set_id, data, suppress_log = false) {
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;
let count = 0;
const ems = data.emotes || data.emoticons,
new_ems = data.emotes = {},
css = [];
data.id = set_id;
data.emoticons = undefined;
const bad_emotes = [];
for(const emote of ems) {
if ( ! emote.id || ! emote.name || ! emote.urls ) {
bad_emotes.push(emote);
continue;
}
processEmote(emote, set_id) {
if ( ! emote.id || ! emote.name || ! emote.urls )
return null;
emote.set_id = set_id;
emote.src = emote.urls[1];
@ -896,12 +873,143 @@ export default class Emotes extends Module {
if ( has(MODIFIERS, emote.id) )
Object.assign(emote, MODIFIERS[emote.id]);
return emote;
}
addEmoteToSet(set_id, emote) {
const set = this.emote_sets[set_id];
if ( ! set )
throw new Error(`Invalid emote set "${set_id}"`);
let processed = this.processEmote(emote, set_id);
if ( ! processed )
throw new Error("Invalid emote data object.");
// Are we removing an existing emote?
const old_emote = set.emotes[processed.id],
old_css = old_emote && this.generateEmoteCSS(old_emote);
// Store the emote.
set.emotes[processed.id] = processed;
if ( ! old_emote )
set.count++;
// Now we need to update the CSS. If we had old emote CSS, then we
// will need to totally rebuild the CSS.
const style_key = `es--${set_id}`;
if ( old_css && old_css.length ) {
const css = [];
for(const em of Object.values(set.emotes)) {
const emote_css = this.generateEmoteCSS(em);
if ( emote_css && emote_css.length )
css.push(emote_css);
}
if ( this.style && (css.length || set.css) )
this.style.set(style_key, css.join('') + (set.css || ''));
else if ( css.length )
set.pending_css = css.join('');
} else {
const emote_css = this.generateEmoteCSS(processed);
if ( emote_css && emote_css.length ) {
if ( this.style )
this.style.set(style_key, (this.style.get(style_key) || '') + emote_css);
else
set.pending_css = (set.pending_css || '') + emote_css;
}
}
// Send a loaded event because this emote set changed.
this.emit(':loaded', set_id, set);
}
removeEmoteFromSet(set_id, emote_id) {
const set = this.emote_sets[set_id];
if ( ! set )
throw new Error(`Invalid emote set "${set_id}"`);
if ( emote_id && emote_id.id )
emote_id = emote_id.id;
const emote = set.emotes[emote_id];
if ( ! emote )
return;
const emote_css = this.generateEmoteCSS(emote);
const css = (emote_css && emote_css.length) ? [] : null;
// Rebuild the emotes object to avoid gaps.
const new_emotes = {};
let count = 0;
for(const em of Object.values(set.emotes)) {
if ( em.id == emote_id )
continue;
new_emotes[em.id] = em;
count++;
if ( css != null) {
const em_css = this.generateEmoteCSS(em);
if ( em_css && em_css.length )
css.push(em_css);
}
}
set.emotes = new_emotes;
set.count = count;
if ( css != null ) {
const style_key = `es--${set_id}`;
if ( this.style && (css.length || set.css) )
this.style.set(style_key, css.join('') + (set.css || ''));
else if ( css.length )
set.pending_css = css.join('');
}
// Send a loaded event because this emote set changed.
this.emit(':loaded', set_id, set);
}
loadSetData(set_id, data, suppress_log = false) {
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;
let count = 0;
const ems = data.emotes || data.emoticons,
new_ems = data.emotes = {},
css = [];
data.id = set_id;
data.emoticons = undefined;
const bad_emotes = [];
for(const emote of ems) {
let processed = this.processEmote(emote, set_id);
if ( ! processed ) {
bad_emotes.push(emote);
continue;
}
const emote_css = this.generateEmoteCSS(processed);
if ( emote_css )
css.push(emote_css);
count++;
new_ems[emote.id] = emote;
new_ems[processed.id] = processed;
}
if ( bad_emotes.length )

View file

@ -1668,6 +1668,10 @@ export default class Chat extends Module {
b[item.setID] = item.version;
}
// Validate User Type
if ( user.type == null && msg.badges && msg.badges.moderator )
user.type = 'mod';
// Standardize Timestamp
if ( ! msg.timestamp && msg.sentAt )
msg.timestamp = new Date(msg.sentAt).getTime();
@ -2235,6 +2239,9 @@ export default class Chat extends Module {
}
fixLinkInfo(data) {
if ( ! data )
return data;
if ( data.error && data.message )
data.error = data.message;

View file

@ -11,7 +11,7 @@ const USER_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/([^/]+)$/;
const BAD_USERS = [
'directory', '_deck', 'p', 'downloads', 'jobs', 'turbo', 'settings', 'friends',
'subscriptions', 'inventory', 'wallet'
'subscriptions', 'inventory', 'wallet', 'store', 'drops', 'search', 'prime'
];
import GET_CLIP from './clip_info.gql';

View file

@ -97,6 +97,7 @@ export const Links = {
i18n: this.i18n,
fragments: data.fragments,
i18n_prefix: data.i18n_prefix,
allow_media: show_images,
allow_unsafe: show_unsafe,

View file

@ -5,6 +5,10 @@
<img :src="icon" class="tw-image">
</div>
<div v-if="reloading" class="tw-mg-b-05 ffz-pill">
{{ t('addon.reloading', 'Reloading') }}
</div>
<div v-if="external" class="tw-mg-b-05 ffz-pill">
{{ t('addon.external', 'External') }}
</div>
@ -96,6 +100,20 @@
{{ t('addon.disable', 'Disable') }}
</span>
</button>
<button
v-if="addon.dev && can_reload"
class="tw-button ffz-button--hollow tw-mg-r-1"
:class="{'tw-button--disabled': reloading}"
:disabled="reloading"
@click="reloadAddon()"
>
<span class="tw-button__icon tw-button__icon--left">
<figure class="ffz-i-arrows-cw" />
</span>
<span class="tw-button__text">
{{ t('addon.reload', 'Reload') }}
</span>
</button>
<button
v-if="addon.settings"
class="tw-button ffz-button--hollow tw-mg-r-1"
@ -151,6 +169,8 @@ export default {
data() {
return {
enabled: this.item.isAddonEnabled(this.id),
can_reload: this.addon.dev && this.item.canReloadAddon(this.id),
reloading: false,
external: this.item.isAddonExternal(this.id),
version: this.item.getVersion(this.id),
expanded: false
@ -251,6 +271,19 @@ export default {
list.push(`add_ons.${this.addon.name.toSnakeCase()}`);
this.$emit('navigate', ...list);
},
reloadAddon() {
this.reloading = true;
this.item.reloadAddon(this.id)
.then(() => {
this.reloading = false;
this.can_reload = this.item.canReloadAddon(this.id);
})
.catch(err => {
console.error(err);
this.reloading = false;
});
}
}
}

View file

@ -4,27 +4,6 @@
{{ t('setting.experiments.about', 'This feature allows you to override experiment values. Please note that, for most experiments, you may have to refresh the page for your changes to take effect.') }}
</div>
<section v-if="experiments_locked">
<div class="tw-c-background-accent tw-c-text-overlay tw-pd-1 tw-mg-b-2">
<h3 class="ffz-i-attention">
{{ t('setting.dev-warning', "It's dangerous to go at all.") }}
</h3>
<markdown :source="t('setting.dev-warning.explain', 'Be careful, this is an advanced feature intended for developer use only. Normal users should steer clear. Adjusting your experiments can have unexpected impacts on your Twitch experience. FrankerFaceZ is not responsible for any issues you encounter as a result of tampering with experiments, and we will not provide support.\n\nIf you\'re sure about this, please type `{code}` into the box below and hit enter.', {code})" />
</div>
<div class="tw-flex tw-align-items-center">
<input
ref="code"
type="text"
class="tw-block tw-full-width tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-x-1 tw-pd-y-05"
autocapitalize="off"
autocorrect="off"
@keydown.enter="enterCode"
>
</div>
</section>
<section v-else>
<div class="tw-mg-b-2 tw-flex tw-align-items-center">
<div class="tw-flex-grow-1">
{{ t('setting.experiments.unique-id', 'Unique ID: {id}', {id: unique_id}) }}
@ -128,7 +107,7 @@
<span>
{{ t('setting.experiments.twitch', 'Twitch Experiments') }}
</span>
<span v-if="filter" class="tw-mg-l-1 tw-font-size-base tw-regular tw-c-text-alt-2">
<span v-if="experiments_locked && filter" class="tw-mg-l-1 tw-font-size-base tw-regular tw-c-text-alt-2">
{{ t('setting.experiments.visible', '(Showing {visible,number} of {total,number})', {
visible: visible_twitch.length,
total: sorted_twitch.length
@ -136,6 +115,27 @@
</span>
</h3>
<section v-if="experiments_locked">
<div class="tw-c-background-accent tw-c-text-overlay tw-pd-1 tw-mg-b-2">
<h3 class="ffz-i-attention">
{{ t('setting.dev-warning', "It's dangerous to go at all.") }}
</h3>
<markdown :source="t('setting.dev-warning.explain', 'Be careful, this is an advanced feature intended for developer use only. Normal users should steer clear. Adjusting your experiments can have unexpected impacts on your Twitch experience. FrankerFaceZ is not responsible for any issues you encounter as a result of tampering with experiments, and we will not provide support.\n\nIf you\'re sure about this, please type `{code}` into the box below and hit enter.', {code})" />
</div>
<div class="tw-flex tw-align-items-center">
<input
ref="code"
type="text"
class="tw-block tw-full-width tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-x-1 tw-pd-y-05 tw-mg-b-5"
autocapitalize="off"
autocorrect="off"
@keydown.enter="enterCode"
>
</div>
</section>
<section v-else>
<div class="ffz--experiment-list">
<section
v-for="({key, exp}) of visible_twitch"

View file

@ -215,7 +215,12 @@ export default class MainMenu extends Module {
this.on('settings:added-definition', (key, definition) => {
this._addDefinitionToTree(key, definition);
this.scheduleUpdate();
})
});
this.on('settings:removed-definition', key => {
this._removeDefinitionFromTree(key);
this.scheduleUpdate();
});
this.on('socket:command:new_version', version => {
if ( version === window.FrankerFaceZ.version_info.commit )
@ -361,6 +366,7 @@ export default class MainMenu extends Module {
this.log.info('Context proxy gone.');
this.updateContext({proxied: false});
}
});
try {
@ -508,6 +514,58 @@ export default class MainMenu extends Module {
}
_removeDefinitionFromTree(key) {
if ( ! this._settings_tree )
return;
let page;
for(const val of Object.values(this._settings_tree)) {
if ( ! val || ! Array.isArray(val.settings) )
continue;
for(let i = 0; i < val.settings.length; i++) {
const entry = val.settings[i];
if ( entry && entry[0] === key ) {
val.settings.splice(i, 1);
page = val;
break;
}
}
if ( page )
break;
}
// Was it found?
if ( ! page )
return;
this._maybeDeleteSection(page);
}
_maybeDeleteSection(page) {
// Is the section empty?
if ( page.settings && page.settings.length )
return;
const id = page.full_key;
// Check for children.
for(const val of Object.values(this._settings_tree)) {
if ( val.parent === id )
return;
}
// Nope~
delete this._settings_tree[id];
if ( page.parent ) {
const parent = this._settings_tree[page.parent];
if ( parent )
this._maybeDeleteSection(parent);
}
}
_addDefinitionToTree(key, def) {
if ( ! def.ui || ! this._settings_tree )
return;

View file

@ -941,11 +941,45 @@ export default class SettingsManager extends Module {
setContext(context) { return this.main_context.setContext(context) }
// ========================================================================
// Add-On Proxy
// ========================================================================
getAddonProxy(module) {
const path = module.__path;
const add = (key, definition) => {
return this.add(key, definition, path);
}
const addUI = (key, definition) => {
return this.addUI(key, definition, path);
}
const addClearable = (key, definition) => {
return this.addClearable(key, definition, path);
}
const handler = {
get(obj, prop) {
if ( prop === 'add' )
return add;
if ( prop === 'addUI' )
return addUI;
if ( prop === 'addClearable' )
return addClearable;
return Reflect.get(...arguments);
}
}
return new Proxy(this, handler);
}
// ========================================================================
// Definitions
// ========================================================================
add(key, definition) {
add(key, definition, source) {
if ( typeof key === 'object' ) {
for(const k in key)
if ( has(key, k) )
@ -960,6 +994,8 @@ export default class SettingsManager extends Module {
definition.required_by = required_by;
definition.requires = definition.requires || [];
definition.__source = source;
for(const req_key of definition.requires) {
const req = this.definitions.get(req_key);
if ( ! req )
@ -1007,7 +1043,42 @@ export default class SettingsManager extends Module {
}
addUI(key, definition) {
remove(key) {
const definition = this.definitions.get(key);
if ( ! definition )
return;
// If the definition is an array, we're already not defined.
if ( Array.isArray(definition) )
return;
// Remove this definition from the definitions list.
if ( Array.isArray(definition.required_by) && definition.required_by.length > 0 )
this.definitions.set(key, definition.required_by);
else
this.definitions.delete(key);
// Remove it from all the things it required.
if ( Array.isArray(definition.requires) )
for(const req_key of definition.requires) {
let req = this.definitions.get(req_key);
if ( req.required_by )
req = req.required_by;
if ( Array.isArray(req) ) {
const idx = req.indexOf(key);
if ( idx !== -1 )
req.splice(idx, 1);
}
}
if ( definition.changed )
this.off(`:changed:${key}`, definition.changed);
this.emit(':removed-definition', key, definition);
}
addUI(key, definition, source) {
if ( typeof key === 'object' ) {
for(const k in key)
if ( has(key, k) )
@ -1018,6 +1089,8 @@ export default class SettingsManager extends Module {
if ( ! definition.ui )
definition = {ui: definition};
definition.__source = source;
const ui = definition.ui;
ui.path_tokens = ui.path_tokens ?
format_path_tokens(ui.path_tokens) :
@ -1038,14 +1111,16 @@ export default class SettingsManager extends Module {
}
addClearable(key, definition) {
addClearable(key, definition, source) {
if ( typeof key === 'object' ) {
for(const k in key)
if ( has(key, k) )
this.addClearable(k, key[k]);
this.addClearable(k, key[k], source);
return;
}
definition.__source = source;
this.clearables[key] = definition;
}

View file

@ -276,6 +276,24 @@ export default class ChatHook extends Module {
// Settings
this.settings.add('chat.disable-handling', {
default: null,
requires: ['context.disable-chat-processing'],
process(ctx, val) {
if ( val != null )
return ! val;
if ( ctx.get('context.disable-chat-processing') )
return true;
return false;
},
ui: {
path: 'Debugging > Chat >> Processing',
title: 'Enable processing of chat messages.',
component: 'setting-check-box',
force_seen: true
}
});
this.settings.addUI('debug.chat-test', {
path: 'Debugging > Chat >> Chat',
component: 'chat-tester',
@ -887,6 +905,11 @@ export default class ChatHook extends Module {
}
updateDisableHandling() {
this.disable_handling = this.chat.context.get('chat.disable-handling');
}
onEnable() {
this.on('site.web_munch:loaded', this.grabTypes);
this.on('site.web_munch:loaded', this.defineClasses);
@ -909,6 +932,8 @@ export default class ChatHook extends Module {
this.chat.context.on('changed:chat.banners.prediction', this.cleanHighlights, this);
this.chat.context.on('changed:chat.banners.drops', this.cleanHighlights, this);
this.chat.context.on('changed:chat.disable-handling', this.updateDisableHandling, this);
this.chat.context.on('changed:chat.subs.gift-banner', () => this.GiftBanner.forceUpdate(), this);
this.chat.context.on('changed:chat.effective-width', this.updateChatCSS, this);
this.settings.main_context.on('changed:chat.use-width', this.updateChatCSS, this);
@ -992,6 +1017,7 @@ export default class ChatHook extends Module {
this.chat.context.getChanges('chat.input.show-elevate-your-message', val =>
this.css_tweaks.toggleHide('elevate-your-message', ! val));
this.updateDisableHandling();
this.updateChatCSS();
this.updateColors();
this.updateLineBorders();
@ -1325,7 +1351,7 @@ export default class ChatHook extends Module {
});
this.subpump.on(':pubsub-message', event => {
if ( event.prefix !== 'community-points-channel-v1' )
if ( event.prefix !== 'community-points-channel-v1' || this.disable_handling )
return;
const service = this.ChatService.first,
@ -2186,7 +2212,7 @@ export default class ChatHook extends Module {
const old_announce = this.onAnnouncementEvent;
this.onAnnouncementEvent = function(e) {
console.log('announcement', e);
//console.log('announcement', e);
return old_announce.call(this, e);
}
@ -2197,6 +2223,9 @@ export default class ChatHook extends Module {
if ( t.chat.context.get('chat.filtering.blocked-types').has('Subscription') )
return;
if ( t.disable_handling )
return old_sub.call(i, e);
if ( t.chat.context.get('chat.subs.show') < 3 )
return;
@ -2236,6 +2265,9 @@ export default class ChatHook extends Module {
if ( t.chat.context.get('chat.filtering.blocked-types').has('Resubscription') )
return;
if ( t.disable_handling )
return old_resub.call(i, e);
if ( t.chat.context.get('chat.subs.show') < 2 && ! e.body )
return;
@ -2267,6 +2299,9 @@ export default class ChatHook extends Module {
if ( t.chat.context.get('chat.filtering.blocked-types').has('SubGift') )
return;
if ( t.disable_handling )
return old_subgift.call(i, e);
const key = `${e.channel}:${e.user.userID}`,
mystery = mysteries[key];
@ -2316,6 +2351,9 @@ export default class ChatHook extends Module {
const old_communityintro = this.onCommunityIntroductionEvent;
this.onCommunityIntroductionEvent = function(e) {
try {
if ( t.disable_handling )
return old_communityintro.call(this, e);
if ( t.chat.context.get('chat.filtering.blocked-types').has('CommunityIntroduction') ) {
const out = i.convertMessage(e);
return i.postMessageToCurrentChannel(e, out);
@ -2335,6 +2373,9 @@ export default class ChatHook extends Module {
if ( t.chat.context.get('chat.filtering.blocked-types').has('AnonSubGift') )
return;
if ( t.disable_handling )
return old_anonsubgift.call(i, e);
const key = `${e.channel}:ANON`,
mystery = mysteries[key];
@ -2388,6 +2429,9 @@ export default class ChatHook extends Module {
if ( t.chat.context.get('chat.filtering.blocked-types').has('SubMysteryGift') )
return;
if ( t.disable_handling )
return old_submystery.call(i, e);
let mystery = null;
if ( e.massGiftCount > t.chat.context.get('chat.subs.merge-gifts') ) {
const key = `${e.channel}:${e.user.userID}`;
@ -2423,6 +2467,9 @@ export default class ChatHook extends Module {
if ( t.chat.context.get('chat.filtering.blocked-types').has('AnonSubMysteryGift') )
return;
if ( t.disable_handling )
return old_anonsubmystery.call(i, e);
let mystery = null;
if ( e.massGiftCount > t.chat.context.get('chat.subs.merge-gifts') ) {
const key = `${e.channel}:ANON`;
@ -2457,6 +2504,9 @@ export default class ChatHook extends Module {
if ( t.chat.context.get('chat.filtering.blocked-types').has('Ritual') )
return;
if ( t.disable_handling )
return old_ritual.call(i, e);
const out = i.convertMessage(e);
out.ffz_type = 'ritual';
out.ritual = e.type;
@ -2475,6 +2525,9 @@ export default class ChatHook extends Module {
if ( t.chat.context.get('chat.filtering.blocked-types').has('ChannelPointsReward') )
return;
if ( t.disable_handling )
return old_points.call(i, e);
const reward = e.rewardID && get(e.rewardID, i.props.rewardMap);
if ( reward ) {
const out = i.convertMessage(e);

View file

@ -39,6 +39,12 @@ export default class ChatLine extends Module {
this.line_types = {};
this.line_types.unknown = {
renderNotice: (msg, current_user, room, inst, e) => {
return `Unknown message type: ${msg.ffz_type}`
}
};
this.line_types.cheer = {
renderNotice: (msg, current_user, room, inst, e) => {
return this.i18n.tList(
@ -741,6 +747,9 @@ other {# messages were deleted by a moderator.}
if ( ! type && msg.bits > 0 && t.chat.context.get('chat.bits.cheer-notice') )
type = t.line_types.cheer;
if ( ! type && msg.ffz_type )
type = t.line_types.unknown;
if ( type ) {
if ( type.render )
return type.render(msg, current_user, current_room, this, e);

View file

@ -192,6 +192,7 @@ export default class RichContent extends Module {
i18n: t.i18n,
fragments: this.state.fragments,
i18n_prefix: this.state.i18n_prefix,
allow_media: t.chat.context.get('tooltip.link-images'),
allow_unsafe: t.chat.context.get('tooltip.link-nsfw-images')

View file

@ -8,6 +8,13 @@ export class Addon extends Module {
this.inject('settings');
}
__processModule(module, name) {
if ( module.getAddonProxy )
return module.getAddonProxy(this);
return module;
}
static register(id, info) {
if ( typeof id === 'object' ) {
info = id;

View file

@ -271,6 +271,13 @@ export class ManagedStyle {
this._style = null;
}
get(key) {
const block = this._blocks[key];
if ( block )
return block.textContent;
return undefined;
}
set(key, value, force) {
const block = this._blocks[key];
if ( block ) {

View file

@ -132,6 +132,36 @@ export class EventEmitter {
this.__dead_events++;
}
offContext(event, ctx) {
if ( event == null ) {
for(const evt in Object.keys(this.__listeners)) {
if ( ! this.__running.has(evt) )
this.offContext(evt, ctx);
}
return;
}
if ( this.__running.has(event) )
throw new Error(`concurrent modification: tried removing event listener while event is running`);
let list = this.__listeners[event];
if ( ! list )
return;
if ( ! fn )
list = null;
else {
list = list.filter(x => x && x[1] !== ctx);
if ( ! list.length )
list = null;
}
this.__listeners[event] = list;
if ( ! list )
this.__dead_events++;
}
events() {
this.__cleanListeners();
return Object.keys(this.__listeners);

View file

@ -106,5 +106,10 @@ export default [
"right-open",
"list-bullet",
"mastodon",
"volume-up"
"volume-up",
"unmod",
"mod",
"flag",
"mange-suspicious",
"doc-text"
];

View file

@ -111,6 +111,13 @@ export class Module extends EventEmitter {
return this.__disable(args, this.__path, []);
}
canUnload() {
return this.__canUnload(this.__path, []);
}
canDisable() {
return this.__canDisable(this.__path, []);
}
__load(args, initial, chain) {
const path = this.__path || this.name,
@ -172,6 +179,43 @@ export class Module extends EventEmitter {
}
__canUnload(initial, chain) {
const path = this.__path || this.name,
state = this.__load_state;
if ( chain.includes(this) )
throw new CyclicDependencyError(`cyclic load requirements when checking if can unload ${initial}`, [...chain, this]);
else if ( this.load_dependents ) {
chain.push(this);
for(const dep of this.load_dependents) {
const module = this.resolve(dep);
if ( module ) {
if ( chain.includes(module) )
throw new CyclicDependencyError(`cyclic load requirements when checking if can unload ${initial}`, [...chain, this, module]);
if ( ! module.__canUnload(initial, Array.from(chain)) )
return false;
}
}
}
if ( state === State.UNLOADING )
return true;
else if ( state === State.UNLOADED )
return true;
else if ( this.onLoad && ! this.onUnload )
return false;
else if ( state === State.LOADING )
return false;
return true;
}
__unload(args, initial, chain) {
const path = this.__path || this.name,
state = this.__load_state;
@ -193,7 +237,7 @@ export class Module extends EventEmitter {
else if ( state === State.UNLOADED )
return Promise.resolve();
else if ( ! this.onUnload )
else if ( this.onLoad && ! this.onUnload )
return Promise.reject(new ModuleError(`attempted to unload module ${path} but module cannot be unloaded`));
else if ( state === State.LOADING )
@ -220,7 +264,9 @@ export class Module extends EventEmitter {
}
this.__time('unload-self');
if ( this.onUnload )
return this.onUnload(...args);
return null;
})().then(ret => {
this.__load_state = State.UNLOADED;
@ -307,6 +353,40 @@ export class Module extends EventEmitter {
}
__canDisable(initial, chain) {
const path = this.__path || this.name,
state = this.__state;
if ( chain.includes(this) )
throw new CyclicDependencyError(`cyclic load requirements when checking if can disable ${initial}`, [...chain, this]);
else if ( this.dependents ) {
chain.push(this);
for(const dep of this.dependents) {
const module = this.resolve(dep);
if ( module ) {
if ( chain.includes(module) )
throw new CyclicDependencyError(`cyclic load requirements when checking if can disable ${initial}`, [...chain, this, module]);
if ( ! module.__canDisable(initial, Array.from(chain)) )
return false;
}
}
}
if ( state === State.DISABLING || state === State.DISABLED )
return true;
else if ( ! this.onDisable )
return false;
else if ( state === State.ENABLING )
return false;
return true;
}
__disable(args, initial, chain) {
const path = this.__path || this.name,
state = this.__state;
@ -516,6 +596,11 @@ export class Module extends EventEmitter {
if ( this.enabled && ! module.enabled )
module.enable();
module.references.push([this.__path, name]);
if ( this.__processModule )
module = this.__processModule(module, name);
return this[name] = module;
}
@ -569,9 +654,15 @@ export class Module extends EventEmitter {
if ( require )
requires.push(module.abs_path('.'));
if ( this.enabled && ! module.enabled )
module.enable();
module.references.push([this.__path, variable]);
if ( this.__processModule )
module = this.__processModule(module, name);
return this[variable] = module;
}
@ -600,6 +691,7 @@ export class Module extends EventEmitter {
inst.dependents = dependents[0];
inst.load_dependents = dependents[1];
inst.references = dependents[2];
if ( inst instanceof SiteModule && ! requires.includes('site') )
requires.push('site');

View file

@ -8,7 +8,7 @@ import {has} from 'utilities/object';
import Markdown from 'markdown-it';
import MILA from 'markdown-it-link-attributes';
export const VERSION = 6;
export const VERSION = 7;
export const TOKEN_TYPES = {};
@ -282,6 +282,34 @@ TOKEN_TYPES.box = function(token, createElement, ctx) {
style['--ffz-lines'] = token.lines;
}
if ( token.border )
classes.push('tw-border');
if ( token.rounding ) {
const round = getRoundClass(token.rounding);
if ( round )
classes.push(round);
}
if ( token.background ) {
if ( token.background === 'text' )
style.backgroundColor = `var(--color-text-base)`;
else if ( token.background === 'text-alt' )
style.backgroundColor = `var(--color-text-alt)`;
else if ( token.background === 'text-alt-2' )
style.backgroundColor = `var(--color-text-alt-2)`;
else if ( VALID_COLORS.includes(token.background) )
classes.push(`tw-c-background-${token.background}`);
else
style.backgroundColor = token.background;
}
if ( token.width )
style.width = token.width;
if ( token.height )
style.height = token.height;
applySpacing('pd', token, classes, style);
applySpacing('mg', token, classes, style);
@ -338,7 +366,8 @@ TOKEN_TYPES.fieldset = function(token, createElement, ctx) {
const name = renderTokens(field.name, createElement, ctx, token.markdown),
value = renderTokens(field.value, createElement, ctx, token.markdown);
value = renderTokens(field.value, createElement, ctx, token.markdown),
icon = renderTokens(field.icon, createElement, ctx, token.markdown);
if ( name == null || value == null )
continue;
@ -347,20 +376,19 @@ TOKEN_TYPES.fieldset = function(token, createElement, ctx) {
fields.push(createElement('div', {
class: [
'ffz--field',
field.inline ? 'ffz--field-inline' : false
field.inline ? 'ffz--field-inline' : false,
icon ? 'ffz--field-icon' : false
]
}, [
createElement('div', {
class: 'ffz--field__name tw-semibold'
}, name),
createElement('div', {
class: 'ffz--field__value tw-c-text-alt'
}, value)
createElement('div', {class: 'ffz--field__icon'}, icon),
createElement('div', {class: 'ffz--field__name tw-semibold'}, name),
createElement('div', {class: 'ffz--field__value tw-c-text-alt'}, value)
]));
else
fields.push(createElement('div', {
className: `ffz--field ${field.inline ? 'ffz--field-inline' : ''}`
className: `ffz--field ${field.inline ? 'ffz--field-inline' : ''} ${icon ? 'ffz--field-icon' : ''}`
}, [
createElement('div', {className: 'ffz--field__icon'}, icon),
createElement('div', {className: 'ffz--field__name tw-semibold'}, name),
createElement('div', {className: 'ffz--field__value tw-c-text-alt'}, value)
]));
@ -531,6 +559,7 @@ TOKEN_TYPES.gallery = function(token, createElement, ctx) {
function header_vue(token, h, ctx) {
let content = [];
let background;
if ( token.title ) {
const out = renderWithCapture(token.title, h, ctx, token.markdown);
@ -569,6 +598,25 @@ function header_vue(token, h, ctx) {
]
}, content);
let bgtoken = resolveToken(token.sfw_background, ctx);
const nsfw_bg_token = resolveToken(token.background, ctx);
if ( nsfw_bg_token && canShowImage(nsfw_bg_token, ctx) )
bgtoken = nsfw_bg_token;
if ( bgtoken ) {
if ( bgtoken.type === 'image' )
background = render_image({
...bgtoken,
aspect: undefined
}, h, ctx);
else if ( bgtoken.type === 'icon' )
background = h('figure', {
class: `ffz-i-${bgtoken.name}`
});
else
background = renderWithCapture(token.background, h, ctx, token.markdown).content;
}
let imtok = resolveToken(token.sfw_image, ctx);
const nsfw_token = resolveToken(token.image, ctx);
if ( nsfw_token && canShowImage(nsfw_token, ctx) )
@ -576,11 +624,19 @@ function header_vue(token, h, ctx) {
if ( imtok ) {
const aspect = imtok.aspect;
let image;
let image = render_image({
if ( imtok.type === 'image' )
image = render_image({
...imtok,
aspect: undefined
}, h, ctx);
if ( imtok.type === 'icon' )
image = h('figure', {
class: `ffz-i-${imtok.name}`
});
const right = token.image_side === 'right';
if ( image ) {
@ -626,11 +682,24 @@ function header_vue(token, h, ctx) {
content
]);
if ( background )
content = h('div', {
class: 'ffz--rich-header--background'
}, [
h('div', {
class: 'ffz--rich-header__background'
}, [
background
]),
content
]);
return content;
}
function header_normal(token, createElement, ctx) {
let content = [];
let background;
if ( token.title ) {
const out = renderWithCapture(token.title, createElement, ctx, token.markdown);
@ -656,6 +725,25 @@ function header_normal(token, createElement, ctx) {
}, out.content));
}
let bgtoken = resolveToken(token.sfw_background, ctx);
const nsfw_bg_token = resolveToken(token.background, ctx);
if ( nsfw_bg_token && canShowImage(nsfw_bg_token, ctx) )
bgtoken = nsfw_bg_token;
if ( bgtoken ) {
if ( bgtoken.type === 'image' )
background = render_image({
...bgtoken,
aspect: undefined
}, createElement, ctx);
else if ( bgtoken.type === 'icon' )
background = createElement('figure', {
className: `ffz-i-${bgtoken.name}`
});
else
background = renderWithCapture(token.background, createElement, ctx, token.markdown).content;
}
content = createElement('div', {
className: `tw-flex tw-full-width tw-overflow-hidden ${token.compact ? 'ffz--rich-header ffz--compact-header tw-align-items-center' : 'tw-justify-content-center tw-flex-column tw-flex-grow-1'}`
}, content);
@ -668,10 +756,19 @@ function header_normal(token, createElement, ctx) {
if ( imtok ) {
const aspect = imtok.aspect;
let image = render_image({
let image;
if ( imtok.type === 'image' )
image = render_image({
...imtok,
aspect: undefined
}, createElement, ctx);
if ( imtok.type === 'icon' )
image = createElement('figure', {
className: `ffz-i-${imtok.name}`
});
const right = token.image_side === 'right';
if ( image ) {
@ -718,6 +815,16 @@ function header_normal(token, createElement, ctx) {
content
]);
if ( background )
content = createElement('div', {
className: 'ffz--rich-header--background'
}, [
createElement('div', {
className: 'ffz--rich-header__background'
}, background),
content
]);
return content;
}
@ -783,6 +890,9 @@ function render_image(token, createElement, ctx) {
}
};
if ( token.contain )
stuff.style.objectFit = 'contain';
if ( ctx.onload )
stuff.on = {load: ctx.onload};
@ -811,6 +921,9 @@ function render_image(token, createElement, ctx) {
}
});
if ( token.contain )
image.style.objectFit = 'contain';
if ( ! aspect )
return image;
@ -840,8 +953,12 @@ TOKEN_TYPES.i18n = function(token, createElement, ctx) {
return null;
}
let key = token.key;
if ( ctx.i18n_prefix )
key = `${ctx.i18n_prefix}.${key}`;
return renderTokens(
ctx.i18n.tList(token.key, token.phrase, token.content),
ctx.i18n.tList(key, token.phrase, token.content),
createElement,
ctx,
token.markdown

View file

@ -48,6 +48,70 @@
margin-right: -0.5rem;
}
.ffz--chat-card {
--ffz-rich-header-outline: var(--color-background-base);
.ffz--rich-header--background {
margin: 0; // overflow hidden is in play
}
}
.ffz__tooltip {
--ffz-rich-header-outline: var(--color-background-tooltip);
.ffz--rich-header--background {
margin: -.8rem;
}
}
.ffz--rich-header--background {
position: relative;
overflow: hidden;
--ffz-rich-header-outline: #000;
--color-background-base: #000;
--color-text-base: #efeff1;
--color-text-alt: #dedee3;
--color-text-alt-2: #adadb8;
--color-background-tooltip: var(--color-background-base);
--color-text-tooltip: var(--color-text-base);
--color-text-tooltip-alt: var(--color-text-alt);
--color-text-tooltip-alt-2: var(--color-text-alt-2);
padding: 1rem;
margin: -1rem;
margin-bottom: 0 !important;
background: var(--color-background-base);
text-shadow: -1px 1px 2px var(--ffz-rich-header-outline),
1px 1px 2px var(--ffz-rich-header-outline),
1px -1px 0 var(--ffz-rich-header-outline),
-1px -1px 0 var(--ffz-rich-header-outline);
& > * {
position: relative;
z-index: 1;
}
.ffz--rich-header__background {
position: absolute !important;
z-index: 0 !important;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0.5;
& > img {
height: 100%;
width: 100%;
object-fit: cover;
}
}
}
.ffz--overlay {
position: relative;
@ -132,6 +196,11 @@
height: 4.8rem;
max-width: 25%;
figure {
line-height: 4.8rem;
font-size: 2.4rem;
}
img {
object-fit: contain;
height: 100%;
@ -145,6 +214,12 @@
.ffz--compact-header .ffz--header-image {
height: 2.4rem;
figure {
line-height: 2.4rem;
font-size: 1.6rem;
}
}
.ffz--rich-gallery, .ffz--compact-header {
@ -200,6 +275,18 @@
width: unset;
min-width: 150px;
}
.ffz--field-icon {
position: relative;
padding-left: 2.5rem;
}
}
.ffz--field__icon {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 0;
}
.ffz--twitter-badge {

View file

@ -72,6 +72,10 @@
.ffz-i-right-open:before { content: '\e846'; } /* '' */
.ffz-i-mastodon:before { content: '\e847'; } /* '' */
.ffz-i-volume-up:before { content: '\e848'; } /* '' */
.ffz-i-unmod:before { content: '\e849'; } /* '' */
.ffz-i-mod:before { content: '\e84a'; } /* '' */
.ffz-i-flag:before { content: '\e84b'; } /* '' */
.ffz-i-mange-suspicious:before { content: '\e84c'; } /* '' */
.ffz-i-move:before { content: '\f047'; } /* '' */
.ffz-i-link-ext:before { content: '\f08e'; } /* '' */
.ffz-i-twitter:before { content: '\f099'; } /* '' */
@ -84,6 +88,7 @@
.ffz-i-chat-empty:before { content: '\f0e6'; } /* '' */
.ffz-i-download-cloud:before { content: '\f0ed'; } /* '' */
.ffz-i-upload-cloud:before { content: '\f0ee'; } /* '' */
.ffz-i-doc-text:before { content: '\f0f6'; } /* '' */
.ffz-i-reply:before { content: '\f112'; } /* '' */
.ffz-i-smile:before { content: '\f118'; } /* '' */
.ffz-i-keyboard:before { content: '\f11c'; } /* '' */

File diff suppressed because one or more lines are too long

View file

@ -72,6 +72,10 @@
.ffz-i-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe846;&nbsp;'); }
.ffz-i-mastodon { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe847;&nbsp;'); }
.ffz-i-volume-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe848;&nbsp;'); }
.ffz-i-unmod { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe849;&nbsp;'); }
.ffz-i-mod { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84a;&nbsp;'); }
.ffz-i-flag { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84b;&nbsp;'); }
.ffz-i-mange-suspicious { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84c;&nbsp;'); }
.ffz-i-move { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf047;&nbsp;'); }
.ffz-i-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); }
.ffz-i-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf099;&nbsp;'); }
@ -84,6 +88,7 @@
.ffz-i-chat-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e6;&nbsp;'); }
.ffz-i-download-cloud { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0ed;&nbsp;'); }
.ffz-i-upload-cloud { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0ee;&nbsp;'); }
.ffz-i-doc-text { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0f6;&nbsp;'); }
.ffz-i-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf112;&nbsp;'); }
.ffz-i-smile { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf118;&nbsp;'); }
.ffz-i-keyboard { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf11c;&nbsp;'); }

View file

@ -83,6 +83,10 @@
.ffz-i-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe846;&nbsp;'); }
.ffz-i-mastodon { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe847;&nbsp;'); }
.ffz-i-volume-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe848;&nbsp;'); }
.ffz-i-unmod { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe849;&nbsp;'); }
.ffz-i-mod { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84a;&nbsp;'); }
.ffz-i-flag { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84b;&nbsp;'); }
.ffz-i-mange-suspicious { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84c;&nbsp;'); }
.ffz-i-move { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf047;&nbsp;'); }
.ffz-i-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); }
.ffz-i-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf099;&nbsp;'); }
@ -95,6 +99,7 @@
.ffz-i-chat-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e6;&nbsp;'); }
.ffz-i-download-cloud { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0ed;&nbsp;'); }
.ffz-i-upload-cloud { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0ee;&nbsp;'); }
.ffz-i-doc-text { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0f6;&nbsp;'); }
.ffz-i-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf112;&nbsp;'); }
.ffz-i-smile { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf118;&nbsp;'); }
.ffz-i-keyboard { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf11c;&nbsp;'); }

View file

@ -1,11 +1,11 @@
@font-face {
font-family: 'ffz-fontello';
src: url('../font/ffz-fontello.eot?19069837');
src: url('../font/ffz-fontello.eot?19069837#iefix') format('embedded-opentype'),
url('../font/ffz-fontello.woff2?19069837') format('woff2'),
url('../font/ffz-fontello.woff?19069837') format('woff'),
url('../font/ffz-fontello.ttf?19069837') format('truetype'),
url('../font/ffz-fontello.svg?19069837#ffz-fontello') format('svg');
src: url('../font/ffz-fontello.eot?30569253');
src: url('../font/ffz-fontello.eot?30569253#iefix') format('embedded-opentype'),
url('../font/ffz-fontello.woff2?30569253') format('woff2'),
url('../font/ffz-fontello.woff?30569253') format('woff'),
url('../font/ffz-fontello.ttf?30569253') format('truetype'),
url('../font/ffz-fontello.svg?30569253#ffz-fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@ -15,7 +15,7 @@
@media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face {
font-family: 'ffz-fontello';
src: url('../font/ffz-fontello.svg?19069837#ffz-fontello') format('svg');
src: url('../font/ffz-fontello.svg?30569253#ffz-fontello') format('svg');
}
}
*/
@ -127,6 +127,10 @@
.ffz-i-right-open:before { content: '\e846'; } /* '' */
.ffz-i-mastodon:before { content: '\e847'; } /* '' */
.ffz-i-volume-up:before { content: '\e848'; } /* '' */
.ffz-i-unmod:before { content: '\e849'; } /* '' */
.ffz-i-mod:before { content: '\e84a'; } /* '' */
.ffz-i-flag:before { content: '\e84b'; } /* '' */
.ffz-i-mange-suspicious:before { content: '\e84c'; } /* '' */
.ffz-i-move:before { content: '\f047'; } /* '' */
.ffz-i-link-ext:before { content: '\f08e'; } /* '' */
.ffz-i-twitter:before { content: '\f099'; } /* '' */
@ -139,6 +143,7 @@
.ffz-i-chat-empty:before { content: '\f0e6'; } /* '' */
.ffz-i-download-cloud:before { content: '\f0ed'; } /* '' */
.ffz-i-upload-cloud:before { content: '\f0ee'; } /* '' */
.ffz-i-doc-text:before { content: '\f0f6'; } /* '' */
.ffz-i-reply:before { content: '\f112'; } /* '' */
.ffz-i-smile:before { content: '\f118'; } /* '' */
.ffz-i-keyboard:before { content: '\f11c'; } /* '' */

View file

@ -0,0 +1,54 @@
.ffz-ct--obj-open,
.ffz-ct--obj-close {
&[depth="1"], &[depth="5"], &[depth="9"] {
color: var(--color-text-alt-2);
}
&[depth="2"], &[depth="6"], &[depth="10"] {
color: var(--color-text-error);
}
&[depth="3"], &[depth="7"], &[depth="11"] {
color: var(--color-text-prime);
}
&[depth="4"], &[depth="8"] {
color: var(--color-text-success);
}
}
.ffz-ct--obj-sep,
.ffz-ct--obj-key-sep,
.ffz-ct--params,
.ffz-ct--prefix,
.ffz-ct--tags {
color: var(--color-text-alt-2);
}
.ffz-ct--obj-key,
.ffz-ct--command,
.ffz-ct--tag {
color: var(--color-text-warn);
}
.ffz-ct--channel,
.ffz-ct--user {
color: var(--color-text-success);
}
.ffz-ct--literal,
.ffz-ct--param {
color: var(--color-text-prime);
}
.ffz-ct--string,
.ffz-ct--tag-value {
color: var(--color-text-base);
}
.ffz-ct--tags,
.ffz-ct--params {
overflow-wrap: anywhere;
}