1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-28 15:27:43 +00:00
* Added: Setting to hide the "Not Live" bar beneath videos and clips that appears when the channel is currently live.
* Fixed: Handling of `https://www.twitch.tv/<channel>/clip/<slug>` urls for rich chat embeds and rich link tool-tips.
* Fixed: Lower the priority of custom highlight terms so they will not break links.
* Fixed: Holding multiple modifier keys to display in-line chat actions.
* Fixed: Clean up out-dated avatar display setting for the directory.

* API Added: Allow add-ons to access the Popper JS library via `FrankerFaceZ.utilities.popper`.
* API Added: `<icon-picker />` Vue component for selecting an icon.
* API Added: `<react-link />` Vue component for creating links that cause the React app to navigate without a page load.
* API Added: `<t-list />` Vue component for translating text including Vue elements.
* API Added: `maybeLoad(icon)` function for font awesome to only load the font if the icon is from font awesome.
* API Added: `generateUUID()` function to `FrankerFaceZ.utilities.object`
* API Added: The `vue-observe-visibility` module is now loaded with Vue and made available in all Vue contexts.
This commit is contained in:
SirStendec 2019-06-08 17:35:48 -04:00
parent 3157aeb390
commit aa25bff498
22 changed files with 210 additions and 218 deletions

5
package-lock.json generated
View file

@ -9887,6 +9887,11 @@
}
}
},
"vue-observe-visibility": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/vue-observe-visibility/-/vue-observe-visibility-0.4.4.tgz",
"integrity": "sha512-2eDYHgL2MJ2wkkNZnus56D0CG8m80BFLuvEcGnD7rQ9jxFogpXMsM9aM5Md+XT8AmYGYCqVfOVBaWFtVvwbpmw=="
},
"vue-style-loader": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-3.1.2.tgz",

View file

@ -74,6 +74,7 @@
"vue": "^2.5.16",
"vue-clickaway": "^2.2.2",
"vue-color": "^2.4.6",
"vue-observe-visibility": "^0.4.4",
"vue-template-compiler": "^2.5.16",
"vuedraggable": "^2.16.0"
}

Binary file not shown.

View file

@ -100,6 +100,12 @@
<glyph glyph-name="camera" unicode="&#xe82e;" d="M560 600c22 0 39-17 40-38v-424a40 40 0 0 0-40-38h-420a40 40 0 0 0-40 40v421c0 21 18 39 40 39h420z m323-55c11-7 17-18 17-31v-328a35 35 0 0 0-17-30 37 37 0 0 0-35-2l-148 54v284l148 55a37 37 0 0 0 35-2z" horiz-adv-x="1000" />
<glyph glyph-name="cw" unicode="&#xe82f;" d="M857 707v-250q0-14-10-25t-26-11h-250q-23 0-32 23-10 22 7 38l77 77q-82 77-194 77-58 0-111-23t-91-61-61-91-23-111 23-111 61-91 91-61 111-23q66 0 125 29t100 82q4 6 13 7 8 0 14-5l76-77q5-4 6-11t-5-13q-60-74-147-114t-182-41q-87 0-167 34t-136 92-92 137-34 166 34 166 92 137 136 92 167 34q82 0 158-31t137-88l72 72q17 18 39 8 22-9 22-33z" horiz-adv-x="857.1" />
<glyph glyph-name="up-dir" unicode="&#xe830;" d="M571 171q0-14-10-25t-25-10h-500q-15 0-25 10t-11 25 11 26l250 250q10 10 25 10t25-10l250-250q10-11 10-26z" horiz-adv-x="571.4" />
<glyph glyph-name="up-big" unicode="&#xe831;" d="M899 308q0-28-21-50l-41-42q-22-21-51-21-30 0-50 21l-165 164v-393q0-29-20-47t-51-19h-71q-30 0-51 19t-21 47v393l-164-164q-20-21-50-21t-50 21l-42 42q-21 21-21 50 0 30 21 51l363 363q20 21 50 21 30 0 51-21l363-363q21-22 21-51z" horiz-adv-x="928.6" />
<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" />
<glyph glyph-name="twitter" unicode="&#xf099;" 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" />

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Before After
Before After

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -151,7 +151,7 @@ ${typeof x[1] === 'string' ? x[1] : JSON.stringify(x[1], null, 4)}`
FrankerFaceZ.Logger = Logger;
const VER = FrankerFaceZ.version_info = {
major: 4, minor: 2, revision: 6,
major: 4, minor: 3, revision: 0,
commit: __git_commit__,
build: __webpack_hash__,
toString: () =>
@ -174,7 +174,8 @@ FrankerFaceZ.utilities = {
time: require('utilities/time'),
tooltip: require('utilities/tooltip'),
i18n: require('utilities/translation-core'),
dayjs: require('dayjs')
dayjs: require('dayjs'),
popper: require('popper.js').default
}

View file

@ -4,140 +4,15 @@
{{ t('setting.actions.icon', 'Icon') }}
</label>
<div class="tw-full-width">
<div class="tw-search-input">
<label for="icon-search" class="tw-hide-accessible">
{{ t('setting.actions.icon.search', 'Search for Icon') }}
</label>
<div class="tw-relative tw-mg-t-05">
<div class="tw-absolute tw-align-items-center tw-c-text-alt-2 tw-flex tw-full-height tw-input__icon tw-justify-content-center tw-left-0 tw-top-0 tw-z-default">
<figure class="ffz-i-search" />
</div>
<input
id="icon-search"
:placeholder="t('setting.actions.icon.search', 'Search for Icon')"
v-model="search"
type="search"
class="tw-block tw-border-radius-medium tw-font-size-6 tw-full-width tw-input tw-pd-l-3 tw-pd-r-1 tw-pd-y-05"
autocapitalize="off"
autocorrect="off"
autocomplete="off"
>
</div>
</div>
<simplebar classes="tw-c-background-alt-2 tw-border-l tw-border-r tw-border-b ffz-icon-picker tw-mg-b-05">
<div v-if="visible.length" role="radiogroup" class="tw-pd-1 tw-flex tw-flex-wrap" >
<div
v-for="i of visible"
:key="i[0]"
:aria-checked="value.icon === i[0]"
:class="{'tw-interactable--selected': value.icon === i[0]}"
class="ffz-icon tw-interactable tw-interactable--inverted"
role="radio"
tabindex="0"
@keydown.space.stop.prevent=""
@keyup.space="change(i[0])"
@keyup.enter="change(i[0])"
@click="change(i[0])"
>
<figure :class="`tw-mg-y-05 tw-mg-x-1 ${i[0]}`" />
</div>
</div>
<div v-else class="tw-align-center tw-pd-1 tw-c-text-alt-2">
{{ t('setting.actions.empty-search', 'no results') }}
</div>
</simplebar>
</div>
<icon-picker :value="value.icon" @input="change" />
</div>
</template>
<script>
import {escape_regex, deep_copy} from 'utilities/object';
import {load, ICONS as FA_ICONS, ALIASES as FA_ALIASES} from 'utilities/font-awesome';
const FFZ_ICONS = [
'zreknarf',
'crown',
'verified',
'inventory',
'ignore',
'pin-outline',
'pin',
'block',
'ok',
'clock',
'eye',
'eye-off',
'trash',
'discord',
'star',
'star-empty',
'twitch',
'twitter',
'download',
'upload',
'download-cloud',
'upload-cloud',
'tag',
'tags',
'retweet',
'thumbs-up',
'thumbs-down',
'bell',
'bell-off',
'pencil',
'info',
'help',
'calendar',
'lock',
'lock-open',
'arrows-cw',
'gift',
'eyedropper',
'github',
'user-secret'
];
const FFZ_ALIASES = {
'block': ['ban', 'block'],
'ok': ['ok', 'unban', 'untimeout', 'checkmark'],
'clock': ['clock', 'clock-o', 'timeout']
};
const ICONS = FFZ_ICONS
.map(x => [`ffz-i-${x}`, FFZ_ALIASES[x] ? FFZ_ALIASES[x].join(' ') : x])
.concat(FA_ICONS.filter(x => ! FFZ_ICONS.includes(x)).map(x => [`ffz-fa fa-${x}`, FA_ALIASES[x] ? FA_ALIASES[x].join(' ') : x]));
export default {
props: ['value'],
data() {
return {
search: '',
icons: deep_copy(ICONS)
}
},
computed: {
visible() {
if ( ! this.search || ! this.search.length )
return this.icons;
const search = this.search.toLowerCase().replace(' ', '-'),
reg = new RegExp('(?:^|-| )' + escape_regex(search), 'i');
return this.icons.filter(x => reg.test(x[1]));
}
},
mounted() {
load();
},
methods: {
change(val) {
this.value.icon = val;
@ -146,15 +21,4 @@ export default {
}
}
</script>
<style lang="scss" scoped>
.ffz-icon-picker {
max-height: 15rem;
font-size: 1.6rem;
.ffz-icon {
width: auto !important;
}
}
</style>
</script>

View file

@ -5,6 +5,7 @@
// ============================================================================
const CLIP_URL = /^(?:https?:\/\/)?clips\.twitch\.tv\/(\w+)(?:\/)?(\w+)?(?:\/edit)?/;
const NEW_CLIP_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/\w+\/clip\/(\w+)/;
const VIDEO_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/(?:\w+\/v|videos)\/(\w+)/;
import GET_CLIP from './clip_info.gql';
@ -74,13 +75,18 @@ export const Clips = {
hide_token: true,
test(token) {
return token.type === 'link' && CLIP_URL.test(token.url)
if ( token.type !== 'link' )
return false;
return CLIP_URL.test(token.url) || NEW_CLIP_URL.test(token.url);
},
process(token) {
const match = CLIP_URL.exec(token.url),
apollo = this.resolve('site.apollo');
let match = CLIP_URL.exec(token.url);
if ( ! match )
match = NEW_CLIP_URL.exec(token.url);
const apollo = this.resolve('site.apollo');
if ( ! apollo || ! match || match[1] === 'create' )
return;

View file

@ -415,7 +415,7 @@ export const BlockedBadges = {
export const CustomHighlights = {
type: 'highlight',
priority: 100,
priority: 35,
component: () => import(/* webpackChunkName: 'vue-chat' */ './components/chat-highlight.vue'),

View file

@ -319,18 +319,8 @@ export default class Scroller extends Module {
}
inst.ffzHandleKey = inst.ffzHandleKey.bind(inst);
Mousetrap.bindGlobal('alt', inst.ffzHandleKey, 'keydown');
Mousetrap.bindGlobal('alt', inst.ffzHandleKey, 'keyup');
Mousetrap.bindGlobal('shift', inst.ffzHandleKey, 'keydown');
Mousetrap.bindGlobal('shift', inst.ffzHandleKey, 'keyup');
Mousetrap.bindGlobal('ctrl', inst.ffzHandleKey, 'keydown');
Mousetrap.bindGlobal('ctrl', inst.ffzHandleKey, 'keyup');
Mousetrap.bindGlobal('command', inst.ffzHandleKey, 'keydown');
Mousetrap.bindGlobal('command', inst.ffzHandleKey, 'keyup');
window.addEventListener('keydown', inst.ffzHandleKey);
window.addEventListener('keyup', inst.ffzHandleKey);
inst.hoverPause = inst.ffzMouseMove.bind(inst);
inst.hoverResume = inst.ffzMouseLeave.bind(inst);
@ -624,18 +614,8 @@ export default class Scroller extends Module {
inst.ffzInstallHandler();
}
onUnmount() { // eslint-disable-line class-methods-use-this
const Mousetrap = this.web_munch.getModule('mousetrap') || window.Mousetrap;
if ( Mousetrap != null ) {
Mousetrap.unbind('alt', 'keydown');
Mousetrap.unbind('alt', 'keyup');
Mousetrap.unbind('shift', 'keydown');
Mousetrap.unbind('shift', 'keyup');
Mousetrap.unbind('ctrl', 'keydown');
Mousetrap.unbind('ctrl', 'keyup');
Mousetrap.unbind('command', 'keydown');
Mousetrap.unbind('command', 'keyup');
}
onUnmount(inst) { // eslint-disable-line class-methods-use-this
window.removeEventListener('keydown', inst.ffzHandleKey);
window.removeEventListener('keyup', inst.ffzHandleKey);
}
}

View file

@ -230,14 +230,15 @@ export default class CSSTweaks extends Module {
changed: val => this.toggle('square-avatars', !val)
});
this.settings.add('channel.not-live-bar', {
this.settings.add('channel.hide-not-live-bar', {
default: true,
ui: {
path: 'Channel > Appearance >> General',
title: 'Show notification below clips and videos if the streamer is live.',
title: 'Hide the "Not Live" bar.',
description: 'Hide the bar which appears beneath clips and videos when the streamer is live, telling you they are live.',
component: 'setting-check-box'
},
changed: val => this.toggle('not-live-bar', !val)
changed: val => this.toggleHide('not-live-bar', val)
});
}
@ -254,7 +255,7 @@ export default class CSSTweaks extends Module {
this.toggleHide('top-discover', !this.settings.get('layout.discover'));
this.toggle('square-avatars', ! this.settings.get('channel.round-avatars'));
this.toggleHide('not-live-bar', ! this.settings.get('channel.not-live-bar'));
this.toggleHide('not-live-bar', this.settings.get('channel.hide-not-live-bar'));
const recs = this.settings.get('layout.side-nav.show-rec-channels');
this.toggleHide('side-rec-channels', recs === 0);

View file

@ -85,26 +85,15 @@ export default class Directory extends SiteModule {
this.settings.add('directory.show-channel-avatars', {
default: 1,
default: true,
ui: {
path: 'Directory > Channels >> Appearance',
title: 'Channel Avatars',
description: 'Show channel avatars next to stream titles or directly on their thumbnails.',
component: 'setting-select-box',
data: [
{value: 0, title: 'Disabled'},
{value: 1, title: 'By Title'},
{value: 2, title: 'Over Thumbnail (Hidden on Hover)'},
{value: 3, title: 'Over Thumbnail'}
]
title: 'Display channel avatars.',
component: 'setting-check-box'
},
changed: value => {
this.css_tweaks.toggleHide('profile-hover', value === 2);
this.DirectoryCard.forceUpdate();
}
changed: () => this.DirectoryCard.forceUpdate()
});
@ -225,7 +214,7 @@ export default class Directory extends SiteModule {
cls.prototype.renderIconicImage = function() {
if ( this.props.context !== CARD_CONTEXTS.SingleChannelList &&
t.settings.get('directory.show-channel-avatars') !== 1 )
! t.settings.get('directory.show-channel-avatars') )
return;
return old_render_iconic.call(this);

View file

@ -2,7 +2,7 @@
<div class="ffz--icon-picker">
<div class="tw-full-width">
<div class="tw-search-input">
<label for="icon-search" class="tw-hide-accessible">
<label :for="'icon-search$' + id" class="tw-hide-accessible">
{{ t('setting.icon.search', 'Search for Icon') }}
</label>
<div class="tw-relative tw-mg-t-05">
@ -10,7 +10,7 @@
<figure class="ffz-i-search" />
</div>
<input
id="icon-search"
:id="'icon-search$' + id"
:placeholder="t('setting.actions.icon.search', 'Search for Icon')"
v-model="search"
type="search"
@ -23,13 +23,14 @@
</div>
<simplebar classes="tw-c-background-alt-2 tw-border-l tw-border-r tw-border-b ffz--icon-picker__list tw-mg-b-05">
<div v-if="visible.length" role="radiogroup" class="tw-pd-1 tw-flex tw-flex-wrap" >
<div v-if="visible.length" role="radiogroup" class="tw-pd-1 tw-flex tw-flex-wrap tw-justify-content-between" >
<div
v-for="i of visible"
:key="i[0]"
:aria-checked="value.icon === i[0]"
:class="{'tw-interactable--selected': value.icon === i[0]}"
class="ffz-icon tw-interactable tw-interactable--inverted"
:aria-checked="value === i[0]"
:class="{'tw-interactable--selected': value === i[0]}"
:data-title="i[1]"
class="ffz-tooltip ffz-icon tw-interactable tw-interactable--inverted"
role="radio"
tabindex="0"
@keydown.space.stop.prevent=""
@ -50,50 +51,81 @@
<script>
let id = 0;
import {escape_regex, deep_copy} from 'utilities/object';
import {load, ICONS as FA_ICONS, ALIASES as FA_ALIASES} from 'utilities/font-awesome';
const FFZ_ICONS = [
'cancel',
'zreknarf',
'crown',
'verified',
'inventory',
'ignore',
'pin-outline',
'pin',
'block',
'ok',
'search',
'clock',
'eye',
'eye-off',
'trash',
'discord',
'star',
'star-empty',
'twitch',
'twitter',
'down-dir',
'right-dir',
'attention',
'ok',
'cog',
'plus',
'folder-open',
'download',
'upload',
'download-cloud',
'upload-cloud',
'floppy',
'crown',
'verified',
'heart',
'heart-empty',
'tag',
'tags',
'retweet',
'thumbs-up',
'thumbs-down',
'bell',
'bell-off',
'pencil',
'info',
'help',
'calendar',
'left-dir',
'inventory',
'lock',
'lock-open',
'arrows-cw',
'ignore',
'block',
'pin',
'pin-outline',
'gift',
'eyedropper',
'discord',
'eye',
'eye-off',
'views',
'conversations',
'channels',
'camera',
'cw',
'up-dir',
'up-big',
'link-ext',
'twitter',
'github',
'user-secret'
'gauge',
'download-cloud',
'upload-cloud',
'smile',
'keyboard',
'calendar-empty',
'ellipsis-vert',
'twitch',
'bell-off',
'trash',
'eyedropper',
'user-secret',
'window-maximize',
'window-minimize',
'window-restore',
'window-close'
];
const FFZ_ALIASES = {
@ -112,6 +144,7 @@ export default {
data() {
return {
id: id++,
search: '',
icons: deep_copy(ICONS)
}
@ -131,11 +164,19 @@ export default {
mounted() {
load();
this.$nextTick(() => {
if ( this.value ) {
const el = this.$el.querySelector('.tw-interactable--selected');
if ( el )
el.scrollIntoViewIfNeeded();
}
});
},
methods: {
change(val) {
this.value.icon = val;
this.value = val;
this.$emit('input', this.value);
}
}

View file

@ -1,5 +1,5 @@
<template>
<a :href="href" @click="reactNavigate(href, $event)">
<a :href="href" @click="onClick($event)">
<slot />
</a>
</template>
@ -7,7 +7,17 @@
<script>
export default {
props: ['href']
props: ['href', 'click'],
methods: {
onClick(event) {
this.$emit('click', event);
if ( ! event.defaultPrevented )
this.reactNavigate(this.href, event);
}
}
}
</script>

View file

@ -237,3 +237,9 @@ export const load = () => {
}));
}
export const maybeLoad = icon => {
if ( loaded || ! String(icon).startsWith('fa-') && ! String(icon).startsWith('ffz-fa') )
return;
load();
}

View file

@ -2,6 +2,28 @@
const HOP = Object.prototype.hasOwnProperty;
// Source: https://gist.github.com/jed/982883 (WTFPL)
export function generateUUID(input) {
return input // if the placeholder was passed, return
? ( // a random number from 0 to 15
input ^ // unless b is 8,
Math.random() // in which case
* 16 // a random number from
>> input/4 // 8 to 11
).toString(16) // in hexadecimal
: ( // or otherwise a concatenated string:
[1e7] + // 10000000 +
-1e3 + // -1000 +
-4e3 + // -4000 +
-8e3 + // -80000000 +
-1e11 // -100000000000,
).replace( // replacing
/[018]/g, // zeroes, ones, and eights with
generateUUID // random hex digits
);
}
export function has(object, key) {
return object ? HOP.call(object, key) : false;
}

View file

@ -7,6 +7,7 @@
import Module from 'utilities/module';
import {has} from 'utilities/object';
import {DEBUG} from 'utilities/constants';
export class Vue extends Module {
@ -18,12 +19,15 @@ export class Vue extends Module {
async onLoad() {
const Vue = window.ffzVue = this.Vue = (await import(/* webpackChunkName: "vue" */ 'vue')).default,
ObserveVisibility = await import(/* webpackChunkName: "vue" */ 'vue-observe-visibility'),
RavenVue = await import(/* webpackChunkName: "vue" */ 'raven-js/plugins/vue'),
components = this._components;
this.component((await import(/* webpackChunkName: "vue" */ 'src/std-components/index.js')).default);
if ( this.root.raven )
Vue.use(ObserveVisibility);
if ( ! DEBUG && this.root.raven )
this.root.raven.addPlugin(RavenVue, Vue);
for(const key in components)
@ -82,6 +86,11 @@ export class Vue extends Module {
}
});
this.on('i18n:transform', () => {
this._vue_i18n.locale = this.i18n.locale;
this._vue_i18n.phrases = {};
});
this.on('i18n:changed', () => {
this._vue_i18n.locale = this.i18n.locale;
this._vue_i18n.phrases = {};
@ -97,6 +106,41 @@ export class Vue extends Module {
vue.prototype.$i18n = this._vue_i18n;
}
vue.component('t-list', {
props: {
tag: {
required: false
},
phrase: {
type: String,
required: true
},
default: {
type: String,
required: true
},
data: {
type: Object,
required: false
}
},
render(createElement) {
return createElement(
this.tag || 'span',
this.$i18n.tList_(
this.phrase,
this.default,
Object.assign({}, this.data, this.$scopedSlots)
).map(out => {
if ( typeof out === 'function' )
return out();
return out;
})
);
}
})
vue.mixin({
methods: {
reactNavigate(url, event) {

View file

@ -121,6 +121,9 @@
.ffz-i-conversations:before { content: '\e82c'; } /* '' */
.ffz-i-channels:before { content: '\e82d'; } /* '' */
.ffz-i-camera:before { content: '\e82e'; } /* '' */
.ffz-i-cw:before { content: '\e82f'; } /* '' */
.ffz-i-up-dir:before { content: '\e830'; } /* '' */
.ffz-i-up-big:before { content: '\e831'; } /* '' */
.ffz-i-link-ext:before { content: '\f08e'; } /* '' */
.ffz-i-twitter:before { content: '\f099'; } /* '' */
.ffz-i-github:before { content: '\f09b'; } /* '' */

View file

@ -10,6 +10,7 @@
@import "./widgets/add-ons.scss";
@import "./widgets/color-picker.scss";
@import "./widgets/icon-picker.scss";
.tw-display-inline { display: inline !important }

View file

@ -0,0 +1,12 @@
.ffz--icon-picker__list {
max-height: 15rem;
font-size: 1.6rem;
.ffz-icon {
width: auto !important;
> * {
pointer-events: none;
}
}
}