Merge branch 'develop' into bh-worker
This commit is contained in:
commit
04ddada482
|
@ -39,6 +39,7 @@
|
|||
- Fix: フォローリクエストの通知が残る問題を修正
|
||||
|
||||
### Client
|
||||
- アカウント作成時に初期設定ウィザードを表示するように
|
||||
- チャンネル内検索ができるように
|
||||
- チャンネル検索ですべてのチャンネルの取得/表示ができるように
|
||||
- 通知の表示をカスタマイズできるように
|
||||
|
|
|
@ -1036,6 +1036,21 @@ channelArchiveConfirmTitle: "{name}をアーカイブしますか?"
|
|||
channelArchiveConfirmDescription: "アーカイブすると、チャンネル一覧や検索結果に表示されなくなり、新たな書き込みもできなくなります。"
|
||||
thisChannelArchived: "このチャンネルはアーカイブされています。"
|
||||
displayOfNote: "ノートの表示"
|
||||
initialAccountSetting: "初期設定"
|
||||
youFollowing: "フォロー中"
|
||||
|
||||
_initialAccountSetting:
|
||||
accountCreated: "アカウントの作成が完了しました!"
|
||||
letsFillYourProfile: "まずはあなたのプロフィールを設定しましょう。"
|
||||
profileSetting: "プロフィール設定"
|
||||
theseSettingsCanEditLater: "これらの設定は後から変更できます。"
|
||||
youCanEditMoreSettingsInSettingsPageLater: "この他にも様々な設定を「設定」ページから行えます。ぜひ後で確認してみてください。"
|
||||
followUsers: "タイムラインを構築するため、気になるユーザーをフォローしてみましょう。"
|
||||
pushNotificationDescription: "プッシュ通知を有効にすると{name}の通知をお使いのデバイスで受け取ることができます。"
|
||||
initialAccountSettingCompleted: "初期設定が完了しました!"
|
||||
haveFun: "{name}をお楽しみください!"
|
||||
ifYouNeedLearnMore: "{name}(Misskey)の使い方などを詳しく知るには{link}をご覧ください。"
|
||||
skipAreYouSure: "初期設定をスキップしますか?"
|
||||
|
||||
_serverRules:
|
||||
description: "新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。"
|
||||
|
@ -1615,32 +1630,16 @@ _time:
|
|||
hour: "時間"
|
||||
day: "日"
|
||||
|
||||
_tutorial:
|
||||
_timelineTutorial:
|
||||
title: "Misskeyの使い方"
|
||||
step1_1: "ようこそ。"
|
||||
step1_2: "この画面は「タイムライン」と呼ばれ、あなたや、あなたが「フォロー」する人の「ノート」が時系列で表示されます。"
|
||||
step1_3: "あなたはまだ何もノートを投稿しておらず、誰もフォローしていないので、タイムラインには何も表示されていないはずです。"
|
||||
step2_1: "ノートを作成したり誰かをフォローしたりする前に、まずあなたのプロフィールを完成させましょう。"
|
||||
step2_2: "あなたがどんな人かわかると、多くの人にノートを見てもらえたり、フォローしてもらいやすくなります。"
|
||||
step3_1: "プロフィール設定はうまくできましたか?"
|
||||
step3_2: "では試しに、何かノートを投稿してみてください。画面上にある鉛筆マークのボタンを押すとフォームが開きます。"
|
||||
step3_3: "内容を書いたら、フォーム右上のボタンを押すと投稿できます。"
|
||||
step3_4: "内容が思いつかない?「Misskey始めました」というのはいかがでしょう。"
|
||||
step4_1: "投稿できましたか?"
|
||||
step4_2: "あなたのノートがタイムラインに表示されていれば成功です。"
|
||||
step5_1: "次は、他の人をフォローしてタイムラインを賑やかにしたいところです。"
|
||||
step5_2: "{featured}で人気のノートが見れるので、その中から気になった人を選んでフォローしたり、{explore}で人気のユーザーを探すこともできます。"
|
||||
step5_3: "ユーザーをフォローするには、ユーザーのアイコンをクリックしてユーザーページを表示し、「フォロー」ボタンを押します。"
|
||||
step5_4: "ユーザーによっては、フォローが承認されるまで時間がかかる場合があります。"
|
||||
step6_1: "タイムラインに他のユーザーのノートが表示されていれば成功です。"
|
||||
step6_2: "他の人のノートには、「リアクション」を付けることができ、簡単にあなたの反応を伝えられます。"
|
||||
step6_3: "リアクションを付けるには、ノートの「+」マークをクリックして、好きなリアクションを選択します。"
|
||||
step7_1: "これで、Misskeyの基本的な使い方の説明は終わりました。お疲れ様でした。"
|
||||
step7_2: "もっとMisskeyについて知りたいときは、{help}を見てみてください。"
|
||||
step7_3: "では、Misskeyをお楽しみください🚀"
|
||||
step8_1: "最後に、プッシュ通知を有効化してみませんか?"
|
||||
step8_2: "プッシュ通知を受け取ることで、Misskeyを開いていない時にもリアクションやフォロー、メンションなどに気づけます。"
|
||||
step8_3: "通知の設定は後から変更できます。"
|
||||
step1_1: "この画面は「タイムライン」です。{name}に投稿された「ノート」が時系列で表示されます。"
|
||||
step1_2: "タイムラインにはいくつか種類があり、例えば「ホームタイムライン」にはあなたがフォローしている人のノートが流れ、「ローカルタイムライン」には{name}全体のノートが流れます。"
|
||||
step2_1: "試しに、何かノートを投稿してみましょう。画面上にある鉛筆マークのボタンを押すとフォームが開きます。"
|
||||
step2_2: "初めてのノートの内容は、あなたの自己紹介や「{name}始めました」などがおすすめです。"
|
||||
step3_1: "投稿できましたか?"
|
||||
step3_2: "あなたのノートがタイムラインに表示されていれば成功です。"
|
||||
step4_1: "ノートには、「リアクション」を付けることができます。"
|
||||
step4_2: "リアクションを付けるには、ノートの「+」マークをクリックして、好きな絵文字を選択します。"
|
||||
|
||||
_2fa:
|
||||
alreadyRegistered: "既に設定は完了しています。"
|
||||
|
@ -1822,7 +1821,7 @@ _profile:
|
|||
metadataDescription: "プロフィールに表として追加情報を表示することができます。"
|
||||
metadataLabel: "ラベル"
|
||||
metadataContent: "内容"
|
||||
changeAvatar: "アバター画像を変更"
|
||||
changeAvatar: "アイコン画像を変更"
|
||||
changeBanner: "バナー画像を変更"
|
||||
|
||||
_exportOrImport:
|
||||
|
|
|
@ -44,7 +44,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
private queryService: QueryService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.channelsRepository.createQueryBuilder(), ps.sinceId, ps.untilId)
|
||||
const query = this.queryService.makePaginationQuery(this.channelsRepository.createQueryBuilder('channel'), ps.sinceId, ps.untilId)
|
||||
.andWhere('channel.isArchived = FALSE')
|
||||
.andWhere({ userId: me.id });
|
||||
|
||||
|
|
|
@ -399,6 +399,8 @@ Promise.all([
|
|||
glob('src/components/Mk{A,B}*.vue'),
|
||||
glob('src/components/MkGalleryPostPreview.vue'),
|
||||
glob('src/components/MkSignupServerRules.vue'),
|
||||
glob('src/components/MkUserSetupDialog.vue'),
|
||||
glob('src/components/MkUserSetupDialog.*.vue'),
|
||||
glob('src/pages/user/home.vue'),
|
||||
])
|
||||
.then((globs) => globs.flat())
|
||||
|
|
|
@ -3,6 +3,7 @@ import { FORCE_REMOUNT } from '@storybook/core-events';
|
|||
import { type Preview, setup } from '@storybook/vue3';
|
||||
import isChromatic from 'chromatic/isChromatic';
|
||||
import { initialize, mswDecorator } from 'msw-storybook-addon';
|
||||
import { userDetailed } from './fakes';
|
||||
import locale from './locale';
|
||||
import { commonHandlers, onUnhandledRequest } from './mocks';
|
||||
import themes from './themes';
|
||||
|
@ -10,6 +11,7 @@ import '../src/style.scss';
|
|||
|
||||
const appInitialized = Symbol();
|
||||
|
||||
let lastStory = null;
|
||||
let moduleInitialized = false;
|
||||
let unobserve = () => {};
|
||||
let misskeyOS = null;
|
||||
|
@ -42,10 +44,16 @@ function loadTheme(applyTheme: typeof import('../src/scripts/theme')['applyTheme
|
|||
unobserve = () => observer.disconnect();
|
||||
}
|
||||
|
||||
function initLocalStorage() {
|
||||
localStorage.clear();
|
||||
localStorage.setItem('account', JSON.stringify(userDetailed()));
|
||||
localStorage.setItem('locale', JSON.stringify(locale));
|
||||
}
|
||||
|
||||
initialize({
|
||||
onUnhandledRequest,
|
||||
});
|
||||
localStorage.setItem("locale", JSON.stringify(locale));
|
||||
initLocalStorage();
|
||||
queueMicrotask(() => {
|
||||
Promise.all([
|
||||
import('../src/components'),
|
||||
|
@ -76,6 +84,27 @@ queueMicrotask(() => {
|
|||
const preview = {
|
||||
decorators: [
|
||||
(Story, context) => {
|
||||
if (lastStory === context.id) {
|
||||
lastStory = null;
|
||||
} else {
|
||||
lastStory = context.id;
|
||||
const channel = addons.getChannel();
|
||||
const resetIndexedDBPromise = globalThis.indexedDB?.databases
|
||||
? indexedDB.databases().then((r) => {
|
||||
for (var i = 0; i < r.length; i++) {
|
||||
indexedDB.deleteDatabase(r[i].name!);
|
||||
}
|
||||
}).catch(() => {})
|
||||
: Promise.resolve();
|
||||
const resetDefaultStorePromise = import('../src/store').then(({ defaultStore }) => {
|
||||
// @ts-expect-error
|
||||
defaultStore.init();
|
||||
}).catch(() => {});
|
||||
Promise.all([resetIndexedDBPromise, resetDefaultStorePromise]).then(() => {
|
||||
initLocalStorage();
|
||||
channel.emit(FORCE_REMOUNT, { storyId: context.id });
|
||||
});
|
||||
}
|
||||
const story = Story();
|
||||
if (!moduleInitialized) {
|
||||
const channel = addons.getChannel();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div>
|
||||
<div :class="$style.label" @click="focus"><slot name="label"></slot></div>
|
||||
<div :class="[$style.input, { inline, disabled, focused }]">
|
||||
<div :class="[$style.input, { [$style.inline]: inline, [$style.disabled]: disabled, [$style.focused]: focused }]">
|
||||
<div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div>
|
||||
<input
|
||||
ref="inputEl"
|
||||
|
|
|
@ -89,7 +89,6 @@ defineExpose({
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
contain: content;
|
||||
container-type: inline-size;
|
||||
border-radius: var(--radius);
|
||||
|
||||
--root-margin: 24px;
|
||||
|
@ -142,6 +141,7 @@ defineExpose({
|
|||
flex: 1;
|
||||
overflow: auto;
|
||||
background: var(--panel);
|
||||
container-type: size;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -87,7 +87,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
async openDrive() {
|
||||
os.selectDriveFile();
|
||||
os.selectDriveFile(false);
|
||||
},
|
||||
|
||||
async selectUser() {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { expect } from '@storybook/jest';
|
|||
import { userEvent, waitFor, within } from '@storybook/testing-library';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { onBeforeUnmount } from 'vue';
|
||||
import MkSignupServerRules from './MkSignupDialog,rules.vue';
|
||||
import MkSignupServerRules from './MkSignupDialog.rules.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { instance } from '@/instance';
|
||||
export const Empty = {
|
||||
|
|
|
@ -1,30 +1,30 @@
|
|||
<template>
|
||||
<div class="_panel vjnjpkug">
|
||||
<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div>
|
||||
<MkAvatar class="avatar" :user="user" indicator/>
|
||||
<div class="title">
|
||||
<MkA class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
|
||||
<p class="username"><MkAcct :user="user"/></p>
|
||||
<div class="_panel" :class="$style.root">
|
||||
<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div>
|
||||
<MkAvatar :class="$style.avatar" :user="user" indicator/>
|
||||
<div :class="$style.title">
|
||||
<MkA :class="$style.name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
|
||||
<p :class="$style.username"><MkAcct :user="user"/></p>
|
||||
</div>
|
||||
<span v-if="$i && $i.id !== user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
|
||||
<div class="description">
|
||||
<span v-if="$i && $i.id !== user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span>
|
||||
<div :class="$style.description">
|
||||
<div v-if="user.description" class="mfm">
|
||||
<Mfm :text="user.description" :author="user" :i="$i"/>
|
||||
</div>
|
||||
<span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span>
|
||||
</div>
|
||||
<div class="status">
|
||||
<div>
|
||||
<p>{{ i18n.ts.notes }}</p><span>{{ user.notesCount }}</span>
|
||||
<div :class="$style.status">
|
||||
<div :class="$style.statusItem">
|
||||
<p :class="$style.statusItemLabel">{{ i18n.ts.notes }}</p><span :class="$style.statusItemValue">{{ user.notesCount }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ i18n.ts.following }}</p><span>{{ user.followingCount }}</span>
|
||||
<div :class="$style.statusItem">
|
||||
<p :class="$style.statusItemLabel">{{ i18n.ts.following }}</p><span :class="$style.statusItemValue">{{ user.followingCount }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ i18n.ts.followers }}</p><span>{{ user.followersCount }}</span>
|
||||
<div :class="$style.statusItem">
|
||||
<p :class="$style.statusItemLabel">{{ i18n.ts.followers }}</p><span :class="$style.statusItemValue">{{ user.followersCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<MkFollowButton v-if="$i && user.id != $i.id" class="koudoku-button" :user="user" mini/>
|
||||
<MkFollowButton v-if="$i && user.id != $i.id" :class="$style.follow" :user="user" mini/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -40,18 +40,19 @@ defineProps<{
|
|||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vjnjpkug {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
> .banner {
|
||||
.banner {
|
||||
height: 84px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
> .avatar {
|
||||
.avatar {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 62px;
|
||||
|
@ -62,11 +63,12 @@ defineProps<{
|
|||
border: solid 4px var(--panel);
|
||||
}
|
||||
|
||||
> .title {
|
||||
.title {
|
||||
display: block;
|
||||
padding: 10px 0 10px 88px;
|
||||
}
|
||||
|
||||
> .name {
|
||||
.name {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
|
@ -74,7 +76,7 @@ defineProps<{
|
|||
word-break: break-all;
|
||||
}
|
||||
|
||||
> .username {
|
||||
.username {
|
||||
display: block;
|
||||
margin: 0;
|
||||
line-height: 16px;
|
||||
|
@ -82,9 +84,8 @@ defineProps<{
|
|||
color: var(--fg);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
> .followed {
|
||||
.followed {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
|
@ -95,44 +96,43 @@ defineProps<{
|
|||
border-radius: 6px;
|
||||
}
|
||||
|
||||
> .description {
|
||||
.description {
|
||||
padding: 16px;
|
||||
font-size: 0.8em;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
}
|
||||
|
||||
> .mfm {
|
||||
.mfm {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
> .status {
|
||||
.status {
|
||||
padding: 10px 16px;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
}
|
||||
|
||||
> div {
|
||||
.statusItem {
|
||||
display: inline-block;
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
> p {
|
||||
.statusItemLabel {
|
||||
margin: 0;
|
||||
font-size: 0.7em;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
> span {
|
||||
.statusItemValue {
|
||||
font-size: 1em;
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .koudoku-button {
|
||||
.follow {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</template>
|
||||
|
||||
<template #default="{ items }">
|
||||
<div class="efvhhmdq">
|
||||
<div :class="$style.root">
|
||||
<MkUserInfo v-for="item in items" :key="item.id" class="user" :user="extractor(item)"/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -29,8 +29,8 @@ const props = withDefaults(defineProps<{
|
|||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.efvhhmdq {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
grid-gap: var(--margin);
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { rest } from 'msw';
|
||||
import { commonHandlers } from '../../.storybook/mocks';
|
||||
import { userDetailed } from '../../.storybook/fakes';
|
||||
import MkUserSetupDialog_Follow from './MkUserSetupDialog.Follow.vue';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkUserSetupDialog_Follow,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkUserSetupDialog_Follow v-bind="props" />',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
msw: {
|
||||
handlers: [
|
||||
...commonHandlers,
|
||||
rest.post('/api/users', (req, res, ctx) => {
|
||||
return res(ctx.json([
|
||||
userDetailed('44'),
|
||||
userDetailed('49'),
|
||||
]));
|
||||
}),
|
||||
rest.post('/api/pinned-users', (req, res, ctx) => {
|
||||
return res(ctx.json([
|
||||
userDetailed('44'),
|
||||
userDetailed('49'),
|
||||
]));
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
} satisfies StoryObj<typeof MkUserSetupDialog_Follow>;
|
|
@ -0,0 +1,63 @@
|
|||
<template>
|
||||
<div class="_gaps">
|
||||
<div style="text-align: center;">{{ i18n.ts._initialAccountSetting.followUsers }}</div>
|
||||
|
||||
<MkFolder :default-open="true">
|
||||
<template #label>{{ i18n.ts.recommended }}</template>
|
||||
|
||||
<MkPagination :pagination="pinnedUsers">
|
||||
<template #default="{ items }">
|
||||
<div :class="$style.users">
|
||||
<XUser v-for="item in items" :key="item.id" :user="item"/>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder :default-open="true">
|
||||
<template #label>{{ i18n.ts.popularUsers }}</template>
|
||||
|
||||
<MkPagination :pagination="popularUsers">
|
||||
<template #default="{ items }">
|
||||
<div :class="$style.users">
|
||||
<XUser v-for="item in items" :key="item.id" :user="item"/>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { instance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import XUser from '@/components/MkUserSetupDialog.User.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import * as os from '@/os';
|
||||
import { $i } from '@/account';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done'): void;
|
||||
}>();
|
||||
|
||||
const pinnedUsers = { endpoint: 'pinned-users', noPaging: true };
|
||||
|
||||
const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
state: 'alive',
|
||||
origin: 'local',
|
||||
sort: '+follower',
|
||||
} };
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.users {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
|
||||
grid-gap: var(--margin);
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,31 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import MkUserSetupDialog_Profile from './MkUserSetupDialog.Profile.vue';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkUserSetupDialog_Profile,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkUserSetupDialog_Profile v-bind="props" />',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies StoryObj<typeof MkUserSetupDialog_Profile>;
|
101
packages/frontend/src/components/MkUserSetupDialog.Profile.vue
Normal file
101
packages/frontend/src/components/MkUserSetupDialog.Profile.vue
Normal file
|
@ -0,0 +1,101 @@
|
|||
<template>
|
||||
<div class="_gaps">
|
||||
<MkInfo>{{ i18n.ts._initialAccountSetting.theseSettingsCanEditLater }}</MkInfo>
|
||||
|
||||
<FormSlot>
|
||||
<template #label>{{ i18n.ts.avatar }}</template>
|
||||
<div v-adaptive-bg :class="$style.avatarSection" class="_panel">
|
||||
<MkAvatar :class="$style.avatar" :user="$i" @click="setAvatar"/>
|
||||
<div style="margin-top: 16px;">
|
||||
<MkButton primary rounded inline @click="setAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</FormSlot>
|
||||
|
||||
<MkInput v-model="name" :max="30" manual-save>
|
||||
<template #label>{{ i18n.ts._profile.name }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkTextarea v-model="description" :max="500" tall manual-save>
|
||||
<template #label>{{ i18n.ts._profile.description }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<MkInfo>{{ i18n.ts._initialAccountSetting.youCanEditMoreSettingsInSettingsPageLater }}</MkInfo>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { instance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import FormSlot from '@/components/form/slot.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { chooseFileFromPc } from '@/scripts/select-file';
|
||||
import * as os from '@/os';
|
||||
import { $i } from '@/account';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done'): void;
|
||||
}>();
|
||||
|
||||
const name = ref('');
|
||||
const description = ref('');
|
||||
|
||||
watch(name, () => {
|
||||
os.apiWithDialog('i/update', {
|
||||
// 空文字列をnullにしたいので??は使うな
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
name: name.value || null,
|
||||
});
|
||||
});
|
||||
|
||||
watch(description, () => {
|
||||
os.apiWithDialog('i/update', {
|
||||
// 空文字列をnullにしたいので??は使うな
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
description: description.value || null,
|
||||
});
|
||||
});
|
||||
|
||||
function setAvatar(ev) {
|
||||
chooseFileFromPc(false).then(async (files) => {
|
||||
const file = files[0];
|
||||
|
||||
let originalOrCropped = file;
|
||||
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.t('cropImageAsk'),
|
||||
okText: i18n.ts.cropYes,
|
||||
cancelText: i18n.ts.cropNo,
|
||||
});
|
||||
|
||||
if (!canceled) {
|
||||
originalOrCropped = await os.cropImage(file, {
|
||||
aspectRatio: 1,
|
||||
});
|
||||
}
|
||||
|
||||
const i = await os.apiWithDialog('i/update', {
|
||||
avatarId: originalOrCropped.id,
|
||||
});
|
||||
$i.avatarId = i.avatarId;
|
||||
$i.avatarUrl = i.avatarUrl;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.avatarSection {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,32 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { userDetailed } from '../../.storybook/fakes';
|
||||
import MkUserSetupDialog_User from './MkUserSetupDialog.User.vue';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkUserSetupDialog_User,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkUserSetupDialog_User v-bind="props" />',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
user: userDetailed(),
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies StoryObj<typeof MkUserSetupDialog_User>;
|
101
packages/frontend/src/components/MkUserSetupDialog.User.vue
Normal file
101
packages/frontend/src/components/MkUserSetupDialog.User.vue
Normal file
|
@ -0,0 +1,101 @@
|
|||
<template>
|
||||
<div v-adaptive-bg class="_panel" style="position: relative;">
|
||||
<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div>
|
||||
<MkAvatar :class="$style.avatar" :user="user" indicator/>
|
||||
<div :class="$style.title">
|
||||
<div :class="$style.name"><MkUserName :user="user" :nowrap="false"/></div>
|
||||
<p :class="$style.username"><MkAcct :user="user"/></p>
|
||||
</div>
|
||||
<div :class="$style.description">
|
||||
<div v-if="user.description" :class="$style.mfm">
|
||||
<Mfm :text="user.description" :author="user" :i="$i"/>
|
||||
</div>
|
||||
<span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span>
|
||||
</div>
|
||||
<div :class="$style.footer">
|
||||
<MkButton v-if="!isFollowing" primary gradate rounded full @click="follow"><i class="ti ti-plus"></i> {{ i18n.ts.follow }}</MkButton>
|
||||
<div v-else style="opacity: 0.7; text-align: center;">{{ i18n.ts.youFollowing }} <i class="ti ti-check"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as misskey from 'misskey-js';
|
||||
import { ref } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { $i } from '@/account';
|
||||
import * as os from '@/os';
|
||||
|
||||
const props = defineProps<{
|
||||
user: misskey.entities.UserDetailed;
|
||||
}>();
|
||||
|
||||
const isFollowing = ref(false);
|
||||
|
||||
async function follow() {
|
||||
isFollowing.value = true;
|
||||
os.api('following/create', {
|
||||
userId: props.user.id,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.banner {
|
||||
height: 60px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
left: 13px;
|
||||
z-index: 2;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
border: solid 4px var(--panel);
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
padding: 10px 0 10px 88px;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
line-height: 16px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.username {
|
||||
display: block;
|
||||
margin: 0;
|
||||
line-height: 16px;
|
||||
font-size: 0.8em;
|
||||
color: var(--fg);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.description {
|
||||
padding: 0 16px 16px 88px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.mfm {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 5;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.footer {
|
||||
border-top: solid 0.5px var(--divider);
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,51 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { rest } from 'msw';
|
||||
import { commonHandlers } from '../../.storybook/mocks';
|
||||
import { userDetailed } from '../../.storybook/fakes';
|
||||
import MkUserSetupDialog from './MkUserSetupDialog.vue';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkUserSetupDialog,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkUserSetupDialog v-bind="props" />',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
msw: {
|
||||
handlers: [
|
||||
...commonHandlers,
|
||||
rest.post('/api/users', (req, res, ctx) => {
|
||||
return res(ctx.json([
|
||||
userDetailed('44'),
|
||||
userDetailed('49'),
|
||||
]));
|
||||
}),
|
||||
rest.post('/api/pinned-users', (req, res, ctx) => {
|
||||
return res(ctx.json([
|
||||
userDetailed('44'),
|
||||
userDetailed('49'),
|
||||
]));
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
} satisfies StoryObj<typeof MkUserSetupDialog>;
|
144
packages/frontend/src/components/MkUserSetupDialog.vue
Normal file
144
packages/frontend/src/components/MkUserSetupDialog.vue
Normal file
|
@ -0,0 +1,144 @@
|
|||
<template>
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="500"
|
||||
:height="550"
|
||||
@close="close(true)"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>{{ i18n.ts.initialAccountSetting }}</template>
|
||||
|
||||
<div style="overflow-x: clip;">
|
||||
<Transition
|
||||
mode="out-in"
|
||||
:enter-active-class="$style.transition_x_enterActive"
|
||||
:leave-active-class="$style.transition_x_leaveActive"
|
||||
:enter-from-class="$style.transition_x_enterFrom"
|
||||
:leave-to-class="$style.transition_x_leaveTo"
|
||||
>
|
||||
<template v-if="page === 0">
|
||||
<div :class="$style.centerPage">
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<div class="_gaps" style="text-align: center;">
|
||||
<i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
|
||||
<div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.accountCreated }}</div>
|
||||
<div>{{ i18n.ts._initialAccountSetting.letsFillYourProfile }}</div>
|
||||
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" @click="page++">{{ i18n.ts._initialAccountSetting.profileSetting }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 1">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<XProfile/>
|
||||
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 2">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<XFollow/>
|
||||
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 3">
|
||||
<div :class="$style.centerPage">
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<div class="_gaps" style="text-align: center;">
|
||||
<i class="ti ti-bell-ringing-2" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
|
||||
<div style="font-size: 120%;">{{ i18n.ts.pushNotification }}</div>
|
||||
<div style="padding: 0 16px;">{{ i18n.t('_initialAccountSetting.pushNotificationDescription', { name: instance.name ?? host }) }}</div>
|
||||
<MkPushNotificationAllowButton primary show-only-to-register style="margin: 0 auto;"/>
|
||||
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 4">
|
||||
<div :class="$style.centerPage">
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<div class="_gaps" style="text-align: center;">
|
||||
<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
|
||||
<div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.initialAccountSettingCompleted }}</div>
|
||||
<I18n :src="i18n.ts._initialAccountSetting.ifYouNeedLearnMore" tag="div" style="padding: 0 16px;">
|
||||
<template #name>{{ instance.name ?? host }}</template>
|
||||
<template #link>
|
||||
<a href="https://misskey-hub.net/help.html" target="_blank" class="_link">{{ i18n.ts.help }}</a>
|
||||
</template>
|
||||
</I18n>
|
||||
<div>{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}</div>
|
||||
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" @click="close(false)">{{ i18n.ts.close }}</MkButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
</Transition>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef, watch } from 'vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import XProfile from '@/components/MkUserSetupDialog.Profile.vue';
|
||||
import XFollow from '@/components/MkUserSetupDialog.Follow.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { instance } from '@/instance';
|
||||
import { host } from '@/config';
|
||||
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
|
||||
import { defaultStore } from '@/store';
|
||||
import * as os from '@/os';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
|
||||
const page = ref(defaultStore.state.accountSetupWizard);
|
||||
|
||||
watch(page, () => {
|
||||
defaultStore.set('accountSetupWizard', page.value);
|
||||
});
|
||||
|
||||
async function close(skip: boolean) {
|
||||
if (skip) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts._initialAccountSetting.skipAreYouSure,
|
||||
});
|
||||
if (canceled) return;
|
||||
}
|
||||
|
||||
dialog.value.close();
|
||||
defaultStore.set('accountSetupWizard', -1);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.transition_x_enterActive,
|
||||
.transition_x_leaveActive {
|
||||
transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1);
|
||||
}
|
||||
.transition_x_enterFrom {
|
||||
opacity: 0;
|
||||
transform: translateX(50px);
|
||||
}
|
||||
.transition_x_leaveTo {
|
||||
opacity: 0;
|
||||
transform: translateX(-50px);
|
||||
}
|
||||
|
||||
.centerPage {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100cqh;
|
||||
padding-bottom: 30px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
|
@ -222,7 +222,7 @@ watch(() => props.user.avatarBlurhash, () => {
|
|||
transform: rotate(37.5deg) skew(30deg);
|
||||
|
||||
&, &::after {
|
||||
border-radius: 0 75% 75%;
|
||||
border-radius: 25% 75% 75%;
|
||||
}
|
||||
|
||||
> .layer {
|
||||
|
@ -251,7 +251,7 @@ watch(() => props.user.avatarBlurhash, () => {
|
|||
transform: rotate(-37.5deg) skew(-30deg);
|
||||
|
||||
&, &::after {
|
||||
border-radius: 75% 0 75% 75%;
|
||||
border-radius: 75% 25% 75% 75%;
|
||||
}
|
||||
|
||||
> .layer {
|
||||
|
|
|
@ -343,6 +343,16 @@ if ($i) {
|
|||
// only add post shortcuts if logged in
|
||||
hotkeys['p|n'] = post;
|
||||
|
||||
if (defaultStore.state.accountSetupWizard !== -1) {
|
||||
// このウィザードが実装される前に登録したユーザーには表示させないため
|
||||
// TODO: そのうち消す
|
||||
if (Date.now() - new Date($i.createdAt).getTime() < 1000 * 60 * 60 * 24) {
|
||||
popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {}, 'closed');
|
||||
} else {
|
||||
defaultStore.set('accountSetupWizard', -1);
|
||||
}
|
||||
}
|
||||
|
||||
if ($i.isDeleted) {
|
||||
alert({
|
||||
type: 'warning',
|
||||
|
|
|
@ -19,6 +19,7 @@ import MkContextMenu from '@/components/MkContextMenu.vue';
|
|||
import { MenuItem } from '@/types/menu';
|
||||
import copyToClipboard from './scripts/copy-to-clipboard';
|
||||
import { showMovedDialog } from './scripts/show-moved-dialog';
|
||||
import { DriveFile } from 'misskey-js/built/entities';
|
||||
|
||||
export const openingWindowsCount = ref(0);
|
||||
|
||||
|
@ -420,7 +421,7 @@ export async function selectUser(opts: { includeSelf?: boolean } = {}) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function selectDriveFile(multiple: boolean) {
|
||||
export async function selectDriveFile(multiple: boolean): Promise<DriveFile[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
|
||||
type: 'file',
|
||||
|
@ -428,7 +429,7 @@ export async function selectDriveFile(multiple: boolean) {
|
|||
}, {
|
||||
done: files => {
|
||||
if (files) {
|
||||
resolve(multiple ? files : files[0]);
|
||||
resolve(files);
|
||||
}
|
||||
},
|
||||
}, 'closed');
|
||||
|
|
|
@ -33,8 +33,8 @@ const emit = defineEmits<{
|
|||
let file: any = $ref(null);
|
||||
|
||||
async function choose() {
|
||||
os.selectDriveFile(false).then((fileResponse: any) => {
|
||||
file = fileResponse;
|
||||
os.selectDriveFile(false).then((fileResponse) => {
|
||||
file = fileResponse[0];
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
fileId: fileResponse.id,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div :class="$style.container">
|
||||
<div :class="$style.title">
|
||||
<div :class="$style.titleText"><i class="ti ti-info-circle"></i> {{ i18n.ts._tutorial.title }}</div>
|
||||
<div :class="$style.titleText"><i class="ti ti-info-circle"></i> {{ i18n.ts._timelineTutorial.title }}</div>
|
||||
<div :class="$style.step">
|
||||
<button class="_button" :class="$style.stepArrow" :disabled="tutorial === 0" @click="tutorial--">
|
||||
<i class="ti ti-chevron-left"></i>
|
||||
|
@ -12,66 +12,30 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="tutorial === 0" :class="$style.body">
|
||||
<div>{{ i18n.ts._tutorial.step1_1 }}</div>
|
||||
<div>{{ i18n.ts._tutorial.step1_2 }}</div>
|
||||
<div>{{ i18n.ts._tutorial.step1_3 }}</div>
|
||||
<div>{{ i18n.t('_timelineTutorial.step1_1', { name: instance.name ?? host }) }}</div>
|
||||
<div>{{ i18n.t('_timelineTutorial.step1_2', { name: instance.name ?? host }) }}</div>
|
||||
</div>
|
||||
<div v-else-if="tutorial === 1" :class="$style.body">
|
||||
<div>{{ i18n.ts._tutorial.step2_1 }}</div>
|
||||
<div>{{ i18n.ts._tutorial.step2_2 }}</div>
|
||||
<MkA class="_link" to="/settings/profile">{{ i18n.ts.editProfile }}</MkA>
|
||||
<div>{{ i18n.ts._timelineTutorial.step2_1 }}</div>
|
||||
<div>{{ i18n.t('_timelineTutorial.step2_2', { name: instance.name ?? host }) }}</div>
|
||||
</div>
|
||||
<div v-else-if="tutorial === 2" :class="$style.body">
|
||||
<div>{{ i18n.ts._tutorial.step3_1 }}</div>
|
||||
<div>{{ i18n.ts._tutorial.step3_2 }}</div>
|
||||
<div>{{ i18n.ts._tutorial.step3_3 }}</div>
|
||||
<small :class="$style.small">{{ i18n.ts._tutorial.step3_4 }}</small>
|
||||
<div>{{ i18n.ts._timelineTutorial.step3_1 }}</div>
|
||||
<div>{{ i18n.ts._timelineTutorial.step3_2 }}</div>
|
||||
</div>
|
||||
<div v-else-if="tutorial === 3" :class="$style.body">
|
||||
<div>{{ i18n.ts._tutorial.step4_1 }}</div>
|
||||
<div>{{ i18n.ts._tutorial.step4_2 }}</div>
|
||||
</div>
|
||||
<div v-else-if="tutorial === 4" :class="$style.body">
|
||||
<div>{{ i18n.ts._tutorial.step5_1 }}</div>
|
||||
<I18n :src="i18n.ts._tutorial.step5_2" tag="div">
|
||||
<template #featured>
|
||||
<MkA class="_link" to="/explore">{{ i18n.ts.featured }}</MkA>
|
||||
</template>
|
||||
<template #explore>
|
||||
<MkA class="_link" to="/explore#users">{{ i18n.ts.explore }}</MkA>
|
||||
</template>
|
||||
</I18n>
|
||||
<div>{{ i18n.ts._tutorial.step5_3 }}</div>
|
||||
<small :class="$style.small">{{ i18n.ts._tutorial.step5_4 }}</small>
|
||||
</div>
|
||||
<div v-else-if="tutorial === 5" :class="$style.body">
|
||||
<div>{{ i18n.ts._tutorial.step6_1 }}</div>
|
||||
<div>{{ i18n.ts._tutorial.step6_2 }}</div>
|
||||
<div>{{ i18n.ts._tutorial.step6_3 }}</div>
|
||||
</div>
|
||||
<div v-else-if="tutorial === 6" :class="$style.body">
|
||||
<div>{{ i18n.ts._tutorial.step7_1 }}</div>
|
||||
<I18n :src="i18n.ts._tutorial.step7_2" tag="div">
|
||||
<template #help>
|
||||
<a href="https://misskey-hub.net/help.html" target="_blank" class="_link">{{ i18n.ts.help }}</a>
|
||||
</template>
|
||||
</I18n>
|
||||
<div>{{ i18n.ts._tutorial.step7_3 }}</div>
|
||||
</div>
|
||||
<div v-else-if="tutorial === 7" :class="$style.body">
|
||||
<div>{{ i18n.ts._tutorial.step8_1 }}</div>
|
||||
<div>{{ i18n.ts._tutorial.step8_2 }}</div>
|
||||
<small :class="$style.small">{{ i18n.ts._tutorial.step8_3 }}</small>
|
||||
<div>{{ i18n.ts._timelineTutorial.step4_1 }}</div>
|
||||
<div>{{ i18n.ts._timelineTutorial.step4_2 }}</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.footer">
|
||||
<template v-if="tutorial === tutorialsNumber - 1">
|
||||
<MkPushNotificationAllowButton :class="$style.footerItem" primary show-only-to-register @click="tutorial = -1"/>
|
||||
<MkButton :class="$style.footerItem" :primary="false" @click="tutorial = -1">{{ i18n.ts.noThankYou }}</MkButton>
|
||||
<MkButton :class="$style.footerItem" primary rounded gradate @click="tutorial = -1">{{ i18n.ts.done }} <i class="ti ti-check"></i></MkButton>
|
||||
</template>
|
||||
<template v-else>
|
||||
<MkButton :class="$style.footerItem" primary @click="tutorial++"><i class="ti ti-check"></i> {{ i18n.ts.next }}</MkButton>
|
||||
<MkButton :class="$style.footerItem" primary rounded gradate @click="tutorial++">{{ i18n.ts.next }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -80,15 +44,16 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
|
||||
import { defaultStore } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
import { instance } from '@/instance';
|
||||
import { host } from '@/config';
|
||||
|
||||
const tutorialsNumber = 8;
|
||||
const tutorialsNumber = 4;
|
||||
|
||||
const tutorial = computed({
|
||||
get() { return defaultStore.reactiveState.tutorial.value || 0; },
|
||||
set(value) { defaultStore.set('tutorial', value); },
|
||||
get() { return defaultStore.reactiveState.timelineTutorial.value || 0; },
|
||||
set(value) { defaultStore.set('timelineTutorial', value); },
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :display-my-avatar="true"/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<div ref="rootEl" v-hotkey.global="keymap">
|
||||
<XTutorial v-if="$i && defaultStore.reactiveState.tutorial.value != -1" class="_panel" style="margin-bottom: var(--margin);"/>
|
||||
<XTutorial v-if="$i && defaultStore.reactiveState.timelineTutorial.value != -1" class="_panel" style="margin-bottom: var(--margin);"/>
|
||||
<MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/>
|
||||
|
||||
<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
|
||||
|
|
|
@ -6,19 +6,16 @@ import { i18n } from '@/i18n';
|
|||
import { defaultStore } from '@/store';
|
||||
import { uploadFile } from '@/scripts/upload';
|
||||
|
||||
function select(src: any, label: string | null, multiple: boolean): Promise<DriveFile | DriveFile[]> {
|
||||
export function chooseFileFromPc(multiple: boolean, keepOriginal = false): Promise<DriveFile[]> {
|
||||
return new Promise((res, rej) => {
|
||||
const keepOriginal = ref(defaultStore.state.keepOriginalUploading);
|
||||
|
||||
const chooseFileFromPc = () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.multiple = multiple;
|
||||
input.onchange = () => {
|
||||
const promises = Array.from(input.files).map(file => uploadFile(file, defaultStore.state.uploadFolder, undefined, keepOriginal.value));
|
||||
const promises = Array.from(input.files).map(file => uploadFile(file, defaultStore.state.uploadFolder, undefined, keepOriginal));
|
||||
|
||||
Promise.all(promises).then(driveFiles => {
|
||||
res(multiple ? driveFiles : driveFiles[0]);
|
||||
res(driveFiles);
|
||||
}).catch(err => {
|
||||
// アップロードのエラーは uploadFile 内でハンドリングされているためアラートダイアログを出したりはしてはいけない
|
||||
});
|
||||
|
@ -32,15 +29,19 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv
|
|||
(window as any).__misskey_input_ref__ = input;
|
||||
|
||||
input.click();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const chooseFileFromDrive = () => {
|
||||
export function chooseFileFromDrive(multiple: boolean): Promise<DriveFile[]> {
|
||||
return new Promise((res, rej) => {
|
||||
os.selectDriveFile(multiple).then(files => {
|
||||
res(files);
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const chooseFileFromUrl = () => {
|
||||
export function chooseFileFromUrl(): Promise<DriveFile> {
|
||||
return new Promise((res, rej) => {
|
||||
os.inputText({
|
||||
title: i18n.ts.uploadFromUrl,
|
||||
type: 'url',
|
||||
|
@ -53,7 +54,7 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv
|
|||
const connection = stream.useChannel('main');
|
||||
connection.on('urlUploadFinished', urlResponse => {
|
||||
if (urlResponse.marker === marker) {
|
||||
res(multiple ? [urlResponse.file] : urlResponse.file);
|
||||
res(urlResponse.file);
|
||||
connection.dispose();
|
||||
}
|
||||
});
|
||||
|
@ -69,7 +70,12 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv
|
|||
text: i18n.ts.uploadFromUrlMayTakeTime,
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function select(src: any, label: string | null, multiple: boolean): Promise<DriveFile[]> {
|
||||
return new Promise((res, rej) => {
|
||||
const keepOriginal = ref(defaultStore.state.keepOriginalUploading);
|
||||
|
||||
os.popupMenu([label ? {
|
||||
text: label,
|
||||
|
@ -81,23 +87,23 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv
|
|||
}, {
|
||||
text: i18n.ts.upload,
|
||||
icon: 'ti ti-upload',
|
||||
action: chooseFileFromPc,
|
||||
action: () => chooseFileFromPc(multiple, keepOriginal.value).then(files => res(files)),
|
||||
}, {
|
||||
text: i18n.ts.fromDrive,
|
||||
icon: 'ti ti-cloud',
|
||||
action: chooseFileFromDrive,
|
||||
action: () => chooseFileFromDrive(multiple).then(files => res(files)),
|
||||
}, {
|
||||
text: i18n.ts.fromUrl,
|
||||
icon: 'ti ti-link',
|
||||
action: chooseFileFromUrl,
|
||||
action: () => chooseFileFromUrl().then(file => res([file])),
|
||||
}], src);
|
||||
});
|
||||
}
|
||||
|
||||
export function selectFile(src: any, label: string | null = null): Promise<DriveFile> {
|
||||
return select(src, label, false) as Promise<DriveFile>;
|
||||
return select(src, label, false).then(files => files[0]);
|
||||
}
|
||||
|
||||
export function selectFiles(src: any, label: string | null = null): Promise<DriveFile[]> {
|
||||
return select(src, label, true) as Promise<DriveFile[]>;
|
||||
return select(src, label, true);
|
||||
}
|
||||
|
|
|
@ -38,7 +38,11 @@ export const pageViewInterruptors: PageViewInterruptor[] = [];
|
|||
// TODO: それぞれいちいちwhereとかdefaultというキーを付けなきゃいけないの冗長なのでなんとかする(ただ型定義が面倒になりそう)
|
||||
// あと、現行の定義の仕方なら「whereが何であるかに関わらずキー名の重複不可」という制約を付けられるメリットもあるからそのメリットを引き継ぐ方法も考えないといけない
|
||||
export const defaultStore = markRaw(new Storage('base', {
|
||||
tutorial: {
|
||||
accountSetupWizard: {
|
||||
where: 'account',
|
||||
default: 0,
|
||||
},
|
||||
timelineTutorial: {
|
||||
where: 'account',
|
||||
default: 0,
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue