1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-08-02 16:08:31 +00:00

4.0.0-rc19.1

* Changed: Remember which sections of the settings menu a user has expanded and restore that state when re-opening the menu.
* Changed: Begin tracking which settings a user has seen, so that a future update can begin highlighting newly added settings.
* Changed: Clean up the new localization code a bit more.
This commit is contained in:
SirStendec 2019-05-03 22:36:26 -04:00
parent 35316b8827
commit 3e9e1b2ede
21 changed files with 242 additions and 38 deletions

View file

@ -205,7 +205,7 @@ export class TranslationManager extends Module {
if ( locale === 'en' )
return {};
if ( locale === 'de' )
/*if ( locale === 'de' )
return {
site: {
menu_button: 'FrankerFaceZ Leitstelle'
@ -272,8 +272,8 @@ export class TranslationManager extends Module {
_: 'Erweiterung'
},
'inherited-from': 'Vererbt von: %{title}',
'overridden-by': 'Überschrieben von: %{title}'
'inherited-from': 'Vererbt von: {title}',
'overridden-by': 'Überschrieben von: {title}'
},
'main-menu': {
@ -306,7 +306,7 @@ export class TranslationManager extends Module {
'main-menu': {
search: '検索設定',
version: 'バージョン%{version}',
version: 'バージョン{version}',
about: {
_: '約',
@ -314,7 +314,7 @@ export class TranslationManager extends Module {
support: '対応'
}
}
}
}*/
const resp = await fetch(`${SERVER}/script/i18n/${locale}.json`);
if ( ! resp.ok ) {

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: '-rc19',
major: 4, minor: 0, revision: 0, extra: '-rc19.1',
commit: __git_commit__,
build: __webpack_hash__,
toString: () =>

View file

@ -39,6 +39,16 @@ export default class Actions extends Module {
type: 'array_merge',
always_inherit: true,
// Clean up after Vue being stupid.
process(ctx, val) {
if ( Array.isArray(val) )
for(const entry of val)
if ( entry.i18n && typeof entry.i18n !== 'string' )
delete entry.i18n;
return val;
},
ui: {
path: 'Chat > Actions > Reasons >> Custom Reasons',
component: 'chat-reasons',

View file

@ -21,7 +21,7 @@ export const open_url = {
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/edit-url.vue'),
title: 'Open URL',
description: '%{options.url}',
description: '{options.url}',
tooltip(data) {
const url = this.replaceVariables(data.options.url, data);
@ -67,7 +67,7 @@ export const chat = {
},
title: 'Chat Command',
description: '%{options.command}',
description: '{options.command}',
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/edit-chat.vue'),
@ -183,7 +183,7 @@ export const timeout = {
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/edit-timeout.vue'),
title: 'Timeout User',
description: '{options.duration,number} second%{options.duration,en_plural}',
description: '{options.duration,number} second{options.duration,en_plural}',
reason_text(data) {
return this.i18n.t('chat.actions.timeout-reason',

View file

@ -175,7 +175,7 @@ export const Videos = {
});
else if ( game )
desc_1 = this.i18n.t('clip.desc.1.playing', '{user} playing %{game}', {
desc_1 = this.i18n.t('clip.desc.1.playing', '{user} playing {game}', {
user,
game: game_display
});

View file

@ -65,6 +65,8 @@
:modal="nav"
:filter="filter"
@change-item="changeItem"
@mark-seen="markSeen"
@mark-expanded="markExpanded"
@navigate="navigate"
/>
</simplebar>
@ -97,6 +99,7 @@
:item="currentItem"
:filter="filter"
@change-item="changeItem"
@mark-seen="markSeen"
@navigate="navigate"
/>
</simplebar>
@ -171,6 +174,8 @@ export default {
return;
}
this.markSeen(item);
this.currentItem = item;
let current = item;
while(current = current.parent) // eslint-disable-line no-cond-assign

View file

@ -46,6 +46,7 @@
>
<a href="#" @click="$emit('change-item', i, false)">
{{ t(i.i18n_key, i.title, i) }}
<span v-if="i.unseen" class="tw-pill">{{ i.unseen }}</span>
</a>
</li>
</ul>
@ -62,6 +63,7 @@
:item="i"
:filter="filter"
@change-item="changeItem"
@mark-seen="markSeen"
@navigate="navigate"
/>
</div>
@ -94,6 +96,10 @@ export default {
return item.search_terms.includes(this.filter);
},
markSeen(item) {
this.$emit('mark-seen', item);
},
changeItem(item) {
this.$emit('change-item', item);
},

View file

@ -38,6 +38,9 @@
<span v-if="item.pill" class="tw-pill">
{{ item.pill_i18n_key ? t(item.pill_i18n_key, item.pill, item) : item.pill }}
</span>
<span v-else-if="item.unseen" class="tw-pill">
{{ item.unseen }}
</span>
</div>
<menu-tree
v-if="item.items && item.expanded"
@ -46,6 +49,8 @@
:modal="item.items"
:filter="filter"
@change-item="i => $emit('change-item', i)"
@mark-seen="i => $emit('mark-seen', i)"
@mark-expanded="i => $emit('mark-expanded', i)"
/>
</li>
</ul>
@ -75,11 +80,16 @@ function findNextVisible(node, modal) {
}
function recursiveExpand(node) {
function recursiveExpand(node, vue) {
if ( ! node.expanded ) {
node.expanded = true;
if ( vue )
vue.$emit('mark-expanded', node);
}
if ( node.items )
for(const item of node.items)
recursiveExpand(item);
recursiveExpand(item, vue);
}
@ -121,12 +131,13 @@ export default {
else if ( this.currentItem === item )
item.expanded = false;
this.$emit('mark-expanded', item);
this.$emit('change-item', item);
},
expandAll() {
for(const item of this.modal)
recursiveExpand(item);
recursiveExpand(item, this);
},
prevItem() {
@ -164,9 +175,10 @@ export default {
const i = this.currentItem;
if ( i.expanded && i.items )
if ( i.expanded && i.items ) {
i.expanded = false;
else if ( i.parent )
this.$emit('mark-expanded', i);
} else if ( i.parent )
this.$emit('change-item', i.parent);
},
@ -176,11 +188,13 @@ export default {
const i = this.currentItem;
if ( i.expanded && i.items )
this.$emit('change-item', i.items[0]);
else
else {
i.expanded = true;
this.$emit('mark-expanded', i);
}
if ( event.ctrlKey )
recursiveExpand(this.currentItem);
recursiveExpand(this.currentItem, this);
}
}
}

View file

@ -15,6 +15,7 @@
<label :for="item.full_key" class="tw-checkbox__label">
{{ t(item.i18n_key, item.title, item) }}
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
</label>
<button

View file

@ -6,6 +6,7 @@
<div class="tw-flex tw-align-items-start">
<label :for="item.full_key" class="tw-mg-y-05">
{{ t(item.i18n_key, item.title, item) }}
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
</label>
<div class="tw-flex tw-flex-column tw-mg-05">

View file

@ -2,6 +2,7 @@
<div class="tw-input">
<header>
{{ t(item.i18n_key, item.title, item) }}
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
</header>
<section
v-if="item.description"

View file

@ -6,6 +6,7 @@
<div class="tw-flex tw-align-items-center">
<label :for="item.full_key">
{{ t(item.i18n_key, item.title, item) }}
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
</label>
<select

View file

@ -6,6 +6,7 @@
<div class="tw-flex tw-align-items-center">
<label :for="item.full_key">
{{ t(item.i18n_key, item.title, item) }}
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
</label>
<input

View file

@ -41,6 +41,7 @@ export default class MainMenu extends Module {
this.dialog = new Dialog(() => this.buildDialog());
this.has_update = false;
this.opened = false;
this.settings.addUI('profiles', {
path: 'Data Management @{"sort": 1000, "profile_warning": false} > Profiles @{"profile_warning": false}',
@ -102,6 +103,8 @@ export default class MainMenu extends Module {
if ( this._vue )
this._vue.$children[0].context.has_update = true;
});
this.scheduleUpdate();
}
openPopout() {
@ -130,7 +133,16 @@ export default class MainMenu extends Module {
async onEnable() {
await this.site.awaitElement(Dialog.EXCLUSIVE);
this.dialog.on('hide', this.destroyDialog, this);
this.dialog.on('show', () => {
this.opened = true;
this.updateButtonUnseen();
this.emit('show')
});
this.dialog.on('hide', () => {
this.emit('hide');
this.destroyDialog();
});
this.dialog.on('resize', () => {
if ( this._vue )
this._vue.$children[0].maximized = this.dialog.maximized
@ -146,6 +158,20 @@ export default class MainMenu extends Module {
}
getUnseen() {
const pages = this.getSettingsTree();
if ( ! Array.isArray(pages) )
return 0;
let i=0;
for(const page of pages)
if ( page )
i += (page.unseen || 0);
return i;
}
buildDialog() {
if ( this._menu )
return this._menu;
@ -174,10 +200,19 @@ export default class MainMenu extends Module {
}
updateButtonUnseen() {
const mb = this.resolve('site.menu_button');
if ( mb )
mb.new_settings = this.opened ? 0 : this.getUnseen();
}
updateLiveMenu() {
clearTimeout(this._update_timer);
this._update_timer = null;
this.updateButtonUnseen();
if ( ! this._vue || ! this._vue.$children || ! this._vue.$children[0] )
return;
@ -222,7 +257,6 @@ export default class MainMenu extends Module {
return;
const tree = this._settings_tree,
expanded = this.settings.provider.get('settings-expanded', {}),
tokens = def.ui.path_tokens,
len = tokens.length;
@ -241,16 +275,13 @@ export default class MainMenu extends Module {
full_key: key,
sort: 0,
parent: prefix,
expanded: prefix === null,
expanded: prefix == null,
i18n_key: `setting.${key}`,
desc_i18n_key: `setting.${key}.description`
};
Object.assign(token, raw_token);
if ( has(expanded, key) )
token.expanded = expanded[key];
prefix = key;
}
@ -268,6 +299,8 @@ export default class MainMenu extends Module {
this.rebuildSettingsTree();
const tree = this._settings_tree,
settings_seen = this.settings.provider.get('cfg-seen', []),
collapsed = this.settings.provider.get('cfg-collapsed'),
root = {},
copies = {},
@ -291,6 +324,9 @@ export default class MainMenu extends Module {
token.parent = p_key ? parent : null;
token.page = token.page || parent.page;
if ( collapsed )
token.expanded = ! collapsed.includes(token.full_key);
if ( token.page && ! token.component )
needs_component.add(token);
@ -337,6 +373,18 @@ export default class MainMenu extends Module {
tok.search_terms = terms.map(format_term).join('\n');
if ( ! settings_seen.includes(setting_key)) {
// Mark existing settings as unseen for now.
// Let users run this for a while to build up their cache.
settings_seen.push(setting_key);
/*let i = tok;
while(i) {
i.unseen = (i.unseen||0) + 1;
i = i.parent;
}*/
}
list.push(tok);
}
@ -401,9 +449,24 @@ export default class MainMenu extends Module {
return a.key && a.key.localeCompare(b.key);
});
this.log.info(`Built Tree in ${(performance.now() - started).toFixed(5)}ms with ${Object.keys(tree).length} structure nodes and ${this._settings_count} settings nodes.`);
const items = root.items || [];
items.keys = copies;
// Save for now, since we just want to mark everything as seen.
this.settings.provider.set('cfg-seen', settings_seen);
if ( ! collapsed ) {
const new_collapsed = [];
for(const key of Object.keys(copies)) {
const item = copies[key];
if ( item && item.items && item.parent )
new_collapsed.push(key);
}
this.settings.provider.set('cfg-collapsed', new_collapsed);
}
this.log.info(`Built Tree in ${(performance.now() - started).toFixed(5)}ms with ${Object.keys(tree).length} structure nodes and ${this._settings_count} settings nodes.`);
return items;
}
@ -560,9 +623,40 @@ export default class MainMenu extends Module {
return _c;
}
markSeen(item, seen) {
let had_seen = true;
if ( ! seen ) {
had_seen = false;
seen = this.settings.provider.get('cfg-seen', []);
}
if ( Array.isArray(item.contents) ) {
for(const child of item.contents)
child && this.markSeen(child, seen);
} else if ( ! item.setting )
return;
if ( ! seen.includes(item.setting) ) {
seen.push(item.setting);
let i = item.parent;
while(i) {
i.unseen = (i.unseen || 1) - 1;
i = i.parent;
}
}
if ( ! had_seen )
this.settings.provider.set('cfg-seen', seen);
}
getData() {
const settings = this.getSettingsTree(),
context = this.getContext();
context = this.getContext(),
current = this.has_update ? settings.keys['home.changelog'] : settings.keys['home'];
this.markSeen(current);
return {
context,
@ -571,14 +665,28 @@ export default class MainMenu extends Module {
faded: false,
nav: settings,
currentItem: this.has_update ?
settings.keys['home.changelog'] :
settings.keys['home'], // settings[0],
currentItem: current,
nav_keys: settings.keys,
maximized: this.dialog.maximized,
exclusive: this.dialog.exclusive,
markSeen: item => this.markSeen(item),
markExpanded: item => {
const collapsed = this.settings.provider.get('cfg-collapsed', []),
included = collapsed.indexOf(item.full_key);
if ( item.expanded && included !== -1 )
collapsed.splice(included, 1);
else if ( ! item.expanded && included === -1 )
collapsed.push(item.full_key);
else
return;
this.settings.provider.set('cfg-collapsed', collapsed);
},
resize: e => ! this.dialog.exclusive && this.dialog.toggleSize(e),
close: e => ! this.dialog.exclusive && this.dialog.toggleVisible(e),

View file

@ -9,6 +9,7 @@ export default {
has_value: false,
profile: null,
_unseen: false,
source: null,
source_value: undefined,
}
@ -23,6 +24,11 @@ export default {
this._update_profile();
this._uses_changed(ctx.uses(setting));
if ( this.item.unseen ) {
this._unseen = true;
this.item.unseen = 0;
}
ctx.on(`uses_changed:${setting}`, this._uses_changed, this);
},
@ -63,6 +69,10 @@ export default {
return data;
},
unseen() {
return this._unseen || this.item.unseen > 0
},
default_value() {
if ( typeof this.item.default === 'function' )
return this.item.default(this.context.context);

View file

@ -50,7 +50,7 @@
</span>
</button>
</header>
<section class="tw-border-t tw-full-height tw-full-width tw-flex tw-flex-nowrap tw-overflow-hidden">
<section class="tw-border-t tw-full-height tw-full-width tw-flex tw-overflow-hidden">
<div v-for="(key, idx) of phrases" :key="idx" class="tw-block tw-mg-1">
{{ key }}
</div>

View file

@ -48,7 +48,7 @@
</span>
<span
v-if="userAge"
:data-title="t('viewer-card.age-tip', 'Member Since: %{age,datetime}', {age: userAge})"
:data-title="t('viewer-card.age-tip', 'Member Since: {age,datetime}', {age: userAge})"
class="ffz-tooltip ffz-i-clock"
>
{{ t('viewer-card.age', '{age,humantime}', {age: userAge}) }}

View file

@ -37,7 +37,7 @@
v-if="user.following"
:disabled="user.loading"
:class="{'tw-button--disabled': user.loading}"
:data-title="user.loading ? null : t('featured-follow.button.unfollow', 'Unfollow %{user}', {user: user.displayName})"
:data-title="user.loading ? null : t('featured-follow.button.unfollow', 'Unfollow {user}', {user: user.displayName})"
data-tooltip-type="html"
class="tw-button tw-button--status tw-button--success ffz-tooltip ffz--featured-button-unfollow"
@click="clickWithTip($event, unfollowUser, user.id)"

View file

@ -19,6 +19,7 @@ export default class MenuButton extends SiteModule {
this._pill_content = null;
this._has_update = false;
this._important_update = false;
this._new_settings = 0;
this.NavBar = this.fine.define(
'nav-bar',
@ -26,6 +27,22 @@ export default class MenuButton extends SiteModule {
);
}
get new_settings() {
return this._new_settings;
}
set new_settings(val) {
if ( val === this._new_settings )
return;
this._new_settings = val;
this.update();
}
get has_new() {
return this._new_settings > 0
}
get important_update() {
return this._important_update;
}
@ -115,6 +132,11 @@ export default class MenuButton extends SiteModule {
{this.i18n.t('site.menu_button.dev', 'dev')}
</div>
</div>)}
{this.has_new && ! pill && (<div class="ffz-menu__pill tw-absolute">
<div class="tw-pill">
{this.i18n.formatNumber(this.new_settings)}
</div>
</div>)}
{pill && (<div class="ffz-menu__pill tw-absolute">
<div class="tw-animation tw-animation--animate tw-animation--duration-medium tw-animation--timing-ease-in tw-animation--bounce-in">
<div class="tw-pill tw-pill--notification">
@ -127,6 +149,9 @@ export default class MenuButton extends SiteModule {
{this.has_update && (<div class="tw-mg-t-1">
{this.i18n.t('site.menu_button.update-desc', 'There is an update available. Please refresh your page.')}
</div>)}
{this.has_new && (<div class="tw-mg-t-1">
{this.i18n.t('site.menu_button.new-desc', 'There {count,plural,one {is one new setting} other {are # new settings}}.', {count: this._new_settings})}
</div>)}
{DEBUG && (<div class="tw-mg-t-1">
{this.i18n.t('site.menu_button.dev-desc', 'You are running a developer build of FrankerFaceZ.')}
</div>)}

View file

@ -27,9 +27,10 @@
:class="{'active': selected === idx, 'ffz-unmatched-item': showing && ! shouldShow(i)}"
role="tab"
class="tab tw-pd-y-05 tw-pd-x-1"
@click="selected = idx"
@click="select(idx)"
>
{{ t(i.i18n_key, i.title, i) }}
<span v-if="i.unseen > 0" class="tw-pill">{{ i.unseen }}</span>
</div>
</header>
<section
@ -81,6 +82,10 @@ export default {
}
},
mounted() {
this.markSeen()
},
methods: {
focus() {
this.$el.querySelector('header').focus();
@ -106,22 +111,37 @@ export default {
this.lastTab();
},
markSeen() {
this.$emit('mark-seen', this.item.tabs[this.selected]);
},
firstTab() {
this.selected = 0;
this.markSeen();
},
lastTab() {
this.selected = this.item.tabs.length - 1;
this.markSeen();
},
prevTab() {
if ( this.selected > 0 )
if ( this.selected > 0 ) {
this.selected--;
this.markSeen();
}
},
select(idx) {
this.selected = idx;
this.markSeen();
},
nextTab() {
if ( this.selected + 1 < this.item.tabs.length )
if ( this.selected + 1 < this.item.tabs.length ) {
this.selected++;
this.markSeen();
}
},
shouldShow(item) {

View file

@ -8,6 +8,7 @@ import dayjs from 'dayjs';
import Parser from '@ffz/icu-msgparser';
import {get} from 'utilities/object';
import {duration_to_string} from 'utilities/time';
// ============================================================================
@ -58,11 +59,11 @@ export const DEFAULT_TYPES = {
return this.formatDateTime(val, node.f);
},
duration(val, node) {
duration(val) {
return duration_to_string(val);
},
localestring(val, node) {
localestring(val) {
return this.toLocaleString(val);
},