merge upstream
This commit is contained in:
commit
d8908ef2d8
1065 changed files with 32953 additions and 20092 deletions
|
|
@ -10,9 +10,9 @@ import '@/style.scss';
|
|||
import { mainBoot } from '@/boot/main-boot.js';
|
||||
import { subBoot } from '@/boot/sub-boot.js';
|
||||
|
||||
const subBootPaths = ['/share', '/auth', '/miauth', '/oauth', '/signup-complete'];
|
||||
const subBootPaths = ['/share', '/auth', '/miauth', '/oauth', '/signup-complete', '/install-extensions'];
|
||||
|
||||
if (subBootPaths.some(i => location.pathname === i || location.pathname.startsWith(i + '/'))) {
|
||||
if (subBootPaths.some(i => window.location.pathname === i || window.location.pathname.startsWith(i + '/'))) {
|
||||
subBoot();
|
||||
} else {
|
||||
mainBoot();
|
||||
|
|
|
|||
|
|
@ -1,393 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineAsyncComponent, reactive, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { apiUrl } from '@@/js/config.js';
|
||||
import type { MenuItem, MenuButton } from '@/types/menu.js';
|
||||
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { del, get, set } from '@/scripts/idb-proxy.js';
|
||||
import { waiting, popup, popupMenu, success, alert } from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js';
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
||||
type Account = Misskey.entities.MeDetailed & { token: string };
|
||||
|
||||
const accountData = miLocalStorage.getItem('account');
|
||||
|
||||
// TODO: 外部からはreadonlyに
|
||||
export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
|
||||
|
||||
export const iAmModerator = $i != null && ($i.isAdmin === true || $i.isModerator === true);
|
||||
export const iAmAdmin = $i != null && $i.isAdmin;
|
||||
|
||||
export function signinRequired() {
|
||||
if ($i == null) throw new Error('signin required');
|
||||
return $i;
|
||||
}
|
||||
|
||||
export let notesCount = $i == null ? 0 : $i.notesCount;
|
||||
export function incNotesCount() {
|
||||
notesCount++;
|
||||
}
|
||||
|
||||
export async function signout() {
|
||||
if (!$i) return;
|
||||
|
||||
waiting();
|
||||
miLocalStorage.removeItem('account');
|
||||
await removeAccount($i.id);
|
||||
document.cookie = `token=; path=/; max-age=0${ location.protocol === 'https:' ? '; Secure' : ''}`;
|
||||
const accounts = await getAccounts();
|
||||
|
||||
//#region Remove service worker registration
|
||||
try {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
const push = await registration.pushManager.getSubscription();
|
||||
if (push) {
|
||||
await window.fetch(`${apiUrl}/sw/unregister`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
i: $i.token,
|
||||
endpoint: push.endpoint,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (accounts.length === 0) {
|
||||
await navigator.serviceWorker.getRegistrations()
|
||||
.then(registrations => {
|
||||
return Promise.all(registrations.map(registration => registration.unregister()));
|
||||
});
|
||||
}
|
||||
} catch (err) {}
|
||||
//#endregion
|
||||
|
||||
if (accounts.length > 0) login(accounts[0].token);
|
||||
else unisonReload('/');
|
||||
}
|
||||
|
||||
export async function getAccounts(): Promise<{ id: Account['id'], token: Account['token'] }[]> {
|
||||
return (await get('accounts')) || [];
|
||||
}
|
||||
|
||||
export async function addAccount(id: Account['id'], token: Account['token']) {
|
||||
const accounts = await getAccounts();
|
||||
if (!accounts.some(x => x.id === id)) {
|
||||
await set('accounts', accounts.concat([{ id, token }]));
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeAccount(idOrToken: Account['id']) {
|
||||
const accounts = await getAccounts();
|
||||
const i = accounts.findIndex(x => x.id === idOrToken || x.token === idOrToken);
|
||||
if (i !== -1) accounts.splice(i, 1);
|
||||
|
||||
if (accounts.length > 0) {
|
||||
await set('accounts', accounts);
|
||||
} else {
|
||||
await del('accounts');
|
||||
}
|
||||
}
|
||||
|
||||
function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Promise<Account> {
|
||||
document.cookie = `token=; path=/; max-age=0${ location.protocol === 'https:' ? '; Secure' : ''}`;
|
||||
document.cookie = `token=${token}; path=/queue; max-age=86400${ location.protocol === 'https:' ? '; SameSite=Strict; Secure' : ''}`; // bull dashboardの認証とかで使う
|
||||
|
||||
return new Promise((done, fail) => {
|
||||
window.fetch(`${apiUrl}/i`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
i: token,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(res => new Promise<Account | { error: Record<string, any> }>((done2, fail2) => {
|
||||
if (res.status >= 500 && res.status < 600) {
|
||||
// サーバーエラー(5xx)の場合をrejectとする
|
||||
// (認証エラーなど4xxはresolve)
|
||||
return fail2(res);
|
||||
}
|
||||
res.json().then(done2, fail2);
|
||||
}))
|
||||
.then(async res => {
|
||||
if ('error' in res) {
|
||||
if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') {
|
||||
// SUSPENDED
|
||||
if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
|
||||
await showSuspendedDialog();
|
||||
}
|
||||
} else if (res.error.id === 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a') {
|
||||
// USER_IS_DELETED
|
||||
// アカウントが削除されている
|
||||
if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
|
||||
await alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.accountDeleted,
|
||||
text: i18n.ts.accountDeletedDescription,
|
||||
});
|
||||
}
|
||||
} else if (res.error.id === 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14') {
|
||||
// AUTHENTICATION_FAILED
|
||||
// トークンが無効化されていたりアカウントが削除されたりしている
|
||||
if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
|
||||
await alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.tokenRevoked,
|
||||
text: i18n.ts.tokenRevokedDescription,
|
||||
});
|
||||
}
|
||||
} else if (res.error.id === 'd5826d14-3982-4d2e-8011-b9e9f02499ef') {
|
||||
// rate limited
|
||||
const timeToWait = res.error.info?.resetMs ?? 1000;
|
||||
window.setTimeout(() => {
|
||||
fetchAccount(token, id, forceShowDialog).then(done, fail);
|
||||
}, timeToWait);
|
||||
return;
|
||||
} else {
|
||||
await alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.failedToFetchAccountInformation,
|
||||
text: JSON.stringify(res.error),
|
||||
});
|
||||
}
|
||||
|
||||
// rejectかつ理由がtrueの場合、削除対象であることを示す
|
||||
fail(true);
|
||||
} else {
|
||||
(res as Account).token = token;
|
||||
done(res as Account);
|
||||
}
|
||||
})
|
||||
.catch(fail);
|
||||
});
|
||||
}
|
||||
|
||||
export function updateAccount(accountData: Account) {
|
||||
if (!$i) return;
|
||||
for (const key of Object.keys($i)) {
|
||||
delete $i[key];
|
||||
}
|
||||
for (const [key, value] of Object.entries(accountData)) {
|
||||
$i[key] = value;
|
||||
}
|
||||
miLocalStorage.setItem('account', JSON.stringify($i));
|
||||
}
|
||||
|
||||
export function updateAccountPartial(accountData: Partial<Account>) {
|
||||
if (!$i) return;
|
||||
for (const [key, value] of Object.entries(accountData)) {
|
||||
$i[key] = value;
|
||||
}
|
||||
miLocalStorage.setItem('account', JSON.stringify($i));
|
||||
}
|
||||
|
||||
export async function refreshAccount() {
|
||||
if (!$i) return;
|
||||
return fetchAccount($i.token, $i.id)
|
||||
.then(updateAccount, reason => {
|
||||
if (reason === true) return signout();
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
export async function login(token: Account['token'], redirect?: string) {
|
||||
const showing = ref(true);
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), {
|
||||
success: false,
|
||||
showing: showing,
|
||||
}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
if (_DEV_) console.log('logging as token ', token);
|
||||
const me = await fetchAccount(token, undefined, true)
|
||||
.catch(reason => {
|
||||
if (reason === true) {
|
||||
// 削除対象の場合
|
||||
removeAccount(token);
|
||||
}
|
||||
|
||||
showing.value = false;
|
||||
throw reason;
|
||||
});
|
||||
miLocalStorage.setItem('account', JSON.stringify(me));
|
||||
await addAccount(me.id, token);
|
||||
|
||||
if (redirect) {
|
||||
// 他のタブは再読み込みするだけ
|
||||
reloadChannel.postMessage(null);
|
||||
// このページはredirectで指定された先に移動
|
||||
location.href = redirect;
|
||||
return;
|
||||
}
|
||||
|
||||
unisonReload();
|
||||
}
|
||||
|
||||
export async function openAccountMenu(opts: {
|
||||
includeCurrentAccount?: boolean;
|
||||
withExtraOperation: boolean;
|
||||
active?: Misskey.entities.UserDetailed['id'];
|
||||
onChoose?: (account: Misskey.entities.UserDetailed) => void;
|
||||
}, ev: MouseEvent) {
|
||||
if (!$i) return;
|
||||
|
||||
async function switchAccount(account: Misskey.entities.UserDetailed) {
|
||||
const storedAccounts = await getAccounts();
|
||||
const found = storedAccounts.find(x => x.id === account.id);
|
||||
if (found == null) return;
|
||||
switchAccountWithToken(found.token);
|
||||
}
|
||||
|
||||
function switchAccountWithToken(token: string) {
|
||||
login(token);
|
||||
}
|
||||
|
||||
const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id));
|
||||
const accountsPromise = misskeyApi('users/show', { userIds: storedAccounts.map(x => x.id) });
|
||||
|
||||
function createItem(account: Misskey.entities.UserDetailed) {
|
||||
return {
|
||||
type: 'user' as const,
|
||||
user: account,
|
||||
active: opts.active != null ? opts.active === account.id : false,
|
||||
action: () => {
|
||||
if (opts.onChoose) {
|
||||
opts.onChoose(account);
|
||||
} else {
|
||||
switchAccount(account);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const accountItemPromises = storedAccounts.map(a => new Promise<ReturnType<typeof createItem> | MenuButton>(res => {
|
||||
accountsPromise.then(accounts => {
|
||||
const account = accounts.find(x => x.id === a.id);
|
||||
if (account == null) return res({
|
||||
type: 'button' as const,
|
||||
text: a.id,
|
||||
action: () => {
|
||||
switchAccountWithToken(a.token);
|
||||
},
|
||||
});
|
||||
|
||||
res(createItem(account));
|
||||
});
|
||||
}));
|
||||
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
if (opts.withExtraOperation) {
|
||||
menuItems.push({
|
||||
type: 'link',
|
||||
text: i18n.ts.profile,
|
||||
to: `/@${$i.username}`,
|
||||
avatar: $i,
|
||||
}, {
|
||||
type: 'divider',
|
||||
});
|
||||
|
||||
if (opts.includeCurrentAccount) {
|
||||
menuItems.push(createItem($i));
|
||||
}
|
||||
|
||||
menuItems.push(...accountItemPromises);
|
||||
|
||||
menuItems.push({
|
||||
type: 'parent',
|
||||
icon: 'ti ti-plus',
|
||||
text: i18n.ts.addAccount,
|
||||
children: [{
|
||||
text: i18n.ts.existingAccount,
|
||||
action: () => {
|
||||
getAccountWithSigninDialog().then(res => {
|
||||
if (res != null) {
|
||||
success();
|
||||
}
|
||||
});
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.createAccount,
|
||||
action: () => {
|
||||
getAccountWithSignupDialog().then(res => {
|
||||
if (res != null) {
|
||||
switchAccountWithToken(res.token);
|
||||
}
|
||||
});
|
||||
},
|
||||
}],
|
||||
}, {
|
||||
type: 'link',
|
||||
icon: 'ti ti-users',
|
||||
text: i18n.ts.manageAccounts,
|
||||
to: '/settings/accounts',
|
||||
}, {
|
||||
type: 'button' as const,
|
||||
icon: 'ph-power ph-bold ph-lg',
|
||||
text: i18n.ts.logout,
|
||||
action: () => { signout(); },
|
||||
});
|
||||
} else {
|
||||
if (opts.includeCurrentAccount) {
|
||||
menuItems.push(createItem($i));
|
||||
}
|
||||
|
||||
menuItems.push(...accountItemPromises);
|
||||
}
|
||||
|
||||
popupMenu(menuItems, ev.currentTarget ?? ev.target, {
|
||||
align: 'left',
|
||||
});
|
||||
}
|
||||
|
||||
export function getAccountWithSigninDialog(): Promise<{ id: string, token: string } | null> {
|
||||
return new Promise((resolve) => {
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
|
||||
done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
|
||||
await addAccount(res.id, res.i);
|
||||
resolve({ id: res.id, token: res.i });
|
||||
},
|
||||
cancelled: () => {
|
||||
resolve(null);
|
||||
},
|
||||
closed: () => {
|
||||
dispose();
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function getAccountWithSignupDialog(): Promise<{ id: string, token: string } | null> {
|
||||
return new Promise((resolve) => {
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
|
||||
done: async (res: Misskey.entities.SignupResponse) => {
|
||||
await addAccount(res.id, res.token);
|
||||
resolve({ id: res.id, token: res.token });
|
||||
},
|
||||
cancelled: () => {
|
||||
resolve(null);
|
||||
},
|
||||
closed: () => {
|
||||
dispose();
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (_DEV_) {
|
||||
(window as any).$i = $i;
|
||||
}
|
||||
339
packages/frontend/src/accounts.ts
Normal file
339
packages/frontend/src/accounts.ts
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineAsyncComponent, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { apiUrl, host } from '@@/js/config.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import { showSuspendedDialog } from '@/utility/show-suspended-dialog.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { waiting, popup, popupMenu, success, alert } from '@/os.js';
|
||||
import { unisonReload, reloadChannel } from '@/utility/unison-reload.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { store } from '@/store.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { signout } from '@/signout.js';
|
||||
|
||||
type AccountWithToken = Misskey.entities.MeDetailed & { token: string };
|
||||
|
||||
export async function getAccounts(): Promise<{
|
||||
host: string;
|
||||
user: Misskey.entities.User;
|
||||
token: string | null;
|
||||
}[]> {
|
||||
const tokens = store.s.accountTokens;
|
||||
const accounts = prefer.s.accounts;
|
||||
return accounts.map(([host, user]) => ({
|
||||
host,
|
||||
user,
|
||||
token: tokens[host + '/' + user.id] ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
async function addAccount(host: string, user: Misskey.entities.User, token: AccountWithToken['token']) {
|
||||
if (!prefer.s.accounts.some(x => x[0] === host && x[1].id === user.id)) {
|
||||
store.set('accountTokens', { ...store.s.accountTokens, [host + '/' + user.id]: token });
|
||||
prefer.commit('accounts', [...prefer.s.accounts, [host, user]]);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeAccount(host: string, id: AccountWithToken['id']) {
|
||||
const tokens = JSON.parse(JSON.stringify(store.s.accountTokens));
|
||||
delete tokens[host + '/' + id];
|
||||
store.set('accountTokens', tokens);
|
||||
prefer.commit('accounts', prefer.s.accounts.filter(x => x[0] !== host || x[1].id !== id));
|
||||
}
|
||||
|
||||
const isAccountDeleted = Symbol('isAccountDeleted');
|
||||
|
||||
function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Promise<Misskey.entities.MeDetailed> {
|
||||
return new Promise((done, fail) => {
|
||||
window.fetch(`${apiUrl}/i`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
i: token,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(res => new Promise<Misskey.entities.MeDetailed | { error: Record<string, any> }>((done2, fail2) => {
|
||||
if (res.status >= 500 && res.status < 600) {
|
||||
// サーバーエラー(5xx)の場合をrejectとする
|
||||
// (認証エラーなど4xxはresolve)
|
||||
return fail2(res);
|
||||
}
|
||||
res.json().then(done2, fail2);
|
||||
}))
|
||||
.then(async res => {
|
||||
if ('error' in res) {
|
||||
if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') {
|
||||
// SUSPENDED
|
||||
if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
|
||||
await showSuspendedDialog();
|
||||
}
|
||||
} else if (res.error.id === 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a') {
|
||||
// USER_IS_DELETED
|
||||
// アカウントが削除されている
|
||||
if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
|
||||
await alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.accountDeleted,
|
||||
text: i18n.ts.accountDeletedDescription,
|
||||
});
|
||||
}
|
||||
} else if (res.error.id === 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14') {
|
||||
// AUTHENTICATION_FAILED
|
||||
// トークンが無効化されていたりアカウントが削除されたりしている
|
||||
if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
|
||||
await alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.tokenRevoked,
|
||||
text: i18n.ts.tokenRevokedDescription,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.failedToFetchAccountInformation,
|
||||
text: JSON.stringify(res.error),
|
||||
});
|
||||
}
|
||||
|
||||
fail(isAccountDeleted);
|
||||
} else {
|
||||
done(res);
|
||||
}
|
||||
})
|
||||
.catch(fail);
|
||||
});
|
||||
}
|
||||
|
||||
export function updateCurrentAccount(accountData: Misskey.entities.MeDetailed) {
|
||||
if (!$i) return;
|
||||
const token = $i.token;
|
||||
for (const key of Object.keys($i)) {
|
||||
delete $i[key];
|
||||
}
|
||||
for (const [key, value] of Object.entries(accountData)) {
|
||||
$i[key] = value;
|
||||
}
|
||||
prefer.commit('accounts', prefer.s.accounts.map(([host, user]) => {
|
||||
// TODO: $iのホストも比較したいけど通常null
|
||||
if (user.id === $i.id) {
|
||||
return [host, $i];
|
||||
} else {
|
||||
return [host, user];
|
||||
}
|
||||
}));
|
||||
$i.token = token;
|
||||
miLocalStorage.setItem('account', JSON.stringify($i));
|
||||
}
|
||||
|
||||
export function updateCurrentAccountPartial(accountData: Partial<Misskey.entities.MeDetailed>) {
|
||||
if (!$i) return;
|
||||
for (const [key, value] of Object.entries(accountData)) {
|
||||
$i[key] = value;
|
||||
}
|
||||
prefer.commit('accounts', prefer.s.accounts.map(([host, user]) => {
|
||||
// TODO: $iのホストも比較したいけど通常null
|
||||
if (user.id === $i.id) {
|
||||
const newUser = JSON.parse(JSON.stringify($i));
|
||||
for (const [key, value] of Object.entries(accountData)) {
|
||||
newUser[key] = value;
|
||||
}
|
||||
return [host, newUser];
|
||||
}
|
||||
return [host, user];
|
||||
}));
|
||||
miLocalStorage.setItem('account', JSON.stringify($i));
|
||||
}
|
||||
|
||||
export async function refreshCurrentAccount() {
|
||||
if (!$i) return;
|
||||
return fetchAccount($i.token, $i.id).then(updateCurrentAccount).catch(reason => {
|
||||
if (reason === isAccountDeleted) {
|
||||
removeAccount(host, $i.id);
|
||||
if (Object.keys(store.s.accountTokens).length > 0) {
|
||||
login(Object.values(store.s.accountTokens)[0]);
|
||||
} else {
|
||||
signout();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function login(token: AccountWithToken['token'], redirect?: string) {
|
||||
const showing = ref(true);
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), {
|
||||
success: false,
|
||||
showing: showing,
|
||||
}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
|
||||
const me = await fetchAccount(token, undefined, true).catch(reason => {
|
||||
showing.value = false;
|
||||
throw reason;
|
||||
});
|
||||
|
||||
miLocalStorage.setItem('account', JSON.stringify({
|
||||
...me,
|
||||
token,
|
||||
}));
|
||||
|
||||
await addAccount(host, me, token);
|
||||
|
||||
if (redirect) {
|
||||
// 他のタブは再読み込みするだけ
|
||||
reloadChannel.postMessage(null);
|
||||
// このページはredirectで指定された先に移動
|
||||
window.location.href = redirect;
|
||||
return;
|
||||
}
|
||||
|
||||
unisonReload();
|
||||
}
|
||||
|
||||
export async function switchAccount(host: string, id: string) {
|
||||
const token = store.s.accountTokens[host + '/' + id];
|
||||
if (token) {
|
||||
login(token);
|
||||
} else {
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
|
||||
done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
|
||||
store.set('accountTokens', { ...store.s.accountTokens, [host + '/' + res.id]: res.i });
|
||||
login(res.i);
|
||||
},
|
||||
closed: () => {
|
||||
dispose();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function openAccountMenu(opts: {
|
||||
includeCurrentAccount?: boolean;
|
||||
withExtraOperation: boolean;
|
||||
active?: Misskey.entities.User['id'];
|
||||
onChoose?: (account: Misskey.entities.User) => void;
|
||||
}, ev: MouseEvent) {
|
||||
if (!$i) return;
|
||||
|
||||
function createItem(host: string, account: Misskey.entities.User): MenuItem {
|
||||
return {
|
||||
type: 'user' as const,
|
||||
user: account,
|
||||
active: opts.active != null ? opts.active === account.id : false,
|
||||
action: async () => {
|
||||
if (opts.onChoose) {
|
||||
opts.onChoose(account);
|
||||
} else {
|
||||
switchAccount(host, account.id);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
// TODO: $iのホストも比較したいけど通常null
|
||||
const accountItems = (await getAccounts().then(accounts => accounts.filter(x => x.user.id !== $i.id))).map(a => createItem(a.host, a.user));
|
||||
|
||||
if (opts.withExtraOperation) {
|
||||
menuItems.push({
|
||||
type: 'link',
|
||||
text: i18n.ts.profile,
|
||||
to: `/@${$i.username}`,
|
||||
avatar: $i,
|
||||
}, {
|
||||
type: 'divider',
|
||||
});
|
||||
|
||||
if (opts.includeCurrentAccount) {
|
||||
menuItems.push(createItem(host, $i));
|
||||
}
|
||||
|
||||
menuItems.push(...accountItems);
|
||||
|
||||
menuItems.push({
|
||||
type: 'parent',
|
||||
icon: 'ti ti-plus',
|
||||
text: i18n.ts.addAccount,
|
||||
children: [{
|
||||
text: i18n.ts.existingAccount,
|
||||
action: () => {
|
||||
getAccountWithSigninDialog().then(res => {
|
||||
if (res != null) {
|
||||
success();
|
||||
}
|
||||
});
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.createAccount,
|
||||
action: () => {
|
||||
getAccountWithSignupDialog().then(res => {
|
||||
if (res != null) {
|
||||
switchAccount(host, res.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
}],
|
||||
}, {
|
||||
type: 'link',
|
||||
icon: 'ti ti-users',
|
||||
text: i18n.ts.manageAccounts,
|
||||
to: '/settings/accounts',
|
||||
});
|
||||
} else {
|
||||
if (opts.includeCurrentAccount) {
|
||||
menuItems.push(createItem(host, $i));
|
||||
}
|
||||
|
||||
menuItems.push(...accountItems);
|
||||
}
|
||||
|
||||
popupMenu(menuItems, ev.currentTarget ?? ev.target, {
|
||||
align: 'left',
|
||||
});
|
||||
}
|
||||
|
||||
export function getAccountWithSigninDialog(): Promise<{ id: string, token: string } | null> {
|
||||
return new Promise((resolve) => {
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
|
||||
done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
|
||||
const user = await fetchAccount(res.i, res.id, true);
|
||||
await addAccount(host, user, res.i);
|
||||
resolve({ id: res.id, token: res.i });
|
||||
},
|
||||
cancelled: () => {
|
||||
resolve(null);
|
||||
},
|
||||
closed: () => {
|
||||
dispose();
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function getAccountWithSignupDialog(): Promise<{ id: string, token: string } | null> {
|
||||
return new Promise((resolve) => {
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
|
||||
done: async (res: Misskey.entities.SignupResponse) => {
|
||||
const user = JSON.parse(JSON.stringify(res));
|
||||
delete user.token;
|
||||
await addAccount(host, user, res.token);
|
||||
resolve({ id: res.id, token: res.token });
|
||||
},
|
||||
cancelled: () => {
|
||||
resolve(null);
|
||||
},
|
||||
closed: () => {
|
||||
dispose();
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -8,8 +8,8 @@ import * as Misskey from 'misskey-js';
|
|||
import { url, lang } from '@@/js/config.js';
|
||||
import { assertStringAndIsIn } from './common.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { customEmojis } from '@/custom-emojis.js';
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ export function createAiScriptEnv(opts: { storageKey: string, token?: string })
|
|||
// バグがあればundefinedもあり得るため念のため
|
||||
if (typeof token.value !== 'string') throw new Error('invalid token');
|
||||
}
|
||||
const actualToken: string|null = token?.value ?? opts.token ?? null;
|
||||
const actualToken: string | null = token?.value ?? opts.token ?? null;
|
||||
if (param == null) {
|
||||
throw new errors.AiScriptRuntimeError('expected param');
|
||||
}
|
||||
|
|
@ -3,7 +3,8 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { errors, utils, type values } from '@syuilo/aiscript';
|
||||
import { errors, utils } from '@syuilo/aiscript';
|
||||
import type { values } from '@syuilo/aiscript';
|
||||
|
||||
export function assertStringAndIsIn<A extends readonly string[]>(value: values.Value | undefined, expects: A): asserts value is values.VStr & { value: A[number] } {
|
||||
utils.assertString(value);
|
||||
|
|
@ -5,7 +5,8 @@
|
|||
|
||||
import { utils, values } from '@syuilo/aiscript';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { ref, Ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { assertStringAndIsIn } from './common.js';
|
||||
|
||||
107
packages/frontend/src/analytics.ts
Normal file
107
packages/frontend/src/analytics.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { AnalyticsInstance, AnalyticsPlugin } from 'analytics';
|
||||
|
||||
/**
|
||||
* analytics moduleを読み込まなくても動作するようにするためのラッパー
|
||||
*/
|
||||
class AnalyticsProxy implements AnalyticsInstance {
|
||||
private analytics?: AnalyticsInstance;
|
||||
|
||||
constructor(analytics?: AnalyticsInstance) {
|
||||
if (analytics) {
|
||||
this.analytics = analytics;
|
||||
}
|
||||
}
|
||||
|
||||
public setAnalytics(analytics: AnalyticsInstance) {
|
||||
if (this.analytics) {
|
||||
throw new Error('Analytics instance already exists.');
|
||||
}
|
||||
this.analytics = analytics;
|
||||
}
|
||||
|
||||
public identify(...args: Parameters<AnalyticsInstance['identify']>) {
|
||||
return this.analytics?.identify(...args) ?? Promise.resolve();
|
||||
}
|
||||
|
||||
public track(...args: Parameters<AnalyticsInstance['track']>) {
|
||||
return this.analytics?.track(...args) ?? Promise.resolve();
|
||||
}
|
||||
|
||||
public page(...args: Parameters<AnalyticsInstance['page']>) {
|
||||
return this.analytics?.page(...args) ?? Promise.resolve();
|
||||
}
|
||||
|
||||
public user(...args: Parameters<AnalyticsInstance['user']>) {
|
||||
return this.analytics?.user(...args) ?? Promise.resolve();
|
||||
}
|
||||
|
||||
public reset(...args: Parameters<AnalyticsInstance['reset']>) {
|
||||
return this.analytics?.reset(...args) ?? Promise.resolve();
|
||||
}
|
||||
|
||||
public ready(...args: Parameters<AnalyticsInstance['ready']>) {
|
||||
return this.analytics?.ready(...args) ?? function () { void 0; };
|
||||
}
|
||||
|
||||
public on(...args: Parameters<AnalyticsInstance['on']>) {
|
||||
return this.analytics?.on(...args) ?? function () { void 0; };
|
||||
}
|
||||
|
||||
public once(...args: Parameters<AnalyticsInstance['once']>) {
|
||||
return this.analytics?.once(...args) ?? function () { void 0; };
|
||||
}
|
||||
|
||||
public getState(...args: Parameters<AnalyticsInstance['getState']>) {
|
||||
return this.analytics?.getState(...args) ?? Promise.resolve();
|
||||
}
|
||||
|
||||
public get storage() {
|
||||
return this.analytics?.storage ?? {
|
||||
getItem: () => null,
|
||||
setItem: () => void 0,
|
||||
removeItem: () => void 0,
|
||||
};
|
||||
}
|
||||
|
||||
public get plugins() {
|
||||
return this.analytics?.plugins ?? {
|
||||
enable: (p, c) => Promise.resolve(c ? c() : void 0),
|
||||
disable: (p, c) => Promise.resolve(c ? c() : void 0),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const analytics = new AnalyticsProxy();
|
||||
|
||||
export async function initAnalytics(instance: Misskey.entities.MetaDetailed) {
|
||||
// アナリティクスプロバイダに関する設定がひとつもない場合は、アナリティクスモジュールを読み込まない
|
||||
if (!instance.googleAnalyticsMeasurementId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { default: Analytics } = await import('analytics');
|
||||
const plugins: AnalyticsPlugin[] = [];
|
||||
|
||||
// Google Analytics
|
||||
if (instance.googleAnalyticsMeasurementId) {
|
||||
const { default: googleAnalytics } = await import('@analytics/google-analytics');
|
||||
|
||||
plugins.push(googleAnalytics({
|
||||
measurementIds: [instance.googleAnalyticsMeasurementId],
|
||||
debug: _DEV_,
|
||||
}));
|
||||
}
|
||||
|
||||
analytics.setAnalytics(Analytics({
|
||||
app: 'misskey',
|
||||
version: _VERSION_,
|
||||
debug: _DEV_,
|
||||
plugins,
|
||||
}));
|
||||
}
|
||||
|
|
@ -3,27 +3,31 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { computed, watch, version as vueVersion, App } from 'vue';
|
||||
import { computed, watch, version as vueVersion } from 'vue';
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import { version, lang, langsVersion, updateLocale, locale } from '@@/js/config.js';
|
||||
import defaultLightTheme from '@@/themes/l-light.json5';
|
||||
import defaultDarkTheme from '@@/themes/d-green-lime.json5';
|
||||
import type { App } from 'vue';
|
||||
import widgets from '@/widgets/index.js';
|
||||
import directives from '@/directives/index.js';
|
||||
import components from '@/components/index.js';
|
||||
import { applyTheme } from '@/scripts/theme.js';
|
||||
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js';
|
||||
import { applyTheme } from '@/theme.js';
|
||||
import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js';
|
||||
import { updateI18n, i18n } from '@/i18n.js';
|
||||
import { $i, refreshAccount, login } from '@/account.js';
|
||||
import { defaultStore, ColdDeviceStorage } from '@/store.js';
|
||||
import { refreshCurrentAccount, login } from '@/accounts.js';
|
||||
import { store } from '@/store.js';
|
||||
import { fetchInstance, instance } from '@/instance.js';
|
||||
import { deviceKind, updateDeviceKind } from '@/scripts/device-kind.js';
|
||||
import { reloadChannel } from '@/scripts/unison-reload.js';
|
||||
import { getUrlWithoutLoginId } from '@/scripts/login-id.js';
|
||||
import { getAccountFromId } from '@/scripts/get-account-from-id.js';
|
||||
import { deviceKind, updateDeviceKind } from '@/utility/device-kind.js';
|
||||
import { reloadChannel } from '@/utility/unison-reload.js';
|
||||
import { getUrlWithoutLoginId } from '@/utility/login-id.js';
|
||||
import { getAccountFromId } from '@/utility/get-account-from-id.js';
|
||||
import { deckStore } from '@/ui/deck/deck-store.js';
|
||||
import { analytics, initAnalytics } from '@/analytics.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { fetchCustomEmojis } from '@/custom-emojis.js';
|
||||
import { setupRouter } from '@/router/main.js';
|
||||
import { createMainRouter } from '@/router/definition.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { $i } from '@/i.js';
|
||||
|
||||
export async function common(createVue: () => App<Element>) {
|
||||
console.info(`Sharkey v${version}`);
|
||||
|
|
@ -33,11 +37,6 @@ export async function common(createVue: () => App<Element>) {
|
|||
|
||||
console.info(`vue ${vueVersion}`);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(window as any).$i = $i;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(window as any).$store = defaultStore;
|
||||
|
||||
window.addEventListener('error', event => {
|
||||
console.error(event);
|
||||
/*
|
||||
|
|
@ -97,32 +96,32 @@ export async function common(createVue: () => App<Element>) {
|
|||
//#endregion
|
||||
|
||||
// タッチデバイスでCSSの:hoverを機能させる
|
||||
document.addEventListener('touchend', () => {}, { passive: true });
|
||||
window.document.addEventListener('touchend', () => {}, { passive: true });
|
||||
|
||||
// URLに#pswpを含む場合は取り除く
|
||||
if (location.hash === '#pswp') {
|
||||
history.replaceState(null, '', location.href.replace('#pswp', ''));
|
||||
if (window.location.hash === '#pswp') {
|
||||
window.history.replaceState(null, '', window.location.href.replace('#pswp', ''));
|
||||
}
|
||||
|
||||
// 一斉リロード
|
||||
reloadChannel.addEventListener('message', path => {
|
||||
if (path !== null) location.href = path;
|
||||
else location.reload();
|
||||
if (path !== null) window.location.href = path;
|
||||
else window.location.reload();
|
||||
});
|
||||
|
||||
// If mobile, insert the viewport meta tag
|
||||
if (['smartphone', 'tablet'].includes(deviceKind)) {
|
||||
const viewport = document.getElementsByName('viewport').item(0);
|
||||
const viewport = window.document.getElementsByName('viewport').item(0);
|
||||
viewport.setAttribute('content',
|
||||
`${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`);
|
||||
}
|
||||
|
||||
//#region Set lang attr
|
||||
const html = document.documentElement;
|
||||
const html = window.document.documentElement;
|
||||
html.setAttribute('lang', lang);
|
||||
//#endregion
|
||||
|
||||
await defaultStore.ready;
|
||||
await store.ready;
|
||||
await deckStore.ready;
|
||||
|
||||
const fetchInstanceMetaPromise = fetchInstance();
|
||||
|
|
@ -132,11 +131,11 @@ export async function common(createVue: () => App<Element>) {
|
|||
});
|
||||
|
||||
//#region loginId
|
||||
const params = new URLSearchParams(location.search);
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const loginId = params.get('loginId');
|
||||
|
||||
if (loginId) {
|
||||
const target = getUrlWithoutLoginId(location.href);
|
||||
const target = getUrlWithoutLoginId(window.location.href);
|
||||
|
||||
if (!$i || $i.id !== loginId) {
|
||||
const account = await getAccountFromId(loginId);
|
||||
|
|
@ -145,71 +144,78 @@ export async function common(createVue: () => App<Element>) {
|
|||
}
|
||||
}
|
||||
|
||||
history.replaceState({ misskey: 'loginId' }, '', target);
|
||||
window.history.replaceState({ misskey: 'loginId' }, '', target);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
|
||||
watch(defaultStore.reactiveState.darkMode, (darkMode) => {
|
||||
applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'));
|
||||
watch(store.r.darkMode, (darkMode) => {
|
||||
applyTheme(darkMode
|
||||
? (prefer.s.darkTheme ?? defaultDarkTheme)
|
||||
: (prefer.s.lightTheme ?? defaultLightTheme),
|
||||
);
|
||||
}, { immediate: miLocalStorage.getItem('theme') == null });
|
||||
|
||||
document.documentElement.dataset.colorScheme = defaultStore.state.darkMode ? 'dark' : 'light';
|
||||
window.document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light';
|
||||
|
||||
const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
|
||||
const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
|
||||
const darkTheme = prefer.model('darkTheme');
|
||||
const lightTheme = prefer.model('lightTheme');
|
||||
|
||||
watch(darkTheme, (theme) => {
|
||||
if (defaultStore.state.darkMode) {
|
||||
applyTheme(theme);
|
||||
if (store.s.darkMode) {
|
||||
applyTheme(theme ?? defaultDarkTheme);
|
||||
}
|
||||
});
|
||||
|
||||
watch(lightTheme, (theme) => {
|
||||
if (!defaultStore.state.darkMode) {
|
||||
applyTheme(theme);
|
||||
if (!store.s.darkMode) {
|
||||
applyTheme(theme ?? defaultLightTheme);
|
||||
}
|
||||
});
|
||||
|
||||
//#region Sync dark mode
|
||||
if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
|
||||
defaultStore.set('darkMode', isDeviceDarkmode());
|
||||
if (prefer.s.syncDeviceDarkMode) {
|
||||
store.set('darkMode', isDeviceDarkmode());
|
||||
}
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => {
|
||||
if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
|
||||
defaultStore.set('darkMode', mql.matches);
|
||||
if (prefer.s.syncDeviceDarkMode) {
|
||||
store.set('darkMode', mql.matches);
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
|
||||
if (prefer.s.darkTheme && store.s.darkMode) {
|
||||
if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme);
|
||||
} else if (prefer.s.lightTheme && !store.s.darkMode) {
|
||||
if (miLocalStorage.getItem('themeId') !== prefer.s.lightTheme.id) applyTheme(prefer.s.lightTheme);
|
||||
}
|
||||
|
||||
fetchInstanceMetaPromise.then(() => {
|
||||
if (defaultStore.state.themeInitial) {
|
||||
if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme));
|
||||
if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme));
|
||||
defaultStore.set('themeInitial', false);
|
||||
}
|
||||
// TODO: instance.defaultLightTheme/instance.defaultDarkThemeが不正な形式だった場合のケア
|
||||
if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.commit('lightTheme', JSON.parse(instance.defaultLightTheme));
|
||||
if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.commit('darkTheme', JSON.parse(instance.defaultDarkTheme));
|
||||
});
|
||||
|
||||
watch(defaultStore.reactiveState.overridedDeviceKind, (kind) => {
|
||||
watch(prefer.r.overridedDeviceKind, (kind) => {
|
||||
updateDeviceKind(kind);
|
||||
}, { immediate: true });
|
||||
|
||||
watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
|
||||
document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
|
||||
watch(prefer.r.useBlurEffectForModal, v => {
|
||||
window.document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
|
||||
}, { immediate: true });
|
||||
|
||||
watch(defaultStore.reactiveState.useBlurEffect, v => {
|
||||
watch(prefer.r.useBlurEffect, v => {
|
||||
if (v) {
|
||||
document.documentElement.style.removeProperty('--MI-blur');
|
||||
window.document.documentElement.style.removeProperty('--MI-blur');
|
||||
} else {
|
||||
document.documentElement.style.setProperty('--MI-blur', 'none');
|
||||
window.document.documentElement.style.setProperty('--MI-blur', 'none');
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// Keep screen on
|
||||
const onVisibilityChange = () => document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
const onVisibilityChange = () => window.document.addEventListener('visibilitychange', () => {
|
||||
if (window.document.visibilityState === 'visible') {
|
||||
try {
|
||||
navigator.wakeLock.request('screen');
|
||||
} catch (err) {
|
||||
|
|
@ -217,13 +223,13 @@ export async function common(createVue: () => App<Element>) {
|
|||
}
|
||||
}
|
||||
});
|
||||
if (defaultStore.state.keepScreenOn && 'wakeLock' in navigator) {
|
||||
if (prefer.s.keepScreenOn && 'wakeLock' in navigator) {
|
||||
navigator.wakeLock.request('screen')
|
||||
.then(onVisibilityChange)
|
||||
.catch(() => {
|
||||
// On WebKit-based browsers, user activation is required to send wake lock request
|
||||
// https://webkit.org/blog/13862/the-user-activation-api/
|
||||
document.addEventListener(
|
||||
window.document.addEventListener(
|
||||
'click',
|
||||
() => navigator.wakeLock.request('screen').then(onVisibilityChange),
|
||||
{ once: true },
|
||||
|
|
@ -231,13 +237,17 @@ export async function common(createVue: () => App<Element>) {
|
|||
});
|
||||
}
|
||||
|
||||
if (prefer.s.makeEveryTextElementsSelectable) {
|
||||
window.document.documentElement.classList.add('forceSelectableAll');
|
||||
}
|
||||
|
||||
//#region Fetch user
|
||||
if ($i && $i.token) {
|
||||
if (_DEV_) {
|
||||
console.log('account cache found. refreshing...');
|
||||
}
|
||||
|
||||
refreshAccount();
|
||||
refreshCurrentAccount();
|
||||
}
|
||||
//#endregion
|
||||
|
||||
|
|
@ -245,9 +255,20 @@ export async function common(createVue: () => App<Element>) {
|
|||
await fetchCustomEmojis();
|
||||
} catch (err) { /* empty */ }
|
||||
|
||||
const app = createVue();
|
||||
// analytics
|
||||
fetchInstanceMetaPromise.then(async () => {
|
||||
await initAnalytics(instance);
|
||||
|
||||
setupRouter(app, createMainRouter);
|
||||
if ($i) {
|
||||
analytics.identify($i.id);
|
||||
}
|
||||
|
||||
analytics.page({
|
||||
path: window.location.pathname,
|
||||
});
|
||||
});
|
||||
|
||||
const app = createVue();
|
||||
|
||||
if (_DEV_) {
|
||||
app.config.performance = true;
|
||||
|
|
@ -262,16 +283,16 @@ export async function common(createVue: () => App<Element>) {
|
|||
const rootEl = ((): HTMLElement => {
|
||||
const MISSKEY_MOUNT_DIV_ID = 'sharkey_app';
|
||||
|
||||
const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID);
|
||||
const currentRoot = window.document.getElementById(MISSKEY_MOUNT_DIV_ID);
|
||||
|
||||
if (currentRoot) {
|
||||
console.warn('multiple import detected');
|
||||
return currentRoot;
|
||||
}
|
||||
|
||||
const root = document.createElement('div');
|
||||
const root = window.document.createElement('div');
|
||||
root.id = MISSKEY_MOUNT_DIV_ID;
|
||||
document.body.appendChild(root);
|
||||
window.document.body.appendChild(root);
|
||||
return root;
|
||||
})();
|
||||
|
||||
|
|
@ -284,34 +305,37 @@ export async function common(createVue: () => App<Element>) {
|
|||
removeSplash();
|
||||
|
||||
//#region Self-XSS 対策メッセージ
|
||||
console.log(
|
||||
`%c${i18n.ts._selfXssPrevention.warning}`,
|
||||
'color: #f00; background-color: #ff0; font-size: 36px; padding: 4px;',
|
||||
);
|
||||
console.log(
|
||||
`%c${i18n.ts._selfXssPrevention.title}`,
|
||||
'color: #f00; font-weight: 900; font-family: "Hiragino Sans W9", "Hiragino Kaku Gothic ProN", sans-serif; font-size: 24px;',
|
||||
);
|
||||
console.log(
|
||||
`%c${i18n.ts._selfXssPrevention.description1}`,
|
||||
'font-size: 16px; font-weight: 700;',
|
||||
);
|
||||
console.log(
|
||||
`%c${i18n.ts._selfXssPrevention.description2}`,
|
||||
'font-size: 16px;',
|
||||
'font-size: 20px; font-weight: 700; color: #f00;',
|
||||
);
|
||||
console.log(i18n.tsx._selfXssPrevention.description3({ link: 'https://misskey-hub.net/docs/for-users/resources/self-xss/' }));
|
||||
if (!_DEV_) {
|
||||
console.log(
|
||||
`%c${i18n.ts._selfXssPrevention.warning}`,
|
||||
'color: #f00; background-color: #ff0; font-size: 36px; padding: 4px;',
|
||||
);
|
||||
console.log(
|
||||
`%c${i18n.ts._selfXssPrevention.title}`,
|
||||
'color: #f00; font-weight: 900; font-family: "Hiragino Sans W9", "Hiragino Kaku Gothic ProN", sans-serif; font-size: 24px;',
|
||||
);
|
||||
console.log(
|
||||
`%c${i18n.ts._selfXssPrevention.description1}`,
|
||||
'font-size: 16px; font-weight: 700;',
|
||||
);
|
||||
console.log(
|
||||
`%c${i18n.ts._selfXssPrevention.description2}`,
|
||||
'font-size: 16px;',
|
||||
'font-size: 20px; font-weight: 700; color: #f00;',
|
||||
);
|
||||
console.log(i18n.tsx._selfXssPrevention.description3({ link: 'https://misskey-hub.net/docs/for-users/resources/self-xss/' }));
|
||||
}
|
||||
//#endregion
|
||||
|
||||
return {
|
||||
isClientUpdated,
|
||||
lastVersion,
|
||||
app,
|
||||
};
|
||||
}
|
||||
|
||||
function removeSplash() {
|
||||
const splash = document.getElementById('splash');
|
||||
const splash = window.document.getElementById('splash');
|
||||
if (splash) {
|
||||
splash.style.opacity = '0';
|
||||
splash.style.pointerEvents = 'none';
|
||||
|
|
|
|||
|
|
@ -5,36 +5,43 @@
|
|||
|
||||
import { createApp, defineAsyncComponent, markRaw } from 'vue';
|
||||
import { ui } from '@@/js/config.js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import { common } from './common.js';
|
||||
import type * as Misskey from 'misskey-js';
|
||||
import type { Component } from 'vue';
|
||||
import type { Keymap } from '@/utility/hotkey.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { alert, confirm, popup, post, toast } from '@/os.js';
|
||||
import { alert, confirm, popup, post } from '@/os.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import { $i, signout, updateAccountPartial } from '@/account.js';
|
||||
import * as sound from '@/utility/sound.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { ColdDeviceStorage, defaultStore } from '@/store.js';
|
||||
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
||||
import { store } from '@/store.js';
|
||||
import { reactionPicker } from '@/utility/reaction-picker.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
|
||||
import { initializeSw } from '@/scripts/initialize-sw.js';
|
||||
import { claimAchievement, claimedAchievements } from '@/utility/achievements.js';
|
||||
import { initializeSw } from '@/utility/initialize-sw.js';
|
||||
import { deckStore } from '@/ui/deck/deck-store.js';
|
||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
||||
import { mainRouter } from '@/router/main.js';
|
||||
import { setFavIconDot } from '@/scripts/favicon-dot.js';
|
||||
import { type Keymap, makeHotkey } from '@/scripts/hotkey.js';
|
||||
import { emojiPicker } from '@/utility/emoji-picker.js';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { setFavIconDot } from '@/utility/favicon-dot.js';
|
||||
import { type Keymap, makeHotkey } from '@/utility/hotkey.js';
|
||||
import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { launchPlugins } from '@/plugin.js';
|
||||
import { updateCurrentAccountPartial } from '@/accounts.js';
|
||||
import { signout } from '@/signout.js';
|
||||
import { migrateOldSettings } from '@/pref-migrate.js';
|
||||
|
||||
export async function mainBoot() {
|
||||
const { isClientUpdated } = await common(() => {
|
||||
const { isClientUpdated, lastVersion } = await common(() => {
|
||||
let uiStyle = ui;
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
if (!$i) uiStyle = 'visitor';
|
||||
|
||||
if (searchParams.has('zen')) uiStyle = 'zen';
|
||||
if (uiStyle === 'deck' && deckStore.state.useSimpleUiForNonRootPages && location.pathname !== '/') uiStyle = 'zen';
|
||||
if (uiStyle === 'deck' && prefer.s['deck.useSimpleUiForNonRootPages'] && window.location.pathname !== '/') uiStyle = 'zen';
|
||||
|
||||
if (searchParams.has('ui')) uiStyle = searchParams.get('ui');
|
||||
|
||||
|
|
@ -67,13 +74,23 @@ export async function mainBoot() {
|
|||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
|
||||
// prefereces migration
|
||||
// TODO: そのうち消す
|
||||
if (lastVersion && (compareVersions('2025.3.2-alpha.0', lastVersion) === 1)) {
|
||||
console.log('Preferences migration');
|
||||
|
||||
migrateOldSettings();
|
||||
}
|
||||
}
|
||||
|
||||
const stream = useStream();
|
||||
|
||||
let reloadDialogShowing = false;
|
||||
stream.on('_disconnected_', async () => {
|
||||
if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
|
||||
if (prefer.s.serverDisconnectedBehavior === 'reload') {
|
||||
window.location.reload();
|
||||
} else if (prefer.s.serverDisconnectedBehavior === 'dialog') {
|
||||
if (reloadDialogShowing) return;
|
||||
reloadDialogShowing = true;
|
||||
const { canceled } = await confirm({
|
||||
|
|
@ -83,7 +100,7 @@ export async function mainBoot() {
|
|||
});
|
||||
reloadDialogShowing = false;
|
||||
if (!canceled) {
|
||||
location.reload();
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -100,30 +117,24 @@ export async function mainBoot() {
|
|||
removeCustomEmojis(emojiData.emojis);
|
||||
});
|
||||
|
||||
for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
|
||||
import('@/plugin.js').then(async ({ install }) => {
|
||||
// Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740
|
||||
await new Promise(r => setTimeout(r, 0));
|
||||
install(plugin);
|
||||
});
|
||||
}
|
||||
launchPlugins();
|
||||
|
||||
try {
|
||||
if (defaultStore.state.enableSeasonalScreenEffect) {
|
||||
if (prefer.s.enableSeasonalScreenEffect) {
|
||||
const month = new Date().getMonth() + 1;
|
||||
if (defaultStore.state.hemisphere === 'S') {
|
||||
if (prefer.s.hemisphere === 'S') {
|
||||
// ▼南半球
|
||||
if (month === 7 || month === 8) {
|
||||
const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
|
||||
const SnowfallEffect = (await import('@/utility/snowfall-effect.js')).SnowfallEffect;
|
||||
new SnowfallEffect({}).render();
|
||||
}
|
||||
} else {
|
||||
// ▼北半球
|
||||
if (month === 12 || month === 1) {
|
||||
const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
|
||||
const SnowfallEffect = (await import('@/utility/snowfall-effect.js')).SnowfallEffect;
|
||||
new SnowfallEffect({}).render();
|
||||
} else if (month === 3 || month === 4) {
|
||||
const SakuraEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
|
||||
const SakuraEffect = (await import('@/utility/snowfall-effect.js')).SnowfallEffect;
|
||||
new SakuraEffect({
|
||||
sakura: true,
|
||||
}).render();
|
||||
|
|
@ -136,8 +147,8 @@ export async function mainBoot() {
|
|||
}
|
||||
|
||||
if ($i) {
|
||||
defaultStore.loaded.then(() => {
|
||||
if (defaultStore.state.accountSetupWizard !== -1) {
|
||||
store.loaded.then(async () => {
|
||||
if (store.s.accountSetupWizard !== -1) {
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
|
|
@ -152,7 +163,7 @@ export async function mainBoot() {
|
|||
});
|
||||
}
|
||||
|
||||
function onAnnouncementCreated (ev: { announcement: Misskey.entities.Announcement }) {
|
||||
function onAnnouncementCreated(ev: { announcement: Misskey.entities.Announcement }) {
|
||||
const announcement = ev.announcement;
|
||||
if (announcement.display === 'dialog') {
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), {
|
||||
|
|
@ -260,7 +271,7 @@ export async function mainBoot() {
|
|||
let lastVisibilityChangedAt = Date.now();
|
||||
|
||||
function claimPlainLucky() {
|
||||
if (document.visibilityState !== 'visible') {
|
||||
if (window.document.visibilityState !== 'visible') {
|
||||
if (justPlainLuckyTimer != null) window.clearTimeout(justPlainLuckyTimer);
|
||||
return;
|
||||
}
|
||||
|
|
@ -275,7 +286,7 @@ export async function mainBoot() {
|
|||
window.addEventListener('visibilitychange', () => {
|
||||
const now = Date.now();
|
||||
|
||||
if (document.visibilityState === 'visible') {
|
||||
if (window.document.visibilityState === 'visible') {
|
||||
// タブを高速で切り替えたら取得処理が何度も走るのを防ぐ
|
||||
if ((now - lastVisibilityChangedAt) < 1000 * 10) {
|
||||
justPlainLuckyTimer = window.setTimeout(claimPlainLucky, 1000 * 10);
|
||||
|
|
@ -320,7 +331,7 @@ export async function mainBoot() {
|
|||
|
||||
const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
|
||||
const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
|
||||
if (neverShowDonationInfo !== 'true' && (createdAt.getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) {
|
||||
if (neverShowDonationInfo !== 'true' && (createdAt.getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !window.location.pathname.startsWith('/miauth')) {
|
||||
if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) {
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {
|
||||
closed: () => dispose(),
|
||||
|
|
@ -354,13 +365,13 @@ export async function mainBoot() {
|
|||
|
||||
// 自分の情報が更新されたとき
|
||||
main.on('meUpdated', i => {
|
||||
updateAccountPartial(i);
|
||||
updateCurrentAccountPartial(i);
|
||||
});
|
||||
|
||||
main.on('readAllNotifications', () => {
|
||||
setFavIconDot(false);
|
||||
|
||||
updateAccountPartial({
|
||||
updateCurrentAccountPartial({
|
||||
hasUnreadNotification: false,
|
||||
unreadNotificationsCount: 0,
|
||||
});
|
||||
|
|
@ -370,39 +381,24 @@ export async function mainBoot() {
|
|||
attemptShowNotificationDot();
|
||||
|
||||
const unreadNotificationsCount = ($i?.unreadNotificationsCount ?? 0) + 1;
|
||||
updateAccountPartial({
|
||||
updateCurrentAccountPartial({
|
||||
hasUnreadNotification: true,
|
||||
unreadNotificationsCount,
|
||||
});
|
||||
});
|
||||
|
||||
main.on('unreadMention', () => {
|
||||
updateAccountPartial({ hasUnreadMentions: true });
|
||||
});
|
||||
|
||||
main.on('readAllUnreadMentions', () => {
|
||||
updateAccountPartial({ hasUnreadMentions: false });
|
||||
});
|
||||
|
||||
main.on('unreadSpecifiedNote', () => {
|
||||
updateAccountPartial({ hasUnreadSpecifiedNotes: true });
|
||||
});
|
||||
|
||||
main.on('readAllUnreadSpecifiedNotes', () => {
|
||||
updateAccountPartial({ hasUnreadSpecifiedNotes: false });
|
||||
});
|
||||
|
||||
main.on('readAllAntennas', () => {
|
||||
updateAccountPartial({ hasUnreadAntenna: false });
|
||||
});
|
||||
|
||||
main.on('unreadAntenna', () => {
|
||||
updateAccountPartial({ hasUnreadAntenna: true });
|
||||
updateCurrentAccountPartial({ hasUnreadAntenna: true });
|
||||
sound.playMisskeySfx('antenna');
|
||||
});
|
||||
|
||||
main.on('newChatMessage', () => {
|
||||
updateCurrentAccountPartial({ hasUnreadChatMessages: true });
|
||||
sound.playMisskeySfx('chat');
|
||||
});
|
||||
|
||||
main.on('readAllAnnouncements', () => {
|
||||
updateAccountPartial({ hasUnreadAnnouncement: false });
|
||||
updateCurrentAccountPartial({ hasUnreadAnnouncement: false });
|
||||
});
|
||||
|
||||
// 個人宛てお知らせが発行されたとき
|
||||
|
|
@ -422,13 +418,13 @@ export async function mainBoot() {
|
|||
post();
|
||||
},
|
||||
'd': () => {
|
||||
defaultStore.set('darkMode', !defaultStore.state.darkMode);
|
||||
store.set('darkMode', !store.s.darkMode);
|
||||
},
|
||||
's': () => {
|
||||
mainRouter.push('/search');
|
||||
},
|
||||
} as const satisfies Keymap;
|
||||
document.addEventListener('keydown', makeHotkey(keymap), { passive: false });
|
||||
window.document.addEventListener('keydown', makeHotkey(keymap), { passive: false });
|
||||
|
||||
initializeSw();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { createApp, defineAsyncComponent } from 'vue';
|
||||
import { common } from './common.js';
|
||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
||||
import { emojiPicker } from '@/utility/emoji-picker.js';
|
||||
|
||||
export async function subBoot() {
|
||||
const { isClientUpdated } = await common(() => createApp(
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
*/
|
||||
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { Cache } from '@/scripts/cache.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { Cache } from '@/utility/cache.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
|
||||
export const clipsCache = new Cache<Misskey.entities.Clip[]>(1000 * 60 * 30, () => misskeyApi('clips/list'));
|
||||
export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/list'));
|
||||
|
|
|
|||
|
|
@ -88,9 +88,9 @@ import { i18n } from '@/i18n.js';
|
|||
import { dateString } from '@/filters/date.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import RouterView from '@/components/global/RouterView.vue';
|
||||
import { useRouterFactory } from '@/router/supplier';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||
import { createRouter } from '@/router.js';
|
||||
|
||||
const props = defineProps<{
|
||||
report: Misskey.entities.AdminAbuseUserReportsResponse[number];
|
||||
|
|
@ -100,10 +100,9 @@ const emit = defineEmits<{
|
|||
(ev: 'resolved', reportId: string): void;
|
||||
}>();
|
||||
|
||||
const routerFactory = useRouterFactory();
|
||||
const targetRouter = routerFactory(`/admin/user/${props.report.targetUserId}`);
|
||||
const targetRouter = createRouter(`/admin/user/${props.report.targetUserId}`);
|
||||
targetRouter.init();
|
||||
const reporterRouter = routerFactory(`/admin/user/${props.report.reporterId}`);
|
||||
const reporterRouter = createRouter(`/admin/user/${props.report.reporterId}`);
|
||||
reporterRouter.init();
|
||||
|
||||
const moderationNote = ref(props.report.moderationNote ?? '');
|
||||
|
|
@ -135,7 +134,7 @@ function forward() {
|
|||
|
||||
function showMenu(ev: MouseEvent) {
|
||||
os.popupMenu([{
|
||||
icon: 'ti ti-id',
|
||||
icon: 'ti ti-hash',
|
||||
text: 'Copy ID',
|
||||
action: () => {
|
||||
copyToClipboard(props.report.id);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { userDetailed } from '../../.storybook/fakes.js';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, shallowRef } from 'vue';
|
||||
import { ref, useTemplateRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
|
|
@ -47,7 +47,7 @@ const emit = defineEmits<{
|
|||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const uiWindow = shallowRef<InstanceType<typeof MkWindow>>();
|
||||
const uiWindow = useTemplateRef('uiWindow');
|
||||
const comment = ref(props.initialComment ?? '');
|
||||
|
||||
function send() {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import { userDetailed } from '../../.storybook/fakes.js';
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import * as Misskey from 'misskey-js';
|
|||
import MkMention from './MkMention.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { host as localHost } from '@@/js/config.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
|
||||
const user = ref<Misskey.entities.UserLite>();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@
|
|||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { userDetailed } from '../../.storybook/fakes.js';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import MkAchievements from './MkAchievements.vue';
|
||||
import { ACHIEVEMENT_TYPES } from '@/scripts/achievements.js';
|
||||
import { ACHIEVEMENT_TYPES } from '@/utility/achievements.js';
|
||||
export const Empty = {
|
||||
render(args) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -55,9 +55,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import * as Misskey from 'misskey-js';
|
||||
import { onMounted, ref, computed } from 'vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/scripts/achievements.js';
|
||||
import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/utility/achievements.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
user: Misskey.entities.User;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import isChromatic from 'chromatic/isChromatic';
|
||||
import MkAnalogClock from './MkAnalogClock.vue';
|
||||
export const Default = {
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { defaultIdlingRenderScheduler } from '@/scripts/idle-render.js';
|
||||
import { defaultIdlingRenderScheduler } from '@/utility/idle-render.js';
|
||||
|
||||
// https://stackoverflow.com/questions/1878907/how-can-i-find-the-difference-between-two-angles
|
||||
const angleDiff = (a: number, b: number) => {
|
||||
|
|
@ -192,7 +192,7 @@ function tick() {
|
|||
tick();
|
||||
|
||||
function calcColors() {
|
||||
const computedStyle = getComputedStyle(document.documentElement);
|
||||
const computedStyle = getComputedStyle(window.document.documentElement);
|
||||
const dark = tinycolor(computedStyle.getPropertyValue('--MI_THEME-bg')).isDark();
|
||||
const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
|
||||
majorGraduationColor.value = dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<canvas ref="canvasEl" style="width: 100%; height: 100%; pointer-events: none;"></canvas>
|
||||
<canvas ref="canvasEl" style="display: block; width: 100%; height: 100%; pointer-events: none;"></canvas>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, shallowRef } from 'vue';
|
||||
import { onMounted, onUnmounted, useTemplateRef } from 'vue';
|
||||
import isChromatic from 'chromatic/isChromatic';
|
||||
|
||||
const canvasEl = shallowRef<HTMLCanvasElement>();
|
||||
const canvasEl = useTemplateRef('canvasEl');
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
scale?: number;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import MkAnnouncementDialog from './MkAnnouncementDialog.vue';
|
||||
|
|
|
|||
|
|
@ -22,22 +22,23 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, shallowRef } from 'vue';
|
||||
import { onMounted, useTemplateRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import MkModal from '@/components/MkModal.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i, updateAccountPartial } from '@/account.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { updateCurrentAccountPartial } from '@/accounts.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
announcement: Misskey.entities.Announcement;
|
||||
}>(), {
|
||||
});
|
||||
|
||||
const rootEl = shallowRef<HTMLDivElement>();
|
||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
const modal = useTemplateRef('modal');
|
||||
|
||||
async function ok() {
|
||||
if (props.announcement.needConfirmationToRead) {
|
||||
|
|
@ -51,7 +52,7 @@ async function ok() {
|
|||
|
||||
modal.value?.close();
|
||||
misskeyApi('i/read-announcement', { announcementId: props.announcement.id });
|
||||
updateAccountPartial({
|
||||
updateCurrentAccountPartial({
|
||||
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import MkAntennaEditor from './MkAntennaEditor.vue';
|
||||
|
|
|
|||
|
|
@ -59,10 +59,10 @@ import MkTextarea from '@/components/MkTextarea.vue';
|
|||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { deepMerge } from '@/scripts/merge.js';
|
||||
import type { DeepPartial } from '@/scripts/merge.js';
|
||||
import { deepMerge } from '@/utility/merge.js';
|
||||
import type { DeepPartial } from '@/utility/merge.js';
|
||||
|
||||
type PartialAllowedAntenna = Omit<Misskey.entities.Antenna, 'id' | 'createdAt' | 'updatedAt'> & {
|
||||
id?: string;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import MkAntennaEditorDialog from './MkAntennaEditorDialog.vue';
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef } from 'vue';
|
||||
import { useTemplateRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import XAntennaEditor from '@/components/MkAntennaEditor.vue';
|
||||
|
|
@ -40,7 +40,7 @@ const emit = defineEmits<{
|
|||
(ev: 'closed'): void,
|
||||
}>();
|
||||
|
||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
const dialog = useTemplateRef('dialog');
|
||||
|
||||
function onAntennaCreated(newAntenna: Misskey.entities.Antenna) {
|
||||
emit('created', newAntenna);
|
||||
|
|
|
|||
|
|
@ -63,14 +63,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Ref, ref, computed } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import * as os from '@/os.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/scripts/aiscript/ui.js';
|
||||
import type { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/aiscript/ui.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkPostForm from '@/components/MkPostForm.vue';
|
||||
|
||||
|
|
|
|||
|
|
@ -117,14 +117,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
||||
import { $i, getAccounts, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/account.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { getAccounts, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/accounts.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { getProxiedImageUrl } from '@/utility/media-proxy.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
|
||||
const props = defineProps<{
|
||||
name?: string;
|
||||
|
|
@ -158,7 +157,7 @@ async function init() {
|
|||
|
||||
const accounts = await getAccounts();
|
||||
|
||||
const accountIdsToFetch = accounts.map(a => a.id).filter(id => !users.value.has(id));
|
||||
const accountIdsToFetch = accounts.map(a => a.user.id).filter(id => !users.value.has(id));
|
||||
|
||||
if (accountIdsToFetch.length > 0) {
|
||||
const usersRes = await misskeyApi('users/show', {
|
||||
|
|
@ -170,7 +169,7 @@ async function init() {
|
|||
|
||||
users.value.set(user.id, {
|
||||
...user,
|
||||
token: accounts.find(a => a.id === user.id)!.token,
|
||||
token: accounts.find(a => a.user.id === user.id)!.token,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { userDetailed } from '../../.storybook/fakes.js';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import MkAutocomplete from './MkAutocomplete.vue';
|
||||
import MkInput from './MkInput.vue';
|
||||
import { tick } from '@/scripts/test-utils.js';
|
||||
import { tick } from '@/utility/test-utils.js';
|
||||
const common = {
|
||||
render(args) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -44,26 +44,28 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { markRaw, ref, shallowRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
||||
import { markRaw, ref, useTemplateRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import { emojilist, getEmojiName } from '@@/js/emojilist.js';
|
||||
import contains from '@/scripts/contains.js';
|
||||
import { char2twemojiFilePath, char2fluentEmojiFilePath, char2tossfaceFilePath } from '@@/js/emoji-base.js';
|
||||
import { MFM_TAGS, MFM_PARAMS } from '@@/js/const.js';
|
||||
import type { EmojiDef } from '@/utility/search-emoji.js';
|
||||
import contains from '@/utility/contains.js';
|
||||
import { acct } from '@/filters/user.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { store } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { customEmojis } from '@/custom-emojis.js';
|
||||
import { MFM_TAGS, MFM_PARAMS } from '@@/js/const.js';
|
||||
import { searchEmoji, EmojiDef } from '@/scripts/search-emoji.js';
|
||||
import { searchEmoji } from '@/utility/search-emoji.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const lib = emojilist.filter(x => x.category !== 'flags');
|
||||
|
||||
const emojiDb = computed(() => {
|
||||
//#region Unicode Emoji
|
||||
const char2path = defaultStore.reactiveState.emojiStyle.value === 'twemoji' ? char2twemojiFilePath : defaultStore.reactiveState.emojiStyle.value === 'tossface' ? char2tossfaceFilePath : char2fluentEmojiFilePath;
|
||||
const char2path = prefer.r.emojiStyle.value === 'twemoji' ? char2twemojiFilePath : prefer.r.emojiStyle.value === 'tossface' ? char2tossfaceFilePath : char2fluentEmojiFilePath;
|
||||
|
||||
const unicodeEmojiDB: EmojiDef[] = lib.map(x => ({
|
||||
emoji: x.char,
|
||||
|
|
@ -71,7 +73,7 @@ const emojiDb = computed(() => {
|
|||
url: char2path(x.char),
|
||||
}));
|
||||
|
||||
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
|
||||
for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) {
|
||||
for (const [emoji, keywords] of Object.entries(index)) {
|
||||
for (const k of keywords) {
|
||||
unicodeEmojiDB.push({
|
||||
|
|
@ -137,7 +139,7 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
const suggests = ref<Element>();
|
||||
const rootEl = shallowRef<HTMLDivElement>();
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
|
||||
const fetching = ref(true);
|
||||
const users = ref<any[]>([]);
|
||||
|
|
@ -153,10 +155,10 @@ function complete(type: string, value: any) {
|
|||
emit('done', { type, value });
|
||||
emit('closed');
|
||||
if (type === 'emoji') {
|
||||
let recents = defaultStore.state.recentlyUsedEmojis;
|
||||
let recents = store.s.recentlyUsedEmojis;
|
||||
recents = recents.filter((emoji: any) => emoji !== value);
|
||||
recents.unshift(value);
|
||||
defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
|
||||
store.set('recentlyUsedEmojis', recents.splice(0, 32));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -197,8 +199,10 @@ function exec() {
|
|||
users.value = JSON.parse(cache);
|
||||
fetching.value = false;
|
||||
} else {
|
||||
const [username, host] = props.q.toString().split('@');
|
||||
misskeyApi('users/search-by-username-and-host', {
|
||||
username: props.q,
|
||||
username: username,
|
||||
host: host,
|
||||
limit: 10,
|
||||
detail: false,
|
||||
}).then(searchedUsers => {
|
||||
|
|
@ -234,7 +238,7 @@ function exec() {
|
|||
} else if (props.type === 'emoji') {
|
||||
if (!props.q || props.q === '') {
|
||||
// 最近使った絵文字をサジェスト
|
||||
emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
|
||||
emojis.value = store.s.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -355,7 +359,7 @@ onMounted(() => {
|
|||
|
||||
props.textarea.addEventListener('keydown', onKeydown);
|
||||
|
||||
document.body.addEventListener('mousedown', onMousedown);
|
||||
window.document.body.addEventListener('mousedown', onMousedown);
|
||||
|
||||
nextTick(() => {
|
||||
exec();
|
||||
|
|
@ -371,7 +375,7 @@ onMounted(() => {
|
|||
onBeforeUnmount(() => {
|
||||
props.textarea.removeEventListener('keydown', onKeydown);
|
||||
|
||||
document.body.removeEventListener('mousedown', onMousedown);
|
||||
window.document.body.removeEventListener('mousedown', onMousedown);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
@ -407,7 +411,7 @@ onBeforeUnmount(() => {
|
|||
text-overflow: ellipsis;
|
||||
|
||||
&:hover {
|
||||
background: var(--MI_THEME-X3);
|
||||
background: light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05));
|
||||
}
|
||||
|
||||
&[data-selected='true'] {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { userDetailed } from '../../.storybook/fakes.js';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
userIds: string[];
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable import/no-default-export */
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import MkButton from './MkButton.vue';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<button
|
||||
v-if="!link"
|
||||
ref="el" class="_button"
|
||||
:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]"
|
||||
:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike, [$style.iconOnly]: iconOnly, [$style.wait]: wait }]"
|
||||
:type="type"
|
||||
:name="name"
|
||||
:value="value"
|
||||
:disabled="disabled"
|
||||
:disabled="disabled || wait"
|
||||
@click="emit('click', $event)"
|
||||
@mousedown="onMousedown"
|
||||
>
|
||||
|
|
@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</button>
|
||||
<MkA
|
||||
v-else class="_button"
|
||||
:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]"
|
||||
:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike, [$style.iconOnly]: iconOnly, [$style.wait]: wait }]"
|
||||
:to="to ?? '#'"
|
||||
:behavior="linkBehavior"
|
||||
@mousedown="onMousedown"
|
||||
|
|
@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onMounted, shallowRef } from 'vue';
|
||||
import { nextTick, onMounted, useTemplateRef } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
|
|
@ -57,14 +57,15 @@ const props = defineProps<{
|
|||
name?: string;
|
||||
value?: string;
|
||||
disabled?: boolean;
|
||||
iconOnly?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'click', payload: MouseEvent): void;
|
||||
}>();
|
||||
|
||||
const el = shallowRef<HTMLElement | null>(null);
|
||||
const ripples = shallowRef<HTMLElement | null>(null);
|
||||
const el = useTemplateRef('el');
|
||||
const ripples = useTemplateRef('ripples');
|
||||
|
||||
onMounted(() => {
|
||||
if (props.autofocus) {
|
||||
|
|
@ -91,7 +92,7 @@ function onMousedown(evt: MouseEvent): void {
|
|||
const target = evt.target! as HTMLElement;
|
||||
const rect = target.getBoundingClientRect();
|
||||
|
||||
const ripple = document.createElement('div');
|
||||
const ripple = window.document.createElement('div');
|
||||
ripple.classList.add(ripples.value!.dataset.childrenClass!);
|
||||
ripple.style.top = (evt.clientY - rect.top - 1).toString() + 'px';
|
||||
ripple.style.left = (evt.clientX - rect.left - 1).toString() + 'px';
|
||||
|
|
@ -147,6 +148,11 @@ function onMousedown(evt: MouseEvent): void {
|
|||
background: var(--MI_THEME-buttonHoverBg);
|
||||
}
|
||||
|
||||
&.iconOnly {
|
||||
padding: 7px;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
&.small {
|
||||
font-size: 90%;
|
||||
padding: 6px 12px;
|
||||
|
|
@ -220,28 +226,28 @@ function onMousedown(evt: MouseEvent): void {
|
|||
background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
|
||||
background: linear-gradient(90deg, hsl(from var(--MI_THEME-buttonGradateA) h s calc(l + 5)), hsl(from var(--MI_THEME-buttonGradateB) h s calc(l + 5)));
|
||||
}
|
||||
|
||||
&:not(:disabled):active {
|
||||
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
|
||||
background: linear-gradient(90deg, hsl(from var(--MI_THEME-buttonGradateA) h s calc(l + 5)), hsl(from var(--MI_THEME-buttonGradateB) h s calc(l + 5)));
|
||||
}
|
||||
}
|
||||
|
||||
&.danger {
|
||||
font-weight: bold;
|
||||
color: #ff2a2a;
|
||||
color: var(--MI_THEME-error);
|
||||
|
||||
&.primary {
|
||||
color: #fff;
|
||||
background: #ff2a2a;
|
||||
background: var(--MI_THEME-error);
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background: #ff4242;
|
||||
background: hsl(from var(--MI_THEME-error) h s calc(l + 10));
|
||||
}
|
||||
|
||||
&:not(:disabled):active {
|
||||
background: #d42e2e;
|
||||
background: hsl(from var(--MI_THEME-error) h s calc(l - 10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -250,6 +256,10 @@ function onMousedown(evt: MouseEvent): void {
|
|||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.wait {
|
||||
cursor: wait !important;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch, onUnmounted } from 'vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { ref, useTemplateRef, computed, onMounted, onBeforeUnmount, watch, onUnmounted } from 'vue';
|
||||
import { store } from '@/store.js';
|
||||
|
||||
// APIs provided by Captcha services
|
||||
// see: https://docs.hcaptcha.com/configuration/#javascript-api
|
||||
|
|
@ -53,6 +53,8 @@ type CaptchaContainer = {
|
|||
};
|
||||
|
||||
declare global {
|
||||
// Window を拡張してるため、空ではない
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
interface Window extends CaptchaContainer { }
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +72,7 @@ const emit = defineEmits<{
|
|||
|
||||
const available = ref(false);
|
||||
|
||||
const captchaEl = shallowRef<HTMLDivElement | undefined>();
|
||||
const captchaEl = useTemplateRef('captchaEl');
|
||||
const captchaWidgetId = ref<string | undefined>(undefined);
|
||||
const testcaptchaInput = ref('');
|
||||
const testcaptchaPassed = ref(false);
|
||||
|
|
@ -115,7 +117,7 @@ watch(() => [props.instanceUrl, props.sitekey, props.secretKey], async () => {
|
|||
if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha') {
|
||||
available.value = true;
|
||||
} else if (src.value !== null) {
|
||||
(document.getElementById(scriptId.value) ?? document.head.appendChild(Object.assign(document.createElement('script'), {
|
||||
(window.document.getElementById(scriptId.value) ?? window.document.head.appendChild(Object.assign(window.document.createElement('script'), {
|
||||
async: true,
|
||||
id: scriptId.value,
|
||||
src: src.value,
|
||||
|
|
@ -152,12 +154,12 @@ async function requestRender() {
|
|||
if (captcha.value.render && captchaEl.value instanceof Element && props.sitekey) {
|
||||
// reCAPTCHAのレンダリング重複判定を回避するため、captchaEl配下に仮のdivを用意する.
|
||||
// (同じdivに対して複数回renderを呼び出すとreCAPTCHAはエラーを返すので)
|
||||
const elem = document.createElement('div');
|
||||
const elem = window.document.createElement('div');
|
||||
captchaEl.value.appendChild(elem);
|
||||
|
||||
captchaWidgetId.value = captcha.value.render(elem, {
|
||||
sitekey: props.sitekey,
|
||||
theme: defaultStore.state.darkMode ? 'dark' : 'light',
|
||||
theme: store.s.darkMode ? 'dark' : 'light',
|
||||
callback: callback,
|
||||
'expired-callback': () => callback(undefined),
|
||||
'error-callback': () => callback(undefined),
|
||||
|
|
@ -185,7 +187,7 @@ async function requestRender() {
|
|||
|
||||
function clearWidget() {
|
||||
if (props.provider === 'mcaptcha') {
|
||||
const container = document.getElementById('mcaptcha__widget-container');
|
||||
const container = window.document.getElementById('mcaptcha__widget-container');
|
||||
if (container) {
|
||||
container.innerHTML = '';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable import/no-default-export */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { expect, userEvent, within } from '@storybook/test';
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable import/no-default-export */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { channel } from '../../.storybook/fakes.js';
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkPagination :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.notFound }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -19,8 +19,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Paging } from '@/components/MkPagination.vue';
|
||||
import MkChannelPreview from '@/components/MkChannelPreview.vue';
|
||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable import/no-default-export */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { channel } from '../../.storybook/fakes.js';
|
||||
import MkChannelPreview from './MkChannelPreview.vue';
|
||||
export const Default = {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable import/no-default-export */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { http } from 'msw';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import { getChartResolver } from '../../.storybook/charts.js';
|
||||
|
|
|
|||
|
|
@ -45,23 +45,19 @@ export type ChartSrc =
|
|||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/* eslint-disable id-denylist --
|
||||
Chart.js has a `data` attribute in most chart definitions, which triggers the
|
||||
id-denylist violation when setting it. This is causing about 60+ lint issues.
|
||||
As this is part of Chart.js's API it makes sense to disable the check here.
|
||||
*/
|
||||
import { onMounted, ref, shallowRef, watch } from 'vue';
|
||||
|
||||
import { onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
|
||||
import { chartVLine } from '@/scripts/chart-vline.js';
|
||||
import { alpha } from '@/scripts/color.js';
|
||||
import { misskeyApiGet } from '@/utility/misskey-api.js';
|
||||
import { store } from '@/store.js';
|
||||
import { useChartTooltip } from '@/use/use-chart-tooltip.js';
|
||||
import { chartVLine } from '@/utility/chart-vline.js';
|
||||
import { alpha } from '@/utility/color.js';
|
||||
import date from '@/filters/date.js';
|
||||
import bytes from '@/filters/bytes.js';
|
||||
import { initChart } from '@/scripts/init-chart.js';
|
||||
import { chartLegend } from '@/scripts/chart-legend.js';
|
||||
import { initChart } from '@/utility/init-chart.js';
|
||||
import { chartLegend } from '@/utility/chart-legend.js';
|
||||
import MkChartLegend from '@/components/MkChartLegend.vue';
|
||||
|
||||
initChart();
|
||||
|
|
@ -96,7 +92,7 @@ const props = withDefaults(defineProps<{
|
|||
nowForChromatic: undefined,
|
||||
});
|
||||
|
||||
const legendEl = shallowRef<InstanceType<typeof MkChartLegend>>();
|
||||
const legendEl = useTemplateRef('legendEl');
|
||||
|
||||
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
|
||||
const negate = arr => arr.map(x => -x);
|
||||
|
|
@ -134,7 +130,7 @@ let chartData: {
|
|||
bytes?: boolean;
|
||||
} | null = null;
|
||||
|
||||
const chartEl = shallowRef<HTMLCanvasElement | null>(null);
|
||||
const chartEl = useTemplateRef('chartEl');
|
||||
const fetching = ref(true);
|
||||
|
||||
const getDate = (ago: number) => {
|
||||
|
|
@ -161,7 +157,7 @@ const render = () => {
|
|||
chartInstance.destroy();
|
||||
}
|
||||
|
||||
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
|
||||
const maxes = chartData.series.map((x, i) => Math.max(...x.data.map(d => d.y)));
|
||||
|
||||
|
|
@ -849,7 +845,7 @@ watch(() => [props.src, props.span], fetchAndRender);
|
|||
onMounted(() => {
|
||||
fetchAndRender();
|
||||
});
|
||||
/* eslint-enable id-denylist */
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef } from 'vue';
|
||||
import { Chart, LegendItem } from 'chart.js';
|
||||
import { Chart } from 'chart.js';
|
||||
import type { LegendItem } from 'chart.js';
|
||||
|
||||
const chart = shallowRef<Chart>();
|
||||
const type = shallowRef<string>();
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable import/no-default-export */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { expect, userEvent, within } from '@storybook/test';
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ import { computed, onMounted, onUnmounted, ref } from 'vue';
|
|||
import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
import * as game from '@/scripts/clicker-game.js';
|
||||
import * as game from '@/utility/clicker-game.js';
|
||||
import number from '@/filters/number.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { claimAchievement } from '@/utility/achievements.js';
|
||||
|
||||
const saveData = game.saveData;
|
||||
const cookies = computed(() => saveData.value?.cookies);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable import/no-default-export */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { clip } from '../../.storybook/fakes.js';
|
||||
import MkClipPreview from './MkClipPreview.vue';
|
||||
export const Default = {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import * as Misskey from 'misskey-js';
|
||||
import { computed } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { $i } from '@/i.js';
|
||||
import number from '@/filters/number.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { computed, ref, watch } from 'vue';
|
||||
import { bundledLanguagesInfo } from 'shiki/langs';
|
||||
import type { BundledLanguage } from 'shiki/langs';
|
||||
import { getHighlighter, getTheme } from '@/scripts/code-highlighter.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { getHighlighter, getTheme } from '@/utility/code-highlighter.js';
|
||||
import { store } from '@/store.js';
|
||||
|
||||
const props = defineProps<{
|
||||
code: string;
|
||||
|
|
@ -22,7 +22,7 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const highlighter = await getHighlighter();
|
||||
const darkMode = defaultStore.reactiveState.darkMode;
|
||||
const darkMode = store.r.darkMode;
|
||||
const codeLang = ref<BundledLanguage | 'aiscript'>('js');
|
||||
|
||||
const [lightThemeName, darkThemeName] = await Promise.all([
|
||||
|
|
@ -93,7 +93,7 @@ watch(() => props.lang, (to) => {
|
|||
|
||||
.codeBlockRoot :global(.shiki) {
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
border-radius: var(--MI-radius-sm);
|
||||
border: 1px solid var(--MI_THEME-divider);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable import/no-default-export */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import MkCode from './MkCode.vue';
|
||||
const code = `for (let i, 100) {
|
||||
<: if (i % 15 == 0) "FizzBuzz"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</button>
|
||||
<Suspense>
|
||||
<template #fallback>
|
||||
<MkLoading />
|
||||
<MkLoading/>
|
||||
</template>
|
||||
<XCode v-if="show && lang" :code="code" :lang="lang"/>
|
||||
<pre v-else-if="show" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre>
|
||||
|
|
@ -28,9 +28,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { defineAsyncComponent, ref } from 'vue';
|
||||
import * as os from '@/os.js';
|
||||
import MkLoading from '@/components/global/MkLoading.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
code: string;
|
||||
|
|
@ -42,13 +42,12 @@ const props = withDefaults(defineProps<{
|
|||
forceShow: false,
|
||||
});
|
||||
|
||||
const show = ref(props.forceShow === true ? true : !defaultStore.state.dataSaver.code);
|
||||
const show = ref(props.forceShow === true ? true : !prefer.s.dataSaver.code);
|
||||
|
||||
const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue'));
|
||||
|
||||
function copy() {
|
||||
copyToClipboard(props.code);
|
||||
os.success();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable import/no-default-export */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import MkCodeEditor from './MkCodeEditor.vue';
|
||||
const code = `for (let i, 100) {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, toRefs, shallowRef, nextTick } from 'vue';
|
||||
import { ref, watch, toRefs, useTemplateRef, nextTick } from 'vue';
|
||||
import { debounce } from 'throttle-debounce';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
|
@ -61,7 +61,7 @@ const { modelValue } = toRefs(props);
|
|||
const v = ref<string>(modelValue.value ?? '');
|
||||
const focused = ref(false);
|
||||
const changed = ref(false);
|
||||
const inputEl = shallowRef<HTMLTextAreaElement>();
|
||||
const inputEl = useTemplateRef('inputEl');
|
||||
|
||||
const focus = () => inputEl.value?.focus();
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable import/no-default-export */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import MkCodeInline from './MkCodeInline.vue';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable import/no-default-export */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import MkColorInput from './MkColorInput.vue';
|
||||
export const Default = {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef, toRefs } from 'vue';
|
||||
import { ref, useTemplateRef, toRefs } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string | null;
|
||||
|
|
@ -39,7 +39,7 @@ const emit = defineEmits<{
|
|||
|
||||
const { modelValue } = toRefs(props);
|
||||
const v = ref(modelValue.value);
|
||||
const inputEl = shallowRef<HTMLElement>();
|
||||
const inputEl = useTemplateRef('inputEl');
|
||||
|
||||
const onInput = () => {
|
||||
emit('update:modelValue', v.value ?? '');
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</header>
|
||||
<Transition
|
||||
:enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
|
||||
:leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
|
||||
:enterFromClass="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''"
|
||||
:leaveToClass="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''"
|
||||
:enterActiveClass="prefer.s.animation ? $style.transition_toggle_enterActive : ''"
|
||||
:leaveActiveClass="prefer.s.animation ? $style.transition_toggle_leaveActive : ''"
|
||||
:enterFromClass="prefer.s.animation ? $style.transition_toggle_enterFrom : ''"
|
||||
:leaveToClass="prefer.s.animation ? $style.transition_toggle_leaveTo : ''"
|
||||
@enter="enter"
|
||||
@afterEnter="afterEnter"
|
||||
@leave="leave"
|
||||
|
|
@ -39,8 +39,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
|
@ -58,9 +58,9 @@ const props = withDefaults(defineProps<{
|
|||
maxHeight: null,
|
||||
});
|
||||
|
||||
const rootEl = shallowRef<HTMLElement>();
|
||||
const contentEl = shallowRef<HTMLElement>();
|
||||
const headerEl = shallowRef<HTMLElement>();
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
const contentEl = useTemplateRef('contentEl');
|
||||
const headerEl = useTemplateRef('headerEl');
|
||||
const showBody = ref(props.expanded);
|
||||
const ignoreOmit = ref(false);
|
||||
const omitted = ref(false);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable import/no-default-export */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { userEvent, within } from '@storybook/test';
|
||||
import MkContextMenu from './MkContextMenu.vue';
|
||||
import * as os from '@/os.js';
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<Transition
|
||||
appear
|
||||
:enterActiveClass="defaultStore.state.animation ? $style.transition_fade_enterActive : ''"
|
||||
:leaveActiveClass="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''"
|
||||
:enterFromClass="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''"
|
||||
:leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
|
||||
:enterActiveClass="prefer.s.animation ? $style.transition_fade_enterActive : ''"
|
||||
:leaveActiveClass="prefer.s.animation ? $style.transition_fade_leaveActive : ''"
|
||||
:enterFromClass="prefer.s.animation ? $style.transition_fade_enterFrom : ''"
|
||||
:leaveToClass="prefer.s.animation ? $style.transition_fade_leaveTo : ''"
|
||||
>
|
||||
<div ref="rootEl" :class="$style.root" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
|
||||
<MkMenu :items="items" :align="'left'" @close="emit('closed')"/>
|
||||
|
|
@ -18,11 +18,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onBeforeUnmount, shallowRef, ref } from 'vue';
|
||||
import { onMounted, onBeforeUnmount, useTemplateRef, ref } from 'vue';
|
||||
import MkMenu from './MkMenu.vue';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import contains from '@/scripts/contains.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import contains from '@/utility/contains.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
@ -34,7 +34,7 @@ const emit = defineEmits<{
|
|||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const rootEl = shallowRef<HTMLDivElement>();
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
|
||||
const zIndex = ref<number>(os.claimZIndex('high'));
|
||||
|
||||
|
|
@ -68,11 +68,11 @@ onMounted(() => {
|
|||
rootEl.value.style.left = `${left}px`;
|
||||
}
|
||||
|
||||
document.body.addEventListener('mousedown', onMousedown);
|
||||
window.document.body.addEventListener('mousedown', onMousedown);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.body.removeEventListener('mousedown', onMousedown);
|
||||
window.document.body.removeEventListener('mousedown', onMousedown);
|
||||
});
|
||||
|
||||
function onMousedown(evt: Event) {
|
||||
|
|
|
|||
|
|
@ -3,14 +3,12 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable import/no-default-export */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { file } from '../../.storybook/fakes.js';
|
||||
import { commonHandlers } from '../../.storybook/mocks.js';
|
||||
import MkCropperDialog from './MkCropperDialog.vue';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
|
|
@ -55,7 +53,7 @@ export const Default = {
|
|||
http.get('/proxy/image.webp', async ({ request }) => {
|
||||
const url = new URL(request.url).searchParams.get('url');
|
||||
if (url === 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true') {
|
||||
const image = await (await fetch('client-assets/fedi.jpg')).blob();
|
||||
const image = await (await window.fetch('client-assets/fedi.jpg')).blob();
|
||||
return new HttpResponse(image, {
|
||||
headers: {
|
||||
'Content-Type': 'image/jpeg',
|
||||
|
|
|
|||
|
|
@ -31,17 +31,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, shallowRef, ref } from 'vue';
|
||||
import { onMounted, useTemplateRef, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import Cropper from 'cropperjs';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { apiUrl } from '@@/js/config.js';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { apiUrl } from '@@/js/config.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
|
||||
import { getProxiedImageUrl } from '@/utility/media-proxy.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'ok', cropped: Misskey.entities.DriveFile): void;
|
||||
|
|
@ -56,8 +56,8 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const imgUrl = getProxiedImageUrl(props.file.url, undefined, true);
|
||||
const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
const imgEl = shallowRef<HTMLImageElement>();
|
||||
const dialogEl = useTemplateRef('dialogEl');
|
||||
const imgEl = useTemplateRef('imgEl');
|
||||
let cropper: Cropper | null = null;
|
||||
const loading = ref(true);
|
||||
|
||||
|
|
@ -81,8 +81,8 @@ const ok = async () => {
|
|||
formData.append('i', $i!.token);
|
||||
if (props.uploadFolder) {
|
||||
formData.append('folderId', props.uploadFolder);
|
||||
} else if (props.uploadFolder !== null && defaultStore.state.uploadFolder) {
|
||||
formData.append('folderId', defaultStore.state.uploadFolder);
|
||||
} else if (props.uploadFolder !== null && prefer.s.uploadFolder) {
|
||||
formData.append('folderId', prefer.s.uploadFolder);
|
||||
}
|
||||
|
||||
window.fetch(apiUrl + '/drive/files/create', {
|
||||
|
|
@ -122,7 +122,7 @@ onMounted(() => {
|
|||
cropper = new Cropper(imgEl.value!, {
|
||||
});
|
||||
|
||||
const computedStyle = getComputedStyle(document.documentElement);
|
||||
const computedStyle = getComputedStyle(window.document.documentElement);
|
||||
|
||||
const selection = cropper.getCropperSelection()!;
|
||||
selection.themeColor = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable import/no-default-export */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { emojiDetailed } from '../../.storybook/fakes.js';
|
||||
import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
|
||||
export const Default = {
|
||||
|
|
|
|||
|
|
@ -57,14 +57,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { shallowRef } from 'vue';
|
||||
import { useTemplateRef } from 'vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
emoji: Misskey.entities.EmojiDetailed,
|
||||
emoji: Misskey.entities.EmojiDetailed,
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -73,7 +73,7 @@ const emit = defineEmits<{
|
|||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
const dialogEl = useTemplateRef('dialogEl');
|
||||
|
||||
function cancel() {
|
||||
emit('cancel');
|
||||
|
|
@ -85,7 +85,7 @@ function cancel() {
|
|||
.emojiImgWrapper {
|
||||
max-width: 100%;
|
||||
height: 40cqh;
|
||||
background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-X5) 8px, var(--MI_THEME-X5) 14px);
|
||||
background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05)) 8px, light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05)) 14px);
|
||||
border-radius: var(--MI-radius);
|
||||
margin: auto;
|
||||
overflow-y: hidden;
|
||||
|
|
@ -101,7 +101,7 @@ function cancel() {
|
|||
display: inline-block;
|
||||
word-break: break-all;
|
||||
padding: 3px 10px;
|
||||
background-color: var(--MI_THEME-X5);
|
||||
background-color: light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05));
|
||||
border: solid 1px var(--MI_THEME-divider);
|
||||
border-radius: var(--MI-radius);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable import/no-default-export */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { expect, userEvent, within } from '@storybook/test';
|
||||
import { file } from '../../.storybook/fakes.js';
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { computed } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
|
||||
import { concat } from '@/scripts/array.js';
|
||||
import { concat } from '@/utility/array.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
||||
|
|
|
|||
|
|
@ -4,14 +4,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, h, PropType, TransitionGroup, useCssModule } from 'vue';
|
||||
import { defineComponent, h, TransitionGroup, useCssModule } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
import type { MisskeyEntity } from '@/types/date-separated-list.js';
|
||||
import MkAd from '@/components/global/MkAd.vue';
|
||||
import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { MisskeyEntity } from '@/types/date-separated-list.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { $i } from '@/account.js';
|
||||
|
||||
export default defineComponent({
|
||||
|
|
@ -152,7 +153,7 @@ export default defineComponent({
|
|||
[$style['direction-up']]: props.direction === 'up',
|
||||
};
|
||||
|
||||
return () => defaultStore.state.animation ? h(TransitionGroup, {
|
||||
return () => prefer.s.animation ? h(TransitionGroup, {
|
||||
class: classes,
|
||||
name: 'list',
|
||||
tag: 'div',
|
||||
|
|
@ -170,21 +171,17 @@ export default defineComponent({
|
|||
container-type: inline-size;
|
||||
|
||||
&:global {
|
||||
> .list-move {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
> .list-move {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
&.deny-move-transition > .list-move {
|
||||
transition: none !important;
|
||||
}
|
||||
> .list-enter-active {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
> .list-enter-active {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
> *:empty {
|
||||
display: none;
|
||||
}
|
||||
> *:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.date-separated-list-nogap) > *:not(:last-child) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkDialog from './MkDialog.vue';
|
||||
const Base = {
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<i v-else-if="type === 'question'" :class="$style.iconInner" class="ti ti-help-circle"></i>
|
||||
<MkLoading v-else-if="type === 'waiting'" :class="$style.iconInner" :em="true"/>
|
||||
</div>
|
||||
<header v-if="title" :class="$style.title"><Mfm :text="title"/></header>
|
||||
<div v-if="text" :class="$style.text"><Mfm :text="text" :isBlock="true" :plain="plain" /></div>
|
||||
<header v-if="title" :class="$style.title" class="_selectable"><Mfm :text="title"/></header>
|
||||
<div v-if="text" :class="$style.text" class="_selectable"><Mfm :text="text" :isBlock="true" :plain="plain"/></div>
|
||||
<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown">
|
||||
<template v-if="input.type === 'password'" #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption>
|
||||
|
|
@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef, computed } from 'vue';
|
||||
import { ref, useTemplateRef, computed } from 'vue';
|
||||
import MkModal from '@/components/MkModal.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
|
|
@ -118,7 +118,7 @@ const emit = defineEmits<{
|
|||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||
const modal = useTemplateRef('modal');
|
||||
|
||||
const inputValue = ref<string | number | null>(props.input?.default ?? null);
|
||||
const selectedValue = ref(props.select?.default ?? null);
|
||||
|
|
@ -143,6 +143,7 @@ const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'character
|
|||
// overload function を使いたいので lint エラーを無視する
|
||||
function done(canceled: true): void;
|
||||
function done(canceled: false, result: Result): void; // eslint-disable-line no-redeclare
|
||||
|
||||
function done(canceled: boolean, result?: Result): void { // eslint-disable-line no-redeclare
|
||||
emit('done', { canceled, result } as { canceled: true } | { canceled: false, result: Result });
|
||||
modal.value?.close();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import isChromatic from 'chromatic/isChromatic';
|
||||
import MkDigitalClock from './MkDigitalClock.vue';
|
||||
export const Default = {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { defaultIdlingRenderScheduler } from '@/scripts/idle-render.js';
|
||||
import { defaultIdlingRenderScheduler } from '@/utility/idle-render.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
showS?: boolean;
|
||||
|
|
|
|||
42
packages/frontend/src/components/MkDisableSection.vue
Normal file
42
packages/frontend/src/components/MkDisableSection.vue
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="[$style.root]">
|
||||
<div :inert="disabled" :class="[{ [$style.disabled]: disabled }]">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div v-if="disabled" :class="[$style.cover]"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.3;
|
||||
filter: saturate(0.5);
|
||||
}
|
||||
|
||||
.cover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: not-allowed;
|
||||
--color: light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05));
|
||||
background-size: auto auto;
|
||||
background-image: repeating-linear-gradient(135deg, transparent, transparent 10px, var(--color) 4px, var(--color) 14px);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { onBeforeUnmount } from 'vue';
|
||||
import MkDonation from './MkDonation.vue';
|
||||
import { instance } from '@/instance.js';
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import MkDrive_file from './MkDrive.file.vue';
|
||||
import { file } from '../../.storybook/fakes.js';
|
||||
export const Default = {
|
||||
|
|
|
|||
|
|
@ -48,10 +48,10 @@ import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
|||
import bytes from '@/filters/bytes.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js';
|
||||
import { deviceKind } from '@/utility/device-kind.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkDrive_folder from './MkDrive.folder.vue';
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template v-if="!hover"><i :class="$style.icon" class="ti ti-folder ti-fw"></i></template>
|
||||
{{ folder.name }}
|
||||
</p>
|
||||
<p v-if="defaultStore.state.uploadFolder == folder.id" :class="$style.upload">
|
||||
<p v-if="prefer.s.uploadFolder == folder.id" :class="$style.upload">
|
||||
{{ i18n.ts.uploadFolder }}
|
||||
</p>
|
||||
<button v-if="selectMode" class="_button" :class="$style.checkboxWrapper" @click.prevent.stop="checkboxClicked">
|
||||
|
|
@ -38,11 +38,11 @@ import { computed, defineAsyncComponent, ref } from 'vue';
|
|||
import * as Misskey from 'misskey-js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { claimAchievement } from '@/utility/achievements.js';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
folder: Misskey.entities.DriveFolder;
|
||||
|
|
@ -244,8 +244,8 @@ function deleteFolder() {
|
|||
misskeyApi('drive/folders/delete', {
|
||||
folderId: props.folder.id,
|
||||
}).then(() => {
|
||||
if (defaultStore.state.uploadFolder === props.folder.id) {
|
||||
defaultStore.set('uploadFolder', null);
|
||||
if (prefer.s.uploadFolder === props.folder.id) {
|
||||
prefer.commit('uploadFolder', null);
|
||||
}
|
||||
}).catch(err => {
|
||||
switch (err.id) {
|
||||
|
|
@ -266,7 +266,7 @@ function deleteFolder() {
|
|||
}
|
||||
|
||||
function setAsUploadFolder() {
|
||||
defaultStore.set('uploadFolder', props.folder.id);
|
||||
prefer.commit('uploadFolder', props.folder.id);
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent) {
|
||||
|
|
@ -295,9 +295,9 @@ function onContextmenu(ev: MouseEvent) {
|
|||
danger: true,
|
||||
action: deleteFolder,
|
||||
}];
|
||||
if (defaultStore.state.devMode) {
|
||||
if (prefer.s.devMode) {
|
||||
menu = menu.concat([{ type: 'divider' }, {
|
||||
icon: 'ti ti-id',
|
||||
icon: 'ti ti-hash',
|
||||
text: i18n.ts.copyFolderId,
|
||||
action: () => {
|
||||
copyToClipboard(props.folder.id);
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkDrive from './MkDrive.vue';
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
|
||||
import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkButton from './MkButton.vue';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
|
@ -112,12 +112,12 @@ import XFolder from '@/components/MkDrive.folder.vue';
|
|||
import XFile from '@/components/MkDrive.file.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { uploadFile, uploads } from '@/scripts/upload.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { uploadFile, uploads } from '@/utility/upload.js';
|
||||
import { claimAchievement } from '@/utility/achievements.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const searchQuery = ref('');
|
||||
|
||||
|
|
@ -139,8 +139,8 @@ const emit = defineEmits<{
|
|||
(ev: 'open-folder', v: Misskey.entities.DriveFolder): void;
|
||||
}>();
|
||||
|
||||
const loadMoreFiles = shallowRef<InstanceType<typeof MkButton>>();
|
||||
const fileInput = shallowRef<HTMLInputElement>();
|
||||
const loadMoreFiles = useTemplateRef('loadMoreFiles');
|
||||
const fileInput = useTemplateRef('fileInput');
|
||||
|
||||
const folder = ref<Misskey.entities.DriveFolder | null>(null);
|
||||
const files = ref<Misskey.entities.DriveFile[]>([]);
|
||||
|
|
@ -152,7 +152,7 @@ const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
|
|||
const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
|
||||
const uploadings = uploads;
|
||||
const connection = useStream().useChannel('drive');
|
||||
const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい
|
||||
const keepOriginal = ref<boolean>(prefer.s.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい
|
||||
|
||||
// ドロップされようとしているか
|
||||
const draghover = ref(false);
|
||||
|
|
@ -730,7 +730,7 @@ function onContextmenu(ev: MouseEvent) {
|
|||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (defaultStore.state.enableInfiniteScroll && loadMoreFiles.value) {
|
||||
if (prefer.s.enableInfiniteScroll && loadMoreFiles.value) {
|
||||
nextTick(() => {
|
||||
ilFilesObserver.observe(loadMoreFiles.value?.$el);
|
||||
});
|
||||
|
|
@ -751,7 +751,7 @@ onMounted(() => {
|
|||
});
|
||||
|
||||
onActivated(() => {
|
||||
if (defaultStore.state.enableInfiniteScroll) {
|
||||
if (prefer.s.enableInfiniteScroll) {
|
||||
nextTick(() => {
|
||||
ilFilesObserver.observe(loadMoreFiles.value?.$el);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import MkDriveFileThumbnail from './MkDriveFileThumbnail.vue';
|
||||
import { file } from '../../.storybook/fakes.js';
|
||||
export const Default = {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef } from 'vue';
|
||||
import { ref, useTemplateRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XDrive from '@/components/MkDrive.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
|
|
@ -43,7 +43,7 @@ const emit = defineEmits<{
|
|||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
const dialog = useTemplateRef('dialog');
|
||||
|
||||
const selected = ref<Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]>([]);
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { shallowRef, ref, computed, nextTick, onMounted, onDeactivated, onUnmounted } from 'vue';
|
||||
import { useTemplateRef, ref, computed, nextTick, onMounted, onDeactivated, onUnmounted } from 'vue';
|
||||
import { url } from '@@/js/config.js';
|
||||
import { embedRouteWithScrollbar } from '@@/js/embed-page.js';
|
||||
import type { EmbeddableEntity, EmbedParams } from '@@/js/embed-page.js';
|
||||
|
|
@ -105,8 +105,8 @@ import MkInfo from '@/components/MkInfo.vue';
|
|||
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { normalizeEmbedParams, getEmbedCode } from '@/scripts/get-embed-code.js';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||
import { normalizeEmbedParams, getEmbedCode } from '@/utility/get-embed-code.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'ok'): void;
|
||||
|
|
@ -121,7 +121,7 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
//#region Modalの制御
|
||||
const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
const dialogEl = useTemplateRef('dialogEl');
|
||||
|
||||
function cancel() {
|
||||
emit('cancel');
|
||||
|
|
@ -180,7 +180,7 @@ function applyToPreview() {
|
|||
nextTick(() => {
|
||||
if (currentPreviewUrl === embedPreviewUrl.value) {
|
||||
// URLが変わらなくてもリロード
|
||||
iframeEl.value?.contentWindow?.location.reload();
|
||||
iframeEl.value?.contentWindow?.window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -194,14 +194,13 @@ function generate() {
|
|||
|
||||
function doCopy() {
|
||||
copyToClipboard(result.value);
|
||||
os.success();
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region プレビューのリサイズ
|
||||
const resizerRootEl = shallowRef<HTMLDivElement>();
|
||||
const resizerRootEl = useTemplateRef('resizerRootEl');
|
||||
const iframeLoading = ref(true);
|
||||
const iframeEl = shallowRef<HTMLIFrameElement>();
|
||||
const iframeEl = useTemplateRef('iframeEl');
|
||||
const iframeHeight = ref(0);
|
||||
const iframeScale = ref(1);
|
||||
const iframeStyle = computed(() => {
|
||||
|
|
|
|||
|
|
@ -61,8 +61,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, Ref } from 'vue';
|
||||
import { CustomEmojiFolderTree, getEmojiName } from '@@/js/emojilist.js';
|
||||
import { ref, computed } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import { getEmojiName } from '@@/js/emojilist.js';
|
||||
import type { CustomEmojiFolderTree } from '@@/js/emojilist.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { customEmojis } from '@/custom-emojis.js';
|
||||
import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue';
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkEmojiPicker from './MkEmojiPicker.vue';
|
||||
export const Default = {
|
||||
|
|
|
|||
|
|
@ -115,31 +115,34 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef, computed, watch, onMounted } from 'vue';
|
||||
import { ref, useTemplateRef, computed, watch, onMounted } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import {
|
||||
emojilist,
|
||||
emojiCharByCategory,
|
||||
UnicodeEmojiDef,
|
||||
unicodeEmojiCategories as categories,
|
||||
getEmojiName,
|
||||
CustomEmojiFolderTree,
|
||||
getUnicodeEmoji,
|
||||
} from '@@/js/emojilist.js';
|
||||
import type {
|
||||
UnicodeEmojiDef,
|
||||
CustomEmojiFolderTree,
|
||||
} from '@@/js/emojilist.js';
|
||||
import XSection from '@/components/MkEmojiPicker.section.vue';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { isTouchUsing } from '@/scripts/touch.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import { isTouchUsing } from '@/utility/touch.js';
|
||||
import { deviceKind } from '@/utility/device-kind.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { store } from '@/store.js';
|
||||
import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
showPinned?: boolean;
|
||||
pinnedEmojis?: string[];
|
||||
pinnedEmojis?: string[];
|
||||
maxHeight?: number;
|
||||
asDrawer?: boolean;
|
||||
asWindow?: boolean;
|
||||
|
|
@ -154,15 +157,16 @@ const emit = defineEmits<{
|
|||
(ev: 'esc'): void;
|
||||
}>();
|
||||
|
||||
const searchEl = shallowRef<HTMLInputElement>();
|
||||
const emojisEl = shallowRef<HTMLDivElement>();
|
||||
const searchEl = useTemplateRef('searchEl');
|
||||
const emojisEl = useTemplateRef('emojisEl');
|
||||
|
||||
const {
|
||||
emojiPickerScale,
|
||||
emojiPickerWidth,
|
||||
emojiPickerHeight,
|
||||
recentlyUsedEmojis,
|
||||
} = defaultStore.reactiveState;
|
||||
} = prefer.r;
|
||||
|
||||
const recentlyUsedEmojis = store.r.recentlyUsedEmojis;
|
||||
|
||||
const recentlyUsedEmojisDef = computed(() => {
|
||||
return recentlyUsedEmojis.value.map(getDef).filter(x => x != null);
|
||||
|
|
@ -317,7 +321,7 @@ watch(q, () => {
|
|||
}
|
||||
if (matches.size >= max) return matches;
|
||||
|
||||
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
|
||||
for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) {
|
||||
for (const emoji of emojis) {
|
||||
if (keywords.every(keyword => index[emoji.char].some(k => k.includes(keyword)))) {
|
||||
matches.add(emoji);
|
||||
|
|
@ -334,7 +338,7 @@ watch(q, () => {
|
|||
}
|
||||
if (matches.size >= max) return matches;
|
||||
|
||||
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
|
||||
for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) {
|
||||
for (const emoji of emojis) {
|
||||
if (index[emoji.char].some(k => k.startsWith(newQ))) {
|
||||
matches.add(emoji);
|
||||
|
|
@ -351,7 +355,7 @@ watch(q, () => {
|
|||
}
|
||||
if (matches.size >= max) return matches;
|
||||
|
||||
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
|
||||
for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) {
|
||||
for (const emoji of emojis) {
|
||||
if (index[emoji.char].some(k => k.includes(newQ))) {
|
||||
matches.add(emoji);
|
||||
|
|
@ -413,7 +417,7 @@ function computeButtonTitle(ev: MouseEvent): void {
|
|||
|
||||
function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef, ev?: MouseEvent) {
|
||||
const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
|
||||
if (el) {
|
||||
if (el && prefer.s.animation) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = rect.left + (el.offsetWidth / 2);
|
||||
const y = rect.top + (el.offsetHeight / 2);
|
||||
|
|
@ -427,10 +431,10 @@ function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef,
|
|||
|
||||
// 最近使った絵文字更新
|
||||
if (!pinned.value?.includes(key)) {
|
||||
let recents = defaultStore.state.recentlyUsedEmojis;
|
||||
let recents = store.s.recentlyUsedEmojis;
|
||||
recents = recents.filter((emoji) => emoji !== key);
|
||||
recents.unshift(key);
|
||||
defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
|
||||
store.set('recentlyUsedEmojis', recents.splice(0, 32));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -582,7 +586,7 @@ defineExpose({
|
|||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
background: linear-gradient(-45deg, transparent 0% 48%, var(--MI_THEME-X6) 48% 52%, transparent 52% 100%);
|
||||
background: linear-gradient(-45deg, transparent 0% 48%, light-dark(rgba(0, 0, 0, 0.25), rgba(255, 255, 255, 0.15)) 48% 52%, transparent 52% 100%);
|
||||
opacity: 1;
|
||||
|
||||
> .emoji {
|
||||
|
|
@ -617,7 +621,7 @@ defineExpose({
|
|||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
background: linear-gradient(-45deg, transparent 0% 48%, var(--MI_THEME-X6) 48% 52%, transparent 52% 100%);
|
||||
background: linear-gradient(-45deg, transparent 0% 48%, light-dark(rgba(0, 0, 0, 0.25), rgba(255, 255, 255, 0.15)) 48% 52%, transparent 52% 100%);
|
||||
opacity: 1;
|
||||
|
||||
> .emoji {
|
||||
|
|
@ -738,7 +742,7 @@ defineExpose({
|
|||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
background: linear-gradient(-45deg, transparent 0% 48%, var(--MI_THEME-X6) 48% 52%, transparent 52% 100%);
|
||||
background: linear-gradient(-45deg, transparent 0% 48%, light-dark(rgba(0, 0, 0, 0.25), rgba(255, 255, 255, 0.15)) 48% 52%, transparent 52% 100%);
|
||||
opacity: 1;
|
||||
|
||||
> .emoji {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
ref="modal"
|
||||
v-slot="{ type, maxHeight }"
|
||||
:zPriority="'middle'"
|
||||
:preferType="defaultStore.state.emojiPickerStyle"
|
||||
:preferType="prefer.s.emojiPickerStyle"
|
||||
:hasInteractionWithOtherFocusTrappedEls="true"
|
||||
:transparentBg="true"
|
||||
:manualShowing="manualShowing"
|
||||
|
|
@ -37,19 +37,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { shallowRef } from 'vue';
|
||||
import { useTemplateRef } from 'vue';
|
||||
import MkModal from '@/components/MkModal.vue';
|
||||
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
manualShowing?: boolean | null;
|
||||
src?: HTMLElement;
|
||||
showPinned?: boolean;
|
||||
pinnedEmojis?: string[],
|
||||
pinnedEmojis?: string[],
|
||||
asReactionPicker?: boolean;
|
||||
targetNote?: Misskey.entities.Note;
|
||||
choseAndClose?: boolean;
|
||||
choseAndClose?: boolean;
|
||||
}>(), {
|
||||
manualShowing: null,
|
||||
showPinned: true,
|
||||
|
|
@ -64,8 +64,8 @@ const emit = defineEmits<{
|
|||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||
const picker = shallowRef<InstanceType<typeof MkEmojiPicker>>();
|
||||
const modal = useTemplateRef('modal');
|
||||
const picker = useTemplateRef('picker');
|
||||
|
||||
function chosen(emoji: string) {
|
||||
emit('done', emoji);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import MkExtensionInstaller from './MkExtensionInstaller.vue';
|
||||
import lightTheme from '@@/themes/_light.json5';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,54 +11,91 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<!-- 拡張用? -->
|
||||
<i v-else class="ti ti-download"></i>
|
||||
</div>
|
||||
<h2 :class="$style.extInstallerTitle">{{ i18n.ts._externalResourceInstaller[`_${extension.type}`].title }}</h2>
|
||||
<div :class="$style.extInstallerNormDesc">{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}</div>
|
||||
<MkInfo v-if="isPlugin" :warn="true">{{ i18n.ts._plugin.installWarn }}</MkInfo>
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts._externalResourceInstaller[`_${extension.type}`].metaTitle }}</template>
|
||||
<div class="_gaps_s">
|
||||
<FormSplit>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.name }}</template>
|
||||
<template #value>{{ extension.meta.name }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.author }}</template>
|
||||
<template #value>{{ extension.meta.author }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
<MkKeyValue v-if="isPlugin">
|
||||
<template #key>{{ i18n.ts.description }}</template>
|
||||
<template #value>{{ extension.meta.description ?? i18n.ts.none }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="isPlugin">
|
||||
<template #key>{{ i18n.ts.version }}</template>
|
||||
<template #value>{{ extension.meta.version }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="isPlugin">
|
||||
<template #key>{{ i18n.ts.permission }}</template>
|
||||
<template #value>
|
||||
<ul v-if="extension.meta.permissions && extension.meta.permissions.length > 0" :class="$style.extInstallerKVList">
|
||||
<li v-for="permission in extension.meta.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li>
|
||||
</ul>
|
||||
<template v-else>{{ i18n.ts.none }}</template>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="isTheme">
|
||||
<template #key>{{ i18n.ts._externalResourceInstaller._meta.base }}</template>
|
||||
<template #value>{{ i18n.ts[extension.meta.base ?? 'none'] }}</template>
|
||||
</MkKeyValue>
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-code"></i></template>
|
||||
<template #label>{{ i18n.ts._plugin.viewSource }}</template>
|
||||
|
||||
<MkCode :code="extension.raw"/>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</FormSection>
|
||||
<h2 v-if="isPlugin" :class="$style.extInstallerTitle">{{ i18n.ts._externalResourceInstaller._plugin.title }}</h2>
|
||||
<h2 v-else-if="isTheme" :class="$style.extInstallerTitle">{{ i18n.ts._externalResourceInstaller._theme.title }}</h2>
|
||||
|
||||
<MkInfo :warn="true">{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}</MkInfo>
|
||||
|
||||
<div v-if="isPlugin" class="_gaps_s">
|
||||
<MkFolder :defaultOpen="true">
|
||||
<template #icon><i class="ti ti-info-circle"></i></template>
|
||||
<template #label>{{ i18n.ts.metadata }}</template>
|
||||
|
||||
<div class="_gaps_s">
|
||||
<FormSplit>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.name }}</template>
|
||||
<template #value>{{ extension.meta.name }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.author }}</template>
|
||||
<template #value>{{ extension.meta.author }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.description }}</template>
|
||||
<template #value>{{ extension.meta.description ?? i18n.ts.none }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.version }}</template>
|
||||
<template #value>{{ extension.meta.version }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.permission }}</template>
|
||||
<template #value>
|
||||
<ul v-if="extension.meta.permissions && extension.meta.permissions.length > 0" :class="$style.extInstallerKVList">
|
||||
<li v-for="permission in extension.meta.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li>
|
||||
</ul>
|
||||
<template v-else>{{ i18n.ts.none }}</template>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder :withSpacer="false">
|
||||
<template #icon><i class="ti ti-code"></i></template>
|
||||
<template #label>{{ i18n.ts._plugin.viewSource }}</template>
|
||||
|
||||
<MkCode :code="extension.raw"/>
|
||||
</MkFolder>
|
||||
</div>
|
||||
<div v-else-if="isTheme" class="_gaps_s">
|
||||
<MkFolder :defaultOpen="true">
|
||||
<template #icon><i class="ti ti-info-circle"></i></template>
|
||||
<template #label>{{ i18n.ts.metadata }}</template>
|
||||
|
||||
<div class="_gaps_s">
|
||||
<FormSplit>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.name }}</template>
|
||||
<template #value>{{ extension.meta.name }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.author }}</template>
|
||||
<template #value>{{ extension.meta.author }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts._externalResourceInstaller._meta.base }}</template>
|
||||
<template #value>{{ i18n.ts[extension.meta.base ?? 'none'] }}</template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder :withSpacer="false">
|
||||
<template #icon><i class="ti ti-code"></i></template>
|
||||
<template #label>{{ i18n.ts._theme.code }}</template>
|
||||
|
||||
<MkCode :code="extension.raw"/>
|
||||
</MkFolder>
|
||||
</div>
|
||||
|
||||
<slot name="additionalInfo"/>
|
||||
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton primary @click="emits('confirm')"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
|
||||
<MkButton danger rounded large @click="emits('cancel')"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton>
|
||||
<MkButton gradate rounded large @click="emits('confirm')"><i class="ti ti-download"></i> {{ i18n.ts.install }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -105,6 +142,7 @@ const props = defineProps<{
|
|||
|
||||
const emits = defineEmits<{
|
||||
(ev: 'confirm'): void;
|
||||
(ev: 'cancel'): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
|
@ -112,13 +150,13 @@ const emits = defineEmits<{
|
|||
.extInstallerRoot {
|
||||
border-radius: var(--MI-radius);
|
||||
background: var(--MI_THEME-panel);
|
||||
padding: 1.5rem;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.extInstallerIconWrapper {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 24px;
|
||||
font-size: 20px;
|
||||
line-height: 48px;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
|
|
@ -135,10 +173,6 @@ const emits = defineEmits<{
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.extInstallerNormDesc {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.extInstallerKVList {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
|
|
|
|||
43
packages/frontend/src/components/MkFeatureBanner.vue
Normal file
43
packages/frontend/src/components/MkFeatureBanner.vue
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div v-panel :class="$style.root">
|
||||
<img :class="$style.img" :src="icon"/>
|
||||
<div :class="$style.text">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
withDefaults(defineProps<{
|
||||
icon: string;
|
||||
color: string;
|
||||
}>(), {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.root {
|
||||
padding: 20px 24px;
|
||||
text-align: center;
|
||||
border-radius: var(--MI-radius);
|
||||
background: linear-gradient(180deg, color(from v-bind(color) srgb r g b / 0.1), color(from v-bind(color) srgb r g b / 0));
|
||||
}
|
||||
|
||||
.img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
width: 40px;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-top: 12px;
|
||||
font-size: 85%;
|
||||
mix-blend-mode: luminosity;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef, ref } from 'vue';
|
||||
import { useTemplateRef, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
|
|
@ -42,7 +42,7 @@ const emit = defineEmits<{
|
|||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
const dialog = useTemplateRef('dialog');
|
||||
|
||||
const caption = ref(props.default);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import MkFlashPreview from './MkFlashPreview.vue';
|
||||
import { flash } from './../../.storybook/fakes.js';
|
||||
export const Public = {
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</button>
|
||||
</header>
|
||||
<Transition
|
||||
:enterActiveClass="defaultStore.state.animation ? $style.folderToggleEnterActive : ''"
|
||||
:leaveActiveClass="defaultStore.state.animation ? $style.folderToggleLeaveActive : ''"
|
||||
:enterFromClass="defaultStore.state.animation ? $style.folderToggleEnterFrom : ''"
|
||||
:leaveToClass="defaultStore.state.animation ? $style.folderToggleLeaveTo : ''"
|
||||
:enterActiveClass="prefer.s.animation ? $style.folderToggleEnterActive : ''"
|
||||
:leaveActiveClass="prefer.s.animation ? $style.folderToggleLeaveActive : ''"
|
||||
:enterFromClass="prefer.s.animation ? $style.folderToggleEnterFrom : ''"
|
||||
:leaveToClass="prefer.s.animation ? $style.folderToggleLeaveTo : ''"
|
||||
@enter="enter"
|
||||
@afterEnter="afterEnter"
|
||||
@leave="leave"
|
||||
|
|
@ -31,10 +31,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, shallowRef, watch } from 'vue';
|
||||
import { onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { getBgColor } from '@/scripts/get-bg-color.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { getBgColor } from '@/utility/get-bg-color.js';
|
||||
|
||||
const miLocalStoragePrefix = 'ui:folder:' as const;
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ const props = withDefaults(defineProps<{
|
|||
persistKey: null,
|
||||
});
|
||||
|
||||
const rootEl = shallowRef<HTMLElement>();
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
const parentBg = ref<string | null>(null);
|
||||
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
|
||||
const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded);
|
||||
|
|
|
|||
|
|
@ -27,10 +27,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : undefined, overflow: maxHeight ? `auto` : undefined }" :aria-hidden="!opened">
|
||||
<Transition
|
||||
:enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
|
||||
:leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
|
||||
:enterFromClass="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''"
|
||||
:leaveToClass="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''"
|
||||
:enterActiveClass="prefer.s.animation ? $style.transition_toggle_enterActive : ''"
|
||||
:leaveActiveClass="prefer.s.animation ? $style.transition_toggle_leaveActive : ''"
|
||||
:enterFromClass="prefer.s.animation ? $style.transition_toggle_enterFrom : ''"
|
||||
:leaveToClass="prefer.s.animation ? $style.transition_toggle_leaveTo : ''"
|
||||
@enter="enter"
|
||||
@afterEnter="afterEnter"
|
||||
@leave="leave"
|
||||
|
|
@ -56,9 +56,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onMounted, ref, shallowRef } from 'vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { getBgColor } from '@/scripts/get-bg-color.js';
|
||||
import { nextTick, onMounted, ref, useTemplateRef } from 'vue';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { getBgColor } from '@/utility/get-bg-color.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
defaultOpen?: boolean;
|
||||
|
|
@ -74,7 +74,7 @@ const props = withDefaults(defineProps<{
|
|||
spacerMax: 22,
|
||||
});
|
||||
|
||||
const rootEl = shallowRef<HTMLElement>();
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
const bgSame = ref(false);
|
||||
const opened = ref(props.defaultOpen);
|
||||
const openedAtLeastOnce = ref(props.defaultOpen);
|
||||
|
|
@ -116,7 +116,7 @@ function toggle() {
|
|||
}
|
||||
|
||||
onMounted(() => {
|
||||
const computedStyle = getComputedStyle(document.documentElement);
|
||||
const computedStyle = getComputedStyle(window.document.documentElement);
|
||||
const parentBg = getBgColor(rootEl.value?.parentElement) ?? 'transparent';
|
||||
const myBg = computedStyle.getPropertyValue('--MI_THEME-panel');
|
||||
bgSame.value = parentBg === myBg;
|
||||
|
|
|
|||
|
|
@ -39,13 +39,13 @@ import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|||
import * as Misskey from 'misskey-js';
|
||||
import { host } from '@@/js/config.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { claimAchievement } from '@/utility/achievements.js';
|
||||
import { pleaseLogin } from '@/utility/please-login.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
user: Misskey.entities.UserDetailed,
|
||||
|
|
@ -106,7 +106,7 @@ async function onClick() {
|
|||
userId: props.user.id,
|
||||
});
|
||||
} else {
|
||||
if (defaultStore.state.alwaysConfirmFollow && !hasPendingFollowRequestFromYou.value) {
|
||||
if (prefer.s.alwaysConfirmFollow && !hasPendingFollowRequestFromYou.value) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.tsx.followConfirm({ name: props.user.name || props.user.username }),
|
||||
|
|
@ -136,11 +136,11 @@ async function onClick() {
|
|||
} else {
|
||||
await misskeyApi('following/create', {
|
||||
userId: props.user.id,
|
||||
withReplies: defaultStore.state.defaultWithReplies,
|
||||
withReplies: prefer.s.defaultFollowWithReplies,
|
||||
});
|
||||
emit('update:user', {
|
||||
...props.user,
|
||||
withReplies: defaultStore.state.defaultWithReplies,
|
||||
withReplies: prefer.s.defaultFollowWithReplies,
|
||||
});
|
||||
hasPendingFollowRequestFromYou.value = true;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ import * as Misskey from 'misskey-js';
|
|||
import { computed, ref } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { selectFile } from '@/scripts/select-file.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { selectFile } from '@/utility/select-file.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
|
||||
const props = defineProps<{
|
||||
fileId?: string | null;
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
</div>
|
||||
<div v-else class="_fullinfo">
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
|
|
@ -71,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, shallowRef } from 'vue';
|
||||
import { reactive, useTemplateRef } from 'vue';
|
||||
import MkInput from './MkInput.vue';
|
||||
import MkTextarea from './MkTextarea.vue';
|
||||
import MkSwitch from './MkSwitch.vue';
|
||||
|
|
@ -80,7 +80,7 @@ import MkRange from './MkRange.vue';
|
|||
import MkButton from './MkButton.vue';
|
||||
import MkRadios from './MkRadios.vue';
|
||||
import XFile from './MkFormDialog.file.vue';
|
||||
import type { Form } from '@/scripts/form.js';
|
||||
import type { Form } from '@/utility/form.js';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
|
|
@ -99,7 +99,7 @@ const emit = defineEmits<{
|
|||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
const dialog = useTemplateRef('dialog');
|
||||
const values = reactive({});
|
||||
|
||||
for (const item in props.form) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
tail === 'left' ? $style.left : $style.right,
|
||||
negativeMargin === true && $style.negativeMargin,
|
||||
shadow === true && $style.shadow,
|
||||
accented === true && $style.accented
|
||||
]"
|
||||
>
|
||||
<div :class="$style.bg">
|
||||
|
|
@ -30,10 +31,12 @@ withDefaults(defineProps<{
|
|||
tail?: 'left' | 'right' | 'none';
|
||||
negativeMargin?: boolean;
|
||||
shadow?: boolean;
|
||||
accented?: boolean;
|
||||
}>(), {
|
||||
tail: 'right',
|
||||
negativeMargin: false,
|
||||
shadow: false,
|
||||
accented: false,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
@ -47,6 +50,10 @@ withDefaults(defineProps<{
|
|||
min-height: calc(var(--fukidashi-radius) * 2);
|
||||
padding-top: calc(var(--fukidashi-radius) * .13);
|
||||
|
||||
&.accented {
|
||||
--fukidashi-bg: var(--MI_THEME-accent);
|
||||
}
|
||||
|
||||
&.shadow {
|
||||
filter: drop-shadow(0 4px 32px var(--MI_THEME-shadow));
|
||||
}
|
||||
|
|
@ -77,7 +84,7 @@ withDefaults(defineProps<{
|
|||
|
||||
.content {
|
||||
position: relative;
|
||||
padding: 8px 12px;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.tail {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { galleryPost } from '../../.storybook/fakes.js';
|
||||
import MkGalleryPostPreview from './MkGalleryPostPreview.vue';
|
||||
export const Default = {
|
||||
|
|
|
|||
|
|
@ -35,14 +35,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import * as Misskey from 'misskey-js';
|
||||
import { computed, ref } from 'vue';
|
||||
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const props = defineProps<{
|
||||
post: Misskey.entities.GalleryPost;
|
||||
}>();
|
||||
|
||||
const hover = ref(false);
|
||||
const safe = computed(() => defaultStore.state.nsfw === 'ignore' || defaultStore.state.nsfw === 'respect' && !props.post.isSensitive);
|
||||
const safe = computed(() => prefer.s.nsfw === 'ignore' || prefer.s.nsfw === 'respect' && !props.post.isSensitive);
|
||||
const show = computed(() => safe.value || hover.value);
|
||||
|
||||
function enterHover(): void {
|
||||
|
|
|
|||
|
|
@ -13,14 +13,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, nextTick, watch, shallowRef, ref } from 'vue';
|
||||
import { onMounted, nextTick, watch, useTemplateRef, ref } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
|
||||
import { alpha } from '@/scripts/color.js';
|
||||
import { initChart } from '@/scripts/init-chart.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { store } from '@/store.js';
|
||||
import { useChartTooltip } from '@/use/use-chart-tooltip.js';
|
||||
import { alpha } from '@/utility/color.js';
|
||||
import { initChart } from '@/utility/init-chart.js';
|
||||
|
||||
initChart();
|
||||
|
||||
|
|
@ -35,8 +35,8 @@ const props = withDefaults(defineProps<{
|
|||
label: '',
|
||||
});
|
||||
|
||||
const rootEl = shallowRef<HTMLDivElement | null>(null);
|
||||
const chartEl = shallowRef<HTMLCanvasElement | null>(null);
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
const chartEl = useTemplateRef('chartEl');
|
||||
const now = new Date();
|
||||
let chartInstance: Chart | null = null;
|
||||
const fetching = ref(true);
|
||||
|
|
@ -106,7 +106,7 @@ async function renderChart() {
|
|||
|
||||
await nextTick();
|
||||
|
||||
const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300';
|
||||
const color = store.s.darkMode ? '#b4e900' : '#86b300';
|
||||
|
||||
// 視覚上の分かりやすさのため上から最も大きい3つの値の平均を最大値とする
|
||||
const max = values.slice().sort((a, b) => b - a).slice(0, 3).reduce((a, b) => a + b, 0) / 3;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue