lint
This commit is contained in:
parent
3bb7afe544
commit
0fb9c372dd
22 changed files with 69 additions and 71 deletions
|
@ -55,6 +55,7 @@ module.exports = {
|
|||
'vue/multi-word-component-names': 'warn',
|
||||
'vue/require-v-for-key': 'warn',
|
||||
'vue/no-unused-components': 'warn',
|
||||
'vue/no-unused-vars': 'warn',
|
||||
'vue/valid-v-for': 'warn',
|
||||
'vue/return-in-computed-property': 'warn',
|
||||
'vue/no-setup-props-destructure': 'warn',
|
||||
|
|
|
@ -43,7 +43,7 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
const uiWindow = shallowRef<InstanceType<typeof MkWindow>>();
|
||||
const comment = ref(props.initialComment || '');
|
||||
const comment = ref(props.initialComment ?? '');
|
||||
|
||||
function send() {
|
||||
os.apiWithDialog('users/report-abuse', {
|
||||
|
|
|
@ -209,7 +209,7 @@ function exec() {
|
|||
}
|
||||
} else if (props.type === 'hashtag') {
|
||||
if (!props.q || props.q === '') {
|
||||
hashtags.value = JSON.parse(miLocalStorage.getItem('hashtags') || '[]');
|
||||
hashtags.value = JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]');
|
||||
fetching.value = false;
|
||||
} else {
|
||||
const cacheKey = `autocomplete:hashtag:${props.q}`;
|
||||
|
|
|
@ -69,7 +69,7 @@ const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown
|
|||
if (loaded) {
|
||||
available.value = true;
|
||||
} else {
|
||||
(document.getElementById(scriptId.value) || document.head.appendChild(Object.assign(document.createElement('script'), {
|
||||
(document.getElementById(scriptId.value) ?? document.head.appendChild(Object.assign(document.createElement('script'), {
|
||||
async: true,
|
||||
id: scriptId.value,
|
||||
src: src.value,
|
||||
|
|
|
@ -45,8 +45,8 @@ onMounted(() => {
|
|||
src: media.url,
|
||||
w: media.properties.width,
|
||||
h: media.properties.height,
|
||||
alt: media.comment || media.name,
|
||||
comment: media.comment || media.name,
|
||||
alt: media.comment ?? media.name,
|
||||
comment: media.comment ?? media.name,
|
||||
};
|
||||
if (media.properties.orientation != null && media.properties.orientation >= 5) {
|
||||
[item.w, item.h] = [item.h, item.w];
|
||||
|
@ -90,8 +90,8 @@ onMounted(() => {
|
|||
[itemData.w, itemData.h] = [itemData.h, itemData.w];
|
||||
}
|
||||
itemData.msrc = file.thumbnailUrl;
|
||||
itemData.alt = file.comment || file.name;
|
||||
itemData.comment = file.comment || file.name;
|
||||
itemData.alt = file.comment ?? file.name;
|
||||
itemData.comment = file.comment ?? file.name;
|
||||
itemData.thumbCropped = true;
|
||||
});
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ const props = withDefaults(defineProps<{
|
|||
showGlobalToggle: true,
|
||||
});
|
||||
|
||||
let includingTypes = $computed(() => props.includingTypes || []);
|
||||
let includingTypes = $computed(() => props.includingTypes ?? []);
|
||||
|
||||
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
|
||||
|
|
|
@ -104,7 +104,7 @@ const {
|
|||
enableInfiniteScroll,
|
||||
} = defaultStore.reactiveState;
|
||||
|
||||
const contentEl = $computed(() => props.pagination.pageEl || rootEl);
|
||||
const contentEl = $computed(() => props.pagination.pageEl ?? rootEl);
|
||||
const scrollableElement = $computed(() => getScrollContainer(contentEl));
|
||||
|
||||
// 先頭が表示されているかどうかを検出
|
||||
|
|
|
@ -154,7 +154,7 @@ let autocomplete = $ref(null);
|
|||
let draghover = $ref(false);
|
||||
let quoteId = $ref(null);
|
||||
let hasNotSpecifiedMentions = $ref(false);
|
||||
let recentHashtags = $ref(JSON.parse(miLocalStorage.getItem('hashtags') || '[]'));
|
||||
let recentHashtags = $ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]'));
|
||||
let imeText = $ref('');
|
||||
|
||||
const draftKey = $computed((): string => {
|
||||
|
@ -533,7 +533,7 @@ function onDrop(ev): void {
|
|||
}
|
||||
|
||||
function saveDraft() {
|
||||
const draftData = JSON.parse(miLocalStorage.getItem('drafts') || '{}');
|
||||
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}');
|
||||
|
||||
draftData[draftKey] = {
|
||||
updatedAt: new Date(),
|
||||
|
@ -642,7 +642,7 @@ async function post(ev?: MouseEvent) {
|
|||
emit('posted');
|
||||
if (postData.text && postData.text !== '') {
|
||||
const hashtags_ = mfm.parse(postData.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
|
||||
const history = JSON.parse(miLocalStorage.getItem('hashtags') || '[]') as string[];
|
||||
const history = JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]') as string[];
|
||||
miLocalStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history))));
|
||||
}
|
||||
posting = false;
|
||||
|
@ -746,7 +746,7 @@ onMounted(() => {
|
|||
nextTick(() => {
|
||||
// 書きかけの投稿を復元
|
||||
if (!props.instant && !props.mention && !props.specified) {
|
||||
const draft = JSON.parse(miLocalStorage.getItem('drafts') || '{}')[draftKey];
|
||||
const draft = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}')[draftKey];
|
||||
if (draft) {
|
||||
text = draft.data.text;
|
||||
useCw = draft.data.useCw;
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<template #label>{{ i18n.ts.username }}</template>
|
||||
<template #prefix>@</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="host" @update:model-value="search" :datalist="[hostname]">
|
||||
<MkInput v-model="host" :datalist="[hostname]" @update:model-value="search">
|
||||
<template #label>{{ i18n.ts.host }}</template>
|
||||
<template #prefix>@</template>
|
||||
</MkInput>
|
||||
|
|
|
@ -24,7 +24,7 @@ const rawUrl = computed(() => {
|
|||
return props.url;
|
||||
}
|
||||
if (props.host == null && !customEmojiName.value.includes('@')) {
|
||||
return customEmojis.value.find(x => x.name === customEmojiName.value)?.url || null;
|
||||
return customEmojis.value.find(x => x.name === customEmojiName.value)?.url ?? null;
|
||||
}
|
||||
return props.host ? `/emoji/${customEmojiName.value}@${props.host}.webp` : `/emoji/${customEmojiName.value}.webp`;
|
||||
});
|
||||
|
@ -32,7 +32,7 @@ const rawUrl = computed(() => {
|
|||
const url = computed(() =>
|
||||
defaultStore.reactiveState.disableShowingAnimatedImages.value && rawUrl.value
|
||||
? getStaticImageUrl(rawUrl.value)
|
||||
: rawUrl.value
|
||||
: rawUrl.value,
|
||||
);
|
||||
|
||||
const alt = computed(() => `:${customEmojiName.value}:`);
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
<div v-if="show" ref="el" :class="[$style.root]" :style="{ background: bg }">
|
||||
<div :class="[$style.upper, { [$style.slim]: narrow, [$style.thin]: thin_ }]">
|
||||
<div v-if="!thin_ && narrow && props.displayMyAvatar && $i" class="_button" :class="$style.buttonsLeft" @click="openAccountMenu">
|
||||
<MkAvatar :class="$style.avatar" :user="$i" />
|
||||
<MkAvatar :class="$style.avatar" :user="$i"/>
|
||||
</div>
|
||||
<div v-else-if="!thin_ && narrow && !hideTitle" :class="$style.buttonsLeft" />
|
||||
<div v-else-if="!thin_ && narrow && !hideTitle" :class="$style.buttonsLeft"/>
|
||||
|
||||
<template v-if="metadata">
|
||||
<div v-if="!hideTitle" :class="$style.titleContainer" @click="top">
|
||||
|
@ -36,11 +36,11 @@
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref, inject } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import XTabs, { Tab } from './MkPageHeader.tabs.vue';
|
||||
import { scrollToTop } from '@/scripts/scroll';
|
||||
import { globalEvents } from '@/events';
|
||||
import { injectPageMetadata } from '@/scripts/page-metadata';
|
||||
import { $i, openAccountMenu as openAccountMenu_ } from '@/account';
|
||||
import XTabs, { Tab } from './MkPageHeader.tabs.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
tabs?: Tab[];
|
||||
|
@ -96,7 +96,7 @@ function onTabClick(): void {
|
|||
}
|
||||
|
||||
const calcBg = () => {
|
||||
const rawBg = metadata?.bg || 'var(--bg)';
|
||||
const rawBg = metadata?.bg ?? 'var(--bg)';
|
||||
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
||||
tinyBg.setAlpha(0.85);
|
||||
bg.value = tinyBg.toRgbString();
|
||||
|
|
|
@ -113,7 +113,7 @@ function onTabClick(tab: Tab, ev: MouseEvent): void {
|
|||
}
|
||||
|
||||
const calcBg = () => {
|
||||
const rawBg = metadata?.bg || 'var(--bg)';
|
||||
const rawBg = metadata?.bg ?? 'var(--bg)';
|
||||
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
||||
tinyBg.setAlpha(0.85);
|
||||
bg.value = tinyBg.toRgbString();
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs" /></template>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="500">
|
||||
<div v-if="state == 'fetch-session-error'">
|
||||
<p>{{ i18n.ts.somethingHappened }}</p>
|
||||
</div>
|
||||
<div v-else-if="$i && !session">
|
||||
<MkLoading />
|
||||
<MkLoading/>
|
||||
</div>
|
||||
<div v-else-if="$i && session">
|
||||
<XForm
|
||||
|
@ -21,15 +21,16 @@
|
|||
</div>
|
||||
<div v-if="state == 'accepted' && session">
|
||||
<h1>{{ session.app.isAuthorized ? $t('already-authorized') : i18n.ts.allowed }}</h1>
|
||||
<p v-if="session.app.callbackUrl">{{ i18n.ts._auth.callback }}
|
||||
<MkEllipsis />
|
||||
<p v-if="session.app.callbackUrl">
|
||||
{{ i18n.ts._auth.callback }}
|
||||
<MkEllipsis/>
|
||||
</p>
|
||||
<p v-if="!session.app.callbackUrl">{{ i18n.ts._auth.pleaseGoBack }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p :class="$style.loginMessage">{{ i18n.ts._auth.pleaseLogin }}</p>
|
||||
<MkSignin @login="onLogin" />
|
||||
<MkSignin @login="onLogin"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
|
@ -37,12 +38,12 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { AuthSession } from 'misskey-js/built/entities';
|
||||
import XForm from './auth.form.vue';
|
||||
import MkSignin from '@/components/MkSignin.vue';
|
||||
import * as os from '@/os';
|
||||
import { $i, login } from '@/account';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { AuthSession } from 'misskey-js/built/entities';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -82,7 +83,7 @@ onMounted(async () => {
|
|||
} else {
|
||||
state = 'waiting';
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (err) {
|
||||
state = 'fetch-session-error';
|
||||
}
|
||||
});
|
||||
|
|
|
@ -124,11 +124,11 @@ function saveFields() {
|
|||
|
||||
function save() {
|
||||
os.apiWithDialog('i/update', {
|
||||
name: profile.name || null,
|
||||
description: profile.description || null,
|
||||
location: profile.location || null,
|
||||
birthday: profile.birthday || null,
|
||||
lang: profile.lang || null,
|
||||
name: profile.name ?? null,
|
||||
description: profile.description ?? null,
|
||||
location: profile.location ?? null,
|
||||
birthday: profile.birthday ?? null,
|
||||
lang: profile.lang ?? null,
|
||||
isBot: !!profile.isBot,
|
||||
isCat: !!profile.isCat,
|
||||
showTimelineReplies: !!profile.showTimelineReplies,
|
||||
|
|
|
@ -48,8 +48,8 @@ export class Storage<T extends StateDef> {
|
|||
// 簡易的にキューイングして占有ロックとする
|
||||
private currentIdbJob: Promise<any> = Promise.resolve();
|
||||
private addIdbSetJob<T>(job: () => Promise<T>) {
|
||||
const promise = this.currentIdbJob.then(job, e => {
|
||||
console.error('Pizzax failed to save data to idb!', e);
|
||||
const promise = this.currentIdbJob.then(job, err => {
|
||||
console.error('Pizzax failed to save data to idb!', err);
|
||||
return job();
|
||||
});
|
||||
this.currentIdbJob = promise;
|
||||
|
@ -130,22 +130,22 @@ export class Storage<T extends StateDef> {
|
|||
await defaultStore.ready;
|
||||
|
||||
api('i/registry/get-all', { scope: ['client', this.key] })
|
||||
.then(kvs => {
|
||||
const cache: Partial<T> = {};
|
||||
for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
|
||||
if (v.where === 'account') {
|
||||
if (Object.prototype.hasOwnProperty.call(kvs, k)) {
|
||||
this.reactiveState[k].value = this.state[k] = (kvs as Partial<T>)[k];
|
||||
cache[k] = (kvs as Partial<T>)[k];
|
||||
} else {
|
||||
this.reactiveState[k].value = this.state[k] = v.default;
|
||||
.then(kvs => {
|
||||
const cache: Partial<T> = {};
|
||||
for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
|
||||
if (v.where === 'account') {
|
||||
if (Object.prototype.hasOwnProperty.call(kvs, k)) {
|
||||
this.reactiveState[k].value = this.state[k] = (kvs as Partial<T>)[k];
|
||||
cache[k] = (kvs as Partial<T>)[k];
|
||||
} else {
|
||||
this.reactiveState[k].value = this.state[k] = v.default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return set(this.registryCacheKeyName, cache);
|
||||
})
|
||||
.then(() => resolve());
|
||||
return set(this.registryCacheKeyName, cache);
|
||||
})
|
||||
.then(() => resolve());
|
||||
}, 1);
|
||||
} else {
|
||||
resolve();
|
||||
|
|
|
@ -240,7 +240,7 @@ export function getNoteMenu(props: {
|
|||
icon: 'ti ti-external-link',
|
||||
text: i18n.ts.showOnRemote,
|
||||
action: () => {
|
||||
window.open(appearNote.url || appearNote.uri, '_blank');
|
||||
window.open(appearNote.url ?? appearNote.uri, '_blank');
|
||||
},
|
||||
} : undefined,
|
||||
{
|
||||
|
@ -302,7 +302,7 @@ export function getNoteMenu(props: {
|
|||
icon: 'ti ti-exclamation-circle',
|
||||
text: i18n.ts.reportAbuse,
|
||||
action: () => {
|
||||
const u = appearNote.url || appearNote.uri || `${url}/notes/${appearNote.id}`;
|
||||
const u = appearNote.url ?? appearNote.uri ?? `${url}/notes/${appearNote.id}`;
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
|
||||
user: appearNote.user,
|
||||
initialComment: `Note: ${u}\n-----\n`,
|
||||
|
@ -344,7 +344,7 @@ export function getNoteMenu(props: {
|
|||
icon: 'ti ti-external-link',
|
||||
text: i18n.ts.showOnRemote,
|
||||
action: () => {
|
||||
window.open(appearNote.url || appearNote.uri, '_blank');
|
||||
window.open(appearNote.url ?? appearNote.uri, '_blank');
|
||||
},
|
||||
} : undefined]
|
||||
.filter(x => x !== undefined);
|
||||
|
|
|
@ -58,7 +58,7 @@ export class HpmlScope {
|
|||
|
||||
constructor(layerdStates: HpmlScope['layerdStates'], name?: HpmlScope['name']) {
|
||||
this.layerdStates = layerdStates;
|
||||
this.name = name || 'anonymous';
|
||||
this.name = name ?? 'anonymous';
|
||||
}
|
||||
|
||||
@autobind
|
||||
|
|
|
@ -63,7 +63,7 @@ export class HpmlTypeChecker {
|
|||
|
||||
@autobind
|
||||
public getExpectedType(v: Expr, slot: number): Type {
|
||||
const def = funcDefs[v.type || ''];
|
||||
const def = funcDefs[v.type ?? ''];
|
||||
if (def == null) {
|
||||
throw new Error('Unknown type: ' + v.type);
|
||||
}
|
||||
|
@ -107,7 +107,7 @@ export class HpmlTypeChecker {
|
|||
return pageVar.type;
|
||||
}
|
||||
|
||||
const envVar = envVarsDef[v.value || ''];
|
||||
const envVar = envVarsDef[v.value ?? ''];
|
||||
if (envVar !== undefined) {
|
||||
return envVar;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ export function getScrollContainer(el: HTMLElement | null): HTMLElement | null {
|
|||
}
|
||||
}
|
||||
|
||||
export function getStickyTop(el: HTMLElement, container: HTMLElement | null = null, top: number = 0) {
|
||||
export function getStickyTop(el: HTMLElement, container: HTMLElement | null = null, top = 0) {
|
||||
if (!el.parentElement) return top;
|
||||
const data = el.dataset.stickyContainerHeaderHeight;
|
||||
const newTop = data ? Number(data) + top : top;
|
||||
|
@ -23,14 +23,14 @@ export function getScrollPosition(el: HTMLElement | null): number {
|
|||
return container == null ? window.scrollY : container.scrollTop;
|
||||
}
|
||||
|
||||
export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance: number = 1, once: boolean = false) {
|
||||
export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false) {
|
||||
// とりあえず評価してみる
|
||||
if (isTopVisible(el)) {
|
||||
cb();
|
||||
if (once) return null;
|
||||
}
|
||||
|
||||
const container = getScrollContainer(el) || window;
|
||||
const container = getScrollContainer(el) ?? window;
|
||||
|
||||
const onScroll = ev => {
|
||||
if (!document.body.contains(el)) return;
|
||||
|
@ -45,7 +45,7 @@ export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance: numbe
|
|||
return removeListener;
|
||||
}
|
||||
|
||||
export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance: number = 1, once: boolean = false) {
|
||||
export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false) {
|
||||
const container = getScrollContainer(el);
|
||||
|
||||
// とりあえず評価してみる
|
||||
|
@ -54,7 +54,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance: nu
|
|||
if (once) return null;
|
||||
}
|
||||
|
||||
const containerOrWindow = container || window;
|
||||
const containerOrWindow = container ?? window;
|
||||
const onScroll = ev => {
|
||||
if (!document.body.contains(el)) return;
|
||||
if (isBottomVisible(el, 1, container)) {
|
||||
|
@ -104,12 +104,12 @@ export function scrollToBottom(
|
|||
} else {
|
||||
window.scroll({
|
||||
top: (el.scrollHeight - window.innerHeight + getStickyTop(el, container) + (window.innerWidth <= 500 ? 96 : 0)) || 0,
|
||||
...options
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function isTopVisible(el: HTMLElement, tolerance: number = 1): boolean {
|
||||
export function isTopVisible(el: HTMLElement, tolerance = 1): boolean {
|
||||
const scrollTop = getScrollPosition(el);
|
||||
return scrollTop <= tolerance;
|
||||
}
|
||||
|
@ -124,6 +124,6 @@ export function getBodyScrollHeight() {
|
|||
return Math.max(
|
||||
document.body.scrollHeight, document.documentElement.scrollHeight,
|
||||
document.body.offsetHeight, document.documentElement.offsetHeight,
|
||||
document.body.clientHeight, document.documentElement.clientHeight
|
||||
document.body.clientHeight, document.documentElement.clientHeight,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { api } from '@/os';
|
||||
import { $i } from '@/account';
|
||||
import { Theme } from './scripts/theme';
|
||||
import { miLocalStorage } from './local-storage';
|
||||
import { api } from '@/os';
|
||||
import { $i } from '@/account';
|
||||
|
||||
const lsCacheKey = $i ? `themes:${$i.id}` as const : null;
|
||||
|
||||
export function getThemes(): Theme[] {
|
||||
if ($i == null) return [];
|
||||
return JSON.parse(miLocalStorage.getItem(lsCacheKey!) || '[]');
|
||||
return JSON.parse(miLocalStorage.getItem(lsCacheKey!) ?? '[]');
|
||||
}
|
||||
|
||||
export async function fetchThemes(): Promise<void> {
|
||||
|
|
|
@ -125,7 +125,7 @@ function onAiClick(ev) {
|
|||
|
||||
if (window.innerWidth < 1024) {
|
||||
const currentUI = miLocalStorage.getItem('ui');
|
||||
miLocalStorage.setItem('ui_temp', currentUI || 'default');
|
||||
miLocalStorage.setItem('ui_temp', currentUI ?? 'default');
|
||||
miLocalStorage.setItem('ui', 'default');
|
||||
location.reload();
|
||||
}
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
export default function(user: { name?: string | null, username: string }): string {
|
||||
// Show username if name is empty.
|
||||
// XXX: typescript-eslint has no configuration to allow using `||` against string.
|
||||
// https://github.com/typescript-eslint/typescript-eslint/issues/4906
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
return user.name || user.username;
|
||||
return user.name === '' ? user.username : user.name ?? user.username;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue