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": {
|
"he": {
|
||||||
"version": "1.1.1",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||||
"integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0="
|
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
|
||||||
},
|
},
|
||||||
"hmac-drbg": {
|
"hmac-drbg": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
|
@ -6853,9 +6853,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"path-parse": {
|
"path-parse": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
|
||||||
"integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=",
|
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"path-to-regexp": {
|
"path-to-regexp": {
|
||||||
|
@ -7568,9 +7568,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"version": "1.11.1",
|
"version": "1.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz",
|
||||||
"integrity": "sha512-T/KD65Ot0PB97xTrG8afQ46x3oiVhnfGjGESSI9NWYcG92+OUPZKkwHqGWXH2t9jK1crnQjubECW0FuOth+hxw==",
|
"integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"private": {
|
"private": {
|
||||||
|
@ -8136,12 +8136,12 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"resolve": {
|
"resolve": {
|
||||||
"version": "1.5.0",
|
"version": "1.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.0.tgz",
|
||||||
"integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==",
|
"integrity": "sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"path-parse": "^1.0.5"
|
"path-parse": "^1.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"resolve-cwd": {
|
"resolve-cwd": {
|
||||||
|
@ -9759,9 +9759,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vue": {
|
"vue": {
|
||||||
"version": "2.5.16",
|
"version": "2.6.10",
|
||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-2.5.16.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.10.tgz",
|
||||||
"integrity": "sha512-/ffmsiVuPC8PsWcFkZngdpas19ABm5mh2wA7iDqcltyCTwlgZjHGeJYOXkBMo422iPwIcviOtrTCUpSfXmToLQ=="
|
"integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ=="
|
||||||
},
|
},
|
||||||
"vue-clickaway": {
|
"vue-clickaway": {
|
||||||
"version": "2.2.2",
|
"version": "2.2.2",
|
||||||
|
@ -9796,15 +9796,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vue-hot-reload-api": {
|
"vue-hot-reload-api": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.3.tgz",
|
||||||
"integrity": "sha512-2j/t+wIbyVMP5NvctQoSUvLkYKoWAAk2QlQiilrM2a6/ulzFgdcLUJfTvs4XQ/3eZhHiBmmEojbjmM4AzZj8JA==",
|
"integrity": "sha512-KmvZVtmM26BQOMK1rwUZsrqxEGeKiYSZGA7SNWE6uExx8UX/cj9hq2MRV/wWC3Cq6AoeDGk57rL9YMFRel/q+g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"vue-loader": {
|
"vue-loader": {
|
||||||
"version": "13.7.1",
|
"version": "13.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-13.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-13.7.3.tgz",
|
||||||
"integrity": "sha512-v6PbKMGl/hWHGPxB2uGHsA66vusrXF66J/h1QiFXtU6z5zVSK8jq5xl95M1p3QNXmuEJKNP3nxoXfbgQNs7hJg==",
|
"integrity": "sha512-ACCwbfeC6HjY2pnDii+Zer+MZ6sdOtwvLmDXRK/BoD3WNR551V22R6KEagwHoTRJ0ZlIhpCBkptpCU6+Ri/05w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"consolidate": "^0.14.0",
|
"consolidate": "^0.14.0",
|
||||||
|
@ -9831,10 +9831,16 @@
|
||||||
"color-convert": "^1.9.0"
|
"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": {
|
"chalk": {
|
||||||
"version": "2.3.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||||
"integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==",
|
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-styles": "^3.2.1",
|
"ansi-styles": "^3.2.1",
|
||||||
|
@ -9848,26 +9854,41 @@
|
||||||
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
|
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"loader-utils": {
|
"json5": {
|
||||||
"version": "1.1.0",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||||
"integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=",
|
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"big.js": "^3.1.3",
|
"minimist": "^1.2.0"
|
||||||
"emojis-list": "^2.0.0",
|
|
||||||
"json5": "^0.5.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"postcss": {
|
"loader-utils": {
|
||||||
"version": "6.0.21",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.21.tgz",
|
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz",
|
||||||
"integrity": "sha512-y/bKfbQz2Nn/QBC08bwvYUxEFOVGfPIUOTsJ2CK5inzlXW9SdYR1x4pEsG9blRAF/PX+wRNdOah+gx/hv4q7dw==",
|
"integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"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",
|
"source-map": "^0.6.1",
|
||||||
"supports-color": "^5.3.0"
|
"supports-color": "^5.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"source-map": {
|
"source-map": {
|
||||||
|
@ -9877,9 +9898,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"supports-color": {
|
"supports-color": {
|
||||||
"version": "5.3.0",
|
"version": "5.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||||
"integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg==",
|
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"has-flag": "^3.0.0"
|
"has-flag": "^3.0.0"
|
||||||
|
@ -9902,32 +9923,53 @@
|
||||||
"loader-utils": "^1.0.2"
|
"loader-utils": "^1.0.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loader-utils": {
|
"big.js": {
|
||||||
"version": "1.1.0",
|
"version": "5.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
|
||||||
"integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=",
|
"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,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"big.js": "^3.1.3",
|
"minimist": "^1.2.0"
|
||||||
"emojis-list": "^2.0.0",
|
|
||||||
"json5": "^0.5.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": {
|
"vue-template-compiler": {
|
||||||
"version": "2.5.16",
|
"version": "2.6.10",
|
||||||
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.5.16.tgz",
|
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.10.tgz",
|
||||||
"integrity": "sha512-ZbuhCcF/hTYmldoUOVcu2fcbeSAZnfzwDskGduOrnjBiIWHgELAd+R8nAtX80aZkceWDKGQ6N9/0/EUpt+l22A==",
|
"integrity": "sha512-jVZkw4/I/HT5ZMvRnhv78okGusqe0+qH2A0Em0Cp8aq78+NK9TII263CDVz2QXZsIT+yyV/gZc/j/vlwa+Epyg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"de-indent": "^1.0.2",
|
"de-indent": "^1.0.2",
|
||||||
"he": "^1.1.0"
|
"he": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vue-template-es2015-compiler": {
|
"vue-template-es2015-compiler": {
|
||||||
"version": "1.6.0",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz",
|
||||||
"integrity": "sha512-x3LV3wdmmERhVCYy3quqA57NJW7F3i6faas++pJQWtknWT+n7k30F4TVdHvCLn48peTJFRvCpxs3UuFPqgeELg==",
|
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"vuedraggable": {
|
"vuedraggable": {
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
"style-loader": "^0.18.2",
|
"style-loader": "^0.18.2",
|
||||||
"to-string-loader": "^1.1.5",
|
"to-string-loader": "^1.1.5",
|
||||||
"uglifyjs-webpack-plugin": "^1.2.5",
|
"uglifyjs-webpack-plugin": "^1.2.5",
|
||||||
"vue-loader": "^13.7.1",
|
"vue-loader": "^13.7.3",
|
||||||
"webpack": "^3.11.0",
|
"webpack": "^3.11.0",
|
||||||
"webpack-dev-middleware": "^1.12.2",
|
"webpack-dev-middleware": "^1.12.2",
|
||||||
"webpack-dev-server": "^2.11.2",
|
"webpack-dev-server": "^2.11.2",
|
||||||
|
@ -71,11 +71,11 @@
|
||||||
"react": "^16.4.1",
|
"react": "^16.4.1",
|
||||||
"safe-regex": "^1.1.0",
|
"safe-regex": "^1.1.0",
|
||||||
"sortablejs": "^1.9.0",
|
"sortablejs": "^1.9.0",
|
||||||
"vue": "^2.5.16",
|
"vue": "^2.6.10",
|
||||||
"vue-clickaway": "^2.2.2",
|
"vue-clickaway": "^2.2.2",
|
||||||
"vue-color": "^2.4.6",
|
"vue-color": "^2.4.6",
|
||||||
"vue-observe-visibility": "^0.4.4",
|
"vue-observe-visibility": "^0.4.4",
|
||||||
"vue-template-compiler": "^2.5.16",
|
"vue-template-compiler": "^2.6.10",
|
||||||
"vuedraggable": "^2.16.0"
|
"vuedraggable": "^2.16.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ export default class AddonManager extends Module {
|
||||||
|
|
||||||
async onEnable() {
|
async onEnable() {
|
||||||
this.settings.addUI('add-ons', {
|
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',
|
component: 'addon-list',
|
||||||
title: 'Add-Ons',
|
title: 'Add-Ons',
|
||||||
no_filter: true,
|
no_filter: true,
|
||||||
|
|
|
@ -151,7 +151,7 @@ ${typeof x[1] === 'string' ? x[1] : JSON.stringify(x[1], null, 4)}`
|
||||||
FrankerFaceZ.Logger = Logger;
|
FrankerFaceZ.Logger = Logger;
|
||||||
|
|
||||||
const VER = FrankerFaceZ.version_info = {
|
const VER = FrankerFaceZ.version_info = {
|
||||||
major: 4, minor: 4, revision: 0,
|
major: 4, minor: 4, revision: 1,
|
||||||
commit: __git_commit__,
|
commit: __git_commit__,
|
||||||
build: __webpack_hash__,
|
build: __webpack_hash__,
|
||||||
toString: () =>
|
toString: () =>
|
||||||
|
|
|
@ -77,6 +77,7 @@
|
||||||
:href="url"
|
:href="url"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
class="tw-link tw-c-text-overlay"
|
||||||
>
|
>
|
||||||
{{ url }}
|
{{ url }}
|
||||||
</a>
|
</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"
|
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)"
|
@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 }}
|
{{ profile.i18n_key ? t(profile.i18n_key, profile.name) : profile.name }}
|
||||||
</span>
|
</span>
|
||||||
<div v-if="profile.description" class="tw-tooltip tw-tooltip--down tw-tooltip--align-left">
|
<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"
|
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)"
|
@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.') }}
|
{{ t('setting.backup-restore.enable-auto', 'Yes, allow automatic updates.') }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</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"
|
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)"
|
@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.') }}
|
{{ t('setting.backup-restore.disable-auto', 'No, prevent automatic updates.') }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -528,7 +528,7 @@ export default class MainMenu extends Module {
|
||||||
move: idx => context.manager.moveProfile(profile.id, idx),
|
move: idx => context.manager.moveProfile(profile.id, idx),
|
||||||
save: () => profile.save(),
|
save: () => profile.save(),
|
||||||
update: data => {
|
update: data => {
|
||||||
profile.data = data
|
profile.data = deep_copy(data)
|
||||||
profile.save()
|
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
|
<select
|
||||||
:id="'page$' + id"
|
:id="'page$' + id"
|
||||||
v-model="value.data.route"
|
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
|
<option
|
||||||
v-for="(route, key) in routes"
|
v-for="(route, key) in routes"
|
||||||
|
@ -21,10 +21,18 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="tw-border-t tw-mg-t-05">
|
||||||
v-if="parts && parts.length"
|
<div class="tw-pd-y-05">
|
||||||
class="tw-border-t tw-mg-t-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
|
<div
|
||||||
v-for="part in parts"
|
v-for="part in parts"
|
||||||
:key="part.key"
|
:key="part.key"
|
||||||
|
@ -37,7 +45,7 @@
|
||||||
<input
|
<input
|
||||||
:id="'page$' + id + '$part-' + part.key"
|
:id="'page$' + id + '$part-' + part.key"
|
||||||
v-model="value.data.values[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>
|
||||||
</div>
|
</div>
|
||||||
|
@ -66,6 +74,25 @@ export default {
|
||||||
return this.routes[this.value.data.route];
|
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() {
|
parts() {
|
||||||
const out = [];
|
const out = [];
|
||||||
if ( ! this.route || ! this.route.parts )
|
if ( ! this.route || ! this.route.parts )
|
||||||
|
@ -78,7 +105,8 @@ export default {
|
||||||
out.push({
|
out.push({
|
||||||
key: part.name,
|
key: part.name,
|
||||||
i18n: `settings.filter.page.route.${this.route.name}.${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;
|
let i = 1;
|
||||||
for(const part of route.parts) {
|
for(const part of route.parts) {
|
||||||
if ( typeof part === 'object' ) {
|
if ( typeof part === 'object' ) {
|
||||||
if ( config.values[part.name] != null )
|
const val = config.values[part.name];
|
||||||
parts.push([i, config.values[part.name]]);
|
if ( val && val.length )
|
||||||
|
parts.push([i, val]);
|
||||||
|
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
@ -141,4 +142,22 @@ export const Page = {
|
||||||
values: {}
|
values: {}
|
||||||
}),
|
}),
|
||||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/page.vue')
|
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.
|
// Before we do anything else, make sure the provider is ready.
|
||||||
await this.provider.awaitReady();
|
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
|
// Load profiles, but don't run any events because we haven't done
|
||||||
// migrations yet.
|
// migrations yet.
|
||||||
this.loadProfiles(true);
|
this.loadProfiles(true);
|
||||||
|
@ -198,6 +202,17 @@ export default class SettingsManager extends Module {
|
||||||
// Profile Management
|
// 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.
|
* Get an existing {@link SettingsProfile} instance.
|
||||||
* @param {number} id - The id of the profile.
|
* @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 Fine from 'utilities/compat/fine';
|
||||||
import FineRouter from 'utilities/compat/fine-router';
|
import FineRouter from 'utilities/compat/fine-router';
|
||||||
import Apollo from 'utilities/compat/apollo';
|
import Apollo from 'utilities/compat/apollo';
|
||||||
|
import TwitchData from 'utilities/twitch-data';
|
||||||
|
|
||||||
import Switchboard from './switchboard';
|
import Switchboard from './switchboard';
|
||||||
|
|
||||||
|
@ -31,6 +32,7 @@ export default class Twilight extends BaseSite {
|
||||||
this.inject(Fine);
|
this.inject(Fine);
|
||||||
this.inject('router', FineRouter);
|
this.inject('router', FineRouter);
|
||||||
this.inject(Apollo, false);
|
this.inject(Apollo, false);
|
||||||
|
this.inject(TwitchData);
|
||||||
this.inject(Switchboard);
|
this.inject(Switchboard);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,26 @@ export default class Channel extends Module {
|
||||||
this.wrapRaidController(inst);
|
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.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') )
|
if ( this.settings.get('channel.hosting.enable') || has(inst.state, 'hostMode') || has(inst.state, 'hostedChannel') )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
@ -110,7 +110,8 @@ const CHAT_TYPES = make_enum(
|
||||||
'AnonSubMysteryGift',
|
'AnonSubMysteryGift',
|
||||||
'FirstCheerMessage',
|
'FirstCheerMessage',
|
||||||
'BitsBadgeTierMessage',
|
'BitsBadgeTierMessage',
|
||||||
'InlinePrivateCallout'
|
'InlinePrivateCallout',
|
||||||
|
'ChannelPointsReward'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
@ -518,7 +519,7 @@ export default class ChatHook extends Module {
|
||||||
this.updateMentionCSS();
|
this.updateMentionCSS();
|
||||||
|
|
||||||
this.ChatController.on('mount', this.chatMounted, this);
|
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.ChatController.on('receive-props', this.chatUpdated, this);
|
||||||
|
|
||||||
this.ChatService.ready((cls, instances) => {
|
this.ChatService.ready((cls, instances) => {
|
||||||
|
@ -1565,6 +1566,12 @@ export default class ChatHook extends Module {
|
||||||
chatHidden: props.isHidden
|
chatHidden: props.isHidden
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if ( props.isEmbedded || props.isPopout )
|
||||||
|
this.settings.updateContext({
|
||||||
|
channel: props.channelLogin && props.channelLogin.toLowerCase(),
|
||||||
|
channelID: props.channelID
|
||||||
|
});
|
||||||
|
|
||||||
this.chat.context.updateContext({
|
this.chat.context.updateContext({
|
||||||
moderator: props.isCurrentUserModerator,
|
moderator: props.isCurrentUserModerator,
|
||||||
channel: props.channelLogin && props.channelLogin.toLowerCase(),
|
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 )
|
if ( chat.chatBuffer && chat.chatBuffer.ffzController === this )
|
||||||
chat.chatBuffer.ffzController = null;
|
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);
|
this.removeRoom(chat);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1600,6 +1619,12 @@ export default class ChatHook extends Module {
|
||||||
|
|
||||||
// TODO: Check if this is the room for the current channel.
|
// 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({
|
this.settings.updateContext({
|
||||||
moderator: props.isCurrentUserModerator,
|
moderator: props.isCurrentUserModerator,
|
||||||
chatHidden: props.isHidden
|
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;
|
this.open = false;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.search = item.label || item.name || item.value;
|
this.search = item.displayName || item.label || item.name || item.value;
|
||||||
this.open = false;
|
this.open = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$emit('input', this.search);
|
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) {
|
navigate(route, data, opts) {
|
||||||
const r = this.routes[route];
|
this.history.push(this.getURL(route, data, opts));
|
||||||
if ( ! r )
|
|
||||||
throw new Error(`unable to find route "${route}"`);
|
|
||||||
|
|
||||||
const url = r.url(data, opts);
|
|
||||||
this.history.push(url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_navigateTo(location) {
|
_navigateTo(location) {
|
||||||
|
@ -55,6 +50,11 @@ export default class FineRouter extends Module {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.location = path;
|
this.location = path;
|
||||||
|
this._pickRoute();
|
||||||
|
}
|
||||||
|
|
||||||
|
_pickRoute() {
|
||||||
|
const path = this.location;
|
||||||
|
|
||||||
for(const route of this.__routes) {
|
for(const route of this.__routes) {
|
||||||
const match = route.regex.exec(path);
|
const match = route.regex.exec(path);
|
||||||
|
@ -73,6 +73,14 @@ export default class FineRouter extends Module {
|
||||||
this.emit(':route', null, null);
|
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) {
|
getRoute(name) {
|
||||||
return this.routes[name];
|
return this.routes[name];
|
||||||
}
|
}
|
||||||
|
@ -92,26 +100,35 @@ export default class FineRouter extends Module {
|
||||||
return this.route_names[route];
|
return this.route_names[route];
|
||||||
}
|
}
|
||||||
|
|
||||||
routeName(route, name) {
|
routeName(route, name, process = true) {
|
||||||
if ( typeof route === 'object' ) {
|
if ( typeof route === 'object' ) {
|
||||||
for(const key in route)
|
for(const key in route)
|
||||||
if ( has(route, key) )
|
if ( has(route, key) )
|
||||||
this.routeName(key, route[key]);
|
this.routeName(key, route[key], false);
|
||||||
|
|
||||||
|
if ( process )
|
||||||
|
this.emit(':updated-route-names');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.route_names[route] = name;
|
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' ) {
|
if ( typeof name === 'object' ) {
|
||||||
for(const key in name)
|
for(const key in name)
|
||||||
if ( has(name, key) )
|
if ( has(name, key) )
|
||||||
this.route(key, name[key], false);
|
this.route(key, name[key], false);
|
||||||
|
|
||||||
if ( sort )
|
if ( process ) {
|
||||||
this.__routes.sort((a,b) => b.score - a.score);
|
this.__routes.sort((a,b) => b.score - a.score);
|
||||||
|
if ( this.location )
|
||||||
|
this._pickRoute();
|
||||||
|
this.emit(':updated-routes');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,7 +147,11 @@ export default class FineRouter extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.__routes.push(route);
|
this.__routes.push(route);
|
||||||
if ( sort )
|
if ( process ) {
|
||||||
this.__routes.sort((a,b) => b.score - a.score);
|
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 {
|
.ffz--filter-editor {
|
||||||
|
label {
|
||||||
|
width: auto;
|
||||||
|
min-width: unset;
|
||||||
|
}
|
||||||
|
|
||||||
.ffz--rule {
|
.ffz--rule {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue