mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-10-14 23:11:58 +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
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++;
|
||||
}
|
||||
|
@ -141,4 +142,22 @@ export const Page = {
|
|||
values: {}
|
||||
}),
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/page.vue')
|
||||
};
|
||||
|
||||
export const Channel = {
|
||||
createTest(config = {}) {
|
||||
const login = config.login,
|
||||
id = config.id;
|
||||
|
||||
return ctx => ctx.channelID === id || (ctx.channelID == null && ctx.channelLogin === login);
|
||||
},
|
||||
|
||||
title: 'Current Channel',
|
||||
i18n: 'settings.filter.channel',
|
||||
|
||||
default: () => ({
|
||||
login: null,
|
||||
id: null
|
||||
}),
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/channel.vue')
|
||||
};
|
|
@ -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.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue