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:
parent
d1cd145b8a
commit
80282914c4
20 changed files with 448 additions and 122 deletions
|
@ -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: () =>
|
||||
|
|
142
src/modules/main_menu/components/backup-restore.vue
Normal file
142
src/modules/main_menu/components/backup-restore.vue
Normal 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>
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
// ========================================================================
|
||||
|
|
|
@ -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'});
|
||||
}
|
||||
|
||||
|
|
|
@ -212,6 +212,7 @@ Twilight.ROUTES = {
|
|||
'prime': '/prime',
|
||||
'turbo': '/turbo',
|
||||
'user': '/:userName',
|
||||
'squad': '/:userName/squad',
|
||||
'embed-chat': '/embed/:userName/chat'
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -188,7 +188,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.emote-picker__tab > *,
|
||||
.emote-picker-tab-item button > *,
|
||||
.emote-picker__emote-link > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue