Merge upstream

This commit is contained in:
ASTRO:? 2025-02-13 16:13:52 +09:00
commit bc9acabd6c
No known key found for this signature in database
GPG key ID: 8947F3AF5B0B4BFE
65 changed files with 502 additions and 500 deletions

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true" as="image" type="image/png" crossorigin="anonymous">
<link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true" as="image" type="image/jpeg" crossorigin="anonymous">
<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@3.5.0/dist/tabler-icons.min.css">
<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
<link rel="stylesheet" href="https://unpkg.com/@fontsource/m-plus-rounded-1c/index.css">
<style>
html {

View file

@ -4,7 +4,6 @@
"type": "module",
"scripts": {
"watch": "vite",
"dev": "vite --config vite.config.local-dev.ts --debug hmr",
"build": "vite build",
"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"",
"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
@ -27,7 +26,7 @@
"@rollup/plugin-typescript": "12.1.2",
"@rollup/pluginutils": "5.1.4",
"@syuilo/aiscript": "0.19.0",
"@tabler/icons-webfont": "3.28.1",
"@tabler/icons-webfont": "3.29.0",
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.2.1",
"@vue/compiler-sfc": "3.5.13",
@ -41,7 +40,7 @@
"chartjs-chart-matrix": "2.0.1",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.2.0",
"chromatic": "11.24.0",
"chromatic": "11.25.2",
"compare-versions": "6.1.1",
"cropperjs": "2.0.0-rc.0",
"date-fns": "4.1.0",
@ -59,13 +58,13 @@
"misskey-reversi": "workspace:*",
"photoswipe": "5.4.4",
"punycode.js": "2.3.1",
"rollup": "4.30.1",
"rollup": "4.34.0",
"sanitize-html": "2.14.0",
"sass": "1.83.4",
"shiki": "1.27.2",
"shiki": "2.2.0",
"strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
"three": "0.172.0",
"three": "0.173.0",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.10",
@ -73,7 +72,7 @@
"typescript": "5.7.3",
"uuid": "11.0.5",
"v-code-diff": "1.13.1",
"vite": "6.0.7",
"vite": "6.0.11",
"vue": "3.5.13",
"vue-gtag": "2.0.1",
"vuedraggable": "next",
@ -82,49 +81,49 @@
"devDependencies": {
"@misskey-dev/eslint-plugin": "1.0.0",
"@misskey-dev/summaly": "github:MisskeyIO/summaly#5.1.3",
"@storybook/addon-actions": "8.5.0",
"@storybook/addon-essentials": "8.5.0",
"@storybook/addon-interactions": "8.5.0",
"@storybook/addon-links": "8.5.0",
"@storybook/addon-mdx-gfm": "8.5.0",
"@storybook/addon-storysource": "8.5.0",
"@storybook/blocks": "8.5.0",
"@storybook/components": "8.5.0",
"@storybook/core-events": "8.5.0",
"@storybook/manager-api": "8.5.0",
"@storybook/preview-api": "8.5.0",
"@storybook/react": "8.5.0",
"@storybook/react-vite": "8.5.0",
"@storybook/test": "8.5.0",
"@storybook/theming": "8.5.0",
"@storybook/types": "8.5.0",
"@storybook/vue3": "8.5.0",
"@storybook/vue3-vite": "8.5.0",
"@storybook/addon-actions": "8.5.2",
"@storybook/addon-essentials": "8.5.2",
"@storybook/addon-interactions": "8.5.2",
"@storybook/addon-links": "8.5.2",
"@storybook/addon-mdx-gfm": "8.5.2",
"@storybook/addon-storysource": "8.5.2",
"@storybook/blocks": "8.5.2",
"@storybook/components": "8.5.2",
"@storybook/core-events": "8.5.2",
"@storybook/manager-api": "8.5.2",
"@storybook/preview-api": "8.5.2",
"@storybook/react": "8.5.2",
"@storybook/react-vite": "8.5.2",
"@storybook/test": "8.5.2",
"@storybook/theming": "8.5.2",
"@storybook/types": "8.5.2",
"@storybook/vue3": "8.5.2",
"@storybook/vue3-vite": "8.5.2",
"@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "^1.6.4",
"@types/escape-regexp": "0.0.3",
"@types/estree": "1.0.6",
"@types/matter-js": "0.19.8",
"@types/micromatch": "4.0.9",
"@types/node": "22.10.7",
"@types/node": "22.13.0",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.13.0",
"@types/three": "0.172.0",
"@types/three": "0.173.0",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.5.13",
"@types/ws": "8.5.14",
"@typescript-eslint/eslint-plugin": "7.10.0",
"@typescript-eslint/parser": "7.10.0",
"@vitest/coverage-v8": "2.1.8",
"@vitest/coverage-v8": "3.0.4",
"@vue/runtime-core": "3.5.13",
"acorn": "8.14.0",
"cross-env": "7.0.3",
"cypress": "13.17.0",
"cypress": "14.0.1",
"eslint": "8.57.1",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "9.32.0",
"fast-glob": "3.3.3",
"happy-dom": "16.6.0",
"happy-dom": "16.8.1",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"msw": "2.7.0",
@ -134,10 +133,10 @@
"react": "19.0.0",
"react-dom": "19.0.0",
"start-server-and-test": "2.0.10",
"storybook": "8.5.0",
"storybook": "8.5.2",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "2.1.8",
"vitest": "3.0.4",
"vitest-fetch-mock": "0.3.0",
"vue-component-type-helpers": "2.2.0",
"vue-eslint-parser": "9.4.3",

View file

@ -1,110 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
// devモードで起動される際index.htmlを使うときはrouterが暴発してしまってうまく読み込めない。
// よって、devモードとして起動されるときはビルド時に組み込む形としておく。
// (pnpm start時はpugファイルの中で静的リソースとして読み込むようになっており、この問題は起こっていない)
import '@tabler/icons-webfont/dist/tabler-icons.scss';
await main();
import('@/_boot_.js');
/**
* backend/src/server/web/boot.jsで差し込まれている起動処理のうち
*/
async function main() {
const forceError = localStorage.getItem('forceError');
if (forceError != null) {
renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.');
}
const metaRes = await window.fetch('/api/meta', {
method: 'POST',
body: JSON.stringify({}),
credentials: 'omit',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json',
},
});
if (metaRes.status !== 200) {
renderError('META_FETCH');
return;
}
const meta = await metaRes.json();
//#region Detect language & fetch translations
// dev-modeの場合は常に取り直す
const supportedLangs = _LANGS_.map(it => it[0]);
let lang: string | null | undefined = localStorage.getItem('lang');
if (lang == null || !supportedLangs.includes(lang)) {
if (supportedLangs.includes(navigator.language)) {
lang = navigator.language;
} else {
lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
// Fallback
if (lang == null) lang = 'ko-KR';
}
}
// TODO:今のままだと言語ファイル変更後はpnpm devをリスタートする必要があるので、chokidarを使ったり等で対応できるようにする
const locale = _LANGS_FULL_.find(it => it[0] === lang);
localStorage.setItem('lang', lang);
localStorage.setItem('locale', JSON.stringify(locale[1]));
localStorage.setItem('localeVersion', _VERSION_);
//#endregion
//#region Theme
const theme = localStorage.getItem('theme');
if (theme) {
for (const [k, v] of Object.entries(JSON.parse(theme))) {
document.documentElement.style.setProperty(`--${k}`, v.toString());
// HTMLの theme-color 適用
if (k === 'htmlThemeColor') {
for (const tag of document.head.children) {
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
tag.setAttribute('content', v);
break;
}
}
}
}
}
const colorScheme = localStorage.getItem('colorScheme');
if (colorScheme) {
document.documentElement.style.setProperty('color-scheme', colorScheme);
}
//#endregion
const fontSize = localStorage.getItem('fontSize');
if (fontSize) {
document.documentElement.classList.add('f-' + fontSize);
}
const useSystemFont = localStorage.getItem('useSystemFont');
if (useSystemFont) {
document.documentElement.classList.add('useSystemFont');
}
const wallpaper = localStorage.getItem('wallpaper') ?? meta.backgroundImageUrl;
if (wallpaper) {
document.documentElement.style.background = `url(${wallpaper}) no-repeat fixed center`;
document.documentElement.style.backgroundSize = 'cover';
}
const customCss = localStorage.getItem('customCss');
if (customCss && customCss.length > 0) {
const style = document.createElement('style');
style.innerHTML = customCss;
document.head.appendChild(style);
}
}
function renderError(code: string, details?: string) {
console.log(code, details);
}

View file

@ -43,6 +43,12 @@ export async function signout() {
if (!$i) return;
waiting();
document.cookie.split(';').forEach((cookie) => {
const cookieName = cookie.split('=')[0].trim();
if (cookieName === 'token') {
document.cookie = `${cookieName}=; max-age=0; path=/`;
}
});
miLocalStorage.removeItem('account');
await removeAccount($i.id);
const accounts = await getAccounts();

View file

@ -100,6 +100,11 @@ export async function common(createVue: () => App<Element>) {
// タッチデバイスでCSSの:hoverを機能させる
document.addEventListener('touchend', () => {}, { passive: true });
// URLに#pswpを含む場合は取り除く
if (location.hash === '#pswp') {
history.replaceState(null, '', location.href.replace('#pswp', ''));
}
// 一斉リロード
reloadChannel.addEventListener('message', path => {
if (path !== null) location.href = path;

View file

@ -26,22 +26,34 @@ let prevTime = 0;
let angle1 = 0;
let angle2 = 0;
let scene, camera, renderer, width, height, uniforms, texture, maskTexture, dataArray1, dataArray2, dataArrayOrigin, bufferLength: number;
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera();
const renderer = new THREE.WebGLRenderer({ antialias: true });
let width: number;
let height: number;
let uniforms: { [p: string]: THREE.IUniform };
let texture: THREE.Texture;
let maskTexture: THREE.Texture;
let dataArray1: Uint8Array;
let dataArray2: Uint8Array;
let dataArrayOrigin: Uint8Array;
let bufferLength: number;
const init = () => {
const parent = container.value ?? { offsetWidth: 0 };
width = parent.offsetWidth;
height = Math.floor(width * 9 / 16);
scene = new THREE.Scene();
camera = new THREE.OrthographicCamera();
scene.clear();
camera.clear();
camera.left = width / -2;
camera.right = width / 2;
camera.top = height / 2;
camera.bottom = height / -2;
camera.updateProjectionMatrix();
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
if (container.value) {
@ -176,7 +188,7 @@ const animate = (time) => {
renderer.render(scene, camera);
};
const onResize = () => {
const resize = () => {
const parent = container.value ?? { offsetWidth: 0 };
width = parent.offsetWidth;
height = Math.floor(width * 9 / 16);
@ -189,17 +201,25 @@ const onResize = () => {
uniforms.resolution.value.set(width, height);
};
const ro = new ResizeObserver((entries, observer) => {
resize();
});
onMounted(async () => {
nextTick().then(() => {
init();
window.addEventListener('resize', onResize);
resize();
});
if (!container.value) return;
ro.observe(container.value);
});
onUnmounted(() => {
if (renderer) {
renderer.dispose();
}
ro.disconnect();
});
defineExpose({

View file

@ -48,8 +48,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</audio>
<div :class="[$style.controlsChild, $style.controlsLeft]">
<button class="_button" :class="$style.controlButton" @click.prevent.stop="togglePlayPause">
<i v-if="isPlaying" class="ti ti-player-pause-filled"></i>
<i v-else class="ti ti-player-play-filled"></i>
<i v-if="isPlaying" class="ti-filled ti-filled-player-pause"></i>
<i v-else class="ti-filled ti-filled-player-play"></i>
</button>
</div>
<div :class="[$style.controlsChild, $style.controlsRight]">

View file

@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<source :src="video.url">
</video>
<button v-if="isReady && !isPlaying" class="_button" :class="$style.videoOverlayPlayButton" @click="togglePlayPause"><i class="ti ti-player-play-filled"></i></button>
<button v-if="isReady && !isPlaying" class="_button" :class="$style.videoOverlayPlayButton" @click="togglePlayPause"><i class="ti-filled ti-filled-player-play"></i></button>
<div v-else-if="!isActuallyPlaying" :class="$style.videoLoading">
<MkLoading/>
</div>
@ -75,8 +75,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="videoControls" :class="$style.videoControls" @click.self="togglePlayPause">
<div :class="[$style.controlsChild, $style.controlsLeft]">
<button class="_button" :class="$style.controlButton" @click.prevent.stop="togglePlayPause">
<i v-if="isPlaying" class="ti ti-player-pause-filled"></i>
<i v-else class="ti ti-player-play-filled"></i>
<i v-if="isPlaying" class="ti-filled ti-filled-player-pause"></i>
<i v-else class="ti-filled ti-filled-player-play"></i>
</button>
</div>
<div :class="[$style.controlsChild, $style.controlsRight]">

View file

@ -119,7 +119,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-ban"></i>
</button>
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
<i v-if=" (appearNote.reactionAcceptance === 'likeOnly' || !$i?.policies.canUseReaction) && appearNote.myReaction != null " class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
<i v-if=" (appearNote.reactionAcceptance === 'likeOnly' || !$i?.policies.canUseReaction) && appearNote.myReaction != null " class="ti-filled ti-filled-heart" style="color: var(--eventReactionHeart);"></i>
<i v-else-if="appearNote.myReaction != null " class="ti ti-minus" style="color: var(--accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly' || $i && !$i.policies.canUseReaction" class="ti ti-heart"></i>
<i v-else class="ti ti-plus"></i>
@ -594,7 +594,7 @@ function emitUpdReaction(emoji: string, delta: number) {
contain: content;
content-visibility: auto;
contain-intrinsic-size: auto none auto 128px;
contain-intrinsic-size: none auto 128px;
&:focus-visible {
outline: none;

View file

@ -127,7 +127,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-ban"></i>
</button>
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
<i v-if=" (appearNote.reactionAcceptance === 'likeOnly' || !$i?.policies.canUseReaction) && appearNote.myReaction != null " class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
<i v-if=" (appearNote.reactionAcceptance === 'likeOnly' || !$i?.policies.canUseReaction) && appearNote.myReaction != null " class="ti-filled ti-filled-heart" style="color: var(--eventReactionHeart);"></i>
<i v-else-if="appearNote.myReaction != null " class="ti ti-minus" style="color: var(--accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly' || $i && !$i.policies.canUseReaction" class="ti ti-heart"></i>
<i v-else class="ti ti-plus"></i>

View file

@ -220,7 +220,7 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
contain: content;
content-visibility: auto;
contain-intrinsic-size: auto none auto 100px;
contain-intrinsic-size: none auto 100px;
}
.head {

View file

@ -24,7 +24,7 @@ import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
import { useStream } from '@/stream.js';
import * as sound from '@/scripts/sound.js';
import { deepMerge } from '@/scripts/merge.js';
import { $i, iAmModerator } from '@/account.js';
import { $i } from '@/account.js';
import { instance } from '@/instance.js';
import { defaultStore } from '@/store.js';
import { Paging } from '@/components/MkPagination.vue';
@ -108,7 +108,6 @@ async function prepend(data) {
let connection: Misskey.ChannelConnection | null = null;
let connection2: Misskey.ChannelConnection | null = null;
let paginationQuery: Paging | null = null;
const minimize = !iAmModerator;
const stream = useStream();
@ -117,13 +116,13 @@ function connectChannel() {
if (props.antenna == null) return;
connection = stream.useChannel('antenna', {
antennaId: props.antenna,
minimize: minimize,
minimize: true,
});
} else if (props.src === 'home') {
connection = stream.useChannel('homeTimeline', {
withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined,
minimize: minimize,
minimize: true,
});
connection2 = stream.useChannel('main');
} else if (props.src === 'local') {
@ -131,27 +130,27 @@ function connectChannel() {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
minimize: minimize,
minimize: true,
});
} else if (props.src === 'media') {
connection = stream.useChannel('hybridTimeline', {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: true,
minimize: minimize,
minimize: true,
});
} else if (props.src === 'social') {
connection = stream.useChannel('hybridTimeline', {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
minimize: minimize,
minimize: true,
});
} else if (props.src === 'global') {
connection = stream.useChannel('globalTimeline', {
withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined,
minimize: minimize,
minimize: true,
});
} else if (props.src === 'mentions') {
connection = stream.useChannel('main');
@ -170,19 +169,19 @@ function connectChannel() {
withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined,
listId: props.list,
minimize: minimize,
minimize: true,
});
} else if (props.src === 'channel') {
if (props.channel == null) return;
connection = stream.useChannel('channel', {
channelId: props.channel,
minimize: minimize,
minimize: true,
});
} else if (props.src === 'role') {
if (props.role == null) return;
connection = stream.useChannel('roleTimeline', {
roleId: props.role,
minimize: minimize,
minimize: true,
});
}
if (props.src !== 'directs' && props.src !== 'mentions') connection?.on('note', prepend);

View file

@ -1,36 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<!--
開発モードのviteはこのファイルを起点にサーバーを起動します。
このファイルに書かれた [t]js のリンクと (s)cssのリンクと、その依存関係にあるファイルはビルドされます
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>[DEV] Loading...</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy-Report-Only"
content="default-src 'self' https://newassets.hcaptcha.com/ https://challenges.cloudflare.com/ http://localhost:7493/ https://fonts.gstatic.com/ https://www.google-analytics.com/ https://www.googletagmanager.com/;
worker-src 'self';
script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com https://www.googletagmanager.com https://esm.sh;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://fonts.gstatic.com https://www.googletagmanager.com;
img-src 'self' data: blob: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://fonts.gstatic.com https://www.googletagmanager.com;
media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com https://api.pwnedpasswords.com https://www.google-analytics.com https://analytics.google.com;
frame-src *;"
/>
<meta property="og:site_name" content="[DEV BUILD] Misskey" />
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id="misskey_app"></div>
<script type="module" src="./_dev_boot_.ts"></script>
</body>
</html>

View file

@ -18,6 +18,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>
<div v-for="ad in ads" class="_panel _gaps_m" :class="$style.ad">
<MkAd v-if="ad.url" :key="ad.id" :specify="ad"/>
<MkInput v-if="ad.id" v-model="ad.id" :readonly="true">
<template #label>ID</template>
</MkInput>
<MkInput v-model="ad.url" type="url">
<template #label>URL</template>
</MkInput>

View file

@ -2,13 +2,29 @@
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="900">
<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
<MkInput v-model="movedFromId" style="margin: 0; flex: 1;">
<template #label> {{ i18n.ts.moveFromId }}</template>
</MkInput>
<MkInput v-model="movedToId" style="margin: 0; flex: 1;">
<template #label> {{ i18n.ts.movedToId }}</template>
</MkInput>
<div style="display: flex; flex-direction: column; gap: var(--margin); flex-wrap: wrap;">
<div :class="$style.inputs">
<MkSelect v-model="from" :class="$style.input">
<template #label>{{ i18n.ts._accountMigration.movedFromServer }}</template>
<option value="all">{{ i18n.ts.all }}</option>
<option value="remote">{{ i18n.ts.remote }}</option>
<option value="local">{{ i18n.ts.local }}</option>
</MkSelect>
<MkSelect v-model="to" :class="$style.input">
<template #label>{{ i18n.ts._accountMigration.movedToServer }}</template>
<option value="all">{{ i18n.ts.all }}</option>
<option value="remote">{{ i18n.ts.remote }}</option>
<option value="local">{{ i18n.ts.local }}</option>
</MkSelect>
</div>
<div :class="$style.inputs">
<MkInput v-model="movedFromId" :class="$style.input">
<template #label> {{ i18n.ts.moveFromId }}</template>
</MkInput>
<MkInput v-model="movedToId" :class="$style.input">
<template #label> {{ i18n.ts.movedToId }}</template>
</MkInput>
</div>
</div>
<MkPagination v-slot="{items}" ref="logs" :pagination="pagination" style="margin-top: var(--margin);">
@ -48,11 +64,15 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { userPage } from '@/filters/user.js';
import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue';
const logs = shallowRef<InstanceType<typeof MkPagination>>();
const movedToId = ref('');
const movedFromId = ref('');
const from = ref('all');
const to = ref('all');
const pagination = {
endpoint: 'admin/show-user-account-move-logs' as const,
@ -60,6 +80,8 @@ const pagination = {
params: computed(() => ({
movedFromId: movedFromId.value === '' ? null : movedFromId.value,
movedToId: movedToId.value === '' ? null : movedToId.value,
from: from.value,
to: to.value,
})),
};
@ -95,4 +117,14 @@ definePageMetadata(() => ({
flex-direction: column;
}
.inputs {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.input {
margin: 0;
flex: 1;
}
</style>

View file

@ -182,24 +182,24 @@ definePageMetadata(() => ({
}
.rkxwuolj {
> .files {
> .file {
> img {
display: block;
max-width: 100%;
max-height: 500px;
margin: 0 auto;
}
& + .file {
margin-top: 16px;
}
}
}
> .body {
padding: 32px;
> .files {
> .file {
> img {
display: block;
max-width: 100%;
max-height: 500px;
margin: 0 auto;
}
& + .file {
margin-top: 16px;
}
}
}
> .title {
font-weight: bold;
font-size: 1.2em;

View file

@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="game.map == null"><i class="ti ti-dice"></i></div>
<div v-else :class="$style.board" :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }">
<div v-for="(x, i) in game.map.join('')" :class="[$style.boardCell, { [$style.boardCellNone]: x == ' ' }]" @click="onMapCellClick(i, x)">
<i v-if="x === 'b' || x === 'w'" style="pointer-events: none; user-select: none;" :class="x === 'b' ? 'ti ti-circle-filled' : 'ti ti-circle'"></i>
<i v-if="x === 'b' || x === 'w'" style="pointer-events: none; user-select: none;" :class="x === 'b' ? 'ti-filled ti-filled-circle' : 'ti ti-circle'"></i>
</div>
</div>
</div>

View file

@ -145,7 +145,6 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { infoImageUrl } from '@/instance.js';
import { signinRequired } from '@/account.js';
import MkFolder from '@/components/MkFolder.vue';
@ -276,7 +275,7 @@ async function toggleBlockItem(item) {
}
async function saveMutedWords(mutedWords: (string | string[])[]) {
await misskeyApi('i/update', { mutedWords });
await os.apiWithDialog('i/update', { mutedWords });
}
const headerActions = computed(() => []);

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { query } from '@/scripts/url.js';
import { appendQuery, omitHttps, query } from '@/scripts/url.js';
import { url } from '@/config.js';
import { instance } from '@/instance.js';
@ -12,18 +12,26 @@ export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji'
if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) {
// もう既にproxyっぽそうだったらurlを取り出す
imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl;
const url = (new URL(imageUrl)).searchParams.get('url');
if (url) {
imageUrl = url;
} else if (imageUrl.startsWith(instance.mediaProxy + '/')) {
imageUrl = imageUrl.slice(instance.mediaProxy.length + 1);
} else if (imageUrl.startsWith('/proxy/')) {
imageUrl = imageUrl.slice('/proxy/'.length);
} else if (imageUrl.startsWith(localProxy + '/')) {
imageUrl = imageUrl.slice(localProxy.length + 1);
}
}
return `${mustOrigin ? localProxy : instance.mediaProxy}/${
type === 'preview' ? 'preview.webp'
: 'image.webp'
}?${query({
url: imageUrl,
...(!noFallback ? { 'fallback': '1' } : {}),
...(type ? { [type]: '1' } : {}),
...(mustOrigin ? { origin: '1' } : {}),
})}`;
return appendQuery(
`${mustOrigin ? localProxy : instance.mediaProxy}/${type === 'preview' ? 'preview' : 'image'}/${encodeURIComponent(omitHttps(imageUrl))}`,
query({
...(!noFallback ? { 'fallback': '1' } : {}),
...(type ? { [type]: '1' } : {}),
...(mustOrigin ? { origin: '1' } : {}),
}),
);
}
export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null {
@ -46,8 +54,8 @@ export function getStaticImageUrl(baseUrl: string): string {
return u.href;
}
return `${instance.mediaProxy}/static.webp?${query({
url: u.href,
static: '1',
})}`;
return appendQuery(
`${instance.mediaProxy}/static/${encodeURIComponent(omitHttps(u.href))}`,
query({ static: '1' }),
);
}

View file

@ -8,7 +8,7 @@
* 2. undefinedの時はクエリを付けない
* new URLSearchParams(obj)
*/
export function query(obj: Record<string, any>): string {
export function query(obj: Record<string, unknown>): string {
const params = Object.entries(obj)
.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined)
.reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>);
@ -21,3 +21,9 @@ export function query(obj: Record<string, any>): string {
export function appendQuery(url: string, query: string): string {
return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`;
}
export function omitHttps(url: string): string {
if (url.startsWith('https://')) return url.slice(8);
if (url.startsWith('https%3A%2F%2F')) return url.slice(14);
return url;
}

View file

@ -177,6 +177,16 @@ rt {
}
}
.ti-filled {
width: 1.28em;
vertical-align: -12%;
line-height: 1em;
&:before {
font-size: 128%;
}
}
.ti-fw {
display: inline-block;
text-align: center;

View file

@ -1,89 +0,0 @@
import dns from 'dns';
import { readFile } from 'node:fs/promises';
import { defineConfig } from 'vite';
import * as yaml from 'js-yaml';
import locales from '../../locales/index.js';
import { getConfig } from './vite.config.js';
dns.setDefaultResultOrder('ipv4first');
const defaultConfig = getConfig();
const { port } = yaml.load(await readFile('../../.config/default.yml', 'utf-8'));
const httpUrl = `http://localhost:${port}/`;
const websocketUrl = `ws://localhost:${port}/`;
const devConfig = {
// 基本の設定は vite.config.js から引き継ぐ
...defaultConfig,
root: 'src',
publicDir: '../assets',
base: './',
server: {
host: 'localhost',
port: 5173,
proxy: {
'/api': {
changeOrigin: true,
target: httpUrl,
},
'/assets': httpUrl,
'/static-assets': httpUrl,
'/client-assets': httpUrl,
'/files': httpUrl,
'/twemoji': httpUrl,
'/fluent-emoji': httpUrl,
'/sw.js': httpUrl,
'/streaming': {
target: websocketUrl,
ws: true,
},
'/favicon.ico': httpUrl,
'/identicon': {
target: httpUrl,
rewrite(path) {
return path.replace('@localhost:5173', '');
},
},
'/url': httpUrl,
'/proxy': httpUrl,
'/_info_card_': httpUrl,
'/bios': httpUrl,
'/cli': httpUrl,
'/inbox': httpUrl,
'/emoji/': httpUrl,
'/queue': httpUrl,
'/notes': {
target: httpUrl,
headers: {
'Accept': 'application/activity+json',
},
},
'/users': {
target: httpUrl,
headers: {
'Accept': 'application/activity+json',
},
},
'/.well-known': {
target: httpUrl,
},
},
},
build: {
...defaultConfig.build,
rollupOptions: {
...defaultConfig.build?.rollupOptions,
input: 'index.html',
},
},
define: {
...defaultConfig.define,
_LANGS_FULL_: JSON.stringify(Object.entries(locales)),
},
};
export default defineConfig(({ command, mode }) => devConfig);

View file

@ -3,6 +3,8 @@ import pluginReplace from '@rollup/plugin-replace';
import pluginVue from '@vitejs/plugin-vue';
import typescript from '@rollup/plugin-typescript';
import { type UserConfig, defineConfig } from 'vite';
import * as yaml from 'js-yaml';
import { promises as fsp } from 'fs';
import locales from '../../locales/index.js';
import meta from '../../package.json';
@ -10,6 +12,9 @@ import packageInfo from './package.json' with { type: 'json' };
import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name.js';
import pluginJson5 from './vite.json5.js';
const url = process.env.NODE_ENV === 'development' ? yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')).url : null;
const host = url ? (new URL(url)).hostname : undefined;
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue'];
/**
@ -65,6 +70,7 @@ export function getConfig(): UserConfig {
base: '/vite/',
server: {
host,
port: 5173,
},