diff --git a/res/font/ffz-fontello.eot b/res/font/ffz-fontello.eot
index d7f8d3a1..00c0f8ae 100644
Binary files a/res/font/ffz-fontello.eot and b/res/font/ffz-fontello.eot differ
diff --git a/res/font/ffz-fontello.svg b/res/font/ffz-fontello.svg
index 61b5077b..bc27f344 100644
--- a/res/font/ffz-fontello.svg
+++ b/res/font/ffz-fontello.svg
@@ -108,6 +108,10 @@
+
+
+
+
diff --git a/res/font/ffz-fontello.ttf b/res/font/ffz-fontello.ttf
index 41678442..7f775d92 100644
Binary files a/res/font/ffz-fontello.ttf and b/res/font/ffz-fontello.ttf differ
diff --git a/res/font/ffz-fontello.woff b/res/font/ffz-fontello.woff
index c2849308..b331d14f 100644
Binary files a/res/font/ffz-fontello.woff and b/res/font/ffz-fontello.woff differ
diff --git a/res/font/ffz-fontello.woff2 b/res/font/ffz-fontello.woff2
index 8481f2dd..bc9adb42 100644
Binary files a/res/font/ffz-fontello.woff2 and b/res/font/ffz-fontello.woff2 differ
diff --git a/src/main.js b/src/main.js
index 3114d80c..71d92b2e 100644
--- a/src/main.js
+++ b/src/main.js
@@ -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: 4, revision: 2,
+ major: 4, minor: 5, revision: 0,
commit: __git_commit__,
build: __webpack_hash__,
toString: () =>
diff --git a/src/modules/tooltips.js b/src/modules/tooltips.js
index 9cd32b20..b2aba3fd 100644
--- a/src/modules/tooltips.js
+++ b/src/modules/tooltips.js
@@ -31,6 +31,30 @@ export default class TooltipProvider extends Module {
]
}
+ this.types.child = target => {
+ const child = target.querySelector(':scope > .ffz-tooltip-child');
+ if ( ! child )
+ return null;
+
+ target._ffz_child = child;
+ child.remove();
+ child.classList.remove('ffz-tooltip-child');
+ return child;
+ };
+
+ this.types.child.onHide = target => {
+ const child = target._ffz_child;
+ if ( child ) {
+ target._ffz_child = null;
+ child.remove();
+
+ if ( ! target.querySelector(':scope > .ffz-tooltip-child') ) {
+ child.classList.add('ffz-tooltip-child');
+ target.appendChild(child);
+ }
+ }
+ }
+
this.types.text = target => sanitize(target.dataset.title);
this.types.html = target => target.dataset.title;
}
@@ -46,6 +70,10 @@ export default class TooltipProvider extends Module {
content: this.process.bind(this),
interactive: this.checkInteractive.bind(this),
hover_events: this.checkHoverEvents.bind(this),
+
+ onShow: this.delegateOnShow.bind(this),
+ onHide: this.delegateOnHide.bind(this),
+
popper: {
placement: 'top',
modifiers: {
@@ -74,6 +102,22 @@ export default class TooltipProvider extends Module {
this.tips.cleanup();
}
+ delegateOnShow(target, tip) {
+ const type = target.dataset.tooltipType,
+ handler = this.types[type];
+
+ if ( handler && handler.onShow )
+ handler.onShow(target, tip);
+ }
+
+ delegateOnHide(target, tip) {
+ const type = target.dataset.tooltipType,
+ handler = this.types[type];
+
+ if ( handler && handler.onHide )
+ handler.onHide(target, tip);
+ }
+
checkDelayShow(target, tip) {
const type = target.dataset.tooltipType,
handler = this.types[type];
diff --git a/src/sites/twitch-twilight/modules/css_tweaks/index.js b/src/sites/twitch-twilight/modules/css_tweaks/index.js
index 326be42e..ab68a870 100644
--- a/src/sites/twitch-twilight/modules/css_tweaks/index.js
+++ b/src/sites/twitch-twilight/modules/css_tweaks/index.js
@@ -32,6 +32,7 @@ const CLASSES = {
'dir-live-ind': '.live-channel-card:not([data-a-target*="host"]) .stream-type-indicator.stream-type-indicator--live,.stream-thumbnail__card .stream-type-indicator.stream-type-indicator--live,.preview-card .stream-type-indicator.stream-type-indicator--live,.preview-card .preview-card-stat.preview-card-stat--live',
'profile-hover': '.preview-card .tw-relative:hover .ffz-channel-avatar',
'not-live-bar': 'div[data-test-selector="non-live-video-banner-layout"]',
+ 'channel-live-ind': 'div[data-target="channel-header__live-indicator"]'
};
@@ -220,6 +221,20 @@ export default class CSSTweaks extends Module {
// Other?
+ this.settings.add('channel.hide-live-indicator', {
+ requires: ['context.route.name'],
+ process(ctx, val) {
+ return ctx.get('context.route.name') === 'user' ? val : false
+ },
+ default: false,
+ ui: {
+ path: 'Channel > Appearance >> General',
+ title: 'Hide the "LIVE" indicator on live channel pages.',
+ component: 'setting-check-box'
+ },
+ changed: val => this.toggleHide('channel-live-ind', val)
+ });
+
this.settings.add('channel.round-avatars', {
default: true,
ui: {
@@ -231,7 +246,7 @@ export default class CSSTweaks extends Module {
});
this.settings.add('channel.hide-not-live-bar', {
- default: true,
+ default: false,
ui: {
path: 'Channel > Appearance >> General',
title: 'Hide the "Not Live" bar.',
@@ -256,6 +271,7 @@ export default class CSSTweaks extends Module {
this.toggle('square-avatars', ! this.settings.get('channel.round-avatars'));
this.toggleHide('not-live-bar', this.settings.get('channel.hide-not-live-bar'));
+ this.toggleHide('channel-live-ind', this.settings.get('channel.hide-live-indicator'));
const recs = this.settings.get('layout.side-nav.show-rec-channels');
this.toggleHide('side-rec-channels', recs === 0);
diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/portrait.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/portrait.scss
index 10632bea..f593c119 100644
--- a/src/sites/twitch-twilight/modules/css_tweaks/styles/portrait.scss
+++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/portrait.scss
@@ -2,14 +2,22 @@
--ffz-player-width: calc(100vw - var(--ffz-portrait-extra-width));
--ffz-player-height: calc(calc(calc(var(--ffz-player-width) * 0.5625) + var(--ffz-portrait-extra-height)));
--ffz-theater-height: calc(calc(100vw * 0.5625) + var(--ffz-portrait-extra-height));
+ --ffz-chat-height: calc(100vh - var(--ffz-player-height));
+
+ & > .tw-flex-column {
+ .ffz--portrait-invert & {
+ top: var(--ffz-chat-height) !important;
+ }
- & > .tw-full-height {
height: var(--ffz-player-height) !important;
}
.persistent-player.persistent-player__border--mini {
pointer-events: none;
- bottom: calc(100vh - var(--ffz-player-height)) !important;
+
+ body:not(.ffz--portrait-invert) & {
+ bottom: var(--ffz-chat-height) !important;
+ }
> * {
pointer-events: auto;
@@ -17,10 +25,18 @@
}
.persistent-player.persistent-player--theatre {
- top: 0 !important;
+ .ffz--portrait-invert & {
+ top: unset !important;
+ bottom: 0 !important;
+ }
+
+ body:not(.ffz--portrait-invert) & {
+ top: 0 !important;
+ bottom: unset !important;
+ }
+
left: 0 !important;
right: 0 !important;
- bottom: unset !important;
height: var(--ffz-theater-height) !important;
width: 100% !important;
}
@@ -37,9 +53,28 @@
bottom: 0 !important;
left: 0 !important;
right: 0 !important;
- height: calc(100vh - var(--ffz-player-height)) !important;
+ height: var(--ffz-chat-height) !important;
width: unset !important;
- border-top: 1px solid #dad8de;
+
+ body:not(.ffz--portrait-invert) & {
+ top: unset !important;
+ bottom: 0 !important;
+ border-top: 1px solid #dad8de;
+
+ .tw-root--theme-dark & {
+ border-top-color: #2a2a2a;
+ }
+ }
+
+ .ffz--portrait-invert & {
+ top: 0 !important;
+ bottom: unset !important;
+ border-bottom: 1px solid #dad8de;
+
+ .tw-root--theme-dark & {
+ border-bottom-color: #2a2a2a;
+ }
+ }
& > .tw-full-height {
width: 100% !important;
@@ -47,18 +82,33 @@
.right-column__toggle-visibility {
position: fixed !important;
- top: 6.5rem;
+
+ body:not(.ffz--portrait-invert) & {
+ top: 6.5rem;
+ }
+
+ .ffz--portrait-invert & {
+ top: calc(var(--ffz-chat-height) + 6.5rem);
+ }
+
right: .5rem;
left: unset !important;
transform: rotate(90deg);
}
.emote-picker__tab-content {
- max-height: calc(calc(100vh - var(--ffz-player-height)) - 26rem);
+ max-height: calc(var(--ffz-chat-height) - 26rem);
}
&.right-column--theatre {
- top: unset !important;
+ .ffz--portrait-invert & {
+ bottom: unset !important;
+ }
+
+ body:not(.ffz--portrait-invert) & {
+ top: unset !important;
+ }
+
height: calc(100vh - var(--ffz-theater-height)) !important;
.emote-picker__tab-content {
@@ -66,10 +116,6 @@
}
}
- .tw-root--theme-dark & {
- border-top-color: #2a2a2a
- }
-
.video-chat {
flex-basis: unset;
}
diff --git a/src/sites/twitch-twilight/modules/layout.js b/src/sites/twitch-twilight/modules/layout.js
index 8eb0f1a7..250a8653 100644
--- a/src/sites/twitch-twilight/modules/layout.js
+++ b/src/sites/twitch-twilight/modules/layout.js
@@ -40,6 +40,16 @@ export default class Layout extends Module {
}
});
+ this.settings.add('layout.portrait-invert', {
+ default: false,
+ ui: {
+ path: 'Appearance > Layout >> Channel',
+ title: 'When in portrait mode, place chat at the top.',
+ component: 'setting-check-box'
+ },
+ changed: val => document.body.classList.toggle('ffz--portrait-invert', val)
+ });
+
this.settings.add('layout.portrait-threshold', {
default: 1.25,
ui: {
@@ -147,6 +157,8 @@ export default class Layout extends Module {
}
onEnable() {
+ document.body.classList.toggle('ffz--portrait-invert', this.settings.get('layout.portrait-invert'));
+
this.css_tweaks.toggle('portrait', this.settings.get('layout.inject-portrait'));
this.css_tweaks.toggle('portrait-swapped', this.settings.get('layout.use-portrait-swapped'));
this.css_tweaks.setVariable('portrait-extra-width', `${this.settings.get('layout.portrait-extra-width')}rem`);
diff --git a/src/std-components/aspect.vue b/src/std-components/aspect.vue
index ba1c61af..b8f46d99 100644
--- a/src/std-components/aspect.vue
+++ b/src/std-components/aspect.vue
@@ -4,7 +4,7 @@
class="tw-aspect"
>
diff --git a/src/std-components/icon-picker.vue b/src/std-components/icon-picker.vue
index 5290e632..c62e1d43 100644
--- a/src/std-components/icon-picker.vue
+++ b/src/std-components/icon-picker.vue
@@ -130,9 +130,13 @@ const FFZ_ICONS = [
'up-dir',
'up-big',
'play',
+ 'user',
+ 'clip',
'link-ext',
'twitter',
'github',
+ 'sort-down',
+ 'sort-up',
'gauge',
'download-cloud',
'upload-cloud',
@@ -140,6 +144,9 @@ const FFZ_ICONS = [
'keyboard',
'calendar-empty',
'ellipsis-vert',
+ 'sort-alt-up',
+ 'sort-alt-down',
+ 'language',
'twitch',
'bell-off',
'trash',
diff --git a/src/std-components/react-link.vue b/src/std-components/react-link.vue
index d37b54c3..b83b67aa 100644
--- a/src/std-components/react-link.vue
+++ b/src/std-components/react-link.vue
@@ -13,7 +13,7 @@ export default {
onClick(event) {
this.$emit('click', event);
- if ( ! event.defaultPrevented )
+ if ( ! event.defaultPrevented && ! this.href.includes('//') )
this.reactNavigate(this.href, event);
}
}
diff --git a/src/utilities/compat/fine-router.js b/src/utilities/compat/fine-router.js
index 8e94ee43..8317be3d 100644
--- a/src/utilities/compat/fine-router.js
+++ b/src/utilities/compat/fine-router.js
@@ -73,11 +73,26 @@ export default class FineRouter extends Module {
this.emit(':route', null, null);
}
- getURL(route, data, opts) {
+ getURL(route, data, opts, ...args) {
const r = this.routes[route];
if ( ! r )
throw new Error(`unable to find route "${route}"`);
+ if ( typeof data !== 'object' ) {
+ const parts = [data, opts, ...args];
+ data = {};
+
+ let i = 0;
+ for(const part of r.parts) {
+ if ( part && part.name ) {
+ data[part.name] = parts[i];
+ i++;
+ if ( i >= parts.length )
+ break;
+ }
+ }
+ }
+
return r.url(data, opts);
}
diff --git a/src/utilities/time.js b/src/utilities/time.js
index cac7ca41..65df2367 100644
--- a/src/utilities/time.js
+++ b/src/utilities/time.js
@@ -22,9 +22,11 @@ export function duration_to_string(elapsed, separate_days, days_only, no_hours,
days = days > 0 ? `${days} days, ` : '';
}
+ const show_hours = (!no_hours || days || hours);
+
return `${days}${
- (!no_hours || days || hours) ? `${days && hours < 10 ? '0' : ''}${hours}:` : ''
- }${minutes < 10 ? '0' : ''}${minutes}${
+ show_hours ? `${days && hours < 10 ? '0' : ''}${hours}:` : ''
+ }${show_hours && minutes < 10 ? '0' : ''}${minutes}${
no_seconds ? '' : `:${seconds < 10 ? '0' : ''}${seconds}`}`;
}
diff --git a/src/utilities/twitch-data.js b/src/utilities/twitch-data.js
index 212f5976..1c82d8ae 100644
--- a/src/utilities/twitch-data.js
+++ b/src/utilities/twitch-data.js
@@ -378,8 +378,11 @@ export default class TwitchData extends Module {
if ( this.tag_cache.has(id) )
out = this.tag_cache.get(id);
- if ( ! out || (want_description && ! out.description) )
- this.getTag(id, want_description).then(tag => callback(id, tag)).catch(err => callback(id, null, err));
+ if ( (want_description && (! out || ! out.description)) || (! out && callback) ) {
+ const promise = this.getTag(id, want_description);
+ if ( callback )
+ promise.then(tag => callback(id, tag)).catch(err => callback(id, null, err));
+ }
return out;
}
diff --git a/src/utilities/vue.js b/src/utilities/vue.js
index 44aa5bb3..897c67a1 100644
--- a/src/utilities/vue.js
+++ b/src/utilities/vue.js
@@ -170,6 +170,10 @@ export class Vue extends Module {
router.history.push(url);
}
},
+ getReactURL(route, data, opts, ...args) {
+ const router = t.resolve('site.router');
+ return router.getURL(route, data, opts, ...args);
+ },
t(key, phrase, options) {
return this.$i18n.t_(key, phrase, options);
},
diff --git a/styles/icons.scss b/styles/icons.scss
index cce8b72d..0b56e7bf 100644
--- a/styles/icons.scss
+++ b/styles/icons.scss
@@ -125,6 +125,8 @@
.ffz-i-up-dir:before { content: '\e830'; } /* '' */
.ffz-i-up-big:before { content: '\e831'; } /* '' */
.ffz-i-play:before { content: '\e832'; } /* '' */
+.ffz-i-user:before { content: '\e833'; } /* '' */
+.ffz-i-clip:before { content: '\e834'; } /* '' */
.ffz-i-link-ext:before { content: '\f08e'; } /* '' */
.ffz-i-twitter:before { content: '\f099'; } /* '' */
.ffz-i-github:before { content: '\f09b'; } /* '' */
diff --git a/styles/tooltips.scss b/styles/tooltips.scss
index 9bc1b1c8..bdefdb50 100644
--- a/styles/tooltips.scss
+++ b/styles/tooltips.scss
@@ -8,6 +8,18 @@ body {
}
+.ffz-tooltip.ffz-tooltip--no-mouse {
+ > * {
+ pointer-events: none;
+ }
+}
+
+
+.ffz-tooltip-child {
+ display: none !important;
+}
+
+
.tw-balloon {
&[x-placement^="bottom"] > .tw-balloon__tail {
bottom: 100%;