Merge branch 'develop' into img-max
This commit is contained in:
6
packages/frontend/.storybook/.gitignore
vendored
6
packages/frontend/.storybook/.gitignore
vendored
@@ -1,9 +1,7 @@
|
||||
# (cd path/to/frontend; pnpm tsc -p .storybook)
|
||||
# (cd path/to/frontend; node .storybook/generate.js)
|
||||
/changes.js
|
||||
/generate.js
|
||||
# (cd path/to/frontend; node .storybook/preload-locale.js)
|
||||
/preload-locale.js
|
||||
/locale.ts
|
||||
# (cd path/to/frontend; node .storybook/preload-theme.js)
|
||||
/main.js
|
||||
/preload-theme.js
|
||||
/themes.ts
|
||||
|
80
packages/frontend/.storybook/changes.ts
Normal file
80
packages/frontend/.storybook/changes.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import micromatch from 'micromatch';
|
||||
import main from './main';
|
||||
|
||||
interface Stats {
|
||||
readonly modules: readonly {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly reasons: readonly {
|
||||
readonly moduleName: string;
|
||||
}[];
|
||||
}[];
|
||||
}
|
||||
|
||||
fs.readFile(
|
||||
path.resolve(__dirname, '../storybook-static/preview-stats.json')
|
||||
).then((buffer) => {
|
||||
const stats: Stats = JSON.parse(buffer.toString());
|
||||
const keys = new Set(stats.modules.map((stat) => stat.id));
|
||||
const map = new Map(
|
||||
Array.from(keys, (key) => [
|
||||
key,
|
||||
new Set(
|
||||
stats.modules
|
||||
.filter((stat) => stat.id === key)
|
||||
.flatMap((stat) => stat.reasons)
|
||||
.map((reason) => reason.moduleName)
|
||||
),
|
||||
])
|
||||
);
|
||||
const modules = new Set(
|
||||
process.argv
|
||||
.slice(2)
|
||||
.map((arg) =>
|
||||
path.relative(
|
||||
path.resolve(__dirname, '..'),
|
||||
path.resolve(__dirname, '../../..', arg)
|
||||
)
|
||||
)
|
||||
.map((path) => (path.startsWith('.') ? path : `./${path}`))
|
||||
);
|
||||
if (
|
||||
micromatch(Array.from(modules), [
|
||||
'../../assets/**',
|
||||
'../../fluent-emojis/**',
|
||||
'../../locales/**',
|
||||
'../../misskey-assets/**',
|
||||
'assets/**',
|
||||
'public/**',
|
||||
'../../pnpm-lock.yaml',
|
||||
]).length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
for (;;) {
|
||||
const oldSize = modules.size;
|
||||
for (const module of Array.from(modules)) {
|
||||
if (map.has(module)) {
|
||||
for (const dependency of Array.from(map.get(module)!)) {
|
||||
modules.add(dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (modules.size === oldSize) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const stories = micromatch(
|
||||
Array.from(modules),
|
||||
main.stories.map((story) => `./${path.relative('..', story)}`)
|
||||
);
|
||||
if (stories.length) {
|
||||
for (const story of stories) {
|
||||
process.stdout.write(` --only-story-files ${story}`);
|
||||
}
|
||||
} else {
|
||||
process.stdout.write(` --skip`);
|
||||
}
|
||||
});
|
@@ -1,6 +1,7 @@
|
||||
import { resolve } from 'node:path';
|
||||
import type { StorybookConfig } from '@storybook/vue3-vite';
|
||||
import { mergeConfig } from 'vite';
|
||||
import turbosnap from 'vite-plugin-turbosnap';
|
||||
const config = {
|
||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
addons: [
|
||||
@@ -20,8 +21,13 @@ const config = {
|
||||
core: {
|
||||
disableTelemetry: true,
|
||||
},
|
||||
async viteFinal(config, options) {
|
||||
async viteFinal(config) {
|
||||
return mergeConfig(config, {
|
||||
plugins: [
|
||||
turbosnap({
|
||||
rootDir: config.root ?? process.cwd(),
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
target: [
|
||||
'chrome108',
|
||||
|
@@ -1,4 +1,10 @@
|
||||
<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@2.12.0/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/@fontsource/m-plus-rounded-1c/index.css">
|
||||
<style>
|
||||
html {
|
||||
font-family: 'Hiragino Maru Gothic Pro', 'BIZ UDGothic', Roboto, HelveticaNeue, Arial, 'M PLUS Rounded 1c', sans-serif;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
window.global = window;
|
||||
</script>
|
||||
|
@@ -18,5 +18,10 @@
|
||||
"jsx": "react",
|
||||
"jsxFactory": "h"
|
||||
},
|
||||
"files": ["./generate.tsx", "./preload-locale.ts", "./preload-theme.ts"]
|
||||
"files": [
|
||||
"./changes.ts",
|
||||
"./generate.tsx",
|
||||
"./preload-locale.ts",
|
||||
"./preload-theme.ts"
|
||||
]
|
||||
}
|
||||
|
@@ -58,13 +58,13 @@
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"syuilo-password-strength": "0.0.1",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.150.1",
|
||||
"three": "0.151.3",
|
||||
"throttle-debounce": "5.0.0",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tsc-alias": "1.8.5",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typescript": "5.0.2",
|
||||
"typescript": "5.0.3",
|
||||
"uuid": "9.0.0",
|
||||
"vanilla-tilt": "1.8.0",
|
||||
"vite": "4.2.1",
|
||||
@@ -91,13 +91,14 @@
|
||||
"@storybook/types": "7.0.2",
|
||||
"@storybook/vue3": "7.0.2",
|
||||
"@storybook/vue3-vite": "7.0.2",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/vue": "^6.6.1",
|
||||
"@testing-library/jest-dom": "5.16.5",
|
||||
"@testing-library/vue": "7.0.0",
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/estree": "^1.0.0",
|
||||
"@types/estree": "1.0.0",
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@types/matter-js": "0.18.2",
|
||||
"@types/micromatch": "3.1.1",
|
||||
"@types/node": "18.15.11",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/sanitize-html": "2.9.0",
|
||||
@@ -108,32 +109,34 @@
|
||||
"@types/uuid": "9.0.1",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.57.0",
|
||||
"@typescript-eslint/parser": "5.57.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.57.1",
|
||||
"@typescript-eslint/parser": "5.57.1",
|
||||
"@vitest/coverage-c8": "^0.29.8",
|
||||
"@vue/runtime-core": "3.2.47",
|
||||
"astring": "^1.8.4",
|
||||
"chokidar-cli": "^3.0.0",
|
||||
"chromatic": "^6.17.2",
|
||||
"astring": "1.8.4",
|
||||
"chokidar-cli": "3.0.0",
|
||||
"chromatic": "6.17.3",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "12.9.0",
|
||||
"eslint": "8.37.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-vue": "9.10.0",
|
||||
"fast-glob": "^3.2.12",
|
||||
"fast-glob": "3.2.12",
|
||||
"happy-dom": "8.9.0",
|
||||
"msw": "^1.1.0",
|
||||
"msw-storybook-addon": "^1.8.0",
|
||||
"prettier": "^2.8.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"micromatch": "3.1.10",
|
||||
"msw": "1.2.1",
|
||||
"msw-storybook-addon": "1.8.0",
|
||||
"prettier": "2.8.7",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"start-server-and-test": "2.0.0",
|
||||
"storybook": "7.0.2",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"summaly": "github:misskey-dev/summaly",
|
||||
"vitest": "^0.29.8",
|
||||
"vitest-fetch-mock": "^0.2.2",
|
||||
"vue-eslint-parser": "9.1.0",
|
||||
"vite-plugin-turbosnap": "^1.0.1",
|
||||
"vitest": "0.29.8",
|
||||
"vitest-fetch-mock": "0.2.2",
|
||||
"vue-eslint-parser": "9.1.1",
|
||||
"vue-tsc": "1.2.0"
|
||||
}
|
||||
}
|
||||
|
32
packages/frontend/src/components/MkAccountMoved.vue
Normal file
32
packages/frontend/src/components/MkAccountMoved.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div :class="$style.root">
|
||||
<i class="ti ti-plane-departure" style="margin-right: 8px;"></i>
|
||||
{{ i18n.ts.accountMoved }}
|
||||
<MkMention :class="$style.link" :username="acct" :host="host ?? localHost"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MkMention from './MkMention.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { host as localHost } from '@/config';
|
||||
|
||||
defineProps<{
|
||||
acct: string;
|
||||
host: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
padding: 16px;
|
||||
font-size: 90%;
|
||||
background: var(--infoWarnBg);
|
||||
color: var(--error);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.link {
|
||||
margin-left: 4px;
|
||||
}
|
||||
</style>
|
@@ -17,8 +17,8 @@
|
||||
<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>
|
||||
<span v-if="okButtonDisabled && disabledReason === 'charactersExceeded'" v-text="i18n.t('_dialog.charactersExceeded', { current: (inputValue as string).length, max: input.maxLength ?? 'NaN' })" />
|
||||
<span v-else-if="okButtonDisabled && disabledReason === 'charactersBelow'" v-text="i18n.t('_dialog.charactersBelow', { current: (inputValue as string).length, min: input.minLength ?? 'NaN' })" />
|
||||
<span v-if="okButtonDisabled && disabledReason === 'charactersExceeded'" v-text="i18n.t('_dialog.charactersExceeded', { current: (inputValue as string).length, max: input.maxLength ?? 'NaN' })"/>
|
||||
<span v-else-if="okButtonDisabled && disabledReason === 'charactersBelow'" v-text="i18n.t('_dialog.charactersBelow', { current: (inputValue as string).length, min: input.minLength ?? 'NaN' })"/>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkSelect v-if="select" v-model="selectedValue" autofocus>
|
||||
@@ -32,11 +32,11 @@
|
||||
</template>
|
||||
</MkSelect>
|
||||
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
|
||||
<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" :disabled="okButtonDisabled" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
|
||||
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
|
||||
<MkButton v-if="showOkButton" inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabled" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
|
||||
<MkButton v-if="showCancelButton || input || select" inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
|
||||
</div>
|
||||
<div v-if="actions" :class="$style.buttons">
|
||||
<MkButton v-for="action in actions" :key="action.text" inline :primary="action.primary" :danger="action.danger" @click="() => { action.callback(); modal?.close(); }">{{ action.text }}</MkButton>
|
||||
<MkButton v-for="action in actions" :key="action.text" inline rounded :primary="action.primary" :danger="action.danger" @click="() => { action.callback(); modal?.close(); }">{{ action.text }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkModal>
|
||||
|
@@ -439,6 +439,7 @@ defineExpose({
|
||||
|
||||
&.asDrawer {
|
||||
width: 100% !important;
|
||||
padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0;
|
||||
|
||||
> .emojis {
|
||||
::v-deep(section) {
|
||||
|
@@ -11,15 +11,21 @@
|
||||
<div :class="$style.body" class="_shadow" @mousedown="onBodyMousedown" @keydown="onKeydown">
|
||||
<div :class="[$style.header, { [$style.mini]: mini }]" @contextmenu.prevent.stop="onContextmenu">
|
||||
<span :class="$style.headerLeft">
|
||||
<button v-for="button in buttonsLeft" v-tooltip="button.title" class="_button" :class="[$style.headerButton, { [$style.highlighted]: button.highlighted }]" @click="button.onClick"><i :class="button.icon"></i></button>
|
||||
<template v-if="!minimized">
|
||||
<button v-for="button in buttonsLeft" v-tooltip="button.title" class="_button" :class="[$style.headerButton, { [$style.highlighted]: button.highlighted }]" @click="button.onClick"><i :class="button.icon"></i></button>
|
||||
</template>
|
||||
</span>
|
||||
<span :class="$style.headerTitle" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown">
|
||||
<slot name="header"></slot>
|
||||
</span>
|
||||
<span :class="$style.headerRight">
|
||||
<button v-for="button in buttonsRight" v-tooltip="button.title" class="_button" :class="[$style.headerButton, { [$style.highlighted]: button.highlighted }]" @click="button.onClick"><i :class="button.icon"></i></button>
|
||||
<template v-if="!minimized">
|
||||
<button v-for="button in buttonsRight" v-tooltip="button.title" class="_button" :class="[$style.headerButton, { [$style.highlighted]: button.highlighted }]" @click="button.onClick"><i :class="button.icon"></i></button>
|
||||
</template>
|
||||
<button v-if="canResize && minimized" v-tooltip="i18n.ts.windowRestore" class="_button" :class="$style.headerButton" @click="unMinimize()"><i class="ti ti-maximize"></i></button>
|
||||
<button v-else-if="canResize && !maximized" v-tooltip="i18n.ts.windowMinimize" class="_button" :class="$style.headerButton" @click="minimize()"><i class="ti ti-minimize"></i></button>
|
||||
<button v-if="canResize && maximized" v-tooltip="i18n.ts.windowRestore" class="_button" :class="$style.headerButton" @click="unMaximize()"><i class="ti ti-picture-in-picture"></i></button>
|
||||
<button v-else-if="canResize && !maximized" v-tooltip="i18n.ts.windowMaximize" class="_button" :class="$style.headerButton" @click="maximize()"><i class="ti ti-rectangle"></i></button>
|
||||
<button v-else-if="canResize && !maximized && !minimized" v-tooltip="i18n.ts.windowMaximize" class="_button" :class="$style.headerButton" @click="maximize()"><i class="ti ti-rectangle"></i></button>
|
||||
<button v-if="closeButton" v-tooltip="i18n.ts.close" class="_button" :class="$style.headerButton" @click="close()"><i class="ti ti-x"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
@@ -27,7 +33,7 @@
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="canResize">
|
||||
<template v-if="canResize && !minimized">
|
||||
<div :class="$style.handleTop" @mousedown.prevent="onTopHandleMousedown"></div>
|
||||
<div :class="$style.handleRight" @mousedown.prevent="onRightHandleMousedown"></div>
|
||||
<div :class="$style.handleBottom" @mousedown.prevent="onBottomHandleMousedown"></div>
|
||||
@@ -100,10 +106,11 @@ let rootEl = $shallowRef<HTMLElement | null>();
|
||||
let showing = $ref(true);
|
||||
let beforeClickedAt = 0;
|
||||
let maximized = $ref(false);
|
||||
let unMaximizedTop = '';
|
||||
let unMaximizedLeft = '';
|
||||
let unMaximizedWidth = '';
|
||||
let unMaximizedHeight = '';
|
||||
let minimized = $ref(false);
|
||||
let unResizedTop = '';
|
||||
let unResizedLeft = '';
|
||||
let unResizedWidth = '';
|
||||
let unResizedHeight = '';
|
||||
|
||||
function close() {
|
||||
showing = false;
|
||||
@@ -132,10 +139,10 @@ function top() {
|
||||
|
||||
function maximize() {
|
||||
maximized = true;
|
||||
unMaximizedTop = rootEl.style.top;
|
||||
unMaximizedLeft = rootEl.style.left;
|
||||
unMaximizedWidth = rootEl.style.width;
|
||||
unMaximizedHeight = rootEl.style.height;
|
||||
unResizedTop = rootEl.style.top;
|
||||
unResizedLeft = rootEl.style.left;
|
||||
unResizedWidth = rootEl.style.width;
|
||||
unResizedHeight = rootEl.style.height;
|
||||
rootEl.style.top = '0';
|
||||
rootEl.style.left = '0';
|
||||
rootEl.style.width = '100%';
|
||||
@@ -144,10 +151,35 @@ function maximize() {
|
||||
|
||||
function unMaximize() {
|
||||
maximized = false;
|
||||
rootEl.style.top = unMaximizedTop;
|
||||
rootEl.style.left = unMaximizedLeft;
|
||||
rootEl.style.width = unMaximizedWidth;
|
||||
rootEl.style.height = unMaximizedHeight;
|
||||
rootEl.style.top = unResizedTop;
|
||||
rootEl.style.left = unResizedLeft;
|
||||
rootEl.style.width = unResizedWidth;
|
||||
rootEl.style.height = unResizedHeight;
|
||||
}
|
||||
|
||||
function minimize() {
|
||||
minimized = true;
|
||||
unResizedWidth = rootEl.style.width;
|
||||
unResizedHeight = rootEl.style.height;
|
||||
rootEl.style.width = minWidth + 'px';
|
||||
rootEl.style.height = props.mini ? '32px' : '39px';
|
||||
}
|
||||
|
||||
function unMinimize() {
|
||||
const main = rootEl;
|
||||
if (main == null) return;
|
||||
|
||||
minimized = false;
|
||||
rootEl.style.width = unResizedWidth;
|
||||
rootEl.style.height = unResizedHeight;
|
||||
const browserWidth = window.innerWidth;
|
||||
const browserHeight = window.innerHeight;
|
||||
const windowWidth = main.offsetWidth;
|
||||
const windowHeight = main.offsetHeight;
|
||||
|
||||
const position = main.getBoundingClientRect();
|
||||
if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px';
|
||||
if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px';
|
||||
}
|
||||
|
||||
function onBodyMousedown() {
|
||||
@@ -155,7 +187,11 @@ function onBodyMousedown() {
|
||||
}
|
||||
|
||||
function onDblClick() {
|
||||
maximize();
|
||||
if (minimized) {
|
||||
unMinimize();
|
||||
} else {
|
||||
maximize();
|
||||
}
|
||||
}
|
||||
|
||||
function onHeaderMousedown(evt: MouseEvent) {
|
||||
@@ -187,7 +223,7 @@ function onHeaderMousedown(evt: MouseEvent) {
|
||||
|
||||
const clickX = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientX : evt.clientX;
|
||||
const clickY = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientY : evt.clientY;
|
||||
const moveBaseX = beforeMaximized ? parseInt(unMaximizedWidth, 10) / 2 : clickX - position.left; // TODO: parseIntやめる
|
||||
const moveBaseX = beforeMaximized ? parseInt(unResizedWidth, 10) / 2 : clickX - position.left; // TODO: parseIntやめる
|
||||
const moveBaseY = beforeMaximized ? 20 : clickY - position.top;
|
||||
const browserWidth = window.innerWidth;
|
||||
const browserHeight = window.innerHeight;
|
||||
|
@@ -22,7 +22,7 @@ export const Default = {
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkA v-bind="props">Text</MkA>',
|
||||
template: '<MkA v-bind="props">Misskey</MkA>',
|
||||
};
|
||||
},
|
||||
async play({ canvasElement }) {
|
||||
|
@@ -4,12 +4,16 @@
|
||||
<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/>
|
||||
<div v-if="user.isCat" :class="[$style.ears, { [$style.mask]: useBlurEffect }]">
|
||||
<div :class="$style.earLeft">
|
||||
<div v-if="useBlurEffect" :class="$style.layer">
|
||||
<div v-if="false" :class="$style.layer">
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.earRight">
|
||||
<div v-if="useBlurEffect" :class="$style.layer">
|
||||
<div v-if="false" :class="$style.layer">
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -195,11 +199,21 @@ watch(() => props.user.avatarBlurhash, () => {
|
||||
|
||||
> .plot {
|
||||
contain: strict;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
clip-path: path('M0 0H1V1H0z');
|
||||
transform: scale(32767);
|
||||
transform-origin: 0 0;
|
||||
opacity: 0.5;
|
||||
|
||||
&:first-child {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
opacity: calc(1 / 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,6 +235,14 @@ watch(() => props.user.avatarBlurhash, () => {
|
||||
|
||||
> .plot {
|
||||
background-position: 20% 10%; /* ~= 37.5deg */
|
||||
|
||||
&:first-child {
|
||||
background-position-x: 21%;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
background-position-y: 11%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,7 +263,16 @@ watch(() => props.user.avatarBlurhash, () => {
|
||||
-38.5857864376%); /* 40 - 2 * sqrt(2) */
|
||||
|
||||
> .plot {
|
||||
position: absolute;
|
||||
background-position: 80% 10%; /* ~= 37.5deg */
|
||||
|
||||
&:first-child {
|
||||
background-position-x: 79%;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
background-position-y: 11%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { waitFor } from '@storybook/testing-library';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import MkPageHeader from './MkPageHeader.vue';
|
||||
export const Empty = {
|
||||
@@ -22,16 +23,16 @@ export const Empty = {
|
||||
template: '<MkPageHeader v-bind="props" />',
|
||||
};
|
||||
},
|
||||
async play() {
|
||||
const wait = new Promise((resolve) => setTimeout(resolve, 800));
|
||||
await waitFor(async () => await wait);
|
||||
},
|
||||
args: {
|
||||
static: true,
|
||||
tabs: [],
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
chromatic: {
|
||||
/* This component has animations that are implemented with JavaScript. So it's unstable to take a snapshot. */
|
||||
disableSnapshot: true,
|
||||
},
|
||||
},
|
||||
} satisfies StoryObj<typeof MkPageHeader>;
|
||||
export const OneTab = {
|
||||
|
@@ -13,8 +13,6 @@ export class UserPreview {
|
||||
this.el = el;
|
||||
this.user = user;
|
||||
|
||||
this.attach();
|
||||
|
||||
this.show = this.show.bind(this);
|
||||
this.close = this.close.bind(this);
|
||||
this.onMouseover = this.onMouseover.bind(this);
|
||||
@@ -22,6 +20,8 @@ export class UserPreview {
|
||||
this.onClick = this.onClick.bind(this);
|
||||
this.attach = this.attach.bind(this);
|
||||
this.detach = this.detach.bind(this);
|
||||
|
||||
this.attach();
|
||||
}
|
||||
|
||||
private show() {
|
||||
|
@@ -187,7 +187,7 @@ try {
|
||||
} catch (err) {}
|
||||
|
||||
const app = createApp(
|
||||
window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
|
||||
new URLSearchParams(window.location.search).has('zen') ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
|
||||
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
|
||||
ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
|
||||
ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
|
||||
|
@@ -217,6 +217,7 @@ const patrons = [
|
||||
'氷月氷華里',
|
||||
'Ebise Lutica',
|
||||
'巣黒るい@リスケモ男の娘VTuber!',
|
||||
'ふぇいぽむ',
|
||||
];
|
||||
|
||||
let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure'));
|
||||
|
@@ -113,16 +113,37 @@ function remove(ad) {
|
||||
|
||||
function save(ad) {
|
||||
if (ad.id == null) {
|
||||
os.apiWithDialog('admin/ad/create', {
|
||||
os.api('admin/ad/create', {
|
||||
...ad,
|
||||
expiresAt: new Date(ad.expiresAt).getTime(),
|
||||
startsAt: new Date(ad.startsAt).getTime(),
|
||||
}).then(() => {
|
||||
os.alert({
|
||||
type: 'success',
|
||||
text: i18n.ts.saved,
|
||||
});
|
||||
refresh();
|
||||
}).catch(err => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: err,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
os.apiWithDialog('admin/ad/update', {
|
||||
os.api('admin/ad/update', {
|
||||
...ad,
|
||||
expiresAt: new Date(ad.expiresAt).getTime(),
|
||||
startsAt: new Date(ad.startsAt).getTime(),
|
||||
}).then(() => {
|
||||
os.alert({
|
||||
type: 'success',
|
||||
text: i18n.ts.saved,
|
||||
});
|
||||
}).catch(err => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: err,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -141,6 +162,25 @@ function more() {
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
os.api('admin/ad/list').then(adsResponse => {
|
||||
ads = adsResponse.map(r => {
|
||||
const exdate = new Date(r.expiresAt);
|
||||
const stdate = new Date(r.startsAt);
|
||||
exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
|
||||
stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff);
|
||||
return {
|
||||
...r,
|
||||
expiresAt: exdate.toISOString().slice(0, 16),
|
||||
startsAt: stdate.toISOString().slice(0, 16),
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
refresh();
|
||||
|
||||
const headerActions = $computed(() => [{
|
||||
asFullButton: true,
|
||||
icon: 'ti ti-plus',
|
||||
|
@@ -69,6 +69,7 @@ function save(announcement) {
|
||||
type: 'success',
|
||||
text: i18n.ts.saved,
|
||||
});
|
||||
refresh();
|
||||
}).catch(err => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
@@ -90,6 +91,14 @@ function save(announcement) {
|
||||
}
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
os.api('admin/announcements/list').then(announcementResponse => {
|
||||
announcements = announcementResponse;
|
||||
});
|
||||
}
|
||||
|
||||
refresh();
|
||||
|
||||
const headerActions = $computed(() => [{
|
||||
asFullButton: true,
|
||||
icon: 'ti ti-plus',
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div v-if="channel" class="_gaps_m">
|
||||
<div v-if="channelId == null || channel != null" class="_gaps_m">
|
||||
<MkInput v-model="name">
|
||||
<template #label>{{ i18n.ts.name }}</template>
|
||||
</MkInput>
|
||||
|
@@ -47,6 +47,7 @@ const featuredPagination = {
|
||||
const favoritesPagination = {
|
||||
endpoint: 'channels/my-favorites' as const,
|
||||
limit: 100,
|
||||
noPaging: true,
|
||||
};
|
||||
const followingPagination = {
|
||||
endpoint: 'channels/followed' as const,
|
||||
|
@@ -88,7 +88,7 @@ const tagUsers = $computed(() => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const pinnedUsers = { endpoint: 'pinned-users' };
|
||||
const pinnedUsers = { endpoint: 'pinned-users', noPaging: true };
|
||||
const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
state: 'alive',
|
||||
origin: 'local',
|
||||
|
@@ -24,6 +24,7 @@ import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'antennas/list' as const,
|
||||
noPaging: true,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
|
@@ -32,6 +32,7 @@ import { clipsCache } from '@/cache';
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'clips/list' as const,
|
||||
noPaging: true,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
|
@@ -30,6 +30,7 @@ const pagingComponent = $shallowRef<InstanceType<typeof MkPagination>>();
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'users/lists/list' as const,
|
||||
noPaging: true,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
|
@@ -130,11 +130,6 @@ const menuDef = computed(() => [{
|
||||
}, {
|
||||
title: i18n.ts.otherSettings,
|
||||
items: [{
|
||||
icon: 'ti ti-package',
|
||||
text: i18n.ts.importAndExport,
|
||||
to: '/settings/import-export',
|
||||
active: currentPage?.route.name === 'import-export',
|
||||
}, {
|
||||
icon: 'ti ti-badges',
|
||||
text: i18n.ts.roles,
|
||||
to: '/settings/roles',
|
||||
@@ -165,6 +160,16 @@ const menuDef = computed(() => [{
|
||||
to: '/settings/webhook',
|
||||
active: currentPage?.route.name === 'webhook',
|
||||
}, {
|
||||
icon: 'ti ti-package',
|
||||
text: i18n.ts.importAndExport,
|
||||
to: '/settings/import-export',
|
||||
active: currentPage?.route.name === 'import-export',
|
||||
}, /*{
|
||||
icon: 'ti ti-plane',
|
||||
text: i18n.ts.accountMigration,
|
||||
to: '/settings/migration',
|
||||
active: currentPage?.route.name === 'migration',
|
||||
},*/ {
|
||||
icon: 'ti ti-dots',
|
||||
text: i18n.ts.other,
|
||||
to: '/settings/other',
|
||||
@@ -231,7 +236,7 @@ onUnmounted(() => {
|
||||
});
|
||||
|
||||
watch(router.currentRef, (to) => {
|
||||
if (to.route.name === "settings" && to.child?.route.name == null && !narrow) {
|
||||
if (to.route.name === 'settings' && to.child?.route.name == null && !narrow) {
|
||||
router.replace('/settings/profile');
|
||||
}
|
||||
});
|
||||
|
73
packages/frontend/src/pages/settings/migration.vue
Normal file
73
packages/frontend/src/pages/settings/migration.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="_gaps_m">
|
||||
<FormSection first>
|
||||
<template #label>{{ i18n.ts._accountMigration.moveTo }}</template>
|
||||
<MkInput v-model="moveToAccount" manual-save>
|
||||
<template #prefix><i class="ti ti-plane-departure"></i></template>
|
||||
<template #label>{{ i18n.ts._accountMigration.moveToLabel }}</template>
|
||||
</MkInput>
|
||||
</FormSection>
|
||||
<FormInfo warn>{{ i18n.ts._accountMigration.moveAccountDescription }}</FormInfo>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts._accountMigration.moveFrom }}</template>
|
||||
<MkInput v-model="accountAlias" manual-save>
|
||||
<template #prefix><i class="ti ti-plane-arrival"></i></template>
|
||||
<template #label>{{ i18n.ts._accountMigration.moveFromLabel }}</template>
|
||||
</MkInput>
|
||||
</FormSection>
|
||||
<FormInfo warn>{{ i18n.ts._accountMigration.moveFromDescription }}</FormInfo>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormInfo from '@/components/MkInfo.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const moveToAccount = ref('');
|
||||
const accountAlias = ref('');
|
||||
|
||||
async function move(): Promise<void> {
|
||||
const account = moveToAccount.value;
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('migrationConfirm', { account: account.toString() }),
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
os.apiWithDialog('i/move', {
|
||||
moveToAccount: account,
|
||||
});
|
||||
}
|
||||
|
||||
async function save(): Promise<void> {
|
||||
const account = accountAlias.value;
|
||||
os.apiWithDialog('i/known-as', {
|
||||
alsoKnownAs: account,
|
||||
});
|
||||
}
|
||||
|
||||
watch(accountAlias, async () => {
|
||||
await save();
|
||||
});
|
||||
|
||||
watch(moveToAccount, async () => {
|
||||
await move();
|
||||
});
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.accountMigration,
|
||||
icon: 'ti ti-plane',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.description {
|
||||
font-size: .85em;
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
@@ -7,6 +7,7 @@
|
||||
<!-- <div class="punished" v-if="user.isSilenced"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i> {{ i18n.ts.userSilenced }}</div> -->
|
||||
|
||||
<div class="profile _gaps">
|
||||
<MkAccountMoved v-if="user.movedToUri" :host="user.movedToUri.host" :acct="user.movedToUri.username"/>
|
||||
<MkRemoteCaution v-if="user.host != null" :href="user.url ?? user.uri!" class="warn"/>
|
||||
|
||||
<div :key="user.id" class="main _panel">
|
||||
@@ -117,6 +118,7 @@ import calcAge from 's-age';
|
||||
import * as misskey from 'misskey-js';
|
||||
import MkNote from '@/components/MkNote.vue';
|
||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||
import MkAccountMoved from '@/components/MkAccountMoved.vue';
|
||||
import MkRemoteCaution from '@/components/MkRemoteCaution.vue';
|
||||
import MkOmit from '@/components/MkOmit.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
|
@@ -161,6 +161,10 @@ export const routes = [{
|
||||
path: '/preferences-backups',
|
||||
name: 'preferences-backups',
|
||||
component: page(() => import('./pages/settings/preferences-backups.vue')),
|
||||
}, {
|
||||
path: '/migration',
|
||||
name: 'migration',
|
||||
component: page(() => import('./pages/settings/migration.vue'))
|
||||
}, {
|
||||
path: '/custom-css',
|
||||
name: 'general',
|
||||
|
@@ -127,6 +127,7 @@ hr {
|
||||
}
|
||||
|
||||
.ti {
|
||||
width: 1.28em;
|
||||
vertical-align: -12%;
|
||||
line-height: 1em;
|
||||
|
||||
|
Reference in New Issue
Block a user