mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-28 05:15:54 +00:00
4.4.1
* 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:
parent
c34b7e30e2
commit
275248ca36
24 changed files with 819 additions and 88 deletions
150
package-lock.json
generated
150
package-lock.json
generated
|
@ -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": {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: () =>
|
||||
|
|
|
@ -77,6 +77,7 @@
|
|||
:href="url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="tw-link tw-c-text-overlay"
|
||||
>
|
||||
{{ url }}
|
||||
</a>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
|
||||
|
|
137
src/settings/components/channel.vue
Normal file
137
src/settings/components/channel.vue
Normal 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>
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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++;
|
||||
}
|
||||
|
@ -142,3 +143,21 @@ export const Page = {
|
|||
}),
|
||||
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')
|
||||
};
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
50
src/sites/twitch-twilight/modules/dashboard.js
Normal file
50
src/sites/twitch-twilight/modules/dashboard.js
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
16
src/utilities/data/search-category.gql
Normal file
16
src/utilities/data/search-category.gql
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
16
src/utilities/data/search-user.gql
Normal file
16
src/utilities/data/search-user.gql
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
src/utilities/data/tags-fetch.gql
Normal file
9
src/utilities/data/tags-fetch.gql
Normal file
|
@ -0,0 +1,9 @@
|
|||
query FFZ_FetchTags($ids: [ID!]) {
|
||||
contentTags(ids: $ids) {
|
||||
id
|
||||
isLanguageTag
|
||||
tagName
|
||||
localizedName
|
||||
localizedDescription
|
||||
}
|
||||
}
|
9
src/utilities/data/tags-top.gql
Normal file
9
src/utilities/data/tags-top.gql
Normal file
|
@ -0,0 +1,9 @@
|
|||
query FFZ_TopTags($limit: Int) {
|
||||
topTags(limit: $limit) {
|
||||
id
|
||||
isLanguageTag
|
||||
tagName
|
||||
localizedName
|
||||
localizedDescription
|
||||
}
|
||||
}
|
11
src/utilities/data/user-fetch.gql
Normal file
11
src/utilities/data/user-fetch.gql
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
306
src/utilities/twitch-data.js
Normal file
306
src/utilities/twitch-data.js
Normal 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;
|
||||
}
|
||||
}
|
|
@ -149,6 +149,11 @@ textarea.tw-input {
|
|||
|
||||
|
||||
.ffz--filter-editor {
|
||||
label {
|
||||
width: auto;
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
.ffz--rule {
|
||||
outline: none;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue