1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-28 15:27:43 +00:00
* Added: `Current Channel` rule for profiles, to match all pages associated with a certain channel without needing many page rules.
* Fixed: Unreadable text in light theme when importing a profile.
* Changed: Display a matching page URL in the `Current Page` rule for profiles.
* Changed: Do not display an inactive profile warning on the Add-Ons settings page, since those are not affected by profiles.
* Changed: Update Vue to a more recent version.
* Maintenance: Update the chat types enum based on the latest version of Twitch.
* API Added: `TwitchData` module (`site.twitch_data`) for querying Twitch's API for data.
This commit is contained in:
SirStendec 2019-06-14 21:24:48 -04:00
parent c34b7e30e2
commit 275248ca36
24 changed files with 819 additions and 88 deletions

150
package-lock.json generated
View file

@ -4736,9 +4736,9 @@
}
},
"he": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
"integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0="
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
},
"hmac-drbg": {
"version": "1.0.1",
@ -6853,9 +6853,9 @@
"dev": true
},
"path-parse": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz",
"integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=",
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
},
"path-to-regexp": {
@ -7568,9 +7568,9 @@
"dev": true
},
"prettier": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.11.1.tgz",
"integrity": "sha512-T/KD65Ot0PB97xTrG8afQ46x3oiVhnfGjGESSI9NWYcG92+OUPZKkwHqGWXH2t9jK1crnQjubECW0FuOth+hxw==",
"version": "1.18.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz",
"integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==",
"dev": true
},
"private": {
@ -8136,12 +8136,12 @@
"dev": true
},
"resolve": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz",
"integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==",
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.0.tgz",
"integrity": "sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw==",
"dev": true,
"requires": {
"path-parse": "^1.0.5"
"path-parse": "^1.0.6"
}
},
"resolve-cwd": {
@ -9759,9 +9759,9 @@
}
},
"vue": {
"version": "2.5.16",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.5.16.tgz",
"integrity": "sha512-/ffmsiVuPC8PsWcFkZngdpas19ABm5mh2wA7iDqcltyCTwlgZjHGeJYOXkBMo422iPwIcviOtrTCUpSfXmToLQ=="
"version": "2.6.10",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.10.tgz",
"integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ=="
},
"vue-clickaway": {
"version": "2.2.2",
@ -9796,15 +9796,15 @@
}
},
"vue-hot-reload-api": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.0.tgz",
"integrity": "sha512-2j/t+wIbyVMP5NvctQoSUvLkYKoWAAk2QlQiilrM2a6/ulzFgdcLUJfTvs4XQ/3eZhHiBmmEojbjmM4AzZj8JA==",
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.3.tgz",
"integrity": "sha512-KmvZVtmM26BQOMK1rwUZsrqxEGeKiYSZGA7SNWE6uExx8UX/cj9hq2MRV/wWC3Cq6AoeDGk57rL9YMFRel/q+g==",
"dev": true
},
"vue-loader": {
"version": "13.7.1",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-13.7.1.tgz",
"integrity": "sha512-v6PbKMGl/hWHGPxB2uGHsA66vusrXF66J/h1QiFXtU6z5zVSK8jq5xl95M1p3QNXmuEJKNP3nxoXfbgQNs7hJg==",
"version": "13.7.3",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-13.7.3.tgz",
"integrity": "sha512-ACCwbfeC6HjY2pnDii+Zer+MZ6sdOtwvLmDXRK/BoD3WNR551V22R6KEagwHoTRJ0ZlIhpCBkptpCU6+Ri/05w==",
"dev": true,
"requires": {
"consolidate": "^0.14.0",
@ -9831,10 +9831,16 @@
"color-convert": "^1.9.0"
}
},
"big.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
"dev": true
},
"chalk": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz",
"integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==",
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.1",
@ -9848,26 +9854,41 @@
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
},
"loader-utils": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz",
"integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=",
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"dev": true,
"requires": {
"big.js": "^3.1.3",
"emojis-list": "^2.0.0",
"json5": "^0.5.0"
"minimist": "^1.2.0"
}
},
"postcss": {
"version": "6.0.21",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.21.tgz",
"integrity": "sha512-y/bKfbQz2Nn/QBC08bwvYUxEFOVGfPIUOTsJ2CK5inzlXW9SdYR1x4pEsG9blRAF/PX+wRNdOah+gx/hv4q7dw==",
"loader-utils": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz",
"integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==",
"dev": true,
"requires": {
"chalk": "^2.3.2",
"big.js": "^5.2.2",
"emojis-list": "^2.0.0",
"json5": "^1.0.1"
}
},
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
},
"postcss": {
"version": "6.0.23",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz",
"integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==",
"dev": true,
"requires": {
"chalk": "^2.4.1",
"source-map": "^0.6.1",
"supports-color": "^5.3.0"
"supports-color": "^5.4.0"
}
},
"source-map": {
@ -9877,9 +9898,9 @@
"dev": true
},
"supports-color": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz",
"integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg==",
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
@ -9902,32 +9923,53 @@
"loader-utils": "^1.0.2"
},
"dependencies": {
"loader-utils": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz",
"integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=",
"big.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
"dev": true
},
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"dev": true,
"requires": {
"big.js": "^3.1.3",
"emojis-list": "^2.0.0",
"json5": "^0.5.0"
"minimist": "^1.2.0"
}
},
"loader-utils": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz",
"integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^2.0.0",
"json5": "^1.0.1"
}
},
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
}
}
},
"vue-template-compiler": {
"version": "2.5.16",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.5.16.tgz",
"integrity": "sha512-ZbuhCcF/hTYmldoUOVcu2fcbeSAZnfzwDskGduOrnjBiIWHgELAd+R8nAtX80aZkceWDKGQ6N9/0/EUpt+l22A==",
"version": "2.6.10",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.10.tgz",
"integrity": "sha512-jVZkw4/I/HT5ZMvRnhv78okGusqe0+qH2A0Em0Cp8aq78+NK9TII263CDVz2QXZsIT+yyV/gZc/j/vlwa+Epyg==",
"requires": {
"de-indent": "^1.0.2",
"he": "^1.1.0"
}
},
"vue-template-es2015-compiler": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.6.0.tgz",
"integrity": "sha512-x3LV3wdmmERhVCYy3quqA57NJW7F3i6faas++pJQWtknWT+n7k30F4TVdHvCLn48peTJFRvCpxs3UuFPqgeELg==",
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz",
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
"dev": true
},
"vuedraggable": {

View file

@ -42,7 +42,7 @@
"style-loader": "^0.18.2",
"to-string-loader": "^1.1.5",
"uglifyjs-webpack-plugin": "^1.2.5",
"vue-loader": "^13.7.1",
"vue-loader": "^13.7.3",
"webpack": "^3.11.0",
"webpack-dev-middleware": "^1.12.2",
"webpack-dev-server": "^2.11.2",
@ -71,11 +71,11 @@
"react": "^16.4.1",
"safe-regex": "^1.1.0",
"sortablejs": "^1.9.0",
"vue": "^2.5.16",
"vue": "^2.6.10",
"vue-clickaway": "^2.2.2",
"vue-color": "^2.4.6",
"vue-observe-visibility": "^0.4.4",
"vue-template-compiler": "^2.5.16",
"vue-template-compiler": "^2.6.10",
"vuedraggable": "^2.16.0"
}
}

View file

@ -34,7 +34,7 @@ export default class AddonManager extends Module {
async onEnable() {
this.settings.addUI('add-ons', {
path: 'Add-Ons @{"description": "Add-Ons are additional modules, often written by other people, that can be loaded automatically by FrankerFaceZ to add new capabilities and behaviors to the extension and Twitch."}',
path: 'Add-Ons @{"description": "Add-Ons are additional modules, often written by other people, that can be loaded automatically by FrankerFaceZ to add new capabilities and behaviors to the extension and Twitch.", "profile_warning": false}',
component: 'addon-list',
title: 'Add-Ons',
no_filter: true,

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

View file

@ -77,6 +77,7 @@
:href="url"
target="_blank"
rel="noopener noreferrer"
class="tw-link tw-c-text-overlay"
>
{{ url }}
</a>

View file

@ -69,7 +69,7 @@
class="tw-block tw-full-width tw-mg-y-05 tw-mg-r-1 tw-pd-05 tw-button tw-button--hollow tw-tooltip-wrapper"
@click="importProfile(profile)"
>
<span class="tw-button__text">
<span class="tw-button__text tw-c-text-overlay">
{{ profile.i18n_key ? t(profile.i18n_key, profile.name) : profile.name }}
</span>
<div v-if="profile.description" class="tw-tooltip tw-tooltip--down tw-tooltip--align-left">
@ -98,7 +98,7 @@
class="tw-block tw-full-width tw-mg-y-05 tw-mg-r-1 tw-pd-05 tw-button tw-button--hollow"
@click="confirmImport(true)"
>
<span class="tw-button__text ffz-i-ok">
<span class="tw-button__text tw-c-text-overlay ffz-i-ok">
{{ t('setting.backup-restore.enable-auto', 'Yes, allow automatic updates.') }}
</span>
</button>
@ -107,7 +107,7 @@
class="tw-block tw-full-width tw-mg-y-05 tw-mg-r-1 tw-pd-05 tw-button tw-button--hollow"
@click="confirmImport(false)"
>
<span class="tw-button__text ffz-i-cancel">
<span class="tw-button__text tw-c-text-overlay ffz-i-cancel">
{{ t('setting.backup-restore.disable-auto', 'No, prevent automatic updates.') }}
</span>
</button>

View file

@ -528,7 +528,7 @@ export default class MainMenu extends Module {
move: idx => context.manager.moveProfile(profile.id, idx),
save: () => profile.save(),
update: data => {
profile.data = data
profile.data = deep_copy(data)
profile.save()
},

View file

@ -0,0 +1,137 @@
<template>
<section class="tw-flex-grow-1 tw-align-self-start">
<div class="tw-flex tw-align-items-center">
<label :for="'channel$' + id">
{{ t(type.i18n, type.title) }}
</label>
<div class="ffz--search-avatar tw-mg-x-05">
<figure class="tw-avatar tw-avatar--size-30">
<div class="tw-border-radius-rounded tw-overflow-hidden">
<img
v-if="current"
:alt="current.displayName"
:src="current.profileImageURL"
class="tw-avatar__img tw-image"
>
</div>
</figure>
</div>
<autocomplete
v-slot="slot"
:input-id="'channel$' + id"
:items="fetchUsers"
:value="search"
:suggest-on-focus="true"
:escape-to-clear="false"
class="tw-flex-grow-1"
@selected="onSelected"
>
<div class="tw-pd-x-1 tw-pd-y-05">
<div class="tw-card tw-relative">
<div class="tw-align-items-center tw-flex tw-flex-nowrap tw-flex-row">
<div class="tw-card-img tw-card-img--size-3 tw-flex-shrink-0 tw-overflow-hidden">
<aspect :ratio="1">
<img
:alt="slot.item.displayName"
:src="slot.item.profileImageURL"
class="tw-image"
>
</aspect>
</div>
<div class="tw-card-body tw-overflow-hidden tw-relative">
<p class="tw-pd-x-1">{{ slot.item.displayName }}</p>
</div>
</div>
</div>
</div>
</autocomplete>
</div>
</section>
</template>
<script>
import {debounce, deep_copy} from 'utilities/object';
let last_id = 0;
export default {
props: ['value', 'type', 'filters', 'context'],
data() {
return {
id: last_id++,
current: null,
loaded_id: null
}
},
computed: {
search() {
return this.current && this.current.displayName || this.value.data.login;
}
},
watch: {
value: {
handler() {
this.cacheUser();
},
deep: true
}
},
created() {
const ffz = FrankerFaceZ.get();
this.loader = ffz.resolve('site.twitch_data');
this.cacheUser = debounce(this.cacheUser, 50);
},
beforeDestroy() {
this.cacheUser = null;
},
mounted() {
this.cacheUser();
},
methods: {
async cacheUser() {
if ( ! this.loader || this.loaded_id === this.value.data.id )
return;
this.current = null;
this.loaded_id = this.value.data.id;
if ( ! this.loaded_id )
return;
const data = await this.loader.getUser(this.loaded_id);
if ( data )
this.current = deep_copy(data);
else
this.current = null;
},
async fetchUsers(query) {
if ( ! this.loader )
return [];
const data = await this.loader.getMatchingUsers(query);
if ( ! data || ! data.items )
return [];
return deep_copy(data.items);
},
onSelected(item) {
this.current = item;
this.value.data.login = item && item.login || null;
this.value.data.id = item && item.id || null;
}
}
}
</script>

View file

@ -8,7 +8,7 @@
<select
:id="'page$' + id"
v-model="value.data.route"
class="tw-flex-grow-1 tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-select"
class="tw-flex-grow-1 tw-mg-l-1 tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-select"
>
<option
v-for="(route, key) in routes"
@ -21,10 +21,18 @@
</select>
</div>
<div
v-if="parts && parts.length"
class="tw-border-t tw-mg-t-05"
>
<div class="tw-border-t tw-mg-t-05">
<div class="tw-pd-y-05">
<t-list
phrase="setting.filter.page.url"
default="URL: {url}"
>
<template #url>
<span class="tw-c-text-alt">{{ url }}</span>
</template>
</t-list>
</div>
<div
v-for="part in parts"
:key="part.key"
@ -37,7 +45,7 @@
<input
:id="'page$' + id + '$part-' + part.key"
v-model="value.data.values[part.key]"
class="tw-flex-grow-1 tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-input"
class="tw-mg-l-1 tw-flex-grow-1 tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-input"
>
</div>
</div>
@ -66,6 +74,25 @@ export default {
return this.routes[this.value.data.route];
},
url() {
if ( ! this.route )
return null;
const parts = {};
for(const part of this.parts) {
const value = this.value.data.values[part.key];
parts[part.key] = value || `<${part.key}${part.optional ? '*' : ''}>`;
}
try {
return decodeURI(new URL(this.route.url(parts), location));
} catch(err) {
console.error(err);
return null;
}
},
parts() {
const out = [];
if ( ! this.route || ! this.route.parts )
@ -78,7 +105,8 @@ export default {
out.push({
key: part.name,
i18n: `settings.filter.page.route.${this.route.name}.${part.name}`,
title: name[0].toLocaleUpperCase() + name.substr(1)
title: name[0].toLocaleUpperCase() + name.substr(1),
optional: part.optional
});
}
}

View file

@ -109,8 +109,9 @@ export const Page = {
let i = 1;
for(const part of route.parts) {
if ( typeof part === 'object' ) {
if ( config.values[part.name] != null )
parts.push([i, config.values[part.name]]);
const val = config.values[part.name];
if ( val && val.length )
parts.push([i, val]);
i++;
}
@ -141,4 +142,22 @@ export const Page = {
values: {}
}),
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/page.vue')
};
export const Channel = {
createTest(config = {}) {
const login = config.login,
id = config.id;
return ctx => ctx.channelID === id || (ctx.channelID == null && ctx.channelLogin === login);
},
title: 'Current Channel',
i18n: 'settings.filter.channel',
default: () => ({
login: null,
id: null
}),
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/channel.vue')
};

View file

@ -79,6 +79,10 @@ export default class SettingsManager extends Module {
// Before we do anything else, make sure the provider is ready.
await this.provider.awaitReady();
// When the router updates we additional routes, make sure to
// trigger a rebuild of profile context and re-select profiles.
this.on('site.router:updated-routes', this.updateRoutes, this);
// Load profiles, but don't run any events because we haven't done
// migrations yet.
this.loadProfiles(true);
@ -198,6 +202,17 @@ export default class SettingsManager extends Module {
// Profile Management
// ========================================================================
updateRoutes() {
// Clear the existing matchers.
for(const profile of this.__profiles)
profile.matcher = null;
// And then re-select the active profiles.
for(const context of this.__contexts)
context.selectProfiles();
}
/**
* Get an existing {@link SettingsProfile} instance.
* @param {number} id - The id of the profile.

View file

@ -10,6 +10,7 @@ import WebMunch from 'utilities/compat/webmunch';
import Fine from 'utilities/compat/fine';
import FineRouter from 'utilities/compat/fine-router';
import Apollo from 'utilities/compat/apollo';
import TwitchData from 'utilities/twitch-data';
import Switchboard from './switchboard';
@ -31,6 +32,7 @@ export default class Twilight extends BaseSite {
this.inject(Fine);
this.inject('router', FineRouter);
this.inject(Apollo, false);
this.inject(TwitchData);
this.inject(Switchboard);
}

View file

@ -66,7 +66,26 @@ export default class Channel extends Module {
this.wrapRaidController(inst);
});
this.ChannelPage.on('mount', inst => {
this.settings.updateContext({
channel: get('state.channel.login', inst),
channelID: get('state.channel.id', inst)
});
});
this.ChannelPage.on('unmount', () => {
this.settings.updateContext({
channel: null,
channelID: null
});
});
this.ChannelPage.on('update', inst => {
this.settings.updateContext({
channel: get('state.channel.login', inst),
channelID: get('state.channel.id', inst)
});
if ( this.settings.get('channel.hosting.enable') || has(inst.state, 'hostMode') || has(inst.state, 'hostedChannel') )
return;

View file

@ -110,7 +110,8 @@ const CHAT_TYPES = make_enum(
'AnonSubMysteryGift',
'FirstCheerMessage',
'BitsBadgeTierMessage',
'InlinePrivateCallout'
'InlinePrivateCallout',
'ChannelPointsReward'
);
@ -518,7 +519,7 @@ export default class ChatHook extends Module {
this.updateMentionCSS();
this.ChatController.on('mount', this.chatMounted, this);
this.ChatController.on('unmount', this.chatUmounted, this);
this.ChatController.on('unmount', this.chatUnmounted, this);
this.ChatController.on('receive-props', this.chatUpdated, this);
this.ChatService.ready((cls, instances) => {
@ -1565,6 +1566,12 @@ export default class ChatHook extends Module {
chatHidden: props.isHidden
});
if ( props.isEmbedded || props.isPopout )
this.settings.updateContext({
channel: props.channelLogin && props.channelLogin.toLowerCase(),
channelID: props.channelID
});
this.chat.context.updateContext({
moderator: props.isCurrentUserModerator,
channel: props.channelLogin && props.channelLogin.toLowerCase(),
@ -1576,10 +1583,22 @@ export default class ChatHook extends Module {
}
chatUmounted(chat) {
chatUnmounted(chat) {
if ( chat.chatBuffer && chat.chatBuffer.ffzController === this )
chat.chatBuffer.ffzController = null;
if ( chat.props.isEmbedded || chat.props.isPopout )
this.settings.updateContext({
channel: null,
channelID: null
});
this.chat.context.updateContext({
moderator: false,
channel: null,
channelID: null
});
this.removeRoom(chat);
}
@ -1600,6 +1619,12 @@ export default class ChatHook extends Module {
// TODO: Check if this is the room for the current channel.
if ( props.isEmbedded || props.isPopout )
this.settings.updateContext({
channel: props.channelLogin && props.channelLogin.toLowerCase(),
channelID: props.channelID
});
this.settings.updateContext({
moderator: props.isCurrentUserModerator,
chatHidden: props.isHidden

View file

@ -0,0 +1,50 @@
'use strict';
// ============================================================================
// Dashboard
// ============================================================================
import Module from 'utilities/module';
import { get } from 'utilities/object';
export default class Dashboard extends Module {
constructor(...args) {
super(...args);
this.should_enable = true;
this.inject('settings');
this.inject('site.fine');
this.Dashboard = this.fine.define(
'dashboard',
n => n.cards && n.defaultCards && n.saveCardsConfig,
['dash']
);
}
onEnable() {
this.Dashboard.on('mount', this.onUpdate, this);
this.Dashboard.on('update', this.onUpdate, this);
this.Dashboard.on('unmount', this.onUnmount, this);
this.Dashboard.ready((cls, instances) => {
for(const inst of instances)
this.onUpdate(inst);
});
}
onUpdate(inst) {
this.settings.updateContext({
channel: get('props.channelLogin', inst),
channelID: get('props.channelID', inst)
});
}
onUnmount() {
this.settings.updateContext({
channel: null,
channelID: null
});
}
}

View file

@ -379,12 +379,12 @@ export default {
this.open = false;
} else {
this.search = item.label || item.name || item.value;
this.search = item.displayName || item.label || item.name || item.value;
this.open = false;
}
this.$emit('input', this.search);
this.$emit('selected', objectHas(item, 'value') ? item.value : objectHas(item, 'name') ? item.name : (item.label || item.displayName));
this.$emit('selected', item);
}
}
}

View file

@ -40,12 +40,7 @@ export default class FineRouter extends Module {
}
navigate(route, data, opts) {
const r = this.routes[route];
if ( ! r )
throw new Error(`unable to find route "${route}"`);
const url = r.url(data, opts);
this.history.push(url);
this.history.push(this.getURL(route, data, opts));
}
_navigateTo(location) {
@ -55,6 +50,11 @@ export default class FineRouter extends Module {
return;
this.location = path;
this._pickRoute();
}
_pickRoute() {
const path = this.location;
for(const route of this.__routes) {
const match = route.regex.exec(path);
@ -73,6 +73,14 @@ export default class FineRouter extends Module {
this.emit(':route', null, null);
}
getURL(route, data, opts) {
const r = this.routes[route];
if ( ! r )
throw new Error(`unable to find route "${route}"`);
return r.url(data, opts);
}
getRoute(name) {
return this.routes[name];
}
@ -92,26 +100,35 @@ export default class FineRouter extends Module {
return this.route_names[route];
}
routeName(route, name) {
routeName(route, name, process = true) {
if ( typeof route === 'object' ) {
for(const key in route)
if ( has(route, key) )
this.routeName(key, route[key]);
this.routeName(key, route[key], false);
if ( process )
this.emit(':updated-route-names');
return;
}
this.route_names[route] = name;
if ( process )
this.emit(':updated-route-names');
}
route(name, path, sort = true) {
route(name, path, process = true) {
if ( typeof name === 'object' ) {
for(const key in name)
if ( has(name, key) )
this.route(key, name[key], false);
if ( sort )
if ( process ) {
this.__routes.sort((a,b) => b.score - a.score);
if ( this.location )
this._pickRoute();
this.emit(':updated-routes');
}
return;
}
@ -130,7 +147,11 @@ export default class FineRouter extends Module {
}
this.__routes.push(route);
if ( sort )
if ( process ) {
this.__routes.sort((a,b) => b.score - a.score);
if ( this.location )
this._pickRoute();
this.emit(':updated-routes');
}
}
}

View file

@ -0,0 +1,16 @@
query FFZ_SearchCategory($query: String!, $first: Int, $after: String) {
searchFor(userQuery: $query, platform: "web", target: {index: GAME, cursor: $after, limit: $first}) {
games {
cursor
pageInfo {
hasNextPage
}
items {
id
name
displayName
boxArtURL(width: 40, height: 56)
}
}
}
}

View file

@ -0,0 +1,16 @@
query FFZ_SearchUser($query: String!, $first: Int, $after: String) {
searchFor(userQuery: $query, platform: "web", target: {index: USER, cursor: $after, limit: $first}) {
users {
cursor
pageInfo {
hasNextPage
}
items {
id
login
displayName
profileImageURL(width: 50)
}
}
}
}

View file

@ -0,0 +1,9 @@
query FFZ_FetchTags($ids: [ID!]) {
contentTags(ids: $ids) {
id
isLanguageTag
tagName
localizedName
localizedDescription
}
}

View file

@ -0,0 +1,9 @@
query FFZ_TopTags($limit: Int) {
topTags(limit: $limit) {
id
isLanguageTag
tagName
localizedName
localizedDescription
}
}

View file

@ -0,0 +1,11 @@
query FFZ_FetchUser($id: ID, $login: String) {
user(id: $id, login: $login) {
id
login
displayName
profileImageURL(width: 50)
roles {
isPartner
}
}
}

View file

@ -0,0 +1,306 @@
'use strict';
// ============================================================================
// Twitch Data
// Get data, from Twitch.
// ============================================================================
import Module from 'utilities/module';
import {get, debounce, generateUUID} from 'utilities/object';
const LANGUAGE_MATCHER = /^auto___lang_(\w+)$/;
export default class TwitchData extends Module {
constructor(...args) {
super(...args);
this.inject('site');
this.inject('site.apollo');
this.inject('site.web_munch');
this.tag_cache = new Map;
this._waiting_tags = new Map;
this._loadTags = debounce(this._loadTags.bind(this), 50);
}
queryApollo(query, variables, options) {
let thing;
if ( ! variables && ! options && query.query )
thing = query;
else {
thing = {
query,
variables
};
if ( options )
thing = Object.assign(thing, options);
}
return this.apollo.client.query(thing);
}
get languageCode() {
const session = this.site.getSession();
return session && session.languageCode || 'en'
}
get locale() {
const session = this.site.getSession();
return session && session.locale || 'en-US'
}
get searchClient() {
if ( this._search )
return this._search;
const apollo = this.apollo.client,
core = this.listeners.getCore(),
search_module = this.web_munch.getModule('algolia-search'),
SearchClient = search_module && search_module.a;
if ( ! SearchClient || ! apollo || ! core )
return null;
this._search = new SearchClient({
appId: core.config.algoliaApplicationID,
apiKey: core.config.algoliaAPIKey,
apolloClient: apollo,
logger: core.logger,
config: core.config,
stats: core.stats
});
return this._search;
}
// ========================================================================
// Categories
// ========================================================================
async getMatchingCategories(query) {
const data = await this.queryApollo(
require('./data/search-category.gql'),
{ query }
);
return {
cursor: get('data.searchFor.games.cursor', data),
items: get('data.searchFor.games.items', data) || [],
finished: ! get('data.searchFor.games.pageInfo.hasNextPage', data)
};
}
// ========================================================================
// Users
// ========================================================================
async getMatchingUsers(query) {
const data = await this.queryApollo(
require('./data/search-user.gql'),
{ query }
);
return {
cursor: get('data.searchFor.users.cursor', data),
items: get('data.searchFor.users.items', data) || [],
finished: ! get('data.searchFor.users.pageInfo.hasNextPage', data)
};
}
async getUser(id, login) {
const data = await this.queryApollo(
require('./data/user-fetch.gql'),
{ id, login }
);
return get('data.user', data);
}
// ========================================================================
// Tags
// ========================================================================
async _loadTags() {
if ( this._loading_tags )
return;
this._loading_tags = true;
const processing = this._waiting_tags;
this._waiting_tags = new Map;
try {
const data = await this.queryApollo(
require('./data/tags-fetch.gql'),
{
ids: [...processing.keys()]
}
);
const nodes = get('data.contentTags', data);
if ( Array.isArray(nodes) )
for(const node of nodes) {
const tag = {
id: node.id,
value: node.id,
is_language: node.isLanguageTag,
name: node.tagName,
label: node.localizedName,
description: node.localizedDescription
};
this.tag_cache.set(tag.id, tag);
const promises = processing.get(tag.id);
if ( promises )
for(const pair of promises)
pair[0](tag);
promises.delete(tag.id);
}
for(const promises of processing.values())
for(const pair of promises)
pair[0](null);
} catch(err) {
for(const promises of processing.values())
for(const pair of promises)
pair[1](err);
}
this._loading_tags = false;
if ( this._waiting_tags.size )
this._loadTags();
}
getTag(id, want_description = false) {
if ( this.tag_cache.has(id) ) {
const out = this.tag_cache.get(id);
if ( out && (out.description || ! want_description) )
return Promise.resolve(out);
}
return new Promise((s, f) => {
if ( this._waiting_tags.has(id) )
this._waiting_tags.get(id).push([s, f]);
else {
this._waiting_tags.set(id, [[s, f]]);
if ( ! this._loading_tags )
this._loadTags();
}
});
}
getTagImmediate(id, callback, want_description = false) {
let out = null;
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));
return out;
}
async getTopTags(limit = 50) {
const data = await this.queryApollo(
require('./data/tags-top.gql'),
{limit}
);
const nodes = get('data.topTags', data);
if ( ! Array.isArray(nodes) )
return [];
const out = [], seen = new Set;
for(const node of nodes) {
if ( ! node || seen.has(node.id) )
continue;
seen.add(node.id);
const tag = {
id: node.id,
value: node.id,
is_language: node.isLanguageTag,
name: node.tagName,
label: node.localizedName,
description: node.localizedDescription
};
this.tag_cache.set(tag.id, tag);
out.push(tag);
}
return out;
}
getLanguagesFromTags(tags, callback) {
const out = [],
fn = callback ? debounce(() => {
this.getLanguagesFromTags(tags, callback);
}, 16) : null
if ( Array.isArray(tags) )
for(const tag_id of tags) {
const tag = this.getTagImmediate(tag_id, fn);
if ( tag && tag.is_language ) {
const match = LANGUAGE_MATCHER.exec(tag.name);
if ( match )
out.push(match[1]);
}
}
return out;
}
async getMatchingTags(query, locale) {
if ( ! locale )
locale = this.locale;
const data = await this.searchClient.queryForType(
'tag', query, generateUUID(), {
hitsPerPage: 100,
facetFilters: [
],
restrictSearchableAttributes: [
`localizations.${locale}`,
'tag_name'
]
}
);
const nodes = get('streamTags.hits', data);
if ( ! Array.isArray(nodes) )
return [];
const out = [], seen = new Set;
for(const node of nodes) {
if ( ! node || seen.has(node.tag_id) )
continue;
seen.add(node.tag_id);
if ( ! this.tag_cache.has(node.tag_id) ) {
const tag = {
id: node.tag_id,
value: node.tag_id,
is_language: node.tag_name && LANGUAGE_MATCHER.test(node.tag_name),
label: node.localizations && (node.localizations[locale] || node.localizations['en-us']) || node.tag_name
};
this.tag_cache.set(tag.id);
out.push(tag);
} else {
out.push(this.tag_cache.get(node.tag_id));
}
}
return out;
}
}

View file

@ -149,6 +149,11 @@ textarea.tw-input {
.ffz--filter-editor {
label {
width: auto;
min-width: unset;
}
.ffz--rule {
outline: none;