1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-08-07 06:40:54 +00:00

4.0.0-rc15

* Added: Data Management > Backup and Restore
* Added: Option to expand merged mass sub gift messages by default.
* Added: Option to hide the Discover link in the top navigation bar.
* Changed: Use icons for navigation of the emote menu. Fix padding as well.
* Fixed: Player problems on Squad Streams pages.
* Fixed: Option to hide Live indicators on channels in the directory.
This commit is contained in:
SirStendec 2019-03-14 21:43:44 -04:00
parent d1cd145b8a
commit 80282914c4
20 changed files with 448 additions and 122 deletions

View file

@ -149,7 +149,7 @@ ${typeof x[1] === 'string' ? x[1] : JSON.stringify(x[1], null, 4)}`
FrankerFaceZ.Logger = Logger;
const VER = FrankerFaceZ.version_info = {
major: 4, minor: 0, revision: 0, extra: '-rc14.2',
major: 4, minor: 0, revision: 0, extra: '-rc15',
commit: __git_commit__,
build: __webpack_hash__,
toString: () =>

View file

@ -0,0 +1,142 @@
<template lang="html">
<div class="ffz--backup-restore tw-pd-t-05">
<div class="tw-pd-b-1 tw-mg-b-1 tw-border-b">
{{ t('setting.backup-restore.about', 'This tool allows you to backup and restore your FrankerFaceZ settings, including all settings from the Control Center along with other data such as favorited emotes and blocked games.') }}
</div>
<div class="tw-flex tw-align-items-center tw-justify-content-center tw-mg-b-1">
<button
class="tw-button tw-mg-x-1"
@click="backup"
>
<span class="tw-button__icon tw-button__icon--left">
<figure class="ffz-i-download" />
</span>
<span class="tw-button__text">
{{ t('setting.backup-restore.save-backup', 'Save Backup') }}
</span>
</button>
<button
class="tw-button tw-mg-x-1"
@click="restore"
>
<span class="tw-button__icon tw-button__icon--left">
<figure class="ffz-i-upload" />
</span>
<span class="tw-button__text">
{{ t('setting.backup-restore.restore-backup', 'Restore Backup') }}
</span>
</button>
</div>
<div v-if="error" class="tw-c-background-accent-alt-2 tw-c-text-overlay tw-pd-1 tw-mg-b-1">
<h3 class="ffz-i-attention">
{{ t('setting.backup-restore.error', 'There was an error processing this backup.') }}
</h3>
<div v-if="error_desc">
{{ error_desc }}
</div>
</div>
<div v-if="message" class="tw-c-background-accent-alt-2 tw-c-text-overlay tw-pd-1 tw-mg-b-1">
{{ message }}
</div>
</div>
</template>
<script>
import {openFile, readFile} from 'utilities/dom';
import { saveAs } from 'file-saver';
export default {
props: ['item'],
data() {
return {
error_desc: null,
error: false,
message: null
}
},
methods: {
async backup() {
this.error = false;
this.message = null;
let blob;
try {
const settings = this.item.getFFZ().resolve('settings'),
data = await settings.getFullBackup();
blob = new Blob([JSON.stringify(data)], {type: 'application/json;charset=utf-8'});
} catch(err) {
this.error_desc = this.t('setting.backup-restore.dump-error', 'Unable to export settings data to JSON.');
this.error = true;
return;
}
try {
saveAs(blob, 'ffz-settings.json');
} catch(err) {
this.error_desc = this.t('setting.backup-restore.save-error', 'Unable to save.');
}
},
async restore() {
this.error = false;
this.message = null;
let contents;
try {
contents = await readFile(await openFile('application/json'));
} catch(err) {
this.error_desc = this.t('setting.backup-restore.read-error', 'Unable to read file.');
this.error = true;
return;
}
let data;
try {
data = JSON.parse(contents);
} catch(err) {
this.error_desc = this.t('setting.backup-restore.json-error', 'Unable to parse file as JSON.');
this.error = true;
return;
}
if ( ! data || data.version !== 2 ) {
this.error_desc = this.t('setting.backup-restore.old-file', 'This file is invalid or was created in another version of FrankerFaceZ and cannot be loaded.');
this.error = true;
return;
}
if ( data.type !== 'full' ) {
this.error_desc = this.t('setting.backup-restore.non-full', 'This file is not a full backup and cannot be restored with this tool.');
this.error = true;
return;
}
const settings = this.item.getFFZ().resolve('settings'),
provider = settings.provider;
await provider.awaitReady();
provider.clear();
let i = 0;
for(const key of Object.keys(data.values)) {
const val = data.values[key];
provider.set(key, val);
provider.emit('changed', key, val, false);
i++;
}
this.message = this.t('setting.backup-restore.restored', '%{count} items have been restored. Please refresh this page.', {
count: i
});
}
}
}
</script>

View file

@ -47,6 +47,12 @@ export default class MainMenu extends Module {
component: 'profile-manager'
});
this.settings.addUI('backup', {
path: 'Data Management > Backup and Restore @{"profile_warning": false}',
component: 'backup-restore',
getFFZ: () => this.resolve('core')
});
this.settings.addUI('home', {
path: 'Home @{"sort": -1000, "profile_warning": false}',
component: 'home-page'

View file

@ -95,6 +95,28 @@ export default class SettingsManager extends Module {
}
// ========================================================================
// Backup and Restore
// ========================================================================
async getFullBackup() {
// Before we do anything else, make sure the provider is ready.
await this.provider.awaitReady();
const out = {
version: 2,
type: 'full',
values: {}
};
for(const [k, v] of this.provider.entries())
out.values[k] = v;
return out;
}
// ========================================================================
// Provider Interaction
// ========================================================================

View file

@ -191,10 +191,14 @@ export class LocalStorageProvider extends SettingsProvider {
}
clear() {
for(const key of this._cached.keys())
localStorage.removeItem(this.prefix + key);
const old_cache = this._cached;
this._cached = new Map;
for(const key of old_cache.keys()) {
localStorage.removeItem(this.prefix + key);
this.emit('changed', key, undefined, true);
}
this._cached.clear();
this.broadcast({type: 'clear'});
}

View file

@ -212,6 +212,7 @@ Twilight.ROUTES = {
'prime': '/prime',
'turbo': '/turbo',
'user': '/:userName',
'squad': '/:userName/squad',
'embed-chat': '/embed/:userName/chat'
}

View file

@ -938,7 +938,7 @@ export default class EmoteMenu extends Module {
clickTab(event) {
this.setState({
tab: event.target.dataset.tab
tab: event.currentTarget.dataset.tab
});
}
@ -1706,49 +1706,75 @@ export default class EmoteMenu extends Module {
/>}
</div>
</div>)}
<div class="emote-picker__tabs-container tw-flex tw-border-t tw-c-background-base">
<div
class={`ffz-tooltip emote-picker__tab tw-pd-x-1${tab === 'fav' ? ' emote-picker__tab--active' : ''}`}
id="emote-picker__fav"
data-tab="fav"
data-tooltip-type="html"
data-title={t.i18n.t('emote-menu.favorites', 'Favorites')}
onClick={this.clickTab}
>
<figure class="ffz-i-star" />
<div class="emote-picker__tab-nav-container tw-flex tw-border-t tw-c-background-alt">
<div class={`emote-picker-tab-item${tab === 'fav' ? ' emote-picker-tab-item--active' : ''} tw-relative`}>
<button
class="ffz-tooltip tw-block tw-full-width tw-interactable tw-interactable--inverted tw-interactive"
id="emote-picker__fav"
data-tab="fav"
data-tooltip-type="html"
data-title={t.i18n.t('emote-menu.favorites', 'Favorites')}
onClick={this.clickTab}
>
<div class="tw-inline-flex tw-pd-x-1 tw-pd-y-05 tw-font-size-4">
<figure class="ffz-i-star" />
</div>
</button>
</div>
{this.state.has_channel_tab && <div
class={`emote-picker__tab tw-pd-x-1${tab === 'channel' ? ' emote-picker__tab--active' : ''}`}
id="emote-picker__channel"
data-tab="channel"
onClick={this.clickTab}
>
{t.i18n.t('emote-menu.channel', 'Channel')}
{this.state.has_channel_tab && <div class={`emote-picker-tab-item${tab === 'channel' ? ' emote-picker-tab-item--active' : ''} tw-relative`}>
<button
class="ffz-tooltip tw-block tw-full-width tw-interactable tw-interactable--inverted tw-interactive"
id="emote-picker__channel"
data-tab="channel"
data-tooltip-type="html"
data-title={t.i18n.t('emote-menu.channel', 'Channel')}
onClick={this.clickTab}
>
<div class="tw-inline-flex tw-pd-x-1 tw-pd-y-05 tw-font-size-4">
<figure class="ffz-i-camera" />
</div>
</button>
</div>}
<div
class={`emote-picker__tab tw-pd-x-1${tab === 'all' ? ' emote-picker__tab--active' : ''}`}
id="emote-picker__all"
data-tab="all"
onClick={this.clickTab}
>
{t.i18n.t('emote-menu.my-emotes', 'My Emotes')}
<div class={`emote-picker-tab-item${tab === 'all' ? ' emote-picker-tab-item--active' : ''} tw-relative`}>
<button
class="ffz-tooltip tw-block tw-full-width tw-interactable tw-interactable--inverted tw-interactive"
id="emote-picker__all"
data-tab="all"
data-tooltip-type="html"
data-title={t.i18n.t('emote-menu.my-emotes', 'My Emotes')}
onClick={this.clickTab}
>
<div class="tw-inline-flex tw-pd-x-1 tw-pd-y-05 tw-font-size-4">
<figure class="ffz-i-channels" />
</div>
</button>
</div>
{this.state.has_emoji_tab && <div
class={`emote-picker__tab tw-pd-x-1${tab === 'emoji' ? ' emote-picker__tab--active' : ''}`}
id="emote-picker__emoji"
data-tab="emoji"
onClick={this.clickTab}
>
{t.i18n.t('emote-menu.emoji', 'Emoji')}
{this.state.has_emoji_tab && <div class={`emote-picker-tab-item${tab === 'emoji' ? ' emote-picker-tab-item--active' : ''} tw-relative`}>
<button
class="ffz-tooltip tw-block tw-full-width tw-interactable tw-interactable--inverted tw-interactive"
id="emote-picker__emoji"
data-tab="emoji"
data-tooltip-type="html"
data-title={t.i18n.t('emote-menu.emoji', 'Emoji')}
onClick={this.clickTab}
>
<div class="tw-inline-flex tw-pd-x-1 tw-pd-y-05 tw-font-size-4">
<figure class="ffz-i-smile" />
</div>
</button>
</div>}
<div class="tw-flex-grow-1" />
{!loading && (<div
class="ffz-tooltip emote-picker__tab tw-pd-x-1 tw-mg-r-0"
data-tooltip-type="html"
data-title="Refresh Data"
onClick={this.clickRefresh}
>
<figure class="ffz-i-arrows-cw" />
{!loading && (<div class="emote-picker-tab-item tw-relative">
<button
class="ffz-tooltip tw-block tw-full-width tw-interactable tw-interactable--inverted tw-interactive"
data-tooltip-type="html"
data-title={t.i18n.t('emote-menu.refresh', 'Refresh Data')}
onClick={this.clickRefresh}
>
<div class="tw-inline-flex tw-pd-x-1 tw-pd-y-05 tw-font-size-4">
<figure class="ffz-i-arrows-cw" />
</div>
</button>
</div>)}
</div>
</div>

View file

@ -89,6 +89,7 @@ const CHAT_TYPES = make_enum(
'Resubscription',
'GiftPaidUpgrade',
'AnonGiftPaidUpgrade',
'PrimePaidUpgrade',
'SubGift',
'AnonSubGift',
'Clear',
@ -274,6 +275,15 @@ export default class ChatHook extends Module {
}
});
this.settings.add('chat.subs.merge-gifts-visibility', {
default: false,
ui: {
path: 'Chat > Appearance >> Subscriptions',
title: 'Expand merged mass sub gift messages by default.',
component: 'setting-check-box'
}
});
this.settings.add('chat.lines.alternate', {
default: false,
ui: {

View file

@ -416,8 +416,11 @@ export default class ChatLine extends Module {
});
}
const expanded = t.chat.context.get('chat.subs.merge-gifts-visibility') ?
! this.state.ffz_expanded : this.state.ffz_expanded;
let sub_list = null;
if( this.state.ffz_expanded && mystery && mystery.recipients && mystery.recipients.length > 0 ) {
if( expanded && mystery && mystery.recipients && mystery.recipients.length > 0 ) {
const the_list = [];
for(const x of mystery.recipients) {
if ( the_list.length )
@ -455,7 +458,7 @@ export default class ChatLine extends Module {
mystery ? e('div', {
className: 'tw-pd-l-05 tw-font-size-4'
}, e('figure', {
className: `ffz-i-${this.state.ffz_expanded ? 'down' : 'right'}-dir tw-pd-y-1`
className: `ffz-i-${expanded ? 'down' : 'right'}-dir tw-pd-y-1`
})) : null
]),
sub_list,

View file

@ -12,6 +12,7 @@ import {has} from 'utilities/object';
const PORTRAIT_ROUTES = ['user', 'video', 'user-video', 'user-clip', 'user-videos', 'user-clips', 'user-collections', 'user-events', 'user-followers', 'user-following']
const CLASSES = {
'top-discover': '.top-nav__nav-link[data-a-target="discover-link"]',
'side-nav': '.side-nav',
'side-rec-channels': '.side-nav .recommended-channels',
'side-rec-friends': '.side-nav .recommended-friends',
@ -30,7 +31,7 @@ const CLASSES = {
'pinned-cheer': '.pinned-cheer,.pinned-cheer-v2',
'whispers': '.whispers',
'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',
'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',
};
@ -242,7 +243,17 @@ export default class CSSTweaks extends Module {
component: 'setting-check-box'
},
changed: val => this.toggle('theatre-nav', val)
})
});
this.settings.add('layout.discover', {
default: true,
ui: {
path: 'Appearance > Layout >> Top Navigation',
title: 'Show Discover link.',
component: 'setting-check-box'
},
changed: val => this.toggleHide('top-discover', !val)
});
this.settings.add('layout.prime-offers', {
default: true,
@ -290,6 +301,7 @@ export default class CSSTweaks extends Module {
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'));
this.toggleHide('prime-offers', !this.settings.get('layout.prime-offers'));
this.toggleHide('top-discover', !this.settings.get('layout.discover'));
const recs = this.settings.get('layout.side-nav.show-rec-channels');
this.toggleHide('side-rec-channels', recs === 0);

View file

@ -188,7 +188,7 @@
}
}
.emote-picker__tab > *,
.emote-picker-tab-item button > *,
.emote-picker__emote-link > * {
pointer-events: none;
}

View file

@ -132,6 +132,33 @@ export function setChildren(el, children, no_sanitize, no_empty) {
}
export function openFile(contentType, multiple) {
return new Promise(resolve => {
const input = document.createElement('input');
input.type = 'file';
input.accept = contentType;
input.multiple = multiple;
input.onchange = () => {
const files = Array.from(input.files);
resolve(multiple ? files : files[0])
}
input.click();
})
}
export function readFile(file, encoding = 'utf-8') {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsText(file, encoding);
reader.onload = () => resolve(reader.result);
reader.onerror = e => reject(e);
});
}
const el = createElement('span');
export function sanitize(text) {